001    package ezvcard.util;
002    
003    import java.util.ArrayList;
004    import java.util.List;
005    import java.util.Set;
006    
007    import org.jsoup.nodes.Element;
008    import org.jsoup.nodes.Node;
009    import org.jsoup.nodes.TextNode;
010    import org.jsoup.select.Elements;
011    
012    /*
013     Copyright (c) 2013, Michael Angstadt
014     All rights reserved.
015    
016     Redistribution and use in source and binary forms, with or without
017     modification, are permitted provided that the following conditions are met: 
018    
019     1. Redistributions of source code must retain the above copyright notice, this
020     list of conditions and the following disclaimer. 
021     2. Redistributions in binary form must reproduce the above copyright notice,
022     this list of conditions and the following disclaimer in the documentation
023     and/or other materials provided with the distribution. 
024    
025     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
026     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
027     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
028     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
029     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
030     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
031     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
032     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
033     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
034     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
035    
036     The views and conclusions contained in the software and documentation are those
037     of the authors and should not be interpreted as representing official policies, 
038     either expressed or implied, of the FreeBSD Project.
039     */
040    
041    /**
042     * Wraps hCard functionality around an HTML {@link Element} object.
043     * @author Michael Angstadt
044     */
045    public class HCardElement {
046            private final Element element;
047    
048            /**
049             * @param element the HTML element to wrap
050             */
051            public HCardElement(Element element) {
052                    this.element = element;
053            }
054    
055            /**
056             * Gets the name of the HTML element.
057             * @return the tag name
058             */
059            public String tagName() {
060                    return element.tagName();
061            }
062    
063            /**
064             * Gets an attribute value.
065             * @param name the attribute name
066             * @return the attribute value or empty string if it doesn't exist
067             */
068            public String attr(String name) {
069                    return element.attr(name);
070            }
071    
072            /**
073             * Gets the absolute URL of an attribute that has a URL.
074             * @param name the attribute name
075             * @return the absolute URL or empty string if it doesn't exist
076             */
077            public String absUrl(String name) {
078                    String url = element.absUrl(name); //returns empty string for some protocols like "tel:" and "data:", but not for "http:" or "mailto:"
079                    if (url.length() == 0) {
080                            url = element.attr(name);
081                    }
082                    return url;
083            }
084    
085            /**
086             * Gets the element's CSS classes.
087             * @return the CSS classes
088             */
089            public Set<String> classNames() {
090                    return element.classNames();
091            }
092    
093            /**
094             * Gets the hCard value of this element. The value is determined based on
095             * the following:
096             * <ol>
097             * <li>If the element is <code>&lt;abbr&gt;</code> and contains a
098             * <code>title</code> attribute, then the value of the <code>title</code>
099             * attribute is returned.</li>
100             * <li>Else, if the element contains one or more child elements that have a
101             * CSS class of <code>value</code>, then append together the text contents
102             * of these elements.</li>
103             * <li>Else, use the text content of the element itself.</li>
104             * </ol>
105             * All <code>&lt;br&gt;</code> tags are converted to newlines. All text
106             * within <code>&lt;del&gt;</code> tags are ignored.
107             * @return the element's hCard value
108             */
109            public String value() {
110                    return value(element);
111            }
112    
113            /**
114             * Gets the hCard value of the first descendant element that has the given
115             * CSS class name.
116             * @param cssClass the CSS class name
117             * @return the hCard value or null if not found
118             */
119            public String firstValue(String cssClass) {
120                    Elements elements = element.getElementsByClass(cssClass);
121                    return elements.isEmpty() ? null : value(elements.first());
122            }
123    
124            /**
125             * Gets the hCard values of all descendant elements that have the given CSS
126             * class name.
127             * @param cssClass the CSS class name
128             * @return the hCard values
129             */
130            public List<String> allValues(String cssClass) {
131                    Elements elements = element.getElementsByClass(cssClass);
132                    List<String> values = new ArrayList<String>(elements.size());
133                    for (Element e : elements) {
134                            values.add(value(e));
135                    }
136                    return values;
137            }
138    
139            /**
140             * Gets all type values (e.g. "home" and "cell" for the "tel" type).
141             * @return the type values (in lower-case)
142             */
143            public List<String> types() {
144                    List<String> types = allValues("type");
145                    for (int i = 0; i < types.size(); i++) {
146                            String type = types.get(i);
147                            types.set(i, type.toLowerCase());
148                    }
149                    return types;
150            }
151    
152            /**
153             * Appends text to the element, replacing newlines with
154             * <code>&lt;br&gt;</code> tags.
155             * @param text the text to append
156             */
157            public void append(String text) {
158                    //replace newlines with "<br>" tags
159                    String split[] = text.split("\\r\\n|\\n|\\r");
160                    if (split[0].length() > 0) {
161                            element.appendText(split[0]);
162                    }
163                    for (int i = 1; i < split.length; i++) {
164                            String s = split[i];
165                            element.appendElement("br");
166                            if (s.length() > 0) {
167                                    element.appendText(s);
168                            }
169                    }
170            }
171    
172            /**
173             * Gets the wrapped HTML element.
174             * @return the wrapped HTML element
175             */
176            public Element getElement() {
177                    return element;
178            }
179    
180            private String value(Element element) {
181                    //value of "title" attribute should be returned if it's a "<abbr>" tag
182                    //example: <abbr class="latitude" title="48.816667">N 48� 81.6667</abbr>
183                    if ("abbr".equals(element.tagName())) {
184                            String title = element.attr("title");
185                            if (title.length() > 0) {
186                                    return title;
187                            }
188                    }
189    
190                    StringBuilder value = new StringBuilder();
191                    Elements valueElements = element.getElementsByClass("value");
192                    if (valueElements.isEmpty()) {
193                            //get the text content of all child nodes except "type" elements
194                            visitForValue(element, value);
195                    } else {
196                            //append together all children whose CSS class is "value"
197                            for (Element valueElement : valueElements) {
198                                    if (HtmlUtils.isChildOf(valueElement, valueElements)) {
199                                            //ignore "value" elements that are descendants of other "value" elements
200                                            continue;
201                                    }
202    
203                                    if ("abbr".equals(valueElement.tagName())) {
204                                            String title = valueElement.attr("title");
205                                            if (title.length() > 0) {
206                                                    value.append(title);
207                                                    continue;
208                                            }
209                                    }
210                                    visitForValue(valueElement, value);
211                            }
212                    }
213                    return value.toString().trim();
214            }
215    
216            private void visitForValue(Element element, StringBuilder value) {
217                    for (Node node : element.childNodes()) {
218                            if (node instanceof Element) {
219                                    Element e = (Element) node;
220                                    if (!e.classNames().contains("type")) { //ignore "type" elements
221                                            if ("br".equals(e.tagName())) {
222                                                    //convert "<br>" to a newline
223                                                    value.append(System.getProperty("line.separator"));
224                                            } else if ("del".equals(e.tagName())) {
225                                                    //skip "<del>" tags
226                                            } else {
227                                                    visitForValue(e, value);
228                                            }
229                                    }
230                            } else if (node instanceof TextNode) {
231                                    TextNode t = (TextNode) node;
232                                    value.append(t.text());
233                            }
234                    }
235            }
236    }