001: /*
002: * Copyright 2007 The Kuali Foundation.
003: *
004: * Licensed under the Educational Community License, Version 1.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/ecl1.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.document;
017:
018: import static org.apache.commons.beanutils.PropertyUtils.getProperty;
019:
020: import java.util.ArrayList;
021: import java.util.HashMap;
022: import java.util.Iterator;
023: import java.util.List;
024: import java.util.Map;
025:
026: import org.kuali.kfs.KFSConstants;
027: import org.kuali.kfs.bo.AccountingLine;
028: import org.kuali.kfs.context.SpringContext;
029: import org.kuali.kfs.rule.event.AccountingLineEvent;
030: import org.kuali.kfs.rule.event.AddAccountingLineEvent;
031: import org.kuali.kfs.rule.event.DeleteAccountingLineEvent;
032: import org.kuali.kfs.rule.event.ReviewAccountingLineEvent;
033: import org.kuali.kfs.rule.event.UpdateAccountingLineEvent;
034: import org.kuali.kfs.service.AccountingLineService;
035:
036: /**
037: * Helper class used for delegating tasks of an <code>{@link AccountingDocument}</code> that effect a parallel hierarchy.
038: * Basically, this is here because it is the alternative to duplicating code. Rather than having the encapsulated methods exist in
039: * multiple documents that use it, but aren't necessarily <code>{@link AccountingDocument}</code> instances, use the
040: * <code>{@link AccountingDocumentHelper}</code>.<br/>
041: * <p>
042: * This is the result of having <code>{@link SourceAccountingLine}</code> and <code>{@link TargetAccountingLine}</code> parallel
043: * hierarchies that may need to be extended at the <code>{@link AccountingLine}</code> hierarchical level and the higher.
044: *
045: * @see org.kuali.kfs.bo.SourceAccountingLine
046: * @see org.kuali.kfs.bo.TargetAccountingLine
047: * @see org.kuali.kfs.document.AccountingDocument
048: * @see org.kuali.kfs.document.AccountingDocumentBase
049: */
050: public class AccountingDocumentHelper<KfsDocument extends GeneralLedgerPostingDocument>
051: implements java.io.Serializable {
052: private static final org.apache.commons.logging.Log LOG = org.apache.commons.logging.LogFactory
053: .getLog(AccountingDocumentHelper.class);
054:
055: private KfsDocument document;
056:
057: /**
058: * Since documents that use this may not necessarily need to be <code>{@link AccountingDocument}</code> instances, they must
059: * be at least <code>{@link GeneralLedgerPostingDocument}</code> instances.
060: */
061: public AccountingDocumentHelper(KfsDocument document) {
062: setDocument(document);
063: }
064:
065: public void setDocument(KfsDocument document) {
066: this .document = document;
067: }
068:
069: public KfsDocument getDocument() {
070: return document;
071: }
072:
073: /**
074: * Wrapper for getTargetAccountingLineClass
075: *
076: * @return Class
077: */
078: private Class getTargetAccountingLineClass() {
079: try {
080: return (Class) getProperty(getDocument(),
081: "targetAccountingLineClass");
082: } catch (Exception e) {
083: LOG
084: .warn("Something went very wrong when trying to get the targetAccountingLineClass property from the "
085: + getDocument().getClass() + " class");
086: return null;
087: }
088: }
089:
090: /**
091: * Wrapper for getSourceAccountingLineClass
092: *
093: * @return Class
094: */
095: private Class getSourceAccountingLineClass() {
096: try {
097: return (Class) getProperty(getDocument(),
098: "sourceAccountingLineClass");
099: } catch (Exception e) {
100: LOG
101: .warn("Something went very wrong when trying to get the sourceAccountingLineClass property from the "
102: + getDocument().getClass() + " class");
103: return null;
104: }
105: }
106:
107: /**
108: * Wrapper for getTargetAccountingLines
109: *
110: * @return List
111: */
112: private List getTargetAccountingLines() {
113: try {
114: return (List) getProperty(getDocument(),
115: "targetAccountingLines");
116: } catch (Exception e) {
117: LOG
118: .warn("Something went very wrong when trying to get the targetAccountingLines property from the "
119: + getDocument().getClass() + " class");
120: return null;
121: }
122: }
123:
124: /**
125: * Wrapper for getSourceAccountingLines
126: *
127: * @return List
128: */
129: private List getSourceAccountingLines() {
130: try {
131: return (List) getProperty(getDocument(),
132: "sourceAccountingLines ");
133: } catch (Exception e) {
134: LOG
135: .warn("Something went very wrong when trying to get the sourceAccountingLines property from the "
136: + getDocument().getClass() + " class");
137: return null;
138: }
139: }
140:
141: /**
142: * Local <code>{@link AccountingLineService}</code> delegation. To override which <code>{@link AccountingLineService}</code>
143: * is used, just override this.
144: *
145: * @return AccountingLineService;
146: */
147: protected AccountingLineService getAccountingLineService() {
148: return SpringContext.getBean(AccountingLineService.class);
149: }
150:
151: public List generateSaveEvents() {
152: List events = new ArrayList();
153:
154: // foreach (source, target)
155: // 1. retrieve persisted accountingLines for document
156: // 2. retrieve current accountingLines from given document
157: // 3. compare, creating add/delete/update events as needed
158: // 4. apply rules as appropriate returned events
159: LOG.debug("Getting persisted source lines");
160: List persistedSourceLines = getAccountingLineService()
161: .getByDocumentHeaderId(getSourceAccountingLineClass(),
162: getDocument().getDocumentNumber());
163: LOG.debug("Done getting persisted source lines");
164: LOG.debug(persistedSourceLines.toString());
165: List currentSourceLines = getSourceAccountingLines();
166:
167: List sourceEvents = generateEvents(
168: persistedSourceLines,
169: currentSourceLines,
170: KFSConstants.DOCUMENT_PROPERTY_NAME
171: + "."
172: + KFSConstants.EXISTING_SOURCE_ACCT_LINE_PROPERTY_NAME);
173: for (Object event : sourceEvents) {
174: AccountingLineEvent sourceEvent = (AccountingLineEvent) event;
175: events.add(sourceEvent);
176: }
177:
178: List persistedTargetLines = getAccountingLineService()
179: .getByDocumentHeaderId(getTargetAccountingLineClass(),
180: getDocument().getDocumentNumber());
181: List currentTargetLines = getTargetAccountingLines();
182:
183: List targetEvents = generateEvents(
184: persistedTargetLines,
185: currentTargetLines,
186: KFSConstants.DOCUMENT_PROPERTY_NAME
187: + "."
188: + KFSConstants.EXISTING_TARGET_ACCT_LINE_PROPERTY_NAME);
189: for (Object event : targetEvents) {
190: AccountingLineEvent targetEvent = (AccountingLineEvent) event;
191: events.add(targetEvent);
192: }
193:
194: return events;
195: }
196:
197: /**
198: * Generates a List of instances of AccountingLineEvent subclasses, one for each accountingLine in the union of the
199: * persistedLines and currentLines lists. Events in the list will be grouped in order by event-type (review, update, add,
200: * delete).
201: *
202: * @param persistedLines
203: * @param currentLines
204: * @param errorPathPrefix
205: * @param document
206: * @return List of AccountingLineEvent subclass instances
207: */
208: protected List generateEvents(List persistedLines,
209: List currentLines, String errorPathPrefix) {
210: List addEvents = new ArrayList();
211: List updateEvents = new ArrayList();
212: List reviewEvents = new ArrayList();
213: List deleteEvents = new ArrayList();
214:
215: //
216: // generate events
217: Map persistedLineMap = buildAccountingLineMap(persistedLines);
218:
219: // (iterate through current lines to detect additions and updates, removing affected lines from persistedLineMap as we go
220: // so deletions can be detected by looking at whatever remains in persistedLineMap)
221: int index = 0;
222: for (Iterator i = currentLines.iterator(); i.hasNext(); index++) {
223: String indexedErrorPathPrefix = errorPathPrefix + "["
224: + index + "]";
225: AccountingLine currentLine = (AccountingLine) i.next();
226: Integer key = currentLine.getSequenceNumber();
227:
228: AccountingLine persistedLine = (AccountingLine) persistedLineMap
229: .get(key);
230: // if line is both current and persisted...
231: if (persistedLine != null) {
232: // ...check for updates
233: if (!currentLine.isLike(persistedLine)) {
234: UpdateAccountingLineEvent updateEvent = new UpdateAccountingLineEvent(
235: indexedErrorPathPrefix, getDocument(),
236: persistedLine, currentLine);
237: updateEvents.add(updateEvent);
238: } else {
239: ReviewAccountingLineEvent reviewEvent = new ReviewAccountingLineEvent(
240: indexedErrorPathPrefix, getDocument(),
241: currentLine);
242: reviewEvents.add(reviewEvent);
243: }
244:
245: persistedLineMap.remove(key);
246: } else {
247: // it must be a new addition
248: AddAccountingLineEvent addEvent = new AddAccountingLineEvent(
249: indexedErrorPathPrefix, getDocument(),
250: currentLine);
251: addEvents.add(addEvent);
252: }
253: }
254:
255: // detect deletions
256: for (Iterator i = persistedLineMap.entrySet().iterator(); i
257: .hasNext();) {
258: // the deleted line is not displayed on the page, so associate the error with the whole group
259: String groupErrorPathPrefix = errorPathPrefix
260: + KFSConstants.ACCOUNTING_LINE_GROUP_SUFFIX;
261: Map.Entry e = (Map.Entry) i.next();
262: AccountingLine persistedLine = (AccountingLine) e
263: .getValue();
264: DeleteAccountingLineEvent deleteEvent = new DeleteAccountingLineEvent(
265: groupErrorPathPrefix, getDocument(), persistedLine,
266: true);
267: deleteEvents.add(deleteEvent);
268: }
269:
270: //
271: // merge the lists
272: List lineEvents = new ArrayList();
273: lineEvents.addAll(reviewEvents);
274: lineEvents.addAll(updateEvents);
275: lineEvents.addAll(addEvents);
276: lineEvents.addAll(deleteEvents);
277:
278: return lineEvents;
279: }
280:
281: /**
282: * @param accountingLines
283: * @return Map containing accountingLines from the given List, indexed by their sequenceNumber
284: */
285: protected Map buildAccountingLineMap(List accountingLines) {
286: Map lineMap = new HashMap();
287:
288: for (Iterator i = accountingLines.iterator(); i.hasNext();) {
289: AccountingLine accountingLine = (AccountingLine) i.next();
290: Integer sequenceNumber = accountingLine.getSequenceNumber();
291:
292: Object oldLine = lineMap
293: .put(sequenceNumber, accountingLine);
294:
295: // verify that sequence numbers are unique...
296: if (oldLine != null) {
297: throw new IllegalStateException(
298: "sequence number collision detected for sequence number "
299: + sequenceNumber);
300: }
301: }
302:
303: return lineMap;
304: }
305: }
|