/**
* <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.control.generic.layout;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.olat.core.commons.fullWebApp.LayoutMain3ColsController;
import org.olat.core.extensions.ExtManager;
import org.olat.core.extensions.Extension;
import org.olat.core.extensions.action.ActionExtension;
import org.olat.core.extensions.action.GenericActionExtension;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.Component;
import org.olat.core.gui.components.panel.Panel;
import org.olat.core.gui.components.stack.BreadcrumbPanel;
import org.olat.core.gui.components.stack.BreadcrumbPanelAware;
import org.olat.core.gui.components.stack.BreadcrumbedStackedPanel;
import org.olat.core.gui.components.tree.GenericTreeModel;
import org.olat.core.gui.components.tree.GenericTreeNode;
import org.olat.core.gui.components.tree.MenuTree;
import org.olat.core.gui.components.tree.TreeEvent;
import org.olat.core.gui.components.tree.TreeModel;
import org.olat.core.gui.components.tree.TreeNode;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.controller.MainLayoutBasicController;
import org.olat.core.gui.control.generic.dtabs.Activateable2;
import org.olat.core.id.OLATResourceable;
import org.olat.core.id.context.ContextEntry;
import org.olat.core.id.context.StateEntry;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
import org.olat.core.util.CodeHelper;
import org.olat.core.util.StringHelper;
import org.olat.core.util.resource.OresHelper;
import org.olat.util.logging.activity.LoggingResourceable;
/**
* Description:<br>
* This generic Controller gets menu-items configured for a site If any other
* than configured (spring: olat_extensions.xml) items need to be in menu, use
* addChildNodeToAppend/Prepend() before init(). init() needs to be called to
* put content to panel.
* <P>
* Initial Date: 02.07.2008 <br>
*
* @author Roman Haag, frentix GmbH, roman.haag@frentix.com
* @author patrickb, www.uzh.ch, slightly changed to allow specialised forms of
* GenericActionExtension
*/
public abstract class GenericMainController extends MainLayoutBasicController {
private static final String GMCMT = "GMCMenuTree";
private MenuTree olatMenuTree;
private Panel content;
private BreadcrumbPanel stackVC;
private LayoutMain3ColsController columnLayoutCtr;
private Controller contentCtr;
private final List<GenericTreeNode> nodesToAppend;
private final List<GenericTreeNode> nodesToPrepend;
private final String className;
public GenericMainController(UserRequest ureq, WindowControl wControl) {
super(ureq, wControl);
nodesToAppend = new ArrayList<GenericTreeNode>();
nodesToPrepend = new ArrayList<GenericTreeNode>();
className = this.getClass().getName();
}
/**
* use after optional addChildNodeToAppend() or addChildNodeToPrepend() calls
* to initialize MainController and set Panel
*
* @param ureq
*/
public void init(UserRequest ureq) {
olatMenuTree = new MenuTree("olatMenuTree");
TreeModel tm = buildTreeModel(ureq);
olatMenuTree.setTreeModel(tm);
content = new Panel("content");
TreeNode firstNode = tm.getRootNode();
TreeNode nodeToSelect = getLastDelegate(firstNode);
olatMenuTree.setSelectedNodeId(nodeToSelect.getIdent());
olatMenuTree.addListener(this);
// default is to not display the root element and to let user open/close sub elements
olatMenuTree.setRootVisible(false);
olatMenuTree.setExpandSelectedNode(false);
Object uobject = nodeToSelect.getUserObject();
contentCtr = getContentCtr(uobject, ureq);
listenTo(contentCtr); // auto dispose later
Component resComp = contentCtr.getInitialComponent();
content.setContent(resComp);
columnLayoutCtr = new LayoutMain3ColsController(ureq, getWindowControl(), olatMenuTree, content, className);
listenTo(columnLayoutCtr); // auto dispose later
//create the stack
stackVC = new BreadcrumbedStackedPanel("genericStack", getTranslator(), this);
stackVC.pushController("content", columnLayoutCtr);
putInitialPanel(stackVC);
}
/**
* get the last delegate of a TreeNode.
*
* @param node
* @return the first treeNode in the hierarchy -under the given node- that
* does not have a delegate
*/
private TreeNode getLastDelegate(TreeNode node) {
if (node.getDelegate() == null) return node;
return getLastDelegate(node.getDelegate());
}
protected void addCssClassToMain(String cssClass) {
columnLayoutCtr.addCssClassToMain(cssClass);
}
/**
* build a node before with sth. like: GenericTreeNode gtnA = new
* GenericTreeNode(); gtnA.setTitle("appended"); //or with translate
* gtnA.setAltText("alternative text"); gtnA.setUserObject("identifier or
* object to use"); then add it with this method
*
* @param nodeToAppend node to add, besides the configured ones
*/
public void addChildNodeToAppend(GenericTreeNode nodeToAppend) {
checkNodeToAdd(nodeToAppend);
nodesToAppend.add(nodeToAppend);
}
/**
* build a node before with sth. like: GenericTreeNode gtnA = new
* GenericTreeNode(); gtnA.setTitle("appended"); //or with translate
* gtnA.setAltText("alternative text"); gtnA.setUserObject("identifier or
* object to use"); then add it with this method
*
* @param nodeToPrepend node to add, besides the configured ones
*/
public void addChildNodeToPrepend(GenericTreeNode nodeToPrepend) {
checkNodeToAdd(nodeToPrepend);
nodesToPrepend.add(nodeToPrepend);
}
/**
* checks if a userObject has been set, is needed to find the required
* controller
*
* @param node
*/
private void checkNodeToAdd(GenericTreeNode node) {
if (node.getUserObject() == null) { throw new AssertException(
"GenericTreeNode to append/prepend needs to have a UserObject set! Please use setUserObject()."); }
}
private TreeModel buildTreeModel(UserRequest ureq) {
GenericTreeNode rootTreeNode = new GenericTreeNode();
rootTreeNode.setTitle(getTranslator().translate("main.menu.title"));
rootTreeNode.setAltText(getTranslator().translate("main.menu.title.alt"));
GenericTreeModel gtm = new GenericTreeModel();
gtm.setRootNode(rootTreeNode);
// Prepend
boolean rootNodeSet = false;
if (nodesToPrepend.size() != 0) {
for (GenericTreeNode node : nodesToPrepend) {
rootTreeNode.addChild(node);
if (!rootNodeSet) {
rootTreeNode.setDelegate(node);
rootTreeNode.setUserObject(node.getUserObject());
rootNodeSet = true;
}
}
}
// add extension menues
ExtManager extm = ExtManager.getInstance();
int j = 0;
GenericTreeNode gtnChild;
Map<GenericTreeNode, String> subMenuNodes = new LinkedHashMap<GenericTreeNode, String>();
for (Extension anExt : extm.getExtensions()) {
// check for sites
ActionExtension ae = (ActionExtension) anExt.getExtensionFor(className, ureq);
if (ae != null && ae instanceof GenericActionExtension) {
if(anExt.isEnabled()){
GenericActionExtension gAe = (GenericActionExtension) ae;
gtnChild = gAe.createMenuNode(ureq);
if(StringHelper.containsNonWhitespace(gAe.getNavigationKey())) {
gtnChild.setCssClass("o_sel_" + gAe.getNavigationKey());
}
if (gAe.getNodeIdentifierIfParent() != null) {
// it's a parent-node, set identifier
gtnChild.setIdent(gAe.getNodeIdentifierIfParent());
}
if (j == 0 && !rootNodeSet) {
// first node, set as delegate of rootTreenode
rootTreeNode.setDelegate(gtnChild);
rootTreeNode.setUserObject(gAe);
rootTreeNode.addChild(gtnChild);
}
// fixdiff FXOLAT-250 :: make genericMainController aware of multi-level
// navigation (submenues)
else if (gAe.getParentTreeNodeIdentifier() != null) {
// this is a sub-menu-node, do not add to tree-model already, since
// parent tree may not yet be in model
// (parent could be "after" child, in ActionExtensions-Collection)
String parentNodeID = gAe.getParentTreeNodeIdentifier();
subMenuNodes.put(gtnChild, parentNodeID);
}
// "normal" menu-entry
else {
rootTreeNode.addChild(gtnChild);
}
j++;
}else{
logInfo("found disabled GenericActionExtension for "+className+" ", ae.toString());
}
}
}// loop over extensions
// fixdiff FXOLAT-250 :: make genericMainController aware of multi-level
// navigation (submenues)
// loop over submenuNodes and add to their parents
for (Entry<GenericTreeNode, String> childNodeEntry : subMenuNodes.entrySet()) {
GenericTreeNode childNode = childNodeEntry.getKey();
GenericTreeNode parentNode = (GenericTreeNode) gtm.getNodeById(childNodeEntry.getValue());
if (parentNode != null) {
parentNode.addChild(childNode);
if (parentNode.getDelegate() == null ) {
boolean addDelegate = true;
//add delegate only if hte parent hasn't not a controller defined
Object uo = parentNode.getUserObject();
if(uo instanceof GenericActionExtension) {
GenericActionExtension gae = (GenericActionExtension)uo;
if(StringHelper.containsNonWhitespace(gae.getClassNameOfCorrespondingController())) {
addDelegate = false;
}
}
if(addDelegate) {
parentNode.setDelegate(childNode);
parentNode.setUserObject(childNode.getUserObject());
}
}
} else {
logWarn("Could not add navigation-menu (" + childNode.getTitle() + ") to parent:: " + childNodeEntry.getValue(), null);
// make it at least appear on top level
rootTreeNode.addChild(childNode);
}
}
// Append
if (nodesToAppend.size() != 0) {
for (GenericTreeNode node : nodesToAppend) {
rootTreeNode.addChild(node);
}
}
return gtm;
}
@Override
protected void event(UserRequest ureq, Component source, Event event) {
if (source == olatMenuTree) {
if (event instanceof TreeEvent && event.getCommand().equals(MenuTree.COMMAND_TREENODE_CLICKED)) {
TreeEvent te = (TreeEvent)event;
if(te.getSubCommand() != null) {
// filter open/close events
} else {
// process menu commands
TreeNode selTreeNode = olatMenuTree.getSelectedNode();
// cleanup old content controller (never null)
removeAsListenerAndDispose(contentCtr);
// create new content controller
// Following cases:
// 1a) Simple Action Extension using only ureq and windowControl ->
// handled by default implementation of createController
// 1b) Specialised Action Extension which needs some more internals ->
// handled by the class extending GenericMainController, by overwriting
// createController
// 2) uobject is something special which needs evaluation by class
// extending GenericMainController
Object uobject = selTreeNode.getUserObject();
TreeNode delegatee = selTreeNode.getDelegate();
if (delegatee != null) {
olatMenuTree.setSelectedNode(delegatee);
}
contentCtr = getContentCtr(uobject, ureq);
listenTo(contentCtr);
Component resComp = contentCtr.getInitialComponent();
content.setContent(resComp);
addToHistory(ureq, contentCtr);
}
} else { // the action was not allowed anymore
content.setContent(null); // display an empty field (empty panel)
}
} else {
logWarn("Unhandled olatMenuTree event: " + event.getCommand(), null);
}
}
/**
* needs to be implemented to return corresponding controller for menu items
* which were not generated by a GenericActionExtension
*
* @param uobject
* @param ureq
* @return corresponding controller to be opened by click to menu-item / null
* if none has been defined
*/
protected abstract Controller handleOwnMenuTreeEvent(Object uobject, UserRequest ureq);
/**
* creates Controller for clicked Node, default implementation.
*
* @param ae
* @param ureq
* @return corresponding controller
*/
protected Controller createController(ActionExtension ae, UserRequest ureq) {
// default implementation for simple case where action extension.
// fxdiff BAKS-7 Resume function
WindowControl bwControl = getWindowControl();
if (olatMenuTree.getTreeModel() instanceof GenericTreeModel) {
if (ae instanceof Extension) {
Extension nE = (Extension) ae;
// get our ores for the extension
OLATResourceable ores;
if (ae instanceof GenericActionExtension && StringHelper.containsNonWhitespace(((GenericActionExtension) ae).getNavigationKey())) {
// there is a navigation-key, use the nice way
ores = OresHelper.createOLATResourceableInstance(((GenericActionExtension) ae).getNavigationKey(), 0L);
} else {
ores = OresHelper.createOLATResourceableInstance(GMCMT, CodeHelper.getUniqueIDFromString(nE.getUniqueExtensionID()));
}
ThreadLocalUserActivityLogger.addLoggingResourceInfo(LoggingResourceable.wrapBusinessPath(ores));
bwControl = addToHistory(ureq, ores, null);
}
}
Controller ctrl = ae.createController(ureq, bwControl, null);
if(ctrl instanceof BreadcrumbPanelAware) {
((BreadcrumbPanelAware)ctrl).setBreadcrumbPanel(stackVC);
}
return ctrl;
}
private Controller getContentCtr(Object uobject, UserRequest ureq) {
Controller contentCtr1Tmp = null;
if (uobject instanceof ActionExtension) {
ActionExtension ae = (ActionExtension) uobject;
contentCtr1Tmp = createController(ae, ureq);
} else {
contentCtr1Tmp = handleOwnMenuTreeEvent(uobject, ureq);
}
if (contentCtr1Tmp == null) { throw new AssertException(
"Node must either be an ActionExtension or implementation must handle this MenuTreeEvent: " + (uobject == null ? "NULL" : uobject.toString())); }
return contentCtr1Tmp;
}
/**
* activates the correct treenode for a given ActionExtension
* @param ureq
* @param ae
*/
private void activateTreeNodeByActionExtension(UserRequest ureq, ActionExtension ae) {
TreeNode node = ((GenericTreeModel) olatMenuTree.getTreeModel()).findNodeByUserObject(ae);
if (node != null) {
olatMenuTree.setSelectedNodeId(node.getIdent());
TreeEvent te = new TreeEvent(MenuTree.COMMAND_TREENODE_CLICKED, node.getIdent());
event(ureq, olatMenuTree, te);
}
}
// fxdiff BAKS-7 Resume function
private void activate(UserRequest ureq, String viewIdentifier) {
ActionExtension ae;
if (viewIdentifier != null && viewIdentifier.startsWith(GMCMT)) {
Long extensionID = Long.parseLong(viewIdentifier.substring(viewIdentifier.indexOf(':') + 1));
Extension ee = ExtManager.getInstance().getExtensionByID(extensionID);
if(ee == null){
logWarn("ExtManager did not find an Extension for extensionID '"+extensionID+"'. Activate canceled..." , null);
return;
}
ae = (ActionExtension) ee.getExtensionFor(className, ureq);
} else {
int vwindex = viewIdentifier.lastIndexOf(":");
String naviKey = viewIdentifier;
if(vwindex >= 0){
naviKey = viewIdentifier.substring(0,viewIdentifier.indexOf(':'));
}
ae = ExtManager.getInstance().getActionExtensioByNavigationKey(className, naviKey);
if(ae == null){
// this happens, if someone uses a navigation key, that no actionExtension uses...
logWarn("couldn't find an ActionExtension for navigationKey '"+naviKey+"' . I suggest adjusting spring configuration for GenericMainController.." , null);
}
}
if(ae == null){
// no action extension to activate...
return;
}
try {
if (olatMenuTree.getTreeModel() instanceof GenericTreeModel) {
activateTreeNodeByActionExtension(ureq, ae);
} else {
// just for precaution (treenode selection won't work, but correct
// content is displayed)
contentCtr = getContentCtr(ae, ureq);
listenTo(contentCtr);
Component resComp = contentCtr.getInitialComponent();
content.setContent(resComp);
// fxdiff BAKS-7 Resume function
addToHistory(ureq, contentCtr);
}
} catch (Exception e) {
logWarn("", e);
}
}
protected void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) {
if (entries == null || entries.isEmpty()) return;
ContextEntry entry = entries.get(0);
TreeNode selectedNode = getMenuTree().getSelectedNode();
String node = entry.getOLATResourceable().getResourceableTypeName();
if (node != null && node.startsWith(GMCMT)) {
activate(ureq, node + ":" + entries.get(0).getOLATResourceable().getResourceableId());
if (entries.size() >= 1) {
entries = entries.subList(1, entries.size());
}
if (contentCtr instanceof Activateable2) {
((Activateable2)contentCtr).activate(ureq, entries, entry.getTransientState());
}
} else {
// maybe the node is a GAE-NavigationKey ?
GenericActionExtension gAE = ExtManager.getInstance().getActionExtensioByNavigationKey(className, node);
if (gAE != null) {
//if the controller is already selected, only activate it, don't reinstanciate it
if(selectedNode != null && selectedNode.getUserObject() != gAE) {
activateTreeNodeByActionExtension(ureq, gAE);
}
if (entries.size() >= 1) {
entries = entries.subList(1, entries.size());
}
if (contentCtr instanceof Activateable2) {
((Activateable2) contentCtr).activate(ureq, entries, entry.getTransientState());
}
}
}
}
@Override
protected void doDispose() {
// nothing to do
}
public MenuTree getMenuTree() {
return olatMenuTree;
}
}