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 }