001/*
002 * #%L
003 * Netarchivesuite - harvester
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
012 * 
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023
024package dk.netarkivet.harvester.datamodel;
025
026import java.util.Calendar;
027import java.util.Date;
028import java.util.GregorianCalendar;
029
030import dk.netarkivet.common.exceptions.ArgumentNotValid;
031
032/**
033 * This class implements a frequency of a number of months.
034 */
035
036public class MonthlyFrequency extends Frequency {
037
038    /** The minute of the hour the event should happen at. */
039    private int minute;
040    /** The hour of the day the event should happen at. */
041    private int hour;
042    /** The day of the month the event should happen at. */
043    private int dayOfMonth;
044
045    /**
046     * Create a new monthly frequency that happens every numUnits month, anytime.
047     *
048     * @param numUnits Number of days from event to event.
049     * @throws ArgumentNotValid if numUnits if 0 or negative
050     */
051    public MonthlyFrequency(int numUnits) {
052        super(numUnits, true);
053    }
054
055    /**
056     * Create a new monthly frequency that happens every numUnits month, on the given day of month, hour and minute.
057     *
058     * @param numUnits Number of days from event to event.
059     * @param dayOfMonth The day of the month the event should happen. The month starts on day 1.
060     * @param hour The hour on which the event should happen.
061     * @param minute The minute of hour on which the event should happen.
062     * @throws ArgumentNotValid if numUnits if 0 or negative or dayOfMonth <1 or >31 or hour is <0 or >23 or minutes is
063     * <0 or >59
064     */
065    public MonthlyFrequency(int numUnits, int dayOfMonth, int hour, int minute) {
066        super(numUnits, false);
067
068        Calendar cal = GregorianCalendar.getInstance();
069        if (dayOfMonth < cal.getMinimum(Calendar.DAY_OF_MONTH) || dayOfMonth > cal.getMaximum(Calendar.DAY_OF_MONTH)) {
070            throw new ArgumentNotValid("Day of month must be in legal range '" + cal.getMinimum(Calendar.DAY_OF_MONTH)
071                    + "' to '" + cal.getMaximum(Calendar.DAY_OF_MONTH) + "'");
072        }
073        if (hour < cal.getMinimum(Calendar.HOUR_OF_DAY) || hour > cal.getMaximum(Calendar.HOUR_OF_DAY)) {
074            throw new ArgumentNotValid("Hour of day must be in legal range '" + cal.getMinimum(Calendar.HOUR_OF_DAY)
075                    + "' to '" + cal.getMaximum(Calendar.HOUR_OF_DAY) + "'");
076        }
077        if (minute < cal.getMinimum(Calendar.MINUTE) || minute > cal.getMaximum(Calendar.MINUTE)) {
078            throw new ArgumentNotValid("Minute must be in legal range '" + cal.getMinimum(Calendar.MINUTE) + "' to '"
079                    + cal.getMaximum(Calendar.MINUTE) + "'");
080        }
081        this.dayOfMonth = dayOfMonth;
082        this.hour = hour;
083        this.minute = minute;
084    }
085
086    /**
087     * Given when the last event happened, tell us when the next event should happen (even if the new event is in the
088     * past).
089     * <p>
090     * The time of the next event is guaranteed to be later that lastEvent. For certain frequencies (e.g. once a day,
091     * any time of day), the time of the next event is derived from lastEvent, for others (e.g. once a day at 13:00) the
092     * time of the next event is the first matching time after lastEvent.
093     *
094     * @param lastEvent A time from which the next event should be calculated.
095     * @return At what point the event should happen next.
096     */
097    public Date getNextEvent(Date lastEvent) {
098        ArgumentNotValid.checkNotNull(lastEvent, "lastEvent");
099
100        Calendar last = new GregorianCalendar();
101        last.setTime(getFirstEvent(lastEvent));
102        // Note: If the dayOfMonth becomes impossible by this addition, it is
103        // set back to the maximum possible date for this month
104        last.add(Calendar.MONTH, getNumUnits());
105        return getFirstEvent(last.getTime());
106    }
107
108    /**
109     * Given a starting time, tell us when the first event should happen.
110     *
111     * @param startTime The earliest time the event can happen.
112     * @return At what point the event should happen the first time.
113     */
114    public Date getFirstEvent(Date startTime) {
115        ArgumentNotValid.checkNotNull(startTime, "startTime");
116
117        if (isAnytime()) {
118            return startTime;
119        }
120        Calendar start = new GregorianCalendar();
121        start.setTime(startTime);
122        start.set(Calendar.MINUTE, minute);
123        start.set(Calendar.HOUR_OF_DAY, hour);
124        // set day in month, to the given value if possible, or maximum
125        start.set(Calendar.DAY_OF_MONTH, Math.min(start.getActualMaximum(Calendar.DAY_OF_MONTH), dayOfMonth));
126        if (start.getTime().before(startTime)) {
127            start.add(Calendar.MONTH, 1);
128        }
129        // reset day in month, the last day might be later in this month
130        start.set(Calendar.DAY_OF_MONTH, Math.min(start.getActualMaximum(Calendar.DAY_OF_MONTH), dayOfMonth));
131        return start.getTime();
132
133    }
134
135    /**
136     * If not anytime, the minute at which events should start.
137     *
138     * @return the minute
139     */
140    public int getMinute() {
141        return minute;
142    }
143
144    /**
145     * If not anytime, the hour at which events should start.
146     *
147     * @return the hour
148     */
149    public int getHour() {
150        return hour;
151    }
152
153    /**
154     * If not anytime, the day in the month at which events should start.
155     *
156     * @return the day
157     */
158    public int getDayOfMonth() {
159        return dayOfMonth;
160    }
161
162    /**
163     * Autogenerated equals.
164     *
165     * @param o The object to compare with
166     * @return Whether objects are equal
167     */
168    public boolean equals(Object o) {
169        if (this == o) {
170            return true;
171        }
172        if (!(o instanceof MonthlyFrequency)) {
173            return false;
174        }
175        if (!super.equals(o)) {
176            return false;
177        }
178
179        final MonthlyFrequency monthlyFrequency = (MonthlyFrequency) o;
180
181        if (isAnytime()) {
182            return true;
183        }
184
185        if (dayOfMonth != monthlyFrequency.dayOfMonth) {
186            return false;
187        }
188        if (hour != monthlyFrequency.hour) {
189            return false;
190        }
191        if (minute != monthlyFrequency.minute) {
192            return false;
193        }
194
195        return true;
196    }
197
198    /**
199     * Autogenerated hashcode method.
200     *
201     * @return the hashcode
202     */
203    public int hashCode() {
204        int result = super.hashCode();
205        result = 29 * result + minute;
206        result = 29 * result + hour;
207        result = 29 * result + dayOfMonth;
208        return result;
209    }
210
211    /**
212     * Return the exact minute event should happen on, or null if this is an anyTime event or doesn't define what minute
213     * it should happen on.
214     *
215     * @return the exact minute event should happen on
216     */
217    public Integer getOnMinute() {
218        if (!isAnytime()) {
219            return minute;
220        }
221        return null;
222    }
223
224    /**
225     * Return the exact hour event should happen on, or null if this is an anyTime event or doesn't define what hour it
226     * should happen on.
227     *
228     * @return the exact hour event should happen on
229     */
230    public Integer getOnHour() {
231        if (!isAnytime()) {
232            return hour;
233        }
234        return null;
235    }
236
237    /**
238     * Return the exact day of week event should happen on, or null if this is an anyTime event or doesn't define what
239     * day of week it should happen on.
240     *
241     * @return the exact day of week event should happen on
242     */
243    public Integer getOnDayOfWeek() {
244        return null;
245    }
246
247    /**
248     * Return the exact day of month event should happen on, or null if this is an anyTime event or doesn't define what
249     * day of month it should happen on.
250     *
251     * @return the exact day of month event should happen on
252     */
253    public Integer getOnDayOfMonth() {
254        if (!isAnytime()) {
255            return dayOfMonth;
256        }
257        return null;
258    }
259
260    /**
261     * Return an integer that can be used to identify the kind of frequency. No two subclasses should use the same
262     * integer
263     *
264     * @return an integer that can be used to identify the kind of frequency
265     */
266    public int ordinal() {
267        return TimeUnit.MONTHLY.ordinal();
268    }
269
270    /**
271     * Human readable representation of this object.
272     *
273     * @return Human readable representation
274     */
275    public String toString() {
276        if (isAnytime()) {
277            return "every " + getNumUnits() + " months";
278        }
279        return "every " + getNumUnits() + " months, on day " + dayOfMonth + " at " + hour + ":" + minute;
280    }
281
282}