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 }