View Javadoc

1   /**
2    * Copyright 2004-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.hr.time.flsa;
17  
18  import java.math.BigDecimal;
19  import java.util.ArrayList;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  
24  import org.apache.commons.lang.StringUtils;
25  import org.joda.time.DateTime;
26  import org.joda.time.DateTimeZone;
27  import org.joda.time.Interval;
28  import org.joda.time.LocalDateTime;
29  import org.kuali.hr.lm.leaveblock.LeaveBlock;
30  import org.kuali.hr.time.timeblock.TimeBlock;
31  import org.kuali.hr.time.timeblock.TimeHourDetail;
32  import org.kuali.hr.time.util.TKUtils;
33  import org.kuali.hr.time.util.TkConstants;
34  
35  public class FlsaDay {
36  
37  	private Map<String,List<TimeBlock>> earnCodeToTimeBlocks = new HashMap<String,List<TimeBlock>>();
38  	private List<TimeBlock> appliedTimeBlocks = new ArrayList<TimeBlock>();
39  	
40  	private Map<String,List<LeaveBlock>> earnCodeToLeaveBlocks = new HashMap<String,List<LeaveBlock>>();
41  	private List<LeaveBlock> appliedLeaveBlocks = new ArrayList<LeaveBlock>();
42  
43  	Interval flsaDateInterval;
44  	LocalDateTime flsaDate;
45      DateTimeZone timeZone;
46  
47      /**
48       *
49       * @param flsaDate A LocalDateTime because we want to be conscious of the
50       * relative nature of this flsa/window
51       * @param timeBlocks
52       * @param timeZone The timezone we are constructing, relative.
53       */
54  	public FlsaDay(LocalDateTime flsaDate, List<TimeBlock> timeBlocks, List<LeaveBlock> leaveBlocks, DateTimeZone timeZone) {
55  		this.flsaDate = flsaDate;
56          this.timeZone = timeZone;
57  		flsaDateInterval = new Interval(flsaDate.toDateTime(timeZone), flsaDate.toDateTime(timeZone).plusHours(24));
58  		this.setTimeBlocks(timeBlocks);
59  		this.setLeaveBlocks(leaveBlocks);
60  	}
61  
62  	/**
63  	 * Handles the breaking apart of existing time blocks around FLSA boundaries.
64  	 *
65  	 * This method will compare the FLSA interval against the timeblock interval
66  	 * to determine how many hours overlap.  It will then examine the time hour
67  	 * details
68  	 *
69  	 * @param timeBlocks a sorted list of time blocks.
70  	 */
71  	public void setTimeBlocks(List<TimeBlock> timeBlocks) {
72  		for (TimeBlock block : timeBlocks) {
73              applyBlock(block, this.appliedTimeBlocks);
74  			//if (!applyBlock(block, this.appliedTimeBlocks)) {
75  			//	break;
76              //}
77          }
78  	}
79  	
80  	/**
81  	 * Handles the breaking apart of existing leave blocks around FLSA boundaries.
82  	 *
83  	 * This method will compare the FLSA interval against the leaveblock interval
84  	 * to determine how many hours overlap.  It will then examine the leave hour
85  	 * details
86  	 *
87  	 * @param leaveBlocks a sorted list of leave blocks.
88  	 */
89  	public void setLeaveBlocks(List<LeaveBlock> leaveBlocks) {
90  		for (LeaveBlock block : leaveBlocks) {
91              applyBlock(block, this.appliedLeaveBlocks);
92  			/*if (!applyBlock(block, this.appliedLeaveBlocks)) {
93  				break;
94              }*/
95          }
96  	}
97  
98  	/**
99  	 * This method will compute the mappings present for this object:
100 	 *
101 	 * earnCodeToTimeBlocks
102 	 * earnCodeToHours
103 	 *
104 	 */
105 	public void remapTimeHourDetails() {
106 		List<TimeBlock> reApplied = new ArrayList<TimeBlock>(appliedTimeBlocks.size());
107 		earnCodeToTimeBlocks.clear();
108 		for (TimeBlock block : appliedTimeBlocks) {
109 			applyBlock(block, reApplied);
110 		}
111 	}
112 	
113 	/**
114 	 * This method will compute the mappings present for this object:
115 	 *
116 	 * earnCodeToTimeBlocks
117 	 * earnCodeToHours
118 	 *
119 	 */
120 	public void remapLeaveHourDetails() {
121 		List<LeaveBlock> reApplied = new ArrayList<LeaveBlock>(appliedLeaveBlocks.size());
122 		earnCodeToLeaveBlocks.clear();
123 		for (LeaveBlock block : appliedLeaveBlocks) {
124 			applyBlock(block, reApplied);
125 		}
126 	}
127 
128 	/**
129      * This method determines if the provided TimeBlock is applicable to this
130      * FLSA day, and if so will add it to the applyList. It could be the case
131      * that a TimeBlock is on the boundary of the FLSA day so that only a
132      * partial amount of the hours for that TimeBlock will count towards this
133      * day.
134      *
135      * |---------+------------------+---------|
136      * | Day 1   | Day 1/2 Boundary | Day 2   |
137      * |---------+------------------+---------|
138      * | Block 1 |             | Block 2      |
139      * |---------+------------------+---------|
140      *
141      * The not so obvious ascii diagram above is intended to illustrate the case
142      * where on day one you have 1 fully overlapping time block (block1) and one
143      * partially overlapping time block (block2). Block 2 belongs to both FLSA
144      * Day 1 and Day 2.
145      *
146 	 * @param block A time block that we want to check and apply to this day.
147 	 * @param applyList A list of time blocks we want to add applicable time blocks to.
148 	 *
149 	 * @return True if the block is applicable, false otherwise.  The return
150 	 * value can be used as a quick exit for the setTimeBlocks() method.
151 	 *
152 	 * TODO : Bucketing of partial FLSA days is still suspect, however real life examples of this are likely non-existent to rare.
153      *
154      * Danger may still lurk in day-boundary overlapping time blocks that have multiple Time Hour Detail entries.
155 	 */
156 	private boolean applyBlock(TimeBlock block, List<TimeBlock> applyList) {
157 		DateTime beginDateTime = new DateTime(block.getBeginTimestamp(), this.timeZone);
158 		DateTime endDateTime = new DateTime(block.getEndTimestamp(), this.timeZone);
159 
160 		if (beginDateTime.isAfter(flsaDateInterval.getEnd())) {
161 			return false;
162         }
163 
164 		Interval timeBlockInterval = null;
165 		//Requested to have zero hour time blocks be able to be added to the GUI
166 		boolean zeroHoursTimeBlock = false;
167 		if(endDateTime.getMillis() > beginDateTime.getMillis()){
168 			timeBlockInterval = new Interval(beginDateTime,endDateTime);
169 		}
170 		
171 		if(flsaDateInterval.contains(beginDateTime)){
172 			zeroHoursTimeBlock = true;
173 		}
174 
175 		Interval overlapInterval = flsaDateInterval.overlap(timeBlockInterval);
176 		long overlap = (overlapInterval == null) ? 0L : overlapInterval.toDurationMillis();
177 		BigDecimal overlapHours = TKUtils.convertMillisToHours(overlap);
178 		if((overlapHours.compareTo(BigDecimal.ZERO) == 0) && flsaDateInterval.contains(beginDateTime) && flsaDateInterval.contains(endDateTime)){
179 			if(block.getHours().compareTo(BigDecimal.ZERO) > 0){
180 				overlapHours = block.getHours();
181 			}
182 		}
183 
184         // Local lookup for this time-block to ensure we are not over applicable hours.
185         // You will notice below we are earn codes globally per day, and also locally per timeblock.
186         // The local per-time block mapping is used only to verify that we have not gone over allocated overlap time
187         // for the individual time block.
188         Map<String,BigDecimal> localEarnCodeToHours = new HashMap<String,BigDecimal>();
189 
190 		if (zeroHoursTimeBlock || overlapHours.compareTo(BigDecimal.ZERO) > 0 || (flsaDateInterval.contains(beginDateTime) && StringUtils.equals(block.getEarnCodeType(),TkConstants.EARN_CODE_AMOUNT)))  {
191 
192             List<TimeHourDetail> details = block.getTimeHourDetails();
193             for (TimeHourDetail thd : details) {
194                 BigDecimal localEcHours = localEarnCodeToHours.containsKey(thd.getEarnCode()) ? localEarnCodeToHours.get(thd.getEarnCode()) : BigDecimal.ZERO;
195                 //NOTE adding this in the last few hours before release.. remove if side effects are noticed
196                 if (overlapHours.compareTo(localEcHours) >= 0 || thd.getAmount().compareTo(BigDecimal.ZERO) == 0) {
197                     localEcHours = localEcHours.add(thd.getHours(), TkConstants.MATH_CONTEXT);
198                     localEarnCodeToHours.put(thd.getEarnCode(), localEcHours);
199                 }
200             }
201 
202 			List<TimeBlock> blocks = earnCodeToTimeBlocks.get(block.getEarnCode());
203 			if (blocks == null) {
204 				blocks = new ArrayList<TimeBlock>();
205 				earnCodeToTimeBlocks.put(block.getEarnCode(), blocks);
206 			}
207 			blocks.add(block);
208 			applyList.add(block);
209 		}
210 
211 		return true;
212 	}
213 	
214 	/**
215      * This method determines if the provided LeaveBlock is applicable to this
216      * FLSA day, and if so will add it to the applyList. It could be the case
217      * that a LeaveBlock is on the boundary of the FLSA day so that only a
218      * partial amount of the hours for that LeaveBlock will count towards this
219      * day.
220      *
221      * |---------+------------------+---------|
222      * | Day 1   | Day 1/2 Boundary | Day 2   |
223      * |---------+------------------+---------|
224      * | Block 1 |             | Block 2      |
225      * |---------+------------------+---------|
226      *
227      * The not so obvious ascii diagram above is intended to illustrate the case
228      * where on day one you have 1 fully overlapping leave block (block1) and one
229      * partially overlapping leave block (block2). Block 2 belongs to both FLSA
230      * Day 1 and Day 2.
231      *
232 	 * @param block A leave block that we want to check and apply to this day.
233 	 * @param applyList A list of leave blocks we want to add applicable leave blocks to.
234 	 *
235 	 * @return True if the block is applicable, false otherwise.  The return
236 	 * value can be used as a quick exit for the setLeaveBlocks() method.
237 	 *
238 	 * TODO : Bucketing of partial FLSA days is still suspect, however real life examples of this are likely non-existent to rare.
239 	 */
240 	private boolean applyBlock(LeaveBlock block, List<LeaveBlock> applyList) {
241 		DateTime beginDateTime = new DateTime(block.getLeaveDate(), this.timeZone);
242 		DateTime endDateTime = new DateTime(block.getLeaveDate(), this.timeZone);
243 
244 		if (beginDateTime.isAfter(flsaDateInterval.getEnd()))
245 			return false;
246 
247 		Interval leaveBlockInterval = null;
248 		if(endDateTime.getMillis() > beginDateTime.getMillis()){
249 			leaveBlockInterval = new Interval(beginDateTime,endDateTime);
250 		}
251 
252 		Interval overlapInterval = flsaDateInterval.overlap(leaveBlockInterval);
253 		long overlap = (overlapInterval == null) ? 0L : overlapInterval.toDurationMillis();
254 		BigDecimal overlapHours = TKUtils.convertMillisToHours(overlap);
255 		if((overlapHours.compareTo(BigDecimal.ZERO) == 0) && flsaDateInterval.contains(beginDateTime) && flsaDateInterval.contains(endDateTime)){
256 			if(block.getLeaveAmount().negate().compareTo(BigDecimal.ZERO) > 0){
257 				overlapHours = block.getLeaveAmount().negate();
258 			}
259 		}
260 		
261 		if (overlapHours.compareTo(BigDecimal.ZERO) > 0)  {
262 			List<LeaveBlock> blocks = earnCodeToLeaveBlocks.get(block.getEarnCode());
263 			if (blocks == null) {
264 				blocks = new ArrayList<LeaveBlock>();
265 				earnCodeToLeaveBlocks.put(block.getEarnCode(), blocks);
266 			}
267 			blocks.add(block);
268 			applyList.add(block);
269 		}
270 
271 		return true;
272 	}
273 
274 	public Map<String, List<TimeBlock>> getEarnCodeToTimeBlocks() {
275 		return earnCodeToTimeBlocks;
276 	}
277 
278 	public void setEarnCodeToTimeBlocks(Map<String, List<TimeBlock>> earnCodeToTimeBlocks) {
279 		this.earnCodeToTimeBlocks = earnCodeToTimeBlocks;
280 	}
281 
282 	public List<TimeBlock> getAppliedTimeBlocks() {
283 		return appliedTimeBlocks;
284 	}
285 
286 	public void setAppliedTimeBlocks(List<TimeBlock> appliedTimeBlocks) {
287 		this.appliedTimeBlocks = appliedTimeBlocks;
288 	}
289 
290 	public Map<String, List<LeaveBlock>> getEarnCodeToLeaveBlocks() {
291 		return earnCodeToLeaveBlocks;
292 	}
293 
294 	public void setEarnCodeToLeaveBlocks(Map<String, List<LeaveBlock>> earnCodeToLeaveBlocks) {
295 		this.earnCodeToLeaveBlocks = earnCodeToLeaveBlocks;
296 	}
297 
298 	public List<LeaveBlock> getAppliedLeaveBlocks() {
299 		return appliedLeaveBlocks;
300 	}
301 
302 	public void setAppliedLeaveBlocks(List<LeaveBlock> appliedLeaveBlocks) {
303 		this.appliedLeaveBlocks = appliedLeaveBlocks;
304 	}
305 
306 }