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>&amp;</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    }