1 | /* |
2 | * Copyright 2001-2005 Stephen Colebourne |
3 | * |
4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | * you may not use this file except in compliance with the License. |
6 | * You may obtain a copy of the License at |
7 | * |
8 | * http://www.apache.org/licenses/LICENSE-2.0 |
9 | * |
10 | * Unless required by applicable law or agreed to in writing, software |
11 | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | * See the License for the specific language governing permissions and |
14 | * limitations under the License. |
15 | */ |
16 | package org.joda.time.chrono; |
17 | |
18 | import java.util.HashMap; |
19 | import java.util.Locale; |
20 | |
21 | import org.joda.time.Chronology; |
22 | import org.joda.time.DateTime; |
23 | import org.joda.time.DateTimeField; |
24 | import org.joda.time.DateTimeZone; |
25 | import org.joda.time.DurationField; |
26 | import org.joda.time.MutableDateTime; |
27 | import org.joda.time.ReadableDateTime; |
28 | import org.joda.time.field.DecoratedDateTimeField; |
29 | import org.joda.time.field.DecoratedDurationField; |
30 | import org.joda.time.field.FieldUtils; |
31 | import org.joda.time.format.DateTimeFormatter; |
32 | import org.joda.time.format.ISODateTimeFormat; |
33 | |
34 | /** |
35 | * Wraps another Chronology to impose limits on the range of instants that |
36 | * the fields within a Chronology may support. The limits are applied to both |
37 | * DateTimeFields and DurationFields. |
38 | * <p> |
39 | * Methods in DateTimeField and DurationField throw an IllegalArgumentException |
40 | * whenever given an input instant that is outside the limits or when an |
41 | * attempt is made to move an instant outside the limits. |
42 | * <p> |
43 | * LimitChronology is thread-safe and immutable. |
44 | * |
45 | * @author Brian S O'Neill |
46 | * @author Stephen Colebourne |
47 | * @since 1.0 |
48 | */ |
49 | public final class LimitChronology extends AssembledChronology { |
50 | |
51 | /** Serialization lock */ |
52 | private static final long serialVersionUID = 7670866536893052522L; |
53 | |
54 | /** |
55 | * Wraps another chronology, with datetime limits. When withUTC or |
56 | * withZone is called, the returned LimitChronology instance has |
57 | * the same limits, except they are time zone adjusted. |
58 | * |
59 | * @param base base chronology to wrap |
60 | * @param lowerLimit inclusive lower limit, or null if none |
61 | * @param upperLimit exclusive upper limit, or null if none |
62 | * @throws IllegalArgumentException if chronology is null or limits are invalid |
63 | */ |
64 | public static LimitChronology getInstance(Chronology base, |
65 | ReadableDateTime lowerLimit, |
66 | ReadableDateTime upperLimit) { |
67 | if (base == null) { |
68 | throw new IllegalArgumentException("Must supply a chronology"); |
69 | } |
70 | |
71 | lowerLimit = lowerLimit == null ? null : lowerLimit.toDateTime(); |
72 | upperLimit = upperLimit == null ? null : upperLimit.toDateTime(); |
73 | |
74 | if (lowerLimit != null && upperLimit != null) { |
75 | if (!lowerLimit.isBefore(upperLimit)) { |
76 | throw new IllegalArgumentException |
77 | ("The lower limit must be come before than the upper limit"); |
78 | } |
79 | } |
80 | |
81 | return new LimitChronology(base, (DateTime)lowerLimit, (DateTime)upperLimit); |
82 | } |
83 | |
84 | final DateTime iLowerLimit; |
85 | final DateTime iUpperLimit; |
86 | |
87 | private transient LimitChronology iWithUTC; |
88 | |
89 | /** |
90 | * Wraps another chronology, with datetime limits. When withUTC or |
91 | * withZone is called, the returned LimitChronology instance has |
92 | * the same limits, except they are time zone adjusted. |
93 | * |
94 | * @param lowerLimit inclusive lower limit, or null if none |
95 | * @param upperLimit exclusive upper limit, or null if none |
96 | */ |
97 | private LimitChronology(Chronology base, |
98 | DateTime lowerLimit, DateTime upperLimit) { |
99 | 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 converted = new HashMap(); |
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 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 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 | } |