001    package ezvcard.io;
002    
003    import java.io.File;
004    import java.io.FileWriter;
005    import java.io.IOException;
006    import java.io.OutputStream;
007    import java.io.OutputStreamWriter;
008    import java.io.StringWriter;
009    import java.io.Writer;
010    import java.util.ArrayList;
011    import java.util.Arrays;
012    import java.util.Collections;
013    import java.util.HashMap;
014    import java.util.List;
015    import java.util.Map;
016    
017    import javax.xml.namespace.QName;
018    import javax.xml.transform.OutputKeys;
019    import javax.xml.transform.TransformerException;
020    
021    import org.w3c.dom.Document;
022    import org.w3c.dom.Element;
023    
024    import ezvcard.VCard;
025    import ezvcard.VCardSubTypes;
026    import ezvcard.VCardVersion;
027    import ezvcard.types.MemberType;
028    import ezvcard.types.ProdIdType;
029    import ezvcard.types.VCardType;
030    import ezvcard.util.IOUtils;
031    import ezvcard.util.ListMultimap;
032    import ezvcard.util.XmlUtils;
033    
034    /*
035     Copyright (c) 2012, Michael Angstadt
036     All rights reserved.
037    
038     Redistribution and use in source and binary forms, with or without
039     modification, are permitted provided that the following conditions are met: 
040    
041     1. Redistributions of source code must retain the above copyright notice, this
042     list of conditions and the following disclaimer. 
043     2. Redistributions in binary form must reproduce the above copyright notice,
044     this list of conditions and the following disclaimer in the documentation
045     and/or other materials provided with the distribution. 
046    
047     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
048     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
049     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
050     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
051     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
052     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
053     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
054     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
055     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
056     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
057    
058     The views and conclusions contained in the software and documentation are those
059     of the authors and should not be interpreted as representing official policies, 
060     either expressed or implied, of the FreeBSD Project.
061     */
062    
063    /**
064     * Converts vCards to their XML representation.
065     * @author Michael Angstadt
066     * @see <a href="http://tools.ietf.org/html/rfc6351">RFC 6351</a>
067     */
068    public class XCardDocument {
069            /**
070             * Defines the names of the XML elements that are used to hold each
071             * parameter's value.
072             */
073            private static final Map<String, String> parameterChildElementNames;
074            static {
075                    Map<String, String> m = new HashMap<String, String>();
076                    m.put("altid", "text");
077                    m.put("calscale", "text");
078                    m.put("geo", "uri");
079                    m.put("label", "text");
080                    m.put("language", "language-tag");
081                    m.put("mediatype", "text");
082                    m.put("pid", "text");
083                    m.put("pref", "integer");
084                    m.put("sort-as", "text");
085                    m.put("type", "text");
086                    m.put("tz", "uri");
087                    parameterChildElementNames = Collections.unmodifiableMap(m);
088            }
089    
090            private CompatibilityMode compatibilityMode = CompatibilityMode.RFC;
091            private boolean addProdId = true;
092            private VCardVersion targetVersion = VCardVersion.V4_0; //xCard standard only supports 4.0
093            private List<String> warnings = new ArrayList<String>();
094            private final Document document;
095            private final Element root;
096    
097            public XCardDocument() {
098                    document = XmlUtils.createDocument();
099                    root = createElement("vcards");
100                    document.appendChild(root);
101            }
102    
103            /**
104             * Gets the compatibility mode. Used for customizing the marshalling process
105             * to target a particular application.
106             * @return the compatibility mode
107             */
108            @Deprecated
109            public CompatibilityMode getCompatibilityMode() {
110                    return compatibilityMode;
111            }
112    
113            /**
114             * Sets the compatibility mode. Used for customizing the marshalling process
115             * to target a particular application.
116             * @param compatibilityMode the compatibility mode
117             */
118            @Deprecated
119            public void setCompatibilityMode(CompatibilityMode compatibilityMode) {
120                    this.compatibilityMode = compatibilityMode;
121            }
122    
123            /**
124             * Gets whether or not a "PRODID" type will be added to each vCard, saying
125             * that the vCard was generated by this library.
126             * @return true if it will be added, false if not (defaults to true)
127             */
128            public boolean isAddProdId() {
129                    return addProdId;
130            }
131    
132            /**
133             * Sets whether or not to add a "PRODID" type to each vCard, saying that the
134             * vCard was generated by this library.
135             * @param addProdId true to add this type, false not to (defaults to true)
136             */
137            public void setAddProdId(boolean addProdId) {
138                    this.addProdId = addProdId;
139            }
140    
141            /**
142             * Gets the warnings from the last vCard that was marshalled. This list is
143             * reset every time a new vCard is written.
144             * @return the warnings or empty list if there were no warnings
145             */
146            public List<String> getWarnings() {
147                    return new ArrayList<String>(warnings);
148            }
149    
150            /**
151             * Gets the XML document that was generated.
152             * @return the XML document
153             */
154            public Document getDocument() {
155                    return document;
156            }
157    
158            /**
159             * Writes the XML document to a string without pretty-printing it.
160             * @return the XML string
161             */
162            public String write() {
163                    return write(-1);
164            }
165    
166            /**
167             * Writes the XML document to a string and pretty-prints it.
168             * @param indent the number of indent spaces to use for pretty-printing
169             * @return the XML string
170             */
171            public String write(int indent) {
172                    StringWriter sw = new StringWriter();
173                    try {
174                            write(sw, indent);
175                    } catch (TransformerException e) {
176                            //writing to string
177                    }
178                    return sw.toString();
179            }
180    
181            /**
182             * Writes the XML document to an output stream without pretty-printing it.
183             * @param out the output stream
184             * @throws TransformerException if there's a problem writing to the output
185             * stream
186             */
187            public void write(OutputStream out) throws TransformerException {
188                    write(out, -1);
189            }
190    
191            /**
192             * Writes the XML document to an output stream and pretty-prints it.
193             * @param out the output stream
194             * @param indent the number of indent spaces to use for pretty-printing
195             * @throws TransformerException if there's a problem writing to the output
196             * stream
197             */
198            public void write(OutputStream out, int indent) throws TransformerException {
199                    write(new OutputStreamWriter(out), indent);
200            }
201    
202            /**
203             * Writes the XML document to a file without pretty-printing it.
204             * @param file the file
205             * @throws TransformerException if there's a problem writing to the file
206             */
207            public void write(File file) throws TransformerException, IOException {
208                    write(file, -1);
209            }
210    
211            /**
212             * Writes the XML document to a file and pretty-prints it.
213             * @param file the file stream
214             * @param indent the number of indent spaces to use for pretty-printing
215             * @throws TransformerException if there's a problem writing to the file
216             */
217            public void write(File file, int indent) throws TransformerException, IOException {
218                    FileWriter writer = null;
219                    try {
220                            writer = new FileWriter(file);
221                            write(writer, indent);
222                    } finally {
223                            IOUtils.closeQuietly(writer);
224                    }
225            }
226    
227            /**
228             * Writes the XML document to a writer without pretty-printing it.
229             * @param writer the writer
230             * @throws TransformerException if there's a problem writing to the writer
231             */
232            public void write(Writer writer) throws TransformerException {
233                    write(writer, -1);
234            }
235    
236            /**
237             * Writes the XML document to a writer and pretty-prints it.
238             * @param writer the writer
239             * @param indent the number of indent spaces to use for pretty-printing
240             * @throws TransformerException if there's a problem writing to the writer
241             */
242            public void write(Writer writer, int indent) throws TransformerException {
243                    Map<String, String> properties = new HashMap<String, String>();
244                    if (indent >= 0) {
245                            properties.put(OutputKeys.INDENT, "yes");
246                            properties.put("{http://xml.apache.org/xslt}indent-amount", indent + "");
247                    }
248                    XmlUtils.toWriter(document, writer, properties);
249            }
250    
251            /**
252             * Adds a vCard to the XML document
253             * @param vcard the vCard to add
254             */
255            public void addVCard(VCard vcard) {
256                    warnings.clear();
257    
258                    if (vcard.getFormattedName() == null) {
259                            warnings.add("vCard version " + targetVersion + " requires that a formatted name be defined.");
260                    }
261    
262                    ListMultimap<String, VCardType> typesToAdd = new ListMultimap<String, VCardType>(); //group the types by group name (null = no group name)
263    
264                    for (VCardType type : vcard) {
265                            if (addProdId && type instanceof ProdIdType) {
266                                    //do not add the PRODID in the vCard if "addProdId" is true
267                                    continue;
268                            }
269    
270                            //determine if this type is supported by the target version
271                            if (!supportsTargetVersion(type)) {
272                                    warnings.add("The " + type.getTypeName() + " type is not supported by xCard (vCard version " + targetVersion + ") and will not be added to the xCard.  Supported versions are " + Arrays.toString(type.getSupportedVersions()));
273                                    continue;
274                            }
275    
276                            //check for correct KIND value if there are MEMBER types
277                            if (type instanceof MemberType && (vcard.getKind() == null || !vcard.getKind().isGroup())) {
278                                    warnings.add("The value of KIND must be set to \"group\" in order to add MEMBERs to the vCard.");
279                                    continue;
280                            }
281    
282                            typesToAdd.put(type.getGroup(), type);
283                    }
284    
285                    //add an extended type saying it was generated by this library
286                    if (addProdId) {
287                            EzvcardProdIdType prodId = new EzvcardProdIdType(targetVersion);
288                            typesToAdd.put(prodId.getGroup(), prodId);
289                    }
290    
291                    //marshal each type object
292                    Element vcardElement = createElement("vcard");
293                    for (String groupName : typesToAdd.keySet()) {
294                            Element parent;
295                            if (groupName != null) {
296                                    Element groupElement = createElement("group");
297                                    groupElement.setAttribute("name", groupName);
298                                    vcardElement.appendChild(groupElement);
299                                    parent = groupElement;
300                            } else {
301                                    parent = vcardElement;
302                            }
303    
304                            List<String> warningsBuf = new ArrayList<String>();
305                            for (VCardType type : typesToAdd.get(groupName)) {
306                                    warningsBuf.clear();
307                                    try {
308                                            Element typeElement = marshalType(type, vcard, warningsBuf);
309                                            parent.appendChild(typeElement);
310                                    } catch (SkipMeException e) {
311                                            warningsBuf.add(type.getTypeName() + " property will not be marshalled: " + e.getMessage());
312                                    } catch (EmbeddedVCardException e) {
313                                            warningsBuf.add(type.getTypeName() + " property will not be marshalled: xCard does not supported embedded vCards.");
314                                    } finally {
315                                            warnings.addAll(warningsBuf);
316                                    }
317                            }
318                    }
319                    root.appendChild(vcardElement);
320            }
321    
322            /**
323             * Determines if a type supports the target version.
324             * @param type the type
325             * @return true if it supports the target version, false if not
326             */
327            private boolean supportsTargetVersion(VCardType type) {
328                    for (VCardVersion version : type.getSupportedVersions()) {
329                            if (version == targetVersion) {
330                                    return true;
331                            }
332                    }
333                    return false;
334            }
335    
336            /**
337             * Marshals a type object to an XML element.
338             * @param type the type object to marshal
339             * @param vcard the vcard the type belongs to
340             * @param warningsBuf the list to add the warnings to
341             * @return the XML element or null not to add anything to the final XML
342             * document
343             */
344            private Element marshalType(VCardType type, VCard vcard, List<String> warningsBuf) {
345                    QName qname = type.getQName();
346                    String ns, localPart;
347                    if (qname == null) {
348                            localPart = type.getTypeName().toLowerCase();
349                            ns = targetVersion.getXmlNamespace();
350                    } else {
351                            localPart = qname.getLocalPart();
352                            ns = qname.getNamespaceURI();
353                    }
354                    Element typeElement = createElement(localPart, ns);
355    
356                    //marshal the sub types
357                    VCardSubTypes subTypes = type.marshalSubTypes(targetVersion, warningsBuf, compatibilityMode, vcard);
358                    subTypes.setValue(null); //don't include the VALUE parameter (modification of the "VCardSubTypes" object is safe because it's a copy)
359                    if (!subTypes.getMultimap().isEmpty()) {
360                            Element parametersElement = createElement("parameters");
361                            for (String paramName : subTypes.getNames()) {
362                                    Element parameterElement = createElement(paramName.toLowerCase());
363                                    for (String paramValue : subTypes.get(paramName)) {
364                                            String valueElementName = parameterChildElementNames.get(paramName.toLowerCase());
365                                            if (valueElementName == null) {
366                                                    valueElementName = "unknown";
367                                            }
368                                            Element parameterValueElement = createElement(valueElementName);
369                                            parameterValueElement.setTextContent(paramValue);
370                                            parameterElement.appendChild(parameterValueElement);
371                                    }
372                                    parametersElement.appendChild(parameterElement);
373                            }
374                            typeElement.appendChild(parametersElement);
375                    }
376    
377                    //marshal the value
378                    type.marshalXml(typeElement, targetVersion, warningsBuf, compatibilityMode);
379    
380                    return typeElement;
381            }
382    
383            /**
384             * Creates a new XML element.
385             * @param name the name of the XML element
386             * @return the new XML element
387             */
388            private Element createElement(String name) {
389                    return createElement(name, targetVersion.getXmlNamespace());
390            }
391    
392            /**
393             * Creates a new XML element.
394             * @param name the name of the XML element
395             * @param ns the namespace of the XML element
396             * @return the new XML element
397             */
398            private Element createElement(String name, String ns) {
399                    return document.createElementNS(ns, name);
400            }
401    }