/* * #%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.controller.entity; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.broadleafcommerce.common.exception.SecurityServiceException; import org.broadleafcommerce.common.exception.ServiceException; import org.broadleafcommerce.common.presentation.client.AddMethodType; import org.broadleafcommerce.common.presentation.client.SupportedFieldType; import org.broadleafcommerce.common.sandbox.SandBoxHelper; import org.broadleafcommerce.common.util.BLCArrayUtils; import org.broadleafcommerce.common.util.BLCMessageUtils; import org.broadleafcommerce.common.web.BroadleafRequestContext; import org.broadleafcommerce.common.web.JsonResponse; 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.ClassTree; 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.FilterAndSortCriteria; import org.broadleafcommerce.openadmin.dto.MapMetadata; 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.service.persistence.PersistenceResponse; import org.broadleafcommerce.openadmin.server.service.persistence.module.BasicPersistenceModule; import org.broadleafcommerce.openadmin.web.controller.AdminAbstractController; import org.broadleafcommerce.openadmin.web.controller.modal.ModalHeaderType; import org.broadleafcommerce.openadmin.web.editor.NonNullBooleanEditor; import org.broadleafcommerce.openadmin.web.form.component.DefaultListGridActions; import org.broadleafcommerce.openadmin.web.form.component.ListGrid; import org.broadleafcommerce.openadmin.web.form.entity.DefaultEntityFormActions; import org.broadleafcommerce.openadmin.web.form.entity.DefaultMainActions; import org.broadleafcommerce.openadmin.web.form.entity.EntityForm; import org.broadleafcommerce.openadmin.web.form.entity.EntityFormAction; import org.broadleafcommerce.openadmin.web.form.entity.Field; import org.broadleafcommerce.openadmin.web.form.entity.Tab; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.propertyeditors.StringTrimmerEditor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.MultiValueMap; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.FlashMap; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * The default implementation of the {@link #BroadleafAdminAbstractEntityController}. This delegates every call to * super and does not provide any custom-tailored functionality. It is responsible for rendering the admin for every * entity that is not explicitly customized by its own controller. * * @author Andre Azzolini (apazzolini) */ @Controller("blAdminBasicEntityController") @RequestMapping("/{sectionKey:.+}") public class AdminBasicEntityController extends AdminAbstractController { protected static final Log LOG = LogFactory.getLog(AdminBasicEntityController.class); @Resource(name="blSandBoxHelper") protected SandBoxHelper sandBoxHelper; @Value("${admin.form.validation.errors.hideTopLevelErrors}") protected boolean hideTopLevelErrors = false; // ****************************************** // REQUEST-MAPPING BOUND CONTROLLER METHODS * // ****************************************** /** * Renders the main entity listing for the specified class, which is based on the current sectionKey with some optional * criteria. * * @param request * @param response * @param model * @param pathVars * @param requestParams a Map of property name -> list critiera values * @return the return view path * @throws Exception */ @RequestMapping(value = "", method = RequestMethod.GET) public String viewEntityList(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @RequestParam MultiValueMap<String, String> requestParams) throws Exception { String sectionKey = getSectionKey(pathVars); String sectionClassName = getClassNameForSection(sectionKey); List<SectionCrumb> crumbs = getSectionCrumbs(request, null, null); PersistencePackageRequest ppr = getSectionPersistencePackageRequest(sectionClassName, requestParams, crumbs, pathVars); ClassMetadata cmd = service.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); DynamicResultSet drs = service.getRecords(ppr).getDynamicResultSet(); ListGrid listGrid = formService.buildMainListGrid(drs, cmd, sectionKey, crumbs); if (CollectionUtils.isEmpty(listGrid.getHeaderFields())) { throw new IllegalStateException("At least 1 field must be set to prominent to display in a main grid"); } List<EntityFormAction> mainActions = new ArrayList<EntityFormAction>(); addAddActionIfAllowed(sectionClassName, cmd, mainActions); extensionManager.getProxy().addAdditionalMainActions(sectionClassName, mainActions); extensionManager.getProxy().modifyMainActions(cmd, mainActions); Field firstField = listGrid.getHeaderFields().iterator().next(); if (requestParams.containsKey(firstField.getName())) { model.addAttribute("mainSearchTerm", requestParams.get(firstField.getName()).get(0)); } // If this came from a delete save, we'll have a headerFlash request parameter to take care of if (requestParams.containsKey("headerFlash")) { model.addAttribute("headerFlash", requestParams.get("headerFlash").get(0)); } model.addAttribute("entityFriendlyName", cmd.getPolymorphicEntities().getFriendlyName()); model.addAttribute("currentUrl", request.getRequestURL().toString()); model.addAttribute("listGrid", listGrid); model.addAttribute("mainActions", mainActions); model.addAttribute("viewType", "entityList"); setModelAttributes(model, sectionKey); return "modules/defaultContainer"; } /** * Adds the "Add" button to the main entity form if the current user has permissions to create new instances * of the entity and all of the fields in the entity aren't marked as read only. * * @param sectionClassName * @param cmd * @param mainActions */ protected void addAddActionIfAllowed(String sectionClassName, ClassMetadata cmd, List<EntityFormAction> mainActions) { if (isAddActionAllowed(sectionClassName, cmd)) { mainActions.add(DefaultMainActions.ADD); } mainEntityActionsExtensionManager.getProxy().modifyMainActions(cmd, mainActions); } protected boolean isAddActionAllowed(String sectionClassName, ClassMetadata cmd) { // If the user does not have create permissions, we will not add the "Add New" button boolean canCreate = true; try { adminRemoteSecurityService.securityCheck(sectionClassName, EntityOperationType.ADD); } catch (ServiceException e) { if (e instanceof SecurityServiceException) { canCreate = false; } } if (canCreate) { checkReadOnly: { //check if all the metadata is read only for (Property property : cmd.getProperties()) { if (property.getMetadata() instanceof BasicFieldMetadata) { if (((BasicFieldMetadata) property.getMetadata()).getReadOnly() == null || !((BasicFieldMetadata) property.getMetadata()).getReadOnly()) { break checkReadOnly; } } } canCreate = false; } } return canCreate; } /** * Renders the modal form that is used to add a new parent level entity. Note that this form cannot render any * subcollections as operations on those collections require the parent level entity to first be saved and have * and id. Once the entity is initially saved, we will redirect the user to the normal manage entity screen where * they can then perform operations on sub collections. * * @param request * @param response * @param model * @param pathVars * @param entityType * @return the return view path * @throws Exception */ @RequestMapping(value = "/add", method = RequestMethod.GET) public String viewAddEntityForm(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @RequestParam(defaultValue = "") String entityType) throws Exception { String sectionKey = getSectionKey(pathVars); String sectionClassName = getClassNameForSection(sectionKey); List<SectionCrumb> sectionCrumbs = getSectionCrumbs(request, null, null); ClassMetadata cmd = service.getClassMetadata(getSectionPersistencePackageRequest(sectionClassName, sectionCrumbs, pathVars)) .getDynamicResultSet().getClassMetaData(); // If the entity type isn't specified, we need to determine if there are various polymorphic types for this entity. if (StringUtils.isBlank(entityType)) { if (cmd.getPolymorphicEntities().getChildren().length == 0) { entityType = cmd.getPolymorphicEntities().getFullyQualifiedClassname(); } else { entityType = getDefaultEntityType(); } } else { entityType = URLDecoder.decode(entityType, "UTF-8"); } // If we still don't have a type selected, that means that there were indeed multiple possible types and we // will be allowing the user to pick his desired type. if (StringUtils.isBlank(entityType)) { List<ClassTree> entityTypes = getAddEntityTypes(cmd.getPolymorphicEntities()); model.addAttribute("entityTypes", entityTypes); model.addAttribute("viewType", "modal/entityTypeSelection"); String requestUri = request.getRequestURI(); if (!request.getContextPath().equals("/") && requestUri.startsWith(request.getContextPath())) { requestUri = requestUri.substring(request.getContextPath().length() + 1, requestUri.length()); } model.addAttribute("currentUri", requestUri); } else { EntityForm entityForm = formService.createEntityForm(cmd, sectionCrumbs); // We need to make sure that the ceiling entity is set to the interface and the specific entity type // is set to the type we're going to be creating. entityForm.setCeilingEntityClassname(cmd.getCeilingType()); entityForm.setEntityType(entityType); // When we initially build the class metadata (and thus, the entity form), we had all of the possible // polymorphic fields built out. Now that we have a concrete entity type to render, we can remove the // fields that are not applicable for this given entity type. formService.removeNonApplicableFields(cmd, entityForm, entityType); modifyAddEntityForm(entityForm, pathVars); model.addAttribute("entityForm", entityForm); model.addAttribute("viewType", "modal/entityAdd"); } model.addAttribute("entityFriendlyName", cmd.getPolymorphicEntities().getFriendlyName()); model.addAttribute("currentUrl", request.getRequestURL().toString()); model.addAttribute("modalHeaderType", ModalHeaderType.ADD_ENTITY.getType()); setModelAttributes(model, sectionKey); return "modules/modalContainer"; } /** * Processes the request to add a new entity. If successful, returns a redirect to the newly created entity. * * @param request * @param response * @param model * @param pathVars * @param entityForm * @param result * @return the return view path * @throws Exception */ @RequestMapping(value = "/add", method = RequestMethod.POST) public String addEntity(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @ModelAttribute(value="entityForm") EntityForm entityForm, BindingResult result) throws Exception { String sectionKey = getSectionKey(pathVars); extractDynamicFormFields(entityForm); List<SectionCrumb> sectionCrumbs = getSectionCrumbs(request, null, null); Entity entity = service.addEntity(entityForm, getSectionCustomCriteria(), sectionCrumbs).getEntity(); entityFormValidator.validate(entityForm, entity, result); if (result.hasErrors()) { String sectionClassName = getClassNameForSection(sectionKey); ClassMetadata cmd = service.getClassMetadata(getSectionPersistencePackageRequest(sectionClassName, sectionCrumbs, pathVars)).getDynamicResultSet().getClassMetaData(); entityForm.clearFieldsMap(); formService.populateEntityForm(cmd, entity, entityForm, sectionCrumbs); formService.removeNonApplicableFields(cmd, entityForm, entityForm.getEntityType()); modifyAddEntityForm(entityForm, pathVars); model.addAttribute("viewType", "modal/entityAdd"); model.addAttribute("currentUrl", request.getRequestURL().toString()); model.addAttribute("modalHeaderType", ModalHeaderType.ADD_ENTITY.getType()); model.addAttribute("hideTopLevelErrors", hideTopLevelErrors); setModelAttributes(model, sectionKey); return "modules/modalContainer"; } // Note that AJAX Redirects need the context path prepended to them return "ajaxredirect:" + getContextPath(request) + sectionKey + "/" + entity.getPMap().get("id").getValue(); } /** * Renders the main entity form for the specified entity * * @param request * @param response * @param model * @param pathVars * @param id * @return the return view path * @throws Exception */ @RequestMapping(value = "/{id}", method = RequestMethod.GET) public String viewEntityForm(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable("id") String id) throws Exception { String sectionKey = getSectionKey(pathVars); String sectionClassName = getClassNameForSection(sectionKey); List<SectionCrumb> crumbs = getSectionCrumbs(request, sectionKey, id); PersistencePackageRequest ppr = getSectionPersistencePackageRequest(sectionClassName, crumbs, pathVars); ClassMetadata cmd = service.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); Entity entity = service.getRecord(ppr, id, cmd, false).getDynamicResultSet().getRecords()[0]; Map<String, DynamicResultSet> subRecordsMap = service.getRecordsForAllSubCollections(ppr, entity, crumbs); EntityForm entityForm = formService.createEntityForm(cmd, entity, subRecordsMap, crumbs); modifyEntityForm(entityForm, pathVars); model.addAttribute("entity", entity); model.addAttribute("entityForm", entityForm); model.addAttribute("currentUrl", request.getRequestURL().toString()); setModelAttributes(model, sectionKey); if (sandBoxHelper.isSandBoxable(entityForm.getEntityType())) { Tab changeHistoryTab = new Tab(); changeHistoryTab.setTitle("Change History"); changeHistoryTab.setOrder(Integer.MAX_VALUE); changeHistoryTab.setTabClass("change-history-tab"); entityForm.getTabs().add(changeHistoryTab); } if (isAjaxRequest(request)) { entityForm.setReadOnly(); model.addAttribute("viewType", "modal/entityView"); model.addAttribute("modalHeaderType", ModalHeaderType.VIEW_ENTITY.getType()); return "modules/modalContainer"; } else { model.addAttribute("useAjaxUpdate", true); model.addAttribute("viewType", "entityEdit"); return "modules/defaultContainer"; } } /** * Builds JSON that looks like this: * * {"errors": * [{"message":"This field is Required", * "code": "requiredValidationFailure" * "field":"defaultSku--name", * "errorType", "field", * "tab": "General" * }, * {"message":"This field is Required", * "code": "requiredValidationFailure" * "field":"defaultSku--name", * "errorType", "field", * "tab": "General" * }] * } * */ @RequestMapping(value = "/{id}", method = RequestMethod.POST, produces = "application/json") public String saveEntityJson(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value = "id") String id, @ModelAttribute(value = "entityForm") EntityForm entityForm, BindingResult result, RedirectAttributes ra) throws Exception { saveEntity(request, response, model, pathVars, id, entityForm, result, ra); JsonResponse json = new JsonResponse(response); if (result.hasErrors()) { populateJsonValidationErrors(entityForm, result, json); } List<String> dirtyList = buildDirtyList(pathVars, request, id); if (CollectionUtils.isNotEmpty(dirtyList)) { json.with("dirty", dirtyList); } return json.done(); } public List<String> buildDirtyList(Map<String, String> pathVars, HttpServletRequest request, String id) throws ServiceException { List<String> dirtyList = new ArrayList<>(); String sectionKey = getSectionKey(pathVars); String sectionClassName = getClassNameForSection(sectionKey); List<SectionCrumb> sectionCrumbs = getSectionCrumbs(request, sectionKey, id); PersistencePackageRequest ppr = getSectionPersistencePackageRequest(sectionClassName, sectionCrumbs, pathVars); ClassMetadata cmd = null; Entity entity = null; cmd = service.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); entity = service.getRecord(ppr, id, cmd, false).getDynamicResultSet().getRecords()[0]; for (Property p: entity.getProperties()) { if (p.getIsDirty()) { dirtyList.add(p.getName()); } } return dirtyList; } /** * Populates the given <b>json</b> response object based on the given <b>form</b> and <b>result</b> * @return the same <b>result</b> that was passed in */ protected JsonResponse populateJsonValidationErrors(EntityForm form, BindingResult result, JsonResponse json) { List<Map<String, Object>> errors = new ArrayList<Map<String, Object>>(); for (FieldError e : result.getFieldErrors()){ Map<String, Object> errorMap = new HashMap<String, Object>(); errorMap.put("errorType", "field"); String fieldName = e.getField().substring(e.getField().indexOf("[") + 1, e.getField().indexOf("]")).replace("_", "-"); errorMap.put("field", fieldName); errorMap.put("message", translateErrorMessage(e)); errorMap.put("code", e.getCode()); String tabFieldName = fieldName.replaceAll("-+", "."); Tab errorTab = form.findTabForField(tabFieldName); if (errorTab != null) { errorMap.put("tab", errorTab.getTitle()); } errors.add(errorMap); } for (ObjectError e : result.getGlobalErrors()) { Map<String, Object> errorMap = new HashMap<String, Object>(); errorMap.put("errorType", "global"); errorMap.put("code", e.getCode()); errorMap.put("message", translateErrorMessage(e)); errors.add(errorMap); } json.with("errors", errors); return json; } protected String translateErrorMessage(ObjectError error) { BroadleafRequestContext context = BroadleafRequestContext.getBroadleafRequestContext(); if (context != null && context.getMessageSource() != null) { return context.getMessageSource().getMessage(error.getCode(), null, error.getCode(), context.getJavaLocale()); } else { LOG.warn("Could not find the MessageSource on the current request, not translating the message key"); return error.getCode(); } } /** * Attempts to save the given entity. If validation is unsuccessful, it will re-render the entity form with * error fields highlighted. On a successful save, it will refresh the entity page. * * @param request * @param response * @param model * @param pathVars * @param id * @param entityForm * @param result * @return the return view path * @throws Exception */ @RequestMapping(value = "/{id}", method = RequestMethod.POST) public String saveEntity(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="id") String id, @ModelAttribute(value="entityForm") EntityForm entityForm, BindingResult result, RedirectAttributes ra) throws Exception { String sectionKey = getSectionKey(pathVars); String sectionClassName = getClassNameForSection(sectionKey); List<SectionCrumb> sectionCrumbs = getSectionCrumbs(request, sectionKey, id); PersistencePackageRequest ppr = getSectionPersistencePackageRequest(sectionClassName, sectionCrumbs, pathVars); extractDynamicFormFields(entityForm); Entity entity = service.updateEntity(entityForm, getSectionCustomCriteria(), sectionCrumbs).getEntity(); entityFormValidator.validate(entityForm, entity, result); if (result.hasErrors()) { model.addAttribute("headerFlash", "save.unsuccessful"); model.addAttribute("headerFlashAlert", true); Map<String, DynamicResultSet> subRecordsMap = service.getRecordsForAllSubCollections(ppr, entity, sectionCrumbs); ClassMetadata cmd = service.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); entityForm.clearFieldsMap(); formService.populateEntityForm(cmd, entity, subRecordsMap, entityForm, sectionCrumbs); modifyEntityForm(entityForm, pathVars); model.addAttribute("entity", entity); model.addAttribute("currentUrl", request.getRequestURL().toString()); model.addAttribute("hideTopLevelErrors", hideTopLevelErrors); setModelAttributes(model, sectionKey); if (isAjaxRequest(request)) { entityForm.setReadOnly(); model.addAttribute("viewType", "modal/entityView"); model.addAttribute("modalHeaderType", ModalHeaderType.VIEW_ENTITY.getType()); return "modules/modalContainer"; } else { model.addAttribute("useAjaxUpdate", true); model.addAttribute("viewType", "entityEdit"); return "modules/defaultContainer"; } } ra.addFlashAttribute("headerFlash", "save.successful"); return "redirect:/" + sectionKey + "/" + id; } /** * Attempts to remove the given entity. * * @param request * @param response * @param model * @param pathVars * @param id * @return the return view path * @throws Exception */ @RequestMapping(value = "/{id}/delete", method = RequestMethod.POST) public String removeEntity(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="id") String id, @ModelAttribute(value="entityForm") EntityForm entityForm, BindingResult result, RedirectAttributes ra) throws Exception { String sectionKey = getSectionKey(pathVars); List<SectionCrumb> sectionCrumbs = getSectionCrumbs(request, sectionKey, id); Entity entity = service.removeEntity(entityForm, getSectionCustomCriteria(), sectionCrumbs).getEntity(); // Removal does not normally return an Entity unless there is some validation error if (entity != null) { entityFormValidator.validate(entityForm, entity, result); if (result.hasErrors()) { // Create a flash attribute for the unsuccessful delete FlashMap fm = new FlashMap(); fm.put("headerFlash", "delete.unsuccessful"); fm.put("headerFlashAlert", true); request.setAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE, fm); // Re-look back up the entity so that we can return something populated String sectionClassName = getClassNameForSection(sectionKey); PersistencePackageRequest ppr = getSectionPersistencePackageRequest(sectionClassName, sectionCrumbs, pathVars); ClassMetadata cmd = service.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); entity = service.getRecord(ppr, id, cmd, false).getDynamicResultSet().getRecords()[0]; Map<String, DynamicResultSet> subRecordsMap = service.getRecordsForAllSubCollections(ppr, entity, sectionCrumbs); entityForm.clearFieldsMap(); formService.populateEntityForm(cmd, entity, subRecordsMap, entityForm, sectionCrumbs); modifyEntityForm(entityForm, pathVars); return populateJsonValidationErrors(entityForm, result, new JsonResponse(response)) .done(); } } ra.addFlashAttribute("headerFlash", "delete.successful"); ra.addFlashAttribute("headerFlashAlert", true); if (isAjaxRequest(request)) { // redirect attributes won't work here since ajaxredirect actually makes a new request return "ajaxredirect:" + getContextPath(request) + sectionKey + "?headerFlash=delete.successful"; } else { return "redirect:/" + sectionKey; } } @RequestMapping(value = "/{collectionField:.*}/details", method = RequestMethod.GET) public @ResponseBody Map<String, String> getCollectionValueDetails(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="collectionField") String collectionField, @RequestParam String ids, @RequestParam MultiValueMap<String, String> requestParams) throws Exception { String sectionKey = getSectionKey(pathVars); String sectionClassName = getClassNameForSection(sectionKey); List<SectionCrumb> sectionCrumbs = getSectionCrumbs(request, null, null); PersistencePackageRequest ppr = getSectionPersistencePackageRequest(sectionClassName, requestParams, sectionCrumbs, pathVars); ClassMetadata mainMetadata = service.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); Property collectionProperty = mainMetadata.getPMap().get(collectionField); FieldMetadata md = collectionProperty.getMetadata(); ppr = PersistencePackageRequest.fromMetadata(md, sectionCrumbs); ppr.setStartIndex(getStartIndex(requestParams)); ppr.setMaxIndex(getMaxIndex(requestParams)); if (md instanceof BasicFieldMetadata) { String idProp = ((BasicFieldMetadata) md).getForeignKeyProperty(); String displayProp = ((BasicFieldMetadata) md).getForeignKeyDisplayValueProperty(); List<String> filterValues = BLCArrayUtils.asList(ids.split(FILTER_VALUE_SEPARATOR_REGEX)); ppr.addFilterAndSortCriteria(new FilterAndSortCriteria(idProp, filterValues)); DynamicResultSet drs = service.getRecords(ppr).getDynamicResultSet(); Map<String, String> returnMap = new HashMap<String, String>(); for (Entity e : drs.getRecords()) { String id = e.getPMap().get(idProp).getValue(); String disp = e.getPMap().get(displayProp).getDisplayValue(); if (StringUtils.isBlank(disp)) { disp = e.getPMap().get(displayProp).getValue(); } returnMap.put(id, disp); } return returnMap; } return null; } /** * Shows the modal popup for the current selected "to-one" field. For instance, if you are viewing a list of products * then this method is invoked when a user clicks on the name of the default category field. * * @param request * @param response * @param model * @param pathVars * @param collectionField * @param id * @return * @throws Exception */ @RequestMapping(value = "/{collectionField:.*}/{id}/view", method = RequestMethod.GET) public String viewCollectionItemDetails(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="collectionField") String collectionField, @PathVariable(value="id") String id) throws Exception { String sectionKey = getSectionKey(pathVars); String mainClassName = getClassNameForSection(sectionKey); List<SectionCrumb> sectionCrumbs = getSectionCrumbs(request, sectionKey, id); ClassMetadata mainMetadata = service.getClassMetadata(getSectionPersistencePackageRequest(mainClassName, sectionCrumbs, pathVars)).getDynamicResultSet().getClassMetaData(); Property collectionProperty = mainMetadata.getPMap().get(collectionField); BasicFieldMetadata md = (BasicFieldMetadata) collectionProperty.getMetadata(); AdminSection section = adminNavigationService.findAdminSectionByClassAndSectionId(md.getForeignKeyClass(), sectionKey); String sectionUrlKey = (section.getUrl().startsWith("/")) ? section.getUrl().substring(1) : section.getUrl(); Map<String, String> varsForField = new HashMap<String, String>(); varsForField.put("sectionKey", sectionUrlKey); return viewEntityForm(request, response, model, varsForField, id); } /** * Returns the records for a given collectionField filtered by a particular criteria * * @param request * @param response * @param model * @param pathVars * @param collectionField * @param requestParams * @return the return view path * @throws Exception */ @RequestMapping(value = "/{id}/{collectionField:.*}", method = RequestMethod.GET) public String getCollectionFieldRecords(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="id") String id, @PathVariable(value="collectionField") String collectionField, @RequestParam MultiValueMap<String, String> requestParams) throws Exception { String sectionKey = getSectionKey(pathVars); String mainClassName = getClassNameForSection(sectionKey); List<SectionCrumb> sectionCrumbs = getSectionCrumbs(request, sectionKey, id); PersistencePackageRequest ppr = getSectionPersistencePackageRequest(mainClassName, requestParams, sectionCrumbs, pathVars); ClassMetadata mainMetadata = service.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); Property collectionProperty = mainMetadata.getPMap().get(collectionField); ppr = getSectionPersistencePackageRequest(mainClassName, sectionCrumbs, pathVars); Entity entity = service.getRecord(ppr, id, mainMetadata, false).getDynamicResultSet().getRecords()[0]; // Next, we must get the new list grid that represents this collection ListGrid listGrid = getCollectionListGrid(mainMetadata, entity, collectionProperty, requestParams, sectionKey, sectionCrumbs); model.addAttribute("listGrid", listGrid); model.addAttribute("currentParams", new ObjectMapper().writeValueAsString(requestParams)); // We return the new list grid so that it can replace the currently visible one setModelAttributes(model, sectionKey); return "views/standaloneListGrid"; } /** * Shows the modal dialog that is used to add an item to a given collection. There are several possible outcomes * of this call depending on the type of the specified collection field. * * <ul> * <li> * <b>Basic Collection (Persist)</b> - Renders a blank form for the specified target entity so that the user may * enter information and associate the record with this collection. Used by fields such as ProductAttribute. * </li> * <li> * <b>Basic Collection (Lookup)</b> - Renders a list grid that allows the user to click on an entity and select it. * Used by fields such as "allParentCategories". * </li> * <li> * <b>Adorned Collection (without form)</b> - Renders a list grid that allows the user to click on an entity and * select it. The view rendered by this is identical to basic collection (lookup), but will perform the operation * on an adorned field, which may carry extra meta-information about the created relationship, such as order. * </li> * <li> * <b>Adorned Collection (with form)</b> - Renders a list grid that allows the user to click on an entity and * select it. Once the user selects the entity, he will be presented with an empty form based on the specified * "maintainedAdornedTargetFields" for this field. Used by fields such as "crossSellProducts", which in addition * to linking an entity, provide extra fields, such as a promotional message. * </li> * <li> * <b>Map Collection</b> - Renders a form for the target entity that has an additional key field. This field is * populated either from the configured map keys, or as a result of a lookup in the case of a key based on another * entity. Used by fields such as the mediaMap on a Sku. * </li> * * @param request * @param response * @param model * @param pathVars * @param id * @param collectionField * @param requestParams * @return the return view path * @throws Exception */ @RequestMapping(value = "/{id}/{collectionField:.*}/add", method = RequestMethod.GET) public String showAddCollectionItem(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value = "id") String id, @PathVariable(value = "collectionField") String collectionField, @RequestParam MultiValueMap<String, String> requestParams) throws Exception { String sectionKey = getSectionKey(pathVars); String mainClassName = getClassNameForSection(sectionKey); List<SectionCrumb> sectionCrumbs = getSectionCrumbs(request, sectionKey, id); ClassMetadata mainMetadata = service.getClassMetadata(getSectionPersistencePackageRequest(mainClassName, sectionCrumbs, pathVars)).getDynamicResultSet().getClassMetaData(); Property collectionProperty = mainMetadata.getPMap().get(collectionField); FieldMetadata md = collectionProperty.getMetadata(); PersistencePackageRequest ppr = PersistencePackageRequest.fromMetadata(md, sectionCrumbs) .withFilterAndSortCriteria(getCriteria(requestParams)) .withStartIndex(getStartIndex(requestParams)) .withMaxIndex(getMaxIndex(requestParams)); if (md instanceof BasicCollectionMetadata) { BasicCollectionMetadata fmd = (BasicCollectionMetadata) md; if (fmd.getAddMethodType().equals(AddMethodType.PERSIST)) { ClassMetadata cmd = service.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); // If the entity type isn't specified, we need to determine if there are various polymorphic types // for this entity. String entityType = null; if (requestParams.containsKey("entityType")) { entityType = requestParams.get("entityType").get(0); } if (StringUtils.isBlank(entityType)) { if (cmd.getPolymorphicEntities().getChildren().length == 0) { entityType = cmd.getPolymorphicEntities().getFullyQualifiedClassname(); } else { entityType = getDefaultEntityType(); } } else { entityType = URLDecoder.decode(entityType, "UTF-8"); } if (StringUtils.isBlank(entityType)) { List<ClassTree> entityTypes = getAddEntityTypes(cmd.getPolymorphicEntities()); model.addAttribute("entityTypes", entityTypes); model.addAttribute("viewType", "modal/entityTypeSelection"); model.addAttribute("entityFriendlyName", cmd.getPolymorphicEntities().getFriendlyName()); String requestUri = request.getRequestURI(); if (!request.getContextPath().equals("/") && requestUri.startsWith(request.getContextPath())) { requestUri = requestUri.substring(request.getContextPath().length() + 1, requestUri.length()); } model.addAttribute("currentUri", requestUri); model.addAttribute("modalHeaderType", ModalHeaderType.ADD_ENTITY.getType()); setModelAttributes(model, sectionKey); return "modules/modalContainer"; } else { ppr = ppr.withCeilingEntityClassname(entityType); } } } //service.getContextSpecificRelationshipId(mainMetadata, entity, prefix); model.addAttribute("currentParams", new ObjectMapper().writeValueAsString(requestParams)); return buildAddCollectionItemModel(request, response, model, id, collectionField, sectionKey, collectionProperty, md, ppr, null, null); } /** * Adds the requested collection item * * @param request * @param response * @param model * @param pathVars * @param id * @param collectionField * @param entityForm * @return the return view path * @throws Exception */ @RequestMapping(value = "/{id}/{collectionField:.*}/add", method = RequestMethod.POST) public String addCollectionItem(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="id") String id, @PathVariable(value="collectionField") String collectionField, @ModelAttribute(value="entityForm") EntityForm entityForm, BindingResult result) throws Exception { String sectionKey = getSectionKey(pathVars); String mainClassName = getClassNameForSection(sectionKey); List<SectionCrumb> sectionCrumbs = getSectionCrumbs(request, sectionKey, id); ClassMetadata mainMetadata = service.getClassMetadata(getSectionPersistencePackageRequest(mainClassName, sectionCrumbs, pathVars)).getDynamicResultSet().getClassMetaData(); Property collectionProperty = mainMetadata.getPMap().get(collectionField); if (StringUtils.isBlank(entityForm.getEntityType())) { FieldMetadata fmd = collectionProperty.getMetadata(); if (fmd instanceof BasicCollectionMetadata) { entityForm.setEntityType(((BasicCollectionMetadata) fmd).getCollectionCeilingEntity()); } } PersistencePackageRequest ppr = getSectionPersistencePackageRequest(mainClassName, sectionCrumbs, pathVars); Entity entity = service.getRecord(ppr, id, mainMetadata, false).getDynamicResultSet().getRecords()[0]; // First, we must save the collection entity PersistenceResponse persistenceResponse = service.addSubCollectionEntity(entityForm, mainMetadata, collectionProperty, entity, sectionCrumbs); Entity savedEntity = persistenceResponse.getEntity(); entityFormValidator.validate(entityForm, savedEntity, result); if (result.hasErrors()) { FieldMetadata md = collectionProperty.getMetadata(); ppr = PersistencePackageRequest.fromMetadata(md, sectionCrumbs); return buildAddCollectionItemModel(request, response, model, id, collectionField, sectionKey, collectionProperty, md, ppr, entityForm, savedEntity); } // Next, we must get the new list grid that represents this collection ListGrid listGrid = getCollectionListGrid(mainMetadata, entity, collectionProperty, null, sectionKey, persistenceResponse, sectionCrumbs); model.addAttribute("listGrid", listGrid); // We return the new list grid so that it can replace the currently visible one setModelAttributes(model, sectionKey); return "views/standaloneListGrid"; } /** * Builds out all of the model information needed for showing the add modal for collection items on both the initial GET * as well as after a POST with validation errors * * @param request * @param model * @param id * @param collectionField * @param sectionKey * @param collectionProperty * @param md * @param ppr * @return the appropriate view to display for the modal * @see {@link #addCollectionItem(HttpServletRequest, HttpServletResponse, Model, Map, String, String, EntityForm, BindingResult)} * @see {@link #showAddCollectionItem(HttpServletRequest, HttpServletResponse, Model, Map, String, String, MultiValueMap)} * @throws ServiceException */ protected String buildAddCollectionItemModel(HttpServletRequest request, HttpServletResponse response, Model model, String id, String collectionField, String sectionKey, Property collectionProperty, FieldMetadata md, PersistencePackageRequest ppr, EntityForm entityForm, Entity entity) throws ServiceException { // For requests to add a new collection item include the main class that the subsequent request comes from. // For instance, with basic collections we know the main associated class for a fetch through the ForeignKey // persistence item but map and adorned target lookups make a standard persistence request. This solution // fixes all cases. String mainClassName = getClassNameForSection(sectionKey); ppr.addCustomCriteria("owningClass=" + mainClassName); if (entityForm != null) { entityForm.clearFieldsMap(); } List<SectionCrumb> sectionCrumbs = getSectionCrumbs(request, sectionKey, id); if (md instanceof BasicCollectionMetadata) { BasicCollectionMetadata fmd = (BasicCollectionMetadata) md; // When adding items to basic collections, we will sometimes show a form to persist a new record // and sometimes show a list grid to allow the user to associate an existing record. if (fmd.getAddMethodType().equals(AddMethodType.PERSIST)) { ClassMetadata collectionMetadata = service.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); if (entityForm == null) { entityForm = formService.createEntityForm(collectionMetadata,sectionCrumbs); entityForm.setCeilingEntityClassname(ppr.getCeilingEntityClassname()); entityForm.setEntityType(ppr.getCeilingEntityClassname()); } else { formService.populateEntityForm(collectionMetadata, entityForm, sectionCrumbs); formService.populateEntityFormFieldValues(collectionMetadata, entity, entityForm); } formService.removeNonApplicableFields(collectionMetadata, entityForm, ppr.getCeilingEntityClassname()); entityForm.getTabs().iterator().next().getIsVisible(); model.addAttribute("entityForm", entityForm); model.addAttribute("viewType", "modal/simpleAddEntity"); } else { DynamicResultSet drs = service.getRecords(ppr).getDynamicResultSet(); ListGrid listGrid = formService.buildCollectionListGrid(id, drs, collectionProperty, sectionKey, sectionCrumbs); listGrid.setPathOverride(request.getRequestURL().toString()); model.addAttribute("listGrid", listGrid); model.addAttribute("viewType", "modal/simpleSelectEntity"); } } else if (md instanceof AdornedTargetCollectionMetadata) { AdornedTargetCollectionMetadata fmd = (AdornedTargetCollectionMetadata) md; // Even though this field represents an adorned target collection, the list we want to show in the modal // is the standard list grid for the target entity of this field ppr.setOperationTypesOverride(null); ppr.setType(PersistencePackageRequest.Type.STANDARD); ppr.setSectionEntityField(collectionField); ClassMetadata collectionMetadata = service.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); DynamicResultSet drs = service.getRecords(ppr).getDynamicResultSet(); ListGrid listGrid = formService.buildMainListGrid(drs, collectionMetadata, sectionKey, sectionCrumbs); listGrid.setSubCollectionFieldName(collectionField); listGrid.setPathOverride(request.getRequestURL().toString()); listGrid.setFriendlyName(collectionMetadata.getPolymorphicEntities().getFriendlyName()); if (entityForm == null) { entityForm = formService.buildAdornedListForm(fmd, ppr.getAdornedList(), id, false); } else { formService.buildAdornedListForm(fmd, ppr.getAdornedList(), id, false, entityForm); formService.populateEntityFormFieldValues(collectionMetadata, entity, entityForm); } listGrid.setListGridType(ListGrid.Type.ADORNED); for (Entry<String, Field> entry : entityForm.getFields().entrySet()) { if (entry.getValue().getIsVisible()) { listGrid.setListGridType(ListGrid.Type.ADORNED_WITH_FORM); break; } } model.addAttribute("listGrid", listGrid); model.addAttribute("entityForm", entityForm); model.addAttribute("viewType", "modal/adornedSelectEntity"); } else if (md instanceof MapMetadata) { MapMetadata fmd = (MapMetadata) md; ClassMetadata collectionMetadata = service.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); if (entityForm == null) { entityForm = formService.buildMapForm(fmd, ppr.getMapStructure(), collectionMetadata, id); } else { formService.buildMapForm(fmd, ppr.getMapStructure(), collectionMetadata, id, entityForm); formService.populateEntityFormFieldValues(collectionMetadata, entity, entityForm); } model.addAttribute("entityForm", entityForm); model.addAttribute("viewType", "modal/mapAddEntity"); } model.addAttribute("currentUrl", request.getRequestURL().toString()); model.addAttribute("modalHeaderType", ModalHeaderType.ADD_COLLECTION_ITEM.getType()); model.addAttribute("collectionProperty", collectionProperty); setModelAttributes(model, sectionKey); return "modules/modalContainer"; } /** * Shows the appropriate modal dialog to edit the selected collection item * * @param request * @param response * @param model * @param pathVars * @param id * @param collectionField * @param collectionItemId * @return the return view path * @throws Exception */ @RequestMapping(value = "/{id}/{collectionField:.*}/{collectionItemId}/{alternateId}", method = RequestMethod.GET) public String showUpdateCollectionItem(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="id") String id, @PathVariable(value="collectionField") String collectionField, @PathVariable(value="collectionItemId") String collectionItemId, @PathVariable(value="alternateId") String alternateId) throws Exception { return showViewUpdateCollection(request, model, pathVars, id, collectionField, collectionItemId, alternateId, ModalHeaderType.UPDATE_COLLECTION_ITEM.getType()); } @RequestMapping(value = "/{id}/{collectionField:.*}/{collectionItemId}", method = RequestMethod.GET) public String showUpdateCollectionItem(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="id") String id, @PathVariable(value="collectionField") String collectionField, @PathVariable(value="collectionItemId") String collectionItemId) throws Exception { return showViewUpdateCollection(request, model, pathVars, id, collectionField, collectionItemId, null, ModalHeaderType.UPDATE_COLLECTION_ITEM.getType()); } /** * Shows the appropriate modal dialog to view the selected collection item. This will display the modal as readonly * * @param request * @param response * @param model * @param pathVars * @param id * @param collectionField * @param collectionItemId * @return the return view path * @throws Exception */ @RequestMapping(value = "/{id}/{collectionField:.*}/{collectionItemId}/{alternateId}/view", method = RequestMethod.GET) public String showViewCollectionItem(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="id") String id, @PathVariable(value="collectionField") String collectionField, @PathVariable(value="collectionItemId") String collectionItemId, @PathVariable(value="alternateId") String alternateId) throws Exception { String returnPath = showViewUpdateCollection(request, model, pathVars, id, collectionField, collectionItemId, alternateId, ModalHeaderType.VIEW_COLLECTION_ITEM.getType()); // Since this is a read-only view, actions don't make sense in this context EntityForm ef = (EntityForm) model.asMap().get("entityForm"); ef.removeAllActions(); ef.setReadOnly(); return returnPath; } @RequestMapping(value = "/{id}/{collectionField:.*}/{collectionItemId}/view", method = RequestMethod.GET) public String showViewCollectionItem(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="id") String id, @PathVariable(value="collectionField") String collectionField, @PathVariable(value="collectionItemId") String collectionItemId) throws Exception { String returnPath = showViewUpdateCollection(request, model, pathVars, id, collectionField, collectionItemId, null, ModalHeaderType.VIEW_COLLECTION_ITEM.getType()); // Since this is a read-only view, actions don't make sense in this context EntityForm ef = (EntityForm) model.asMap().get("entityForm"); ef.removeAllActions(); return returnPath; } protected String showViewUpdateCollection(HttpServletRequest request, Model model, Map<String, String> pathVars, String id, String collectionField, String collectionItemId, String alternateId, String modalHeaderType) throws ServiceException { return showViewUpdateCollection(request, model, pathVars, id, collectionField, collectionItemId, alternateId, modalHeaderType, null, null); } protected String showViewUpdateCollection(HttpServletRequest request, Model model, Map<String, String> pathVars, String id, String collectionField, String collectionItemId, String modalHeaderType) throws ServiceException { return showViewUpdateCollection(request, model, pathVars, id, collectionField, collectionItemId, null, modalHeaderType, null, null); } protected String showViewUpdateCollection(HttpServletRequest request, Model model, Map<String, String> pathVars, String id, String collectionField, String collectionItemId, String modalHeaderType, EntityForm entityForm, Entity entity) throws ServiceException { return showViewUpdateCollection(request, model, pathVars, id, collectionField, collectionItemId, null, modalHeaderType, entityForm, entity); } /** * Shows the view and populates the model for updating a collection item. You can also pass in an entityform and entity * which are optional. If they are not passed in then they are automatically looked up * * @param request * @param model * @param pathVars * @param id * @param collectionField * @param collectionItemId * @param modalHeaderType * @param ef * @param entity * @return * @throws ServiceException */ protected String showViewUpdateCollection(HttpServletRequest request, Model model, Map<String, String> pathVars, String id, String collectionField, String collectionItemId, String alternateId, String modalHeaderType, EntityForm entityForm, Entity entity) throws ServiceException { String sectionKey = getSectionKey(pathVars); String mainClassName = getClassNameForSection(sectionKey); List<SectionCrumb> sectionCrumbs = getSectionCrumbs(request, sectionKey, id); ClassMetadata mainMetadata = service.getClassMetadata(getSectionPersistencePackageRequest(mainClassName, sectionCrumbs, pathVars)).getDynamicResultSet().getClassMetaData(); Property collectionProperty = mainMetadata.getPMap().get(collectionField); FieldMetadata md = collectionProperty.getMetadata(); SectionCrumb nextCrumb = new SectionCrumb(); if (md instanceof MapMetadata) { nextCrumb.setSectionIdentifier(((MapMetadata) md).getValueClassName()); } else { nextCrumb.setSectionIdentifier(((CollectionMetadata) md).getCollectionCeilingEntity()); } nextCrumb.setSectionId(collectionItemId); if (!sectionCrumbs.contains(nextCrumb)) { sectionCrumbs.add(nextCrumb); } PersistencePackageRequest ppr = getSectionPersistencePackageRequest(mainClassName, sectionCrumbs, pathVars); Entity parentEntity = service.getRecord(ppr, id, mainMetadata, false).getDynamicResultSet().getRecords()[0]; ppr = PersistencePackageRequest.fromMetadata(md, sectionCrumbs); if (md instanceof BasicCollectionMetadata && ((BasicCollectionMetadata) md).getAddMethodType().equals(AddMethodType.PERSIST)) { BasicCollectionMetadata fmd = (BasicCollectionMetadata) md; ClassMetadata collectionMetadata = service.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); if (entity == null) { entity = service.getRecord(ppr, collectionItemId, collectionMetadata, true).getDynamicResultSet().getRecords()[0]; } Map<String, DynamicResultSet> subRecordsMap = service.getRecordsForAllSubCollections(ppr, entity, sectionCrumbs); if (entityForm == null) { entityForm = formService.createEntityForm(collectionMetadata, entity, subRecordsMap, sectionCrumbs); } else { entityForm.clearFieldsMap(); formService.populateEntityForm(collectionMetadata, entity, subRecordsMap, entityForm, sectionCrumbs); //remove all the actions since we're not trying to redisplay them on the form entityForm.removeAllActions(); } entityForm.removeAction(DefaultEntityFormActions.DELETE); model.addAttribute("entityForm", entityForm); model.addAttribute("viewType", "modal/simpleEditEntity"); } else if (md instanceof AdornedTargetCollectionMetadata) { AdornedTargetCollectionMetadata fmd = (AdornedTargetCollectionMetadata) md; if (entity == null) { entity = service.getAdvancedCollectionRecord(mainMetadata, parentEntity, collectionProperty, collectionItemId, sectionCrumbs, alternateId).getDynamicResultSet().getRecords()[0]; } boolean populateTypeAndId = true; boolean isViewCollectionItem = ModalHeaderType.VIEW_COLLECTION_ITEM.getType().equals(modalHeaderType); if (entityForm == null) { entityForm = formService.buildAdornedListForm(fmd, ppr.getAdornedList(), id, isViewCollectionItem); } else { entityForm.clearFieldsMap(); String entityType = entityForm.getEntityType(); formService.buildAdornedListForm(fmd, ppr.getAdornedList(), id, isViewCollectionItem, entityForm); entityForm.setEntityType(entityType); populateTypeAndId = false; } ClassMetadata cmd = service.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); for (String field : fmd.getMaintainedAdornedTargetFields()) { Property p = cmd.getPMap().get(field); if (p != null && p.getMetadata() instanceof AdornedTargetCollectionMetadata) { // Because we're dealing with a nested adorned target collection, this particular request must act // directly on the first adorned target collection. Because of this, we need the actual id property // from the entity that models the adorned target relationship, and not the id of the target object. Property alternateIdProperty = entity.getPMap().get(BasicPersistenceModule.ALTERNATE_ID_PROPERTY); DynamicResultSet drs = service.getRecordsForCollection(cmd, entity, p, null, null, null, alternateIdProperty.getValue(), sectionCrumbs).getDynamicResultSet(); ListGrid listGrid = formService.buildCollectionListGrid(alternateIdProperty.getValue(), drs, p, ppr.getAdornedList().getAdornedTargetEntityClassname(), sectionCrumbs); listGrid.setListGridType(ListGrid.Type.INLINE); listGrid.getToolbarActions().add(DefaultListGridActions.ADD); entityForm.addListGrid(listGrid, EntityForm.DEFAULT_TAB_NAME, EntityForm.DEFAULT_TAB_ORDER); } else if (p != null && p.getMetadata() instanceof MapMetadata) { // See above comment for AdornedTargetCollectionMetadata MapMetadata mmd = (MapMetadata) p.getMetadata(); Property alternateIdProperty = entity.getPMap().get(BasicPersistenceModule.ALTERNATE_ID_PROPERTY); DynamicResultSet drs = service.getRecordsForCollection(cmd, entity, p, null, null, null, alternateIdProperty.getValue(), sectionCrumbs).getDynamicResultSet(); ListGrid listGrid = formService.buildCollectionListGrid(alternateIdProperty.getValue(), drs, p, mmd.getTargetClass(), sectionCrumbs); listGrid.setListGridType(ListGrid.Type.INLINE); listGrid.getToolbarActions().add(DefaultListGridActions.ADD); entityForm.addListGrid(listGrid, EntityForm.DEFAULT_TAB_NAME, EntityForm.DEFAULT_TAB_ORDER); } } formService.populateEntityFormFields(entityForm, entity, populateTypeAndId, populateTypeAndId); formService.populateAdornedEntityFormFields(entityForm, entity, ppr.getAdornedList()); boolean atLeastOneBasicField = false; for (Entry<String, Field> entry : entityForm.getFields().entrySet()) { if (entry.getValue().getIsVisible()) { atLeastOneBasicField = true; break; } } if (!atLeastOneBasicField) { entityForm.removeAction(DefaultEntityFormActions.SAVE); } model.addAttribute("entityForm", entityForm); model.addAttribute("viewType", "modal/adornedEditEntity"); } else if (md instanceof MapMetadata) { MapMetadata fmd = (MapMetadata) md; ClassMetadata collectionMetadata = service.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); if (entity == null) { entity = service.getAdvancedCollectionRecord(mainMetadata, parentEntity, collectionProperty, collectionItemId, sectionCrumbs, null).getEntity(); } boolean populateTypeAndId = true; if (entityForm == null) { entityForm = formService.buildMapForm(fmd, ppr.getMapStructure(), collectionMetadata, id); } else { //save off the prior key before clearing out the fields map as it will not appear //back on the saved entity String priorKey = entityForm.getFields().get("priorKey").getValue(); entityForm.clearFieldsMap(); formService.buildMapForm(fmd, ppr.getMapStructure(), collectionMetadata, id, entityForm); entityForm.getFields().get("priorKey").setValue(priorKey); populateTypeAndId = false; } formService.populateEntityFormFields(entityForm, entity, populateTypeAndId, populateTypeAndId); formService.populateMapEntityFormFields(entityForm, entity); model.addAttribute("entityForm", entityForm); model.addAttribute("viewType", "modal/mapEditEntity"); } model.addAttribute("currentUrl", request.getRequestURL().toString()); model.addAttribute("modalHeaderType", modalHeaderType); model.addAttribute("collectionProperty", collectionProperty); setModelAttributes(model, sectionKey); return "modules/modalContainer"; } /** * Updates the specified collection item * * @param request * @param response * @param model * @param pathVars * @param id * @param collectionField * @param collectionItemId the collection primary key value (in the case of adorned target collection, this is the primary key value of the target entity) * @param entityForm * @param result * @return the return view path * @throws Exception */ @RequestMapping(value = "/{id}/{collectionField:.*}/{collectionItemId}", method = RequestMethod.POST) public String updateCollectionItem(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="id") String id, @PathVariable(value="collectionField") String collectionField, @PathVariable(value="collectionItemId") String collectionItemId, @ModelAttribute(value="entityForm") EntityForm entityForm, BindingResult result) throws Exception { return updateCollectionItem(request, response, model, pathVars, id, collectionField, collectionItemId, entityForm, null, result); } /** * Updates the specified collection item * * @param request * @param response * @param model * @param pathVars * @param id * @param collectionField * @param collectionItemId the collection primary key value (in the case of adorned target collection, this is the primary key value of the target entity) * @param entityForm * @param alternateId in the case of adorned target collections, this is the primary key value of the collection member * @param result * @return the return view path * @throws Exception */ @RequestMapping(value = "/{id}/{collectionField:.*}/{collectionItemId}/{alternateId}", method = RequestMethod.POST) public String updateCollectionItem(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="id") String id, @PathVariable(value="collectionField") String collectionField, @PathVariable(value="collectionItemId") String collectionItemId, @ModelAttribute(value="entityForm") EntityForm entityForm, @PathVariable(value="alternateId") String alternateId, BindingResult result) throws Exception { String sectionKey = getSectionKey(pathVars); String mainClassName = getClassNameForSection(sectionKey); List<SectionCrumb> sectionCrumbs = getSectionCrumbs(request, sectionKey, id); ClassMetadata mainMetadata = service.getClassMetadata(getSectionPersistencePackageRequest(mainClassName, sectionCrumbs, pathVars)).getDynamicResultSet().getClassMetaData(); Property collectionProperty = mainMetadata.getPMap().get(collectionField); PersistencePackageRequest ppr = getSectionPersistencePackageRequest(mainClassName, sectionCrumbs, pathVars); Entity entity = service.getRecord(ppr, id, mainMetadata, false).getDynamicResultSet().getRecords()[0]; // First, we must save the collection entity PersistenceResponse persistenceResponse = service.updateSubCollectionEntity(entityForm, mainMetadata, collectionProperty, entity, collectionItemId, alternateId, sectionCrumbs); Entity savedEntity = persistenceResponse.getEntity(); entityFormValidator.validate(entityForm, savedEntity, result); if (result.hasErrors()) { return showViewUpdateCollection(request, model, pathVars, id, collectionField, collectionItemId, alternateId, ModalHeaderType.UPDATE_COLLECTION_ITEM.getType(), entityForm, savedEntity); } // Next, we must get the new list grid that represents this collection ListGrid listGrid = getCollectionListGrid(mainMetadata, entity, collectionProperty, null, sectionKey, persistenceResponse, sectionCrumbs); model.addAttribute("listGrid", listGrid); // We return the new list grid so that it can replace the currently visible one setModelAttributes(model, sectionKey); return "views/standaloneListGrid"; } @RequestMapping(value = "/{id}/{collectionField:.*}/{collectionItemId}/sequence", method = RequestMethod.POST) public @ResponseBody Map<String, Object> updateCollectionItemSequence(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="id") String id, @PathVariable(value="collectionField") String collectionField, @PathVariable(value="collectionItemId") String collectionItemId, @RequestParam(value="newSequence") String newSequence) throws Exception { return updateCollectionItemSequence(request, response, model, pathVars, id, collectionField, collectionItemId, newSequence, null); } /** * Updates the given collection item's sequence. This should only be triggered for adorned target collections * where a sort field is specified -- any other invocation is incorrect and will result in an exception. * * @param request * @param response * @param model * @param pathVars * @param id * @param collectionField * @param collectionItemId * @return an object explaining the state of the operation * @throws Exception */ @RequestMapping(value = "/{id}/{collectionField:.*}/{collectionItemId}/{alternateId}/sequence", method = RequestMethod.POST) public @ResponseBody Map<String, Object> updateCollectionItemSequence(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="id") String id, @PathVariable(value="collectionField") String collectionField, @PathVariable(value="collectionItemId") String collectionItemId, @RequestParam(value="newSequence") String newSequence, @PathVariable(value="alternateId") String alternateId) throws Exception { String sectionKey = getSectionKey(pathVars); String mainClassName = getClassNameForSection(sectionKey); List<SectionCrumb> sectionCrumbs = getSectionCrumbs(request, sectionKey, id); ClassMetadata mainMetadata = service.getClassMetadata(getSectionPersistencePackageRequest(mainClassName, sectionCrumbs, pathVars)).getDynamicResultSet().getClassMetaData(); Property collectionProperty = mainMetadata.getPMap().get(collectionField); FieldMetadata md = collectionProperty.getMetadata(); PersistencePackageRequest ppr = getSectionPersistencePackageRequest(mainClassName, sectionCrumbs, pathVars); Entity parentEntity = service.getRecord(ppr, id, mainMetadata, false).getDynamicResultSet().getRecords()[0]; ppr = PersistencePackageRequest.fromMetadata(md, sectionCrumbs); if (md instanceof AdornedTargetCollectionMetadata) { AdornedTargetCollectionMetadata fmd = (AdornedTargetCollectionMetadata) md; AdornedTargetList atl = ppr.getAdornedList(); // Get an entity form for the entity EntityForm entityForm = formService.buildAdornedListForm(fmd, ppr.getAdornedList(), id, false); Entity entity = service.getAdvancedCollectionRecord(mainMetadata, parentEntity, collectionProperty, collectionItemId, sectionCrumbs, alternateId).getDynamicResultSet().getRecords()[0]; formService.populateEntityFormFields(entityForm, entity); formService.populateAdornedEntityFormFields(entityForm, entity, ppr.getAdornedList()); // Set the new sequence (note that it will come in 0-indexed but the persistence module expects 1-indexed) int sequenceValue = Integer.parseInt(newSequence) + 1; Field field = entityForm.findField(atl.getSortField()); field.setValue(String.valueOf(sequenceValue)); Map<String, Object> responseMap = new HashMap<String, Object>(); service.updateSubCollectionEntity(entityForm, mainMetadata, collectionProperty, entity, collectionItemId, alternateId, sectionCrumbs); responseMap.put("status", "ok"); responseMap.put("field", collectionField); return responseMap; } else if (md instanceof BasicCollectionMetadata) { BasicCollectionMetadata cd = (BasicCollectionMetadata) md; Map<String, Object> responseMap = new HashMap<String, Object>(); Entity entity = service.getRecord(ppr, collectionItemId, mainMetadata, false).getDynamicResultSet().getRecords()[0]; ClassMetadata collectionMetadata = service.getClassMetadata(ppr).getDynamicResultSet().getClassMetaData(); EntityForm entityForm = formService.createEntityForm(collectionMetadata, sectionCrumbs); if (!StringUtils.isEmpty(cd.getSortProperty())) { Field f = new Field() .withName(cd.getSortProperty()) .withFieldType(SupportedFieldType.HIDDEN.toString()); entityForm.addHiddenField(f); } formService.populateEntityFormFields(entityForm, entity); if (!StringUtils.isEmpty(cd.getSortProperty())) { int sequenceValue = Integer.parseInt(newSequence) + 1; Field field = entityForm.findField(cd.getSortProperty()); field.setValue(String.valueOf(sequenceValue)); } service.updateSubCollectionEntity(entityForm, mainMetadata, collectionProperty, parentEntity, collectionItemId, sectionCrumbs); responseMap.put("status", "ok"); responseMap.put("field", collectionField); return responseMap; } else { throw new UnsupportedOperationException("Cannot handle sequencing for non adorned target collection fields."); } } /** * Removes the requested collection item * * Note that the request must contain a parameter called "key" when attempting to remove a collection item from a * map collection. * * @param request * @param response * @param model * @param pathVars * @param id * @param collectionField * @param collectionItemId * @return the return view path * @throws Exception */ @RequestMapping(value = "/{id}/{collectionField:.*}/{collectionItemId}/delete", method = RequestMethod.POST) public String removeCollectionItem(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="id") String id, @PathVariable(value="collectionField") String collectionField, @PathVariable(value="collectionItemId") String collectionItemId) throws Exception { return removeCollectionItem(request, response, model, pathVars, id, collectionField, collectionItemId, null); } /** * Removes the requested collection item * * Note that the request must contain a parameter called "key" when attempting to remove a collection item from a * map collection. * * @param request * @param response * @param model * @param pathVars * @param id * @param collectionField * @param collectionItemId * @return the return view path * @throws Exception */ @RequestMapping(value = "/{id}/{collectionField:.*}/{collectionItemId}/{alternateId}/delete", method = RequestMethod.POST) public String removeCollectionItem(HttpServletRequest request, HttpServletResponse response, Model model, @PathVariable Map<String, String> pathVars, @PathVariable(value="id") String id, @PathVariable(value="collectionField") String collectionField, @PathVariable(value="collectionItemId") String collectionItemId, @PathVariable(value="alternateId") String alternateId) throws Exception { String sectionKey = getSectionKey(pathVars); String mainClassName = getClassNameForSection(sectionKey); List<SectionCrumb> sectionCrumbs = getSectionCrumbs(request, sectionKey, id); ClassMetadata mainMetadata = service.getClassMetadata(getSectionPersistencePackageRequest(mainClassName, sectionCrumbs, pathVars)).getDynamicResultSet().getClassMetaData(); Property collectionProperty = mainMetadata.getPMap().get(collectionField); String priorKey = request.getParameter("key"); PersistencePackageRequest ppr = getSectionPersistencePackageRequest(mainClassName, sectionCrumbs, pathVars); Entity entity = service.getRecord(ppr, id, mainMetadata, false).getDynamicResultSet().getRecords()[0]; // First, we must remove the collection entity PersistenceResponse persistenceResponse = service.removeSubCollectionEntity(mainMetadata, collectionProperty, entity, collectionItemId, alternateId, priorKey, sectionCrumbs); if (persistenceResponse.getEntity() != null && persistenceResponse.getEntity().isValidationFailure()) { String error = "There was an error removing the whatever"; if (MapUtils.isNotEmpty(persistenceResponse.getEntity().getPropertyValidationErrors())) { // If we failed, we'll return some JSON with the first error error = persistenceResponse.getEntity().getPropertyValidationErrors().values().iterator().next().get(0); } else if (CollectionUtils.isNotEmpty(persistenceResponse.getEntity().getGlobalValidationErrors())) { error = persistenceResponse.getEntity().getGlobalValidationErrors().get(0); } return new JsonResponse(response) .with("status", "error") .with("message", BLCMessageUtils.getMessage(error)) .done(); } // Next, we must get the new list grid that represents this collection ListGrid listGrid = getCollectionListGrid(mainMetadata, entity, collectionProperty, null, sectionKey, persistenceResponse, sectionCrumbs); model.addAttribute("listGrid", listGrid); // We return the new list grid so that it can replace the currently visible one setModelAttributes(model, sectionKey); return "views/standaloneListGrid"; } // ********************************* // ADDITIONAL SPRING-BOUND METHODS * // ********************************* /** * Invoked on every request to provide the ability to register specific binders for Spring's binding process. * By default, we register a binder that treats empty Strings as null and a Boolean editor that supports either true * or false. If the value is passed in as null, it will treat it as false. * * @param binder */ @InitBinder public void initBinder(WebDataBinder binder) { binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); binder.registerCustomEditor(Boolean.class, new NonNullBooleanEditor()); } }