001 package ezvcard.io;
002
003 import java.io.File;
004 import java.io.FileReader;
005 import java.io.IOException;
006 import java.io.InputStream;
007 import java.io.InputStreamReader;
008 import java.io.Reader;
009 import java.io.StringReader;
010 import java.lang.reflect.Method;
011 import java.util.ArrayList;
012 import java.util.Arrays;
013 import java.util.HashMap;
014 import java.util.Iterator;
015 import java.util.List;
016 import java.util.Map;
017
018 import javax.xml.namespace.NamespaceContext;
019 import javax.xml.namespace.QName;
020 import javax.xml.xpath.XPath;
021 import javax.xml.xpath.XPathConstants;
022 import javax.xml.xpath.XPathExpressionException;
023 import javax.xml.xpath.XPathFactory;
024
025 import org.w3c.dom.Document;
026 import org.w3c.dom.Element;
027 import org.w3c.dom.NodeList;
028 import org.xml.sax.SAXException;
029
030 import ezvcard.VCard;
031 import ezvcard.VCardSubTypes;
032 import ezvcard.VCardVersion;
033 import ezvcard.types.RawType;
034 import ezvcard.types.TypeList;
035 import ezvcard.types.VCardType;
036 import ezvcard.types.XmlType;
037 import ezvcard.util.IOUtils;
038 import ezvcard.util.XmlUtils;
039
040 /*
041 Copyright (c) 2012, Michael Angstadt
042 All rights reserved.
043
044 Redistribution and use in source and binary forms, with or without
045 modification, are permitted provided that the following conditions are met:
046
047 1. Redistributions of source code must retain the above copyright notice, this
048 list of conditions and the following disclaimer.
049 2. Redistributions in binary form must reproduce the above copyright notice,
050 this list of conditions and the following disclaimer in the documentation
051 and/or other materials provided with the distribution.
052
053 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
054 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
055 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
056 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
057 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
058 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
059 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
060 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
061 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
062 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
063
064 The views and conclusions contained in the software and documentation are those
065 of the authors and should not be interpreted as representing official policies,
066 either expressed or implied, of the FreeBSD Project.
067 */
068
069 /**
070 * Unmarshals XML-encoded vCards into {@link VCard} objects.
071 * @author Michael Angstadt
072 * @see <a href="http://tools.ietf.org/html/rfc6351">RFC 6351</a>
073 */
074 public class XCardReader implements IParser {
075 private static final VCardVersion version = VCardVersion.V4_0;
076 public static final XCardNamespaceContext nsContext = new XCardNamespaceContext("v");
077
078 private CompatibilityMode compatibilityMode = CompatibilityMode.RFC;
079 private List<String> warnings = new ArrayList<String>();
080 private Map<QName, Class<? extends VCardType>> extendedTypeClasses = new HashMap<QName, Class<? extends VCardType>>();
081
082 /**
083 * The <code><vcard></code> elements within the XML document.
084 */
085 private Iterator<Element> vcardElements;
086
087 /**
088 * @param xml the XML string to read the vCards from
089 * @throws SAXException if there's a problem parsing the XML
090 */
091 public XCardReader(String xml) throws SAXException {
092 try {
093 init(new StringReader(xml));
094 } catch (IOException e) {
095 //reading from string
096 }
097 }
098
099 /**
100 * @param in the input stream to read the vCards from
101 * @throws IOException if there's a problem reading from the input stream
102 * @throws SAXException if there's a problem parsing the XML
103 */
104 public XCardReader(InputStream in) throws SAXException, IOException {
105 this(new InputStreamReader(in));
106 }
107
108 /**
109 * @param file the file to read the vCards from
110 * @throws IOException if there's a problem reading from the file
111 * @throws SAXException if there's a problem parsing the XML
112 */
113 public XCardReader(File file) throws SAXException, IOException {
114 FileReader reader = null;
115 try {
116 reader = new FileReader(file);
117 init(reader);
118 } finally {
119 IOUtils.closeQuietly(reader);
120 }
121 }
122
123 /**
124 * @param reader the reader to read the vCards from
125 * @throws IOException if there's a problem reading from the reader
126 * @throws SAXException if there's a problem parsing the XML
127 */
128 public XCardReader(Reader reader) throws SAXException, IOException {
129 init(reader);
130 }
131
132 /**
133 * @param document the XML document to read the vCards from
134 */
135 public XCardReader(Document document) {
136 init(document);
137 }
138
139 private void init(Reader reader) throws SAXException, IOException {
140 init(XmlUtils.toDocument(reader));
141 }
142
143 private void init(Document document) {
144 try {
145 XPath xpath = XPathFactory.newInstance().newXPath();
146 xpath.setNamespaceContext(nsContext);
147
148 String prefix = nsContext.prefix;
149 NodeList nodeList = (NodeList) xpath.evaluate("//" + prefix + ":vcards/" + prefix + ":vcard", document, XPathConstants.NODESET);
150 vcardElements = XmlUtils.toElementList(nodeList).iterator();
151 } catch (XPathExpressionException e) {
152 //never thrown, xpath expression is hard coded
153 }
154 }
155
156 /**
157 * Gets the compatibility mode. Used for customizing the unmarshalling
158 * process based on the application that generated the vCard.
159 * @return the compatibility mode
160 */
161 @Deprecated
162 public CompatibilityMode getCompatibilityMode() {
163 return compatibilityMode;
164 }
165
166 /**
167 * Sets the compatibility mode. Used for customizing the unmarshalling
168 * process based on the application that generated the vCard.
169 * @param compatibilityMode the compatibility mode
170 */
171 @Deprecated
172 public void setCompatibilityMode(CompatibilityMode compatibilityMode) {
173 this.compatibilityMode = compatibilityMode;
174 }
175
176 //@Override
177 public void registerExtendedType(Class<? extends VCardType> clazz) {
178 extendedTypeClasses.put(getQNameFromTypeClass(clazz), clazz);
179 }
180
181 //@Override
182 public void unregisterExtendedType(Class<? extends VCardType> clazz) {
183 extendedTypeClasses.remove(getQNameFromTypeClass(clazz));
184 }
185
186 //@Override
187 public List<String> getWarnings() {
188 return new ArrayList<String>(warnings);
189 }
190
191 //@Override
192 public VCard readNext() {
193 warnings.clear();
194
195 if (!vcardElements.hasNext()) {
196 return null;
197 }
198
199 VCard vcard = new VCard();
200 vcard.setVersion(version);
201
202 Element vcardElement = vcardElements.next();
203
204 String ns = version.getXmlNamespace();
205 List<Element> children = XmlUtils.toElementList(vcardElement.getChildNodes());
206 List<String> warningsBuf = new ArrayList<String>();
207 for (Element child : children) {
208 if ("group".equals(child.getLocalName()) && ns.equals(child.getNamespaceURI())) {
209 String group = child.getAttribute("name");
210 if (group.length() == 0) {
211 group = null;
212 }
213 List<Element> propElements = XmlUtils.toElementList(child.getChildNodes());
214 for (Element propElement : propElements) {
215 parseAndAddElement(propElement, group, version, vcard, warningsBuf);
216 }
217 } else {
218 parseAndAddElement(child, null, version, vcard, warningsBuf);
219 }
220 }
221
222 return vcard;
223 }
224
225 /**
226 * Parses a property element from the XML document and adds the property to
227 * the vCard.
228 * @param element the element to parse
229 * @param group the group name or null if the property does not belong to a
230 * group
231 * @param version the vCard version
232 * @param vcard the vCard object
233 * @param warningsBuf the list to add the warnings to
234 */
235 private void parseAndAddElement(Element element, String group, VCardVersion version, VCard vcard, List<String> warningsBuf) {
236 warningsBuf.clear();
237
238 VCardSubTypes subTypes = parseSubTypes(element);
239 VCardType type = createTypeObject(element.getLocalName(), element.getNamespaceURI());
240 type.setGroup(group);
241 try {
242 try {
243 type.unmarshalXml(subTypes, element, version, warningsBuf, compatibilityMode);
244 } catch (UnsupportedOperationException e) {
245 //type class does not support xCard
246 warningsBuf.add("Type class \"" + type.getClass().getName() + "\" does not support xCard unmarshalling. It will be unmarshalled as a " + XmlType.NAME + " property.");
247 type = new XmlType();
248 type.setGroup(group);
249 type.unmarshalXml(subTypes, element, version, warningsBuf, compatibilityMode);
250 }
251 addToVCard(type, vcard);
252 } catch (SkipMeException e) {
253 warningsBuf.add(type.getTypeName() + " property will not be unmarshalled: " + e.getMessage());
254 } catch (EmbeddedVCardException e) {
255 warningsBuf.add(type.getTypeName() + " property will not be unmarshalled: xCard does not supported embedded vCards.");
256 } finally {
257 warnings.addAll(warningsBuf);
258 }
259 }
260
261 /**
262 * Parses the property parameters (aka "sub types").
263 * @param element the property's XML element
264 * @return the parsed parameters
265 */
266 private VCardSubTypes parseSubTypes(Element element) {
267 VCardSubTypes subTypes = new VCardSubTypes();
268
269 List<Element> parametersElements = XmlUtils.toElementList(element.getElementsByTagNameNS(version.getXmlNamespace(), "parameters"));
270 for (Element parametersElement : parametersElements) { // foreach "<parameters>" element (there should only be 1 though)
271 List<Element> paramElements = XmlUtils.toElementList(parametersElement.getChildNodes());
272 for (Element paramElement : paramElements) {
273 String name = paramElement.getLocalName().toUpperCase();
274 List<Element> valueElements = XmlUtils.toElementList(paramElement.getChildNodes());
275 if (valueElements.isEmpty()) {
276 String value = paramElement.getTextContent();
277 subTypes.put(name, value);
278 } else {
279 for (Element valueElement : valueElements) {
280 String value = valueElement.getTextContent();
281 subTypes.put(name, value);
282 }
283 }
284 }
285
286 //remove the <parameters> element from the DOM
287 element.removeChild(parametersElement);
288 }
289
290 return subTypes;
291 }
292
293 /**
294 * Creates the appropriate VCardType instance given the vCard property name.
295 * This method does not unmarshal the type, it just creates the type object.
296 * @param name the property name (e.g. "fn")
297 * @param ns the namespace of the element
298 * @return the type that was created
299 */
300 private VCardType createTypeObject(String name, String ns) {
301 name = name.toUpperCase();
302
303 Class<? extends VCardType> clazz = TypeList.getTypeClass(name);
304 if (clazz != null && VCardVersion.V4_0.getXmlNamespace().equals(ns)) {
305 try {
306 return clazz.newInstance();
307 } catch (Exception e) {
308 //it is the responsibility of the EZ-vCard developer to ensure that this exception is never thrown
309 //all type classes defined in the EZ-vCard library MUST have public, no-arg constructors
310 throw new RuntimeException(e);
311 }
312 } else {
313 Class<? extends VCardType> extendedTypeClass = extendedTypeClasses.get(new QName(ns, name.toLowerCase()));
314 if (extendedTypeClass != null) {
315 try {
316 return extendedTypeClass.newInstance();
317 } catch (Exception e) {
318 //this should never happen because the type class is checked to see if it has a public, no-arg constructor in the "registerExtendedType" method
319 throw new RuntimeException("Extended type class \"" + extendedTypeClass.getName() + "\" MUST have a public, no-arg constructor.");
320 }
321 } else if (name.startsWith("X-")) {
322 return new RawType(name);
323 } else {
324 //add as an XML property
325 return new XmlType();
326 }
327 }
328 }
329
330 /**
331 * Adds a type to the vCard.
332 * @param t the type object
333 * @param vcard the vCard
334 */
335 private void addToVCard(VCardType t, VCard vcard) {
336 Method method = TypeList.getAddMethod(t.getClass());
337 if (method != null) {
338 try {
339 method.invoke(vcard, t);
340 } catch (Exception e) {
341 //this should NEVER be thrown because the method MUST be public
342 throw new RuntimeException(e);
343 }
344 } else {
345 vcard.addExtendedType(t);
346 }
347 }
348
349 /**
350 * Gets the QName from a type class.
351 * @param clazz the type class
352 * @return the QName
353 */
354 private QName getQNameFromTypeClass(Class<? extends VCardType> clazz) {
355 try {
356 VCardType type = clazz.newInstance();
357 QName qname = type.getQName();
358 if (qname == null) {
359 qname = new QName(version.getXmlNamespace(), type.getTypeName().toLowerCase());
360 }
361 return qname;
362 } catch (Exception e) {
363 //there is no public, no-arg constructor
364 throw new RuntimeException(e);
365 }
366 }
367
368 /**
369 * Namespace context to use for xCard XPath expressions.
370 * @see XPath#setNamespaceContext(NamespaceContext)
371 */
372 public static class XCardNamespaceContext implements NamespaceContext {
373 private final String ns;
374 private final String prefix;
375
376 /**
377 * @param prefix the prefix to use
378 */
379 public XCardNamespaceContext(String prefix) {
380 ns = version.getXmlNamespace();
381 this.prefix = prefix;
382 }
383
384 //@Override
385 public String getNamespaceURI(String prefix) {
386 if (prefix.equals(prefix)) {
387 return ns;
388 }
389 return null;
390 }
391
392 //@Override
393 public String getPrefix(String ns) {
394 if (ns.equals(this.ns)) {
395 return prefix;
396 }
397 return null;
398 }
399
400 //@Override
401 public Iterator<String> getPrefixes(String ns) {
402 if (ns.equals(this.ns)) {
403 return Arrays.asList(prefix).iterator();
404 }
405 return null;
406 }
407 }
408 }