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 }