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 }