/* * #%L * BroadleafCommerce Open Admin Platform * %% * Copyright (C) 2009 - 2013 Broadleaf Commerce * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ package org.broadleafcommerce.openadmin.web.service; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.Predicate; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.broadleafcommerce.common.admin.domain.AdminMainEntity; import org.broadleafcommerce.common.exception.ExceptionHelper; import org.broadleafcommerce.common.exception.SecurityServiceException; import org.broadleafcommerce.common.exception.ServiceException; import org.broadleafcommerce.common.extension.ExtensionResultStatusType; import org.broadleafcommerce.common.media.domain.MediaDto; import org.broadleafcommerce.common.persistence.EntityConfiguration; import org.broadleafcommerce.common.presentation.client.AddMethodType; import org.broadleafcommerce.common.presentation.client.LookupType; import org.broadleafcommerce.common.presentation.client.PersistencePerspectiveItemType; import org.broadleafcommerce.common.presentation.client.SupportedFieldType; import org.broadleafcommerce.common.presentation.client.VisibilityEnum; import org.broadleafcommerce.openadmin.dto.AdornedTargetCollectionMetadata; import org.broadleafcommerce.openadmin.dto.AdornedTargetList; import org.broadleafcommerce.openadmin.dto.BasicCollectionMetadata; import org.broadleafcommerce.openadmin.dto.BasicFieldMetadata; import org.broadleafcommerce.openadmin.dto.ClassMetadata; import org.broadleafcommerce.openadmin.dto.CollectionMetadata; import org.broadleafcommerce.openadmin.dto.DynamicResultSet; import org.broadleafcommerce.openadmin.dto.Entity; import org.broadleafcommerce.openadmin.dto.FieldMetadata; import org.broadleafcommerce.openadmin.dto.ForeignKey; import org.broadleafcommerce.openadmin.dto.MapMetadata; import org.broadleafcommerce.openadmin.dto.MapStructure; import org.broadleafcommerce.openadmin.dto.Property; import org.broadleafcommerce.openadmin.dto.SectionCrumb; import org.broadleafcommerce.openadmin.server.domain.PersistencePackageRequest; import org.broadleafcommerce.openadmin.server.security.domain.AdminSection; import org.broadleafcommerce.openadmin.server.security.remote.EntityOperationType; import org.broadleafcommerce.openadmin.server.security.remote.SecurityVerifier; import org.broadleafcommerce.openadmin.server.security.service.RowLevelSecurityService; import org.broadleafcommerce.openadmin.server.security.service.navigation.AdminNavigationService; import org.broadleafcommerce.openadmin.server.service.AdminEntityService; import org.broadleafcommerce.openadmin.server.service.persistence.module.BasicPersistenceModule; import org.broadleafcommerce.openadmin.server.service.persistence.module.DataFormatProvider; import org.broadleafcommerce.openadmin.server.service.persistence.module.FieldManager; import org.broadleafcommerce.openadmin.web.form.component.DefaultListGridActions; import org.broadleafcommerce.openadmin.web.form.component.ListGrid; import org.broadleafcommerce.openadmin.web.form.component.ListGridRecord; import org.broadleafcommerce.openadmin.web.form.component.MediaField; import org.broadleafcommerce.openadmin.web.form.component.RuleBuilderField; import org.broadleafcommerce.openadmin.web.form.entity.CodeField; import org.broadleafcommerce.openadmin.web.form.entity.ComboField; import org.broadleafcommerce.openadmin.web.form.entity.DefaultEntityFormActions; import org.broadleafcommerce.openadmin.web.form.entity.DynamicEntityFormInfo; import org.broadleafcommerce.openadmin.web.form.entity.EntityForm; import org.broadleafcommerce.openadmin.web.form.entity.Field; import org.broadleafcommerce.openadmin.web.rulebuilder.DataDTODeserializer; import org.broadleafcommerce.openadmin.web.rulebuilder.dto.DataDTO; import org.broadleafcommerce.openadmin.web.rulebuilder.dto.DataWrapper; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import java.io.IOException; import java.math.BigDecimal; import java.text.DateFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.annotation.Resource; import javax.persistence.ManyToOne; import javax.persistence.OneToOne; /** * @author Andre Azzolini (apazzolini) */ @Service("blFormBuilderService") public class FormBuilderServiceImpl implements FormBuilderService { private static final Log LOG = LogFactory.getLog(FormBuilderServiceImpl.class); @Resource(name = "blAdminEntityService") protected AdminEntityService adminEntityService; @Resource (name = "blAdminNavigationService") protected AdminNavigationService navigationService; @Resource(name = "blFormBuilderExtensionManager") protected FormBuilderExtensionManager extensionManager; @Resource(name="blEntityConfiguration") protected EntityConfiguration entityConfiguration; @Resource(name="blAdminSecurityRemoteService") protected SecurityVerifier adminRemoteSecurityService; @Resource(name = "blRowLevelSecurityService") protected RowLevelSecurityService rowLevelSecurityService; @Resource(name = "blMediaBuilderService") protected MediaBuilderService mediaBuilderService; @Resource(name = "blListGridErrorMessageExtensionManager") protected ListGridErrorMessageExtensionManager listGridErrorExtensionManager; @Resource protected DataFormatProvider dataFormatProvider; protected static final VisibilityEnum[] FORM_HIDDEN_VISIBILITIES = new VisibilityEnum[] { VisibilityEnum.HIDDEN_ALL, VisibilityEnum.FORM_HIDDEN }; protected static final VisibilityEnum[] GRID_HIDDEN_VISIBILITIES = new VisibilityEnum[] { VisibilityEnum.HIDDEN_ALL, VisibilityEnum.GRID_HIDDEN }; @Override public ListGrid buildMainListGrid(DynamicResultSet drs, ClassMetadata cmd, String sectionKey, List<SectionCrumb> sectionCrumbs) throws ServiceException { List<Field> headerFields = new ArrayList<Field>(); ListGrid.Type type = ListGrid.Type.MAIN; String idProperty = "id"; for (Property p : cmd.getProperties()) { if (p.getMetadata() instanceof BasicFieldMetadata) { BasicFieldMetadata fmd = (BasicFieldMetadata) p.getMetadata(); if (SupportedFieldType.ID.equals(fmd.getFieldType())) { idProperty = fmd.getName(); } if (fmd.isProminent() != null && fmd.isProminent() && !ArrayUtils.contains(getGridHiddenVisibilities(), fmd.getVisibility())) { Field hf = createHeaderField(p, fmd); headerFields.add(hf); } } } ListGrid listGrid = createListGrid(cmd.getCeilingType(), headerFields, type, drs, sectionKey, 0, idProperty, sectionCrumbs); if (CollectionUtils.isNotEmpty(listGrid.getHeaderFields())) { // Set the first column to be able to link to the main entity listGrid.getHeaderFields().iterator().next().setMainEntityLink(true); } else { String message = "There are no listgrid header fields configured for the class " + cmd.getCeilingType(); message += "Please mark some @AdminPresentation fields with 'prominent = true'"; LOG.error(message); } return listGrid; } protected Field createHeaderField(Property p, BasicFieldMetadata fmd) { Field hf; if (fmd.getFieldType().equals(SupportedFieldType.EXPLICIT_ENUMERATION) || fmd.getFieldType().equals(SupportedFieldType.BROADLEAF_ENUMERATION) || fmd.getFieldType().equals(SupportedFieldType.DATA_DRIVEN_ENUMERATION) || fmd.getFieldType().equals(SupportedFieldType.EMPTY_ENUMERATION)) { hf = new ComboField(); ((ComboField) hf).setOptions(fmd.getEnumerationValues()); } else { hf = new Field(); } hf.withName(p.getName()) .withFriendlyName(StringUtils.isNotEmpty(fmd.getFriendlyName()) ? fmd.getFriendlyName() : p.getName()) .withOrder(fmd.getGridOrder()) .withColumnWidth(fmd.getColumnWidth()) .withForeignKeyDisplayValueProperty(fmd.getForeignKeyDisplayValueProperty()) .withForeignKeyClass(fmd.getForeignKeyClass()) .withOwningEntityClass(fmd.getOwningClass() != null ? fmd.getOwningClass() : fmd.getTargetClass()); String fieldType = fmd.getFieldType() == null ? null : fmd.getFieldType().toString(); hf.setFieldType(fieldType); return hf; } @Override public ListGrid buildCollectionListGrid(String containingEntityId, DynamicResultSet drs, Property field, String sectionKey, List<SectionCrumb> sectionCrumbs) throws ServiceException { FieldMetadata fmd = field.getMetadata(); // Get the class metadata for this particular field PersistencePackageRequest ppr = PersistencePackageRequest.fromMetadata(fmd, sectionCrumbs); if (field != null) { ppr.setSectionEntityField(field.getName()); } ClassMetadata cmd = adminEntityService.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); List<Field> headerFields = new ArrayList<Field>(); ListGrid.Type type = null; boolean editable = false; boolean sortable = false; boolean readOnly = false; boolean hideIdColumn = false; boolean canFilterAndSort = true; String idProperty = "id"; for (Property property : cmd.getProperties()) { if (property.getMetadata() instanceof BasicFieldMetadata && SupportedFieldType.ID==((BasicFieldMetadata) property.getMetadata()).getFieldType() && //make sure it's a property for this entity - not an association !property.getName().contains(".")) { idProperty = property.getName(); break; } } // Get the header fields for this list grid. Note that the header fields are different depending on the // kind of field this is. if (fmd instanceof BasicFieldMetadata) { readOnly = ((BasicFieldMetadata) fmd).getReadOnly(); for (Property p : cmd.getProperties()) { if (p.getMetadata() instanceof BasicFieldMetadata) { BasicFieldMetadata md = (BasicFieldMetadata) p.getMetadata(); if (SupportedFieldType.ID.equals(md.getFieldType())) { idProperty = md.getName(); } if (md.isProminent() != null && md.isProminent() && !ArrayUtils.contains(getGridHiddenVisibilities(), md.getVisibility())) { Field hf = createHeaderField(p, md); headerFields.add(hf); } } } type = ListGrid.Type.TO_ONE; } else if (fmd instanceof BasicCollectionMetadata) { BasicCollectionMetadata bcm = (BasicCollectionMetadata) fmd; readOnly = !bcm.isMutable(); for (Property p : cmd.getProperties()) { if (p.getMetadata() instanceof BasicFieldMetadata) { BasicFieldMetadata md = (BasicFieldMetadata) p.getMetadata(); if (md.isProminent() != null && md.isProminent() && !ArrayUtils.contains(getGridHiddenVisibilities(), md.getVisibility())) { Field hf = createHeaderField(p, md); headerFields.add(hf); } } } type = ListGrid.Type.BASIC; if (bcm.getAddMethodType().equals(AddMethodType.PERSIST)) { editable = true; } sortable = StringUtils.isNotBlank(bcm.getSortProperty()); } else if (fmd instanceof AdornedTargetCollectionMetadata) { readOnly = !((AdornedTargetCollectionMetadata) fmd).isMutable(); AdornedTargetCollectionMetadata atcmd = (AdornedTargetCollectionMetadata) fmd; for (String fieldName : atcmd.getGridVisibleFields()) { Property p = cmd.getPMap().get(fieldName); BasicFieldMetadata md = (BasicFieldMetadata) p.getMetadata(); Field hf = createHeaderField(p, md); headerFields.add(hf); } type = ListGrid.Type.ADORNED; if (atcmd.getMaintainedAdornedTargetFields().length > 0) { editable = true; } AdornedTargetList adornedList = (AdornedTargetList) atcmd.getPersistencePerspective() .getPersistencePerspectiveItems().get(PersistencePerspectiveItemType.ADORNEDTARGETLIST); sortable = StringUtils.isNotBlank(adornedList.getSortField()); } else if (fmd instanceof MapMetadata) { readOnly = !((MapMetadata) fmd).isMutable(); MapMetadata mmd = (MapMetadata) fmd; Property p2 = cmd.getPMap().get("key"); BasicFieldMetadata keyMd = (BasicFieldMetadata) p2.getMetadata(); keyMd.setFriendlyName("Key"); Field hf = createHeaderField(p2, keyMd); headerFields.add(hf); if (mmd.isSimpleValue()) { Property valueProperty = cmd.getPMap().get("value"); BasicFieldMetadata valueMd = (BasicFieldMetadata) valueProperty.getMetadata(); valueMd.setFriendlyName("Value"); hf = createHeaderField(valueProperty, valueMd); headerFields.add(hf); idProperty = "key"; hideIdColumn = true; } else { for (Property p : cmd.getProperties()) { if (p.getMetadata() instanceof BasicFieldMetadata) { BasicFieldMetadata md = (BasicFieldMetadata) p.getMetadata(); String valueClassName = mmd.getValueClassName(); if (!StringUtils.isEmpty(mmd.getToOneTargetProperty())) { Class<?> clazz; try { clazz = Class.forName(mmd.getValueClassName()); } catch (ClassNotFoundException e) { throw ExceptionHelper.refineException(e); } java.lang.reflect.Field nestedField = FieldManager.getSingleField(clazz, mmd.getToOneTargetProperty()); ManyToOne manyToOne = nestedField.getAnnotation(ManyToOne.class); if (manyToOne != null && !manyToOne.targetEntity().getName().equals(void.class.getName())) { valueClassName = manyToOne.targetEntity().getName(); } else { OneToOne oneToOne = nestedField.getAnnotation(OneToOne.class); if (oneToOne != null && !oneToOne.targetEntity().getName().equals(void.class.getName())) { valueClassName = oneToOne.targetEntity().getName(); } } } if (md.getTargetClass().equals(valueClassName)) { if (md.isProminent() != null && md.isProminent() && !ArrayUtils.contains(getGridHiddenVisibilities(), md.getVisibility())) { hf = createHeaderField(p, md); headerFields.add(hf); } } } } } type = ListGrid.Type.MAP; editable = true; canFilterAndSort = false; } String ceilingType = ""; if (fmd instanceof BasicFieldMetadata) { ceilingType = cmd.getCeilingType(); } else if (fmd instanceof CollectionMetadata) { ceilingType = ((CollectionMetadata) fmd).getCollectionCeilingEntity(); } if (CollectionUtils.isEmpty(headerFields)) { String message = "There are no listgrid header fields configured for the class " + ceilingType + " and property '" + field.getName() + "'."; if (type == ListGrid.Type.ADORNED || type == ListGrid.Type.ADORNED_WITH_FORM) { message += " Please configure 'gridVisibleFields' in your @AdminPresentationAdornedTargetCollection configuration"; } else { message += " Please mark some @AdminPresentation fields with 'prominent = true'"; } LOG.error(message); } ListGrid listGrid = createListGrid(ceilingType, headerFields, type, drs, sectionKey, fmd.getOrder(), idProperty, sectionCrumbs); listGrid.setSubCollectionFieldName(field.getName()); listGrid.setFriendlyName(field.getMetadata().getFriendlyName()); if (StringUtils.isEmpty(listGrid.getFriendlyName())) { listGrid.setFriendlyName(field.getName()); } listGrid.setContainingEntityId(containingEntityId); listGrid.setReadOnly(readOnly); listGrid.setHideIdColumn(hideIdColumn); listGrid.setCanFilterAndSort(canFilterAndSort); if (editable) { listGrid.getRowActions().add(DefaultListGridActions.UPDATE); } if (readOnly) { listGrid.getRowActions().add(DefaultListGridActions.VIEW); } if (sortable) { listGrid.setCanFilterAndSort(false); listGrid.getToolbarActions().add(DefaultListGridActions.REORDER); } listGrid.getRowActions().add(DefaultListGridActions.REMOVE); return listGrid; } protected ListGrid createListGrid(String className, List<Field> headerFields, ListGrid.Type type, DynamicResultSet drs, String sectionKey, int order, String idProperty, List<SectionCrumb> sectionCrumbs) { // Create the list grid and set some basic attributes ListGrid listGrid = new ListGrid(); listGrid.setClassName(className); listGrid.getHeaderFields().addAll(headerFields); listGrid.setListGridType(type); listGrid.setSectionCrumbs(sectionCrumbs); listGrid.setSectionKey(sectionKey); listGrid.setOrder(order); listGrid.setIdProperty(idProperty); listGrid.setStartIndex(drs.getStartIndex()); listGrid.setTotalRecords(drs.getTotalRecords()); listGrid.setPageSize(drs.getPageSize()); String sectionIdentifier = extractSectionIdentifierFromCrumb(sectionCrumbs); AdminSection section = navigationService.findAdminSectionByClassAndSectionId(className, sectionIdentifier); if (section != null) { listGrid.setExternalEntitySectionKey(section.getUrl()); } // For each of the entities (rows) in the list grid, we need to build the associated // ListGridRecord and set the required fields on the record. These fields are the same ones // that are used for the header fields. for (Entity e : drs.getRecords()) { ListGridRecord record = new ListGridRecord(); record.setListGrid(listGrid); record.setDirty(e.isDirty()); if (e.findProperty("hasError") != null) { Boolean hasError = Boolean.parseBoolean(e.findProperty("hasError").getValue()); record.setIsError(hasError); ExtensionResultStatusType messageResultStatus = listGridErrorExtensionManager .getProxy().determineErrorMessageForEntity(e, record); if (ExtensionResultStatusType.NOT_HANDLED.equals(messageResultStatus)) { record.setErrorKey("listgrid.record.error"); } } if (e.findProperty(idProperty) != null) { record.setId(e.findProperty(idProperty).getValue()); } for (Field headerField : headerFields) { Property p = e.findProperty(headerField.getName()); if (p != null) { Field recordField = new Field().withName(headerField.getName()) .withFriendlyName(headerField.getFriendlyName()) .withOrder(p.getMetadata().getOrder()); if (headerField instanceof ComboField) { recordField.setValue(((ComboField) headerField).getOption(p.getValue())); recordField.setDisplayValue(p.getDisplayValue()); } else { recordField.setValue(p.getValue()); recordField.setDisplayValue(p.getDisplayValue()); } recordField.setDerived(isDerivedField(headerField, recordField, p)); record.getFields().add(recordField); } } if (e.findProperty(AdminMainEntity.MAIN_ENTITY_NAME_PROPERTY) != null) { Field hiddenField = new Field().withName(AdminMainEntity.MAIN_ENTITY_NAME_PROPERTY); hiddenField.setValue(e.findProperty(AdminMainEntity.MAIN_ENTITY_NAME_PROPERTY).getValue()); record.getHiddenFields().add(hiddenField); } if (e.findProperty(BasicPersistenceModule.ALTERNATE_ID_PROPERTY) != null) { record.setAltId(e.findProperty(BasicPersistenceModule.ALTERNATE_ID_PROPERTY).getValue()); } extensionManager.getProxy().modifyListGridRecord(className, record, e); listGrid.getRecords().add(record); } return listGrid; } /** * Determines whether or not a particular field in a record is derived. By default this checks the {@link BasicFieldMetadata} * for the given Property to see if something on the backend has marked it as derived * * @param headerField the header for this recordField * @param recordField the recordField being populated * @param p the property that relates to this recordField * @return whether or not this field is derived * @see {@link #createListGrid(String, List, org.broadleafcommerce.openadmin.web.form.component.ListGrid.Type, DynamicResultSet, String, int, String)} */ protected Boolean isDerivedField(Field headerField, Field recordField, Property p) { return BooleanUtils.isTrue(((BasicFieldMetadata) p.getMetadata()).getIsDerived()); } protected void setEntityFormFields(EntityForm ef, List<Property> properties) { for (Property property : properties) { if (property.getMetadata() instanceof BasicFieldMetadata) { BasicFieldMetadata fmd = (BasicFieldMetadata) property.getMetadata(); if (!ArrayUtils.contains(getFormHiddenVisibilities(), fmd.getVisibility())) { // Depending on visibility, field for the particular property is not created on the form String fieldType = fmd.getFieldType() == null ? null : fmd.getFieldType().toString(); // Create the field and set some basic attributes Field f; if (fieldType.equals(SupportedFieldType.BROADLEAF_ENUMERATION.toString()) || fieldType.equals(SupportedFieldType.EXPLICIT_ENUMERATION.toString()) || fieldType.equals(SupportedFieldType.DATA_DRIVEN_ENUMERATION.toString()) || fieldType.equals(SupportedFieldType.EMPTY_ENUMERATION.toString())) { // We're dealing with fields that should render as drop-downs, so set their possible values f = new ComboField(); ((ComboField) f).setOptions(fmd.getEnumerationValues()); } else if (fieldType.equals(SupportedFieldType.CODE.toString())) { f = new CodeField(); } else if (fieldType.equals(SupportedFieldType.RULE_SIMPLE.toString()) || fieldType.equals(SupportedFieldType.RULE_WITH_QUANTITY.toString())) { // We're dealing with rule builders, so we'll create those specialized fields f = new RuleBuilderField(); ((RuleBuilderField) f).setJsonFieldName(property.getName() + "Json"); ((RuleBuilderField) f).setDataWrapper(new DataWrapper()); ((RuleBuilderField) f).setFieldBuilder(fmd.getRuleIdentifier()); String blankJsonString = "{\"data\":[]}"; ((RuleBuilderField) f).setJson(blankJsonString); DataWrapper dw = convertJsonToDataWrapper(blankJsonString); if (dw != null) { ((RuleBuilderField) f).setDataWrapper(dw); } if (fieldType.equals(SupportedFieldType.RULE_SIMPLE.toString())) { ((RuleBuilderField) f).setStyleClass("rule-builder-simple"); } else if (fieldType.equals(SupportedFieldType.RULE_WITH_QUANTITY.toString())) { ((RuleBuilderField) f).setStyleClass("rule-builder-complex"); } } else if (LookupType.DROPDOWN.equals(fmd.getLookupType())) { // We're dealing with a to-one field that wants to be rendered as a dropdown instead of in a // modal, so we'll provision the combo field here. Available options will be set as part of a // subsequent operation f = new ComboField(); } else if (fieldType.equals(SupportedFieldType.MEDIA.toString())) { f = new MediaField(); } else { // Create a default field since there was no specialized handler f = new Field(); } Boolean required = fmd.getRequiredOverride(); if (required == null) { required = fmd.getRequired(); } f.withName(property.getName()) .withFieldType(fieldType) .withOrder(fmd.getOrder()) .withFriendlyName(fmd.getFriendlyName()) .withForeignKeyDisplayValueProperty(fmd.getForeignKeyDisplayValueProperty()) .withForeignKeyClass(fmd.getForeignKeyClass()) .withOwningEntityClass(fmd.getOwningClass()!=null?fmd.getOwningClass():fmd.getInheritedFromType()) .withRequired(required) .withReadOnly(fmd.getReadOnly()) .withTranslatable(fmd.getTranslatable()) .withAlternateOrdering((Boolean) fmd.getAdditionalMetadata().get(Field.ALTERNATE_ORDERING)) .withLargeEntry(fmd.isLargeEntry()) .withHint(fmd.getHint()) .withTooltip(fmd.getTooltip()) .withHelp(fmd.getHelpText()) .withTypeaheadEnabled(fmd.getEnableTypeaheadLookup()); String defaultValue = fmd.getDefaultValue(); if (StringUtils.isNotEmpty(defaultValue)) { defaultValue = extractDefaultValueFromFieldData(fieldType, fmd); f.withValue(defaultValue); } if (StringUtils.isBlank(f.getFriendlyName())) { f.setFriendlyName(f.getName()); } // Add the field to the appropriate FieldGroup ef.addField(f, fmd.getGroup(), fmd.getGroupOrder(), fmd.getTab(), fmd.getTabOrder()); } } } } @Override public String extractDefaultValueFromFieldData(String fieldType, BasicFieldMetadata fmd) { String defaultValue = fmd.getDefaultValue(); if (fieldType.equals(SupportedFieldType.RULE_SIMPLE.toString()) || fieldType.equals(SupportedFieldType.RULE_WITH_QUANTITY.toString())) { return null; } else if (fieldType.equals(SupportedFieldType.INTEGER.toString())) { try { Integer.parseInt(defaultValue); } catch (NumberFormatException e) { String msg = buildMsgForDefValException(SupportedFieldType.INTEGER.toString(), fmd, defaultValue); LOG.warn(msg); return null; } } else if (fieldType.equals(SupportedFieldType.DECIMAL.toString())) { try { BigDecimal val = new BigDecimal(defaultValue); } catch (NumberFormatException e) { String msg = buildMsgForDefValException(SupportedFieldType.DECIMAL.toString(), fmd, defaultValue); LOG.warn(msg); return null; } } else if (fieldType.equals(SupportedFieldType.BOOLEAN.toString())) { if (!defaultValue.toLowerCase().equals("true") && !defaultValue.toLowerCase().equals("false")) { String msg = buildMsgForDefValException(SupportedFieldType.BOOLEAN.toString(), fmd, defaultValue); LOG.warn(msg); return null; } } else if (fieldType.equals(SupportedFieldType.DATE.toString())) { DateFormat format = dataFormatProvider.getSimpleDateFormatter(); if (defaultValue.toLowerCase().contains("today")) { defaultValue = format.format(new Date()); } else { try { Date date = format.parse(defaultValue); defaultValue = format.format(date); } catch (ParseException e) { String msg = buildMsgForDefValException(SupportedFieldType.DATE.toString(), fmd, defaultValue); LOG.warn(msg); return null; } } } return defaultValue; } private String buildMsgForDefValException(String type, BasicFieldMetadata fmd, String defaultValue) { return fmd.getTargetClass() + " : " + fmd.getName() + " - Failed to parse " + type + " from DefaultValue [ " + defaultValue + " ]"; } @Override public void removeNonApplicableFields(ClassMetadata cmd, EntityForm entityForm, String entityType) { for (Property p : cmd.getProperties()) { if (!ArrayUtils.contains(p.getMetadata().getAvailableToTypes(), entityType)) { entityForm.removeField(p.getName()); } } } @Override public EntityForm createEntityForm(ClassMetadata cmd, List<SectionCrumb> sectionCrumbs) throws ServiceException { EntityForm ef = createStandardEntityForm(); populateEntityForm(cmd, ef, sectionCrumbs); return ef; } protected String extractSectionIdentifierFromCrumb(List<SectionCrumb> sectionCrumbs) { if (sectionCrumbs != null && sectionCrumbs.size() > 0) { return sectionCrumbs.get(0).getSectionIdentifier(); } else { return null; } } @Override public void populateEntityForm(ClassMetadata cmd, EntityForm ef, List<SectionCrumb> sectionCrumbs) throws ServiceException { ef.setCeilingEntityClassname(cmd.getCeilingType()); String sectionIdentifier = extractSectionIdentifierFromCrumb(sectionCrumbs); AdminSection section = navigationService.findAdminSectionByClassAndSectionId(cmd.getCeilingType(), sectionIdentifier); if (section != null) { ef.setSectionKey(section.getUrl()); } else { ef.setSectionKey(cmd.getCeilingType()); } ef.setSectionCrumbsImpl(sectionCrumbs); setEntityFormFields(ef, Arrays.asList(cmd.getProperties())); populateDropdownToOneFields(ef, cmd); extensionManager.getProxy().modifyUnpopulatedEntityForm(ef); } /** * This method is invoked when EntityForms are created and is meant to provide a hook to add * additional entity form actions for implementors of Broadleaf. Broadleaf modules will typically * leverage {@link FormBuilderExtensionHandler#addAdditionalFormActions(EntityForm)} method. * @param ef */ protected void addAdditionalFormActions(EntityForm ef) { } @Override public EntityForm createEntityForm(ClassMetadata cmd, Entity entity, List<SectionCrumb> sectionCrumbs) throws ServiceException { EntityForm ef = createStandardEntityForm(); populateEntityForm(cmd, entity, ef, sectionCrumbs); addAdditionalFormActions(ef); extensionManager.getProxy().addAdditionalFormActions(ef); return ef; } @Override public void populateEntityForm(ClassMetadata cmd, Entity entity, EntityForm ef, List<SectionCrumb> sectionCrumbs) throws ServiceException { // Get the empty form with appropriate fields populateEntityForm(cmd, ef, sectionCrumbs); String idProperty = adminEntityService.getIdProperty(cmd); ef.setId(entity.findProperty(idProperty).getValue()); ef.setEntityType(entity.getType()[0]); populateEntityFormFieldValues(cmd, entity, ef); Property p = entity.findProperty(BasicPersistenceModule.MAIN_ENTITY_NAME_PROPERTY); if (p != null) { ef.setMainEntityName(p.getValue()); } extensionManager.getProxy().modifyPopulatedEntityForm(ef, entity); } @Override public void populateEntityFormFieldValues(ClassMetadata cmd, Entity entity, EntityForm ef) { // Set the appropriate property values for (Property p : cmd.getProperties()) { if (p.getMetadata() instanceof BasicFieldMetadata) { BasicFieldMetadata basicFM = (BasicFieldMetadata) p.getMetadata(); Property entityProp = entity.findProperty(p.getName()); boolean explicitlyShown = VisibilityEnum.FORM_EXPLICITLY_SHOWN.equals(basicFM.getVisibility()); //always show special map fields if (p.getName().equals("key") || p.getName().equals("priorKey")) { explicitlyShown = true; } if (entityProp == null && explicitlyShown) { Field field = ef.findField(p.getName()); if (field != null) { field.setValue(null); } } else if (entityProp == null && !SupportedFieldType.PASSWORD_CONFIRM.equals(basicFM.getExplicitFieldType())) { ef.removeField(p.getName()); } else { Field field = ef.findField(p.getName()); if (field != null) { if (entityProp != null) { //protect against null - can happen with password confirmation fields (i.e. admin user) field.setDirty(entityProp.getIsDirty()); } if (basicFM.getFieldType()==SupportedFieldType.RULE_SIMPLE || basicFM.getFieldType()==SupportedFieldType.RULE_WITH_QUANTITY) { RuleBuilderField rbf = (RuleBuilderField) field; if (entity.getPMap().containsKey(rbf.getJsonFieldName())) { String json = entity.getPMap().get(rbf.getJsonFieldName()).getValue(); rbf.setJson(json); DataWrapper dw = convertJsonToDataWrapper(json); if (dw != null) { rbf.setDataWrapper(dw); } } } if (basicFM.getFieldType() == SupportedFieldType.MEDIA) { field.setValue(entityProp.getValue()); field.setDisplayValue(entityProp.getDisplayValue()); MediaField mf = (MediaField) field; Class<MediaDto> type = entityConfiguration.lookupEntityClass(MediaDto.class.getName(), MediaDto.class); mf.setMedia(mediaBuilderService.convertJsonToMedia(entityProp.getUnHtmlEncodedValue(), type)); } else if (!SupportedFieldType.PASSWORD_CONFIRM.equals(basicFM.getExplicitFieldType())){ field.setValue(entityProp.getValue()); field.setDisplayValue(entityProp.getDisplayValue()); } } } } } } /** * When using Thymeleaf, we need to convert the JSON string back to * a DataWrapper object because Thymeleaf escapes JSON strings. * Thymeleaf uses it's own object de-serializer * see: https://github.com/thymeleaf/thymeleaf/issues/84 * see: http://forum.thymeleaf.org/Spring-Javascript-and-escaped-JSON-td4024739.html * @param json * @return DataWrapper * @throws IOException */ protected DataWrapper convertJsonToDataWrapper(String json) { ObjectMapper mapper = new ObjectMapper(); DataDTODeserializer dtoDeserializer = new DataDTODeserializer(); SimpleModule module = new SimpleModule("DataDTODeserializerModule", new Version(1, 0, 0, null, null, null)); module.addDeserializer(DataDTO.class, dtoDeserializer); mapper.registerModule(module); try { return mapper.readValue(json, DataWrapper.class); } catch (IOException e) { throw new RuntimeException(e); } } protected void populateDropdownToOneFields(EntityForm ef, ClassMetadata cmd) throws ServiceException { for (Property p : cmd.getProperties()) { if (p.getMetadata() instanceof BasicFieldMetadata) { BasicFieldMetadata fmd = (BasicFieldMetadata) p.getMetadata(); if (LookupType.DROPDOWN.equals(fmd.getLookupType()) && !ArrayUtils.contains(getFormHiddenVisibilities(), fmd.getVisibility())) { // Get the records PersistencePackageRequest toOnePpr = PersistencePackageRequest.standard() .withCeilingEntityClassname(fmd.getForeignKeyClass()); Entity[] rows = adminEntityService.getRecords(toOnePpr).getDynamicResultSet().getRecords(); // Determine the id field String idProp = null; ClassMetadata foreignClassMd = adminEntityService.getClassMetadata(toOnePpr).getDynamicResultSet().getClassMetaData(); for (Property foreignP : foreignClassMd.getProperties()) { if (foreignP.getMetadata() instanceof BasicFieldMetadata) { BasicFieldMetadata foreignFmd = (BasicFieldMetadata) foreignP.getMetadata(); if (SupportedFieldType.ID.equals(foreignFmd.getFieldType())) { idProp = foreignP.getName(); } } } if (idProp == null) { throw new RuntimeException("Could not determine ID property for " + fmd.getForeignKeyClass()); } // Determine the display field String displayProp = fmd.getLookupDisplayProperty(); // Build the options map Map<String, String> options = new HashMap<String, String>(); for (Entity row : rows) { Property prop = row.findProperty(displayProp); if (prop == null) { LOG.warn("Could not find displayProp [" + displayProp + "] on entity [" + ef.getCeilingEntityClassname() + "]"); } else { String displayValue = prop.getDisplayValue(); if (StringUtils.isBlank(displayValue)) { displayValue = prop.getValue(); } options.put(row.findProperty(idProp).getValue(), displayValue); } } // Set the options on the entity field ComboField cf = (ComboField) ef.findField(p.getName()); cf.setOptions(options); } } } } @Override public EntityForm createEntityForm(ClassMetadata cmd, Entity entity, Map<String, DynamicResultSet> collectionRecords, List<SectionCrumb> sectionCrumbs) throws ServiceException { EntityForm ef = createStandardEntityForm(); populateEntityForm(cmd, entity, collectionRecords, ef, sectionCrumbs); addAdditionalFormActions(ef); extensionManager.getProxy().addAdditionalFormActions(ef); return ef; } @Override public void populateEntityForm(ClassMetadata cmd, Entity entity, Map<String, DynamicResultSet> collectionRecords, EntityForm ef, List<SectionCrumb> sectionCrumbs) throws ServiceException { // Get the form with values for this entity populateEntityForm(cmd, entity, ef, sectionCrumbs); // Attach the sub-collection list grids and specialty UI support for (Property p : cmd.getProperties()) { if (p.getMetadata() instanceof BasicFieldMetadata) { continue; } if (!ArrayUtils.contains(p.getMetadata().getAvailableToTypes(), entity.getType()[0])) { continue; } if (collectionRecords != null) { DynamicResultSet subCollectionEntities = collectionRecords.get(p.getName()); String containingEntityId = entity.getPMap().get(ef.getIdProperty()).getValue(); ListGrid listGrid = buildCollectionListGrid(containingEntityId, subCollectionEntities, p, ef.getSectionKey(), sectionCrumbs); listGrid.setListGridType(ListGrid.Type.INLINE); CollectionMetadata md = ((CollectionMetadata) p.getMetadata()); ef.addListGrid(listGrid, md.getTab(), md.getTabOrder()); } } for (ListGrid lg : ef.getAllListGrids()) { // We always want the add option to be the first toolbar action for consistency if (lg.getToolbarActions().isEmpty()) { lg.addToolbarAction(DefaultListGridActions.ADD); } else { lg.getToolbarActions().add(0, DefaultListGridActions.ADD); } } if (CollectionUtils.isEmpty(ef.getActions())) { ef.addAction(DefaultEntityFormActions.SAVE); } addDeleteActionIfAllowed(ef, cmd, entity); setReadOnlyState(ef, cmd, entity); extensionManager.getProxy().modifyDetailEntityForm(ef); } /** * Adds the {@link DefaultEntityFormActions#DELETE} if the user is allowed to delete the <b>entity</b>. The user can * delete an entity for the following cases: * <ol> * <li>The user has the security to {@link EntityOperationType#DELETE} the given class name represented by * the <b>entityForm</b> (determined by {@link #getSecurityClassname(EntityForm, ClassMetadata)})</li> * <li>The user has the security necessary to delete the given <b>entity</b> according to the * {@link RowLevelSecurityService#canDelete(Entity)}</li> * </ol> * * @param entityForm the form being generated * @param cmd the metatadata used to build the <b>entityForm</b> for the <b>entity</b> * @param entity the entity being edited * @see {@link SecurityVerifier#securityCheck(String, EntityOperationType)} * @see {@link #getSecurityClassname(EntityForm, ClassMetadata)} * @see {@link RowLevelSecurityService#canDelete(Entity)} */ protected void addDeleteActionIfAllowed(EntityForm entityForm, ClassMetadata cmd, Entity entity) { boolean canDelete = true; try { String securityEntityClassname = getSecurityClassname(entityForm, cmd); adminRemoteSecurityService.securityCheck(securityEntityClassname, EntityOperationType.REMOVE); } catch (ServiceException e) { if (e instanceof SecurityServiceException) { canDelete = false; } } // If I cannot update a record then I certainly cannot delete it either if (canDelete) { canDelete = rowLevelSecurityService.canUpdate(adminRemoteSecurityService.getPersistentAdminUser(), entity); } if (canDelete) { canDelete = rowLevelSecurityService.canRemove(adminRemoteSecurityService.getPersistentAdminUser(), entity); } if (canDelete) { entityForm.addAction(DefaultEntityFormActions.DELETE); } } /** * The given <b>entityForm</b> is marked as readonly for the following cases: * <ol> * <li>All of the properties from <b>cmd</b> are readonly</b></li> * <li>The user does not have the security to {@link EntityOperationType#UPDATE} the given class name represented by * the <b>entityForm</b> (determined by {@link #getSecurityClassname(EntityForm, ClassMetadata)})</li> * <li>The user does not have the security necessary to modify the given <b>entity</b> according to the * {@link RowLevelSecurityService#canUpdate(Entity)}</li> * </ol> * * @param entityForm the form being generated * @param cmd the metatadata used to build the <b>entityForm</b> for the <b>entity</b> * @param entity the entity being edited * @see {@link SecurityVerifier#securityCheck(String, EntityOperationType)} * @see {@link #getSecurityClassname(EntityForm, ClassMetadata)} * @see {@link RowLevelSecurityService#canUpdate(Entity)} */ protected void setReadOnlyState(EntityForm entityForm, ClassMetadata cmd, Entity entity) { boolean readOnly = true; // If all of the fields are read only, we'll mark the form as such for (Property property : cmd.getProperties()) { FieldMetadata fieldMetadata = property.getMetadata(); if (fieldMetadata instanceof BasicFieldMetadata) { readOnly = ((BasicFieldMetadata) fieldMetadata).getReadOnly() != null && ((BasicFieldMetadata) fieldMetadata).getReadOnly(); if (!readOnly) { break; } } else { readOnly = ((CollectionMetadata) fieldMetadata).isMutable(); if (!readOnly) { break; } } } if (!readOnly) { // If the user does not have edit permissions, we will go ahead and make the form read only to prevent confusion try { String securityEntityClassname = getSecurityClassname(entityForm, cmd); adminRemoteSecurityService.securityCheck(securityEntityClassname, EntityOperationType.UPDATE); } catch (ServiceException e) { if (e instanceof SecurityServiceException) { readOnly = true; } } } // if the normal admin security service has not deemed this readonly and the all of the properties on the entity // are not readonly, then check the row-level security if (!readOnly) { readOnly = !rowLevelSecurityService.canUpdate(adminRemoteSecurityService.getPersistentAdminUser(), entity); } if (readOnly) { entityForm.setReadOnly(); } } /** * Obtains the class name suitable for passing along to the {@link SecurityVerifier} * @param form * @param cmd * @return */ protected String getSecurityClassname(EntityForm entityForm, ClassMetadata cmd) { String securityEntityClassname = entityForm.getCeilingEntityClassname(); if (!StringUtils.isEmpty(cmd.getSecurityCeilingType())) { securityEntityClassname = cmd.getSecurityCeilingType(); } else { if (entityForm.getDynamicFormInfos() != null) { for (DynamicEntityFormInfo info : entityForm.getDynamicFormInfos().values()) { if (!StringUtils.isEmpty(info.getSecurityCeilingClassName())) { securityEntityClassname = info.getSecurityCeilingClassName(); break; } } } } return securityEntityClassname; } @Override public void populateEntityFormFields(EntityForm ef, Entity entity) { populateEntityFormFields(ef, entity, true, true); } @Override public void populateEntityFormFields(EntityForm ef, Entity entity, boolean populateType, boolean populateId) { if (populateId) { ef.setId(entity.findProperty(ef.getIdProperty()).getValue()); } if (populateType) { ef.setEntityType(entity.getType()[0]); } for (Entry<String, Field> entry : ef.getFields().entrySet()) { Property entityProp = entity.findProperty(entry.getKey()); if (entityProp != null) { entry.getValue().setValue(entityProp.getValue()); entry.getValue().setDisplayValue(entityProp.getDisplayValue()); } } } @Override public void populateAdornedEntityFormFields(EntityForm ef, Entity entity, AdornedTargetList adornedList) { Field field = ef.getFields().get(adornedList.getTargetObjectPath() + "." + adornedList.getTargetIdProperty()); Property entityProp = entity.findProperty(ef.getIdProperty()); field.setValue(entityProp.getValue()); if (StringUtils.isNotBlank(adornedList.getSortField())) { field = ef.getFields().get(adornedList.getSortField()); entityProp = entity.findProperty(adornedList.getSortField()); if (field != null && entityProp != null) { field.setValue(entityProp.getValue()); } } } @Override public void populateMapEntityFormFields(EntityForm ef, Entity entity) { Field field = ef.getFields().get("priorKey"); Property entityProp = entity.findProperty("key"); if (field != null && entityProp != null) { field.setValue(entityProp.getValue()); } } @Override public EntityForm buildAdornedListForm(AdornedTargetCollectionMetadata adornedMd, AdornedTargetList adornedList, String parentId, boolean isViewCollectionItem) throws ServiceException { EntityForm ef = createStandardEntityForm(); return buildAdornedListForm(adornedMd, adornedList, parentId, isViewCollectionItem, ef); } @Override public EntityForm buildAdornedListForm(AdornedTargetCollectionMetadata adornedMd, AdornedTargetList adornedList, String parentId, boolean isViewCollectionItem, EntityForm ef) throws ServiceException { ef.setEntityType(adornedList.getAdornedTargetEntityClassname()); // Get the metadata for this adorned field PersistencePackageRequest request = PersistencePackageRequest.adorned() .withCeilingEntityClassname(adornedMd.getCollectionCeilingEntity()) .withAdornedList(adornedList); ClassMetadata collectionMetadata = adminEntityService.getClassMetadata(request).getDynamicResultSet().getClassMetaData(); List<Property> entityFormProperties = new ArrayList<Property>(); if (isViewCollectionItem) { Collections.addAll(entityFormProperties, collectionMetadata.getProperties()); } else { // We want our entity form to only render the maintained adorned target fields for (String targetFieldName : adornedMd.getMaintainedAdornedTargetFields()) { Property p = collectionMetadata.getPMap().get(targetFieldName); if (p.getMetadata() instanceof BasicFieldMetadata) { ((BasicFieldMetadata) p.getMetadata()).setVisibility(VisibilityEnum.VISIBLE_ALL); entityFormProperties.add(p); } } } // Set the maintained fields on the form setEntityFormFields(ef, entityFormProperties); // Add these two additional hidden fields that are required for persistence Field f = new Field() .withName(adornedList.getLinkedObjectPath() + "." + adornedList.getLinkedIdProperty()) .withFieldType(SupportedFieldType.HIDDEN.toString()) .withValue(parentId); ef.addHiddenField(f); f = new Field() .withName(adornedList.getTargetObjectPath() + "." + adornedList.getTargetIdProperty()) .withFieldType(SupportedFieldType.HIDDEN.toString()) .withIdOverride("adornedTargetIdProperty"); ef.addHiddenField(f); if (StringUtils.isNotBlank(adornedList.getSortField())) { f = new Field() .withName(adornedList.getSortField()) .withFieldType(SupportedFieldType.HIDDEN.toString()); ef.addHiddenField(f); } ef.setParentId(parentId); return ef; } @Override public EntityForm buildMapForm(MapMetadata mapMd, final MapStructure mapStructure, ClassMetadata cmd, String parentId) throws ServiceException { EntityForm ef = createStandardEntityForm(); return buildMapForm(mapMd, mapStructure, cmd, parentId, ef); } @Override public EntityForm buildMapForm(MapMetadata mapMd, final MapStructure mapStructure, ClassMetadata cmd, String parentId, EntityForm ef) throws ServiceException { ForeignKey foreignKey = (ForeignKey) mapMd.getPersistencePerspective() .getPersistencePerspectiveItems().get(PersistencePerspectiveItemType.FOREIGNKEY); ef.setEntityType(foreignKey.getForeignKeyClass()); Field keyField; if (!mapMd.getForceFreeFormKeys()) { // We will use a combo field to render the key choices ComboField temp = new ComboField(); temp.withName("key") .withFieldType("combo_field") .withFriendlyName("Key"); if (mapMd.getKeys() != null) { // The keys can be explicitly set in the annotation... temp.setOptions(mapMd.getKeys()); } else { // Or they could be based on a different entity PersistencePackageRequest ppr = PersistencePackageRequest.standard() .withCeilingEntityClassname(mapMd.getMapKeyOptionEntityClass()); DynamicResultSet drs = adminEntityService.getRecords(ppr).getDynamicResultSet(); for (Entity entity : drs.getRecords()) { String keyValue = entity.getPMap().get(mapMd.getMapKeyOptionEntityValueField()).getValue(); String keyDisplayValue = entity.getPMap().get(mapMd.getMapKeyOptionEntityDisplayField()).getValue(); temp.putOption(keyValue, keyDisplayValue); } } keyField = temp; } else { keyField = new Field().withName("key") .withFieldType(SupportedFieldType.STRING.toString()) .withFriendlyName("Key"); } keyField.setRequired(true); ef.addMapKeyField(keyField); // Set the fields for this form List<Property> mapFormProperties; if (mapMd.isSimpleValue()) { ef.setIdProperty("key"); mapFormProperties = new ArrayList<Property>(); Property valueProp = cmd.getPMap().get("value"); mapFormProperties.add(valueProp); } else { mapFormProperties = new ArrayList<Property>(Arrays.asList(cmd.getProperties())); CollectionUtils.filter(mapFormProperties, new Predicate() { @Override public boolean evaluate(Object object) { Property p = (Property) object; return ArrayUtils.contains(p.getMetadata().getAvailableToTypes(), mapStructure.getValueClassName()); } }); } setEntityFormFields(ef, mapFormProperties); Field f = new Field() .withName("priorKey") .withFieldType(SupportedFieldType.HIDDEN.toString()); ef.addHiddenField(f); ef.setParentId(parentId); return ef; } protected EntityForm createStandardEntityForm() { EntityForm ef = new EntityForm(); ef.addAction(DefaultEntityFormActions.SAVE); return ef; } protected VisibilityEnum[] getGridHiddenVisibilities() { return FormBuilderServiceImpl.GRID_HIDDEN_VISIBILITIES; } protected VisibilityEnum[] getFormHiddenVisibilities() { return FormBuilderServiceImpl.FORM_HIDDEN_VISIBILITIES; } }