/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. * <p> */ package org.olat.core.gui.components.form.flexible.impl; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.form.flexible.FormItem; import org.olat.core.gui.components.form.flexible.FormItemContainer; import org.olat.core.gui.components.form.flexible.FormUIFactory; import org.olat.core.gui.components.form.flexible.elements.InlineElement; import org.olat.core.gui.components.panel.StackedPanel; import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.Disposable; import org.olat.core.gui.control.Event; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.controller.BasicController; import org.olat.core.gui.translator.Translator; import org.olat.core.logging.AssertException; import org.olat.core.logging.activity.ThreadLocalUserActivityLoggerInstaller; /** * Description:<br> * The form basic controller acts as a facade for <tt>Flexi.Form</tt> * generation. * <ul> * <li>subclass it, and generate a constructor calling <tt>super(..)</tt></li> * <li>add also <tt>initForm(this.flc, this, ureq)</tt> to your constructor * as a last line</li> * <li>if you want your form layout, provide the velocity page name in the * <tt>super(..,name)</tt> call.</li> * <li>add your desired form elements in the <tt>initForm(..)</tt> method * implementaion</li> * <li>add your complex business validation logic by overriding the method * <tt>validateFormLogic(..)</tt></li> * <li>add your code to read form values and process them further in the * <tt>formOK(..)</tt> method implementation</li> * <li>in complex forms with subworkflows the <tt>formInnerEvent(..)</tt> * method can be overwritten and used the same way as known from * <tt>event(..)</tt>.</li> * </ul> * * <P> * Initial Date: 01.02.2007 <br> * * @author patrickb */ public abstract class FormBasicController extends BasicController implements IFormFragmentContainer { public static final int LAYOUT_DEFAULT = 0; public static final int LAYOUT_HORIZONTAL = 1; public static final int LAYOUT_VERTICAL = 2; public static final int LAYOUT_CUSTOM = 3; public static final int LAYOUT_BAREBONE = 4; public static final int LAYOUT_PANEL = 5; public static final int LAYOUT_DEFAULT_6_6 = 6; public static final int LAYOUT_DEFAULT_9_3 = 7; public static final int LAYOUT_DEFAULT_2_10 = 8; protected FormLayoutContainer flc; protected Form mainForm; protected StackedPanel initialPanel; private List<FragmentRef> fragments; protected final FormUIFactory uifactory = FormUIFactory.getInstance(); public FormBasicController(UserRequest ureq, WindowControl wControl) { this(ureq, wControl, (String)null); } public FormBasicController(UserRequest ureq, WindowControl wControl, Translator fallbackTranslator) { this(ureq, wControl, null, null, fallbackTranslator); } public FormBasicController(UserRequest ureq, WindowControl wControl, String pageName) { this(ureq, wControl, null, pageName); } /** * * @param ureq * @param wControl * @param mainFormId Give a fix identifier to the main form for state-less behavior * @param pageName */ public FormBasicController(UserRequest ureq, WindowControl wControl, String mainFormId, String pageName) { super(ureq, wControl); constructorInit(mainFormId, pageName); } /** pay attention when using this with stacked translators, they may get lost somehow! * until a translator-fix: use something like: * Translator pT = Util.createPackageTranslator(Alabla.class, ureq.getLocale(), getTranslator()); * this.setTranslator(pT); */ public FormBasicController(UserRequest ureq, WindowControl wControl, String pageName, Translator fallbackTranslator) { this(ureq, wControl, null, pageName, fallbackTranslator); } /** * * @param ureq * @param wControl * @param mainFormId Give a fix identifier to the main form for state-less behavior * @param pageName * @param fallbackTranslator */ public FormBasicController(UserRequest ureq, WindowControl wControl, String mainFormId, String pageName, Translator fallbackTranslator) { super(ureq, wControl, fallbackTranslator); constructorInit(mainFormId, pageName); } protected FormBasicController(UserRequest ureq, WindowControl wControl, int layout) { this(ureq, wControl, null, layout); } protected FormBasicController(UserRequest ureq, WindowControl wControl, String mainFormId, int layout){ super(ureq, wControl); if (layout == LAYOUT_HORIZONTAL) { // init with horizontal layout flc = FormLayoutContainer.createHorizontalFormLayout("ffo_horizontal", getTranslator()); mainForm = Form.create(mainFormId, "ffo_main_horizontal", flc, this); } else if (layout == LAYOUT_VERTICAL) { // init with vertical layout flc = FormLayoutContainer.createVerticalFormLayout("ffo_vertical", getTranslator()); mainForm = Form.create(mainFormId, "ffo_main_vertical", flc, this); } else if (layout == LAYOUT_BAREBONE) { // init with bare bone layout flc = FormLayoutContainer.createBareBoneFormLayout("ffo_barebone", getTranslator()); mainForm = Form.create(mainFormId, "ffo_main_barebone", flc, this); } else if (layout == LAYOUT_PANEL) { // init with panel layout flc = FormLayoutContainer.createPanelFormLayout("ffo_panel", getTranslator()); mainForm = Form.create(mainFormId, "ffo_main_panel", flc, this); } else if (layout == LAYOUT_DEFAULT_6_6) { flc = FormLayoutContainer.createDefaultFormLayout_6_6("ffo_default_6_6", getTranslator()); mainForm = Form.create(mainFormId, "ffo_main_default_6_6", flc, this); } else if (layout == LAYOUT_DEFAULT_9_3) { // init with panel layout flc = FormLayoutContainer.createDefaultFormLayout_9_3("ffo_default_9_3", getTranslator()); mainForm = Form.create(mainFormId, "ffo_main_default_9_3", flc, this); } else if (layout == LAYOUT_DEFAULT_2_10) { // init with panel layout flc = FormLayoutContainer.createDefaultFormLayout_2_10("ffo_default_2_10", getTranslator()); mainForm = Form.create(mainFormId, "ffo_main_default_2_10", flc, this); } else if (layout == LAYOUT_CUSTOM) { throw new AssertException("Use another constructor to work with a custom layout!"); } else { // init with default layout flc = FormLayoutContainer.createDefaultFormLayout("ffo_default", getTranslator()); mainForm = Form.create(mainFormId, "ffo_main_default", flc, this); } initialPanel = putInitialPanel(mainForm.getInitialComponent()); } protected FormBasicController(UserRequest ureq, WindowControl wControl, int layout, String customLayoutPageName, Form externalMainForm){ super(ureq, wControl); if (layout == LAYOUT_HORIZONTAL) { // init with horizontal layout flc = FormLayoutContainer.createHorizontalFormLayout("ffo_horizontal", getTranslator()); } else if (layout == LAYOUT_VERTICAL) { // init with vertical layout flc = FormLayoutContainer.createVerticalFormLayout("ffo_vertical", getTranslator()); } else if (layout == LAYOUT_BAREBONE) { // init with vertical layout flc = FormLayoutContainer.createBareBoneFormLayout("ffo_barebone", getTranslator()); } else if (layout == LAYOUT_PANEL) { // init with panel layout flc = FormLayoutContainer.createPanelFormLayout("ffo_panel", getTranslator()); } else if (layout == LAYOUT_DEFAULT_6_6) { // init with default layout flc = FormLayoutContainer.createDefaultFormLayout_6_6("ffo_panel", getTranslator()); } else if (layout == LAYOUT_DEFAULT_9_3) { // init with default layout flc = FormLayoutContainer.createDefaultFormLayout_9_3("ffo_panel", getTranslator()); } else if (layout == LAYOUT_DEFAULT_2_10) { // init with default layout flc = FormLayoutContainer.createDefaultFormLayout_2_10("ffo_panel", getTranslator()); } else if (layout == LAYOUT_CUSTOM && customLayoutPageName != null) { // init with provided layout String vc_pageName = velocity_root + "/" + customLayoutPageName + ".html"; flc = FormLayoutContainer.createCustomFormLayout("ffo_" + customLayoutPageName+this.hashCode(), getTranslator(), vc_pageName); } else { // init with default layout flc = FormLayoutContainer.createDefaultFormLayout("ffo_default", getTranslator()); } //instead of the constructorInit's Form.create... use a supplied one mainForm = externalMainForm; flc.setRootForm(externalMainForm); mainForm.addSubFormListener(this); initialPanel = putInitialPanel(flc.getComponent()); } /** * should be rarely overwritten, only if you provide infrastructure around flexi forms * @param mainFormId Give a fix identifier to the main form for state-less behavior * @param pageName */ protected void constructorInit(String mainFormId, String pageName) { String ffo_pagename = null; if (pageName != null) { if(pageName.endsWith(".html")) { // init with provided layout ffo_pagename = "ffo_" + pageName.replace("/", "_"); flc = FormLayoutContainer.createCustomFormLayout(ffo_pagename, getTranslator(), pageName); } else if(pageName.equals("form_horizontal")) { // init with provided layout ffo_pagename = "ffo_" + pageName.replace("/", "_"); flc = FormLayoutContainer.createHorizontalFormLayout(ffo_pagename, getTranslator()); } else if(pageName.equals("form_vertical")) { // init with provided layout ffo_pagename = "ffo_" + pageName.replace("/", "_"); flc = FormLayoutContainer.createVerticalFormLayout(ffo_pagename, getTranslator()); } else { // init with provided layout String vc_pageName = velocity_root + "/" + pageName + ".html"; ffo_pagename = "ffo_" + pageName; flc = FormLayoutContainer.createCustomFormLayout(ffo_pagename, getTranslator(), vc_pageName); } } else { // init with default layout ffo_pagename="ffo_default"; flc = FormLayoutContainer.createDefaultFormLayout(ffo_pagename, getTranslator()); } // mainForm = Form.create(mainFormId, "ffo_main_" + pageName, flc, this); /* * implementor must call initFormElements(...) */ initialPanel = putInitialPanel(mainForm.getInitialComponent()); } protected void initForm(UserRequest ureq) { initForm(this.flc, this, ureq); } /** * The creation initialisation and adding form elements to layout happens * here.<br> * The method is not called automatically, but it should be the last line in * your constructor.<br> * * @param formLayout * @param listener * @param ureq */ abstract protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq); public FormItem getInitialFormItem() { return flc; } protected void removeAsListenerAndDispose(Controller controller) { super.removeAsListenerAndDispose(controller); if(controller instanceof FormBasicController) { FormLayoutContainer container = ((FormBasicController) controller).flc; if(flc.getRootForm() == container.getRootForm()) { flc.getRootForm().removeSubFormListener((FormBasicController)controller); } } } /** * The form first validates each element, then it calls the * <tt>boolean validateFormLogic(ureq)</tt> from the listening controller. * This gives the possibility to implement complex business logic checks.<br> * In the case of valid elements and a valid form logic this method is called. * Typically one will read and save/update values then. */ abstract protected void formOK(UserRequest ureq); protected void formNext(UserRequest ureq) { formOK(ureq); } protected void formFinish(UserRequest ureq) { formOK(ureq); } /** * called if form validation was not ok.<br> * default implementation does nothing. Each element is assumed to set * errormessages. Needed only if complex business logic is checked even */ protected void formNOK(UserRequest ureq) { // by default nothing to do -> e.g. looping until form is ok } /** * Called when a form cancel button has been pressed. The form will * automatically be resetted. * * @param ureq */ protected void formCancelled(UserRequest ureq) { // by default nothing to do } /** * called if an element was activated resetting the form elements to their * initial values. After all resetting took place this method is called. * * @param ureq */ protected void formResetted(UserRequest ureq) { // by default no cleanup is needed outside of the form after resetting } /** * called if an element inside of the form triggered an event * * @param source * @param event */ protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) { // overwrite if you want to listen to inner form elements events } @Override protected void event(UserRequest ureq,Controller source, Event event) { routeEventToFragments(ureq, source, event); super.event(ureq, source, event); } /** * @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest, * org.olat.core.gui.components.Component, * org.olat.core.gui.control.Event) */ @Override public void event(UserRequest ureq, Component source, Event event) { routeEventToFragments(ureq, source, event); if (source == mainForm.getInitialComponent()) { // general form events if (event == org.olat.core.gui.components.form.Form.EVNT_VALIDATION_OK) { // Set container dirty to remove potentially rendered error messages. Do // this before calling formOK() to let formOK override the dirtiness // flag flc.setDirty(true); formOK(ureq); } else if (event == org.olat.core.gui.components.form.Form.EVNT_VALIDATION_NEXT) { // Set container dirty to remove potentially rendered error messages. Do // this before calling formOK() to let formOK override the dirtiness // flag flc.setDirty(true); formNext(ureq); } else if (event == org.olat.core.gui.components.form.Form.EVNT_VALIDATION_FINISH) { // Set container dirty to remove potentially rendered error messages. Do // this before calling formOK() to let formOK override the dirtiness // flag flc.setDirty(true); formFinish(ureq); } else if (event == org.olat.core.gui.components.form.Form.EVNT_VALIDATION_NOK) { // Set container dirty to rendered error messages. Do this before calling // formNOK() to let formNOK override the dirtiness flag flc.setDirty(true); formNOK(ureq); } else if (event == FormEvent.RESET) { // Set container dirty to render everything from scratch, remove error // messages. Do this before calling // formResetted() to let formResetted override the dirtiness flag flc.setDirty(true); formResetted(ureq); } else if (event instanceof FormEvent) { FormEvent fe = (FormEvent) event; // Special case: cancel events are wrapped as form inner events if (fe.getCommand().equals(org.olat.core.gui.components.form.Form.EVNT_FORM_CANCELLED.getCommand())) { // Set container dirty to clear error messages. Do this before calling // formCancelled() to let formCancelled override the dirtiness flag flc.setDirty(true); formResetted(ureq); formCancelled(ureq); return; } /* * evaluate normal inner form events */ FormItem fiSrc = fe.getFormItemSource(); propagateDirtinessToContainer(fiSrc, fe); // formInnerEvent(ureq, fiSrc, fe); // no need to set container dirty, up to controller code if something is dirty } } } protected void propagateDirtinessToContainer(FormItem fiSrc, @SuppressWarnings("unused") FormEvent fe) { // check for InlineElments remove as the tag library has been replaced if(fiSrc instanceof InlineElement){ if(!((InlineElement) fiSrc).isInlineEditingElement()){ //OO-137 //the container need to be redrawn because every form item element //is made of severals components. If a form item is set to invisible //the layout which glue the different components stay visible flc.setDirty(true); } } } public String getAndRemoveFormTitle() { String title = (String)flc.contextGet("off_title"); if(title != null) { flc.contextRemove("off_title"); } return title; } /** * Set an optional form title that is rendered as a fieldset legend. If you * use a custom template this will have no effect * * @param i18nKey */ protected void setFormTitle(String i18nKey) { if (i18nKey == null) { flc.contextRemove("off_title"); } else { flc.contextPut("off_title", getTranslator().translate(i18nKey)); } } /** * Set an optional form title that is rendered as a fieldset legend. If you * use a custom template this will have no effect * * @param i18nKey * @param args optional arguments */ protected void setFormTitle(String i18nKey, String[] args) { if (i18nKey == null) { flc.contextRemove("off_title"); } else { flc.contextPut("off_title", getTranslator().translate(i18nKey, args)); } } /** * Set an optional icon by giving the necessary css classes * @param iconCss */ protected void setFormTitleIconCss(String iconCss) { if (iconCss == null) { flc.contextRemove("off_icon"); } else { flc.contextPut("off_icon", iconCss); } } /** * Set an optional description. This will appear above the form. If you use a * custom template this will have no effect * * @param i18nKey */ protected void setFormDescription(String i18nKey) { if (i18nKey == null) { flc.contextRemove("off_desc"); } else { flc.contextPut("off_desc", getTranslator().translate(i18nKey)); } } /** * Set an optional description. This will appear above the form. If you use a * custom template this will have no effect * * @param i18nKey * @args args optional arguments */ protected void setFormDescription(String i18nKey, String[] args) { if (i18nKey == null) { flc.contextRemove("off_desc"); } else { flc.contextPut("off_desc", getTranslator().translate(i18nKey, args)); } } /** * Set a warning. This will appear before the form, after the description. * If you use a custom template this will have no effect * * @param i18nKey * @args args optional arguments */ protected void setFormWarning (String i18nKey, String[] args) { if (i18nKey == null) { flc.contextRemove("off_warn"); } else { flc.contextPut("off_warn", getTranslator().translate(i18nKey, args)); } } /** * Set a warning. This will appear before the form, after the description. * If you use a custom template this will have no effect * * @param i18nKey */ protected void setFormWarning (String i18nKey) { this.flc.contextRemove("off_info"); if (i18nKey == null) { flc.contextRemove("off_warn"); } else { flc.contextPut("off_warn", getTranslator().translate(i18nKey)); } } /** * Set a message to be displayed in the form, after the description. * The form warning, if there is one, will be removed. * @see org.olat.core.gui.control.controller.BasicController#showInfo(java.lang.String) */ public void setFormInfo (String i18nKey) { flc.contextRemove("off_warn"); if (i18nKey == null) { flc.contextRemove("off_info"); } else { flc.contextPut("off_info", getTranslator().translate(i18nKey)); } } /** * Set a message to be displayed in the form, after the description. * The form warning, if there is one, will be removed. * @see org.olat.core.gui.control.controller.BasicController#showInfo(java.lang.String) */ protected void setFormInfo (String i18nKey, String[] args) { flc.contextRemove("off_warn"); if (i18nKey == null) { flc.contextRemove("off_info"); } else { flc.contextPut("off_info", getTranslator().translate(i18nKey, args)); } } protected void setFormContextHelp(String url) { if (url == null) { flc.contextRemove("off_chelp_url"); } else { flc.contextPut("off_chelp_url", url); } } /** * Set an optional css class to use for this form. May help to achieve custom formatting without * a separate velocity container. * * @param cssClassName the css class name to wrap around form */ protected void setFormStyle(String cssClassName){ if (cssClassName == null){ flc.contextRemove("off_css_class"); } else { flc.contextPut("off_css_class", cssClassName); } } @Override protected void setTranslator(Translator translator) { super.setTranslator(translator); flc.setTranslator(translator); } /** * @param ureq * @return */ protected boolean validateFormLogic(UserRequest ureq) { return true; } /** * Adding disposing of form items that implement the disposable interface. * Some Form items might generate temporary data that needs to be cleaned * up. * <p> * First, super.dispose() is called. * * @see org.olat.core.gui.control.DefaultController#dispose() */ @Override public void dispose() { super.dispose(); ThreadLocalUserActivityLoggerInstaller.runWithUserActivityLogger(new Runnable() { public void run() { // Dispose also disposable form items (such as file uploads that needs to // cleanup temporary files) Iterable<FormItem> formItems = FormBasicController.this.flc.getFormItems(); for (FormItem formItem : formItems) { if (formItem instanceof Disposable) { Disposable disposableFormItem = (Disposable) formItem; disposableFormItem.dispose(); } } forEachFragment(fragment -> { // do nothing for now }); } }, getUserActivityLogger()); } // ------------------------------------------------------------------------ // IFormFragmentContainer implementation /** * A lifecycle method used to signal the controller that it should read the contents * of its configuration and apply it to its view. The idea is that the structure of a * form can be created separately from assigning contents to its fields. * <p>This is particularly useful when dealing with fragments which know how to * load/store data from a given configuration themselves. * @param ureq */ public void readFormData(UserRequest ureq) { // do nothing by default } /** * A lifecycle used to signal the form controller that it should visit the view's * content and save all relevant data into its current configuration. * <p>This is particularly useful when dealing with fragments which know how to * load/store data from a given configuration themselves. * @param ureq */ public void storeFormData(UserRequest ureq) { // do nothing by default } @Override public IFormFragmentHost getFragmentHostInterface() { // this is to document the missing method implementation in a subclass throw new IllegalStateException("In order to host from fragments the controller must override getFragmentHostInterface(): " + this.getClass().getSimpleName() ); } private static class FragmentRef extends WeakReference<IFormFragment>{ public FragmentRef(IFormFragment referent) { super(referent); } } @Override public void setNeedsLayout() { flc.setDirty(true); } @Override public void registerFormFragment(IFormFragment fragment) { if(fragments == null) { fragments = new ArrayList<>(5); } fragments.add(new FragmentRef(fragment)); } @Override public FormItemContainer formItemsContainer() { return flc; } // Helpers @Override protected void fireEvent(UserRequest ureq, Event event) { super.fireEvent(ureq, event); } // Redefinition of a the super method to provide access with the same // package (this is required for the Form Fragments) @Override protected Controller listenTo(Controller controller) { return super.listenTo(controller); } @Override public void forEachFragment(Consumer<IFormFragment> handler) { if(fragments == null) return; fragments.stream() .filter(ref -> ref.get() != null) .forEach(ref -> { IFormFragment frag = ref.get(); if (frag != null) { handler.accept(frag); } }); } protected boolean routeEventToFragments(UserRequest ureq, Component source, Event event) { if(fragments == null) return false; // implement shortcut?! Boolean processed = fragments.stream() .reduce(Boolean.FALSE , (t, u) -> { IFormFragment frag = u.get(); return frag == null ? Boolean.FALSE : frag.processEvent(ureq, source, event); }, (t, u) -> t & u) ; return processed; } protected boolean routeEventToFragments(UserRequest ureq, Controller source, Event event) { if(fragments == null) return false; // implement shortcut?! Boolean processed = fragments.stream() .reduce(Boolean.FALSE , (t, u) -> { IFormFragment frag = u.get(); return frag == null ? Boolean.FALSE : frag.processEvent(ureq, source, event); }, (t, u) -> t & u) ; return processed; } }