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><abbr></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><br></code> tags are converted to newlines. All text
106 * within <code><del></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><br></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 }