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
.text
.DateFormat
;
14 import java
.text
.ParsePosition
;
15 import java
.util
.Calendar
;
16 import java
.util
.Date
;
17 import java
.util
.regex
.Matcher
;
18 import java
.util
.regex
.Pattern
;
20 import javax
.persistence
.Embeddable
;
21 import javax
.persistence
.Transient
;
22 import javax
.xml
.bind
.annotation
.XmlAccessType
;
23 import javax
.xml
.bind
.annotation
.XmlAccessorType
;
24 import javax
.xml
.bind
.annotation
.XmlElement
;
25 import javax
.xml
.bind
.annotation
.XmlRootElement
;
26 import javax
.xml
.bind
.annotation
.XmlType
;
27 import javax
.xml
.bind
.annotation
.adapters
.XmlJavaTypeAdapter
;
29 import org
.apache
.log4j
.Logger
;
30 import org
.hibernate
.annotations
.Type
;
31 import org
.hibernate
.search
.annotations
.Analyze
;
32 import org
.hibernate
.search
.annotations
.Field
;
33 import org
.hibernate
.search
.annotations
.FieldBridge
;
34 import org
.joda
.time
.DateTime
;
35 import org
.joda
.time
.DateTimeFieldType
;
36 import org
.joda
.time
.LocalDate
;
37 import org
.joda
.time
.Partial
;
38 import org
.joda
.time
.ReadableInstant
;
39 import org
.joda
.time
.ReadablePartial
;
40 import org
.joda
.time
.format
.DateTimeFormatter
;
42 import eu
.etaxonomy
.cdm
.common
.CdmUtils
;
43 import eu
.etaxonomy
.cdm
.hibernate
.search
.PartialBridge
;
44 import eu
.etaxonomy
.cdm
.jaxb
.PartialAdapter
;
49 * @created 08-Nov-2007 13:07:00
50 * @updated 05-Dec-2008 23:00:05
52 @XmlAccessorType(XmlAccessType
.FIELD
)
53 @XmlType(name
= "TimePeriod", propOrder
= {
58 @XmlRootElement(name
= "TimePeriod")
60 public class TimePeriod
implements Cloneable
, Serializable
{
61 private static final Logger logger
= Logger
.getLogger(TimePeriod
.class);
62 public static final DateTimeFieldType MONTH_TYPE
= DateTimeFieldType
.monthOfYear();
63 public static final DateTimeFieldType YEAR_TYPE
= DateTimeFieldType
.year();
64 public static final DateTimeFieldType DAY_TYPE
= DateTimeFieldType
.dayOfMonth();
66 @XmlElement(name
= "Start")
67 @XmlJavaTypeAdapter(value
= PartialAdapter
.class)
68 @Type(type
="partialUserType")
69 @Field(analyze
= Analyze
.NO
)
70 @FieldBridge(impl
= PartialBridge
.class)
71 private Partial start
;
73 @XmlElement(name
= "End")
74 @XmlJavaTypeAdapter(value
= PartialAdapter
.class)
75 @Type(type
="partialUserType")
76 @Field(analyze
= Analyze
.NO
)
77 @FieldBridge(impl
= PartialBridge
.class)
81 @XmlElement(name
= "FreeText")
82 private String freeText
;
89 public static TimePeriod
NewInstance(){
90 return new TimePeriod();
98 public static TimePeriod
NewInstance(Partial startDate
){
99 return new TimePeriod(startDate
);
107 public static TimePeriod
NewInstance(Partial startDate
, Partial endDate
){
108 return new TimePeriod(startDate
, endDate
);
116 public static TimePeriod
NewInstance(Integer year
){
117 Integer endYear
= null;
118 return NewInstance(year
, endYear
);
125 public static TimePeriod
NewInstance(Integer startYear
, Integer endYear
){
126 Partial startDate
= null;
127 Partial endDate
= null;
128 if (startYear
!= null){
129 startDate
= new Partial().with(YEAR_TYPE
, startYear
);
131 if (endYear
!= null){
132 endDate
= new Partial().with(YEAR_TYPE
, endYear
);
134 return new TimePeriod(startDate
, endDate
);
140 * Factory method to create a TimePeriod from a <code>Calendar</code>. The Calendar is stored as the starting instant.
143 public static TimePeriod
NewInstance(Calendar startCalendar
){
144 return NewInstance(startCalendar
, null);
148 * Factory method to create a TimePeriod from a <code>ReadableInstant</code>(e.g. <code>DateTime</code>).
149 * The <code>ReadableInstant</code> is stored as the starting instant.
152 public static TimePeriod
NewInstance(ReadableInstant readableInstant
){
153 return NewInstance(readableInstant
, null);
157 * Factory method to create a TimePeriod from a starting and an ending <code>Calendar</code>
160 public static TimePeriod
NewInstance(Calendar startCalendar
, Calendar endCalendar
){
161 Partial startDate
= null;
162 Partial endDate
= null;
163 if (startCalendar
!= null){
164 startDate
= calendarToPartial(startCalendar
);
166 if (endCalendar
!= null){
167 endDate
= calendarToPartial(endCalendar
);
169 return new TimePeriod(startDate
, endDate
);
173 * Factory method to create a TimePeriod from a starting and an ending <code>Date</code>
176 public static TimePeriod
NewInstance(Date startDate
, Date endDate
){
177 //TODO conversion untested, implemented according to http://www.roseindia.net/java/java-conversion/datetocalender.shtml
178 Calendar calStart
= null;
179 Calendar calEnd
= null;
180 if (startDate
!= null){
181 calStart
= Calendar
.getInstance();
182 calStart
.setTime(startDate
);
184 if (endDate
!= null){
185 calEnd
= Calendar
.getInstance();
186 calEnd
.setTime(endDate
);
188 return NewInstance(calStart
, calEnd
);
193 * Factory method to create a TimePeriod from a starting and an ending <code>ReadableInstant</code>(e.g. <code>DateTime</code>)
196 public static TimePeriod
NewInstance(ReadableInstant startInstant
, ReadableInstant endInstant
){
197 Partial startDate
= null;
198 Partial endDate
= null;
199 if (startInstant
!= null){
200 startDate
= readableInstantToPartial(startInstant
);
202 if (endInstant
!= null){
203 endDate
= readableInstantToPartial(endInstant
);
205 return new TimePeriod(startDate
, endDate
);
210 * Transforms a <code>Calendar</code> into a <code>Partial</code>
214 public static Partial
calendarToPartial(Calendar calendar
){
215 LocalDate ld
= new LocalDate(calendar
);
216 Partial partial
= new Partial(ld
);
221 * Transforms a <code>Calendar</code> into a <code>Partial</code>
225 public static Partial
readableInstantToPartial(ReadableInstant readableInstant
){
226 DateTime dt
= readableInstant
.toInstant().toDateTime();
227 LocalDate ld
= dt
.toLocalDate();
228 Partial partial
= new Partial(ld
);
235 protected TimePeriod() {
238 public TimePeriod(Partial startDate
) {
241 public TimePeriod(Partial startDate
, Partial endDate
) {
247 * True, if this time period represents a period not a single point in time.
248 * This is by definition, that the time period has a start and an end value,
249 * and both have a year value that is not null
253 public boolean isPeriod(){
254 if (getStartYear() != null && getEndYear() != null ){
262 * True, if there is no start date and no end date and no freetext representation exists.
266 public boolean isEmpty(){
267 if (CdmUtils
.isEmpty(this.getFreeText()) && start
== null && end
== null ){
275 public Partial
getStart() {
279 public void setStart(Partial start
) {
283 public Partial
getEnd() {
287 public void setEnd(Partial end
) {
292 * For time periods that need to store more information than the one
293 * that can be stored in <code>start</code> and <code>end</code>.
294 * If free text is not <code>null</null> {@link #toString()} will always
295 * return the free text value.
296 * <BR>Use {@link #toString()} for public use.
297 * @return the freeText
299 public String
getFreeText() {
305 * Use {@link #parseSingleDate(String)} for public use.
306 * @param freeText the freeText to set
308 public void setFreeText(String freeText
) {
309 this.freeText
= freeText
;
314 public String
getYear(){
316 if (getStartYear() != null){
317 result
+= String
.valueOf(getStartYear());
318 if (getEndYear() != null){
319 result
+= "-" + String
.valueOf(getEndYear());
322 if (getEndYear() != null){
323 result
+= String
.valueOf(getEndYear());
330 public Integer
getStartYear(){
331 return getPartialValue(start
, YEAR_TYPE
);
335 public Integer
getStartMonth(){
336 return getPartialValue(start
, MONTH_TYPE
);
340 public Integer
getStartDay(){
341 return getPartialValue(start
, DAY_TYPE
);
345 public Integer
getEndYear(){
346 return getPartialValue(end
, YEAR_TYPE
);
350 public Integer
getEndMonth(){
351 return getPartialValue(end
, MONTH_TYPE
);
355 public Integer
getEndDay(){
356 return getPartialValue(end
, DAY_TYPE
);
359 public static Integer
getPartialValue(Partial partial
, DateTimeFieldType type
){
360 if (partial
== null || ! partial
.isSupported(type
)){
363 return partial
.get(type
);
368 public TimePeriod
setStartYear(Integer year
){
369 return setStartField(year
, YEAR_TYPE
);
372 public TimePeriod
setStartMonth(Integer month
) throws IndexOutOfBoundsException
{
373 return setStartField(month
, MONTH_TYPE
);
376 public TimePeriod
setStartDay(Integer day
) throws IndexOutOfBoundsException
{
377 return setStartField(day
, DAY_TYPE
);
380 public TimePeriod
setEndYear(Integer year
){
381 return setEndField(year
, YEAR_TYPE
);
384 public TimePeriod
setEndMonth(Integer month
) throws IndexOutOfBoundsException
{
385 return setEndField(month
, MONTH_TYPE
);
388 public TimePeriod
setEndDay(Integer day
) throws IndexOutOfBoundsException
{
389 return setEndField(day
, DAY_TYPE
);
392 public static Partial
setPartialField(Partial partial
, Integer value
, DateTimeFieldType type
)
393 throws IndexOutOfBoundsException
{
394 if (partial
== null){
395 partial
= new Partial();
398 return partial
.without(type
);
400 checkFieldValues(value
, type
, partial
);
401 return partial
.with(type
, value
);
405 private TimePeriod
setStartField(Integer value
, DateTimeFieldType type
)
406 throws IndexOutOfBoundsException
{
407 start
= setPartialField(start
, value
, type
);
411 private TimePeriod
setEndField(Integer value
, DateTimeFieldType type
)
412 throws IndexOutOfBoundsException
{
413 end
= setPartialField(end
, value
, type
);
418 * Throws an IndexOutOfBoundsException if the value does not have a valid value
419 * (e.g. month > 12, month < 1, day > 31, etc.)
422 * @throws IndexOutOfBoundsException
424 private static void checkFieldValues(Integer value
, DateTimeFieldType type
, Partial partial
)
425 throws IndexOutOfBoundsException
{
427 if (type
.equals(MONTH_TYPE
)){
430 if (type
.equals(DAY_TYPE
)){
432 Integer month
= null;
433 if (partial
.isSupported(MONTH_TYPE
)){
434 month
= partial
.get(MONTH_TYPE
);
439 }else if (month
== 4 ||month
== 6 ||month
== 9 ||month
== 11){
444 if ( (value
< 1 || value
> max
) ){
445 throw new IndexOutOfBoundsException("Value must be between 1 and " + max
);
449 private void initStart(){
451 start
= new Partial();
455 private void initEnd(){
462 //patter for first year in string;
463 private static final Pattern firstYearPattern
= Pattern
.compile("\\d{4}");
465 private static final Pattern uncorrectYearPatter
= Pattern
.compile("\"\\d{4}\"\\s*\\[\\d{4}\\]");
466 //case fl. 1806 or c. 1806 or fl. 1806?
467 private static final Pattern prefixedYearPattern
= Pattern
.compile("(fl|c)\\.\\s*\\d{4}(\\s*-\\s*\\d{4})?\\??");
469 private static final Pattern standardPattern
= Pattern
.compile("\\s*\\d{2,4}(\\s*-(\\s*\\d{2,4})?)?");
470 private static final String strDotDate
= "[0-3]?\\d\\.[01]?\\d\\.\\d{4,4}";
471 private static final String strDotDatePeriodPattern
= String
.format("%s(\\s*-\\s*%s?)?", strDotDate
, strDotDate
);
472 private static final Pattern dotDatePattern
= Pattern
.compile(strDotDatePeriodPattern
);
475 public static TimePeriod
parseString(TimePeriod timePeriod
, String periodString
){
476 //TODO move to parser class
477 //TODO until now only quick and dirty (and partly wrong)
478 TimePeriod result
= timePeriod
;
480 if(timePeriod
== null){
484 if (periodString
== null){
487 periodString
= periodString
.trim();
489 result
.setFreeText(null);
493 if (uncorrectYearPatter
.matcher(periodString
).matches()){
494 result
.setFreeText(periodString
);
495 String realYear
= periodString
.split("\\[")[1];
496 realYear
= realYear
.replace("]", "");
497 result
.setStartYear(Integer
.valueOf(realYear
));
498 result
.setFreeText(periodString
);
499 //case fl. 1806 or c. 1806 or fl. 1806?
500 }else if(prefixedYearPattern
.matcher(periodString
).matches()){
501 result
.setFreeText(periodString
);
502 Matcher yearMatcher
= firstYearPattern
.matcher(periodString
);
504 String startYear
= yearMatcher
.group();
505 result
.setStartYear(Integer
.valueOf(startYear
));
506 if (yearMatcher
.find()){
507 String endYear
= yearMatcher
.group();
508 result
.setEndYear(Integer
.valueOf(endYear
));
510 }else if (dotDatePattern
.matcher(periodString
).matches()){
511 parseDotDatePattern(periodString
, result
);
512 }else if (standardPattern
.matcher(periodString
).matches()){
513 parseStandardPattern(periodString
, result
);
514 //TODO first check ambiguity of parser results e.g. for 7/12/11
515 // }else if (isDateString(periodString)){
516 // String[] startEnd = makeStartEnd(periodString);
517 // String start = startEnd[0];
518 // DateTime startDateTime = dateStringParse(start, true);
519 // result.setStart(startDateTime);
520 // if (startEnd.length > 1){
521 // DateTime endDateTime = dateStringParse(startEnd[1], true);
523 // result.setEnd(endDateTime.toLocalDate());
527 result
.setFreeText(periodString
);
532 private static boolean isDateString(String periodString
) {
533 String
[] startEnd
= makeStartEnd(periodString
);
534 String start
= startEnd
[0];
535 DateTime startDateTime
= dateStringParse(start
, true);
536 if (startDateTime
== null){
539 if (startEnd
.length
> 1){
540 DateTime endDateTime
= dateStringParse(startEnd
[1], true);
541 if (endDateTime
!= null){
550 * @param periodString
553 private static String
[] makeStartEnd(String periodString
) {
554 String
[] startEnd
= new String
[]{periodString
};
555 if (periodString
.contains("-") && periodString
.matches("^-{2,}-^-{2,}")){
556 startEnd
= periodString
.split("-");
562 private static DateTime
dateStringParse(String string
, boolean strict
) {
563 DateFormat dateFormat
= DateFormat
.getDateInstance();
564 ParsePosition pos
= new ParsePosition(0);
565 Date a
= dateFormat
.parse(string
, pos
);
566 if (a
== null || pos
.getIndex() != string
.length()){
569 Calendar cal
= Calendar
.getInstance();
571 DateTime result
= new DateTime(cal
);
577 * @param periodString
580 private static void parseDotDatePattern(String periodString
,TimePeriod result
) {
581 String
[] dates
= periodString
.split("-");
582 Partial dtStart
= null;
583 Partial dtEnd
= null;
585 if (dates
.length
> 2 || dates
.length
<= 0){
586 logger
.warn("More than 1 '-' in period String: " + periodString
);
587 result
.setFreeText(periodString
);
591 if (! CdmUtils
.isEmpty(dates
[0])){
592 dtStart
= parseSingleDotDate(dates
[0].trim());
596 if (dates
.length
>= 2 && ! CdmUtils
.isEmpty(dates
[1])){
597 dtEnd
= parseSingleDotDate(dates
[1].trim());
600 result
.setStart(dtStart
);
601 result
.setEnd(dtEnd
);
602 } catch (IllegalArgumentException e
) {
603 //logger.warn(e.getMessage());
604 result
.setFreeText(periodString
);
611 * @param periodString
614 private static void parseStandardPattern(String periodString
,
616 String
[] years
= periodString
.split("-");
617 Partial dtStart
= null;
618 Partial dtEnd
= null;
620 if (years
.length
> 2 || years
.length
<= 0){
621 logger
.warn("More than 1 '-' in period String: " + periodString
);
625 if (! CdmUtils
.isEmpty(years
[0])){
626 dtStart
= parseSingleDate(years
[0].trim());
630 if (years
.length
>= 2 && ! CdmUtils
.isEmpty(years
[1])){
631 years
[1] = years
[1].trim();
632 if (years
[1].length()==2 && dtStart
!= null && dtStart
.isSupported(DateTimeFieldType
.year())){
633 years
[1] = String
.valueOf(dtStart
.get(DateTimeFieldType
.year())/100) + years
[1];
635 dtEnd
= parseSingleDate(years
[1]);
638 result
.setStart(dtStart
);
639 result
.setEnd(dtEnd
);
640 } catch (IllegalArgumentException e
) {
641 //logger.warn(e.getMessage());
642 result
.setFreeText(periodString
);
647 public static TimePeriod
parseString(String strPeriod
) {
648 TimePeriod timePeriod
= TimePeriod
.NewInstance();
649 return parseString(timePeriod
, strPeriod
);
653 protected static Partial
parseSingleDate(String singleDateString
) throws IllegalArgumentException
{
654 //FIXME until now only quick and dirty and incomplete
655 Partial partial
= new Partial();
656 singleDateString
= singleDateString
.trim();
657 if (CdmUtils
.isNumeric(singleDateString
)){
659 Integer year
= Integer
.valueOf(singleDateString
.trim());
660 if (year
< 1000 && year
> 2100){
661 logger
.warn("Not a valid year: " + year
+ ". Year must be between 1000 and 2100");
662 }else if (year
< 1700 && year
> 2100){
663 logger
.warn("Not a valid taxonomic year: " + year
+ ". Year must be between 1750 and 2100");
664 partial
= partial
.with(YEAR_TYPE
, year
);
666 partial
= partial
.with(YEAR_TYPE
, year
);
668 } catch (NumberFormatException e
) {
669 logger
.debug("Not a Integer format in getCalendar()");
670 throw new IllegalArgumentException(e
);
673 throw new IllegalArgumentException("Until now only years can be parsed as single dates. But date is: " + singleDateString
);
679 protected static Partial
parseSingleDotDate(String singleDateString
) throws IllegalArgumentException
{
680 Partial partial
= new Partial();
681 singleDateString
= singleDateString
.trim();
682 String
[] split
= singleDateString
.split("\\.");
683 int length
= split
.length
;
685 throw new IllegalArgumentException(String
.format("More than 2 dots in date '%s'", singleDateString
));
687 String strYear
= split
[split
.length
-1];
688 String strMonth
= length
>= 2? split
[split
.length
-2]: null;
689 String strDay
= length
>= 3? split
[split
.length
-3]: null;
693 Integer year
= Integer
.valueOf(strYear
.trim());
694 Integer month
= Integer
.valueOf(strMonth
.trim());
695 Integer day
= Integer
.valueOf(strDay
.trim());
696 if (year
< 1000 && year
> 2100){
697 logger
.warn("Not a valid year: " + year
+ ". Year must be between 1000 and 2100");
698 }else if (year
< 1700 && year
> 2100){
699 logger
.warn("Not a valid taxonomic year: " + year
+ ". Year must be between 1750 and 2100");
700 partial
= partial
.with(YEAR_TYPE
, year
);
702 partial
= partial
.with(YEAR_TYPE
, year
);
704 if (month
!= null && month
!= 0){
705 partial
= partial
.with(MONTH_TYPE
, month
);
707 if (day
!= null && day
!= 0){
708 partial
= partial
.with(DAY_TYPE
, day
);
710 } catch (NumberFormatException e
) {
711 logger
.debug("Not a Integer format somewhere in " + singleDateString
);
712 throw new IllegalArgumentException(e
);
720 private class TimePeriodPartialFormatter
extends DateTimeFormatter
{
721 private TimePeriodPartialFormatter(){
725 public String
print(ReadablePartial partial
){
728 String year
= (partial
.isSupported(YEAR_TYPE
))? String
.valueOf(partial
.get(YEAR_TYPE
)):null;
729 String month
= (partial
.isSupported(MONTH_TYPE
))? String
.valueOf(partial
.get(MONTH_TYPE
)):null;;
730 String day
= (partial
.isSupported(DAY_TYPE
))? String
.valueOf(partial
.get(DAY_TYPE
)):null;;
745 result
= (day
!= null)? day
+ "." : "";
746 result
+= (month
!= null)? month
+ "." : "";
747 result
+= (year
!= null)? year
: "";
754 //**************************** to String ****************************************
757 * Returns the {@link #getFreeText()} value if free text is not <code>null</code>.
758 * Otherwise the concatenation of <code>start</code> and <code>end</code> is returned.
760 * @see java.lang.Object#toString()
763 public String
toString(){
764 String result
= null;
765 DateTimeFormatter formatter
= new TimePeriodPartialFormatter();
766 if ( CdmUtils
.isNotEmpty(this.getFreeText())){
767 result
= this.getFreeText();
769 String strStart
= start
!= null ? start
.toString(formatter
): null;
770 String strEnd
= end
!= null ? end
.toString(formatter
): null;
771 result
= CdmUtils
.concat("-", strStart
, strEnd
);
776 //*********** EQUALS **********************************/
780 * @see java.lang.Object#equals(java.lang.Object)
783 public boolean equals(Object obj
) {
787 if (! (obj
instanceof TimePeriod
)){
790 TimePeriod that
= (TimePeriod
)obj
;
792 if (! CdmUtils
.nullSafeEqual(this.start
, that
.start
)){
795 if (! CdmUtils
.nullSafeEqual(this.end
, that
.end
)){
798 if (! CdmUtils
.nullSafeEqual(this.freeText
, that
.freeText
)){
805 * @see java.lang.Object#hashCode()
808 public int hashCode() {
810 hashCode
= 29*hashCode
+
811 (start
== null?
33: start
.hashCode()) +
812 (end
== null?
39: end
.hashCode()) +
813 (freeText
== null?
41: freeText
.hashCode());
814 return super.hashCode();
818 //*********** CLONE **********************************/
822 * @see java.lang.Object#clone()
825 public Object
clone() {
827 TimePeriod result
= (TimePeriod
)super.clone();
828 result
.setStart(this.start
); //DateTime is immutable
829 result
.setEnd(this.end
);
830 result
.setFreeText(this.freeText
);
832 } catch (CloneNotSupportedException e
) {
833 logger
.warn("Clone not supported exception. Should never occurr !!");