/* * 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.text.MessageFormat; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import javax.servlet.jsp.JspException; import javax.servlet.jsp.PageContext; import javax.servlet.jsp.tagext.Tag; import org.apache.commons.collections.BidiMap; import org.apache.commons.collections.bidimap.DualHashBidiMap; import org.apache.commons.lang.StringUtils; import org.kuali.kfs.coa.service.AccountService; import org.kuali.kfs.sys.KFSConstants; import org.kuali.kfs.sys.KFSKeyConstants; import org.kuali.kfs.sys.KFSPropertyConstants; 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.AccountingLineViewFieldDefinition; import org.kuali.kfs.sys.document.service.AccountingLineFieldRenderingTransformation; import org.kuali.kfs.sys.document.service.AccountingLineRenderingService; import org.kuali.kfs.sys.document.web.renderers.DynamicNameLabelRenderer; import org.kuali.kfs.sys.document.web.renderers.FieldRenderer; import org.kuali.rice.core.api.config.property.ConfigurationService; import org.kuali.rice.kns.lookup.LookupUtils; import org.kuali.rice.kns.util.FieldUtils; import org.kuali.rice.kns.web.ui.Field; import org.kuali.rice.krad.bo.PersistableBusinessObject; import org.kuali.rice.krad.service.PersistenceStructureService; import org.kuali.rice.krad.util.ObjectUtils; /** * Represents a field (plus, optionally, a dynamic name field) to be rendered as part of an accounting line. */ public class AccountingLineViewField extends FieldTableJoiningWithHeader implements HeaderLabelPopulating, ReadOnlyable { public static final String ACCOUNTING_LINE_NAME_PREFIX_PLACE_HOLDER = "${accountingLineName}"; private Field field; private AccountingLineViewFieldDefinition definition; private int arbitrarilyHighIndex; private List<AccountingLineViewOverrideField> overrideFields; private PersistenceStructureService persistenceStructureService; protected boolean explodable; /*These are the properties that shall not be refreshed because they commonly * are not in the database by the time they need to be rendered (perhaps they're * only attached to the form).*/ protected List<String> nonRefreshedPropertyList = Collections.unmodifiableList(Arrays.asList(KFSPropertyConstants.SALES_TAX)); public boolean isExplodable() { return this.explodable; } public void setExplodable(boolean explodable) { this.explodable = explodable; } /** * Gets the definition attribute. * * @return Returns the definition. */ public AccountingLineViewFieldDefinition getDefinition() { return definition; } /** * Sets the definition attribute value. * * @param definition The definition to set. */ public void setDefinition(AccountingLineViewFieldDefinition definition) { this.definition = definition; } /** * Determines if this field should use the short label or not * * @return true if the short label should be used, false otherwise */ private boolean shouldUseShortLabel() { return definition.shouldUseShortLabel(); } /** * Gets the field attribute. * * @return Returns the field. * * KRAD Conversion: Gets the fields - No use of data dictionary */ public Field getField() { return field; } /** * Sets the field attribute value. * * @param field The field to set. * * KRAD Conversion: sets the fields - No use of data dictionary */ public void setField(Field field) { this.field = field; } /** * Gets the overrideFields attribute. * * @return Returns the overrideFields. */ public List<AccountingLineViewOverrideField> getOverrideFields() { return overrideFields; } /** * Sets the overrideFields attribute value. * * @param overrideFields The overrideFields to set. */ public void setOverrideFields(List<AccountingLineViewOverrideField> overrideFields) { this.overrideFields = overrideFields; } /** * Checks the field to see if the field itself is hidden * * @see org.kuali.kfs.sys.document.web.AccountingLineViewRenderableElementField#isHidden() */ @Override public boolean isHidden() { return (field.getFieldType().equals(Field.HIDDEN) || definition.isHidden()); } /** * Asks the wrapped field if it is read only (dynamic fields are, of course, always read only and therefore don't count in this * determination) * * @see org.kuali.kfs.sys.document.web.AccountingLineViewRenderableElementField#isReadOnly() */ @Override public boolean isReadOnly() { return field.isReadOnly() || isHidden(); } /** * @see org.kuali.kfs.sys.document.web.TableJoining#getName() */ @Override public String getName() { return field.getPropertyName(); } /** * @see org.kuali.kfs.sys.document.web.TableJoining#readOnlyize() */ @Override public void readOnlyize() { if (!isHidden()) { this.field.setReadOnly(true); } } /** * @see org.kuali.kfs.sys.document.web.TableJoiningWithHeader#getHeaderLabelProperty() */ public String getHeaderLabelProperty() { return this.field.getPropertyName(); } /** * @see org.kuali.kfs.sys.document.web.RenderableElement#renderElement(javax.servlet.jsp.PageContext, * javax.servlet.jsp.tagext.Tag) */ @Override public void renderElement(PageContext pageContext, Tag parentTag, AccountingLineRenderingContext renderingContext) throws JspException { renderField(pageContext, parentTag, renderingContext); if (getOverrideFields() != null && getOverrideFields().size() > 0) { renderOverrideFields(pageContext, parentTag, renderingContext); } if (shouldRenderDynamicFeldLabel() && renderingContext.fieldsCanRenderDynamicLabels()) { renderDynamicNameLabel(pageContext, parentTag, renderingContext); } } /** * Renders the field portion of this tag * * @param pageContext the page context to render to * @param parentTag the tag requesting rendering * @param renderingContext the rendering context of the accounting line * @throws JspException thrown if something goes wrong */ protected void renderField(PageContext pageContext, Tag parentTag, AccountingLineRenderingContext renderingContext) throws JspException { AccountingLine accountingLine = renderingContext.getAccountingLine(); String accountingLineProperty = renderingContext.getAccountingLinePropertyPath(); List<String> fieldNames = renderingContext.getFieldNamesForAccountingLine(); List errors = renderingContext.getErrors(); this.getField().setPropertyPrefix(accountingLineProperty); boolean chartSetByAccount = getName().equals(KFSConstants.CHART_OF_ACCOUNTS_CODE_PROPERTY_NAME) && !SpringContext.getBean(AccountService.class).accountsCanCrossCharts(); //set chartOfAccountsCode readOnly if account can't cross charts if (!renderingContext.isFieldModifyable(this.getName()) || chartSetByAccount) { this.getField().setReadOnly(true); } FieldRenderer renderer = SpringContext.getBean(AccountingLineRenderingService.class).getFieldRendererForField(getField(), accountingLine); if (renderer != null) { prepareFieldRenderer(renderer, getField(), renderingContext.getAccountingDocument(), accountingLine, accountingLineProperty, fieldNames); if (fieldInError(errors)) { renderer.setShowError(true); } if (!isHidden()) { renderer.openNoWrapSpan(pageContext, parentTag); } // dynamically set the accessible title to the current field if (!this.isReadOnly()) { String accessibleTitle = getField().getFieldLabel(); if (renderingContext.isNewLine()) { String format = SpringContext.getBean(ConfigurationService.class).getPropertyValueAsString(KFSKeyConstants.LABEL_NEW_ACCOUNTING_LINE_FIELD); accessibleTitle = MessageFormat.format(format, accessibleTitle, renderingContext.getGroupLabel()); } else { Integer lineNumber = renderingContext.getCurrentLineCount() + 1; String format = SpringContext.getBean(ConfigurationService.class).getPropertyValueAsString(KFSKeyConstants.LABEL_ACCOUNTING_LINE_FIELD); accessibleTitle = MessageFormat.format(format, accessibleTitle, renderingContext.getGroupLabel(), lineNumber); } renderer.setAccessibleTitle(accessibleTitle); } renderer.render(pageContext, parentTag); if (!isHidden()) { if (this.isExplodable()) { renderer.renderExplodableLink(pageContext); } renderer.closeNoWrapSpan(pageContext, parentTag); } renderer.clear(); } } /** * Updates the field so that it can have a quickfinder and inquiry link if need be * * @param accountingDocument the accounting document the accounting line the field will render part of is on or will at some * point be on * @param accountingLine the accounting line that is being rendered * @param fieldNames the list of all fields being displayed on this accounting line * @param accountingLinePrefix the prefix of all field names in the accounting line */ protected void populateFieldForLookupAndInquiry(AccountingDocument accountingDocument, AccountingLine accountingLine, List<String> fieldNames, String accountingLinePrefix) { if (!isHidden()) { LookupUtils.setFieldQuickfinder(accountingLine, getField().getPropertyName(), getField(), fieldNames); // apply the customized lookup parameters if any String overrideLookupParameters = definition.getOverrideLookupParameters(); if (StringUtils.isNotBlank(overrideLookupParameters)) { String lookupParameters = getField().getLookupParameters(); Map<String, String> lookupParametersMap = this.getActualParametersMap(lookupParameters, overrideLookupParameters, accountingLinePrefix); getField().setLookupParameters(lookupParametersMap); // if there are any any lookup parameters present, make sure the other lookup fields are populated. // this can be necessary if there wouldnt natually be a lookup, via DD or OJB relationships, but one // is forced. if (!lookupParametersMap.isEmpty()) { if (getDefinition().getOverrideLookupClass() != null) { getField().setQuickFinderClassNameImpl(getDefinition().getOverrideLookupClass().getName()); } } } // apply the customized field conversions if any String overrideFieldConversions = definition.getOverrideFieldConversions(); if (StringUtils.isNotBlank(overrideFieldConversions)) { String fieldConversions = getField().getFieldConversions(); Map<String, String> fieldConversionsMap = this.getActualParametersMap(fieldConversions, overrideFieldConversions, accountingLinePrefix); getField().setFieldConversions(fieldConversionsMap); } if (isRenderingInquiry(accountingDocument, accountingLine)) { FieldUtils.setInquiryURL(getField(), accountingLine, getField().getPropertyName()); } } } /** * Lazily retrieves the persistence structure service * * @return an implementation of PersistenceStructureService */ protected PersistenceStructureService getPersistenceStructureService() { if (persistenceStructureService == null) { persistenceStructureService = SpringContext.getBean(PersistenceStructureService.class); } return persistenceStructureService; } /** * Does some initial set up on the field renderer - sets the field and the business object being rendered * * @param fieldRenderer the field renderer to prepare * @param accountingLine the accounting line being rendered * @param accountingLineProperty the property to get the accounting line from the form * @param fieldNames the names of all the fields that will be rendered as part of this accounting line * * KRAD Conversion: Customization of the fields - No use of data dictionary */ protected void prepareFieldRenderer(FieldRenderer fieldRenderer, Field field, AccountingDocument document, AccountingLine accountingLine, String accountingLineProperty, List<String> fieldNames) { fieldRenderer.setField(field); getField().setPropertyPrefix(accountingLineProperty); populateFieldForLookupAndInquiry(document, accountingLine, fieldNames, getField().getPropertyPrefix()); if (definition.getDynamicNameLabelGenerator() != null) { fieldRenderer.overrideOnBlur(definition.getDynamicNameLabelGenerator().getDynamicNameLabelOnBlur(accountingLine, accountingLineProperty)); } else if (!StringUtils.isBlank(definition.getDynamicLabelProperty())) { fieldRenderer.setDynamicNameLabel(accountingLineProperty + "." + definition.getDynamicLabelProperty()); } fieldRenderer.setArbitrarilyHighTabIndex(arbitrarilyHighIndex); } /** * Determines if a dynamic field label should be rendered for the given field * * @return true if a dynamic field label should be rendered, false otherwise */ protected boolean shouldRenderDynamicFeldLabel() { return (!getField().getFieldType().equals(Field.HIDDEN) && ((!StringUtils.isBlank(getField().getWebOnBlurHandler()) && !StringUtils.isBlank(definition.getDynamicLabelProperty())) || definition.getDynamicNameLabelGenerator() != null)); } /** * @see org.kuali.kfs.sys.document.web.TableJoining#performFieldTransformation(org.kuali.kfs.sys.document.service.AccountingLineFieldRenderingTransformation, * org.kuali.kfs.sys.businessobject.AccountingLine, java.util.Map, java.util.Map) */ @Override public void performFieldTransformations(List<AccountingLineFieldRenderingTransformation> fieldTransformations, AccountingLine accountingLine, Map unconvertedValues) { for (AccountingLineFieldRenderingTransformation fieldTransformation : fieldTransformations) { fieldTransformation.transformField(accountingLine, getField(), getDefinition(), unconvertedValues); if (getOverrideFields() != null && getOverrideFields().size() > 0) { transformOverrideFields(fieldTransformation, accountingLine, unconvertedValues); } } } /** * Runs a field transformation against all the overrides encapsulated within this field * * @param fieldTransformation the field transformation which will utterly change our fields * @param accountingLine the accounting line being rendered * @param editModes the current document edit modes * @param unconvertedValues a Map of unconvertedValues */ protected void transformOverrideFields(AccountingLineFieldRenderingTransformation fieldTransformation, AccountingLine accountingLine, Map unconvertedValues) { for (AccountingLineViewOverrideField overrideField : getOverrideFields()) { overrideField.transformField(fieldTransformation, accountingLine, unconvertedValues); } } /** * Renders the override fields for the line * * @param pageContext the page context to render to * @param parentTag the tag requesting all this rendering * @param accountingLine the accounting line we're rendering * @param accountingLinePropertyPath the path to get to that accounting * @throws JspException thrown if rendering fails */ public void renderOverrideFields(PageContext pageContext, Tag parentTag, AccountingLineRenderingContext renderingContext) throws JspException { for (AccountingLineViewOverrideField overrideField : getOverrideFields()) { overrideField.setAccountingLineProperty(renderingContext.getAccountingLinePropertyPath()); overrideField.renderElement(pageContext, parentTag, renderingContext); } } /** * Renders a dynamic field label * * @param pageContext the page context to render to * @param parentTag the parent tag requesting this rendering * @param accountingLine the line which owns the field being rendered * @param accountingLinePropertyPath the path from the form to the accounting line */ protected void renderDynamicNameLabel(PageContext pageContext, Tag parentTag, AccountingLineRenderingContext renderingContext) throws JspException { AccountingLine accountingLine = renderingContext.getAccountingLine(); String accountingLinePropertyPath = renderingContext.getAccountingLinePropertyPath(); DynamicNameLabelRenderer renderer = new DynamicNameLabelRenderer(); if (definition.getDynamicNameLabelGenerator() != null) { renderer.setFieldName(definition.getDynamicNameLabelGenerator().getDynamicNameLabelFieldName(accountingLine, accountingLinePropertyPath)); renderer.setFieldValue(definition.getDynamicNameLabelGenerator().getDynamicNameLabelValue(accountingLine, accountingLinePropertyPath)); } else { if (!StringUtils.isBlank(getField().getPropertyValue())) { if (getField().isSecure()) { renderer.setFieldValue(getField().getDisplayMask().maskValue(getField().getPropertyValue())); } else { renderer.setFieldValue(getDynamicNameLabelDisplayedValue(accountingLine)); } } renderer.setFieldName(accountingLinePropertyPath + "." + definition.getDynamicLabelProperty()); } renderer.render(pageContext, parentTag); renderer.clear(); } /** * Gets the value from the accounting line to display as the field value * * @param accountingLine the accounting line to get the value from * @return the value to display for the dynamic name label */ protected String getDynamicNameLabelDisplayedValue(AccountingLine accountingLine) { String dynamicLabelProperty = definition.getDynamicLabelProperty(); Object value = accountingLine; while (!ObjectUtils.isNull(value) && dynamicLabelProperty.indexOf('.') > -1) { String currentProperty = StringUtils.substringBefore(dynamicLabelProperty, "."); dynamicLabelProperty = StringUtils.substringAfter(dynamicLabelProperty, "."); if (value instanceof PersistableBusinessObject) { if (!nonRefreshedPropertyList.contains(currentProperty)){ ((PersistableBusinessObject) value).refreshReferenceObject(currentProperty); } } value = ObjectUtils.getPropertyValue(value, currentProperty); } if (!ObjectUtils.isNull(value)) { value = ObjectUtils.getPropertyValue(value, dynamicLabelProperty); if (value != null) { return value.toString(); } } return null; } /** * @see org.kuali.kfs.sys.document.web.TableJoiningWithHeader#createHeaderLabel() */ @Override public HeaderLabel createHeaderLabel() { return new FieldHeaderLabel(this); } /** * If the field definition had an override col span greater than 1 and it doesn't seem as if the given cell had its colspan * lengthened already, this method will increase the colspan of the table cell to whatever is listed * * @param cell the cell to possibly lengthen */ protected void updateTableCellWithColSpanOverride(AccountingLineTableCell cell) { if (definition.getOverrideColSpan() > 1 && cell.getColSpan() == 1) { cell.setColSpan(definition.getOverrideColSpan()); } } /** * Overridden to allow for colspan override * * @see org.kuali.kfs.sys.document.web.FieldTableJoiningWithHeader#createHeaderLabelTableCell() */ @Override protected AccountingLineTableCell createHeaderLabelTableCell() { AccountingLineTableCell cell = super.createHeaderLabelTableCell(); updateTableCellWithColSpanOverride(cell); return cell; } /** * Overridden to allow for colspan override * * @see org.kuali.kfs.sys.document.web.FieldTableJoining#createTableCell() */ @Override protected AccountingLineTableCell createTableCell() { AccountingLineTableCell cell = super.createTableCell(); updateTableCellWithColSpanOverride(cell); return cell; } /** * @return the colspan override of this field */ public int getColSpanOverride() { return definition.getOverrideColSpan(); } /** * @see org.kuali.kfs.sys.document.web.HeaderLabelPopulating#populateHeaderLabel(org.kuali.kfs.sys.document.web.HeaderLabel, * org.kuali.kfs.sys.document.web.AccountingLineRenderingContext) */ @Override public void populateHeaderLabel(HeaderLabel headerLabel, AccountingLineRenderingContext renderingContext) { FieldHeaderLabel label = (FieldHeaderLabel) headerLabel; label.setLabel(getField().getFieldLabel()); label.setLabeledFieldEmptyOrHidden(isEmpty() || isHidden()); label.setReadOnly(getField().isReadOnly()); label.setRequired(getField().isFieldRequired()); if (renderingContext.fieldsShouldRenderHelp()) { label.setFullClassNameForHelp(renderingContext.getAccountingLine().getClass().getName()); label.setAttributeEntryForHelp(getField().getPropertyName()); } } /** * Adds the wrapped field to the list; adds any override fields this field encapsulates as well * * @see org.kuali.kfs.sys.document.web.RenderableElement#appendFieldNames(java.util.List) * * KRAD Conversion: Customization of adding the fields - No use of data dictionary */ @Override public void appendFields(List<Field> fields) { fields.add(getField()); if (getOverrideFields() != null && getOverrideFields().size() > 0) { for (AccountingLineViewOverrideField field : getOverrideFields()) { field.appendFields(fields); } } } /** * @see org.kuali.kfs.sys.document.web.RenderableElement#populateWithTabIndexIfRequested(int[], int) */ @Override public void populateWithTabIndexIfRequested(int reallyHighIndex) { this.arbitrarilyHighIndex = reallyHighIndex; } /** * Determines if this field is among the fields that are in error * * @param errors the errors on the form * @return true if this field is in error, false otherwise * * KRAD Conversion: Checks if fields have errors - No use of data dictionary */ protected boolean fieldInError(List errors) { boolean fieldInError = false; if (errors != null) { String fieldName = getField().getPropertyName(); if (!StringUtils.isBlank(getField().getPropertyPrefix())) { fieldName = getField().getPropertyPrefix() + "." + fieldName; } fieldInError = matchesError(errors, fieldName); // if there was no match, it may be because the field is part of a collection, and there // is a mismatch between the fieldName and the errorKeys in the errors List. Need to check // again accommodating that by changing the fieldName to be plural if (fieldName.matches(".*[^s]\\[\\d+\\].*") && !fieldInError) { fieldInError = matchesError(errors, fieldName.replaceAll("\\[", "s\\[")); } } return fieldInError; } /** * Check the errorKeys for a match to the fieldName * * @param errors the errors on the form * @param fieldName true if the fieldName matches an errorKey, false otherwise * @return */ private boolean matchesError(List errors, String fieldName) { boolean matchesError = false; for (Object errorKeyAsObject : errors) { final String errorKey = (String) errorKeyAsObject; if (fieldName.equals(errorKey)) { matchesError = true; break; } } return matchesError; } /** * @see org.kuali.kfs.sys.document.web.ReadOnlyable#setEditable() */ @Override public void setEditable() { if (!isHidden()) { this.field.setReadOnly(false); } } /** * Determines whether to render the inquiry for this field * * @param document the document which the accounting line is part of or hopefully sometime will be part of * @param line the accounting line being rendered * @return true if inquiry links should be rendered, false otherwise */ protected boolean isRenderingInquiry(AccountingDocument document, AccountingLine line) { return isReadOnly(); } /** * build the lookup parameter map through applying the override parameters onto the defaults * * @param lookupParameters the default lookup parameter string * @param overrideLookupParameters the override lookup parameter string * @param accountingLinePrefix the actual accounting line prefix * @return the actual lookup parameter map */ private Map<String, String> getActualParametersMap(String parameters, String overrideParameters, String accountingLinePrefix) { BidiMap parametersMap = this.buildBidirecionalMapFromParameters(parameters, accountingLinePrefix); BidiMap overrideParametersMap = this.buildBidirecionalMapFromParameters(overrideParameters, accountingLinePrefix); parametersMap.putAll(overrideParametersMap); return parametersMap; } /** * parse the given lookup parameter string into a bidirectinal map * * @param lookupParameters the lookup parameter string * @param accountingLinePrefix the actual accounting line prefix * @return a bidirectinal map that holds all the given lookup parameters */ private BidiMap buildBidirecionalMapFromParameters(String parameters, String accountingLinePrefix) { BidiMap parameterMap = new DualHashBidiMap(); // if we didnt get any incoming parameters, then just return an empty parameterMap if (StringUtils.isBlank(parameters)) { return parameterMap; } String[] parameterArray = StringUtils.split(parameters, KFSConstants.FIELD_CONVERSIONS_SEPERATOR); for (String parameter : parameterArray) { String[] entrySet = StringUtils.split(parameter, KFSConstants.FIELD_CONVERSION_PAIR_SEPERATOR); if (entrySet != null) { String parameterKey = escapeAccountingLineName(entrySet[0], accountingLinePrefix); String parameterValue = escapeAccountingLineName(entrySet[1], accountingLinePrefix); parameterMap.put(parameterKey, parameterValue); } } return parameterMap; } /** * Escapes the String ${accountingLineName} within a field and replaces it with the actual prefix of an accounting line * * @param propertyName the name of the property to escape the special string ${accountingLineName} out of * @param accountingLinePrefix the actual accounting line prefix * @return the property name with the correct accounting line prefix */ protected String escapeAccountingLineName(String propertyName, String accountingLinePrefix) { return StringUtils.replace(propertyName, ACCOUNTING_LINE_NAME_PREFIX_PLACE_HOLDER, accountingLinePrefix + "."); } }