2 * Copyright (C) 2007 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
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.
10 package eu
.etaxonomy
.cdm
.model
.common
;
12 import java
.io
.Serializable
;
13 import java
.util
.Calendar
;
14 import java
.util
.Date
;
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
;
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
;
39 import com
.fasterxml
.jackson
.annotation
.JsonIgnore
;
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
;
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
53 @XmlAccessorType(XmlAccessType
.FIELD
)
54 @XmlType(name
= "TimePeriod", propOrder
= {
59 @XmlRootElement(name
= "TimePeriod")
62 public class TimePeriod
implements Cloneable
, Serializable
, ICheckEmpty
{
64 private static final long serialVersionUID
= 3405969418194981401L;
65 private static final Logger logger
= LogManager
.getLogger(TimePeriod
.class);
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();
73 public static final String SEP
= UTF8
.EN_DASH
.toString(); //maybe this will be moved to a formatter class in future
75 public static final Partial CONTINUED
= new Partial
76 (new DateTimeFieldType
[]{YEAR_TYPE
, MONTH_TYPE
, DAY_TYPE
},
77 new int[]{9999, 11, 30});
79 private static TimePeriodFormatter formatter
= TimePeriodFormatter
.NewDefaultInstance();
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
;
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
97 @XmlElement(name
= "FreeText")
98 private String freeText
;
100 // ********************** FACTORY METHODS **************************/
102 public static final TimePeriod
NewInstance(){
103 return new TimePeriod();
106 public static final TimePeriod
NewInstance(Partial startDate
){
107 return new TimePeriod(startDate
, null, null);
110 public static final TimePeriod
NewInstance(Partial startDate
, Partial endDate
){
111 return new TimePeriod(startDate
, endDate
, null);
114 public static final TimePeriod
NewInstance(Integer year
){
115 Integer endYear
= null;
116 return NewInstance(year
, endYear
);
119 public static final TimePeriod
NewInstance(Integer startYear
, Integer endYear
){
120 return new TimePeriod(yearToPartial(startYear
), yearToPartial(endYear
), null);
124 * Factory method to create a TimePeriod from a <code>Calendar</code>. The Calendar is stored as the starting instant.
127 public static final TimePeriod
NewInstance(Calendar startCalendar
){
128 return NewInstance(startCalendar
, null);
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.
136 public static final TimePeriod
NewInstance(ReadableInstant readableInstant
){
137 return NewInstance(readableInstant
, null);
141 * Factory method to create a TimePeriod from a starting and an ending <code>Calendar</code>
144 public static final TimePeriod
NewInstance(Calendar startCalendar
, Calendar endCalendar
){
145 return new TimePeriod(calendarToPartial(startCalendar
), calendarToPartial(endCalendar
), null);
149 * Factory method to create a TimePeriod from a starting and an ending <code>Date</code>
152 public static final TimePeriod
NewInstance(Date startDate
, Date endDate
){
153 return NewInstance(dateToPartial(startDate
), dateToPartial(endDate
));
157 * Factory method to create a TimePeriod from a starting and an ending <code>ReadableInstant</code>(e.g. <code>DateTime</code>)
160 public static final TimePeriod
NewInstance(ReadableInstant startInstant
, ReadableInstant endInstant
){
161 return new TimePeriod(readableInstantToPartial(startInstant
), readableInstantToPartial(endInstant
), null);
164 //****************** PARTIAL CONVERTERS ******************/
167 * Transforms a {@link Calendar} into a <code>Partial</code>
171 public static Partial
calendarToPartial(Calendar calendar
){
172 if (calendar
== null){
175 LocalDate ld
= new LocalDate(calendar
);
176 Partial partial
= new Partial(ld
);
182 * Transforms a {@link ReadableInstant} into a <code>Partial</code>
184 public static Partial
readableInstantToPartial(ReadableInstant readableInstant
){
185 if (readableInstant
== null){
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
);
198 * Transforms a {@link Date} into a <code>Partial</code>.
200 public static Partial
dateToPartial(Date date
){
201 //TODO conversion untested, implemented according to http://www.roseindia.net/java/java-conversion/datetocalender.shtml
203 Calendar cal
= Calendar
.getInstance();
205 return calendarToPartial(cal
);
212 * Transforms an Integer into a <code>Partial</code> with the Integer value
213 * being the year of the Partial.
215 public static Partial
yearToPartial(Integer year
){
217 return new Partial().with(YEAR_TYPE
, year
);
222 public static Partial
monthToPartial(Integer month
){
224 return new Partial().with(MONTH_TYPE
, month
);
229 public static Partial
monthAndDayToPartial(Integer month
, Integer day
){
230 if (month
!= null || day
!= null){
231 Partial result
= new Partial();
233 result
= result
.with(MONTH_TYPE
, month
);
236 result
= result
.with(DAY_TYPE
, day
);
244 public static Integer
getPartialValue(Partial partial
, DateTimeFieldType type
){
245 if (partial
== null || ! partial
.isSupported(type
)){
248 return partial
.get(type
);
252 //****************** TIME PERIOD CONVERTERS ******************/
254 public static TimePeriod
fromVerbatim(VerbatimTimePeriod verbatimTimePeriod
){
255 if (verbatimTimePeriod
== null){
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());
266 public static VerbatimTimePeriod
toVerbatim(TimePeriod timePeriod
){
267 if (timePeriod
== null){
269 }else if (timePeriod
instanceof VerbatimTimePeriod
){
270 return (VerbatimTimePeriod
)timePeriod
;
272 VerbatimTimePeriod result
= VerbatimTimePeriod
.NewVerbatimInstance();
273 copyCloned(timePeriod
, result
);
277 public VerbatimTimePeriod
toVerbatim(){
278 return toVerbatim(this);
281 //*********************** CONSTRUCTOR *********************************/
283 protected TimePeriod() {
286 protected TimePeriod(Partial startDate
, Partial endDate
, String freeText
) {
287 this.start
= startDate
;
289 this.freeText
= freeText
;
292 //******************* GETTER / SETTER ************************************/
295 @JsonIgnore // currently used for swagger model scanner
296 public Partial
getStart() {
300 public void setStart(Partial start
) {
305 @JsonIgnore // currently used for swagger model scanner
306 public Partial
getEnd() {
307 return isContinued() ?
null : end
;
310 public void setEnd(Partial end
) {
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
322 public String
getFreeText() {
326 * Use {@link #parseSingleDate(String)} for public use.
327 * @param freeText the freeText to set
329 public void setFreeText(String freeText
) {
330 this.freeText
= freeText
;
334 * Returns the continued flag (internally stored as a constant
335 * far away date. {@link #CONTINUED}
338 public boolean isContinued() {
339 return CONTINUED
.equals(end
);
342 * Sets the (virtual) continued flag.<BR><BR>
343 * NOTE: setting the flag to true, will remove an
347 public void setContinued(boolean isContinued
) {
348 if (isContinued
== true){
349 this.end
= CONTINUED
;
350 }else if (isContinued()){
355 //******************* Transient METHODS ************************************/
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
364 public boolean isPeriod(){
365 if (getStartYear() != null && getEndYear() != null ){
373 * True, if there is no start date, no end date and no freetext representation.
376 public boolean isEmpty(){
377 if (StringUtils
.isBlank(this.getFreeText()) && isEmpty(start
) && isEmpty(end
)){
385 public Integer
getStartYear(){
386 return getPartialValue(start
, YEAR_TYPE
);
390 public Integer
getStartMonth(){
391 return getPartialValue(start
, MONTH_TYPE
);
395 public Integer
getStartDay(){
396 return getPartialValue(start
, DAY_TYPE
);
400 public Integer
getEndYear(){
401 return getPartialValue(getEnd(), YEAR_TYPE
);
405 public Integer
getEndMonth(){
406 return getPartialValue(getEnd(), MONTH_TYPE
);
410 public Integer
getEndDay(){
411 return getPartialValue(getEnd(), DAY_TYPE
);
414 public TimePeriod
setStartYear(Integer year
){
415 return setStartField(year
, YEAR_TYPE
);
418 public TimePeriod
setStartMonth(Integer month
) throws IndexOutOfBoundsException
{
419 return setStartField(month
, MONTH_TYPE
);
422 public TimePeriod
setStartDay(Integer day
) throws IndexOutOfBoundsException
{
423 return setStartField(day
, DAY_TYPE
);
426 public TimePeriod
setEndYear(Integer year
){
427 return setEndField(year
, YEAR_TYPE
);
430 public TimePeriod
setEndMonth(Integer month
) throws IndexOutOfBoundsException
{
431 return setEndField(month
, MONTH_TYPE
);
434 public TimePeriod
setEndDay(Integer day
) throws IndexOutOfBoundsException
{
435 return setEndField(day
, DAY_TYPE
);
439 private TimePeriod
setStartField(Integer value
, DateTimeFieldType type
)
440 throws IndexOutOfBoundsException
{
441 start
= setPartialField(start
, value
, type
);
446 private TimePeriod
setEndField(Integer value
, DateTimeFieldType type
)
447 throws IndexOutOfBoundsException
{
448 end
= setPartialField(getEnd(), value
, type
);
452 public static Partial
setPartialField(Partial partial
, Integer value
, DateTimeFieldType type
)
453 throws IndexOutOfBoundsException
{
454 if (partial
== null){
455 partial
= new Partial();
458 return partial
.without(type
);
460 checkFieldValues(value
, type
, partial
);
461 return partial
.with(type
, value
);
466 // ******************************** internal methods *******************************/
469 * Throws an IndexOutOfBoundsException if the value does not have a valid value
470 * (e.g. month > 12, month < 1, day > 31, etc.)
473 * @throws IndexOutOfBoundsException
475 private static void checkFieldValues(Integer value
, DateTimeFieldType type
, Partial partial
)
476 throws IndexOutOfBoundsException
{
478 if (type
.equals(MONTH_TYPE
)){
481 if (type
.equals(DAY_TYPE
)){
483 Integer month
= null;
484 if (partial
.isSupported(MONTH_TYPE
)){
485 month
= partial
.get(MONTH_TYPE
);
490 }else if (month
== 4 ||month
== 6 ||month
== 9 ||month
== 11){
495 if ( (value
< 1 || value
> max
) ){
496 throw new IndexOutOfBoundsException("Value must be between 1 and " + max
);
500 //**************************** to String ****************************************
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.
506 * @see java.lang.Object#toString()
509 public String
toString(){
510 return formatter
.format(this);
514 * Returns the concatenation of <code>start</code> and <code>end</code>
516 public String
getTimePeriod(){
517 return formatter
.getTimePeriod(this);
521 public String
getYear(){
522 return formatter
.getYear(this);
526 public boolean checkEmpty() {
527 //TODO unify isEmpty && checkEmpty
531 protected boolean isBlank(String str
) {
532 return StringUtils
.isBlank(str
);
535 protected boolean isEmpty(Partial partial
) {
536 return partial
== null?
true : partial
.getFields().length
== 0;
539 //*********** EQUALS **********************************/
542 public boolean equals(Object obj
) {
546 if (! (obj
instanceof TimePeriod
)){
549 TimePeriod that
= (TimePeriod
)obj
;
551 if (! CdmUtils
.nullSafeEqual(this.start
, that
.start
)){
554 if (! CdmUtils
.nullSafeEqual(this.end
, that
.end
)){
557 if (! CdmUtils
.nullSafeEqual(this.freeText
, that
.freeText
)){
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
)){
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
)){
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
)){
589 public int hashCode() {
591 hashCode
= 29*hashCode
+
592 (start
== null?
33: start
.hashCode()) +
593 (end
== null?
39: end
.hashCode()) +
594 (freeText
== null?
41: freeText
.hashCode());
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>
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>
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
) {
619 }else if (tp1Empty
|| tp2Empty
) {
622 return timePeriod1
.equals(timePeriod2
);
626 //*********** CLONE **********************************/
629 public TimePeriod
clone() {
631 TimePeriod result
= (TimePeriod
)super.clone();
632 copyCloned(this, result
);
634 } catch (CloneNotSupportedException e
) {
635 logger
.warn("Clone not supported exception. Should never occurr !!");
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
);