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