001 /*
002 * Copyright 2011 The Kuali Foundation.
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.kuali.kfs.sys.businessobject;
017
018 import java.util.Arrays;
019 import java.util.Collections;
020 import java.util.HashMap;
021 import java.util.HashSet;
022 import java.util.List;
023 import java.util.Map;
024 import java.util.Set;
025
026 import org.kuali.kfs.coa.businessobject.Account;
027 import org.kuali.kfs.coa.businessobject.ObjectCode;
028 import org.kuali.kfs.sys.context.SpringContext;
029 import org.kuali.kfs.sys.document.service.AccountPresenceService;
030 import org.kuali.rice.kns.util.ObjectUtils;
031
032 /**
033 * This class helps implement AccountingLine overrides. It is not persisted itself, but it simplifies working with the persisted
034 * codes. Instances break the code into components. Static methods help with the AccountingLine.
035 */
036 public class AccountingLineOverride {
037
038 /**
039 * These codes are the way the override is persisted in the AccountingLine.
040 */
041 public static final class CODE { // todo: use JDK 1.5 enum
042 public static final String NONE = "NONE";
043 public static final String EXPIRED_ACCOUNT = "EXPIRED_ACCOUNT";
044 public static final String NON_BUDGETED_OBJECT = "NON_BUDGETED_OBJECT";
045 public static final String TRANSACTION_EXCEEDS_REMAINING_BUDGET = "TRANSACTION_EXCEEDS_REMAINING_BUDGET";
046 public static final String EXPIRED_ACCOUNT_AND_NON_BUDGETED_OBJECT = "EXPIRED_ACCOUNT_AND_NON_BUDGETED_OBJECT";
047 public static final String NON_BUDGETED_OBJECT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET = "NON_BUDGETED_OBJECT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET";
048 public static final String EXPIRED_ACCOUNT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET = "EXPIRED_ACCOUNT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET";
049 public static final String EXPIRED_ACCOUNT_AND_NON_BUDGETED_OBJECT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET = "EXPIRED_ACCOUNT_AND_NON_BUDGETED_OBJECT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET";
050 public static final String NON_FRINGE_ACCOUNT_USED = "NON_FRINGE_ACCOUNT_USED";
051 public static final String EXPIRED_ACCOUNT_AND_NON_FRINGE_ACCOUNT_USED = "EXPIRED_ACCOUNT_AND_NON_FRINGE_ACCOUNT_USED";
052 }
053
054 /**
055 * These are the somewhat independent components of an override.
056 */
057 public static final class COMPONENT { // todo: use JDK 1.5 enum
058 public static final Integer EXPIRED_ACCOUNT = new Integer(1);
059 public static final Integer NON_BUDGETED_OBJECT = new Integer(2);
060 public static final Integer TRANSACTION_EXCEEDS_REMAINING_BUDGET = new Integer(3);
061 public static final Integer NON_FRINGE_ACCOUNT_USED = new Integer(8);
062 }
063
064 /**
065 * The names of the AccountingLine properties that the processForOutput() and determineNeededOverrides() methods use. Callers of
066 * those methods may need to refresh these fields from OJB.
067 */
068 public static final List<String> REFRESH_FIELDS = Collections.unmodifiableList(Arrays.asList(new String[] { "account", "objectCode" }));
069
070 /**
071 * This holds an instance of every valid override, mapped by code.
072 */
073 private static final Map<String, AccountingLineOverride> codeToOverrideMap = new HashMap<String, AccountingLineOverride>();
074
075 /**
076 * This holds an instance of every valid override, mapped by components.
077 */
078 private static final Map componentsToOverrideMap = new HashMap();
079
080 static {
081 // populate the code map
082 new AccountingLineOverride(CODE.NONE, new Integer[] {});
083 new AccountingLineOverride(CODE.EXPIRED_ACCOUNT,
084 // todo: use JDK 1.5 ... args
085 new Integer[] { COMPONENT.EXPIRED_ACCOUNT });
086 new AccountingLineOverride(CODE.NON_BUDGETED_OBJECT, new Integer[] { COMPONENT.NON_BUDGETED_OBJECT });
087 new AccountingLineOverride(CODE.TRANSACTION_EXCEEDS_REMAINING_BUDGET, new Integer[] { COMPONENT.TRANSACTION_EXCEEDS_REMAINING_BUDGET });
088 new AccountingLineOverride(CODE.EXPIRED_ACCOUNT_AND_NON_BUDGETED_OBJECT, new Integer[] { COMPONENT.EXPIRED_ACCOUNT, COMPONENT.NON_BUDGETED_OBJECT });
089 new AccountingLineOverride(CODE.NON_BUDGETED_OBJECT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET, new Integer[] { COMPONENT.NON_BUDGETED_OBJECT, COMPONENT.TRANSACTION_EXCEEDS_REMAINING_BUDGET });
090 new AccountingLineOverride(CODE.EXPIRED_ACCOUNT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET, new Integer[] { COMPONENT.EXPIRED_ACCOUNT, COMPONENT.TRANSACTION_EXCEEDS_REMAINING_BUDGET });
091 new AccountingLineOverride(CODE.EXPIRED_ACCOUNT_AND_NON_BUDGETED_OBJECT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET, new Integer[] { COMPONENT.EXPIRED_ACCOUNT, COMPONENT.NON_BUDGETED_OBJECT, COMPONENT.TRANSACTION_EXCEEDS_REMAINING_BUDGET });
092 new AccountingLineOverride(CODE.NON_FRINGE_ACCOUNT_USED, new Integer[] { COMPONENT.NON_FRINGE_ACCOUNT_USED });
093 new AccountingLineOverride(CODE.EXPIRED_ACCOUNT_AND_NON_FRINGE_ACCOUNT_USED, new Integer[] { COMPONENT.EXPIRED_ACCOUNT, COMPONENT.NON_FRINGE_ACCOUNT_USED });
094 }
095
096 private final String code;
097 private final Set components;
098
099 /**
100 * This private constructor is for the static initializer.
101 *
102 * @param myCode
103 * @param myComponents
104 */
105 private AccountingLineOverride(String myCode, Integer[] myComponents) {
106 code = myCode;
107 components = componentsAsSet(myComponents);
108 codeToOverrideMap.put(code, this);
109 componentsToOverrideMap.put(components, this);
110 }
111
112 /**
113 * Checks whether this override contains the given component.
114 *
115 * @param component
116 * @return whether this override contains the given component.
117 */
118 public boolean hasComponent(Integer component) {
119 return components.contains(component);
120 }
121
122 /**
123 * Gets the code of this override.
124 *
125 * @return the code of this override.
126 */
127 public String getCode() {
128 return code;
129 }
130
131 /**
132 * Gets the components of this override.
133 *
134 * @return the components of this override.
135 */
136 private Set getComponents() {
137 return components;
138 }
139
140 /**
141 * @see java.lang.Object#toString()
142 */
143 public String toString() {
144 return "AccountingLineOverride (code " + code + ", components " + components + ")";
145 }
146
147 /**
148 * Returns the AccountingLineOverride that has the components of this AccountingLineOverride minus any components not in the
149 * given mask. This is like <code>&</code>(a bit-wise and), if the components were bits.
150 *
151 * @param mask
152 * @return the AccountingLineOverride that has the components of this AccountingLineOverride minus any components not in the
153 * given mask.
154 * @throws IllegalArgumentException if there is no such valid combination of components
155 */
156 public AccountingLineOverride mask(AccountingLineOverride mask) {
157 Set key = maskComponents(mask);
158 if (!isValidComponentSet(key)) {
159 throw new IllegalArgumentException("invalid component set " + key);
160 }
161 return valueOf(key);
162 }
163
164 /**
165 * Returns the Set of components that this override and the given override have in common.
166 *
167 * @param mask
168 * @return the Set of components that this override and the given override have in common.
169 */
170 private Set maskComponents(AccountingLineOverride mask) {
171 Set retval = new HashSet(components);
172 retval.retainAll(mask.getComponents());
173 return retval;
174 }
175
176 /**
177 * Returns whether this override, when masked by the given override, is valid. Some combinations of components have no override
178 * code defined.
179 *
180 * @param mask
181 * @return whether this override, when masked by the given override, is valid.
182 */
183 public boolean isValidMask(AccountingLineOverride mask) {
184 return isValidComponentSet(maskComponents(mask));
185 }
186
187 /**
188 * Returns whether the given String is a valid override code.
189 *
190 * @param code
191 * @return whether the given String is a valid override code.
192 */
193 public static boolean isValidCode(String code) {
194 return codeToOverrideMap.containsKey(code);
195 }
196
197 /**
198 * Returns whether the given Integers are a valid set of components. Some combinations of components are invalid and have no
199 * code defined.
200 *
201 * @param components
202 * @return whether the given Integers are a valid set of components.
203 */
204 public static boolean isValidComponentSet(Integer[] components) {
205 return isValidComponentSet(componentsAsSet(components));
206 }
207
208 private static boolean isValidComponentSet(Set components) { // todo: JDK 1.5 generic Set
209 return componentsToOverrideMap.containsKey(components);
210 }
211
212 /**
213 * Factory method from code.
214 *
215 * @param code the override code
216 * @return the AccountingLineOverride instance corresponding to the given code.
217 * @throws IllegalArgumentException if the given code is not valid
218 */
219 public static AccountingLineOverride valueOf(String code) {
220 if (!isValidCode(code)) {
221 throw new IllegalArgumentException("invalid code " + code);
222 }
223 return (AccountingLineOverride) codeToOverrideMap.get(code); // todo: JDK 1.5 generic Map instead of cast
224 }
225
226 /**
227 * Factory method from components.
228 *
229 * @param components the override components, treated as a set
230 * @return the AccountingLineOverride instance corresponding to the given component set.
231 * @throws IllegalArgumentException if the given set of components is not valid
232 */
233 public static AccountingLineOverride valueOf(Integer[] components) {
234 Set key = componentsAsSet(components);
235 if (!isValidComponentSet(key)) {
236 throw new IllegalArgumentException("invalid component set " + key);
237 }
238 return valueOf(key);
239 }
240
241 public static AccountingLineOverride valueOf(Set components) {
242 return (AccountingLineOverride) componentsToOverrideMap.get(components); // todo: JDK 1.5 generic Map instead of cast
243 }
244
245 private static Set componentsAsSet(Integer[] components) {
246 return Collections.unmodifiableSet(new HashSet(Arrays.asList(components)));
247 }
248
249 /**
250 * On the given AccountingLine, converts override input checkboxes from a Struts Form into a persistable override code.
251 *
252 * @param line
253 */
254 public static void populateFromInput(AccountingLine line) {
255 // todo: this logic won't work if a single account checkbox might also stands for NON_FRINGE_ACCOUNT_USED; needs thought
256
257 Set overrideInputComponents = new HashSet();
258 if (line.getAccountExpiredOverride()) {
259 overrideInputComponents.add(COMPONENT.EXPIRED_ACCOUNT);
260 }
261 if (line.isObjectBudgetOverride()) {
262 overrideInputComponents.add(COMPONENT.NON_BUDGETED_OBJECT);
263 }
264 if (!isValidComponentSet(overrideInputComponents)) {
265 // todo: error for invalid override checkbox combinations, for which there is no override code
266 }
267 line.setOverrideCode(valueOf(overrideInputComponents).getCode());
268 }
269
270 /**
271 * Prepares the given AccountingLine in a Struts Action for display by a JSP. This means converting the override code to
272 * checkboxes for display and input, as well as analysing the accounting line and determining which override checkboxes are
273 * needed.
274 *
275 * @param line
276 */
277 public static void processForOutput(AccountingLine line) {
278 AccountingLineOverride fromCurrentCode = valueOf(line.getOverrideCode());
279 AccountingLineOverride needed = determineNeededOverrides(line);
280 line.setAccountExpiredOverride(fromCurrentCode.hasComponent(COMPONENT.EXPIRED_ACCOUNT));
281 line.setAccountExpiredOverrideNeeded(needed.hasComponent(COMPONENT.EXPIRED_ACCOUNT));
282 line.setObjectBudgetOverride(fromCurrentCode.hasComponent(COMPONENT.NON_BUDGETED_OBJECT));
283 line.setObjectBudgetOverrideNeeded(needed.hasComponent(COMPONENT.NON_BUDGETED_OBJECT));
284 }
285
286 /**
287 * Determines what overrides the given line needs.
288 *
289 * @param line
290 * @return what overrides the given line needs.
291 */
292 public static AccountingLineOverride determineNeededOverrides(AccountingLine line) {
293 Set neededOverrideComponents = new HashSet();
294 if (needsExpiredAccountOverride(line.getAccount())) {
295 neededOverrideComponents.add(COMPONENT.EXPIRED_ACCOUNT);
296 }
297 if (needsObjectBudgetOverride(line.getAccount(), line.getObjectCode())) {
298 neededOverrideComponents.add(COMPONENT.NON_BUDGETED_OBJECT);
299 }
300
301 if (!isValidComponentSet(neededOverrideComponents)) {
302 // todo: error for invalid override checkbox combinations, for which there is no override code
303 }
304 return valueOf(neededOverrideComponents);
305 }
306
307 /**
308 * Returns whether the given account needs an expired account override.
309 *
310 * @param account
311 * @return whether the given account needs an expired account override.
312 */
313 public static boolean needsExpiredAccountOverride(Account account) {
314 return !ObjectUtils.isNull(account) && account.isActive() && account.isExpired();
315 }
316
317 /**
318 * Returns whether the given account needs an expired account override.
319 *
320 * @param account
321 * @return whether the given account needs an expired account override.
322 */
323 public static boolean needsNonFringAccountOverride(Account account) {
324 return !ObjectUtils.isNull(account) && account.isActive() && !account.isAccountsFringesBnftIndicator();
325 }
326
327 /**
328 * Returns whether the given object code needs an object budget override
329 *
330 * @param account
331 * @return whether the given object code needs an object budget override
332 */
333 public static boolean needsObjectBudgetOverride(Account account, ObjectCode objectCode) {
334 return !ObjectUtils.isNull(account) && !ObjectUtils.isNull(objectCode) && account.isActive() && !SpringContext.getBean(AccountPresenceService.class).isObjectCodeBudgetedForAccountPresence(account, objectCode);
335 }
336 }