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}