001: /*
002: The contents of this file are subject to the Common Public Attribution License
003: Version 1.0 (the "License"); you may not use this file except in compliance with
004: the License. You may obtain a copy of the License at
005: http://www.projity.com/license . The License is based on the Mozilla Public
006: License Version 1.1 but Sections 14 and 15 have been added to cover use of
007: software over a computer network and provide for limited attribution for the
008: Original Developer. In addition, Exhibit A has been modified to be consistent
009: with Exhibit B.
010:
011: Software distributed under the License is distributed on an "AS IS" basis,
012: WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
013: specific language governing rights and limitations under the License. The
014: Original Code is OpenProj. The Original Developer is the Initial Developer and
015: is Projity, Inc. All portions of the code written by Projity are Copyright (c)
016: 2006, 2007. All Rights Reserved. Contributors Projity, Inc.
017:
018: Alternatively, the contents of this file may be used under the terms of the
019: Projity End-User License Agreeement (the Projity License), in which case the
020: provisions of the Projity License are applicable instead of those above. If you
021: wish to allow use of your version of this file only under the terms of the
022: Projity License and not to allow others to use your version of this file under
023: the CPAL, indicate your decision by deleting the provisions above and replace
024: them with the notice and other provisions required by the Projity License. If
025: you do not delete the provisions above, a recipient may use your version of this
026: file under either the CPAL or the Projity License.
027:
028: [NOTE: The text of this license may differ slightly from the text of the notices
029: in Exhibits A and B of the license at http://www.projity.com/license. You should
030: use the latest text at http://www.projity.com/license for your modifications.
031: You may not remove this license text from the source files.]
032:
033: Attribution Information: Attribution Copyright Notice: Copyright © 2006, 2007
034: Projity, Inc. Attribution Phrase (not exceeding 10 words): Powered by OpenProj,
035: an open source solution from Projity. Attribution URL: http://www.projity.com
036: Graphic Image as provided in the Covered Code as file: openproj_logo.png with
037: alternatives listed on http://www.projity.com/logo
038:
039: Display of Attribution Information is required in Larger Works which are defined
040: in the CPAL as a work which combines Covered Code or portions thereof with code
041: not governed by the terms of the CPAL. However, in addition to the other notice
042: obligations, all copies of the Covered Code in Executable and Source Code form
043: distributed must, as a form of attribution of the original author, include on
044: each user interface screen the "OpenProj" logo visible to all users. The
045: OpenProj logo should be located horizontally aligned with the menu bar and left
046: justified on the top left of the screen adjacent to the File menu. The logo
047: must be at least 100 x 25 pixels. When users click on the "OpenProj" logo it
048: must direct them back to http://www.projity.com.
049: */
050: package com.projity.pm.calendar;
051:
052: import java.text.SimpleDateFormat;
053: import java.util.Arrays;
054: import java.util.Calendar;
055: import java.util.Date;
056: import java.util.Iterator;
057: import java.util.TreeSet;
058:
059: import org.apache.commons.lang.time.DateUtils;
060: import org.apache.commons.pool.BasePoolableObjectFactory;
061: import org.apache.commons.pool.impl.GenericObjectPool;
062:
063: import com.projity.datatype.Duration;
064: import com.projity.server.access.ErrorLogger;
065: import com.projity.util.DateTime;
066:
067: /**
068: * This class holds specific calendar informatin either for a base calendar or a concrete one, as well as date math functions
069: */
070: public class CalendarDefinition implements WorkCalendar, Cloneable {
071: static final long serialVersionUID = 73883742020831L;
072: TreeSet dayExceptions = null;
073: WorkDay[] exceptions = null;
074: WorkWeek week = new WorkWeek();
075: protected long id = -1L;
076:
077: /**
078: *
079: */
080: public CalendarDefinition() {
081: super ();
082: dayExceptions = new TreeSet();
083: // TODO Auto-generated constructor stub
084: }
085:
086: public CalendarDefinition(CalendarDefinition base,
087: CalendarDefinition differences) {
088: if (base == null) {
089: week = new WorkWeek();
090: } else {
091: week = (WorkWeek) base.week.clone(); // copy the week days
092: }
093: week.addDaysFrom(differences.week); // Now replace any special weekdays
094:
095: dayExceptions = (TreeSet) differences.dayExceptions.clone(); // copy from differences
096: if (base != null)
097: dayExceptions.addAll(base.dayExceptions); // add in base days. If day is already present it will not be added
098: addSentinelsAndMakeArray();
099:
100: if (!testValid())
101: System.out.println("calendar is invalid " + this .getName());
102: }
103:
104: public boolean testValid() {
105: if (week == null)
106: return false;
107: for (int i = 0; i < 7; i++)
108: if (week.getWeekDay(i) == null)
109: return false;
110: return true;
111:
112: }
113:
114: void addSentinelsAndMakeArray() {
115: // Add endpoint sentinels. This facilitates algorithms which will no longer need to check for boundary conditions
116: dayExceptions.add(WorkDay.MINIMUM);
117: dayExceptions.add(WorkDay.MAXIMUM);
118: exceptions = new WorkDay[dayExceptions.size()];
119: dayExceptions.toArray(exceptions);
120:
121: }
122:
123: public WorkDay[] getExceptions() {
124: return exceptions;
125: }
126:
127: public WorkDay getWeekDay(int d) {
128: return week.getWeekDay(d);
129: }
130:
131: void addOrReplaceException(WorkDay exceptionDay) {
132: dayExceptions.remove(exceptionDay); // remove any existing
133: dayExceptions.add(exceptionDay);
134: exceptions = new WorkDay[dayExceptions.size()];
135: dayExceptions.toArray(exceptions);
136: }
137:
138: /* (non-Javadoc)
139: * @see java.lang.Object#clone()
140: */
141: public Object clone() throws CloneNotSupportedException {
142: CalendarDefinition newOne = (CalendarDefinition) super .clone();
143: newOne.week = (WorkWeek) week.clone();
144: newOne.dayExceptions = new TreeSet();
145:
146: Iterator i = dayExceptions.iterator();
147: while (i.hasNext())
148: newOne.dayExceptions.add(((WorkDay) i.next()).clone());
149: return newOne;
150: }
151:
152: /**
153: * This method adjusts the given time to a working time in the calendar.
154: * The algorithm just subtracts a tick and adds it back for sooner or vice versa for later
155: * @param date
156: * @param useSooner
157: * @return
158: */
159: public long adjustInsideCalendar(long date, boolean useSooner) {
160: long result;
161: if (date < 0) {
162: date = -date;
163: useSooner = !useSooner;
164: }
165: if (useSooner) {
166: long backOne = add(date, -MILLIS_IN_MINUTE, useSooner);
167: result = add(backOne, MILLIS_IN_MINUTE, useSooner);
168: } else {
169: long aheadOne = add(date, MILLIS_IN_MINUTE, useSooner);
170: result = add(aheadOne, -MILLIS_IN_MINUTE, useSooner);
171: }
172: return result;
173: }
174:
175: /**
176: * Algorithm to add a duration to a date. This code MUST be very fast as it is the most executed code in the program.
177: * The time required by the algorithm is determined by the number of exceptions encountered and not the duration itself.
178: * To handle reverse scheduling, the date can be negative. In this case, the date is converted to a positive value, but the duration
179: * is negated.
180: */
181: public long add(long date, long duration, boolean useSooner) {
182: if (date == 0) // don't bother treating null dates since they will never be valid for calculations
183: return 0;
184: long result = date;
185: boolean forward = true;
186: boolean negative = date < 0;
187: boolean elapsed = Duration.isElapsed(duration);
188: duration = Duration.millis(duration);
189:
190: if (negative) {
191: date = -date;
192: duration = -duration;
193: useSooner = !useSooner;
194: if (duration == 0)
195: forward = false;
196: }
197:
198: if (elapsed) { // elapsed times do not use calendars, though the result must fall within working time
199: result = adjustInsideCalendar(date + duration, useSooner);
200: } else {
201: if (duration < 0) {
202: forward = false;
203: duration = -duration;
204: }
205: //TODO move current day into iterator for speed
206: CalendarIterator iterator = CalendarIteratorFactory
207: .getInstance(); // use object pool for speed
208: long currentDay = iterator.dayOf(date);
209: iterator.initialize(this , forward, currentDay);
210: WorkingHours current = iterator.getNext(currentDay);
211: duration -= current.calcWorkTime(iterator.timeOf(date),
212: forward);// handle the first day
213:
214: long numWeeks;
215:
216: /*
217: * First, do a "rough tuning" to get within a week of destination day. This part of the algorithm will
218: * see how many weeks there are in the duration, subtract off the normal working time for a week for each week.
219: * and position the day correctly. It then adjusts the duration based on any exception days during those weeks.
220: * It is possible, if there are many exceptions, that after adjusting for exception days, there are still weeks of
221: * work left. That is why this is called in a loop.
222: */
223:
224: //TODO This code may not working correctly
225: long weekDuration = week.getDuration();
226: while ((numWeeks = (duration / weekDuration)) != 0) {
227: currentDay = iterator.nextDay(currentDay); // move to next day, first is done
228: currentDay = iterator
229: .moveNumberOfDays(
230: (int) (WorkWeek.DAYS_IN_WEEK * (forward ? numWeeks
231: : -numWeeks)), currentDay);
232: duration -= (numWeeks * weekDuration); // subtract off fixed duration
233: duration -= iterator
234: .exceptionDurationDifference(currentDay); // subtract off difference.
235:
236: if (duration <= 0) { // if exceptions cause too much duration, then go back in other direction
237: iterator.reverseDirection();
238: duration = -duration;
239: forward = !forward; // todo is this necessary?
240: } else
241: //TODO verify that this should be in else.
242: currentDay = iterator.prevDay(currentDay); // move back a day for fine tuning which adds it back
243:
244: }
245: //
246: //
247: /*
248: * This part of the algorithm is the fine tuning. It does through the remaining deays and treats them one by one.
249: * Because of the week treatment above, this is guaranteed to go through 6 days at the most.
250: */while (duration >= 0) { // add in days until we go exactly on the spot or past it
251: if (duration == 0 && (forward == useSooner))
252: break;
253: currentDay = iterator.nextDay(currentDay);
254: current = iterator.getNext(currentDay);
255: duration -= current.getDuration(); // use exception day
256: }
257: // Handle the last day
258: long time = -1;
259: while (true) {
260: if (forward) {
261: time = current.calcTimeAtRemainingWork(-duration);
262: } else
263: time = current.calcTimeAtWork(-duration);
264: if (time != -1)
265: break;
266: currentDay = iterator.nextDay(currentDay);
267: current = iterator.getNext(currentDay);
268: }
269: ;
270: result = currentDay + time;
271:
272: CalendarIteratorFactory.recycle(iterator); //No longer using iterator, return it to pool
273: }
274:
275: // if input was negative time, return a negative value
276: if (negative)
277: result = -result;
278: return result;
279: }
280:
281: /**
282: * Get difference of two dates: laterDate - earlierDate according to calendar
283: */
284: public long compare(long laterDate, long earlierDate,
285: boolean elapsed) {
286: boolean negative = laterDate < 0;
287: if (negative) {
288: laterDate = -laterDate;
289: earlierDate = -earlierDate;
290: }
291:
292: if (elapsed) { // if the desired duration is elapsed time, then just to a simple subtraction
293: return laterDate - earlierDate;
294: }
295:
296: // if later is before earlier swap the dates. The value of swap is tested later and sign is reversed if it is used
297: long swap = 0;
298: if (laterDate < earlierDate) {
299: swap = earlierDate;
300: earlierDate = laterDate;
301: laterDate = swap;
302:
303: }
304: if (earlierDate == 0) // degenerate case. A 0 date means undefined, so don't process it
305: return laterDate;
306:
307: CalendarIterator iterator = CalendarIteratorFactory
308: .getInstance(); // use object pool for speed
309: long earlierDay = iterator.dayOf(earlierDate);
310: long laterDay = iterator.dayOf(laterDate);
311: long currentDay;
312: iterator.initialize(this , true, earlierDay);
313: WorkingHours current = iterator.getNext(earlierDay);
314: long duration = 0;
315:
316: // Algo starts here
317: // treat start day
318: duration += current.calcWorkTimeAfter(iterator
319: .timeOf(earlierDate));
320: currentDay = iterator.nextDay(earlierDay); // move to next day, first is done
321:
322: /*
323: * First add in weeks, adjusting for exception days
324: */
325: long numWeeks = (iterator.dayOf(laterDate) - currentDay)
326: / WorkWeek.MS_IN_WEEK;
327: if (numWeeks != 0) {
328: currentDay = iterator.moveNumberOfDays(
329: (int) (WorkWeek.DAYS_IN_WEEK * numWeeks),
330: currentDay);
331: duration += numWeeks * week.getDuration(); // add on normal working duration
332: duration += iterator
333: .exceptionDurationDifference(currentDay); // add difference.
334: }
335:
336: // treat remaining middle days (no more than 6) and the end day
337: for (; currentDay <= laterDay; currentDay = iterator
338: .nextDay(currentDay)) {
339: current = iterator.getNext(currentDay);
340: duration += current.getDuration();
341: }
342:
343: // subtract out part of the end day that is later then laterDate
344: duration -= current.calcWorkTimeAfter(iterator
345: .timeOf(laterDate));
346:
347: CalendarIteratorFactory.recycle(iterator);
348: if (negative)
349: duration = -duration;
350: return (swap == 0) ? duration : -duration; // swap == 0 implies that no swap was done since early date had to be minimum
351: }
352:
353: /**
354: * This class manages a pool of calendar iterators.
355: *
356: */
357: private static class CalendarIteratorFactory extends
358: BasePoolableObjectFactory {
359: private static GenericObjectPool pool = new GenericObjectPool(
360: new CalendarIteratorFactory());
361:
362: public Object makeObject() throws Exception {
363: return new CalendarIterator();
364: }
365:
366: public static CalendarIterator getInstance() {
367: try {
368: return (CalendarIterator) pool.borrowObject();
369: } catch (Exception e) {
370: e.printStackTrace();
371: return null;
372: }
373: }
374:
375: public static void recycle(CalendarIterator object) {
376: try {
377: pool.returnObject(object);
378: } catch (Exception e) {
379: e.printStackTrace();
380: }
381: }
382: }
383:
384: /**
385: * This class is an iterator which is used to return week days or exception days
386: *
387: */
388: private static class CalendarIterator {
389: WorkDay[] exceptions;
390: WorkWeek week;
391: Calendar scratchDate; // will get reused since this class is recycled
392:
393: long exceptionDay;
394: int i;
395: boolean forward;
396: int step;
397:
398: private CalendarIterator() {
399: scratchDate = DateTime.calendarInstance(); // will get reused since this class is recycled
400: }
401:
402: /**
403: *
404: */
405: private void reverseDirection() {
406: if (forward) {
407: i -= 1;
408: } else {
409: i += 1;
410: }
411: step = -step;
412: exceptionDay = exceptions[i].getStart();
413: forward = !forward;
414: }
415:
416: private static SimpleDateFormat f = DateTime
417: .dateFormatInstance();
418:
419: public long dayOf(long date) {
420: scratchDate.setTimeInMillis(date);
421: scratchDate.set(Calendar.HOUR_OF_DAY, 0);
422: scratchDate.set(Calendar.MINUTE, 0);
423: scratchDate.set(Calendar.SECOND, 0); // Fixed rounding bug as we now go to seconds 8/2/07
424: // scratchDate.set(Calendar.MILLISECOND,0);
425: return scratchDate.getTimeInMillis();
426: }
427:
428: public long timeOf(long date) {
429: return date - dayOf(date);
430: }
431:
432: private void initialize(CalendarDefinition cal,
433: boolean forward, long day) {
434: exceptions = cal.exceptions;
435: week = cal.week;
436: this .forward = forward;
437: scratchDate.setTimeInMillis(day);
438: try {
439: DateUtils.truncate(scratchDate, Calendar.DATE);
440: } catch (Exception e) {
441: ErrorLogger.logOnce("hugedate",
442: "date value is garbage " + scratchDate, e);
443: }
444: step = (forward) ? 1 : -1;
445: i = Arrays.binarySearch(exceptions, scratchDate);
446: if (i < 0) {// First day not found
447: i = -i - 1; // set index for the future
448: if (!forward)
449: i -= 1;
450: }
451: exceptionDay = exceptions[i].getStart();
452:
453: }
454:
455: public String dump() {
456: String result = "CalendarIterator ";
457: result += "weekdays\n";
458: for (int i = 0; i < 7; i++) {
459: result += "day[" + i + "]" + week.getWeekDay(i) + "\n";
460: }
461: result += "There are " + exceptions.length
462: + " exceptions\n";
463: for (int j = 0; j < exceptions.length; j++) {
464: result += "exception" + exceptions[j].toString();
465: }
466: return result;
467:
468: }
469:
470: private WorkingHours getNext(long day) {
471: WorkDay workDay;
472:
473: if (day == exceptionDay) {
474: workDay = exceptions[i]; // move index, save off new value for exception day
475: i += step;
476: if (i < 0 || i == exceptions.length) {//TODO
477: System.out
478: .println("invalid calendar iterator - index is negative or past bounds. avoiding");
479: ErrorLogger.logOnce("CalendarIterator",
480: "invalid calendar iterator i=" + i, null);
481: } else
482: exceptionDay = exceptions[i].getStart(); // move index, save off new value for exception day
483: } else {
484: workDay = week.getWeekDay(dayOfWeek(day));
485: }
486: return workDay.getWorkingHours();
487: }
488:
489: private long exceptionDurationDifference(long endDay) {
490: long difference = 0;
491: if (exceptions.length == 2) // skip sentinels
492: return 0;
493: while ((forward && exceptionDay < endDay)
494: || (!forward && exceptionDay > endDay)) {
495: difference -= week.getWeekDay(dayOfWeek(exceptionDay))
496: .getDuration();
497: difference += exceptions[i].getDuration();
498: i += step;
499: if (i < 0)
500: System.out.println("error");
501: exceptionDay = exceptions[i].getStart();
502: }
503: return difference;
504:
505: }
506:
507: private int dayOfWeek(long day) {
508: scratchDate.setTimeInMillis(day);
509: return scratchDate.get(Calendar.DAY_OF_WEEK) - 1;
510:
511: }
512:
513: private long moveNumberOfDays(int numberOfDays, long fromDay) {
514: scratchDate.setTimeInMillis(fromDay);
515: scratchDate.add(Calendar.DATE, numberOfDays);
516: return scratchDate.getTimeInMillis();
517: }
518:
519: private long nextDay(long day) {
520: scratchDate.setTimeInMillis(day);
521: scratchDate.add(Calendar.DATE, forward ? 1 : -1);
522: return scratchDate.getTimeInMillis();
523: }
524:
525: private long prevDay(long day) {
526: scratchDate.setTimeInMillis(day);
527: scratchDate.add(Calendar.DATE, forward ? -1 : 1);
528: return scratchDate.getTimeInMillis();
529: }
530:
531: }
532:
533: /* (non-Javadoc)
534: * @see com.projity.configuration.NamedItem#getName()
535: */
536: public String getName() {
537: // TODO Auto-generated method stub
538: return null;
539: }
540:
541: /* (non-Javadoc)
542: * @see com.projity.configuration.NamedItem#getCategory()
543: */
544: public String getCategory() {
545: // TODO Auto-generated method stub
546: return null;
547: }
548:
549: /* (non-Javadoc)
550: * @see com.projity.pm.time.WorkCalendar#setName(java.lang.String)
551: */
552: public void setName(String name) {
553: // TODO Auto-generated method stub
554:
555: }
556:
557: /* (non-Javadoc)
558: * @see com.projity.pm.calendar.WorkCalendar#getConcreteInstance()
559: */
560: public CalendarDefinition getConcreteInstance() {
561: return this ; // doesn't make sense to call this
562: }
563:
564: public static final int getDayOfWeek(long date) {
565: Calendar scratchDate = DateTime.calendarInstance();
566: scratchDate.setTimeInMillis(date);
567: return scratchDate.get(Calendar.DAY_OF_WEEK) - 1;
568: }
569:
570: public final WorkDay getWorkDay(long date) {
571: WorkDay workDay = null;
572: int i = Arrays.binarySearch(getConcreteInstance().exceptions,
573: new Date(date));
574: if (i >= 0) {
575: workDay = exceptions[i];
576: } else {
577: workDay = week.getWeekDay(getDayOfWeek(date));
578: }
579: return workDay;
580: }
581:
582: public long getId() {
583: return id;
584: }
585:
586: public void setId(long id) {
587: this .id = id;
588: }
589:
590: public long getUniqueId() {
591: return id;
592: }
593:
594: public void setUniqueId(long id) {
595: this .id = id;
596: }
597:
598: transient boolean newId = true;
599:
600: public boolean isNew() {
601: return newId;
602: }
603:
604: public void setNew(boolean newId) {
605: this .newId = newId;
606: }
607:
608: /* (non-Javadoc)
609: * @see com.projity.pm.calendar.WorkCalendar#getBaseCalendar()
610: */
611: public WorkCalendar getBaseCalendar() {
612: return null;
613: }
614:
615: /* (non-Javadoc)
616: * @see com.projity.pm.calendar.WorkCalendar#dependsOn(com.projity.pm.calendar.WorkCalendar)
617: */
618: public boolean dependsOn(WorkCalendar cal) {
619: return false;
620: }
621:
622: /* (non-Javadoc)
623: * @see com.projity.pm.calendar.WorkCalendar#invalidate()
624: */
625: public void invalidate() {
626: }
627:
628: /* (non-Javadoc)
629: * @see com.projity.pm.calendar.WorkCalendar#isInvalid()
630: */
631: public boolean isInvalid() {
632: return false;
633: }
634:
635: CalendarDefinition intersectWith(CalendarDefinition other)
636: throws InvalidCalendarIntersectionException {
637: CalendarDefinition result = new CalendarDefinition();
638: result.week = week.intersectWith(other.week);
639:
640: WorkDay exceptionDay;
641: // merge exceptions
642: for (int i = 0; i < exceptions.length; i++) {
643: exceptionDay = exceptions[i];
644: result.dayExceptions.add(exceptionDay.intersectWith(other
645: .getWorkDay(exceptionDay.getStart())));
646: }
647: for (int i = 0; i < other.exceptions.length; i++) {
648: exceptionDay = other.exceptions[i];
649: result.dayExceptions
650: .add(exceptionDay
651: .intersectWith(getWorkDay(exceptionDay
652: .getStart())));
653: }
654: result.addSentinelsAndMakeArray();
655: return result;
656: }
657:
658: private transient boolean dirty;
659:
660: public boolean isDirty() {
661: return dirty;
662: }
663:
664: public void setDirty(boolean dirty) {
665: this .dirty = dirty;
666: }
667:
668: public String dump() {
669: String result = "Calendar " + getName() + "\n";
670: result += "weekdays\n";
671: for (int i = 0; i < 7; i++) {
672: result += "day[" + i + "]" + getWeekDay(i) + "\n";
673: }
674: result += "There are " + exceptions.length + " exceptions\n";
675: for (int j = 0; j < exceptions.length; j++) {
676: result += "exception" + exceptions[j].toString();
677: }
678: return result;
679:
680: }
681:
682: }
|