001    package ezvcard.types;
002    
003    import java.io.File;
004    import java.io.FileInputStream;
005    import java.io.IOException;
006    import java.io.InputStream;
007    import java.util.List;
008    
009    import org.apache.commons.codec.binary.Base64;
010    
011    import ezvcard.VCard;
012    import ezvcard.VCardSubTypes;
013    import ezvcard.VCardVersion;
014    import ezvcard.io.CompatibilityMode;
015    import ezvcard.io.SkipMeException;
016    import ezvcard.parameters.EncodingParameter;
017    import ezvcard.parameters.MediaTypeParameter;
018    import ezvcard.parameters.ValueParameter;
019    import ezvcard.util.DataUri;
020    import ezvcard.util.HCardElement;
021    import ezvcard.util.IOUtils;
022    import ezvcard.util.XCardElement;
023    
024    /*
025     Copyright (c) 2012, Michael Angstadt
026     All rights reserved.
027    
028     Redistribution and use in source and binary forms, with or without
029     modification, are permitted provided that the following conditions are met: 
030    
031     1. Redistributions of source code must retain the above copyright notice, this
032     list of conditions and the following disclaimer. 
033     2. Redistributions in binary form must reproduce the above copyright notice,
034     this list of conditions and the following disclaimer in the documentation
035     and/or other materials provided with the distribution. 
036    
037     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
038     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
039     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
040     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
041     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
042     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
043     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
044     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
045     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
046     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
047    
048     The views and conclusions contained in the software and documentation are those
049     of the authors and should not be interpreted as representing official policies, 
050     either expressed or implied, of the FreeBSD Project.
051     */
052    
053    /**
054     * Represents a Type that contains binary data (for example, the "PHOTO" type).
055     * @author Michael Angstadt
056     * 
057     * @param <T> the class used for representing the content type of the resource
058     */
059    public abstract class BinaryType<T extends MediaTypeParameter> extends VCardType {
060            /**
061             * The decoded data.
062             */
063            protected byte[] data;
064    
065            /**
066             * The URL to the resource.
067             */
068            protected String url;
069    
070            /**
071             * The content type of the resource (e.g. "JPEG image").
072             */
073            protected T contentType;
074    
075            /**
076             * @param name the type name (e.g. "PHOTO")
077             */
078            public BinaryType(String name) {
079                    super(name);
080            }
081    
082            /**
083             * @param name the type name (e.g. "PHOTO")
084             * @param url the URL to the resource
085             * @param type the content type
086             */
087            public BinaryType(String name, String url, T type) {
088                    this(name);
089                    setUrl(url, type);
090            }
091    
092            /**
093             * @param name the type name (e.g. "PHOTO")
094             * @param data the binary data
095             * @param type the content type
096             */
097            public BinaryType(String name, byte[] data, T type) {
098                    this(name);
099                    setData(data, type);
100            }
101    
102            /**
103             * @param name the type name (e.g. "PHOTO")
104             * @param in an input stream to the binary data (will be closed)
105             * @param type the content type
106             * @throws IOException
107             */
108            public BinaryType(String name, InputStream in, T type) throws IOException {
109                    this(name, IOUtils.toByteArray(in, true), type);
110            }
111    
112            /**
113             * @param name the type name (e.g. "PHOTO")
114             * @param file the file containing the binary data
115             * @param type the content type
116             * @throws IOException
117             */
118            public BinaryType(String name, File file, T type) throws IOException {
119                    this(name, new FileInputStream(file), type);
120            }
121    
122            /**
123             * Gets the binary data of the resource.
124             * @return the binary data or null if there is none
125             */
126            public byte[] getData() {
127                    return data;
128            }
129    
130            /**
131             * Sets the binary data of the resource.
132             * @param data the binary data
133             * @param type the content type (e.g. "JPEG image")
134             */
135            public void setData(byte[] data, T type) {
136                    this.url = null;
137                    this.data = data;
138                    setContentType(type);
139            }
140    
141            /**
142             * Gets the URL to the resource
143             * @return the URL or null if there is none
144             */
145            public String getUrl() {
146                    return url;
147            }
148    
149            /**
150             * Sets the URL to the resource.
151             * @param url the URL
152             * @param type the content type (e.g. "JPEG image")
153             */
154            public void setUrl(String url, T type) {
155                    this.url = url;
156                    this.data = null;
157                    setContentType(type);
158            }
159    
160            /**
161             * Gets the content type of the resource.
162             * @return the content type (e.g. "JPEG image")
163             */
164            public T getContentType() {
165                    return contentType;
166            }
167    
168            /**
169             * Sets the content type of the resource.
170             * @param contentType the content type (e.g. "JPEG image")
171             */
172            public void setContentType(T contentType) {
173                    this.contentType = contentType;
174            }
175    
176            /**
177             * Gets the vCard 4.0 TYPE parameter. This should NOT be used to get the
178             * TYPE parameter for 2.1/3.0 vCards. Use {@link #getContentType} instead.
179             * <p>
180             * vCard versions: 4.0
181             * </p>
182             * @return the TYPE value (typically, this will be either "work" or "home")
183             * or null if it doesn't exist
184             */
185            public String getType() {
186                    return subTypes.getType();
187            }
188    
189            /**
190             * Sets the vCard 4.0 TYPE parameter. This should NOT be used to set the
191             * TYPE parameter for 2.1/3.0 vCards. Use {@link #setContentType} instead.
192             * <p>
193             * vCard versions: 4.0
194             * </p>
195             * @param type the TYPE value (should be either "work" or "home") or null to
196             * remove
197             */
198            public void setType(String type) {
199                    subTypes.setType(type);
200            }
201    
202            /**
203             * Gets all PID parameter values.
204             * <p>
205             * vCard versions: 4.0
206             * </p>
207             * @return the PID values or empty set if there are none
208             * @see VCardSubTypes#getPids
209             */
210            public List<Integer[]> getPids() {
211                    return subTypes.getPids();
212            }
213    
214            /**
215             * Adds a PID value.
216             * <p>
217             * vCard versions: 4.0
218             * </p>
219             * @param localId the local ID
220             * @param clientPidMapRef the ID used to reference the property's globally
221             * unique identifier in the CLIENTPIDMAP property.
222             * @see VCardSubTypes#addPid(int, int)
223             */
224            public void addPid(int localId, int clientPidMapRef) {
225                    subTypes.addPid(localId, clientPidMapRef);
226            }
227    
228            /**
229             * Removes all PID values.
230             * <p>
231             * vCard versions: 4.0
232             * </p>
233             * @see VCardSubTypes#removePids
234             */
235            public void removePids() {
236                    subTypes.removePids();
237            }
238    
239            /**
240             * Gets the preference value.
241             * <p>
242             * vCard versions: 4.0
243             * </p>
244             * @return the preference value or null if it doesn't exist
245             * @see VCardSubTypes#getPref
246             */
247            public Integer getPref() {
248                    return subTypes.getPref();
249            }
250    
251            /**
252             * Sets the preference value.
253             * <p>
254             * vCard versions: 4.0
255             * </p>
256             * @param pref the preference value or null to remove
257             * @see VCardSubTypes#setPref
258             */
259            public void setPref(Integer pref) {
260                    subTypes.setPref(pref);
261            }
262    
263            /**
264             * Gets the ALTID.
265             * <p>
266             * vCard versions: 4.0
267             * </p>
268             * @return the ALTID or null if it doesn't exist
269             * @see VCardSubTypes#getAltId
270             */
271            public String getAltId() {
272                    return subTypes.getAltId();
273            }
274    
275            /**
276             * Sets the ALTID.
277             * <p>
278             * vCard versions: 4.0
279             * </p>
280             * @param altId the ALTID or null to remove
281             * @see VCardSubTypes#setAltId
282             */
283            public void setAltId(String altId) {
284                    subTypes.setAltId(altId);
285            }
286    
287            @Override
288            protected void doMarshalSubTypes(VCardSubTypes copy, VCardVersion version, List<String> warnings, CompatibilityMode compatibilityMode, VCard vcard) {
289                    MediaTypeParameter contentType = this.contentType;
290                    if (contentType == null) {
291                            contentType = new MediaTypeParameter(null, null, null);
292                    }
293    
294                    if (url != null) {
295                            ValueParameter vp = null;
296                            switch (version) {
297                            case V2_1:
298                                    vp = ValueParameter.URL;
299                                    break;
300                            case V3_0:
301                            case V4_0:
302                                    vp = ValueParameter.URI;
303                                    break;
304                            }
305                            copy.setValue(vp);
306    
307                            copy.setEncoding(null);
308    
309                            if (version == VCardVersion.V4_0) {
310                                    //don't null out TYPE, it could be set to "home" or "work"
311                                    copy.setMediaType(contentType.getMediaType());
312                            } else {
313                                    copy.setType(contentType.getValue());
314                                    copy.setMediaType(null);
315                            }
316                    }
317                    if (data != null) {
318                            copy.setMediaType(null);
319                            if (version == VCardVersion.V2_1) {
320                                    copy.setEncoding(EncodingParameter.BASE64);
321                                    copy.setValue(null);
322                                    copy.setType(contentType.getValue());
323                            } else if (version == VCardVersion.V3_0) {
324                                    copy.setEncoding(EncodingParameter.B);
325                                    copy.setValue(null);
326                                    copy.setType(contentType.getValue());
327                            } else {
328                                    copy.setEncoding(null);
329                                    copy.setValue(ValueParameter.URI);
330                                    //don't null out TYPE, it could be set to "home" or "work"
331                            }
332                    }
333            }
334    
335            @Override
336            protected void doMarshalText(StringBuilder sb, VCardVersion version, List<String> warnings, CompatibilityMode compatibilityMode) {
337                    if (url != null) {
338                            sb.append(url);
339                    } else if (data != null) {
340                            String base64 = new String(Base64.encodeBase64(data));
341                            if (version == VCardVersion.V4_0) {
342                                    String mediaType = (contentType == null || contentType.getMediaType() == null) ? "application/octet-stream" : contentType.getMediaType();
343                                    DataUri uri = new DataUri(mediaType, data);
344                                    sb.append(uri.toString());
345                            } else {
346                                    sb.append(base64);
347                            }
348                    } else {
349                            throw new SkipMeException("Property has neither a URL nor binary data attached to it.");
350                    }
351            }
352    
353            @Override
354            protected void doUnmarshalText(String value, VCardVersion version, List<String> warnings, CompatibilityMode compatibilityMode) {
355                    //check for a 4.0 data URI
356                    try {
357                            DataUri uri = new DataUri(value);
358                            T contentType = buildMediaTypeObj(uri.getContentType());
359                            setData(uri.getData(), contentType);
360                            return;
361                    } catch (IllegalArgumentException e) {
362                            //not a data URI
363                    }
364    
365                    //get content type from 4.0 MEDIATYPE parameter
366                    T contentType = null;
367                    String mediaType = subTypes.getMediaType();
368                    if (mediaType != null) {
369                            contentType = buildMediaTypeObj(mediaType);
370                    } else {
371                            //get content type from 2.1/3.0 TYPE parameter
372                            String type = subTypes.getType();
373                            if (type != null && (version == VCardVersion.V2_1 || version == VCardVersion.V3_0)) {
374                                    contentType = buildTypeObj(getType());
375                            }
376                    }
377    
378                    //check for a URL
379                    ValueParameter valueSubType = subTypes.getValue();
380                    if (valueSubType == ValueParameter.URL || valueSubType == ValueParameter.URI) {
381                            setUrl(value, contentType);
382                            return;
383                    }
384    
385                    //check for 2.1/3.0 base64 data
386                    EncodingParameter encodingSubType = subTypes.getEncoding();
387                    if (encodingSubType != null) {
388                            if (encodingSubType != EncodingParameter.B && encodingSubType != EncodingParameter.BASE64) {
389                                    warnings.add("Unrecognized " + EncodingParameter.NAME + " parameter value \"" + encodingSubType + "\" in " + getTypeName() + " property.  Attempting to decode as base64.");
390                            }
391                            setData(Base64.decodeBase64(value), contentType);
392                            return;
393                    }
394    
395                    //see if the value is a URL incase they didn't set the VALUE parameter
396                    if (value.matches("(?i)http.*")) {
397                            setUrl(value, contentType);
398                            return;
399                    }
400    
401                    cannotUnmarshalValue(value, version, warnings, compatibilityMode, contentType);
402            }
403    
404            @Override
405            protected void doMarshalXml(XCardElement parent, List<String> warnings, CompatibilityMode compatibilityMode) {
406                    StringBuilder sb = new StringBuilder();
407                    doMarshalText(sb, parent.version(), warnings, compatibilityMode);
408                    parent.uri(sb.toString());
409            }
410    
411            @Override
412            protected void doUnmarshalXml(XCardElement element, List<String> warnings, CompatibilityMode compatibilityMode) {
413                    String value = element.uri();
414                    if (value != null) {
415                            doUnmarshalText(value, element.version(), warnings, compatibilityMode);
416                    }
417            }
418    
419            @Override
420            protected void doUnmarshalHtml(HCardElement element, List<String> warnings) {
421                    String elementName = element.tagName();
422                    String error = null;
423                    if ("object".equals(elementName)) {
424                            T mediaType = null;
425                            String type = element.attr("type");
426                            if (type.length() > 0) {
427                                    mediaType = buildMediaTypeObj(type);
428                            }
429    
430                            String data = element.absUrl("data");
431                            if (data.length() > 0) {
432                                    try {
433                                            DataUri uri = new DataUri(data);
434                                            mediaType = buildMediaTypeObj(uri.getContentType());
435                                            setData(uri.getData(), mediaType);
436                                    } catch (IllegalArgumentException e) {
437                                            //TODO create buildTypeObjFromExtension() method
438                                            setUrl(data, null);
439                                    }
440                            } else {
441                                    error = "<object> tag does not have a \"data\" attribute.";
442                            }
443                    } else {
444                            error = "Cannot parse HTML tag \"" + elementName + "\".";
445                    }
446    
447                    if (error != null) {
448                            throw new SkipMeException(error);
449                    }
450            }
451    
452            /**
453             * Called if the unmarshalling code cannot determine how to unmarshal the
454             * value.
455             * @param value the value
456             * @param version the version of the vCard
457             * @param warnings the warnings
458             * @param compatibilityMode the compatibility mode
459             * @param contentType the content type of the resource of null if unknown
460             */
461            protected void cannotUnmarshalValue(String value, VCardVersion version, List<String> warnings, CompatibilityMode compatibilityMode, T contentType) {
462                    warnings.add("No " + ValueParameter.NAME + " or " + EncodingParameter.NAME + " parameter given.  Attempting to decode as base64.");
463                    setData(Base64.decodeBase64(value), contentType);
464            }
465    
466            /**
467             * Builds a {@link MediaTypeParameter} object based on the information in
468             * the MEDIATYPE parameter or data URI of 4.0 vCards.
469             * @param mediaType the media type string (e.g. "image/jpeg")
470             * @return the parameter object
471             */
472            protected abstract T buildMediaTypeObj(String mediaType);
473    
474            /**
475             * Builds a {@link MediaTypeParameter} object based on the value of the TYPE
476             * parameter in 2.1/3.0 vCards.
477             * @param type the TYPE value
478             * @return the parameter object
479             */
480            protected abstract T buildTypeObj(String type);
481    }