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