001    package ezvcard.types;
002    
003    import java.util.List;
004    import java.util.SimpleTimeZone;
005    import java.util.TimeZone;
006    import java.util.regex.Matcher;
007    import java.util.regex.Pattern;
008    
009    import ezvcard.VCard;
010    import ezvcard.VCardSubTypes;
011    import ezvcard.VCardVersion;
012    import ezvcard.io.CompatibilityMode;
013    import ezvcard.io.SkipMeException;
014    import ezvcard.parameters.ValueParameter;
015    import ezvcard.util.HCardElement;
016    import ezvcard.util.VCardDateFormatter;
017    import ezvcard.util.VCardStringUtils;
018    import ezvcard.util.XCardElement;
019    
020    /*
021     Copyright (c) 2012, Michael Angstadt
022     All rights reserved.
023    
024     Redistribution and use in source and binary forms, with or without
025     modification, are permitted provided that the following conditions are met: 
026    
027     1. Redistributions of source code must retain the above copyright notice, this
028     list of conditions and the following disclaimer. 
029     2. Redistributions in binary form must reproduce the above copyright notice,
030     this list of conditions and the following disclaimer in the documentation
031     and/or other materials provided with the distribution. 
032    
033     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
034     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
035     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
036     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
037     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
038     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
039     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
040     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
041     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
042     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
043    
044     The views and conclusions contained in the software and documentation are those
045     of the authors and should not be interpreted as representing official policies, 
046     either expressed or implied, of the FreeBSD Project.
047     */
048    
049    /**
050     * Contains the timezone that the person lives/works in.
051     * 
052     * <pre>
053     * VCard vcard = new VCard();
054     * TimezoneType tz = new TimezoneType(-5, 0, &quot;America/New_York&quot;);
055     * vcard.addTimezone(tz);
056     * </pre>
057     * 
058     * <p>
059     * vCard property name: TZ
060     * </p>
061     * <p>
062     * vCard versions: 2.1, 3.0, 4.0
063     * </p>
064     * 
065     * @author Michael Angstadt
066     */
067    public class TimezoneType extends VCardType {
068            public static final String NAME = "TZ";
069    
070            private Integer hourOffset;
071            private Integer minuteOffset;
072            private String text;
073    
074            public TimezoneType() {
075                    super(NAME);
076            }
077    
078            /**
079             * This is the recommended constructor for version 4.0 vCards.
080             * @param text string representing the timezone from the <a
081             * href="http://en.wikipedia.org/wiki/List_of_tz_database_time_zones">Olson
082             * Database</a> (e.g. "America/New_York")
083             */
084            public TimezoneType(String text) {
085                    this(null, null, text);
086            }
087    
088            /**
089             * This is the recommended constructor for version 2.1 and 3.0 vCards.
090             * @param hourOffset the hour offset
091             * @param minuteOffset the minute offset
092             */
093            public TimezoneType(Integer hourOffset, Integer minuteOffset) {
094                    this(hourOffset, minuteOffset, null);
095            }
096    
097            /**
098             * This constructor can be used for all vCard versions.
099             * @param hourOffset the hour offset
100             * @param minuteOffset the minute offset
101             * @param text can be anything, but should be a string representing the
102             * timezone from the <a
103             * href="http://en.wikipedia.org/wiki/List_of_tz_database_time_zones">Olson
104             * Database</a> (e.g. "America/New_York")
105             */
106            public TimezoneType(Integer hourOffset, Integer minuteOffset, String text) {
107                    super(NAME);
108                    setHourOffset(hourOffset);
109                    setMinuteOffset(minuteOffset);
110                    setText(text);
111            }
112    
113            /**
114             * Gets the hour offset.
115             * @return the hour offset or null if not set
116             */
117            public Integer getHourOffset() {
118                    return hourOffset;
119            }
120    
121            /**
122             * Sets the hour offset.
123             * @param hourOffset the hour offset or null to remove
124             */
125            public void setHourOffset(Integer hourOffset) {
126                    this.hourOffset = hourOffset;
127            }
128    
129            /**
130             * Gets the minute offset.
131             * @return the minute offset or null if not set
132             */
133            public Integer getMinuteOffset() {
134                    return minuteOffset;
135            }
136    
137            /**
138             * Sets the minute offset.
139             * @param minuteOffset the minute offset or null to remove
140             * @throws IllegalArgumentException if the minute offset is not between 0
141             * and 59
142             */
143            public void setMinuteOffset(Integer minuteOffset) {
144                    if (minuteOffset != null && (minuteOffset < 0 || minuteOffset > 59)) {
145                            throw new IllegalArgumentException("Minute offset must be between 0 and 59.");
146                    }
147                    this.minuteOffset = minuteOffset;
148            }
149    
150            /**
151             * Gets the text portion of the timezone.
152             * @return the text portion (e.g. "America/New_York")
153             */
154            public String getText() {
155                    return text;
156            }
157    
158            /**
159             * Sets the text portion of the timezone.
160             * @param text the text portion (e.g. "America/New_York")
161             */
162            public void setText(String text) {
163                    this.text = text;
164            }
165    
166            /**
167             * Creates a {@link java.util.TimeZone} representation of this class.
168             * @return a {@link TimeZone} object or null if this object contains no
169             * offset data
170             */
171            public TimeZone toTimeZone() {
172                    if (hourOffset == null || minuteOffset == null) {
173                            return null;
174                    }
175    
176                    int rawHourOffset = hourOffset * 60 * 60 * 1000;
177                    int rawMinuteOffset = minuteOffset * 60 * 1000;
178                    if (rawHourOffset < 0) {
179                            rawMinuteOffset *= -1;
180                    }
181                    int rawOffset = rawHourOffset + rawMinuteOffset;
182                    return new SimpleTimeZone(rawOffset, "");
183            }
184    
185            /**
186             * Gets the TYPE parameter.
187             * <p>
188             * vCard versions: 4.0
189             * </p>
190             * @return the TYPE value (typically, this will be either "work" or "home")
191             * or null if it doesn't exist
192             */
193            public String getType() {
194                    return subTypes.getType();
195            }
196    
197            /**
198             * Sets the TYPE parameter.
199             * <p>
200             * vCard versions: 4.0
201             * </p>
202             * @param type the TYPE value (this should be either "work" or "home") or
203             * null to remove
204             */
205            public void setType(String type) {
206                    subTypes.setType(type);
207            }
208    
209            /**
210             * Gets the MEDIATYPE parameter.
211             * <p>
212             * vCard versions: 4.0
213             * </p>
214             * @return the media type or null if not set
215             */
216            public String getMediaType() {
217                    return subTypes.getMediaType();
218            }
219    
220            /**
221             * Sets the MEDIATYPE parameter.
222             * <p>
223             * vCard versions: 4.0
224             * </p>
225             * @param mediaType the media type or null to remove
226             */
227            public void setMediaType(String mediaType) {
228                    subTypes.setMediaType(mediaType);
229            }
230    
231            /**
232             * Gets all PID parameter values.
233             * <p>
234             * vCard versions: 4.0
235             * </p>
236             * @return the PID values or empty set if there are none
237             * @see VCardSubTypes#getPids
238             */
239            public List<Integer[]> getPids() {
240                    return subTypes.getPids();
241            }
242    
243            /**
244             * Adds a PID value.
245             * <p>
246             * vCard versions: 4.0
247             * </p>
248             * @param localId the local ID
249             * @param clientPidMapRef the ID used to reference the property's globally
250             * unique identifier in the CLIENTPIDMAP property.
251             * @see VCardSubTypes#addPid(int, int)
252             */
253            public void addPid(int localId, int clientPidMapRef) {
254                    subTypes.addPid(localId, clientPidMapRef);
255            }
256    
257            /**
258             * Removes all PID values.
259             * <p>
260             * vCard versions: 4.0
261             * </p>
262             * @see VCardSubTypes#removePids
263             */
264            public void removePids() {
265                    subTypes.removePids();
266            }
267    
268            /**
269             * Gets the preference value.
270             * <p>
271             * vCard versions: 4.0
272             * </p>
273             * @return the preference value or null if it doesn't exist
274             * @see VCardSubTypes#getPref
275             */
276            public Integer getPref() {
277                    return subTypes.getPref();
278            }
279    
280            /**
281             * Sets the preference value.
282             * <p>
283             * vCard versions: 4.0
284             * </p>
285             * @param pref the preference value or null to remove
286             * @see VCardSubTypes#setPref
287             */
288            public void setPref(Integer pref) {
289                    subTypes.setPref(pref);
290            }
291    
292            /**
293             * Gets the ALTID.
294             * <p>
295             * vCard versions: 4.0
296             * </p>
297             * @return the ALTID or null if it doesn't exist
298             * @see VCardSubTypes#getAltId
299             */
300            public String getAltId() {
301                    return subTypes.getAltId();
302            }
303    
304            /**
305             * Sets the ALTID.
306             * <p>
307             * vCard versions: 4.0
308             * </p>
309             * @param altId the ALTID or null to remove
310             * @see VCardSubTypes#setAltId
311             */
312            public void setAltId(String altId) {
313                    subTypes.setAltId(altId);
314            }
315    
316            @Override
317            protected void doMarshalSubTypes(VCardSubTypes copy, VCardVersion version, List<String> warnings, CompatibilityMode compatibilityMode, VCard vcard) {
318                    if (text != null) {
319                            copy.setValue(ValueParameter.TEXT);
320                    } else if (hourOffset != null && minuteOffset != null && version == VCardVersion.V4_0) {
321                            copy.setValue(ValueParameter.UTC_OFFSET);
322                    }
323            }
324    
325            @Override
326            protected void doMarshalText(StringBuilder sb, VCardVersion version, List<String> warnings, CompatibilityMode compatibilityMode) {
327                    if (text != null) {
328                            if ((version == VCardVersion.V2_1 || version == VCardVersion.V3_0) && hourOffset != null && minuteOffset != null) {
329                                    sb.append(VCardDateFormatter.formatTimeZone(hourOffset, minuteOffset, true));
330                                    sb.append(';');
331                            }
332                            sb.append(VCardStringUtils.escape(text));
333                    } else if (hourOffset != null && minuteOffset != null) {
334                            sb.append(VCardDateFormatter.formatTimeZone(hourOffset, minuteOffset, true));
335                    } else {
336                            throw new SkipMeException("Property does not have text or a UTC offset associated with it.");
337                    }
338            }
339    
340            @Override
341            protected void doUnmarshalText(String value, VCardVersion version, List<String> warnings, CompatibilityMode compatibilityMode) {
342                    parseValue(value);
343                    if (text != null) {
344                            text = VCardStringUtils.unescape(text);
345                    }
346            }
347    
348            @Override
349            protected void doMarshalXml(XCardElement parent, List<String> warnings, CompatibilityMode compatibilityMode) {
350                    if (text != null) {
351                            parent.text(text);
352                    } else if (hourOffset != null && minuteOffset != null) {
353                            String offset = VCardDateFormatter.formatTimeZone(hourOffset, minuteOffset, true);
354                            parent.append("utc-offset", offset);
355                    } else {
356                            throw new SkipMeException("Property does not have text or a UTC offset associated with it.");
357                    }
358            }
359    
360            @Override
361            protected void doUnmarshalXml(XCardElement element, List<String> warnings, CompatibilityMode compatibilityMode) {
362                    String value = element.get("text", "uri", "utc-offset");
363                    if (value != null) {
364                            parseValue(value);
365                    }
366            }
367    
368            private void parseValue(String value) {
369                    Pattern p = Pattern.compile("^([-\\+]?\\d{1,2}(:?\\d{2})?)(.*)");
370                    Matcher m = p.matcher(value);
371                    if (m.find()) {
372                            //do some smart parsing--if the value starts with a timezone, then parse it
373                            int offsets[] = VCardDateFormatter.parseTimeZone(m.group(1));
374                            hourOffset = offsets[0];
375                            minuteOffset = offsets[1];
376    
377                            String text = m.group(3);
378                            if (text != null && text.length() == 0) {
379                                    text = null;
380                            }
381                            this.text = text;
382                    } else {
383                            hourOffset = null;
384                            minuteOffset = null;
385                            text = value;
386                    }
387            }
388    
389            @Override
390            protected void doUnmarshalHtml(HCardElement element, List<String> warnings) {
391                    String value = element.value();
392                    doUnmarshalText(value, VCardVersion.V3_0, warnings, CompatibilityMode.RFC);
393            }
394    }