001package ezvcard.io.scribe;
002
003import java.util.TimeZone;
004
005import com.github.mangstadt.vinnie.io.VObjectPropertyValues;
006
007import ezvcard.VCardDataType;
008import ezvcard.VCardVersion;
009import ezvcard.io.CannotParseException;
010import ezvcard.io.ParseContext;
011import ezvcard.io.html.HCardElement;
012import ezvcard.io.json.JCardValue;
013import ezvcard.io.text.WriteContext;
014import ezvcard.io.xml.XCardElement;
015import ezvcard.parameter.VCardParameters;
016import ezvcard.property.Timezone;
017import ezvcard.util.UtcOffset;
018
019/*
020 Copyright (c) 2012-2018, Michael Angstadt
021 All rights reserved.
022
023 Redistribution and use in source and binary forms, with or without
024 modification, are permitted provided that the following conditions are met: 
025
026 1. Redistributions of source code must retain the above copyright notice, this
027 list of conditions and the following disclaimer. 
028 2. Redistributions in binary form must reproduce the above copyright notice,
029 this list of conditions and the following disclaimer in the documentation
030 and/or other materials provided with the distribution. 
031
032 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
033 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
034 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
035 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
036 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
037 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
038 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
039 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
040 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
041 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
042 */
043
044/**
045 * Marshals {@link Timezone} properties.
046 * @author Michael Angstadt
047 */
048//@formatter:off
049/* 
050 * Parsing===================
051 * 
052 * vCard 2.1:
053 * Parse as UTC offset.  If invalid, throw CannotParseException.
054 * 
055 * vCard 3.0, hCard:
056 * VALUE=text:                  Treat as text
057 * No VALUE param:              Parse as UTC offset.  If invalid, add warning and treat as text.
058 * 
059 * vCard 4.0, jCard:
060 * VALUE=text:                  Treat as text
061 * VALUE=utc-offset:    Parse as UTC offset.  If invalid, throw CannotParseException
062 * VALUE=uri:                   Not going to support this, as there is no description of what a timezone URI looks like
063 * No VALUE param:              Parse as UTC offset.  If invalid, treat as text
064 * 
065 * xCard:
066 * text | utc-offset    | result
067 * no   | no                    | throw CannotParseException
068 * yes  | no                    | OK
069 * no   | yes                   | OK
070 * no   | invalid               | throw CannotParseException
071 * yes  | yes                   | Parse text
072 * yes  | invalid               | Parse text
073 * 
074 * Writing===================
075 * 
076 * vCard 2.1:
077 * text | utc-offset    | result
078 * no   | no                    | empty string (validation warning)
079 * no   | yes                   | Write UTC offset
080 * yes  | no                    | empty string (validation warning)
081 * yes  | yes                   | Write UTC offset
082 * 
083 * vCard 3.0:
084 * text | utc-offset    | result
085 * no   | no                    | empty string (validation warning)
086 * no   | yes                   | Write UTC offset
087 * yes  | no                    | Write text, add "VALUE=text" parameter
088 * yes  | yes                   | Write UTC offset
089 * 
090 * vCard 4.0, xCard, jCard:
091 * text | utc-offset    | result
092 * no   | no                    | empty string (validation warning)
093 * no   | yes                   | Write UTC offset, add "VALUE=utc-offset" parameter
094 * yes  | no                    | Write text
095 * yes  | yes                   | Write text
096 */
097//@formatter:on
098public class TimezoneScribe extends VCardPropertyScribe<Timezone> {
099        public TimezoneScribe() {
100                super(Timezone.class, "TZ");
101        }
102
103        @Override
104        protected VCardDataType _defaultDataType(VCardVersion version) {
105                switch (version) {
106                case V2_1:
107                case V3_0:
108                        return VCardDataType.UTC_OFFSET;
109                case V4_0:
110                        return VCardDataType.TEXT;
111                }
112                return null;
113        }
114
115        @Override
116        protected VCardDataType _dataType(Timezone property, VCardVersion version) {
117                String text = property.getText();
118                UtcOffset offset = property.getOffset();
119
120                switch (version) {
121                case V2_1:
122                        return VCardDataType.UTC_OFFSET;
123                case V3_0:
124                        if (offset != null) {
125                                return VCardDataType.UTC_OFFSET;
126                        }
127                        if (text != null) {
128                                return VCardDataType.TEXT;
129                        }
130                        break;
131                case V4_0:
132                        if (text != null) {
133                                return VCardDataType.TEXT;
134                        }
135                        if (offset != null) {
136                                return VCardDataType.UTC_OFFSET;
137                        }
138                        break;
139                }
140
141                return _defaultDataType(version);
142        }
143
144        @Override
145        protected String _writeText(Timezone property, WriteContext context) {
146                String text = property.getText();
147                UtcOffset offset = property.getOffset();
148
149                switch (context.getVersion()) {
150                case V2_1:
151                        if (offset != null) {
152                                return offset.toString(false); //2.1 allows either basic or extended
153                        }
154
155                        if (text != null) {
156                                //attempt to find the offset by treating the text as a timezone ID, like "America/New_York"
157                                TimeZone timezone = timezoneFromId(text);
158                                if (timezone != null) {
159                                        UtcOffset tzOffset = offsetFromTimezone(timezone);
160                                        return tzOffset.toString(false);
161                                }
162                        }
163                        break;
164                case V3_0:
165                        if (offset != null) {
166                                return offset.toString(true); //3.0 only allows extended
167                        }
168
169                        if (text != null) {
170                                return VObjectPropertyValues.escape(text);
171                        }
172                        break;
173                case V4_0:
174                        if (text != null) {
175                                return VObjectPropertyValues.escape(text);
176                        }
177
178                        if (offset != null) {
179                                return offset.toString(false); //4.0 only allows basic
180                        }
181                        break;
182                }
183
184                return "";
185        }
186
187        @Override
188        protected Timezone _parseText(String value, VCardDataType dataType, VCardParameters parameters, ParseContext context) {
189                value = VObjectPropertyValues.unescape(value);
190                return parse(value, dataType, context);
191        }
192
193        @Override
194        protected void _writeXml(Timezone property, XCardElement parent) {
195                String text = property.getText();
196                if (text != null) {
197                        parent.append(VCardDataType.TEXT, text);
198                        return;
199                }
200
201                UtcOffset offset = property.getOffset();
202                if (offset != null) {
203                        parent.append(VCardDataType.UTC_OFFSET, offset.toString(false));
204                        return;
205                }
206
207                parent.append(VCardDataType.TEXT, "");
208        }
209
210        @Override
211        protected Timezone _parseXml(XCardElement element, VCardParameters parameters, ParseContext context) {
212                String text = element.first(VCardDataType.TEXT);
213                if (text != null) {
214                        return new Timezone(text);
215                }
216
217                String utcOffset = element.first(VCardDataType.UTC_OFFSET);
218                if (utcOffset != null) {
219                        try {
220                                return new Timezone(UtcOffset.parse(utcOffset));
221                        } catch (IllegalArgumentException e) {
222                                throw new CannotParseException(19);
223                        }
224                }
225
226                throw missingXmlElements(VCardDataType.TEXT, VCardDataType.UTC_OFFSET);
227        }
228
229        @Override
230        protected Timezone _parseHtml(HCardElement element, ParseContext context) {
231                return parse(element.value(), null, context);
232        }
233
234        @Override
235        protected JCardValue _writeJson(Timezone property) {
236                String text = property.getText();
237                if (text != null) {
238                        return JCardValue.single(text);
239                }
240
241                UtcOffset offset = property.getOffset();
242                if (offset != null) {
243                        return JCardValue.single(offset.toString(true));
244                }
245
246                return JCardValue.single("");
247        }
248
249        @Override
250        protected Timezone _parseJson(JCardValue value, VCardDataType dataType, VCardParameters parameters, ParseContext context) {
251                String valueStr = value.asSingle();
252                return parse(valueStr, dataType, context);
253        }
254
255        private Timezone parse(String value, VCardDataType dataType, ParseContext context) {
256                if (value == null || value.length() == 0) {
257                        return new Timezone((String) null);
258                }
259
260                switch (context.getVersion()) {
261                case V2_1:
262                        //e.g. "-05:00"
263                        try {
264                                return new Timezone(UtcOffset.parse(value));
265                        } catch (IllegalArgumentException e) {
266                                throw new CannotParseException(19);
267                        }
268                case V3_0:
269                case V4_0:
270                        try {
271                                return new Timezone(UtcOffset.parse(value));
272                        } catch (IllegalArgumentException e) {
273                                if (dataType == VCardDataType.UTC_OFFSET) {
274                                        context.addWarning(20);
275                                }
276                                return new Timezone(value);
277                        }
278                }
279
280                return new Timezone((String) null);
281        }
282
283        private UtcOffset offsetFromTimezone(TimeZone timezone) {
284                long offsetMs = timezone.getOffset(System.currentTimeMillis());
285                return new UtcOffset(offsetMs);
286        }
287
288        private TimeZone timezoneFromId(String id) {
289                TimeZone timezone = TimeZone.getTimeZone(id);
290                return "GMT".equals(timezone.getID()) ? null : timezone;
291        }
292}