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}