001: package org.obe.engine.calendar;
002:
003: import org.apache.commons.logging.Log;
004: import org.apache.commons.logging.LogFactory;
005: import org.obe.client.api.repository.RepositoryException;
006:
007: import java.io.Reader;
008: import java.io.StringReader;
009: import java.util.*;
010:
011: /**
012: * Defines a business calendar. This means that it will handle the constructs
013: * of a business hour and a business day. This calendar also allows a system to
014: * define a matrix showing the times when work can proceed. This is analogous
015: * to a normal work calendar. For example, most people work from 9AM to 5PM,
016: * take an hour off for lunch and only work Monday to Friday. This calendar
017: * allows a system to define these rules and apply calendar math to them.
018: * <p/>
019: * A business hour is any hour that has been marked as allowed for work. A
020: * business day is a day which has one or more business hours in it.
021: * <p/>
022: * For example, based on the work week definition from above, if you add two
023: * business hours to Friday, 4PM, the result will be Monday, 10AM.
024: * <p/>
025: * A calendar also has a start and an end date. By default, that is the from
026: * the current time to the end of the current year, but any start and end dates
027: * can be given. The start and end dates always encompass an entire day. This
028: * means the start date always starts at 00h00 and the end date always ends at
029: * 23h59
030: */
031: public class BusinessCalendar extends GregorianCalendar {
032: private static final long serialVersionUID = 4139466865132436642L;
033:
034: protected static final Log _LOGGER = LogFactory
035: .getLog(BusinessCalendar.class);
036:
037: // Everything inside this calendar is calculated based on GMT. The
038: // application of a TimeZone is external to the calculations. This
039: // simplifies things and removes one variable. However, it does mean that
040: // Daylight Savings Time must be handled separately, because GMT does not
041: // have the notion of DST.
042:
043: /**
044: * Number of intervals in a business hour
045: */
046: public static final int INTERVALS_PER_HOUR = 4;
047:
048: /**
049: * This is the smallest amount of time that can be marked as business time
050: * in the calendar. The value in expressed in milliseconds
051: */
052: public static final int INTERVAL_TIME_MILLIS = (60 / INTERVALS_PER_HOUR) * 60 * 1000;
053:
054: private static final int HOURS_PER_DAY = 24;
055: /**
056: * Number of intervals in a full business day
057: */
058: public static final int INTERVALS_PER_DAY = INTERVALS_PER_HOUR
059: * HOURS_PER_DAY;
060:
061: /**
062: * The duration in milliseconds of a business hour.
063: */
064: // public static final int BUSINESS_HOUR_TIME = INTERVALS_PER_HOUR *
065: // INTERVAL_TIME_MILLIS;
066: // For a prettier output in the dump
067: private static String _header;
068:
069: // Milliseconds in a day
070: public static final long MILLIS_IN_DAY = HOURS_PER_DAY * 60 * 60 * 1000;
071:
072: /*
073: * The parent BC from which this calendar inherits some of its attributes.
074: */
075: private BusinessCalendar _parent;
076:
077: /*
078: * List of CalendarRule objects for inclusion rules.
079: */
080: private List _includeRules;
081:
082: /*
083: * List of CalendarRule objects for exclusion rules.
084: */
085: private List _excludeRules;
086:
087: /*
088: * Parser to handle rules creation
089: */
090: // private Parser _parser;
091: /**
092: * The calendar's start date.
093: */
094: private GregorianCalendar _startDate;
095:
096: /**
097: * This is the end of this calendar. Only values between the start and end
098: * have meaning for any date arithmetic.
099: */
100: private GregorianCalendar _endDate;
101:
102: /**
103: * This is the set of bits representing the actual business calendar. This
104: * is broken down into intervals and spans the entire length of time for a
105: * calendar, one bit per interval. If the bit is 1, the interval is a valid
106: * worktime.
107: */
108: private BitSet _businessCalendar;
109:
110: /**
111: * Need to track the locale with which a calendar was initialized. This is
112: * needed to inherit the information from the parent.
113: */
114: private Locale _locale;
115:
116: /*
117: * The size of the BitSet.
118: */
119: private int _totalIntervals;
120: private static final int BUFFER_SIZE = 1024;
121: private static final int MAX_DAY = 31;
122: private static final int MAX_HOUR = 23;
123: private static final int MAX_MINUTE = 59;
124:
125: /**
126: * This uses the system TimeZone and Locale, with the calendar ranging from
127: * the current time to the end of the current year.
128: *
129: * @param startDate
130: * @param endDate
131: */
132: public BusinessCalendar(GregorianCalendar startDate,
133: GregorianCalendar endDate) throws RepositoryException {
134:
135: init(startDate, endDate);
136: }
137:
138: /**
139: * This sets the TimeZone but uses the default Locale
140: *
141: * @param zone Calendar time zone
142: */
143: // public BusinessCalendar(TimeZone zone) throws RepositoryException {
144: // super(zone);
145: // init(null, null);
146: // }
147: /**
148: * This sets the Locale but uses the default TimeZone
149: *
150: * @param aLocale Calendar locale
151: */
152: // public BusinessCalendar(Locale aLocale) throws RepositoryException {
153: // super(aLocale);
154: // _locale = aLocale;
155: // init(null, null);
156: // }
157: /**
158: * Sets both the TimeZone and the Locale
159: *
160: * @param zone Calendar time zone
161: * @param aLocale Calendar locale
162: */
163: public BusinessCalendar(TimeZone zone, Locale aLocale)
164: throws RepositoryException {
165:
166: super (zone, aLocale);
167: _locale = aLocale;
168: init(null, null);
169: }
170:
171: /**
172: * Inherits everything from the parent. It allows this calendar to
173: * maintain an independent list of inclusions/exclusions in addition to
174: * using the parent ones.
175: *
176: * @param parent
177: */
178: // public BusinessCalendar(BusinessCalendar parent)
179: // throws RepositoryException {
180: //
181: // super(parent.getTimeZone(), parent._locale);
182: // _parent = parent;
183: // init(null, null);
184: // }
185: // public BusinessCalendar(BusinessCalendar parent, TimeZone zone)
186: // throws RepositoryException {
187: // super(zone);
188: //
189: // _parent = parent;
190: // init(null, null);
191: // }
192: // public BusinessCalendar(BusinessCalendar parent, Locale aLocale)
193: // throws RepositoryException {
194: // super(aLocale);
195: //
196: // _parent = parent;
197: // _locale = aLocale;
198: // init(null, null);
199: // }
200: public BusinessCalendar(BusinessCalendar parent, TimeZone zone,
201: Locale aLocale) throws RepositoryException {
202: super (zone, aLocale);
203:
204: _parent = parent;
205: _locale = aLocale;
206: init(null, null);
207: }
208:
209: public Object clone() {
210: return super .clone();
211: }
212:
213: public BusinessCalendar getParent() {
214: return _parent;
215: }
216:
217: public List getIncludeRules() {
218: return _includeRules;
219: }
220:
221: public List getExcludeRules() {
222: return _excludeRules;
223: }
224:
225: public Locale getLocale() {
226: return _locale;
227: }
228:
229: public GregorianCalendar getStartDate() {
230: return _startDate;
231: }
232:
233: public GregorianCalendar getEndDate() {
234: return _endDate;
235: }
236:
237: public void parseStatement(String statement)
238: throws RepositoryException {
239: StringReader reader = new StringReader(statement);
240:
241: parseStatements(reader);
242: }
243:
244: public void parseStatements(Reader statements)
245: throws RepositoryException {
246: // _parser.setScanner(new Yylex(statements));
247: // try {
248: // _parser.parse();
249: // } catch (Exception e) {
250: // throw new RepositoryException(e);
251: // }
252:
253: createCalendar();
254: }
255:
256: /**
257: * Dumps calendar contents.
258: *
259: * @return String representation of the calendar.
260: */
261: public String dumpCalendar() {
262: StringBuffer buf = new StringBuffer(BUFFER_SIZE);
263: GregorianCalendar dateTracker = (GregorianCalendar) _startDate
264: .clone();
265:
266: if (_LOGGER.isDebugEnabled()) {
267: _LOGGER.debug("Calendar length: " + _totalIntervals
268: + " Starting date: " + dateTracker.getTime());
269: }
270:
271: int intervals;
272: buf.append(_header);
273: for (int i = 0; i < _totalIntervals; i += intervals) {
274: intervals = BusinessCalendarUtilities
275: .getIntervalsInDate(dateTracker);
276: addOutputLine(dateTracker, intervals, i, buf);
277: dateTracker.add(Calendar.DATE, 1);
278: }
279:
280: return buf.toString();
281: }
282:
283: private int addOutputLine(GregorianCalendar dateTracker,
284: int intervals, int totalCount, StringBuffer buf) {
285: buf.append('\n');
286: buf.append(dateTracker.getTime());
287: buf.append(' ');
288:
289: for (int i = 0; i < intervals; i++) {
290: if (i % INTERVALS_PER_HOUR == 0)
291: buf.append('|');
292: buf.append(_businessCalendar.get(i + totalCount) ? '1'
293: : '0');
294: }
295:
296: buf.append(" (");
297: buf.append(totalCount);
298: buf.append(')');
299: buf.append(" (");
300: buf.append(dateTracker.get(Calendar.DAY_OF_YEAR));
301: buf.append(')');
302:
303: return BusinessCalendarUtilities
304: .getIntervalsInDate(dateTracker);
305: }
306:
307: /**
308: * @param rule
309: */
310: void addIncludeRule(CalendarRule rule) {
311: _includeRules.add(rule);
312: }
313:
314: /**
315: * @param rule
316: */
317: void addExcludeRule(CalendarRule rule) {
318: _excludeRules.add(rule);
319: }
320:
321: /**
322: * Generates the calendar from its current ruleset.
323: */
324: private void createCalendar() {
325: // Create the calendar bitset with all the bits turns off initially
326: // The + 1 is for rounding
327: _totalIntervals = BusinessCalendarUtilities
328: .countBitsForInterval(_startDate.getTime(), _endDate
329: .getTime(), getTimeZone()) + 1;
330:
331: if (_LOGGER.isDebugEnabled()) {
332: _LOGGER.debug("createCalendar( Start Date: " + _startDate
333: + " End Date: " + _endDate + " Total intervals: "
334: + _totalIntervals + ')');
335: }
336:
337: _businessCalendar = new BitSet(_totalIntervals);
338:
339: // Now to create the calendar. The mask is used to set each rule in
340: // the calendar set. It is created using a rule then cleared afterwards
341: // for the next rule.
342: // The rules are applied with the includes first then the excludes,
343: // giving the exclusion rules precedence.
344: BitSet mask = new BitSet(_totalIntervals);
345: for (int i = 0; i < _includeRules.size(); i++) {
346: CalendarRule ruleStatement = (CalendarRule) _includeRules
347: .get(i);
348: if (_LOGGER.isDebugEnabled())
349: _LOGGER.debug("Include Rule: " + ruleStatement);
350: ruleStatement.applyRule(mask, _totalIntervals, _startDate,
351: _endDate, getTimeZone());
352: _businessCalendar.or(mask);
353: BusinessCalendarUtilities.clearBitSetBits(mask, 0,
354: _totalIntervals);
355: }
356:
357: for (int i = 0; i < _excludeRules.size(); i++) {
358: CalendarRule ruleStatement = (CalendarRule) _excludeRules
359: .get(i);
360: if (_LOGGER.isDebugEnabled())
361: _LOGGER.debug("Exclude Rule: " + ruleStatement);
362: ruleStatement.applyRule(mask, _totalIntervals, _startDate,
363: _endDate, getTimeZone());
364: if (_LOGGER.isDebugEnabled())
365: _LOGGER.debug("createCalendar(" + mask + ')');
366: _businessCalendar.andNot(mask);
367: BusinessCalendarUtilities.clearBitSetBits(mask, 0,
368: _totalIntervals);
369: }
370: }
371:
372: private void init(GregorianCalendar startDate,
373: GregorianCalendar endDate) throws RepositoryException {
374: // _parser = new Parser();
375: // _parser.setBusinessCalendar(this);
376: _includeRules = new ArrayList();
377: _excludeRules = new ArrayList();
378:
379: if (startDate == null) {
380: _startDate = new GregorianCalendar();
381: if (endDate == null) {
382: _endDate = new GregorianCalendar(internalGet(YEAR),
383: DECEMBER, MAX_DAY, MAX_HOUR, MAX_MINUTE);
384: } else {
385: _endDate = endDate;
386: }
387: } else if (endDate == null) {
388: _startDate = startDate;
389: _endDate = new GregorianCalendar(internalGet(YEAR),
390: DECEMBER, MAX_DAY, MAX_HOUR, MAX_MINUTE);
391: } else {
392: _startDate = startDate;
393: _endDate = endDate;
394: }
395:
396: // Need to set the calendar values so that the start date and end date
397: // encompass an entire day
398: _startDate.set(HOUR, 0);
399: _startDate.set(MINUTE, 0);
400: _endDate.set(HOUR, MAX_HOUR);
401: _endDate.set(MINUTE, MAX_MINUTE);
402:
403: if (_endDate.before(_startDate))
404: throw new RepositoryException(
405: "End date cannot be before start date");
406:
407: setTimeInMillis(_startDate.getTime().getTime());
408:
409: // Create the header for dumping out the calendar. It is not the most
410: // efficient as it add Strings, but it only gets called once and the
411: // cost of creating an extra StringBuffer() would be higher.
412: StringBuffer fillerBuf = new StringBuffer();
413: int fillerCount = INTERVALS_PER_HOUR - 2 >> 1;
414: for (int i = 0; i < fillerCount; i++)
415: fillerBuf.append(' ');
416: String filler = fillerBuf.toString();
417:
418: StringBuffer header = new StringBuffer();
419: header.append(" Date |");
420: for (int i = 0; i < HOURS_PER_DAY; i++) {
421: header.append(filler);
422: if (i < 10)
423: header.append(0);
424: header.append(i);
425: header.append(filler);
426: header.append('|');
427: }
428: _header = header.toString();
429: }
430: }
|