From 09869e9b2c0d4f968a75849306e4229244f46e39 Mon Sep 17 00:00:00 2001 From: lmihalkovic <laurent.michalkovic@frentix.com> Date: Fri, 13 May 2016 11:34:04 +0200 Subject: [PATCH] OO-2007: new light weight form fragments mechanism to reuse groups of fields without embedding then inside a separate Form/FormController logic. --- .../form/flexible/impl/BasicFormFragment.java | 66 +++++++++ .../flexible/impl/FormBasicController.java | 125 +++++++++++++++++- .../form/flexible/impl/IFormFragment.java | 104 +++++++++++++++ .../flexible/impl/IFormFragmentContainer.java | 74 +++++++++++ .../impl/IFormFragmentController.java | 84 ++++++++++++ .../form/flexible/impl/IFormFragmentHost.java | 58 ++++++++ 6 files changed, 509 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/olat/core/gui/components/form/flexible/impl/BasicFormFragment.java create mode 100644 src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragment.java create mode 100644 src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragmentContainer.java create mode 100644 src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragmentController.java create mode 100644 src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragmentHost.java diff --git a/src/main/java/org/olat/core/gui/components/form/flexible/impl/BasicFormFragment.java b/src/main/java/org/olat/core/gui/components/form/flexible/impl/BasicFormFragment.java new file mode 100644 index 00000000000..ab81a4a8b34 --- /dev/null +++ b/src/main/java/org/olat/core/gui/components/form/flexible/impl/BasicFormFragment.java @@ -0,0 +1,66 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <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 the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <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> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.core.gui.components.form.flexible.impl; + +import org.olat.core.CoreSpringFactory; +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.FormUIFactory; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.WindowControl; +import org.olat.modules.IModuleConfiguration; + +/** + * Base class for implementing {@link IFormFragment} + * + * <p>Initial date: May 6, 2016 + * @author lmihalkovic, http://www.frentix.com + */ +public abstract class BasicFormFragment implements IFormFragment { + + protected IFormFragmentHost host; + protected IFormFragmentContainer container; + + public BasicFormFragment(WindowControl wControl) { + CoreSpringFactory.autowireObject(this); + } + + @Override + public final void initFormFragment(UserRequest ureq, IFormFragmentContainer container, Controller listener, IModuleConfiguration config) { + FormItemContainer formLayout = container.formItemsContainer(); + + this.container = container; + this.host = container.getFragmentHostInterface(); + + initFormFragment(formLayout, listener, ureq, config); + } + + protected abstract void initFormFragment(FormItemContainer formLayout, Controller listener, UserRequest ureq, IModuleConfiguration config); + + protected FormUIFactory uifactory() { + return container.getFragmentHostInterface().getUIFactory(); + } + + @Override + public void readConfiguration(UserRequest ureq, IModuleConfiguration moduleConfiguration) { + // do nothing by default + } + +} diff --git a/src/main/java/org/olat/core/gui/components/form/flexible/impl/FormBasicController.java b/src/main/java/org/olat/core/gui/components/form/flexible/impl/FormBasicController.java index a6d562850bb..574ced700c0 100644 --- a/src/main/java/org/olat/core/gui/components/form/flexible/impl/FormBasicController.java +++ b/src/main/java/org/olat/core/gui/components/form/flexible/impl/FormBasicController.java @@ -25,6 +25,11 @@ */ 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; @@ -67,7 +72,7 @@ import org.olat.core.logging.activity.ThreadLocalUserActivityLoggerInstaller; * * @author patrickb */ -public abstract class FormBasicController extends BasicController { +public abstract class FormBasicController extends BasicController implements IFormFragmentContainer { public static final int LAYOUT_DEFAULT = 0; @@ -345,11 +350,16 @@ public abstract class FormBasicController extends BasicController { * @param source * @param event */ - @SuppressWarnings("unused") 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, @@ -357,6 +367,7 @@ public abstract class FormBasicController extends BasicController { */ @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) { @@ -611,7 +622,117 @@ public abstract class FormBasicController extends BasicController { 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); + } + } + private List<FragmentRef> fragments = new ArrayList<>(); + @Override + public void setNeedsLayout() { + flc.setDirty(true); + } + + @Override + public void registerFormFragment(IFormFragment fragment) { + 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) { + 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) { + // 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) { + // 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; + } + } \ No newline at end of file diff --git a/src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragment.java b/src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragment.java new file mode 100644 index 00000000000..647b54c7170 --- /dev/null +++ b/src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragment.java @@ -0,0 +1,104 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <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 the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <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> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.core.gui.components.form.flexible.impl; + +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.control.Controller; +import org.olat.core.gui.control.Disposable; +import org.olat.core.gui.control.Event; +import org.olat.modules.IModuleConfiguration; + +/** + * A form fragment is a simple abstraction that allows composing {@code Form} instances from + * reusable groups of fields. This is a lighter weight level of reuse than to embed entire + * forms inside containing forms. In the case of fragments, there is a single controller + * managing multiple fragments. + * + * <p>Fragments are hosted by a {@link IFormFragmentContainer}. + * + * <p>Initial date: May 6, 2016<br> + * @author lmihalkovic, http://www.frentix.com + */ +public interface IFormFragment extends Disposable { + + // this is here for compatibility to help migrate chunks of existing code into fragments + default void initForm(IFormFragmentContainer container, Controller _listener, UserRequest ureq, IModuleConfiguration config) { + this.initFormFragment(ureq, container, _listener, config); + this.validateFormLogic(ureq); + } + + /** + * This method must be implemented by fragments however it is not recommended that form + * fragment hosts call it directly. Instead fragment hosts should directly call {@code initForm} + * which will ensure that all initialization is performed in a single step + * + * @param ureq + * @param formLayout + * @param listener + */ + void initFormFragment(UserRequest ureq, IFormFragmentContainer container, Controller listener, IModuleConfiguration config); + + /** + * @see FormBasicController#validateFormLogic(UserRequest) + * @return + */ + boolean validateFormLogic(UserRequest ureq); + + void readConfiguration(UserRequest ureq, IModuleConfiguration moduleConfiguration); + void storeConfiguration(UserRequest ureq, IModuleConfiguration moduleConfiguration); + + default void refreshContents() { + // do nothing by default + } + + /** + * @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest, + * org.olat.core.gui.control.Controller, + * org.olat.core.gui.control.Event) + * @return true if the event was processed, false otherwise + */ + default boolean processEvent(UserRequest ureq, Controller source, Event event) { + return false; + } + + /** + * @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) + * @return true if the event was processed, false otherwise + */ + default boolean processEvent(UserRequest ureq, Component source, Event event) { + return false; + } + + /** + * called if an element inside of form triggered an event + * + * @param source + * @param event + * @return true if the event was processed, false otherwise + */ + default boolean processFormEvent(UserRequest ureq, FormItem source, FormEvent event) { + return false; + } + +} diff --git a/src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragmentContainer.java b/src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragmentContainer.java new file mode 100644 index 00000000000..e66d1507071 --- /dev/null +++ b/src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragmentContainer.java @@ -0,0 +1,74 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <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 the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <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> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.core.gui.components.form.flexible.impl; + +import java.util.function.Consumer; + +import org.olat.core.gui.components.form.flexible.FormItemContainer; + +/** + * Interface implemented by a visual element (usually a Form controller) that can contain + * one or more {@link IFormFragment} instances + * + * <p>Initial date: May 6, 2016<br> + * @author lmihalkovic, http://www.frentix.com + */ +public interface IFormFragmentContainer { + + /** + * Return an instance of the interface used + * @return + */ + default IFormFragmentHost getFragmentHostInterface() { + return null; + } + + /** + * Called by a hosted fragment to tell its container that the current presentation + * no longer accurately reflects the current internal state of the fragment. The + * container would generally use this information to trigger a re-evaluation of its + * layout + */ + void setNeedsLayout(); + + /** + * Used by the fragment in order to add contents to the visual presentation of the + * container + * + * @return + */ + FormItemContainer formItemsContainer(); + + /** + * Used for registering the existance of a fragment with the underlying form controller + * + * @param fragment + */ + void registerFormFragment(IFormFragment fragment); + + /** + * This method can be used to perform an operation on all the fragments currently hosted + * by this container + * + * @param handler + */ + void forEachFragment(Consumer<IFormFragment> handler); + +} diff --git a/src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragmentController.java b/src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragmentController.java new file mode 100644 index 00000000000..b0fa7d7b88e --- /dev/null +++ b/src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragmentController.java @@ -0,0 +1,84 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <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 the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <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> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.core.gui.components.form.flexible.impl; + +import java.util.function.Consumer; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.Event; +import org.olat.core.gui.control.WindowControl; + +/** + * A {@link IFormFragment} requires access to a controller. This interface represents the very + * minimal set of features it expects from its controller. This subset corresponds strictly with + * the features offered by a {@link FormBasicController} and in most cases the fragment controller + * will be the form controller itself. Nonetheless this interface is here to guaranty that a + * fragment will be truly reusable by isolating it from the internal details of more powerful + * form controller. + * + * <p>Initial date: May 6, 2016<br> + * @author lmihalkovic, http://www.frentix.com + */ +public interface IFormFragmentController { + void removeAsListenerAndDispose(Controller controller); + WindowControl getWindowControl(); + void listenTo(Controller controller); + void setFormCanSubmit(boolean canSubmit); + void fireEvent(UserRequest ureq, Event event); + + + /** + * A helper method to help adapt an existing {@link FormBasicController} instance to + * the subset of features exposed by a {@link IFormFragmentController}. + * + * @param delegate the form controller to be adapted + * @param canSubmitHandler a handler to be invoked when the fragment wants to change the overall readiness state of the form + * @return + */ + public static IFormFragmentController fragmentControllerAdapter(final FormBasicController delegate, final Consumer<Boolean> canSubmitHandler) { + return new IFormFragmentController() { + @Override + public void setFormCanSubmit(boolean canSubmit) { + canSubmitHandler.accept(canSubmit); + } + + @Override + public void removeAsListenerAndDispose(Controller controller) { + delegate.removeAsListenerAndDispose(controller); + } + + @Override + public void listenTo(Controller controller) { + delegate.listenTo(controller); + } + + @Override + public WindowControl getWindowControl() { + return delegate.getWindowControlForDebug(); + } + + @Override + public void fireEvent(UserRequest ureq, Event event) { + delegate.fireEvent(ureq, event); + } + }; + } +} diff --git a/src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragmentHost.java b/src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragmentHost.java new file mode 100644 index 00000000000..e678e22130f --- /dev/null +++ b/src/main/java/org/olat/core/gui/components/form/flexible/impl/IFormFragmentHost.java @@ -0,0 +1,58 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <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 the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <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> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.core.gui.components.form.flexible.impl; + +import org.olat.core.gui.components.form.flexible.FormUIFactory; +import org.olat.core.gui.translator.Translator; + +/** + * The interface implemented by a {@link IFormFragmentContainer} to provide the {@link IFormFragment} + * with controller over its runtime behavior. + * + * <p>Initial date: May 6, 2016<br> + * @author lmihalkovic, http://www.frentix.com + */ +public interface IFormFragmentHost { + + /** + * Return a {@link Translator} instance that can provide a translation specific to where the + * fragment is used, or a default translation associated with the fragment itself. + * + * @return + */ + Translator getFragmentTranslator(); + + /** + * Return a {@link FormUIFactory} instance that may have been tweaked to enforce certain + * behavior specific to where the fragment is being embedded + * @return + */ + default FormUIFactory getUIFactory() { + return FormUIFactory.getInstance(); + } + + /** + * Return the controller instance to be used by the fragment for event handling + * + * @return + */ + IFormFragmentController getFragmentController(); + +} -- GitLab