001: /*
002: * Copyright 2006-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:
017: package org.kuali.workflow.attribute;
018:
019: import java.util.ArrayList;
020: import java.util.Collections;
021: import java.util.HashMap;
022: import java.util.HashSet;
023: import java.util.Iterator;
024: import java.util.List;
025: import java.util.Map;
026: import java.util.Set;
027:
028: import javax.xml.xpath.XPath;
029: import javax.xml.xpath.XPathConstants;
030: import javax.xml.xpath.XPathExpressionException;
031:
032: import org.apache.commons.lang.StringUtils;
033: import org.apache.commons.lang.builder.EqualsBuilder;
034: import org.apache.commons.lang.builder.HashCodeBuilder;
035: import org.apache.log4j.Logger;
036: import org.kuali.core.bo.DocumentHeader;
037: import org.kuali.core.bo.user.UuId;
038: import org.kuali.core.lookup.LookupUtils;
039: import org.kuali.core.service.DataDictionaryService;
040: import org.kuali.core.service.UniversalUserService;
041: import org.kuali.core.util.KualiDecimal;
042: import org.kuali.core.util.ObjectUtils;
043: import org.kuali.kfs.KFSConstants;
044: import org.kuali.kfs.KFSPropertyConstants;
045: import org.kuali.kfs.context.SpringContext;
046: import org.kuali.module.chart.bo.Account;
047: import org.kuali.module.chart.bo.Chart;
048: import org.kuali.module.chart.bo.Delegate;
049: import org.kuali.module.chart.service.AccountService;
050: import org.kuali.workflow.KualiWorkflowUtils;
051: import org.w3c.dom.Document;
052: import org.w3c.dom.Element;
053: import org.w3c.dom.Node;
054: import org.w3c.dom.NodeList;
055:
056: import edu.iu.uis.eden.WorkflowServiceErrorImpl;
057: import edu.iu.uis.eden.engine.RouteContext;
058: import edu.iu.uis.eden.exception.EdenUserNotFoundException;
059: import edu.iu.uis.eden.plugin.attributes.RoleAttribute;
060: import edu.iu.uis.eden.plugin.attributes.WorkflowAttribute;
061: import edu.iu.uis.eden.routeheader.DocumentContent;
062: import edu.iu.uis.eden.routetemplate.ResolvedQualifiedRole;
063: import edu.iu.uis.eden.routetemplate.Role;
064: import edu.iu.uis.eden.user.AuthenticationUserId;
065: import edu.iu.uis.eden.user.UserId;
066: import edu.iu.uis.eden.util.Utilities;
067:
068: /**
069: * KualiAccountAttribute which should be used when using Accounts to do routing
070: */
071: public class KualiAccountAttribute implements RoleAttribute,
072: WorkflowAttribute {
073:
074: static final long serialVersionUID = 1000;
075:
076: private static Logger LOG = Logger
077: .getLogger(KualiAccountAttribute.class);
078:
079: private static final String FIN_COA_CD_KEY = "fin_coa_cd";
080:
081: private static final String ACCOUNT_NBR_KEY = "account_nbr";
082:
083: private static final String FDOC_TOTAL_DOLLAR_AMOUNT_KEY = "fdoc_ttl_dlr_amt";
084:
085: private static final String FISCAL_OFFICER_ROLE_KEY = "FISCAL-OFFICER";
086:
087: private static final String FISCAL_OFFICER_ROLE_LABEL = "Fiscal Officer";
088:
089: private static final String FISCAL_OFFICER_PRIMARY_DELEGATE_ROLE_KEY = "FISCAL-OFFICER-PRIMARY-DELEGATE";
090:
091: private static final String FISCAL_OFFICER_PRIMARY_DELEGATE_ROLE_LABEL = "Fiscal Officer Primary Delegate";
092:
093: private static final String FISCAL_OFFICER_SECONDARY_DELEGATE_ROLE_KEY = "FISCAL-OFFICER-SECONDARY-DELEGATE";
094:
095: private static final String FISCAL_OFFICER_SECONDARY_DELEGATE_ROLE_LABEL = "Fiscal Officer Secondary Delegate";
096:
097: private static final String ACCOUNT_SUPERVISOR_ROLE_KEY = "ACCOUNT-SUPERVISOR";
098:
099: private static final String ACCOUNT_SUPERVISOR_ROLE_LABEL = "Account Supervisor";
100:
101: private static final String ACCOUNT_ATTRIBUTE = "KUALI_ACCOUNT_ATTRIBUTE";
102:
103: private static final String ROLE_STRING_DELIMITER = "~!~!~";
104:
105: // below map is used to signify that a document will route to delegates based on a different document type's code
106: private static final Map<String, String> DOCUMENT_TYPE_TRANSLATION = new HashMap<String, String>();
107: static {
108: DOCUMENT_TYPE_TRANSLATION
109: .put(
110: KualiWorkflowUtils.ACCOUNTS_PAYABLE_CREDIT_MEMO_DOCUMENT_TYPE,
111: KualiWorkflowUtils.ACCOUNTS_PAYABLE_PAYMENT_REQUEST_DOCUMENT_TYPE);
112: }
113:
114: private String finCoaCd;
115:
116: private String accountNbr;
117:
118: private String totalDollarAmount;
119:
120: private boolean required;
121:
122: /**
123: * No arg constructor
124: */
125: public KualiAccountAttribute() {
126: }
127:
128: /**
129: * Constructor that takes chart, account, and total dollar amount
130: *
131: * @param finCoaCd
132: * @param accountNbr
133: * @param totalDollarAmount
134: */
135: public KualiAccountAttribute(String finCoaCd, String accountNbr,
136: String totalDollarAmount) {
137: this .finCoaCd = LookupUtils.forceUppercase(Account.class,
138: "chartOfAccountsCode", finCoaCd);
139: this .accountNbr = LookupUtils.forceUppercase(Account.class,
140: "accountNumber", accountNbr);
141: this .totalDollarAmount = totalDollarAmount;
142: }
143:
144: /**
145: * return the universal set of role names provided by this attribute
146: */
147: public List getRoleNames() {
148: List roles = new ArrayList();
149: roles.add(new Role(this .getClass(), FISCAL_OFFICER_ROLE_KEY,
150: FISCAL_OFFICER_ROLE_LABEL));
151: roles.add(new Role(this .getClass(),
152: FISCAL_OFFICER_PRIMARY_DELEGATE_ROLE_KEY,
153: FISCAL_OFFICER_PRIMARY_DELEGATE_ROLE_LABEL));
154: roles.add(new Role(this .getClass(),
155: FISCAL_OFFICER_SECONDARY_DELEGATE_ROLE_KEY,
156: FISCAL_OFFICER_SECONDARY_DELEGATE_ROLE_LABEL));
157: roles.add(new Role(this .getClass(),
158: ACCOUNT_SUPERVISOR_ROLE_KEY,
159: ACCOUNT_SUPERVISOR_ROLE_LABEL));
160: return roles;
161: }
162:
163: /**
164: * return whether or not this attribute is required
165: *
166: * @return
167: */
168: public boolean isRequired() {
169: return required;
170: }
171:
172: /**
173: * simple setter
174: *
175: * @param required
176: */
177: public void setRequired(boolean required) {
178: this .required = required;
179: }
180:
181: /**
182: * simple getter for the rule extension values
183: *
184: * @return
185: */
186: public List getRuleExtensionValues() {
187: return Collections.EMPTY_LIST;
188: }
189:
190: /**
191: * method to validate the routing data, need to determine if this should actually be implemented to throw errors or anything
192: * like that.
193: *
194: * @param paramMap
195: * @return
196: */
197: public List validateRoutingData(Map paramMap) {
198: List errors = new ArrayList();
199: if (isRequired()) {
200: this .finCoaCd = LookupUtils.forceUppercase(Account.class,
201: "chartOfAccountsCode", (String) paramMap
202: .get(FIN_COA_CD_KEY));
203: this .accountNbr = LookupUtils.forceUppercase(Account.class,
204: "accountNumber", (String) paramMap
205: .get(ACCOUNT_NBR_KEY));
206: this .totalDollarAmount = (String) paramMap
207: .get(FDOC_TOTAL_DOLLAR_AMOUNT_KEY);
208: validateAccount(errors);
209: if (StringUtils.isNotBlank(this .totalDollarAmount)
210: && !KualiDecimal.isNumeric(this .totalDollarAmount)) {
211: errors
212: .add(new WorkflowServiceErrorImpl(
213: "Total Dollar Amount is invalid.",
214: "routetemplate.accountattribute.totaldollaramount.invalid"));
215: }
216: }
217: return errors;
218: }
219:
220: private void validateAccount(List errors) {
221: if (StringUtils.isBlank(this .finCoaCd)
222: || StringUtils.isBlank(this .accountNbr)) {
223: errors.add(new WorkflowServiceErrorImpl(
224: "Account is required.",
225: "routetemplate.accountattribute.account.required"));
226: return;
227: }
228: Account account = SpringContext.getBean(AccountService.class)
229: .getByPrimaryIdWithCaching(finCoaCd, accountNbr);
230: if (account == null) {
231: errors.add(new WorkflowServiceErrorImpl(
232: "Account is invalid.",
233: "routetemplate.accountattribute.account.invalid"));
234: }
235: }
236:
237: /**
238: * method to validate the rule data, since this is a role attribute, there is no rule data
239: *
240: * @param paramMap
241: * @return
242: */
243: public List validateRuleData(Map paramMap) {
244: return new ArrayList();
245: }
246:
247: /**
248: * method to actually construct the docContent that will be appended to this documents contents
249: *
250: * @return
251: */
252: public String getDocContent() {
253: if (Utilities.isEmpty(getFinCoaCd())
254: || Utilities.isEmpty(getAccountNbr())) {
255: return "";
256: }
257: return new StringBuffer(
258: KualiWorkflowUtils.XML_REPORT_DOC_CONTENT_PREFIX
259: + "<chart>")
260: .append(getFinCoaCd())
261: .append("</chart><accountNumber>")
262: .append(getAccountNbr())
263: .append("</accountNumber><totalDollarAmount>")
264: .append(getTotalDollarAmount())
265: .append(
266: "</totalDollarAmount>"
267: + KualiWorkflowUtils.XML_REPORT_DOC_CONTENT_SUFFIX)
268: .toString();
269: }
270:
271: public String getAttributeLabel() {
272: return "";
273: }
274:
275: /**
276: * return true since this is a rule attribute, and if there are no routing records returned, then there was no valid mapping in
277: * the docContent for a given role.
278: *
279: * @param docContent
280: * @param ruleExtensions
281: * @return
282: */
283: public boolean isMatch(DocumentContent docContent,
284: List ruleExtensions) {
285: return true;
286: }
287:
288: /**
289: * This method is used by the workflow report to allow the user to fill in some arbitrary values for the routable contents of an
290: * example document, and then to run the report to generate a virtual route log of who the document would route to, etc.
291: *
292: * @return
293: */
294: public List getRoutingDataRows() {
295: List rows = new ArrayList();
296: rows.add(KualiWorkflowUtils.buildTextRowWithLookup(Chart.class,
297: KFSConstants.CHART_OF_ACCOUNTS_CODE_PROPERTY_NAME,
298: FIN_COA_CD_KEY));
299: Map fieldConversionMap = new HashMap();
300: fieldConversionMap.put(
301: KFSConstants.CHART_OF_ACCOUNTS_CODE_PROPERTY_NAME,
302: FIN_COA_CD_KEY);
303: rows.add(KualiWorkflowUtils.buildTextRowWithLookup(
304: Account.class,
305: KFSConstants.ACCOUNT_NUMBER_PROPERTY_NAME,
306: ACCOUNT_NBR_KEY, fieldConversionMap));
307:
308: // List fields = new ArrayList();
309: // fields.add(new Field("Total Dollar Amount", "", Field.TEXT, false, FDOC_TOTAL_DOLLAR_AMOUNT_KEY, "", null, null));
310: // rows.add(new Row(fields));
311: rows.add(KualiWorkflowUtils.buildTextRow(DocumentHeader.class,
312: KFSPropertyConstants.FINANCIAL_DOCUMENT_TOTAL_AMOUNT,
313: FDOC_TOTAL_DOLLAR_AMOUNT_KEY));
314:
315: return rows;
316: }
317:
318: /**
319: * simple getter which returns empty
320: *
321: * @return
322: */
323: public List getRuleRows() {
324: return Collections.EMPTY_LIST;
325: }
326:
327: /**
328: * simple getter which returns the account number
329: *
330: * @return
331: */
332: public String getAccountNbr() {
333: return accountNbr;
334: }
335:
336: /**
337: * simple setter that takes the account number
338: *
339: * @param accountNbr
340: */
341: public void setAccountNbr(String accountNbr) {
342: this .accountNbr = accountNbr;
343: }
344:
345: /**
346: * simple getter which returns the chart
347: *
348: * @return
349: */
350: public String getFinCoaCd() {
351: return finCoaCd;
352: }
353:
354: /**
355: * simple setter which takes the chart
356: *
357: * @param finCoaCd
358: */
359: public void setFinCoaCd(String finCoaCd) {
360: this .finCoaCd = finCoaCd;
361: }
362:
363: /**
364: * simple getter which returns the total dollar amount
365: *
366: * @return
367: */
368: public String getTotalDollarAmount() {
369: return totalDollarAmount;
370: }
371:
372: /**
373: * simple setter which takes the total dollar amount
374: *
375: * @param totalDollarAmount
376: */
377: public void setTotalDollarAmount(String totalDollarAmount) {
378: this .totalDollarAmount = totalDollarAmount;
379: }
380:
381: private String getQualifiedRoleString(FiscalOfficerRole role) {
382: return new StringBuffer(getNullSafeValue(role.roleName))
383: .append(ROLE_STRING_DELIMITER).append(
384: getNullSafeValue(role.chart)).append(
385: ROLE_STRING_DELIMITER).append(
386: getNullSafeValue(role.accountNumber)).append(
387: ROLE_STRING_DELIMITER).append(
388: getNullSafeValue(role.totalDollarAmount))
389: .append(ROLE_STRING_DELIMITER).append(
390: getNullSafeValue(role.fiscalOfficerId))
391: .toString();
392: }
393:
394: private static FiscalOfficerRole getUnqualifiedFiscalOfficerRole(
395: String qualifiedRole) {
396: String[] values = qualifiedRole
397: .split(ROLE_STRING_DELIMITER, -1);
398: if (values.length != 5) {
399: throw new RuntimeException(
400: "Invalid qualifiedRole, expected 5 encoded values: "
401: + qualifiedRole);
402: }
403: FiscalOfficerRole role = new FiscalOfficerRole(values[0]);
404: role.chart = getNullableString(values[1]);
405: role.accountNumber = getNullableString(values[2]);
406: role.totalDollarAmount = getNullableString(values[3]);
407: role.fiscalOfficerId = getNullableString(values[4]);
408: return role;
409: }
410:
411: private static String getNullSafeValue(String value) {
412: return (value == null ? "" : value);
413: }
414:
415: private static String getNullableString(String value) {
416: if (StringUtils.isEmpty(value)) {
417: return null;
418: }
419: return value;
420: }
421:
422: private static String getQualifiedAccountSupervisorRoleString(
423: String roleName, String accountSupervisorySystemsId) {
424: return new StringBuffer(roleName).append(ROLE_STRING_DELIMITER)
425: .append(accountSupervisorySystemsId).toString();
426: }
427:
428: private static String getUnqualifiedAccountSupervisorIdFromString(
429: String qualifiedRole) {
430: return qualifiedRole.split(ROLE_STRING_DELIMITER)[1];
431: }
432:
433: /**
434: * Encodes the qualified role names for Fiscal Officer and Account Supervisor routing.
435: */
436: public List getQualifiedRoleNames(String roleName,
437: DocumentContent docContent)
438: throws EdenUserNotFoundException {
439: String newMaintPrefix = KualiWorkflowUtils.NEW_MAINTAINABLE_PREFIX;
440: String oldMaintPrefix = KualiWorkflowUtils.OLD_MAINTAINABLE_PREFIX;
441: try {
442: List qualifiedRoleNames = new ArrayList();
443: XPath xpath = KualiWorkflowUtils.getXPath(docContent
444: .getDocument());
445: String docTypeName = docContent.getRouteContext()
446: .getDocument().getDocumentType().getName();
447: if (FISCAL_OFFICER_ROLE_KEY.equals(roleName)
448: || FISCAL_OFFICER_PRIMARY_DELEGATE_ROLE_KEY
449: .equals(roleName)
450: || FISCAL_OFFICER_SECONDARY_DELEGATE_ROLE_KEY
451: .equals(roleName)) {
452: Set fiscalOfficers = new HashSet();
453: if (((Boolean) xpath
454: .evaluate(
455: KualiWorkflowUtils
456: .xstreamSafeXPath(KualiWorkflowUtils.XSTREAM_MATCH_ANYWHERE_PREFIX
457: + KualiWorkflowUtils.XML_REPORT_DOC_CONTENT_XPATH_PREFIX),
458: docContent.getDocument(),
459: XPathConstants.BOOLEAN)).booleanValue()) {
460: String chart = xpath
461: .evaluate(
462: KualiWorkflowUtils
463: .xstreamSafeXPath(KualiWorkflowUtils.XSTREAM_MATCH_ANYWHERE_PREFIX
464: + KualiWorkflowUtils.XML_REPORT_DOC_CONTENT_XPATH_PREFIX
465: + "/chart"),
466: docContent.getDocument());
467: String accountNumber = xpath
468: .evaluate(
469: KualiWorkflowUtils
470: .xstreamSafeXPath(KualiWorkflowUtils.XSTREAM_MATCH_ANYWHERE_PREFIX
471: + KualiWorkflowUtils.XML_REPORT_DOC_CONTENT_XPATH_PREFIX
472: + "/accountNumber"),
473: docContent.getDocument());
474: String totalDollarAmount = xpath
475: .evaluate(
476: KualiWorkflowUtils
477: .xstreamSafeXPath(KualiWorkflowUtils.XSTREAM_MATCH_ANYWHERE_PREFIX
478: + KualiWorkflowUtils.XML_REPORT_DOC_CONTENT_XPATH_PREFIX
479: + "/totalDollarAmount"),
480: docContent.getDocument());
481: FiscalOfficerRole role = new FiscalOfficerRole(
482: roleName);
483: role.chart = chart;
484: role.accountNumber = accountNumber;
485: role.totalDollarAmount = totalDollarAmount;
486: fiscalOfficers.add(role);
487: } else if (KualiWorkflowUtils.ACCOUNT_DOC_TYPE
488: .equals(docTypeName)) {
489: // 1) If this is a new account, it routes to the fiscal
490: // officer specified on the new account
491: // 2) If this is an account edit and the fiscal officer
492: // hasn't changed, route to the fiscal officer on the
493: // account
494: // 3) If this is an account edit and the fiscal officer HAS
495: // changed, route to the old fiscal officer and the new
496: // fiscal officer
497: // ...
498: // This logic crystallizes down to the following:
499: // Route on all unique fiscal officers on the document.
500: // Dont route to the same person twice.
501: //
502: String newFiscalOfficerId = KualiWorkflowUtils
503: .xstreamSafeEval(
504: xpath,
505: newMaintPrefix
506: + KFSPropertyConstants.ACCOUNT_FISCAL_OFFICER_SYSTEM_IDENTIFIER,
507: docContent.getDocument());
508: String oldFiscalOfficerId = KualiWorkflowUtils
509: .xstreamSafeEval(
510: xpath,
511: oldMaintPrefix
512: + KFSPropertyConstants.ACCOUNT_FISCAL_OFFICER_SYSTEM_IDENTIFIER,
513: docContent.getDocument());
514: String foChartCode = KualiWorkflowUtils
515: .xstreamSafeEval(
516: xpath,
517: newMaintPrefix
518: + KFSConstants.CHART_OF_ACCOUNTS_CODE_PROPERTY_NAME,
519: docContent.getDocument());
520: String foAccountNumber = KualiWorkflowUtils
521: .xstreamSafeEval(
522: xpath,
523: newMaintPrefix
524: + KFSConstants.ACCOUNT_NUMBER_PROPERTY_NAME,
525: docContent.getDocument());
526: if (StringUtils.isNotBlank(newFiscalOfficerId)) {
527: fiscalOfficers.add(new FiscalOfficerRole(
528: roleName, newFiscalOfficerId,
529: foChartCode, foAccountNumber));
530: }
531: if (StringUtils.isNotBlank(oldFiscalOfficerId)) {
532: if (!oldFiscalOfficerId
533: .equalsIgnoreCase(newFiscalOfficerId)) {
534: fiscalOfficers.add(new FiscalOfficerRole(
535: roleName, oldFiscalOfficerId,
536: foChartCode, foAccountNumber));
537: }
538: }
539: if (fiscalOfficers.isEmpty()) {
540: throw new RuntimeException(
541: "No Fiscal Officers were found in this Account Maintenance Document. Routing cannot continue without Fiscal Officers.");
542: }
543: } else if (KualiWorkflowUtils.SUB_ACCOUNT_DOC_TYPE
544: .equals(docTypeName)
545: || KualiWorkflowUtils.SUB_OBJECT_DOC_TYPE
546: .equals(docTypeName)
547: || KualiWorkflowUtils.ACCOUNT_DEL_DOC_TYPE
548: .equals(docTypeName)) {
549: String foChartCode = KualiWorkflowUtils
550: .xstreamSafeEval(
551: xpath,
552: newMaintPrefix
553: + KFSConstants.CHART_OF_ACCOUNTS_CODE_PROPERTY_NAME,
554: docContent.getDocument());
555: String foAccountNumber = KualiWorkflowUtils
556: .xstreamSafeEval(
557: xpath,
558: newMaintPrefix
559: + KFSConstants.ACCOUNT_NUMBER_PROPERTY_NAME,
560: docContent.getDocument());
561: fiscalOfficers.add(new FiscalOfficerRole(roleName,
562: foChartCode, foAccountNumber));
563: } else if (KualiWorkflowUtils.SUB_OBJECT_CODE_CHANGE_DOC_TYPE
564: .equals(docTypeName)) {
565: // route to the fiscal officers of the accounts on the AccountGlobalDetails
566: NodeList accountGlobalDetails = (NodeList) xpath
567: .evaluate(
568: KualiWorkflowUtils.ACCOUNT_GLOBAL_DETAILS_XPATH,
569: docContent.getDocument(),
570: XPathConstants.NODESET);
571: if (accountGlobalDetails != null) {
572: for (int index = 0; index < accountGlobalDetails
573: .getLength(); index++) {
574: Element accountGlobalDetail = (Element) accountGlobalDetails
575: .item(index);
576: String chartOfAccountsCode = getChildElementValue(
577: accountGlobalDetail,
578: KFSConstants.CHART_OF_ACCOUNTS_CODE_PROPERTY_NAME);
579: String accountNumber = getChildElementValue(
580: accountGlobalDetail,
581: KFSConstants.ACCOUNT_NUMBER_PROPERTY_NAME);
582: fiscalOfficers.add(new FiscalOfficerRole(
583: roleName, chartOfAccountsCode,
584: accountNumber));
585: }
586: }
587: } else {
588: if (!KualiWorkflowUtils
589: .isTargetLineOnly(docTypeName)) {
590: NodeList sourceLineNodes = (NodeList) xpath
591: .evaluate(
592: KualiWorkflowUtils
593: .xstreamSafeXPath(KualiWorkflowUtils.XSTREAM_MATCH_ANYWHERE_PREFIX
594: + KualiWorkflowUtils
595: .getSourceAccountingLineClassName(docTypeName)),
596: docContent.getDocument(),
597: XPathConstants.NODESET);
598: String totalDollarAmount = String
599: .valueOf(calculateTotalDollarAmount(docContent
600: .getDocument()));
601: fiscalOfficers.addAll(getFiscalOfficerCriteria(
602: xpath, sourceLineNodes, roleName,
603: totalDollarAmount));
604: }
605: if (!KualiWorkflowUtils
606: .isSourceLineOnly(docTypeName)) {
607: NodeList targetLineNodes = (NodeList) xpath
608: .evaluate(
609: KualiWorkflowUtils
610: .xstreamSafeXPath(KualiWorkflowUtils.XSTREAM_MATCH_ANYWHERE_PREFIX
611: + KualiWorkflowUtils
612: .getTargetAccountingLineClassName(docTypeName)),
613: docContent.getDocument(),
614: XPathConstants.NODESET);
615: String totalDollarAmount = String
616: .valueOf(calculateTotalDollarAmount(docContent
617: .getDocument()));
618: fiscalOfficers.addAll(getFiscalOfficerCriteria(
619: xpath, targetLineNodes, roleName,
620: totalDollarAmount));
621: }
622: }
623: for (Iterator iterator = fiscalOfficers.iterator(); iterator
624: .hasNext();) {
625: FiscalOfficerRole role = (FiscalOfficerRole) iterator
626: .next();
627: qualifiedRoleNames
628: .add(getQualifiedRoleString(role));
629: }
630: } else if (ACCOUNT_SUPERVISOR_ROLE_KEY.equals(roleName)) {
631: Set<String> super visors = getAccountSupervisorIds(
632: docTypeName, xpath, docContent);
633: for (String accountSupervisorId : super visors) {
634: qualifiedRoleNames
635: .add(getQualifiedAccountSupervisorRoleString(
636: roleName, accountSupervisorId));
637: }
638: }
639: return qualifiedRoleNames;
640: } catch (Exception e) {
641: throw new RuntimeException(e);
642: }
643: }
644:
645: /**
646: * Retrieves a Set of account supervisor Ids that should be routed to for the given document content.
647: */
648: private Set<String> getAccountSupervisorIds(String docTypeName,
649: XPath xpath, DocumentContent docContent) throws Exception {
650: Set<String> super visors = new HashSet<String>();
651: List<String> accountXPaths = new ArrayList<String>();
652: String super visorProperty = "accountsSupervisorySystemsIdentifier";
653: // Account Maintenance Document - route to Account Supervisor of account on new maintainable
654: if (docTypeName.equals(KualiWorkflowUtils.ACCOUNT_DOC_TYPE)) {
655: accountXPaths
656: .add(KualiWorkflowUtils
657: .xstreamSafeXPath(KualiWorkflowUtils.NEW_MAINTAINABLE_PREFIX_NTS));
658: }
659: // Sub Object Code Change Document - route to Account Supervisor of accounts on AccountGlobalDetails
660: else if (docTypeName
661: .equals(KualiWorkflowUtils.SUB_OBJECT_CODE_CHANGE_DOC_TYPE)) {
662: accountXPaths
663: .add(KualiWorkflowUtils.ACCOUNT_GLOBAL_DETAILS_XPATH);
664: }
665: for (String accountXPath : accountXPaths) {
666: NodeList accountNodes = (NodeList) xpath.evaluate(
667: accountXPath, docContent.getDocument(),
668: XPathConstants.NODESET);
669: for (int index = 0; index < accountNodes.getLength(); index++) {
670: Element accountElement = (Element) accountNodes
671: .item(index);
672: String chartOfAccountsCode = getChildElementValue(
673: accountElement,
674: KFSConstants.CHART_OF_ACCOUNTS_CODE_PROPERTY_NAME);
675: String accountNumber = getChildElementValue(
676: accountElement,
677: KFSConstants.ACCOUNT_NUMBER_PROPERTY_NAME);
678: if (!StringUtils.isBlank(accountNumber)
679: && !StringUtils.isBlank(chartOfAccountsCode)) {
680: Account account = SpringContext.getBean(
681: AccountService.class)
682: .getByPrimaryIdWithCaching(
683: chartOfAccountsCode, accountNumber);
684: if (account != null
685: && !StringUtils
686: .isBlank(account
687: .getAccountsSupervisorySystemsIdentifier())) {
688: super visors
689: .add(account
690: .getAccountsSupervisorySystemsIdentifier());
691: }
692: }
693: }
694: }
695: return super visors;
696: }
697:
698: private String getChildElementValue(Element element,
699: String childTagName) {
700: NodeList nodes = element.getChildNodes();
701: for (int index = 0; index < nodes.getLength(); index++) {
702: Node node = nodes.item(index);
703: if (Node.ELEMENT_NODE == node.getNodeType()
704: && node.getNodeName().equals(childTagName)) {
705: return node.getFirstChild().getNodeValue();
706: }
707: }
708: return null;
709: }
710:
711: private static String calculateTotalDollarAmount(Document document)
712: throws XPathExpressionException {
713: KualiDecimal sum = KualiWorkflowUtils
714: .getFinancialDocumentTotalAmount(document);
715: if (ObjectUtils.isNull(sum)) {
716: sum = KualiDecimal.ZERO;
717: }
718: return sum.toString();
719: }
720:
721: private static Set getFiscalOfficerCriteria(XPath xpath,
722: NodeList accountingLineNodes, String roleName,
723: String totalDollarAmount) throws XPathExpressionException {
724: Set fiscalOfficers = new HashSet();
725: for (int i = 0; i < accountingLineNodes.getLength(); i++) {
726: Node accountingLineNode = accountingLineNodes.item(i);
727: FiscalOfficerRole role = new FiscalOfficerRole(roleName);
728: role.chart = xpath
729: .evaluate(
730: KualiWorkflowUtils.XSTREAM_MATCH_RELATIVE_PREFIX
731: + KFSConstants.CHART_OF_ACCOUNTS_CODE_PROPERTY_NAME,
732: accountingLineNode);
733: role.accountNumber = xpath
734: .evaluate(
735: KualiWorkflowUtils.XSTREAM_MATCH_RELATIVE_PREFIX
736: + KFSConstants.ACCOUNT_NUMBER_PROPERTY_NAME,
737: accountingLineNode);
738: role.totalDollarAmount = totalDollarAmount;
739: fiscalOfficers.add(role);
740: }
741: return fiscalOfficers;
742: }
743:
744: /**
745: * Resolves the qualified roles for Fiscal Officers, their delegates, and account supervisors.
746: */
747: public ResolvedQualifiedRole resolveQualifiedRole(
748: RouteContext context, String roleName, String qualifiedRole)
749: throws EdenUserNotFoundException {
750: try {
751: List members = new ArrayList();
752: String workfowDocumentType = context.getDocument()
753: .getDocumentType().getName();
754: if ((DOCUMENT_TYPE_TRANSLATION
755: .containsKey(workfowDocumentType))
756: && (DOCUMENT_TYPE_TRANSLATION
757: .get(workfowDocumentType) != null)) {
758: workfowDocumentType = DOCUMENT_TYPE_TRANSLATION
759: .get(workfowDocumentType);
760: }
761: String kualiDocumentType = SpringContext.getBean(
762: DataDictionaryService.class)
763: .getDocumentTypeCodeByTypeName(workfowDocumentType);
764: String annotation = "";
765: if (FISCAL_OFFICER_ROLE_KEY.equals(roleName)) {
766: FiscalOfficerRole role = getUnqualifiedFiscalOfficerRole(qualifiedRole);
767: annotation = (role.accountNumber == null ? ""
768: : "Routing to account number "
769: + role.accountNumber);
770: UserId fiscalOfficerId = getFiscalOfficerId(role);
771: if (fiscalOfficerId != null) {
772: members.add(fiscalOfficerId);
773: }
774: } else if (FISCAL_OFFICER_PRIMARY_DELEGATE_ROLE_KEY
775: .equals(roleName)) {
776: FiscalOfficerRole role = getUnqualifiedFiscalOfficerRole(qualifiedRole);
777: UserId primaryDelegate = getPrimaryDelegation(role,
778: kualiDocumentType);
779: if (primaryDelegate != null) {
780: members.add(primaryDelegate);
781: }
782: } else if (FISCAL_OFFICER_SECONDARY_DELEGATE_ROLE_KEY
783: .equals(roleName)) {
784: FiscalOfficerRole role = getUnqualifiedFiscalOfficerRole(qualifiedRole);
785: members.addAll(getSecondaryDelegations(role,
786: kualiDocumentType));
787: } else if (ACCOUNT_SUPERVISOR_ROLE_KEY.equals(roleName)) {
788: String accountSupervisorId = getUnqualifiedAccountSupervisorIdFromString(qualifiedRole);
789: annotation = "Routing to Account Supervisor";
790: String super visorNetworkId = SpringContext.getBean(
791: UniversalUserService.class).getUniversalUser(
792: new UuId(accountSupervisorId))
793: .getPersonUserIdentifier();
794: if (!StringUtils.isEmpty(super visorNetworkId)) {
795: members.add(new AuthenticationUserId(
796: super visorNetworkId));
797: } else {
798: LOG.info("No active account supervisor found.");
799: }
800: }
801: return new ResolvedQualifiedRole(roleName, members,
802: annotation);
803: } catch (Exception e) {
804: throw new RuntimeException(
805: "KualiAccountAttribute encountered exception while attempting to resolve qualified role",
806: e);
807: }
808: }
809:
810: private static AuthenticationUserId getFiscalOfficerId(
811: FiscalOfficerRole role) throws Exception {
812: String fiscalOfficerNetworkId = null;
813:
814: // if we already have an ID, validate it, and then we're done
815: if (StringUtils.isNotBlank(role.fiscalOfficerId)) {
816: try {
817: fiscalOfficerNetworkId = SpringContext.getBean(
818: UniversalUserService.class).getUniversalUser(
819: new UuId(role.fiscalOfficerId))
820: .getPersonUserIdentifier();
821: } catch (org.kuali.core.exceptions.UserNotFoundException e) {
822: // do nothing, but leave fiscalOfficerNetworkId blank, which will get caught after this
823: }
824: if (StringUtils.isBlank(fiscalOfficerNetworkId)) {
825: throw new RuntimeException(
826: "FiscalOfficer with UniversalID: "
827: + role.fiscalOfficerId
828: + " was not "
829: + "found in UniversalUsers. Routing cannot continue.");
830: } else {
831: return new AuthenticationUserId(fiscalOfficerNetworkId);
832: }
833: }
834:
835: // if we dont have an ID, but we do have a chart/account, then hit Kuali to retrieve current FO
836: if (StringUtils.isNotBlank(role.chart)
837: && StringUtils.isNotBlank(role.accountNumber)) {
838: Account account = SpringContext.getBean(
839: AccountService.class).getByPrimaryIdWithCaching(
840: role.chart, role.accountNumber);
841: if (account != null) {
842: if (account.getAccountFiscalOfficerUser() != null) {
843: fiscalOfficerNetworkId = account
844: .getAccountFiscalOfficerUser()
845: .getPersonUserIdentifier();
846: }
847: }
848: }
849:
850: // if we cant find a FiscalOfficer, then something is wrong, so throw an exception
851: if (StringUtils.isBlank(fiscalOfficerNetworkId)) {
852: LOG
853: .warn(new StringBuffer(
854: "Could not locate the fiscal officer for the given account ")
855: .append(role.accountNumber).append(
856: " / fiscal officer uid ").append(
857: role.fiscalOfficerId).toString());
858: throw new RuntimeException(
859: new StringBuffer(
860: "Could not locate the fiscal officer for the given account ")
861: .append(role.accountNumber).append(
862: " / fiscal officer uid ").append(
863: role.fiscalOfficerId).toString());
864: }
865: return new AuthenticationUserId(fiscalOfficerNetworkId);
866: }
867:
868: /**
869: * Returns a the UserId of the primary delegation on the given FiscalOfficerRole. If the given role doesn't have an account
870: * number or there is no primary delegate, returns null.
871: *
872: * @throws RuntimeException if there is more than one primary delegation on the given account
873: */
874: private static UserId getPrimaryDelegation(FiscalOfficerRole role,
875: String fisDocumentType) throws Exception {
876: UserId primaryDelegateId = null;
877: if (role.accountNumber != null) {
878: Delegate delegateExample = new Delegate();
879: delegateExample.setChartOfAccountsCode(role.chart);
880: delegateExample.setAccountNumber(role.accountNumber);
881: delegateExample
882: .setFinancialDocumentTypeCode(fisDocumentType);
883: Delegate primaryDelegate = SpringContext.getBean(
884: AccountService.class)
885: .getPrimaryDelegationByExample(delegateExample,
886: role.totalDollarAmount);
887: if (primaryDelegate != null) {
888: primaryDelegateId = new AuthenticationUserId(
889: primaryDelegate.getAccountDelegate()
890: .getPersonUserIdentifier());
891: }
892: }
893: return primaryDelegateId;
894: }
895:
896: /**
897: * Returns a list of UserIds for all secondary delegations on the given FiscalOfficerRole. If the given role doesn't have an
898: * account number or there are no delegations, returns an empty list.
899: */
900: private static List getSecondaryDelegations(FiscalOfficerRole role,
901: String fisDocumentType) throws Exception {
902: List members = new ArrayList();
903: if (role.accountNumber != null) {
904: Delegate delegateExample = new Delegate();
905: delegateExample.setChartOfAccountsCode(role.chart);
906: delegateExample.setAccountNumber(role.accountNumber);
907: delegateExample
908: .setFinancialDocumentTypeCode(fisDocumentType);
909: Iterator secondaryDelegations = SpringContext.getBean(
910: AccountService.class)
911: .getSecondaryDelegationsByExample(delegateExample,
912: role.totalDollarAmount).iterator();
913: while (secondaryDelegations.hasNext()) {
914: members.add(new AuthenticationUserId(
915: ((Delegate) secondaryDelegations.next())
916: .getAccountDelegate()
917: .getPersonUserIdentifier()));
918: }
919: }
920: return members;
921: }
922:
923: /**
924: * A helper class which defines a Fiscal Officer role. Implements an equals() and hashCode() method so that it can be used in a
925: * Set to prevent the generation of needless duplicate requests.
926: */
927: private static class FiscalOfficerRole {
928:
929: public String roleName;
930:
931: public String fiscalOfficerId;
932:
933: public String chart;
934:
935: public String accountNumber;
936:
937: public String totalDollarAmount;
938:
939: public FiscalOfficerRole(String roleName) {
940: this .roleName = roleName;
941: }
942:
943: public FiscalOfficerRole(String roleName,
944: String fiscalOfficerId, String chart,
945: String accountNumber) {
946: this .roleName = roleName;
947: this .fiscalOfficerId = fiscalOfficerId;
948: this .chart = chart;
949: this .accountNumber = accountNumber;
950: }
951:
952: public FiscalOfficerRole(String roleName, String chart,
953: String accountNumber) {
954: this .roleName = roleName;
955: this .chart = chart;
956: this .accountNumber = accountNumber;
957: }
958:
959: @Override
960: public boolean equals(Object object) {
961: if (object instanceof FiscalOfficerRole) {
962: FiscalOfficerRole role = (FiscalOfficerRole) object;
963: return new EqualsBuilder().append(roleName,
964: role.roleName).append(fiscalOfficerId,
965: role.fiscalOfficerId).append(chart, role.chart)
966: .append(accountNumber, role.accountNumber)
967: .append(totalDollarAmount,
968: role.totalDollarAmount).isEquals();
969: }
970: return false;
971: }
972:
973: @Override
974: public int hashCode() {
975: return new HashCodeBuilder().append(roleName).append(
976: fiscalOfficerId).append(chart)
977: .append(accountNumber).append(totalDollarAmount)
978: .hashCode();
979: }
980:
981: }
982:
983: }
|