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 */
016package org.joda.time.chrono;
017
018import java.util.HashMap;
019import java.util.Locale;
020
021import org.joda.time.Chronology;
022import org.joda.time.DateTime;
023import org.joda.time.DateTimeField;
024import org.joda.time.DateTimeZone;
025import org.joda.time.DurationField;
026import org.joda.time.MutableDateTime;
027import org.joda.time.ReadableDateTime;
028import org.joda.time.field.DecoratedDateTimeField;
029import org.joda.time.field.DecoratedDurationField;
030import org.joda.time.field.FieldUtils;
031import org.joda.time.format.DateTimeFormatter;
032import 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 */
049public 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}