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 }