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.DateTime;
023    import org.joda.time.DateTimeField;
024    import org.joda.time.DateTimeZone;
025    import org.joda.time.DurationField;
026    import org.joda.time.MutableDateTime;
027    import org.joda.time.ReadableDateTime;
028    import org.joda.time.field.DecoratedDateTimeField;
029    import org.joda.time.field.DecoratedDurationField;
030    import org.joda.time.field.FieldUtils;
031    import org.joda.time.format.DateTimeFormatter;
032    import org.joda.time.format.ISODateTimeFormat;
033    
034    /**
035     * Wraps another Chronology to impose limits on the range of instants that
036     * the fields within a Chronology may support. The limits are applied to both
037     * DateTimeFields and DurationFields.
038     * <p>
039     * Methods in DateTimeField and DurationField throw an IllegalArgumentException
040     * whenever given an input instant that is outside the limits or when an
041     * attempt is made to move an instant outside the limits.
042     * <p>
043     * LimitChronology is thread-safe and immutable.
044     *
045     * @author Brian S O'Neill
046     * @author Stephen Colebourne
047     * @since 1.0
048     */
049    public final class LimitChronology extends AssembledChronology {
050    
051        /** Serialization lock */
052        private static final long serialVersionUID = 7670866536893052522L;
053    
054        /**
055         * Wraps another chronology, with datetime limits. When withUTC or
056         * withZone is called, the returned LimitChronology instance has
057         * the same limits, except they are time zone adjusted.
058         *
059         * @param base  base chronology to wrap
060         * @param lowerLimit  inclusive lower limit, or null if none
061         * @param upperLimit  exclusive upper limit, or null if none
062         * @throws IllegalArgumentException if chronology is null or limits are invalid
063         */
064        public static LimitChronology getInstance(Chronology base,
065                                                  ReadableDateTime lowerLimit,
066                                                  ReadableDateTime upperLimit) {
067            if (base == null) {
068                throw new IllegalArgumentException("Must supply a chronology");
069            }
070    
071            lowerLimit = lowerLimit == null ? null : lowerLimit.toDateTime();
072            upperLimit = upperLimit == null ? null : upperLimit.toDateTime();
073    
074            if (lowerLimit != null && upperLimit != null) {
075                if (!lowerLimit.isBefore(upperLimit)) {
076                    throw new IllegalArgumentException
077                        ("The lower limit must be come before than the upper limit");
078                }
079            }
080    
081            return new LimitChronology(base, (DateTime)lowerLimit, (DateTime)upperLimit);
082        }
083    
084        final DateTime iLowerLimit;
085        final DateTime iUpperLimit;
086    
087        private transient LimitChronology iWithUTC;
088    
089        /**
090         * Wraps another chronology, with datetime limits. When withUTC or
091         * withZone is called, the returned LimitChronology instance has
092         * the same limits, except they are time zone adjusted.
093         *
094         * @param lowerLimit  inclusive lower limit, or null if none
095         * @param upperLimit  exclusive upper limit, or null if none
096         */
097        private LimitChronology(Chronology base,
098                                DateTime lowerLimit, DateTime upperLimit) {
099            super(base, null);
100            // These can be set after assembly.
101            iLowerLimit = lowerLimit;
102            iUpperLimit = upperLimit;
103        }
104    
105        /**
106         * Returns the inclusive lower limit instant.
107         * 
108         * @return lower limit
109         */
110        public DateTime getLowerLimit() {
111            return iLowerLimit;
112        }
113    
114        /**
115         * Returns the inclusive upper limit instant.
116         * 
117         * @return upper limit
118         */
119        public DateTime getUpperLimit() {
120            return iUpperLimit;
121        }
122    
123        /**
124         * If this LimitChronology is already UTC, then this is
125         * returned. Otherwise, a new instance is returned, with the limits
126         * adjusted to the new time zone.
127         */
128        public Chronology withUTC() {
129            return withZone(DateTimeZone.UTC);
130        }
131    
132        /**
133         * If this LimitChronology has the same time zone as the one given, then
134         * this is returned. Otherwise, a new instance is returned, with the limits
135         * adjusted to the new time zone.
136         */
137        public Chronology withZone(DateTimeZone zone) {
138            if (zone == null) {
139                zone = DateTimeZone.getDefault();
140            }
141            if (zone == getZone()) {
142                return this;
143            }
144    
145            if (zone == DateTimeZone.UTC && iWithUTC != null) {
146                return iWithUTC;
147            }
148    
149            DateTime lowerLimit = iLowerLimit;
150            if (lowerLimit != null) {
151                MutableDateTime mdt = lowerLimit.toMutableDateTime();
152                mdt.setZoneRetainFields(zone);
153                lowerLimit = mdt.toDateTime();
154            }
155    
156            DateTime upperLimit = iUpperLimit;
157            if (upperLimit != null) {
158                MutableDateTime mdt = upperLimit.toMutableDateTime();
159                mdt.setZoneRetainFields(zone);
160                upperLimit = mdt.toDateTime();
161            }
162            
163            LimitChronology chrono = getInstance
164                (getBase().withZone(zone), lowerLimit, upperLimit);
165    
166            if (zone == DateTimeZone.UTC) {
167                iWithUTC = chrono;
168            }
169    
170            return chrono;
171        }
172    
173        public long getDateTimeMillis(int year, int monthOfYear, int dayOfMonth,
174                                      int millisOfDay)
175            throws IllegalArgumentException
176        {
177            long instant = getBase().getDateTimeMillis(year, monthOfYear, dayOfMonth, millisOfDay);
178            checkLimits(instant, "resulting");
179            return instant;
180        }
181    
182        public long getDateTimeMillis(int year, int monthOfYear, int dayOfMonth,
183                                      int hourOfDay, int minuteOfHour,
184                                      int secondOfMinute, int millisOfSecond)
185            throws IllegalArgumentException
186        {
187            long instant = getBase().getDateTimeMillis
188                (year, monthOfYear, dayOfMonth,
189                 hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond);
190            checkLimits(instant, "resulting");
191            return instant;
192        }
193    
194        public long getDateTimeMillis(long instant,
195                                      int hourOfDay, int minuteOfHour,
196                                      int secondOfMinute, int millisOfSecond)
197            throws IllegalArgumentException
198        {
199            checkLimits(instant, null);
200            instant = getBase().getDateTimeMillis
201                (instant, hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond);
202            checkLimits(instant, "resulting");
203            return instant;
204        }
205    
206        protected void assemble(Fields fields) {
207            // Keep a local cache of converted fields so as not to create redundant
208            // objects.
209            HashMap<Object, Object> converted = new HashMap<Object, Object>();
210    
211            // Convert duration fields...
212    
213            fields.eras = convertField(fields.eras, converted);
214            fields.centuries = convertField(fields.centuries, converted);
215            fields.years = convertField(fields.years, converted);
216            fields.months = convertField(fields.months, converted);
217            fields.weekyears = convertField(fields.weekyears, converted);
218            fields.weeks = convertField(fields.weeks, converted);
219            fields.days = convertField(fields.days, converted);
220    
221            fields.halfdays = convertField(fields.halfdays, converted);
222            fields.hours = convertField(fields.hours, converted);
223            fields.minutes = convertField(fields.minutes, converted);
224            fields.seconds = convertField(fields.seconds, converted);
225            fields.millis = convertField(fields.millis, converted);
226    
227            // Convert datetime fields...
228    
229            fields.year = convertField(fields.year, converted);
230            fields.yearOfEra = convertField(fields.yearOfEra, converted);
231            fields.yearOfCentury = convertField(fields.yearOfCentury, converted);
232            fields.centuryOfEra = convertField(fields.centuryOfEra, converted);
233            fields.era = convertField(fields.era, converted);
234            fields.dayOfWeek = convertField(fields.dayOfWeek, converted);
235            fields.dayOfMonth = convertField(fields.dayOfMonth, converted);
236            fields.dayOfYear = convertField(fields.dayOfYear, converted);
237            fields.monthOfYear = convertField(fields.monthOfYear, converted);
238            fields.weekOfWeekyear = convertField(fields.weekOfWeekyear, converted);
239            fields.weekyear = convertField(fields.weekyear, converted);
240            fields.weekyearOfCentury = convertField(fields.weekyearOfCentury, converted);
241    
242            fields.millisOfSecond = convertField(fields.millisOfSecond, converted);
243            fields.millisOfDay = convertField(fields.millisOfDay, converted);
244            fields.secondOfMinute = convertField(fields.secondOfMinute, converted);
245            fields.secondOfDay = convertField(fields.secondOfDay, converted);
246            fields.minuteOfHour = convertField(fields.minuteOfHour, converted);
247            fields.minuteOfDay = convertField(fields.minuteOfDay, converted);
248            fields.hourOfDay = convertField(fields.hourOfDay, converted);
249            fields.hourOfHalfday = convertField(fields.hourOfHalfday, converted);
250            fields.clockhourOfDay = convertField(fields.clockhourOfDay, converted);
251            fields.clockhourOfHalfday = convertField(fields.clockhourOfHalfday, converted);
252            fields.halfdayOfDay = convertField(fields.halfdayOfDay, converted);
253        }
254    
255        private DurationField convertField(DurationField field, HashMap<Object, Object> converted) {
256            if (field == null || !field.isSupported()) {
257                return field;
258            }
259            if (converted.containsKey(field)) {
260                return (DurationField)converted.get(field);
261            }
262            LimitDurationField limitField = new LimitDurationField(field);
263            converted.put(field, limitField);
264            return limitField;
265        }
266    
267        private DateTimeField convertField(DateTimeField field, HashMap<Object, Object> converted) {
268            if (field == null || !field.isSupported()) {
269                return field;
270            }
271            if (converted.containsKey(field)) {
272                return (DateTimeField)converted.get(field);
273            }
274            LimitDateTimeField limitField =
275                new LimitDateTimeField(field,
276                                       convertField(field.getDurationField(), converted),
277                                       convertField(field.getRangeDurationField(), converted),
278                                       convertField(field.getLeapDurationField(), converted));
279            converted.put(field, limitField);
280            return limitField;
281        }
282    
283        void checkLimits(long instant, String desc) {
284            DateTime limit;
285            if ((limit = iLowerLimit) != null && instant < limit.getMillis()) {
286                throw new LimitException(desc, true);
287            }
288            if ((limit = iUpperLimit) != null && instant >= limit.getMillis()) {
289                throw new LimitException(desc, false);
290            }
291        }
292    
293        //-----------------------------------------------------------------------
294        /**
295         * A limit chronology is only equal to a limit chronology with the
296         * same base chronology and limits.
297         * 
298         * @param obj  the object to compare to
299         * @return true if equal
300         * @since 1.4
301         */
302        public boolean equals(Object obj) {
303            if (this == obj) {
304                return true;
305            }
306            if (obj instanceof LimitChronology == false) {
307                return false;
308            }
309            LimitChronology chrono = (LimitChronology) obj;
310            return
311                getBase().equals(chrono.getBase()) &&
312                FieldUtils.equals(getLowerLimit(), chrono.getLowerLimit()) &&
313                FieldUtils.equals(getUpperLimit(), chrono.getUpperLimit());
314        }
315    
316        /**
317         * A suitable hashcode for the chronology.
318         * 
319         * @return the hashcode
320         * @since 1.4
321         */
322        public int hashCode() {
323            int hash = 317351877;
324            hash += (getLowerLimit() != null ? getLowerLimit().hashCode() : 0);
325            hash += (getUpperLimit() != null ? getUpperLimit().hashCode() : 0);
326            hash += getBase().hashCode() * 7;
327            return hash;
328        }
329    
330        /**
331         * A debugging string for the chronology.
332         * 
333         * @return the debugging string
334         */
335        public String toString() {
336            return "LimitChronology[" + getBase().toString() + ", " +
337                (getLowerLimit() == null ? "NoLimit" : getLowerLimit().toString()) + ", " +
338                (getUpperLimit() == null ? "NoLimit" : getUpperLimit().toString()) + ']';
339        }
340    
341        //-----------------------------------------------------------------------
342        /**
343         * Extends IllegalArgumentException such that the exception message is not
344         * generated unless it is actually requested.
345         */
346        private class LimitException extends IllegalArgumentException {
347            private static final long serialVersionUID = -5924689995607498581L;
348    
349            private final boolean iIsLow;
350    
351            LimitException(String desc, boolean isLow) {
352                super(desc);
353                iIsLow = isLow;
354            }
355    
356            public String getMessage() {
357                StringBuffer buf = new StringBuffer(85);
358                buf.append("The");
359                String desc = super.getMessage();
360                if (desc != null) {
361                    buf.append(' ');
362                    buf.append(desc);
363                }
364                buf.append(" instant is ");
365    
366                DateTimeFormatter p = ISODateTimeFormat.dateTime();
367                p = p.withChronology(getBase());
368                if (iIsLow) {
369                    buf.append("below the supported minimum of ");
370                    p.printTo(buf, getLowerLimit().getMillis());
371                } else {
372                    buf.append("above the supported maximum of ");
373                    p.printTo(buf, getUpperLimit().getMillis());
374                }
375                
376                buf.append(" (");
377                buf.append(getBase());
378                buf.append(')');
379    
380                return buf.toString();
381            }
382    
383            public String toString() {
384                return "IllegalArgumentException: " + getMessage();
385            }
386        }
387    
388        private class LimitDurationField extends DecoratedDurationField {
389            private static final long serialVersionUID = 8049297699408782284L;
390    
391            LimitDurationField(DurationField field) {
392                super(field, field.getType());
393            }
394    
395            public int getValue(long duration, long instant) {
396                checkLimits(instant, null);
397                return getWrappedField().getValue(duration, instant);
398            }
399    
400            public long getValueAsLong(long duration, long instant) {
401                checkLimits(instant, null);
402                return getWrappedField().getValueAsLong(duration, instant);
403            }
404    
405            public long getMillis(int value, long instant) {
406                checkLimits(instant, null);
407                return getWrappedField().getMillis(value, instant);
408            }
409    
410            public long getMillis(long value, long instant) {
411                checkLimits(instant, null);
412                return getWrappedField().getMillis(value, instant);
413            }
414    
415            public long add(long instant, int amount) {
416                checkLimits(instant, null);
417                long result = getWrappedField().add(instant, amount);
418                checkLimits(result, "resulting");
419                return result;
420            }
421    
422            public long add(long instant, long amount) {
423                checkLimits(instant, null);
424                long result = getWrappedField().add(instant, amount);
425                checkLimits(result, "resulting");
426                return result;
427            }
428    
429            public int getDifference(long minuendInstant, long subtrahendInstant) {
430                checkLimits(minuendInstant, "minuend");
431                checkLimits(subtrahendInstant, "subtrahend");
432                return getWrappedField().getDifference(minuendInstant, subtrahendInstant);
433            }
434    
435            public long getDifferenceAsLong(long minuendInstant, long subtrahendInstant) {
436                checkLimits(minuendInstant, "minuend");
437                checkLimits(subtrahendInstant, "subtrahend");
438                return getWrappedField().getDifferenceAsLong(minuendInstant, subtrahendInstant);
439            }
440    
441        }
442    
443        private class LimitDateTimeField extends DecoratedDateTimeField {
444            private static final long serialVersionUID = -2435306746995699312L;
445    
446            private final DurationField iDurationField;
447            private final DurationField iRangeDurationField;
448            private final DurationField iLeapDurationField;
449    
450            LimitDateTimeField(DateTimeField field,
451                               DurationField durationField,
452                               DurationField rangeDurationField,
453                               DurationField leapDurationField) {
454                super(field, field.getType());
455                iDurationField = durationField;
456                iRangeDurationField = rangeDurationField;
457                iLeapDurationField = leapDurationField;
458            }
459    
460            public int get(long instant) {
461                checkLimits(instant, null);
462                return getWrappedField().get(instant);
463            }
464            
465            public String getAsText(long instant, Locale locale) {
466                checkLimits(instant, null);
467                return getWrappedField().getAsText(instant, locale);
468            }
469            
470            public String getAsShortText(long instant, Locale locale) {
471                checkLimits(instant, null);
472                return getWrappedField().getAsShortText(instant, locale);
473            }
474            
475            public long add(long instant, int amount) {
476                checkLimits(instant, null);
477                long result = getWrappedField().add(instant, amount);
478                checkLimits(result, "resulting");
479                return result;
480            }
481    
482            public long add(long instant, long amount) {
483                checkLimits(instant, null);
484                long result = getWrappedField().add(instant, amount);
485                checkLimits(result, "resulting");
486                return result;
487            }
488    
489            public long addWrapField(long instant, int amount) {
490                checkLimits(instant, null);
491                long result = getWrappedField().addWrapField(instant, amount);
492                checkLimits(result, "resulting");
493                return result;
494            }
495            
496            public int getDifference(long minuendInstant, long subtrahendInstant) {
497                checkLimits(minuendInstant, "minuend");
498                checkLimits(subtrahendInstant, "subtrahend");
499                return getWrappedField().getDifference(minuendInstant, subtrahendInstant);
500            }
501            
502            public long getDifferenceAsLong(long minuendInstant, long subtrahendInstant) {
503                checkLimits(minuendInstant, "minuend");
504                checkLimits(subtrahendInstant, "subtrahend");
505                return getWrappedField().getDifferenceAsLong(minuendInstant, subtrahendInstant);
506            }
507            
508            public long set(long instant, int value) {
509                checkLimits(instant, null);
510                long result = getWrappedField().set(instant, value);
511                checkLimits(result, "resulting");
512                return result;
513            }
514            
515            public long set(long instant, String text, Locale locale) {
516                checkLimits(instant, null);
517                long result = getWrappedField().set(instant, text, locale);
518                checkLimits(result, "resulting");
519                return result;
520            }
521            
522            public final DurationField getDurationField() {
523                return iDurationField;
524            }
525    
526            public final DurationField getRangeDurationField() {
527                return iRangeDurationField;
528            }
529    
530            public boolean isLeap(long instant) {
531                checkLimits(instant, null);
532                return getWrappedField().isLeap(instant);
533            }
534            
535            public int getLeapAmount(long instant) {
536                checkLimits(instant, null);
537                return getWrappedField().getLeapAmount(instant);
538            }
539            
540            public final DurationField getLeapDurationField() {
541                return iLeapDurationField;
542            }
543            
544            public long roundFloor(long instant) {
545                checkLimits(instant, null);
546                long result = getWrappedField().roundFloor(instant);
547                checkLimits(result, "resulting");
548                return result;
549            }
550            
551            public long roundCeiling(long instant) {
552                checkLimits(instant, null);
553                long result = getWrappedField().roundCeiling(instant);
554                checkLimits(result, "resulting");
555                return result;
556            }
557            
558            public long roundHalfFloor(long instant) {
559                checkLimits(instant, null);
560                long result = getWrappedField().roundHalfFloor(instant);
561                checkLimits(result, "resulting");
562                return result;
563            }
564            
565            public long roundHalfCeiling(long instant) {
566                checkLimits(instant, null);
567                long result = getWrappedField().roundHalfCeiling(instant);
568                checkLimits(result, "resulting");
569                return result;
570            }
571            
572            public long roundHalfEven(long instant) {
573                checkLimits(instant, null);
574                long result = getWrappedField().roundHalfEven(instant);
575                checkLimits(result, "resulting");
576                return result;
577            }
578            
579            public long remainder(long instant) {
580                checkLimits(instant, null);
581                long result = getWrappedField().remainder(instant);
582                checkLimits(result, "resulting");
583                return result;
584            }
585    
586            public int getMinimumValue(long instant) {
587                checkLimits(instant, null);
588                return getWrappedField().getMinimumValue(instant);
589            }
590    
591            public int getMaximumValue(long instant) {
592                checkLimits(instant, null);
593                return getWrappedField().getMaximumValue(instant);
594            }
595    
596            public int getMaximumTextLength(Locale locale) {
597                return getWrappedField().getMaximumTextLength(locale);
598            }
599    
600            public int getMaximumShortTextLength(Locale locale) {
601                return getWrappedField().getMaximumShortTextLength(locale);
602            }
603    
604        }
605    
606    }