001    package org.nakedobjects.applib.value;
002    
003    import java.lang.reflect.InvocationTargetException;
004    import java.lang.reflect.Method;
005    import java.text.DateFormat;
006    import java.util.Calendar;
007    import java.util.TimeZone;
008    
009    import org.nakedobjects.applib.annotation.Value;
010    import org.nakedobjects.applib.clock.Clock;
011    
012    
013    /**
014     * Value object representing a date (not time) value.
015     * 
016     * <p>
017     * TODO: other methods to implement comparison methods:
018     * <ul>
019     * <li>sameDateAs() day == day & month == month & year == year</li>
020     * <li>withinNextDatePeriod(int days, int months, int years)</li>
021     * <li>withinDatePeriod(int days, int months, int years)</li>
022     * <li>withinPreviousDatePeriod(int days, int months, int years)</li>
023     * </ul>
024     */
025    @Value(semanticsProviderName = "org.nakedobjects.metamodel.value.DateValueSemanticsProvider")
026    public class Date extends Magnitude {
027        private static final long serialVersionUID = 1L;
028        private static final TimeZone UTC_TIME_ZONE;
029        private static final DateFormat MEDIUM_FORMAT = DateFormat.getDateInstance(DateFormat.MEDIUM);
030        private final java.util.Date date;
031    
032        static {
033            // for dotnet compatibility -
034            TimeZone timeZone = TimeZone.getTimeZone("Etc/UTC");
035            if (timeZone == null) {
036                // for dotnet compatibility - "Etc/UTC fails in dotnet
037                timeZone = TimeZone.getTimeZone("UTC");
038            }
039            UTC_TIME_ZONE = timeZone;
040        }
041    
042        /**
043         * Create a Date object for today's date.
044         */
045        public Date() {
046            final Calendar cal = Calendar.getInstance();
047            cal.setTimeZone(UTC_TIME_ZONE);
048            final java.util.Date d = new java.util.Date(Clock.getTime());
049            cal.setTime(d);
050            clearTime(cal);
051            date = cal.getTime();
052        }
053    
054        /**
055         * Create a Date object set to the specified day, month and year.
056         */
057        public Date(final int year, final int month, final int day) {
058            checkDate(year, month, day);
059            final Calendar cal = Calendar.getInstance();
060            cal.setTimeZone(UTC_TIME_ZONE);
061            clearTime(cal);
062            cal.set(year, month - 1, day);
063            date = cal.getTime();
064        }
065    
066        /**
067         * Create a Date object based on the specified Java date object. The time portion of the Java date is
068         * disposed of.
069         */
070        public Date(final java.util.Date date) {
071            final Calendar cal = Calendar.getInstance();
072            cal.setTimeZone(UTC_TIME_ZONE);
073    
074            cal.setTime(date);
075            cal.set(Calendar.HOUR, 0);
076            cal.set(Calendar.MINUTE, 0);
077            cal.set(Calendar.SECOND, 0);
078            cal.set(Calendar.MILLISECOND, 0);
079            this.date = cal.getTime();
080        }
081    
082        protected Date createDate(final java.util.Date time) {
083            return new Date(time);
084        }
085    
086        /**
087         * Add the specified days, years and months to this date value and return a new date object containing the
088         * result.
089         */
090        public Date add(final int years, final int months, final int days) {
091            final Calendar cal = Calendar.getInstance();
092            cal.setTimeZone(UTC_TIME_ZONE);
093            cal.setTime(date);
094            cal.add(Calendar.DAY_OF_MONTH, days);
095            cal.add(Calendar.MONTH, months);
096            cal.add(Calendar.YEAR, years);
097            return createDate(cal.getTime());
098        }
099    
100        private void checkDate(final int year, final int month, final int day) {
101            if ((month < 1) || (month > 12)) {
102                throw new IllegalArgumentException("Month must be in the range 1 - 12 inclusive");
103            }
104            final Calendar cal = Calendar.getInstance();
105            cal.setTimeZone(UTC_TIME_ZONE);
106            cal.set(year, month - 1, 0);
107            final int lastDayOfMonth = cal.getMaximum(Calendar.DAY_OF_MONTH);
108            if ((day < 1) || (day > lastDayOfMonth)) {
109                throw new IllegalArgumentException("Day must be in the range 1 - " + lastDayOfMonth + " inclusive: " + day);
110            }
111        }
112    
113        /**
114         * clear all aspects of the time that are not used
115         */
116        private void clearTime(final Calendar cal) {
117            cal.set(Calendar.HOUR, 0);
118            cal.set(Calendar.HOUR_OF_DAY, 0);
119            cal.set(Calendar.MINUTE, 0);
120            cal.set(Calendar.SECOND, 0);
121            cal.set(Calendar.AM_PM, 0);
122            cal.set(Calendar.MILLISECOND, 0);
123        }
124    
125        /**
126         * Return this date value as a Java Date object.
127         * 
128         * @see java.util.Date
129         */
130        public java.util.Date dateValue() {
131            return new java.util.Date(date.getTime());
132        }
133    
134        private int getEndDayOfMonthOneDotOne(final Calendar originalCalendar) {
135            final Calendar newCalendar = Calendar.getInstance();
136            newCalendar.setTimeZone(originalCalendar.getTimeZone());
137            newCalendar.setTime(originalCalendar.getTime());
138    
139            final int firstPossibleDay = originalCalendar.getLeastMaximum(Calendar.DAY_OF_MONTH);
140            final int lastPossibleDay = originalCalendar.getMaximum(Calendar.DAY_OF_MONTH);
141            int lastValidDay = firstPossibleDay;
142    
143            for (int day = firstPossibleDay + 1; day < lastPossibleDay; day++) {
144                newCalendar.set(Calendar.DAY_OF_MONTH, day);
145                if (newCalendar.get(Calendar.MONTH) != originalCalendar.get(Calendar.MONTH)) {
146                    return lastValidDay;
147                }
148                lastValidDay = day;
149            }
150            return lastPossibleDay;
151        }
152    
153        private int getEndDayOfMonth(final Calendar originalCalendar) {
154    
155            final Class<?> cls = originalCalendar.getClass();
156            try {
157                final Method getActualMaximum = cls.getMethod("getActualMaximum", new Class[] { int.class });
158                return ((Integer) getActualMaximum.invoke(originalCalendar, new Object[] { new Integer(Calendar.DAY_OF_MONTH) }))
159                        .intValue();
160            } catch (final NoSuchMethodException ignore) {
161                // expected if pre java 1.2 - fall through
162            } catch (final IllegalAccessException e) {
163                throw new RuntimeException(e.getMessage());
164            } catch (final InvocationTargetException e) {
165                throw new RuntimeException(e.getMessage());
166            }
167            return getEndDayOfMonthOneDotOne(originalCalendar);
168        }
169    
170        /**
171         * Calculates, and returns, a date representing the last day of the month relative to the current date.
172         * 
173         * @author Joshua Cassidy
174         */
175        public Date endOfMonth() {
176            final Calendar c = Calendar.getInstance();
177            c.setTimeZone(UTC_TIME_ZONE);
178            c.setTime(date);
179            c.set(Calendar.DAY_OF_MONTH, getEndDayOfMonth(c));
180            return createDate(c.getTime());
181        }
182    
183        @Override
184        public boolean equals(final Object obj) {
185            if (obj instanceof Date) {
186                final Date d = (Date) obj;
187                return d.date.equals(date);
188            }
189            return super.equals(obj);
190        }
191    
192        /**
193         * Return the day from this date, in the range 1 - 31.
194         */
195        public int getDay() {
196            final Calendar c = Calendar.getInstance();
197            c.setTimeZone(UTC_TIME_ZONE);
198            c.setTime(date);
199            return c.get(Calendar.DAY_OF_MONTH);
200        }
201    
202        /**
203         * Calculates, and returns, an int representing the day of the week relative to the current date. With Mon =
204         * 0 through to Sun = 6
205         * 
206         * @author Joshua Cassidy
207         */
208        public int getDayOfWeek() {
209            final Calendar c = Calendar.getInstance();
210            c.setTime(date);
211            final int day = c.get(Calendar.DAY_OF_WEEK);
212            // Calendar day is 1 - 7 for sun - sat
213            if (day == 1) {
214                return 6;
215            } else {
216                return day - 2;
217            }
218        }
219    
220        /**
221         * Return the month from this date, in the range 1 - 12.
222         */
223        public int getMonth() {
224            final Calendar c = Calendar.getInstance();
225            c.setTimeZone(UTC_TIME_ZONE);
226            c.setTime(date);
227            return c.get(Calendar.MONTH) + 1;
228        }
229    
230        /**
231         * Return the year from this date.
232         */
233        public int getYear() {
234            final Calendar c = Calendar.getInstance();
235            c.setTimeZone(UTC_TIME_ZONE);
236            c.setTime(date);
237            return c.get(Calendar.YEAR);
238        }
239    
240        /**
241         * Returns true if the date of this object has the same value as the specified date
242         */
243        @Override
244        public boolean isEqualTo(final Magnitude date) {
245            if (date instanceof Date) {
246                return this.date.equals(((Date) date).date);
247            } else {
248                throw new IllegalArgumentException("Parameter must be of type Time");
249            }
250        }
251    
252        /**
253         * Returns true if the time of this object is earlier than the specified time
254         */
255        @Override
256        public boolean isLessThan(final Magnitude date) {
257            if (date instanceof Date) {
258                return this.date.before(((Date) date).date);
259            } else {
260                throw new IllegalArgumentException("Parameter must be of type Time");
261            }
262        }
263    
264        private boolean sameAs(final Date as, final int field) {
265            final Calendar c = Calendar.getInstance();
266            c.setTimeZone(UTC_TIME_ZONE);
267            c.setTime(date);
268    
269            final Calendar c2 = Calendar.getInstance();
270            c2.setTimeZone(UTC_TIME_ZONE);
271            c2.setTime(as.date);
272    
273            return c.get(field) == c2.get(field);
274        }
275    
276        /**
277         * Determines if this date and the specified date represent the same day of the month, eg both dates are
278         * for the 3rd.
279         */
280        public boolean sameDayOfMonthAs(final Date as) {
281            return sameAs(as, Calendar.DAY_OF_MONTH);
282        }
283    
284        /**
285         * Determines if this date and the specified date represent the same day of the week, eg both dates are on
286         * a Tuesday.
287         */
288        public boolean sameDayOfWeekAs(final Date as) {
289            return sameAs(as, Calendar.DAY_OF_WEEK);
290        }
291    
292        /**
293         * Determines if this date and the specified date represent the same day of the year, eg both dates are
294         * for the 108th day of the year.
295         */
296        public boolean sameDayOfYearAs(final Date as) {
297            return sameAs(as, Calendar.DAY_OF_YEAR);
298        }
299    
300        /**
301         * Determines if this date and the specified date represent the same month, eg both dates are for the
302         * March.
303         */
304        public boolean sameMonthAs(final Date as) {
305            return sameAs(as, Calendar.MONTH);
306        }
307    
308        /**
309         * Determines if this date and the specified date represent the same week in the year, eg both dates are
310         * the for the 18th week of the year.
311         */
312        public boolean sameWeekAs(final Date as) {
313            return sameAs(as, Calendar.WEEK_OF_YEAR);
314        }
315    
316        /**
317         * Determines if this date and the specified date represent the same year.
318         */
319        public boolean sameYearAs(final Date as) {
320            return sameAs(as, Calendar.YEAR);
321        }
322    
323        /**
324         * Calculates, and returns, a date representing the first day of the month relative to the current date.
325         */
326        public Date startOfMonth() {
327            final Calendar c = Calendar.getInstance();
328            c.setTimeZone(UTC_TIME_ZONE);
329            c.setTime(date);
330            c.set(Calendar.DAY_OF_MONTH, c.getMinimum(Calendar.DAY_OF_MONTH));
331            return createDate(c.getTime());
332        }
333    
334        /**
335         * Calculates, and returns, a date representing the first day of the week relative to the current date.
336         */
337        public Date startOfWeek() {
338            final Calendar c = Calendar.getInstance();
339            c.setTimeZone(UTC_TIME_ZONE);
340            c.setTime(date);
341            c.set(Calendar.DAY_OF_WEEK, c.getFirstDayOfWeek());
342            return createDate(c.getTime());
343        }
344    
345        /**
346         * Calculates, and returns, a date representing the first day of the year relative to the current date.
347         */
348        public Date startOfYear() {
349            final Calendar c = Calendar.getInstance();
350            c.setTimeZone(UTC_TIME_ZONE);
351            c.setTime(date);
352            c.set(Calendar.DAY_OF_YEAR, c.getMinimum(Calendar.DAY_OF_YEAR));
353            return createDate(c.getTime());
354        }
355    
356        public String title() {
357            return MEDIUM_FORMAT.format(date);
358        }
359    
360        @Override
361        public String toString() {
362            return getYear() + "-" + getMonth() + "-" + getDay();
363        }
364    }
365    // Copyright (c) Naked Objects Group Ltd.