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>&lt;vcard&gt;</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    }