001package ezvcard.io.scribe; 002 003import com.github.mangstadt.vinnie.io.VObjectPropertyValues; 004 005import ezvcard.VCard; 006import ezvcard.VCardDataType; 007import ezvcard.VCardVersion; 008import ezvcard.io.CannotParseException; 009import ezvcard.io.ParseContext; 010import ezvcard.io.html.HCardElement; 011import ezvcard.io.json.JCardValue; 012import ezvcard.io.text.WriteContext; 013import ezvcard.io.xml.XCardElement; 014import ezvcard.parameter.Encoding; 015import ezvcard.parameter.MediaTypeParameter; 016import ezvcard.parameter.VCardParameters; 017import ezvcard.property.BinaryProperty; 018import ezvcard.util.DataUri; 019import ezvcard.util.org.apache.commons.codec.binary.Base64; 020 021/* 022 Copyright (c) 2012-2026, Michael Angstadt 023 All rights reserved. 024 025 Redistribution and use in source and binary forms, with or without 026 modification, are permitted provided that the following conditions are met: 027 028 1. Redistributions of source code must retain the above copyright notice, this 029 list of conditions and the following disclaimer. 030 2. Redistributions in binary form must reproduce the above copyright notice, 031 this list of conditions and the following disclaimer in the documentation 032 and/or other materials provided with the distribution. 033 034 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 035 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 036 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 037 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 038 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 039 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 040 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 041 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 042 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 043 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 044 */ 045 046/** 047 * Marshals properties that have binary data. 048 * @author Michael Angstadt 049 * @param <T> the property class 050 * @param <U> the media type class 051 */ 052public abstract class BinaryPropertyScribe<T extends BinaryProperty<U>, U extends MediaTypeParameter> extends VCardPropertyScribe<T> { 053 public BinaryPropertyScribe(Class<T> clazz, String propertyName) { 054 super(clazz, propertyName); 055 } 056 057 @Override 058 protected VCardDataType _defaultDataType(VCardVersion version) { 059 switch (version) { 060 case V2_1: 061 case V3_0: 062 return null; 063 case V4_0: 064 return VCardDataType.URI; 065 } 066 return null; 067 } 068 069 @Override 070 protected VCardDataType _dataType(T property, VCardVersion version) { 071 if (property.getUrl() != null) { 072 switch (version) { 073 case V2_1: 074 return VCardDataType.URL; 075 case V3_0: 076 case V4_0: 077 return VCardDataType.URI; 078 } 079 } 080 081 if (property.getData() != null) { 082 switch (version) { 083 case V2_1: 084 case V3_0: 085 return null; 086 case V4_0: 087 return VCardDataType.URI; 088 } 089 } 090 091 return _defaultDataType(version); 092 } 093 094 @Override 095 protected void _prepareParameters(T property, VCardParameters copy, VCardVersion version, VCard vcard) { 096 MediaTypeParameter contentType = property.getContentType(); 097 if (contentType == null) { 098 contentType = new MediaTypeParameter(null, null, null); 099 } 100 101 if (property.getUrl() != null) { 102 copy.setEncoding(null); 103 104 switch (version) { 105 case V2_1: 106 case V3_0: 107 copy.setType(contentType.getValue()); 108 copy.setMediaType(null); 109 break; 110 case V4_0: 111 copy.setMediaType(contentType.getMediaType()); 112 break; 113 } 114 115 return; 116 } 117 118 if (property.getData() != null) { 119 copy.setMediaType(null); 120 121 switch (version) { 122 case V2_1: 123 copy.setEncoding(Encoding.BASE64); 124 copy.setType(contentType.getValue()); 125 break; 126 case V3_0: 127 copy.setEncoding(Encoding.B); 128 copy.setType(contentType.getValue()); 129 break; 130 case V4_0: 131 copy.setEncoding(null); 132 //don't null out TYPE, it could be set to "home", "work", etc 133 break; 134 } 135 136 return; 137 } 138 } 139 140 @Override 141 protected String _writeText(T property, WriteContext context) { 142 return write(property, context.getVersion()); 143 } 144 145 @Override 146 protected T _parseText(String value, VCardDataType dataType, VCardParameters parameters, ParseContext context) { 147 value = VObjectPropertyValues.unescape(value); 148 return parse(value, dataType, parameters, context.getVersion()); 149 } 150 151 @Override 152 protected void _writeXml(T property, XCardElement parent) { 153 parent.append(VCardDataType.URI, write(property, parent.version())); 154 } 155 156 @Override 157 protected T _parseXml(XCardElement element, VCardParameters parameters, ParseContext context) { 158 String value = element.first(VCardDataType.URI); 159 if (value != null) { 160 return parse(value, VCardDataType.URI, parameters, element.version()); 161 } 162 163 throw missingXmlElements(VCardDataType.URI); 164 } 165 166 @Override 167 protected T _parseHtml(HCardElement element, ParseContext context) { 168 String elementName = element.tagName(); 169 if (!"object".equals(elementName)) { 170 throw new CannotParseException(1, elementName); 171 } 172 173 String data = element.absUrl("data"); 174 if (data.isEmpty()) { 175 throw new CannotParseException(2); 176 } 177 178 try { 179 DataUri uri = DataUri.parse(data); 180 U mediaType = _mediaTypeFromMediaTypeParameter(uri.getContentType()); 181 182 return _newInstance(uri.getData(), mediaType); 183 } catch (IllegalArgumentException e) { 184 //not a data URI 185 U mediaType; 186 String type = element.attr("type"); 187 if (type.isEmpty()) { 188 String extension = getFileExtension(data); 189 mediaType = (extension == null) ? null : _mediaTypeFromFileExtension(extension); 190 } else { 191 mediaType = _mediaTypeFromMediaTypeParameter(type); 192 } 193 194 return _newInstance(data, mediaType); 195 } 196 } 197 198 @Override 199 protected JCardValue _writeJson(T property) { 200 return JCardValue.single(write(property, VCardVersion.V4_0)); 201 } 202 203 @Override 204 protected T _parseJson(JCardValue value, VCardDataType dataType, VCardParameters parameters, ParseContext context) { 205 String valueStr = value.asSingle(); 206 return parse(valueStr, dataType, parameters, VCardVersion.V4_0); 207 } 208 209 /** 210 * Called if the unmarshalling code cannot determine how to unmarshal the 211 * value. 212 * @param value the value 213 * @param version the version of the vCard 214 * @param contentType the content type of the resource of null if unknown 215 * @return the unmarshalled property object or null if it cannot be 216 * unmarshalled 217 */ 218 protected T cannotUnmarshalValue(String value, VCardVersion version, U contentType) { 219 switch (version) { 220 case V2_1: 221 case V3_0: 222 if (value.startsWith("http")) { 223 return _newInstance(value, contentType); 224 } 225 226 return _newInstance(Base64.decodeBase64(value), contentType); 227 case V4_0: 228 return _newInstance(value, contentType); 229 } 230 return null; 231 } 232 233 /** 234 * Builds a {@link MediaTypeParameter} object based on the value of the 235 * MEDIATYPE parameter or data URI of 4.0 vCards. 236 * @param mediaType the media type string (e.g. "image/jpeg") 237 * @return the media type object 238 */ 239 protected abstract U _mediaTypeFromMediaTypeParameter(String mediaType); 240 241 /** 242 * Builds a {@link MediaTypeParameter} object based on the value of the TYPE 243 * parameter in 2.1/3.0 vCards. 244 * @param type the TYPE value (e.g. "JPEG") 245 * @return the media type object 246 */ 247 protected abstract U _mediaTypeFromTypeParameter(String type); 248 249 /** 250 * Searches for a {@link MediaTypeParameter} object, given a file extension. 251 * @param extension the file extension (e.g. "jpg") 252 * @return the media type object or null if not found 253 */ 254 protected abstract U _mediaTypeFromFileExtension(String extension); 255 256 /** 257 * Creates a new instance of the property object from a URI. 258 * @param uri the URI 259 * @param contentType the content type or null if unknown 260 * @return the property object 261 */ 262 protected abstract T _newInstance(String uri, U contentType); 263 264 /** 265 * Creates a new instance of the property object from binary data. 266 * @param data the data 267 * @param contentType the content type or null if unknown 268 * @return the property object 269 */ 270 protected abstract T _newInstance(byte[] data, U contentType); 271 272 /** 273 * Tries to determine a property value's content type by looking at the 274 * property's parameters. 275 * @param parameters the parameters 276 * @param version the vCard version 277 * @return the content type or null if it can't be found 278 */ 279 protected U parseContentTypeFromParameters(VCardParameters parameters, VCardVersion version) { 280 switch (version) { 281 case V2_1: 282 case V3_0: 283 //get the TYPE parameter 284 String type = parameters.getType(); 285 if (type != null) { 286 return _mediaTypeFromTypeParameter(type); 287 } 288 break; 289 case V4_0: 290 //get the MEDIATYPE parameter 291 String mediaType = parameters.getMediaType(); 292 if (mediaType != null) { 293 return _mediaTypeFromMediaTypeParameter(mediaType); 294 } 295 break; 296 } 297 298 return null; 299 } 300 301 /** 302 * Tries to determine a property value's content type by looking at the 303 * property's parameters and value. 304 * @param value the property value 305 * @param parameters the property parameters 306 * @param version the vCard version 307 * @return the content type or null if it can't be found 308 */ 309 protected U parseContentTypeFromValueAndParameters(String value, VCardParameters parameters, VCardVersion version) { 310 U contentType = parseContentTypeFromParameters(parameters, version); 311 if (contentType != null) { 312 return contentType; 313 } 314 315 //look for a file extension in the property value 316 String extension = getFileExtension(value); 317 return (extension == null) ? null : _mediaTypeFromFileExtension(extension); 318 } 319 320 /** 321 * Parses the property. 322 * @param value the property value 323 * @param dataType the data type 324 * @param parameters the property parameters 325 * @param version the vCard version 326 * @return the parsed property 327 */ 328 protected T parse(String value, VCardDataType dataType, VCardParameters parameters, VCardVersion version) { 329 /* 330 * If the value is a data URI, just parse it, no matter what the version 331 * is or what parameters are set. 2.1 and 3.0 technically don't support 332 * data URIs--parse for convenience. 333 */ 334 try { 335 return parseAsDataUri(value); 336 } catch (IllegalArgumentException e) { 337 //not a data URI 338 } 339 340 U contentType = parseContentTypeFromValueAndParameters(value, parameters, version); 341 342 switch (version) { 343 case V2_1: 344 case V3_0: 345 //parse as URL 346 if (dataType == VCardDataType.URL || dataType == VCardDataType.URI) { 347 return _newInstance(value, contentType); 348 } 349 350 //parse as binary 351 Encoding encodingSubType = parameters.getEncoding(); 352 if (encodingSubType == Encoding.BASE64 || encodingSubType == Encoding.B) { 353 return _newInstance(Base64.decodeBase64(value), contentType); 354 } 355 356 break; 357 case V4_0: 358 //already checked for data URI 359 break; 360 } 361 362 return cannotUnmarshalValue(value, version, contentType); 363 } 364 365 /** 366 * Attempts to parse the given string as a data URI. 367 * @param value the string to parse 368 * @return the data URI 369 * @throws IllegalArgumentException if the given value is not a valid data 370 * URI 371 */ 372 protected T parseAsDataUri(String value) throws IllegalArgumentException { 373 DataUri uri = DataUri.parse(value); 374 U contentType = _mediaTypeFromMediaTypeParameter(uri.getContentType()); 375 return _newInstance(uri.getData(), contentType); 376 } 377 378 private String write(T property, VCardVersion version) { 379 String url = property.getUrl(); 380 if (url != null) { 381 return url; 382 } 383 384 byte[] data = property.getData(); 385 if (data != null) { 386 switch (version) { 387 case V2_1: 388 case V3_0: 389 return Base64.encodeBase64String(data); 390 case V4_0: 391 U contentType = property.getContentType(); 392 String mediaType = (contentType == null || contentType.getMediaType() == null) ? "application/octet-stream" : contentType.getMediaType(); 393 return new DataUri(mediaType, data).toString(); 394 } 395 } 396 397 return ""; 398 } 399 400 /** 401 * Gets the file extension from a URL. 402 * @param url the URL 403 * @return the file extension (e.g. "jpg") or null if not found 404 */ 405 protected static String getFileExtension(String url) { 406 int dotPos = url.lastIndexOf('.'); 407 if (dotPos < 0 || dotPos == url.length() - 1) { 408 return null; 409 } 410 411 int slashPos = url.lastIndexOf('/'); 412 if (slashPos > dotPos) { 413 return null; 414 } 415 416 return url.substring(dotPos + 1); 417 } 418}