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-2023, 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 copy.setType(contentType.getValue()); 107 copy.setMediaType(null); 108 break; 109 case V3_0: 110 copy.setType(contentType.getValue()); 111 copy.setMediaType(null); 112 break; 113 case V4_0: 114 copy.setMediaType(contentType.getMediaType()); 115 break; 116 } 117 118 return; 119 } 120 121 if (property.getData() != null) { 122 copy.setMediaType(null); 123 124 switch (version) { 125 case V2_1: 126 copy.setEncoding(Encoding.BASE64); 127 copy.setType(contentType.getValue()); 128 break; 129 case V3_0: 130 copy.setEncoding(Encoding.B); 131 copy.setType(contentType.getValue()); 132 break; 133 case V4_0: 134 copy.setEncoding(null); 135 //don't null out TYPE, it could be set to "home", "work", etc 136 break; 137 } 138 139 return; 140 } 141 } 142 143 @Override 144 protected String _writeText(T property, WriteContext context) { 145 return write(property, context.getVersion()); 146 } 147 148 @Override 149 protected T _parseText(String value, VCardDataType dataType, VCardParameters parameters, ParseContext context) { 150 value = VObjectPropertyValues.unescape(value); 151 return parse(value, dataType, parameters, context.getVersion()); 152 } 153 154 @Override 155 protected void _writeXml(T property, XCardElement parent) { 156 parent.append(VCardDataType.URI, write(property, parent.version())); 157 } 158 159 @Override 160 protected T _parseXml(XCardElement element, VCardParameters parameters, ParseContext context) { 161 String value = element.first(VCardDataType.URI); 162 if (value != null) { 163 return parse(value, VCardDataType.URI, parameters, element.version()); 164 } 165 166 throw missingXmlElements(VCardDataType.URI); 167 } 168 169 @Override 170 protected T _parseHtml(HCardElement element, ParseContext context) { 171 String elementName = element.tagName(); 172 if (!"object".equals(elementName)) { 173 throw new CannotParseException(1, elementName); 174 } 175 176 String data = element.absUrl("data"); 177 if (data.isEmpty()) { 178 throw new CannotParseException(2); 179 } 180 181 try { 182 DataUri uri = DataUri.parse(data); 183 U mediaType = _mediaTypeFromMediaTypeParameter(uri.getContentType()); 184 185 return _newInstance(uri.getData(), mediaType); 186 } catch (IllegalArgumentException e) { 187 //not a data URI 188 U mediaType; 189 String type = element.attr("type"); 190 if (type.length() > 0) { 191 mediaType = _mediaTypeFromMediaTypeParameter(type); 192 } else { 193 String extension = getFileExtension(data); 194 mediaType = (extension == null) ? null : _mediaTypeFromFileExtension(extension); 195 } 196 197 return _newInstance(data, mediaType); 198 } 199 } 200 201 @Override 202 protected JCardValue _writeJson(T property) { 203 return JCardValue.single(write(property, VCardVersion.V4_0)); 204 } 205 206 @Override 207 protected T _parseJson(JCardValue value, VCardDataType dataType, VCardParameters parameters, ParseContext context) { 208 String valueStr = value.asSingle(); 209 return parse(valueStr, dataType, parameters, VCardVersion.V4_0); 210 } 211 212 /** 213 * Called if the unmarshalling code cannot determine how to unmarshal the 214 * value. 215 * @param value the value 216 * @param version the version of the vCard 217 * @param contentType the content type of the resource of null if unknown 218 * @return the unmarshalled property object or null if it cannot be 219 * unmarshalled 220 */ 221 protected T cannotUnmarshalValue(String value, VCardVersion version, U contentType) { 222 switch (version) { 223 case V2_1: 224 case V3_0: 225 if (value.startsWith("http")) { 226 return _newInstance(value, contentType); 227 } 228 return _newInstance(Base64.decodeBase64(value), contentType); 229 case V4_0: 230 return _newInstance(value, contentType); 231 } 232 return null; 233 } 234 235 /** 236 * Builds a {@link MediaTypeParameter} object based on the value of the 237 * MEDIATYPE parameter or data URI of 4.0 vCards. 238 * @param mediaType the media type string (e.g. "image/jpeg") 239 * @return the media type object 240 */ 241 protected abstract U _mediaTypeFromMediaTypeParameter(String mediaType); 242 243 /** 244 * Builds a {@link MediaTypeParameter} object based on the value of the TYPE 245 * parameter in 2.1/3.0 vCards. 246 * @param type the TYPE value (e.g. "JPEG") 247 * @return the media type object 248 */ 249 protected abstract U _mediaTypeFromTypeParameter(String type); 250 251 /** 252 * Searches for a {@link MediaTypeParameter} object, given a file extension. 253 * @param extension the file extension (e.g. "jpg") 254 * @return the media type object or null if not found 255 */ 256 protected abstract U _mediaTypeFromFileExtension(String extension); 257 258 /** 259 * Creates a new instance of the property object from a URI. 260 * @param uri the URI 261 * @param contentType the content type or null if unknown 262 * @return the property object 263 */ 264 protected abstract T _newInstance(String uri, U contentType); 265 266 /** 267 * Creates a new instance of the property object from binary data. 268 * @param data the data 269 * @param contentType the content type or null if unknown 270 * @return the property object 271 */ 272 protected abstract T _newInstance(byte data[], U contentType); 273 274 /** 275 * Tries to determine a property value's content type by looking at the 276 * property's parameters. 277 * @param parameters the parameters 278 * @param version the vCard version 279 * @return the content type or null if it can't be found 280 */ 281 protected U parseContentTypeFromParameters(VCardParameters parameters, VCardVersion version) { 282 switch (version) { 283 case V2_1: 284 case V3_0: 285 //get the TYPE parameter 286 String type = parameters.getType(); 287 if (type != null) { 288 return _mediaTypeFromTypeParameter(type); 289 } 290 break; 291 case V4_0: 292 //get the MEDIATYPE parameter 293 String mediaType = parameters.getMediaType(); 294 if (mediaType != null) { 295 return _mediaTypeFromMediaTypeParameter(mediaType); 296 } 297 break; 298 } 299 300 return null; 301 } 302 303 /** 304 * Tries to determine a property value's content type by looking at the 305 * property's parameters and value. 306 * @param value the property value 307 * @param parameters the property parameters 308 * @param version the vCard version 309 * @return the content type or null if it can't be found 310 */ 311 protected U parseContentTypeFromValueAndParameters(String value, VCardParameters parameters, VCardVersion version) { 312 U contentType = parseContentTypeFromParameters(parameters, version); 313 if (contentType != null) { 314 return contentType; 315 } 316 317 //look for a file extension in the property value 318 String extension = getFileExtension(value); 319 return (extension == null) ? null : _mediaTypeFromFileExtension(extension); 320 } 321 322 /** 323 * Parses the property. 324 * @param value the property value 325 * @param dataType the data type 326 * @param parameters the property parameters 327 * @param version the vCard version 328 * @return the parsed property 329 */ 330 protected T parse(String value, VCardDataType dataType, VCardParameters parameters, VCardVersion version) { 331 U contentType = parseContentTypeFromValueAndParameters(value, parameters, version); 332 333 switch (version) { 334 case V2_1: 335 case V3_0: 336 //parse as URL 337 if (dataType == VCardDataType.URL || dataType == VCardDataType.URI) { 338 return _newInstance(value, contentType); 339 } 340 341 //parse as binary 342 Encoding encodingSubType = parameters.getEncoding(); 343 if (encodingSubType == Encoding.BASE64 || encodingSubType == Encoding.B) { 344 return _newInstance(Base64.decodeBase64(value), contentType); 345 } 346 347 break; 348 case V4_0: 349 try { 350 //parse as data URI 351 DataUri uri = DataUri.parse(value); 352 contentType = _mediaTypeFromMediaTypeParameter(uri.getContentType()); 353 return _newInstance(uri.getData(), contentType); 354 } catch (IllegalArgumentException e) { 355 //not a data URI 356 } 357 break; 358 } 359 360 return cannotUnmarshalValue(value, version, contentType); 361 } 362 363 private String write(T property, VCardVersion version) { 364 String url = property.getUrl(); 365 if (url != null) { 366 return url; 367 } 368 369 byte data[] = property.getData(); 370 if (data != null) { 371 switch (version) { 372 case V2_1: 373 case V3_0: 374 return Base64.encodeBase64String(data); 375 case V4_0: 376 U contentType = property.getContentType(); 377 String mediaType = (contentType == null || contentType.getMediaType() == null) ? "application/octet-stream" : contentType.getMediaType(); 378 return new DataUri(mediaType, data).toString(); 379 } 380 } 381 382 return ""; 383 } 384 385 /** 386 * Gets the file extension from a URL. 387 * @param url the URL 388 * @return the file extension (e.g. "jpg") or null if not found 389 */ 390 protected static String getFileExtension(String url) { 391 int dotPos = url.lastIndexOf('.'); 392 if (dotPos < 0 || dotPos == url.length() - 1) { 393 return null; 394 } 395 396 int slashPos = url.lastIndexOf('/'); 397 if (slashPos > dotPos) { 398 return null; 399 } 400 401 return url.substring(dotPos + 1); 402 } 403}