Java tutorial
/* * Copyright 2011-2013 the original author or authors. * * 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. */ package com.springinpractice.ch11.web.controller; import static org.springframework.util.Assert.notNull; import java.io.IOException; import java.lang.reflect.ParameterizedType; import java.util.List; import javax.inject.Inject; import javax.validation.Valid; import org.springframework.beans.propertyeditors.StringTrimmerEditor; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; 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.ResponseBody; import com.springinpractice.ch11.model.CI; import com.springinpractice.ch11.model.ListWrapper; import com.springinpractice.ch11.service.CIService; import com.springinpractice.ch11.web.sitemap.Paths; import com.springinpractice.ch11.web.view.ViewNames; /** * @author Willie Wheeler (willie.wheeler@gmail.com) */ public abstract class AbstractCrudController<T extends CI<T>> extends AbstractController { public static final String MK_FORM_DATA = "formData"; public static final String MK_ENTITY = "entity"; public static final String MK_HAS_ERRORS = "hasErrors"; public static final String MK_FORM_METHOD = "formMethod"; public static final String MK_SUBMIT_PATH = "submitPath"; public static final String MK_CANCEL_PATH = "cancelPath"; @Inject protected Paths paths; @Inject protected ViewNames viewNames; // IMPORTANT: Only getCiClass() should access this directly! Class<T> ciClass; @SuppressWarnings("unchecked") public AbstractCrudController() { ParameterizedType paramType = (ParameterizedType) getClass().getGenericSuperclass(); this.ciClass = (Class<T>) paramType.getActualTypeArguments()[0]; } protected abstract CIService<T> getService(); protected T newCiInstance() { try { return ciClass.newInstance(); } catch (InstantiationException e) { // Shouldn't happen throw new RuntimeException(e); } catch (IllegalAccessException e) { // Shouldn't happen throw new RuntimeException(e); } } /** * @param binder binder */ @InitBinder(MK_FORM_DATA) public void initBinder(WebDataBinder binder) { binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); binder.setAllowedFields(getAllowedFields()); } // FIXME Make abstract after refactoring is done protected abstract String[] getAllowedFields(); /** * Places reference data on the model. The default implementation is a no-op, but subclasses can override this as * necessary. * * @param model model */ protected void populateReferenceData(Model model) { } // ================================================================================================================= // Create // ================================================================================================================= /** * @param model * @return */ @RequestMapping(value = "/new", method = RequestMethod.GET) public String getCreateForm(Model model) { model.addAttribute(MK_FORM_DATA, newCiInstance()); return prepareCreateForm(model); } /** * @param model * @param formData * @param result * @return */ @RequestMapping(value = "", method = RequestMethod.POST) public String postCreateForm(Model model, @ModelAttribute(MK_FORM_DATA) @Valid T formData, BindingResult result) { getService().create(formData, result); if (result.hasErrors()) { model.addAttribute(MK_HAS_ERRORS, true); return prepareCreateForm(model); } return viewNames.postCreateFormSuccessViewName(ciClass); } /** * @param model model * @return view name */ protected String prepareCreateForm(Model model) { populateReferenceData(model); model.addAttribute(MK_FORM_METHOD, "post"); model.addAttribute(MK_SUBMIT_PATH, paths.getSubmitCreateFormPath(ciClass)); model.addAttribute(MK_CANCEL_PATH, paths.getBasePath(ciClass) + "?a=cancelled"); return addNavigation(model, sitemap.getCreateFormId(ciClass)); } // ================================================================================================================= // Read list // ================================================================================================================= /** * Places a sorted list of the controller's entities on the model. * * @param model model * @return logical view name */ @RequestMapping(value = "", method = RequestMethod.GET) public String getList(Model model) { model.addAttribute(getSortedList()); return addNavigation(model, sitemap.getEntityListViewId(ciClass)); } /** * @return list of CIs */ @RequestMapping(value = "", method = RequestMethod.GET, params = "format=json", produces = "application/json") @ResponseBody public List<T> getListAsJson() { return getSortedList(); } /** * @return list of CIs * @throws Exception */ @SuppressWarnings("unchecked") @RequestMapping(value = "", method = RequestMethod.GET, params = "format=xml", produces = "application/xml") @ResponseBody public ListWrapper<T> getListAsXml() throws Exception { // IMPORTANT: We can't access ciClass directly (need to call getCiClass() to guarantee initialization) String wrapperClassName = ciClass.getName() + "$" + ciClass.getSimpleName() + "ListWrapper"; Class<ListWrapper<T>> wrapperClass = (Class<ListWrapper<T>>) Class.forName(wrapperClassName); ListWrapper<T> wrapper = wrapperClass.newInstance(); wrapper.setList(getSortedList()); return wrapper; } private List<T> getSortedList() { return getService().findAll(); } // ================================================================================================================= // Read details // ================================================================================================================= /** * Gets the requested details entity, placing it on the model under two separate keys: as "entity" to support * generic capabilities like navigation, and as the autogenerated key for JSP clarity. * * @param id entity ID * @param model model * @return logical view name */ @RequestMapping(value = "/{id}", method = RequestMethod.GET) public String getDetails(@PathVariable Long id, Model model) { T ci = doGetDetails(id, model); model.addAttribute(ci); model.addAttribute("entity", ci); return addNavigation(model, sitemap.getCiDetailsViewId(ciClass)); } /** * Subclasses can override this method as necessary. * * @param id ID * @return entity * @deprecated Override the service's findOne() method instead */ @Deprecated protected T doGetDetails(Long id, Model model) { return getDetails(id); } /** * @param id * @param out * @throws IOException */ // From the reference docs: "Furthermore, use of the produces condition ensures the actual content type used to // generate the response respects the media types specified in the produces condition." @RequestMapping(value = "/{id}", method = RequestMethod.GET, params = "format=json", produces = "application/json") @ResponseBody public T getDetailsAsJson(@PathVariable Long id) { return getDetails(id); } /** * @param id * @return */ @RequestMapping(value = "/{id}", method = RequestMethod.GET, params = "format=xml", produces = "application/xml") @ResponseBody public T getDetailsAsXml(@PathVariable Long id) { return getDetails(id); } private T getDetails(Long id) { return getService().findOne(id); } // ================================================================================================================= // Update // ================================================================================================================= /** * @param id * @param model * @return */ @RequestMapping(value = "/{id}/edit", method = RequestMethod.GET) public String getEditForm(@PathVariable Long id, Model model) { model.addAttribute(MK_FORM_DATA, getService().findOne(id)); String viewName = prepareEditForm(id, model); return viewName; } /** * @param id entity ID * @param formData entity form data * @param result result object * @param model model * @return view name */ @RequestMapping(value = "/{id}", method = RequestMethod.PUT) public String putEditForm(@PathVariable Long id, @ModelAttribute(MK_FORM_DATA) @Valid T formData, BindingResult result, Model model) { formData.setId(id); getService().update(formData, result); if (result.hasErrors()) { model.addAttribute(MK_HAS_ERRORS, true); return prepareEditForm(id, model); } return viewNames.putEditFormSuccessViewName(ciClass, id); } /** * @param id * @param model * @return */ protected String prepareEditForm(Long id, Model model) { populateReferenceData(model); // Need to load the entity itself since the entity drives title and breadcrumbs, and we don't want form data // changes to impact them. Can optimize this by giving the entities clone constructors. CI<?> ci = getService().findOne(id); model.addAttribute(ci); model.addAttribute(MK_ENTITY, ci); model.addAttribute(MK_FORM_METHOD, "put"); model.addAttribute(MK_SUBMIT_PATH, paths.getSubmitEditFormPath(ciClass, id)); model.addAttribute(MK_CANCEL_PATH, paths.getDetailsPath(ciClass, id) + "?a=cancelled"); String editFormId = sitemap.getEditFormId(ciClass); notNull(editFormId, "Null edit form ID for CI class " + ciClass); return addNavigation(model, editFormId); } // ================================================================================================================= // Delete // ================================================================================================================= /** * @param id entity ID * @return view name */ @RequestMapping(value = "{id}", method = RequestMethod.DELETE) public String delete(@PathVariable Long id) { notNull(id); getService().delete(id); return viewNames.deleteSuccessViewName(ciClass); } }