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 }