/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 The Kuali Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kuali.kfs.sys.document.web; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.JspFragment; import javax.servlet.jsp.tagext.Tag; import javax.servlet.jsp.tagext.TagSupport; import org.apache.commons.lang.StringUtils; import org.kuali.kfs.sys.businessobject.AccountingLine; import org.kuali.kfs.sys.context.SpringContext; import org.kuali.kfs.sys.document.AccountingDocument; import org.kuali.kfs.sys.document.datadictionary.AccountingLineGroupDefinition; import org.kuali.kfs.sys.document.datadictionary.FinancialSystemTransactionalDocumentEntry; import org.kuali.kfs.sys.document.service.AccountingLineRenderingService; import org.kuali.kfs.sys.web.struts.KualiAccountingDocumentFormBase; import org.kuali.rice.kim.api.identity.Person; import org.kuali.rice.kns.service.DataDictionaryService; import org.kuali.rice.krad.util.GlobalVariables; import org.kuali.rice.krad.util.KRADConstants; import org.kuali.rice.krad.util.ObjectUtils; /** * Tag that is responsible for rendering an accounting line group */ public class AccountingLineGroupTag extends TagSupport { private String collectionPropertyName; private String collectionItemPropertyName; private String newLinePropertyName; private String attributeGroupName; private JspFragment importLineOverride; private AccountingLineGroupDefinition groupDefinition; private AccountingLineGroup group; private KualiAccountingDocumentFormBase form; private AccountingDocument document; private Set<String> editModes; /** * Gets the attributeGroupName attribute. * @return Returns the attributeGroupName. */ public String getAttributeGroupName() { return attributeGroupName; } /** * Sets the attributeGroupName attribute value. * @param attributeGroupName The attributeGroupName to set. */ public void setAttributeGroupName(String attributeGroup) { this.attributeGroupName = attributeGroup; } /** * Gets the collectionPropertyName attribute. * @return Returns the collectionPropertyName. */ public String getCollectionPropertyName() { return collectionPropertyName; } /** * Sets the collectionPropertyName attribute value. * @param collectionPropertyName The collectionPropertyName to set. */ public void setCollectionPropertyName(String collectionProperties) { this.collectionPropertyName = collectionProperties; } /** * Gets the newLinePropertyName attribute. * @return Returns the newLinePropertyName. */ public String getNewLinePropertyName() { return newLinePropertyName; } /** * Sets the newLinePropertyName attribute value. * @param newLinePropertyName The newLinePropertyName to set. */ public void setNewLinePropertyName(String newLineProperty) { this.newLinePropertyName = newLineProperty; } /** * Gets the importLineOverride attribute. * @return Returns the importLineOverride. */ public JspFragment getImportLineOverride() { return importLineOverride; } /** * Sets the importLineOverride attribute value. * @param importLineOverride The importLineOverride to set. */ public void setImportLineOverride(JspFragment importLineOverride) { this.importLineOverride = importLineOverride; } /** * Gets the collectionItemPropertyName attribute. * @return Returns the collectionItemPropertyName. */ public String getCollectionItemPropertyName() { return collectionItemPropertyName; } /** * Sets the collectionItemPropertyName attribute value. * @param collectionItemPropertyName The collectionItemPropertyName to set. */ public void setCollectionItemPropertyName(String collectionItemPropertyName) { if (StringUtils.isBlank(collectionItemPropertyName)) { collectionItemPropertyName = generateItemPropertyFromCollectionNameTheDumbWay(collectionPropertyName); } this.collectionItemPropertyName = collectionItemPropertyName; } /** * Automagically generates teh name of the collection item property from the collection property by taking any extra "s" off the end. Note: * I never claimed this method was smart. You'd be surprised at how often it'll work, though. * @param collectionName the collection name to generate the collection item name from * @return the collection item property name */ protected String generateItemPropertyFromCollectionNameTheDumbWay(String collectionName) { int subStringEnd = collectionName.length(); if (collectionName.endsWith("s")) { subStringEnd -= 1; } return collectionName.substring(0, subStringEnd); } /** * @see javax.servlet.jsp.tagext.TagSupport#doStartTag() */ @Override public int doStartTag() throws JspException { super.doStartTag(); final List<RenderableAccountingLineContainer> containers = generateContainersForAllLines(); group = groupDefinition.createAccountingLineGroup(getDocument(), containers, collectionPropertyName, collectionItemPropertyName, getForm().getDisplayedErrors(), getForm().getDisplayedWarnings(), getForm().getDisplayedInfo(), getForm().getDocumentActions().containsKey(KRADConstants.KUALI_ACTION_CAN_EDIT)); group.updateDeletabilityOfAllLines(); if (getParent() instanceof AccountingLinesTag) { ((AccountingLinesTag)getParent()).addGroupToRender(group); resetTag(); } return Tag.SKIP_BODY; } /** * If our parent isn't AccountingLinesTag, then we should render all the group * @see javax.servlet.jsp.tagext.TagSupport#doEndTag() */ @Override public int doEndTag() throws JspException { super.doEndTag(); if (!(getParent() instanceof AccountingLinesTag)) { group.renderEverything(pageContext, getParent()); resetTag(); } return Tag.EVAL_PAGE; } /** * Clears out any state variables on the tag */ protected void resetTag() { collectionPropertyName = null; collectionItemPropertyName = null; newLinePropertyName = null; attributeGroupName = null; importLineOverride = null; groupDefinition = null; group = null; form = null; document = null; editModes = null; } /** * Gets the correct accounting line group definition for this * @return the accounting line group definition to be used by this tag */ protected AccountingLineGroupDefinition getGroupDefinition() { if (groupDefinition == null) { final String documentTypeClassName = getDocument().getClass().getName(); final FinancialSystemTransactionalDocumentEntry documentEntry = (FinancialSystemTransactionalDocumentEntry)SpringContext.getBean(DataDictionaryService.class).getDataDictionary().getDictionaryObjectEntry(documentTypeClassName); final Map<String, AccountingLineGroupDefinition> groupDefinitions = documentEntry.getAccountingLineGroups(); groupDefinition = groupDefinitions.get(attributeGroupName); } return groupDefinition; } /** * Creates a List of renderable elements, with all rendering transformations accomplished, to be rendered * @param groupDefinition the data dictionary definition of the accounting line group this line is in * @param accountingLine the accounting line itself * @param newLine true if the line is new and not added yet to the document, false otherwise * @return a List of AccountingLineTableRows to render */ protected List<AccountingLineTableRow> getRenderableElementsForLine(AccountingLineGroupDefinition groupDefinition, AccountingLine accountingLine, boolean newLine, boolean topLine, String accountingLinePropertyName) { List<TableJoining> layoutElements = groupDefinition.getAccountingLineView().getAccountingLineLayoutElements(accountingLine.getClass()); AccountingLineRenderingService renderingService = SpringContext.getBean(AccountingLineRenderingService.class); renderingService.performPreTablificationTransformations(layoutElements, groupDefinition, getDocument(), accountingLine, newLine, getForm().getUnconvertedValues(), accountingLinePropertyName); List<AccountingLineTableRow> renderableElements = renderingService.tablify(layoutElements); removeTopRowIfNecessary(groupDefinition, topLine, renderableElements); renderingService.performPostTablificationTransformations(renderableElements, groupDefinition, getDocument(), accountingLine, newLine); return renderableElements; } /** * If it is determined that removing the first row (presumably a header row) is necessary, removes it * @param rows the rows to possibly remove the top row from */ protected void removeTopRowIfNecessary(AccountingLineGroupDefinition definition, boolean topLine, List<AccountingLineTableRow> rows) { if (definition.isTopHeadersAfterFirstLineHiding() && !topLine) { safelyRemoveTopRow(rows); } } /** * Looks through the top row. If there's any fields in the top row, it quietly fails; it also won't remove the top row if that's the only row there is * @param rows the List of rows to remove the top one of if possible */ protected void safelyRemoveTopRow(List<AccountingLineTableRow> rows) { if (rows != null && rows.size() > 1 && rows.get(0).safeToRemove()) { rows.remove(0); } } /** * @return the new accounting line from the form */ protected AccountingLine getNewAccountingLine() { return (AccountingLine)ObjectUtils.getPropertyValue(getForm(), newLinePropertyName); } /** * @return the collection of accounting lines that this tag is supposed to render */ protected List<AccountingLine> getAccountingLineCollection() { return (List<AccountingLine>)ObjectUtils.getPropertyValue(getForm(), collectionPropertyName); } /** * @return a List of accounting line table rows to be rendered for all the accounting lines available for rendering within the group */ protected List<RenderableAccountingLineContainer> generateContainersForAllLines() { List<RenderableAccountingLineContainer> containers = new ArrayList<RenderableAccountingLineContainer>(); final AccountingLineGroupDefinition groupDefinition = getGroupDefinition(); final AccountingDocument document = getDocument(); final Person currentUser = GlobalVariables.getUserSession().getPerson(); boolean addedTopLine = false; // add all existing lines int count = 0; boolean anyEditableLines = false; List<AccountingLine> lines = getAccountingLineCollection(); Collections.sort(lines, getGroupDefinition().getAccountingLineComparator()); for (AccountingLine accountingLine : lines) { final RenderableAccountingLineContainer container = buildContainerForLine(groupDefinition, document, accountingLine, currentUser, new Integer(count), (addedTopLine ? false : true)); containers.add(container); anyEditableLines = anyEditableLines || container.isEditableLine() || isMessageMapContainingErrorsOnLine(container.getAccountingLinePropertyPath()); count += 1; addedTopLine = true; } // add the new line if (StringUtils.isNotBlank(newLinePropertyName) && ((getForm().getDocumentActions().containsKey(KRADConstants.KUALI_ACTION_CAN_EDIT) && groupDefinition.getAccountingLineAuthorizer().renderNewLine(document, collectionPropertyName)) || anyEditableLines)) { containers.add(0, buildContainerForLine(groupDefinition, document, getNewAccountingLine(), currentUser, null, true)); } return containers; } /** * Determines if the error map contains any errors which exist on the currently rendered accounting line * @param accountingLinePropertyName the property name of the accounting line * @return true if there are errors on the line, false otherwise */ protected boolean isMessageMapContainingErrorsOnLine(String accountingLinePropertyName) { for (Object errorKeyAsObject : GlobalVariables.getMessageMap().getAllPropertiesWithErrors()) { if (((String)errorKeyAsObject).startsWith(accountingLinePropertyName)) return true; } return false; } /** * Builds an accounting line container for a given accounting line to render * @param groupDefinition the data dictionary definition of the group this line is a part of * @param accountingDocument the accounting document the line is or will someday live on * @param accountingLine the accounting line to render * @param currentUser the currently logged in user * @param count the count of this line within the collection represented by the group; null if this is a new line for the group * @return the container created */ protected RenderableAccountingLineContainer buildContainerForLine(AccountingLineGroupDefinition groupDefinition, AccountingDocument accountingDocument, AccountingLine accountingLine, Person currentUser, Integer count, boolean topLine) { final String accountingLinePropertyName = count == null ? newLinePropertyName : collectionItemPropertyName+"["+count.toString()+"]"; final boolean newLine = (count == null); final List<AccountingLineTableRow> rows = getRenderableElementsForLine(groupDefinition, accountingLine, newLine, topLine, accountingLinePropertyName); final boolean pageIsEditable = getForm().getDocumentActions().containsKey(KRADConstants.KUALI_ACTION_CAN_EDIT); return new RenderableAccountingLineContainer(getForm(), accountingLine, accountingLinePropertyName, rows, count, groupDefinition.getGroupLabel(), getErrors(), groupDefinition.getAccountingLineAuthorizer(), groupDefinition.getAccountingLineAuthorizer().hasEditPermissionOnAccountingLine(getDocument(), accountingLine, collectionPropertyName, currentUser, pageIsEditable)); } /** * Clean up state held by this tag * @see javax.servlet.jsp.tagext.TagSupport#release() */ @Override public void release() { super.release(); resetTag(); } /** * @return the form that this document is currently using */ protected KualiAccountingDocumentFormBase getForm() { if (form == null) { if (getParent() instanceof AccountingLinesTag) { form = ((AccountingLinesTag)getParent()).getForm(); } else { form = SpringContext.getBean(AccountingLineRenderingService.class).findForm(pageContext); } } return form; } /** * @return the ErrorPropertyList from the request */ protected List getErrors() { return (List)pageContext.getRequest().getAttribute("ErrorPropertyList"); } /** * @return the document this tag is helping to display */ protected AccountingDocument getDocument() { if (document == null) { document = getForm().getFinancialDocument(); } return document; } }