/******************************************************************************* * Copyright (c) 2015 Development Gateway, Inc and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the MIT License (MIT) * which accompanies this distribution, and is available at * https://opensource.org/licenses/MIT * * Contributors: * Development Gateway - initial API and implementation *******************************************************************************/ package org.devgateway.toolkit.forms.wicket.page.edit; import de.agilecoders.wicket.core.markup.html.bootstrap.common.NotificationMessage; import de.agilecoders.wicket.core.markup.html.bootstrap.form.BootstrapForm; import de.agilecoders.wicket.core.util.Attributes; import nl.dries.wicket.hibernate.dozer.DozerModel; import org.apache.log4j.Logger; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.panel.Fragment; import org.apache.wicket.model.CompoundPropertyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.model.StringResourceModel; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.spring.injection.annot.SpringBean; import org.apache.wicket.util.lang.Classes; import org.apache.wicket.util.time.Duration; import org.apache.wicket.util.visit.IVisit; import org.apache.wicket.util.visit.IVisitor; import org.apache.wicket.validation.IValidator; import org.apache.wicket.validation.ValidationError; import org.apache.wicket.validation.validator.EmailAddressValidator; import org.apache.wicket.validation.validator.RangeValidator; import org.devgateway.toolkit.forms.WebConstants; import org.devgateway.toolkit.forms.exceptions.NullJpaRepositoryException; import org.devgateway.toolkit.forms.exceptions.NullListPageClassException; import org.devgateway.toolkit.forms.util.MarkupCacheService; import org.devgateway.toolkit.forms.wicket.components.ComponentUtil; import org.devgateway.toolkit.forms.wicket.components.form.BootstrapCancelButton; import org.devgateway.toolkit.forms.wicket.components.form.BootstrapDeleteButton; import org.devgateway.toolkit.forms.wicket.components.form.BootstrapSubmitButton; import org.devgateway.toolkit.forms.wicket.components.form.CheckBoxBootstrapFormComponent; import org.devgateway.toolkit.forms.wicket.components.form.DateFieldBootstrapFormComponent; import org.devgateway.toolkit.forms.wicket.components.form.DateTimeFieldBootstrapFormComponent; import org.devgateway.toolkit.forms.wicket.components.form.GenericBootstrapFormComponent; import org.devgateway.toolkit.forms.wicket.components.form.Select2ChoiceBootstrapFormComponent; import org.devgateway.toolkit.forms.wicket.components.form.TextAreaFieldBootstrapFormComponent; import org.devgateway.toolkit.forms.wicket.components.form.TextFieldBootstrapFormComponent; import org.devgateway.toolkit.forms.wicket.page.BasePage; import org.devgateway.toolkit.forms.wicket.providers.GenericPersistableJpaRepositoryTextChoiceProvider; import org.devgateway.toolkit.persistence.dao.GenericPersistable; import org.devgateway.toolkit.persistence.dao.Labelable; import org.devgateway.toolkit.persistence.repository.category.TextSearchableRepository; import org.devgateway.toolkit.reporting.spring.util.ReportsCacheService; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.jpa.repository.JpaRepository; import javax.persistence.EntityManager; import java.io.Serializable; /** * @author mpostelnicu Page used to make editing easy, extend to get easy access * to one entity for editing */ public abstract class AbstractEditPage<T extends GenericPersistable> extends BasePage { protected static Logger logger = Logger.getLogger(AbstractEditPage.class); private static final long serialVersionUID = -5928614890244382103L; /** * Factory method for the new instance of the entity being editing. This * will be invoked only when the parameter PARAM_ID is null * * @return */ protected abstract T newInstance(); /** * The repository used to fetch and save the entity, this is initialized in * subclasses */ protected JpaRepository<T, Long> jpaRepository; /** * The page that is responsible for listing the entities (used here as a * return reference after successful save) */ protected Class<? extends BasePage> listPageClass; /** * The form used by all subclasses */ protected EditForm editForm; /** * the entity id, or null if a new entity is requested */ protected Long entityId; /** * This is a wrapper model that ensures we can easily edit the properties of * the entity */ protected CompoundPropertyModel<T> compoundModel; /** * generic submit button for the form */ protected BootstrapSubmitButton saveButton; /** * generic delete button for the form */ protected BootstrapDeleteButton deleteButton; @SpringBean protected EntityManager entityManager; @SpringBean(required = false) protected ReportsCacheService reportsCacheService; @SpringBean(required = false) protected MarkupCacheService markupCacheService; public void flushReportingCaches() { if (reportsCacheService != null && markupCacheService != null) { reportsCacheService.flushCache(); markupCacheService.flushMarkupCache(); markupCacheService.clearReportsCache(); markupCacheService.clearReportsApiCache(); } } public GenericBootstrapValidationVisitor getBootstrapValidationVisitor(final AjaxRequestTarget target) { return new GenericBootstrapValidationVisitor(target); } /** * Traverses all fields and refreshes the ones that are not valid, so that * we can see the errors * * @author mpostelnicu * */ public class GenericBootstrapValidationVisitor implements IVisitor<GenericBootstrapFormComponent<?, ?>, Void> { protected AjaxRequestTarget target; protected GenericBootstrapFormComponent<?, ?> lastInvalidVisitedObject; public GenericBootstrapValidationVisitor(final AjaxRequestTarget target) { this.target = target; } @Override public void component(final GenericBootstrapFormComponent<?, ?> object, final IVisit<Void> visit) { visit.dontGoDeeper(); if (object.getField().isValid()) { return; } target.add(object.getBorder()); // remember last invalid visited object, we used this later to // trigger the visibility of its parent container, if it is folded lastInvalidVisitedObject = object; // there's no point in visiting anything else, we already have a // section with error. This hugely improves speed of large forms // visit.stop(); } public GenericBootstrapFormComponent<?, ?> getLastInvalidVisitedObject() { return lastInvalidVisitedObject; } } public class EditForm extends BootstrapForm<T> { private static final long serialVersionUID = -9127043819229346784L; /** * wrap the model with a {@link CompoundPropertyModel} to ease editing * of fields * * @param model */ public void setCompoundPropertyModel(final IModel<T> model) { compoundModel = new CompoundPropertyModel<T>(model); setModel(compoundModel); } public EditForm(final String id, final IModel<T> model) { this(id); setCompoundPropertyModel(model); } public EditForm(final String id) { super(id); setOutputMarkupId(true); saveButton = getSaveEditPageButton(); add(saveButton); deleteButton = getDeleteEditPageButton(); add(deleteButton); // don't display the delete button if we just create a new entity if (entityId == null) { deleteButton.setVisibilityAllowed(false); } add(new BootstrapCancelButton("cancel", new StringResourceModel("cancelButton", this, null)) { private static final long serialVersionUID = -249084359200507749L; @Override protected void onSubmit(final AjaxRequestTarget target, final Form<?> form) { setResponsePage(listPageClass); } }); } } /** * Generic funcionality for the save page button, this can be extended * further by subclasses * * @author mpostelnicu * */ public class SaveEditPageButton extends BootstrapSubmitButton { private static final long serialVersionUID = 9075809391795974349L; protected boolean redirect = true; protected boolean redirectToSelf = false; public SaveEditPageButton(final String id, final IModel<String> model) { super(id, model); } @Override protected void onSubmit(final AjaxRequestTarget target, final Form<?> form) { // save the object and go back to the list page T saveable = editForm.getModelObject(); // saves the entity and flushes the changes jpaRepository.saveAndFlush(saveable); // clears session and detaches all entities that are currently // attached entityManager.clear(); // we flush the mondrian/wicket/reports cache to ensure it gets // rebuilt flushReportingCaches(); // only redirect if redirect is true if (redirectToSelf) { // we need to close the blockUI if it's opened and enable all // the buttons target.appendJavaScript("$.unblockUI();"); target.appendJavaScript("$('#" + editForm.getMarkupId() + " button').prop('disabled', false);"); } else if (redirect) { setResponsePage(getResponsePage(), getParameterPage()); } // redirect is set back to true, which is the default behavior redirect = true; redirectToSelf = false; } /** * by default, submit button returns back to listPage * * @return */ protected Class<? extends BasePage> getResponsePage() { return listPageClass; } /** * no params by default * * @return */ protected PageParameters getParameterPage() { return null; } @Override protected void onError(final AjaxRequestTarget target, final Form<?> form) { // make all errors visible GenericBootstrapValidationVisitor genericBootstrapValidationVisitor = getBootstrapValidationVisitor(target); editForm.visitChildren(GenericBootstrapFormComponent.class, genericBootstrapValidationVisitor); ValidationError error = new ValidationError(); error.addKey("formHasErrors"); error(error); target.add(feedbackPanel); // autoscroll down to the feedback panel target.appendJavaScript("$('html, body').animate({scrollTop: $(\".feedbackPanel\").offset().top}, 500);"); } /** * @return the redirect */ public boolean isRedirect() { return redirect; } /** * @param redirect * the redirect to set */ public void setRedirect(final boolean redirect) { this.redirect = redirect; } /** * @param redirectToSelf * the redirectToSelf to set */ public void setRedirectToSelf(final boolean redirectToSelf) { this.redirectToSelf = redirectToSelf; } /** * @return the redirectToSelf */ public boolean isRedirectToSelf() { return redirectToSelf; } } public class DeleteEditPageButton extends BootstrapDeleteButton { private static final long serialVersionUID = 865330306716770506L; public DeleteEditPageButton(final String id, final IModel<String> model) { super(id, model); } @Override protected void onSubmit(final AjaxRequestTarget target, final Form<?> form) { T deleteable = editForm.getModelObject(); try { jpaRepository.delete(deleteable); } catch (DataIntegrityViolationException e) { error(new NotificationMessage( new StringResourceModel("delete_error_message", AbstractEditPage.this, null)) .hideAfter(Duration.NONE)); target.add(feedbackPanel); return; } setResponsePage(listPageClass); } @Override protected void onError(final AjaxRequestTarget target, final Form<?> form) { target.add(feedbackPanel); } } /** * Override this to create new save buttons with additional behaviors * * @return */ public SaveEditPageButton getSaveEditPageButton() { return new SaveEditPageButton("save", new StringResourceModel("saveButton", this, null)); } /** * Override this to create new delete buttons if you need additional * behavior * * @return */ public DeleteEditPageButton getDeleteEditPageButton() { return new DeleteEditPageButton("delete", new StringResourceModel("deleteButton", this, null)); } public AbstractEditPage(final PageParameters parameters) { super(parameters); if (!parameters.get(WebConstants.PARAM_ID).isNull()) { entityId = parameters.get(WebConstants.PARAM_ID).toLongObject(); } editForm = new EditForm("editForm") { private static final long serialVersionUID = 1L; @Override protected void onComponentTag(final ComponentTag tag) { super.onComponentTag(tag); if (ComponentUtil.isViewMode()) { Attributes.addClass(tag, "print-view"); } } }; // use this in order to avoid "ServletRequest does not contain multipart // content" error // this error appears when we have a file upload component that is // hidden or not present in the page when the form is created editForm.setMultiPart(true); add(editForm); // this fragment ensures extra buttons are added below the wicket:child // section in child Fragment fragment = new Fragment("extraButtons", "noButtons", this); editForm.add(fragment); } @Override protected void onInitialize() { super.onInitialize(); // we cant do anything if we dont have a jparepository here if (jpaRepository == null) { throw new NullJpaRepositoryException(); } // we dont like receiving null list pages if (listPageClass == null) { throw new NullListPageClassException(); } IModel<T> model = null; if (entityId != null) { model = new DozerModel<>(jpaRepository.findOne(entityId)); } else { T instance = newInstance(); if (instance != null) { model = new Model<>(instance); } } if (model != null) { editForm.setCompoundPropertyModel(model); } } protected String getClassName() { return Classes.simpleName(getClass()); } public IValidator<? super String> isEmail() { return EmailAddressValidator.getInstance(); } public <P extends Comparable<? super P> & Serializable> RangeValidator<P> inRange(final P min, final P max) { return new RangeValidator<>(min, max); } public CheckBoxBootstrapFormComponent addCheckBox(final String name) { CheckBoxBootstrapFormComponent checkBox = new CheckBoxBootstrapFormComponent(name); editForm.add(checkBox); return checkBox; } public TextAreaFieldBootstrapFormComponent<String> addTextAreaField(final String name) { TextAreaFieldBootstrapFormComponent<String> textAreaField = new TextAreaFieldBootstrapFormComponent<>(name); editForm.add(textAreaField); return textAreaField; } public TextFieldBootstrapFormComponent<String> addTextField(final String name) { TextFieldBootstrapFormComponent<String> textField = new TextFieldBootstrapFormComponent<>(name); editForm.add(textField); return textField; } public TextFieldBootstrapFormComponent<Integer> addIntegerTextField(final String name) { TextFieldBootstrapFormComponent<Integer> textField = new TextFieldBootstrapFormComponent<>(name); textField.integer(); editForm.add(textField); return textField; } public TextFieldBootstrapFormComponent<String> addDoubleField(final String name) { TextFieldBootstrapFormComponent<String> textField = new TextFieldBootstrapFormComponent<>(name); textField.asDouble(); editForm.add(textField); return textField; } public DateTimeFieldBootstrapFormComponent addDateTimeField(final String name) { DateTimeFieldBootstrapFormComponent field = new DateTimeFieldBootstrapFormComponent(name); editForm.add(field); return field; } public DateFieldBootstrapFormComponent addDateField(final String name) { DateFieldBootstrapFormComponent field = new DateFieldBootstrapFormComponent(name); editForm.add(field); return field; } public <E extends GenericPersistable & Labelable> Select2ChoiceBootstrapFormComponent<E> addSelect2ChoiceField(final String name, final TextSearchableRepository<E, Long> repository) { GenericPersistableJpaRepositoryTextChoiceProvider<E> choiceProvider = new GenericPersistableJpaRepositoryTextChoiceProvider<>(repository); Select2ChoiceBootstrapFormComponent<E> component = new Select2ChoiceBootstrapFormComponent<>(name, choiceProvider); editForm.add(component); return component; } }