001package biweekly.io;
002
003import java.util.Collection;
004import java.util.HashMap;
005import java.util.HashSet;
006import java.util.Map;
007import java.util.Set;
008import java.util.TimeZone;
009
010import biweekly.component.VTimezone;
011import biweekly.property.ICalProperty;
012import biweekly.property.TimezoneId;
013
014/*
015 Copyright (c) 2013-2015, Michael Angstadt
016 All rights reserved.
017
018 Redistribution and use in source and binary forms, with or without
019 modification, are permitted provided that the following conditions are met: 
020
021 1. Redistributions of source code must retain the above copyright notice, this
022 list of conditions and the following disclaimer. 
023 2. Redistributions in binary form must reproduce the above copyright notice,
024 this list of conditions and the following disclaimer in the documentation
025 and/or other materials provided with the distribution. 
026
027 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
028 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
029 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
030 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
031 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
032 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
033 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
034 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
035 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
036 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
037 */
038
039/**
040 * Holds the timezone-related settings of an iCalendar object.
041 * @author Michael Angstadt
042 */
043public class TimezoneInfo {
044        private final Map<VTimezone, TimeZone> assignments = new HashMap<VTimezone, TimeZone>();
045        private final Map<TimeZone, VTimezone> assignmentsReverse = new HashMap<TimeZone, VTimezone>();
046        private final Map<String, TimeZone> timezonesById = new HashMap<String, TimeZone>();
047
048        private final Map<ICalProperty, TimeZone> propertyTimeZones = new HashMap<ICalProperty, TimeZone>();
049        private final Set<ICalProperty> hasSolidusTimezone = new HashSet<ICalProperty>();
050        private final Set<ICalProperty> floatingProperties = new HashSet<ICalProperty>();
051
052        private VTimezoneGenerator generator = new TzUrlDotOrgGenerator(false);
053
054        private TimeZone defaultTimezone;
055        private boolean globalFloatingTime = false;
056
057        /**
058         * Assigns a user-defined {@link VTimezone} component to its Java
059         * {@link TimeZone} equivalent.
060         * @param component the timezone component
061         * @param timezone the timezone object
062         */
063        public void assign(VTimezone component, TimeZone timezone) {
064                checkForId(component);
065                assignments.put(component, timezone);
066                assignmentsReverse.put(timezone, component);
067                timezonesById.put(component.getTimezoneId().getValue(), timezone);
068        }
069
070        /**
071         * Sets the timezone to format all date/time values in. An attempt will be
072         * made to generate a {@link VTimezone} component if one has not already
073         * been assigned to the given timezone (see
074         * {@link #assign(VTimezone, TimeZone) assign()}).
075         * @param timezone the timezone object or null for UTC (default)
076         * @throws IllegalArgumentException if a {@link VTimezone} component could
077         * not be generated
078         */
079        public void setDefaultTimeZone(TimeZone timezone) {
080                if (timezone == null) {
081                        defaultTimezone = null;
082                        return;
083                }
084
085                VTimezone component = assignmentsReverse.get(timezone);
086                if (component == null) {
087                        component = generator.generate(timezone);
088                        assign(component, timezone);
089                }
090                defaultTimezone = timezone;
091        }
092
093        /**
094         * Sets whether to format all date/time values as floating times. A floating
095         * time value does not have a timezone associated with it, and is to be
096         * interpreted as being in the local timezone of the parsing computer.
097         * @param enable true to enable, false to disable (default)
098         */
099        public void setGlobalFloatingTime(boolean enable) {
100                globalFloatingTime = enable;
101        }
102
103        /**
104         * Instructs the writer to format an individual property's date/time value
105         * in a specific timezone.
106         * @param property the property
107         * @param timezone the timezone or null to format the property according to
108         * the default timezone (default)
109         */
110        public void setTimeZone(ICalProperty property, TimeZone timezone) {
111                setTimeZone(property, timezone, true);
112        }
113
114        /**
115         * Instructs the writer to format an individual property's date/time value
116         * in a specific timezone.
117         * @param property the property
118         * @param timezone the timezone or null to format the property according to
119         * the default timezone (default)
120         * @param generateComponent true to associate the property with a
121         * {@link VTimezone} component containing the timezone definition
122         * (recommended), false not to. If the "timezone" parameter is null, then
123         * this parameter has no effect
124         */
125        public void setTimeZone(ICalProperty property, TimeZone timezone, boolean generateComponent) {
126                if (timezone == null) {
127                        propertyTimeZones.remove(property);
128                        hasSolidusTimezone.remove(property);
129                        return;
130                }
131
132                if (generateComponent) {
133                        VTimezone component = assignmentsReverse.get(timezone);
134                        if (component == null) {
135                                component = generator.generate(timezone);
136                                assign(component, timezone);
137                        }
138                } else {
139                        hasSolidusTimezone.add(property);
140                }
141
142                propertyTimeZones.put(property, timezone);
143        }
144
145        /* package */void setTimeZoneReader(ICalProperty property, TimeZone timezone, boolean solidus) {
146                if (solidus) {
147                        hasSolidusTimezone.add(property);
148                }
149                propertyTimeZones.put(property, timezone);
150        }
151
152        /**
153         * Gets the timezone that is assigned to a property.
154         * @param property the property
155         * @return the timezone or null if no timezone is assigned to the property
156         */
157        public TimeZone getTimeZone(ICalProperty property) {
158                return propertyTimeZones.get(property);
159        }
160
161        /**
162         * Determines if the given property has a solidus timezone (a globally
163         * unique timezone ID).
164         * @param property the property
165         * @return true if the property has a solidus timezone, false if not
166         */
167        public boolean hasSolidusTimezone(ICalProperty property) {
168                return hasSolidusTimezone.contains(property);
169        }
170
171        /**
172         * Gets the timezone that a property should be formatted in when written.
173         * You should call {@link #isFloating} first, to check to see if the
174         * property's value is floating (without a timezone).
175         * @param property the property
176         * @return the timezone or null for UTC
177         */
178        public TimeZone getTimeZoneToWriteIn(ICalProperty property) {
179                TimeZone timezone = getTimeZone(property);
180                return (timezone == null) ? defaultTimezone : timezone;
181        }
182
183        /**
184         * Gets a timezone with a given ID.
185         * @param id the ID
186         * @return the timezone or null if not found
187         */
188        public TimeZone getTimeZoneById(String id) {
189                return timezonesById.get(id);
190        }
191
192        /**
193         * Gets the {@link TimeZone} object that is assigned to a {@link VTimezone}
194         * component.
195         * @param component the component
196         * @return the timezone object or null if none were found
197         */
198        public TimeZone getTimeZoneByComponent(VTimezone component) {
199                return assignments.get(component);
200        }
201
202        /**
203         * Gets the {@link VTimezone} component that a property is assigned to.
204         * @param property the property
205         * @return the component or null if it is not assigned to one
206         */
207        public VTimezone getComponent(ICalProperty property) {
208                if (hasSolidusTimezone.contains(property)) {
209                        return null;
210                }
211
212                TimeZone timezone = getTimeZone(property);
213                return assignmentsReverse.get(timezone);
214        }
215
216        /**
217         * Instructs the writer to format a particular property's date/time value in
218         * floating time. A floating time value does not have a timezone associated
219         * with it, and is to be interpreted as being in the local timezone of the
220         * parsing computer.
221         * @param property the property whose value should be formatted as a
222         * floating time value
223         * @param enable true to enable floating time for this property, false to
224         * disable (default)
225         */
226        public void setFloating(ICalProperty property, boolean enable) {
227                if (enable) {
228                        floatingProperties.add(property);
229                } else {
230                        floatingProperties.remove(property);
231                }
232        }
233
234        /**
235         * Determines if a property value should be formatted in floating time or
236         * not.
237         * @param property the property
238         * @return true to format in floating time, false not to
239         */
240        public boolean isFloating(ICalProperty property) {
241                if (floatingProperties.contains(property)) {
242                        return true;
243                }
244
245                if (propertyTimeZones.containsKey(property)) {
246                        return false;
247                }
248
249                return globalFloatingTime;
250        }
251
252        /**
253         * Gets all of the iCalendar {@link VTimezone} components that have been
254         * registered or generated by this class.
255         * @return the timezone components
256         */
257        public Collection<VTimezone> getComponents() {
258                return assignments.keySet();
259        }
260
261        /**
262         * Gets the timezone generator.
263         * @return the timezone generator
264         */
265        public VTimezoneGenerator getGenerator() {
266                return generator;
267        }
268
269        /**
270         * Sets the timezone generator.
271         * @param generator the timezone generator
272         */
273        public void setGenerator(VTimezoneGenerator generator) {
274                this.generator = generator;
275        }
276
277        /**
278         * Checks a {@link VTimezone} component to see if it has a valid
279         * {@link TimezoneId} property.
280         * @param timezone the timezone component
281         * @throws IllegalArgumentException if the component does not have a valid
282         * {@link TimezoneId} property
283         */
284        private void checkForId(VTimezone timezone) {
285                TimezoneId id = timezone.getTimezoneId();
286                if (id == null || id.getValue() == null || id.getValue().trim().length() == 0) {
287                        throw new IllegalArgumentException("VTimezone component must have a non-empty TimezoneId property");
288                }
289        }
290}