8254ab6688933704c0eb90b623feeb6137606c1e
[cdmlib.git] / cdmlib-model / src / main / java / eu / etaxonomy / cdm / model / common / TimePeriod.java
1 /**
2 * Copyright (C) 2007 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
5 *
6 * The contents of this file are subject to the Mozilla Public License Version 1.1
7 * See LICENSE.TXT at the top of this package for the full license terms.
8 */
9
10 package eu.etaxonomy.cdm.model.common;
11
12 import java.io.Serializable;
13 import java.util.Calendar;
14 import java.util.Date;
15
16 import javax.persistence.Embeddable;
17 import javax.persistence.MappedSuperclass;
18 import javax.persistence.Transient;
19 import javax.xml.bind.annotation.XmlAccessType;
20 import javax.xml.bind.annotation.XmlAccessorType;
21 import javax.xml.bind.annotation.XmlElement;
22 import javax.xml.bind.annotation.XmlRootElement;
23 import javax.xml.bind.annotation.XmlType;
24 import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
25
26 import org.apache.commons.lang3.StringUtils;
27 import org.apache.logging.log4j.LogManager;
28 import org.apache.logging.log4j.Logger;
29 import org.hibernate.annotations.Type;
30 import org.hibernate.search.annotations.Analyze;
31 import org.hibernate.search.annotations.Field;
32 import org.hibernate.search.annotations.FieldBridge;
33 import org.joda.time.DateTime;
34 import org.joda.time.DateTimeFieldType;
35 import org.joda.time.LocalDate;
36 import org.joda.time.Partial;
37 import org.joda.time.ReadableInstant;
38
39 import com.fasterxml.jackson.annotation.JsonIgnore;
40
41 import eu.etaxonomy.cdm.common.CdmUtils;
42 import eu.etaxonomy.cdm.common.UTF8;
43 import eu.etaxonomy.cdm.format.common.TimePeriodFormatter;
44 import eu.etaxonomy.cdm.hibernate.search.PartialBridge;
45 import eu.etaxonomy.cdm.jaxb.PartialAdapter;
46
47 /**
48 * @author m.doering
49 * @since 08-Nov-2007 13:07:00
50 * @updated 05-Dec-2008 23:00:05
51 * @updated 14-Jul-2013 move parser methods to TimePeriodParser
52 */
53 @XmlAccessorType(XmlAccessType.FIELD)
54 @XmlType(name = "TimePeriod", propOrder = {
55 "start",
56 "end",
57 "freeText"
58 })
59 @XmlRootElement(name = "TimePeriod")
60 @Embeddable
61 @MappedSuperclass
62 public class TimePeriod implements Cloneable, Serializable, ICheckEmpty {
63
64 private static final long serialVersionUID = 3405969418194981401L;
65 private static final Logger logger = LogManager.getLogger(TimePeriod.class);
66
67 public static final DateTimeFieldType YEAR_TYPE = DateTimeFieldType.year();
68 public static final DateTimeFieldType MONTH_TYPE = DateTimeFieldType.monthOfYear();
69 public static final DateTimeFieldType DAY_TYPE = DateTimeFieldType.dayOfMonth();
70 public static final DateTimeFieldType HOUR_TYPE = DateTimeFieldType.hourOfDay();
71 public static final DateTimeFieldType MINUTE_TYPE = DateTimeFieldType.minuteOfHour();
72
73 public static final String SEP = UTF8.EN_DASH.toString(); //maybe this will be moved to a formatter class in future
74
75 public static final Partial CONTINUED = new Partial
76 (new DateTimeFieldType[]{YEAR_TYPE, MONTH_TYPE, DAY_TYPE},
77 new int[]{9999, 11, 30});
78
79 private static TimePeriodFormatter formatter = TimePeriodFormatter.NewDefaultInstance();
80
81 @XmlElement(name = "Start")
82 @XmlJavaTypeAdapter(value = PartialAdapter.class)
83 @Type(type="partialUserType")
84 @Field(analyze = Analyze.NO)
85 @FieldBridge(impl = PartialBridge.class)
86 @JsonIgnore // currently used for swagger model scanner
87 private Partial start;
88
89 @XmlElement(name = "End")
90 @XmlJavaTypeAdapter(value = PartialAdapter.class)
91 @Type(type="partialUserType")
92 @Field(analyze = Analyze.NO)
93 @FieldBridge(impl = PartialBridge.class)
94 @JsonIgnore // currently used for swagger model scanner
95 private Partial end;
96
97 @XmlElement(name = "FreeText")
98 private String freeText;
99
100 // ********************** FACTORY METHODS **************************/
101
102 public static final TimePeriod NewInstance(){
103 return new TimePeriod();
104 }
105
106 public static final TimePeriod NewInstance(Partial startDate){
107 return new TimePeriod(startDate, null, null);
108 }
109
110 public static final TimePeriod NewInstance(Partial startDate, Partial endDate){
111 return new TimePeriod(startDate, endDate, null);
112 }
113
114 public static final TimePeriod NewInstance(Integer year){
115 Integer endYear = null;
116 return NewInstance(year, endYear);
117 }
118
119 public static final TimePeriod NewInstance(Integer startYear, Integer endYear){
120 return new TimePeriod(yearToPartial(startYear), yearToPartial(endYear), null);
121 }
122
123 /**
124 * Factory method to create a TimePeriod from a <code>Calendar</code>. The Calendar is stored as the starting instant.
125 * @return
126 */
127 public static final TimePeriod NewInstance(Calendar startCalendar){
128 return NewInstance(startCalendar, null);
129 }
130
131 /**
132 * Factory method to create a TimePeriod from a <code>ReadableInstant</code>(e.g. <code>DateTime</code>).
133 * The <code>ReadableInstant</code> is stored as the starting instant.
134 * @return
135 */
136 public static final TimePeriod NewInstance(ReadableInstant readableInstant){
137 return NewInstance(readableInstant, null);
138 }
139
140 /**
141 * Factory method to create a TimePeriod from a starting and an ending <code>Calendar</code>
142 * @return
143 */
144 public static final TimePeriod NewInstance(Calendar startCalendar, Calendar endCalendar){
145 return new TimePeriod(calendarToPartial(startCalendar), calendarToPartial(endCalendar), null);
146 }
147
148 /**
149 * Factory method to create a TimePeriod from a starting and an ending <code>Date</code>
150 * @return TimePeriod
151 */
152 public static final TimePeriod NewInstance(Date startDate, Date endDate){
153 return NewInstance(dateToPartial(startDate), dateToPartial(endDate));
154 }
155
156 /**
157 * Factory method to create a TimePeriod from a starting and an ending <code>ReadableInstant</code>(e.g. <code>DateTime</code>)
158 * @return
159 */
160 public static final TimePeriod NewInstance(ReadableInstant startInstant, ReadableInstant endInstant){
161 return new TimePeriod(readableInstantToPartial(startInstant), readableInstantToPartial(endInstant), null);
162 }
163
164 //****************** PARTIAL CONVERTERS ******************/
165
166 /**
167 * Transforms a {@link Calendar} into a <code>Partial</code>
168 * @param calendar
169 * @return
170 */
171 public static Partial calendarToPartial(Calendar calendar){
172 if (calendar == null){
173 return null;
174 }else{
175 LocalDate ld = new LocalDate(calendar);
176 Partial partial = new Partial(ld);
177 return partial;
178 }
179 }
180
181 /**
182 * Transforms a {@link ReadableInstant} into a <code>Partial</code>
183 */
184 public static Partial readableInstantToPartial(ReadableInstant readableInstant){
185 if (readableInstant == null){
186 return null;
187 }else{
188 DateTime dt = readableInstant.toInstant().toDateTime();
189 LocalDate ld = dt.toLocalDate();
190 int hour = dt.hourOfDay().get();
191 int minute = dt.minuteOfHour().get();
192 Partial partial = new Partial(ld).with(HOUR_TYPE, hour).with(MINUTE_TYPE, minute);
193 return partial;
194 }
195 }
196
197 /**
198 * Transforms a {@link Date} into a <code>Partial</code>.
199 */
200 public static Partial dateToPartial(Date date){
201 //TODO conversion untested, implemented according to http://www.roseindia.net/java/java-conversion/datetocalender.shtml
202 if (date != null){
203 Calendar cal = Calendar.getInstance();
204 cal.setTime(date);
205 return calendarToPartial(cal);
206 }else{
207 return null;
208 }
209 }
210
211 /**
212 * Transforms an Integer into a <code>Partial</code> with the Integer value
213 * being the year of the Partial.
214 */
215 public static Partial yearToPartial(Integer year){
216 if (year != null){
217 return new Partial().with(YEAR_TYPE, year);
218 }else{
219 return null;
220 }
221 }
222 public static Partial monthToPartial(Integer month){
223 if (month != null){
224 return new Partial().with(MONTH_TYPE, month);
225 }else{
226 return null;
227 }
228 }
229 public static Partial monthAndDayToPartial(Integer month, Integer day){
230 if (month != null || day != null){
231 Partial result = new Partial();
232 if (month != null){
233 result = result.with(MONTH_TYPE, month);
234 }
235 if (day != null){
236 result = result.with(DAY_TYPE, day);
237 }
238 return result;
239 }else{
240 return null;
241 }
242 }
243
244 public static Integer getPartialValue(Partial partial, DateTimeFieldType type){
245 if (partial == null || ! partial.isSupported(type)){
246 return null;
247 }else{
248 return partial.get(type);
249 }
250 }
251
252 //****************** TIME PERIOD CONVERTERS ******************/
253
254 public static TimePeriod fromVerbatim(VerbatimTimePeriod verbatimTimePeriod){
255 if (verbatimTimePeriod == null){
256 return null;
257 }
258 TimePeriod result = TimePeriod.NewInstance();
259 copyCloned(verbatimTimePeriod, result);
260 if (StringUtils.isNotBlank(verbatimTimePeriod.getVerbatimDate()) &&
261 StringUtils.isBlank(result.getFreeText())){
262 result.setFreeText(verbatimTimePeriod.toString());
263 }
264 return result;
265 }
266 public static VerbatimTimePeriod toVerbatim(TimePeriod timePeriod){
267 if (timePeriod == null){
268 return null;
269 }else if (timePeriod instanceof VerbatimTimePeriod){
270 return (VerbatimTimePeriod)timePeriod;
271 }else{
272 VerbatimTimePeriod result = VerbatimTimePeriod.NewVerbatimInstance();
273 copyCloned(timePeriod, result);
274 return result;
275 }
276 }
277 public VerbatimTimePeriod toVerbatim(){
278 return toVerbatim(this);
279 }
280
281 //*********************** CONSTRUCTOR *********************************/
282
283 protected TimePeriod() {
284 super();
285 }
286 protected TimePeriod(Partial startDate, Partial endDate, String freeText) {
287 this.start = startDate;
288 this.end = endDate;
289 this.freeText = freeText;
290 }
291
292 //******************* GETTER / SETTER ************************************/
293
294
295 @JsonIgnore // currently used for swagger model scanner
296 public Partial getStart() {
297 return start;
298 }
299
300 public void setStart(Partial start) {
301 this.start = start;
302 }
303
304
305 @JsonIgnore // currently used for swagger model scanner
306 public Partial getEnd() {
307 return isContinued() ? null : end;
308 }
309
310 public void setEnd(Partial end) {
311 this.end = end;
312 }
313
314 /**
315 * For time periods that need to store more information than the one
316 * that can be stored in <code>start</code> and <code>end</code>.
317 * If free text is not <code>null</null> {@link #toString()} will always
318 * return the free text value.
319 * <BR>Use {@link #toString()} for public use.
320 * @return the freeText
321 */
322 public String getFreeText() {
323 return freeText;
324 }
325 /**
326 * Use {@link #parseSingleDate(String)} for public use.
327 * @param freeText the freeText to set
328 */
329 public void setFreeText(String freeText) {
330 this.freeText = freeText;
331 }
332
333 /**
334 * Returns the continued flag (internally stored as a constant
335 * far away date. {@link #CONTINUED}
336 * @return
337 */
338 public boolean isContinued() {
339 return CONTINUED.equals(end);
340 }
341 /**
342 * Sets the (virtual) continued flag.<BR><BR>
343 * NOTE: setting the flag to true, will remove an
344 * existing end date.
345 * @param isContinued
346 */
347 public void setContinued(boolean isContinued) {
348 if (isContinued == true){
349 this.end = CONTINUED;
350 }else if (isContinued()){
351 this.end = null;
352 }
353 }
354
355 //******************* Transient METHODS ************************************/
356
357 /**
358 * True, if this time period represents a period not a single point in time.
359 * This is by definition, that the time period has a start and an end value,
360 * and both have a year value that is not null
361 * @return
362 */
363 @Transient
364 public boolean isPeriod(){
365 if (getStartYear() != null && getEndYear() != null ){
366 return true;
367 }else{
368 return false;
369 }
370 }
371
372 /**
373 * True, if there is no start date, no end date and no freetext representation.
374 */
375 @Transient
376 public boolean isEmpty(){
377 if (StringUtils.isBlank(this.getFreeText()) && isEmpty(start) && isEmpty(end)){
378 return true;
379 }else{
380 return false;
381 }
382 }
383
384 @Transient
385 public Integer getStartYear(){
386 return getPartialValue(start, YEAR_TYPE);
387 }
388
389 @Transient
390 public Integer getStartMonth(){
391 return getPartialValue(start, MONTH_TYPE);
392 }
393
394 @Transient
395 public Integer getStartDay(){
396 return getPartialValue(start, DAY_TYPE);
397 }
398
399 @Transient
400 public Integer getEndYear(){
401 return getPartialValue(getEnd(), YEAR_TYPE);
402 }
403
404 @Transient
405 public Integer getEndMonth(){
406 return getPartialValue(getEnd(), MONTH_TYPE);
407 }
408
409 @Transient
410 public Integer getEndDay(){
411 return getPartialValue(getEnd(), DAY_TYPE);
412 }
413
414 public TimePeriod setStartYear(Integer year){
415 return setStartField(year, YEAR_TYPE);
416 }
417
418 public TimePeriod setStartMonth(Integer month) throws IndexOutOfBoundsException{
419 return setStartField(month, MONTH_TYPE);
420 }
421
422 public TimePeriod setStartDay(Integer day) throws IndexOutOfBoundsException{
423 return setStartField(day, DAY_TYPE);
424 }
425
426 public TimePeriod setEndYear(Integer year){
427 return setEndField(year, YEAR_TYPE);
428 }
429
430 public TimePeriod setEndMonth(Integer month) throws IndexOutOfBoundsException{
431 return setEndField(month, MONTH_TYPE);
432 }
433
434 public TimePeriod setEndDay(Integer day) throws IndexOutOfBoundsException{
435 return setEndField(day, DAY_TYPE);
436 }
437
438 @Transient
439 private TimePeriod setStartField(Integer value, DateTimeFieldType type)
440 throws IndexOutOfBoundsException{
441 start = setPartialField(start, value, type);
442 return this;
443 }
444
445 @Transient
446 private TimePeriod setEndField(Integer value, DateTimeFieldType type)
447 throws IndexOutOfBoundsException{
448 end = setPartialField(getEnd(), value, type);
449 return this;
450 }
451
452 public static Partial setPartialField(Partial partial, Integer value, DateTimeFieldType type)
453 throws IndexOutOfBoundsException{
454 if (partial == null){
455 partial = new Partial();
456 }
457 if (value == null){
458 return partial.without(type);
459 }else{
460 checkFieldValues(value, type, partial);
461 return partial.with(type, value);
462 }
463 }
464
465
466 // ******************************** internal methods *******************************/
467
468 /**
469 * Throws an IndexOutOfBoundsException if the value does not have a valid value
470 * (e.g. month > 12, month < 1, day > 31, etc.)
471 * @param value
472 * @param type
473 * @throws IndexOutOfBoundsException
474 */
475 private static void checkFieldValues(Integer value, DateTimeFieldType type, Partial partial)
476 throws IndexOutOfBoundsException{
477 int max = 9999999;
478 if (type.equals(MONTH_TYPE)){
479 max = 12;
480 }
481 if (type.equals(DAY_TYPE)){
482 max = 31;
483 Integer month = null;
484 if (partial.isSupported(MONTH_TYPE)){
485 month = partial.get(MONTH_TYPE);
486 }
487 if (month != null){
488 if (month == 2){
489 max = 29;
490 }else if (month == 4 ||month == 6 ||month == 9 ||month == 11){
491 max = 30;
492 }
493 }
494 }
495 if ( (value < 1 || value > max) ){
496 throw new IndexOutOfBoundsException("Value must be between 1 and " + max);
497 }
498 }
499
500 //**************************** to String ****************************************
501
502 /**
503 * Returns the {@link #getFreeText()} value if free text is not <code>null</code>.
504 * Otherwise the concatenation of <code>start</code> and <code>end</code> is returned.
505 *
506 * @see java.lang.Object#toString()
507 */
508 @Override
509 public String toString(){
510 return formatter.format(this);
511 }
512
513 /**
514 * Returns the concatenation of <code>start</code> and <code>end</code>
515 */
516 public String getTimePeriod(){
517 return formatter.getTimePeriod(this);
518 }
519
520 @Transient
521 public String getYear(){
522 return formatter.getYear(this);
523 }
524
525 @Override
526 public boolean checkEmpty() {
527 //TODO unify isEmpty && checkEmpty
528 return isEmpty();
529 }
530
531 protected boolean isBlank(String str) {
532 return StringUtils.isBlank(str);
533 }
534
535 protected boolean isEmpty(Partial partial) {
536 return partial == null? true : partial.getFields().length == 0;
537 }
538
539 //*********** EQUALS **********************************/
540
541 @Override
542 public boolean equals(Object obj) {
543 if (obj == null){
544 return false;
545 }
546 if (! (obj instanceof TimePeriod)){
547 return false;
548 }
549 TimePeriod that = (TimePeriod)obj;
550
551 if (! CdmUtils.nullSafeEqual(this.start, that.start)){
552 return false;
553 }
554 if (! CdmUtils.nullSafeEqual(this.end, that.end)){
555 return false;
556 }
557 if (! CdmUtils.nullSafeEqual(this.freeText, that.freeText)){
558 return false;
559 }
560 //see comment in VerbatimTimePeriod#equals
561 String thisVerbatimDate = (this instanceof VerbatimTimePeriod)?
562 ((VerbatimTimePeriod)this).getVerbatimDate():null;
563 String thatVerbatimDate = (obj instanceof VerbatimTimePeriod)?
564 ((VerbatimTimePeriod)obj).getVerbatimDate():null;
565 if (! CdmUtils.nullSafeEqual(thisVerbatimDate, thatVerbatimDate)){
566 return false;
567 }
568 //see comment in ExtendedTimePeriod#equals
569 Partial thisExtremeStart = (this instanceof ExtendedTimePeriod)?
570 ((ExtendedTimePeriod)this).getExtremeStart():null;
571 Partial thatExtremeStart = (obj instanceof ExtendedTimePeriod)?
572 ((ExtendedTimePeriod)obj).getExtremeStart():null;
573 if (! CdmUtils.nullSafeEqual(thisExtremeStart, thatExtremeStart)){
574 return false;
575 }
576
577 Partial thisExtremeEnd = (this instanceof ExtendedTimePeriod)?
578 ((ExtendedTimePeriod)this).getExtremeEnd():null;
579 Partial thatExtremeEnd = (obj instanceof ExtendedTimePeriod)?
580 ((ExtendedTimePeriod)obj).getExtremeEnd():null;
581 if (! CdmUtils.nullSafeEqual(thisExtremeEnd, thatExtremeEnd)){
582 return false;
583 }
584
585 return true;
586 }
587
588 @Override
589 public int hashCode() {
590 int hashCode = 7;
591 hashCode = 29*hashCode +
592 (start == null? 33: start.hashCode()) +
593 (end == null? 39: end.hashCode()) +
594 (freeText == null? 41: freeText.hashCode());
595 return hashCode;
596 }
597
598 /**
599 * Tests, if time period 1 and time period 2 are equal with <code>null</code> and
600 * <code>empty</code> time periods all handled similar.<BR><BR>
601 * <code>
602 * equalsNullAndEmptySafe(null, null) = true<BR>
603 * equalsNullAndEmptySafe(null, empty) = true<BR>
604 * equalsNullAndEmptySafe(empty, null) = true<BR>
605 * equalsNullAndEmptySafe(empty, empty) = true<BR>
606 * equalsNullAndEmptySafe(null, not-empty) = false<BR>
607 * equalsNullAndEmptySafe(empty, not-empty) = false<BR>
608 * equalsNullAndEmptySafe(not-empty, null) = false<BR>
609 * equalsNullAndEmptySafe(not-empty, empty) = false<BR>
610 * equalsNullAndEmptySafe(not-empty1, not-empty2) = not-empty.equals(not-empty2)<BR>
611 * </code>
612 */
613 @SuppressWarnings("null")
614 public static boolean equalsNullAndEmptySafe(TimePeriod timePeriod1, TimePeriod timePeriod2) {
615 boolean tp1Empty = timePeriod1 == null || timePeriod1.isEmpty();
616 boolean tp2Empty = timePeriod2 == null || timePeriod2.isEmpty();
617 if (tp1Empty && tp2Empty) {
618 return true;
619 }else if (tp1Empty || tp2Empty) {
620 return false;
621 }else {
622 return timePeriod1.equals(timePeriod2);
623 }
624 }
625
626 //*********** CLONE **********************************/
627
628 @Override
629 public TimePeriod clone() {
630 try {
631 TimePeriod result = (TimePeriod)super.clone();
632 copyCloned(this, result);
633 return result;
634 } catch (CloneNotSupportedException e) {
635 logger.warn("Clone not supported exception. Should never occurr !!");
636 return null;
637 }
638 }
639
640 protected static void copyCloned(TimePeriod origin, TimePeriod target) {
641 target.setStart(origin.start); //DateTime is immutable
642 target.setEnd(origin.end);
643 target.setFreeText(origin.freeText);
644 }
645
646 }