001package ezvcard.io.scribe;
002
003import java.time.temporal.Temporal;
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.DateOrTimeProperty;
017import ezvcard.util.PartialDate;
018import ezvcard.util.VCardDateFormat;
019
020/*
021 Copyright (c) 2012-2026, 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
045/**
046 * Marshals properties with date-time values.
047 * @author Michael Angstadt
048 * @param <T> the property class
049 */
050public abstract class DateOrTimePropertyScribe<T extends DateOrTimeProperty> extends VCardPropertyScribe<T> {
051        protected DateOrTimePropertyScribe(Class<T> clazz, String propertyName) {
052                super(clazz, propertyName);
053        }
054
055        @Override
056        protected VCardDataType _defaultDataType(VCardVersion version) {
057                switch (version) {
058                case V2_1:
059                case V3_0:
060                        return null;
061                case V4_0:
062                        return VCardDataType.DATE_AND_OR_TIME;
063                }
064                return null;
065        }
066
067        @Override
068        protected VCardDataType _dataType(T property, VCardVersion version) {
069                switch (version) {
070                case V2_1:
071                case V3_0:
072                        return null;
073                case V4_0:
074                        if (property.getText() != null) {
075                                return VCardDataType.TEXT;
076                        }
077                        if (property.getDate() != null) {
078                                return VCardDateFormat.hasTime(property.getDate()) ? VCardDataType.DATE_TIME : VCardDataType.DATE;
079                        }
080                        if (property.getPartialDate() != null) {
081                                return property.getPartialDate().hasTimeComponent() ? VCardDataType.DATE_TIME : VCardDataType.DATE;
082                        }
083                        return VCardDataType.DATE_AND_OR_TIME;
084                }
085                return null;
086        }
087
088        @Override
089        protected String _writeText(T property, WriteContext context) {
090                VCardVersion version = context.getVersion();
091                boolean extended = (version == VCardVersion.V3_0);
092
093                Temporal date = property.getDate();
094                if (date != null) {
095                        return date(date).extended(extended).write();
096                }
097
098                /*
099                 * Allow partial dates to be written to non-4.0 vCards for leniency and
100                 * round-tripping
101                 */
102                PartialDate partialDate = property.getPartialDate();
103                if (partialDate != null) {
104                        return partialDate.toISO8601(extended);
105                }
106
107                if (version == VCardVersion.V4_0) {
108                        String text = property.getText();
109                        if (text != null) {
110                                return VObjectPropertyValues.escape(text);
111                        }
112                }
113
114                return "";
115        }
116
117        @Override
118        protected T _parseText(String value, VCardDataType dataType, VCardParameters parameters, ParseContext context) {
119                value = VObjectPropertyValues.unescape(value);
120                if (context.getVersion() == VCardVersion.V4_0 && dataType == VCardDataType.TEXT) {
121                        return newInstance(value);
122                }
123
124                return parse(value, context);
125        }
126
127        @Override
128        protected void _writeXml(T property, XCardElement parent) {
129                Temporal date = property.getDate();
130                if (date != null) {
131                        String value = date(date).extended(false).write();
132                        VCardDataType dataType = VCardDateFormat.hasTime(date) ? VCardDataType.DATE_TIME : VCardDataType.DATE;
133                        parent.append(dataType, value);
134                        return;
135                }
136
137                PartialDate partialDate = property.getPartialDate();
138                if (partialDate != null) {
139                        VCardDataType dataType;
140                        if (partialDate.hasTimeComponent() && partialDate.hasDateComponent()) {
141                                dataType = VCardDataType.DATE_TIME;
142                        } else if (partialDate.hasTimeComponent()) {
143                                dataType = VCardDataType.TIME;
144                        } else if (partialDate.hasDateComponent()) {
145                                dataType = VCardDataType.DATE;
146                        } else {
147                                dataType = VCardDataType.DATE_AND_OR_TIME;
148                        }
149
150                        parent.append(dataType, partialDate.toISO8601(false));
151                        return;
152                }
153
154                String text = property.getText();
155                if (text != null) {
156                        parent.append(VCardDataType.TEXT, text);
157                        return;
158                }
159
160                parent.append(VCardDataType.DATE_AND_OR_TIME, "");
161        }
162
163        @Override
164        protected T _parseXml(XCardElement element, VCardParameters parameters, ParseContext context) {
165                String value = element.first(VCardDataType.DATE, VCardDataType.DATE_TIME, VCardDataType.DATE_AND_OR_TIME);
166                if (value != null) {
167                        return parse(value, context);
168                }
169
170                value = element.first(VCardDataType.TEXT);
171                if (value != null) {
172                        return newInstance(value);
173                }
174
175                throw missingXmlElements(VCardDataType.DATE, VCardDataType.DATE_TIME, VCardDataType.DATE_AND_OR_TIME, VCardDataType.TEXT);
176        }
177
178        @Override
179        protected T _parseHtml(HCardElement element, ParseContext context) {
180                String value = null;
181                if ("time".equals(element.tagName())) {
182                        String datetime = element.attr("datetime");
183                        if (!datetime.isEmpty()) {
184                                value = datetime;
185                        }
186                }
187                if (value == null) {
188                        value = element.value();
189                }
190                return parse(value, context);
191        }
192
193        @Override
194        protected JCardValue _writeJson(T property) {
195                Temporal date = property.getDate();
196                if (date != null) {
197                        String value = date(date).extended(true).write();
198                        return JCardValue.single(value);
199                }
200
201                PartialDate partialDate = property.getPartialDate();
202                if (partialDate != null) {
203                        String value = partialDate.toISO8601(true);
204                        return JCardValue.single(value);
205                }
206
207                String text = property.getText();
208                if (text != null) {
209                        return JCardValue.single(text);
210                }
211
212                return JCardValue.single("");
213        }
214
215        @Override
216        protected T _parseJson(JCardValue value, VCardDataType dataType, VCardParameters parameters, ParseContext context) {
217                String valueStr = value.asSingle();
218                if (dataType == VCardDataType.TEXT) {
219                        return newInstance(valueStr);
220                }
221
222                return parse(valueStr, context);
223        }
224
225        private T parse(String value, ParseContext context) {
226                try {
227                        return newInstance(date(value));
228                } catch (IllegalArgumentException e) {
229                        try {
230                                //allow partial dates to be parsed from non-4.0 vCards
231                                //https://github.com/mangstadt/ez-vcard/issues/155
232                                return newInstance(PartialDate.parse(value));
233                        } catch (IllegalArgumentException e2) {
234                                if (context.getVersion() == VCardVersion.V2_1 || context.getVersion() == VCardVersion.V3_0) {
235                                        throw new CannotParseException(5);
236                                }
237                                context.addWarning(6);
238                                return newInstance(value);
239                        }
240                }
241        }
242
243        protected abstract T newInstance(String text);
244
245        protected abstract T newInstance(Temporal date);
246
247        protected abstract T newInstance(PartialDate partialDate);
248}