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}