package org.fenixedu.bennu.portal.domain;
import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Stream;
import org.fenixedu.bennu.core.domain.User;
import org.fenixedu.bennu.core.groups.Group;
import org.fenixedu.bennu.portal.model.Application;
import org.fenixedu.bennu.portal.model.Functionality;
import org.fenixedu.commons.i18n.LocalizedString;
import pt.ist.fenixframework.Atomic;
/**
* A {@link MenuContainer} represents an inner node in the functionality tree. It may hold {@link MenuFunctionality}s or other
* containers, forming a tree-like structure.
*
* @author João Carvalho (joao.pedro.carvalho@tecnico.ulisboa.pt)
*
*/
public final class MenuContainer extends MenuContainer_Base {
/**
* Used to create the {@link MenuContainer} that will represent the root of the
* functionality tree.
*
* @param configuration
* The {@link PortalConfiguration} in which the functionality tree will be installed.
*/
MenuContainer(PortalConfiguration configuration) {
super();
if (configuration.getMenu() != null && configuration.getMenu() != this) {
throw new RuntimeException("There can be only one root menu!");
}
setConfiguration(configuration);
init(null, false, "anyone", new LocalizedString(), new LocalizedString(), "ROOT_PATH");
}
/**
* Creates a new {@link MenuContainer} based on the given {@link Application}, and inserts it under the given parent.
*
* @param parent
* The parent container for the new item
* @param application
* The {@link Application} that will provide the require information for this container
*
* @throws IllegalArgumentException
* If {@code parent} is null.
*/
public MenuContainer(MenuContainer parent, Application application) {
super();
if (parent == null) {
throw new IllegalArgumentException("MenuContainer requires a parent Container!");
}
init(parent, true, application.getAccessGroup(), application.getTitle(), application.getDescription(),
application.getPath());
for (Functionality functionality : application.getFunctionalities()) {
new MenuFunctionality(this, functionality);
}
}
/**
* Creates a new {@link MenuContainer} based on the provider parameters, and inserts it under the given parent.
*
* @param parent
* The parent container for the new item
* @param visible
* Whether this container is to be visible when rendering the menu
* @param accessGroup
* The expression for this container's access group
* @param description
* The textual description for this container
* @param title
* The title for this container
* @param path
* The semantic-url path for this container
*
* @throws IllegalArgumentException
* If {@code parent} is null.
*/
public MenuContainer(MenuContainer parent, boolean visible, String accessGroup, LocalizedString description,
LocalizedString title, String path) {
super();
if (parent == null) {
throw new IllegalArgumentException("MenuFunctionality cannot be created without a parent!");
}
init(parent, visible, accessGroup, title, description, path);
}
/*
* Creates a new copy of this container. It does NOT replicate the container's children.
*/
private MenuContainer(MenuContainer parent, MenuContainer original) {
super();
if (parent == null) {
throw new IllegalArgumentException("MenuFunctionality cannot be created without a parent!");
}
init(parent, original);
}
/**
* Adds a given {@link MenuItem} as the last child of this container.
*
* @param child
* The child element to add to this container
*
* @throws BennuPortalDomainException
* If another child with the same path already exists
*/
@Override
public void addChild(MenuItem child) throws BennuPortalDomainException {
addChild(child, getNextOrder());
}
private Integer getNextOrder() {
return getChildSet().size() + 1;
}
/**
* Adds a given {@link MenuItem} as a child of this container, in the given position.
*
* @param child
* The child element to add to this container
* @param order
* The position this item will be inserted at
*
* @throws BennuPortalDomainException
* If another child with the same path already exists
*/
public void addChild(MenuItem child, Integer order) throws BennuPortalDomainException {
child.setOrd(order);
for (MenuItem existing : getChildSet()) {
if (existing.getPath().equals(child.getPath())) {
throw BennuPortalDomainException.childWithPathAlreadyExists(child.getPath());
}
}
super.addChild(child);
}
/**
* Returns the children of this container, sorted by their order
*
* @return
* The ordered children of this container
*/
public Set<MenuItem> getOrderedChild() {
return Collections.unmodifiableSet(new TreeSet<>(getChildSet()));
}
/**
* Returns the User Menu as a lazy {@link Stream}. This method is preferred
* to the alternatives (returning {@link Set}), as it allows further optimizations.
*
* @return
* The User's Menu as a Stream
*/
public Stream<MenuItem> getUserMenuStream() {
return getChildSet().stream().filter((item) -> item.isVisible() && item.isItemAvailableForCurrentUser()).sorted();
}
/**
* Deletes this container, as well as all its children.
*/
@Atomic
@Override
public void delete() {
if (getConfiguration() != null) {
throw BennuPortalDomainException.cannotDeleteRootContainer();
}
setConfigurationFromSubRoot(null);
for (MenuItem child : getChildSet()) {
child.delete();
}
super.delete();
}
/**
* Returns whether this container is the root of the functionality tree.
*
* @return
* {@code true} if this is the root container.
*/
public boolean isRoot() {
return getConfiguration() != null;
}
public String resolveLayout() {
if (getConfiguration() != null) {
return "default";
}
if (getLayout() != null) {
return getLayout();
}
return getParent().resolveLayout();
}
@Override
public boolean isAvailable(User user) {
return isRoot() ? true : super.isAvailable(user);
}
/**
* Traverses the functionality tree looking for the branch that matches the given path.
* Lookup starts with the children of the current Container.
*
* Note that this method is aware of the availability of tree branches, meaning that if
* the current user doesn't have access to the selected functionality, this method will return null.
*
* @param parts
* The path to be looked up
* @return
* The {@link MenuFunctionality} matching the given path, or null if no functionality matches or the user has no
* access.
*/
public final MenuFunctionality findFunctionalityWithPath(String[] parts) {
return findFunctionalityWithPath(parts, 0);
}
/**
* Helper method that receives the array index to look up, thus avoiding creation of helper objects (such as lists).
*
* The functionality of this method is as follows:
*
* <ol>
* <li>
* If there are no more elements in the path, find the initial content ({@link #findInitialContent()}) of this
* {@link MenuContainer}</li>
*
* <li>
* For each child of this container:
* <ol>
* <li>It checks if the child is available</li>
* <li>It checks if the child matches the given part of the path</li>
* <li>If both of the above are true:
*
* <ul>
* <li>If the selected item is a functionality, return it. Note that it is possible that this element is not the last in the
* path, allowing custom processing.</li>
* <li>
* If the selected item if a container, continue searching, looking for the next element in the path</li>
* </ul>
* </li>
* </ol>
* </li>
* <li>
* If no item was found, return {@code null}.</li>
* </ol>
*/
private final MenuFunctionality findFunctionalityWithPath(String[] parts, int startIndex) {
// 1)
if (parts.length == startIndex) {
return findInitialContent();
}
// 2)
for (MenuItem child : getChildSet()) {
if (child.getPath().equals(parts[startIndex]) && child.isItemAvailableForCurrentUser()) {
if (child.isMenuFunctionality()) {
return child.getAsMenuFunctionality();
} else {
return child.getAsMenuContainer().findFunctionalityWithPath(parts, startIndex + 1);
}
}
}
// 3)
return null;
}
/**
* Returns the initial content of this container, i.e., the first {@link MenuFunctionality}.
*
* @return
* The initial content of this container
*/
public MenuFunctionality findInitialContent() {
for (MenuItem item : getOrderedChild()) {
if (item.isItemAvailableForCurrentUser()) {
if (item.isMenuFunctionality()) {
return item.getAsMenuFunctionality();
} else {
MenuFunctionality functionality = item.getAsMenuContainer().findInitialContent();
if (functionality != null) {
return functionality;
}
}
}
}
return null;
}
/**
* "Moves" this container to the selected point in the menu.
*
* As {@link MenuItem}s are immutable by default, this instance is deleted, and
* a new copy of it is created under the specified container. This process is
* recursive, meaning that all the current children will be replicated as well.
*
* @param newParent
* The new parent in which to insert the copy of this container
* @return
* The newly created container
*/
@Override
public MenuContainer moveTo(MenuContainer newParent) {
MenuContainer copy = new MenuContainer(newParent, this);
for (MenuItem item : getChildSet()) {
item.moveTo(copy);
}
super.delete();
return copy;
}
public boolean isSubRoot() {
return getConfigurationFromSubRoot() != null;
}
@Override
public boolean isVisible() {
return !isSubRoot() && super.isVisible();
}
public static MenuContainer createSubRoot(String key, LocalizedString title, LocalizedString description) {
MenuContainer container =
new MenuContainer(PortalConfiguration.getInstance().getMenu(), false, Group.anyone().getExpression(),
description, title, key);
PortalConfiguration.getInstance().addSubRoot(container);
return container;
}
}