001    /*
002     *  Copyright 2001-2009 Stephen Colebourne
003     *
004     *  Licensed under the Apache License, Version 2.0 (the "License");
005     *  you may not use this file except in compliance with the License.
006     *  You may obtain a copy of the License at
007     *
008     *      http://www.apache.org/licenses/LICENSE-2.0
009     *
010     *  Unless required by applicable law or agreed to in writing, software
011     *  distributed under the License is distributed on an "AS IS" BASIS,
012     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     *  See the License for the specific language governing permissions and
014     *  limitations under the License.
015     */
016    package org.joda.time.chrono;
017    
018    import java.util.HashMap;
019    import java.util.Locale;
020    
021    import org.joda.time.Chronology;
022    import org.joda.time.DateTimeConstants;
023    import org.joda.time.DateTimeField;
024    import org.joda.time.DateTimeZone;
025    import org.joda.time.DurationField;
026    import org.joda.time.IllegalFieldValueException;
027    import org.joda.time.Instant;
028    import org.joda.time.ReadablePartial;
029    import org.joda.time.field.BaseDateTimeField;
030    import org.joda.time.field.BaseDurationField;
031    import org.joda.time.format.DateTimeFormat;
032    
033    /**
034     * Wraps another Chronology to add support for time zones.
035     * <p>
036     * ZonedChronology is thread-safe and immutable.
037     *
038     * @author Brian S O'Neill
039     * @author Stephen Colebourne
040     * @since 1.0
041     */
042    public final class ZonedChronology extends AssembledChronology {
043    
044        /** Serialization lock */
045        private static final long serialVersionUID = -1079258847191166848L;
046    
047        /**
048         * Create a ZonedChronology for any chronology, overriding any time zone it
049         * may already have.
050         *
051         * @param base base chronology to wrap
052         * @param zone the time zone
053         * @throws IllegalArgumentException if chronology or time zone is null
054         */
055        public static ZonedChronology getInstance(Chronology base, DateTimeZone zone) {
056            if (base == null) {
057                throw new IllegalArgumentException("Must supply a chronology");
058            }
059            base = base.withUTC();
060            if (base == null) {
061                throw new IllegalArgumentException("UTC chronology must not be null");
062            }
063            if (zone == null) {
064                throw new IllegalArgumentException("DateTimeZone must not be null");
065            }
066            return new ZonedChronology(base, zone);
067        }
068    
069        static boolean useTimeArithmetic(DurationField field) {
070            // Use time of day arithmetic rules for unit durations less than
071            // typical time zone offsets.
072            return field != null && field.getUnitMillis() < DateTimeConstants.MILLIS_PER_HOUR * 12;
073        }
074    
075        /**
076         * Restricted constructor
077         *
078         * @param base base chronology to wrap
079         * @param zone the time zone
080         */
081        private ZonedChronology(Chronology base, DateTimeZone zone) {
082            super(base, zone);
083        }
084    
085        public DateTimeZone getZone() {
086            return (DateTimeZone)getParam();
087        }
088    
089        public Chronology withUTC() {
090            return getBase();
091        }
092    
093        public Chronology withZone(DateTimeZone zone) {
094            if (zone == null) {
095                zone = DateTimeZone.getDefault();
096            }
097            if (zone == getParam()) {
098                return this;
099            }
100            if (zone == DateTimeZone.UTC) {
101                return getBase();
102            }
103            return new ZonedChronology(getBase(), zone);
104        }
105    
106        public long getDateTimeMillis(int year, int monthOfYear, int dayOfMonth,
107                                      int millisOfDay)
108            throws IllegalArgumentException
109        {
110            return localToUTC(getBase().getDateTimeMillis
111                              (year, monthOfYear, dayOfMonth, millisOfDay));
112        }
113    
114        public long getDateTimeMillis(int year, int monthOfYear, int dayOfMonth,
115                                      int hourOfDay, int minuteOfHour,
116                                      int secondOfMinute, int millisOfSecond)
117            throws IllegalArgumentException
118        {
119            return localToUTC(getBase().getDateTimeMillis
120                              (year, monthOfYear, dayOfMonth, 
121                               hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond));
122        }
123    
124        public long getDateTimeMillis(long instant,
125                                      int hourOfDay, int minuteOfHour,
126                                      int secondOfMinute, int millisOfSecond)
127            throws IllegalArgumentException
128        {
129            return localToUTC(getBase().getDateTimeMillis
130                              (instant + getZone().getOffset(instant),
131                               hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond));
132        }
133    
134        /**
135         * @param instant instant from 1970-01-01T00:00:00 local time
136         * @return instant from 1970-01-01T00:00:00Z
137         */
138        private long localToUTC(long instant) {
139            DateTimeZone zone = getZone();
140            int offset = zone.getOffsetFromLocal(instant);
141            instant -= offset;
142            if (offset != zone.getOffset(instant)) {
143                throw new IllegalArgumentException
144                    ("Illegal instant due to time zone offset transition: " +
145                        DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS").print(new Instant(instant)));
146            }
147            return instant;
148        }
149    
150        protected void assemble(Fields fields) {
151            // Keep a local cache of converted fields so as not to create redundant
152            // objects.
153            HashMap<Object, Object> converted = new HashMap<Object, Object>();
154    
155            // Convert duration fields...
156    
157            fields.eras = convertField(fields.eras, converted);
158            fields.centuries = convertField(fields.centuries, converted);
159            fields.years = convertField(fields.years, converted);
160            fields.months = convertField(fields.months, converted);
161            fields.weekyears = convertField(fields.weekyears, converted);
162            fields.weeks = convertField(fields.weeks, converted);
163            fields.days = convertField(fields.days, converted);
164    
165            fields.halfdays = convertField(fields.halfdays, converted);
166            fields.hours = convertField(fields.hours, converted);
167            fields.minutes = convertField(fields.minutes, converted);
168            fields.seconds = convertField(fields.seconds, converted);
169            fields.millis = convertField(fields.millis, converted);
170    
171            // Convert datetime fields...
172    
173            fields.year = convertField(fields.year, converted);
174            fields.yearOfEra = convertField(fields.yearOfEra, converted);
175            fields.yearOfCentury = convertField(fields.yearOfCentury, converted);
176            fields.centuryOfEra = convertField(fields.centuryOfEra, converted);
177            fields.era = convertField(fields.era, converted);
178            fields.dayOfWeek = convertField(fields.dayOfWeek, converted);
179            fields.dayOfMonth = convertField(fields.dayOfMonth, converted);
180            fields.dayOfYear = convertField(fields.dayOfYear, converted);
181            fields.monthOfYear = convertField(fields.monthOfYear, converted);
182            fields.weekOfWeekyear = convertField(fields.weekOfWeekyear, converted);
183            fields.weekyear = convertField(fields.weekyear, converted);
184            fields.weekyearOfCentury = convertField(fields.weekyearOfCentury, converted);
185    
186            fields.millisOfSecond = convertField(fields.millisOfSecond, converted);
187            fields.millisOfDay = convertField(fields.millisOfDay, converted);
188            fields.secondOfMinute = convertField(fields.secondOfMinute, converted);
189            fields.secondOfDay = convertField(fields.secondOfDay, converted);
190            fields.minuteOfHour = convertField(fields.minuteOfHour, converted);
191            fields.minuteOfDay = convertField(fields.minuteOfDay, converted);
192            fields.hourOfDay = convertField(fields.hourOfDay, converted);
193            fields.hourOfHalfday = convertField(fields.hourOfHalfday, converted);
194            fields.clockhourOfDay = convertField(fields.clockhourOfDay, converted);
195            fields.clockhourOfHalfday = convertField(fields.clockhourOfHalfday, converted);
196            fields.halfdayOfDay = convertField(fields.halfdayOfDay, converted);
197        }
198    
199        private DurationField convertField(DurationField field, HashMap<Object, Object> converted) {
200            if (field == null || !field.isSupported()) {
201                return field;
202            }
203            if (converted.containsKey(field)) {
204                return (DurationField)converted.get(field);
205            }
206            ZonedDurationField zonedField = new ZonedDurationField(field, getZone());
207            converted.put(field, zonedField);
208            return zonedField;
209        }
210    
211        private DateTimeField convertField(DateTimeField field, HashMap<Object, Object> converted) {
212            if (field == null || !field.isSupported()) {
213                return field;
214            }
215            if (converted.containsKey(field)) {
216                return (DateTimeField)converted.get(field);
217            }
218            ZonedDateTimeField zonedField =
219                new ZonedDateTimeField(field, getZone(),
220                                       convertField(field.getDurationField(), converted),
221                                       convertField(field.getRangeDurationField(), converted),
222                                       convertField(field.getLeapDurationField(), converted));
223            converted.put(field, zonedField);
224            return zonedField;
225        }
226    
227        //-----------------------------------------------------------------------
228        /**
229         * A zoned chronology is only equal to a zoned chronology with the
230         * same base chronology and zone.
231         * 
232         * @param obj  the object to compare to
233         * @return true if equal
234         * @since 1.4
235         */
236        public boolean equals(Object obj) {
237            if (this == obj) {
238                return true;
239            }
240            if (obj instanceof ZonedChronology == false) {
241                return false;
242            }
243            ZonedChronology chrono = (ZonedChronology) obj;
244            return
245                getBase().equals(chrono.getBase()) &&
246                getZone().equals(chrono.getZone());
247        }
248    
249        /**
250         * A suitable hashcode for the chronology.
251         * 
252         * @return the hashcode
253         * @since 1.4
254         */
255        public int hashCode() {
256            return 326565 + getZone().hashCode() * 11 + getBase().hashCode() * 7;
257        }
258    
259        /**
260         * A debugging string for the chronology.
261         * 
262         * @return the debugging string
263         */
264        public String toString() {
265            return "ZonedChronology[" + getBase() + ", " + getZone().getID() + ']';
266        }
267    
268        //-----------------------------------------------------------------------
269        /*
270         * Because time durations are typically smaller than time zone offsets, the
271         * arithmetic methods subtract the original offset. This produces a more
272         * expected behavior when crossing time zone offset transitions. For dates,
273         * the new offset is subtracted off. This behavior, if applied to time
274         * fields, can nullify or reverse an add when crossing a transition.
275         */
276        static class ZonedDurationField extends BaseDurationField {
277            private static final long serialVersionUID = -485345310999208286L;
278    
279            final DurationField iField;
280            final boolean iTimeField;
281            final DateTimeZone iZone;
282    
283            ZonedDurationField(DurationField field, DateTimeZone zone) {
284                super(field.getType());
285                if (!field.isSupported()) {
286                    throw new IllegalArgumentException();
287                }
288                iField = field;
289                iTimeField = useTimeArithmetic(field);
290                iZone = zone;
291            }
292    
293            public boolean isPrecise() {
294                return iTimeField ? iField.isPrecise() : iField.isPrecise() && this.iZone.isFixed();
295            }
296    
297            public long getUnitMillis() {
298                return iField.getUnitMillis();
299            }
300    
301            public int getValue(long duration, long instant) {
302                return iField.getValue(duration, addOffset(instant));
303            }
304    
305            public long getValueAsLong(long duration, long instant) {
306                return iField.getValueAsLong(duration, addOffset(instant));
307            }
308    
309            public long getMillis(int value, long instant) {
310                return iField.getMillis(value, addOffset(instant));
311            }
312    
313            public long getMillis(long value, long instant) {
314                return iField.getMillis(value, addOffset(instant));
315            }
316    
317            public long add(long instant, int value) {
318                int offset = getOffsetToAdd(instant);
319                instant = iField.add(instant + offset, value);
320                return instant - (iTimeField ? offset : getOffsetFromLocalToSubtract(instant));
321            }
322    
323            public long add(long instant, long value) {
324                int offset = getOffsetToAdd(instant);
325                instant = iField.add(instant + offset, value);
326                return instant - (iTimeField ? offset : getOffsetFromLocalToSubtract(instant));
327            }
328    
329            public int getDifference(long minuendInstant, long subtrahendInstant) {
330                int offset = getOffsetToAdd(subtrahendInstant);
331                return iField.getDifference
332                    (minuendInstant + (iTimeField ? offset : getOffsetToAdd(minuendInstant)),
333                     subtrahendInstant + offset);
334            }
335    
336            public long getDifferenceAsLong(long minuendInstant, long subtrahendInstant) {
337                int offset = getOffsetToAdd(subtrahendInstant);
338                return iField.getDifferenceAsLong
339                    (minuendInstant + (iTimeField ? offset : getOffsetToAdd(minuendInstant)),
340                     subtrahendInstant + offset);
341            }
342    
343            private int getOffsetToAdd(long instant) {
344                int offset = this.iZone.getOffset(instant);
345                long sum = instant + offset;
346                // If there is a sign change, but the two values have the same sign...
347                if ((instant ^ sum) < 0 && (instant ^ offset) >= 0) {
348                    throw new ArithmeticException("Adding time zone offset caused overflow");
349                }
350                return offset;
351            }
352    
353            private int getOffsetFromLocalToSubtract(long instant) {
354                int offset = this.iZone.getOffsetFromLocal(instant);
355                long diff = instant - offset;
356                // If there is a sign change, but the two values have different signs...
357                if ((instant ^ diff) < 0 && (instant ^ offset) < 0) {
358                    throw new ArithmeticException("Subtracting time zone offset caused overflow");
359                }
360                return offset;
361            }
362    
363            private long addOffset(long instant) {
364                return iZone.convertUTCToLocal(instant);
365            }
366        }
367    
368        /**
369         * A DateTimeField that decorates another to add timezone behaviour.
370         * <p>
371         * This class converts passed in instants to local wall time, and vice
372         * versa on output.
373         */
374        static final class ZonedDateTimeField extends BaseDateTimeField {
375            private static final long serialVersionUID = -3968986277775529794L;
376    
377            final DateTimeField iField;
378            final DateTimeZone iZone;
379            final DurationField iDurationField;
380            final boolean iTimeField;
381            final DurationField iRangeDurationField;
382            final DurationField iLeapDurationField;
383    
384            ZonedDateTimeField(DateTimeField field,
385                               DateTimeZone zone,
386                               DurationField durationField,
387                               DurationField rangeDurationField,
388                               DurationField leapDurationField) {
389                super(field.getType());
390                if (!field.isSupported()) {
391                    throw new IllegalArgumentException();
392                }
393                iField = field;
394                iZone = zone;
395                iDurationField = durationField;
396                iTimeField = useTimeArithmetic(durationField);
397                iRangeDurationField = rangeDurationField;
398                iLeapDurationField = leapDurationField;
399            }
400    
401            public boolean isLenient() {
402                return iField.isLenient();
403            }
404    
405            public int get(long instant) {
406                long localInstant = iZone.convertUTCToLocal(instant);
407                return iField.get(localInstant);
408            }
409    
410            public String getAsText(long instant, Locale locale) {
411                long localInstant = iZone.convertUTCToLocal(instant);
412                return iField.getAsText(localInstant, locale);
413            }
414    
415            public String getAsShortText(long instant, Locale locale) {
416                long localInstant = iZone.convertUTCToLocal(instant);
417                return iField.getAsShortText(localInstant, locale);
418            }
419    
420            public String getAsText(int fieldValue, Locale locale) {
421                return iField.getAsText(fieldValue, locale);
422            }
423    
424            public String getAsShortText(int fieldValue, Locale locale) {
425                return iField.getAsShortText(fieldValue, locale);
426            }
427    
428            public long add(long instant, int value) {
429                if (iTimeField) {
430                    int offset = getOffsetToAdd(instant);
431                    long localInstant = iField.add(instant + offset, value);
432                    return localInstant - offset;
433                } else {
434                   long localInstant = iZone.convertUTCToLocal(instant);
435                   localInstant = iField.add(localInstant, value);
436                   return iZone.convertLocalToUTC(localInstant, false, instant);
437                }
438            }
439    
440            public long add(long instant, long value) {
441                if (iTimeField) {
442                    int offset = getOffsetToAdd(instant);
443                    long localInstant = iField.add(instant + offset, value);
444                    return localInstant - offset;
445                } else {
446                   long localInstant = iZone.convertUTCToLocal(instant);
447                   localInstant = iField.add(localInstant, value);
448                   return iZone.convertLocalToUTC(localInstant, false, instant);
449                }
450            }
451    
452            public long addWrapField(long instant, int value) {
453                if (iTimeField) {
454                    int offset = getOffsetToAdd(instant);
455                    long localInstant = iField.addWrapField(instant + offset, value);
456                    return localInstant - offset;
457                } else {
458                    long localInstant = iZone.convertUTCToLocal(instant);
459                    localInstant = iField.addWrapField(localInstant, value);
460                    return iZone.convertLocalToUTC(localInstant, false, instant);
461                }
462            }
463    
464            public long set(long instant, int value) {
465                long localInstant = iZone.convertUTCToLocal(instant);
466                localInstant = iField.set(localInstant, value);
467                long result = iZone.convertLocalToUTC(localInstant, false, instant);
468                if (get(result) != value) {
469                    throw new IllegalFieldValueException(iField.getType(), Integer.valueOf(value),
470                        "Illegal instant due to time zone offset transition: " +
471                        DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS").print(new Instant(localInstant)) +
472                        " (" + iZone.getID() + ")");
473                }
474                return result;
475            }
476    
477            public long set(long instant, String text, Locale locale) {
478                // cannot verify that new value stuck because set may be lenient
479                long localInstant = iZone.convertUTCToLocal(instant);
480                localInstant = iField.set(localInstant, text, locale);
481                return iZone.convertLocalToUTC(localInstant, false, instant);
482            }
483    
484            public int getDifference(long minuendInstant, long subtrahendInstant) {
485                int offset = getOffsetToAdd(subtrahendInstant);
486                return iField.getDifference
487                    (minuendInstant + (iTimeField ? offset : getOffsetToAdd(minuendInstant)),
488                     subtrahendInstant + offset);
489            }
490    
491            public long getDifferenceAsLong(long minuendInstant, long subtrahendInstant) {
492                int offset = getOffsetToAdd(subtrahendInstant);
493                return iField.getDifferenceAsLong
494                    (minuendInstant + (iTimeField ? offset : getOffsetToAdd(minuendInstant)),
495                     subtrahendInstant + offset);
496            }
497    
498            public final DurationField getDurationField() {
499                return iDurationField;
500            }
501    
502            public final DurationField getRangeDurationField() {
503                return iRangeDurationField;
504            }
505    
506            public boolean isLeap(long instant) {
507                long localInstant = iZone.convertUTCToLocal(instant);
508                return iField.isLeap(localInstant);
509            }
510    
511            public int getLeapAmount(long instant) {
512                long localInstant = iZone.convertUTCToLocal(instant);
513                return iField.getLeapAmount(localInstant);
514            }
515    
516            public final DurationField getLeapDurationField() {
517                return iLeapDurationField;
518            }
519    
520            public long roundFloor(long instant) {
521                if (iTimeField) {
522                    int offset = getOffsetToAdd(instant);
523                    instant = iField.roundFloor(instant + offset);
524                    return instant - offset;
525                } else {
526                    long localInstant = iZone.convertUTCToLocal(instant);
527                    localInstant = iField.roundFloor(localInstant);
528                    return iZone.convertLocalToUTC(localInstant, false, instant);
529                }
530            }
531    
532            public long roundCeiling(long instant) {
533                if (iTimeField) {
534                    int offset = getOffsetToAdd(instant);
535                    instant = iField.roundCeiling(instant + offset);
536                    return instant - offset;
537                } else {
538                    long localInstant = iZone.convertUTCToLocal(instant);
539                    localInstant = iField.roundCeiling(localInstant);
540                    return iZone.convertLocalToUTC(localInstant, false, instant);
541                }
542            }
543    
544            public long remainder(long instant) {
545                long localInstant = iZone.convertUTCToLocal(instant);
546                return iField.remainder(localInstant);
547            }
548    
549            public int getMinimumValue() {
550                return iField.getMinimumValue();
551            }
552    
553            public int getMinimumValue(long instant) {
554                long localInstant = iZone.convertUTCToLocal(instant);
555                return iField.getMinimumValue(localInstant);
556            }
557    
558            public int getMinimumValue(ReadablePartial instant) {
559                return iField.getMinimumValue(instant);
560            }
561    
562            public int getMinimumValue(ReadablePartial instant, int[] values) {
563                return iField.getMinimumValue(instant, values);
564            }
565    
566            public int getMaximumValue() {
567                return iField.getMaximumValue();
568            }
569    
570            public int getMaximumValue(long instant) {
571                long localInstant = iZone.convertUTCToLocal(instant);
572                return iField.getMaximumValue(localInstant);
573            }
574    
575            public int getMaximumValue(ReadablePartial instant) {
576                return iField.getMaximumValue(instant);
577            }
578    
579            public int getMaximumValue(ReadablePartial instant, int[] values) {
580                return iField.getMaximumValue(instant, values);
581            }
582    
583            public int getMaximumTextLength(Locale locale) {
584                return iField.getMaximumTextLength(locale);
585            }
586    
587            public int getMaximumShortTextLength(Locale locale) {
588                return iField.getMaximumShortTextLength(locale);
589            }
590    
591            private int getOffsetToAdd(long instant) {
592                int offset = this.iZone.getOffset(instant);
593                long sum = instant + offset;
594                // If there is a sign change, but the two values have the same sign...
595                if ((instant ^ sum) < 0 && (instant ^ offset) >= 0) {
596                    throw new ArithmeticException("Adding time zone offset caused overflow");
597                }
598                return offset;
599            }
600        }
601    
602    }