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 }