Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.foreach.across.modules.web.menu.Menu Maven / Gradle / Ivy
Go to download
Across is a Java framework that aims to facilitate module based development for Java (web) applications.
It builds heavily on Spring framework and allows defining a module consisting of a number of classes and
configuration files. Every module defines its own Spring application context and can share one or more beans
with other modules.
/*
* Copyright 2014 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.foreach.across.modules.web.menu;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.Ordered;
import java.util.*;
/**
* Represents a hierarchical menu (tree) structure.
* All items in the menu can be sorted using a Comparator, the same Comparator can be applied
* to all submenus in the tree. By default no comparator is attached and items will be sorted
* according to the natural order of their title.
* Alternatively a Menu can be set as Ordered, in which case sorting will have no effect. The Ordered
* property must be set on all submenus separately, as it is not inherited. This means it is possible
* to provide an inheritable Comparator on the menu, but sort the menu itself manually and only use
* the Comparator for all submenus.
* Note that sorting needs to be done explicitly, see {@link #sort()} method.
*/
public class Menu implements Ordered
{
public static final int ROOT_LEVEL = 0;
public static final Comparator SORT_BY_TITLE = new Comparator()
{
public int compare( Menu o1, Menu o2 ) {
String mine = StringUtils.defaultString( o1.getTitle() );
return mine.compareTo( StringUtils.defaultString( o2.getTitle() ) );
}
};
public static final Comparator SORT_BY_ORDER_AND_TITLE = new OrderComparatorWrapper( SORT_BY_TITLE );
private int order = Ordered.LOWEST_PRECEDENCE - 1000;
private boolean ordered, selected, group, disabled;
private String name, path, title, url;
@SuppressWarnings("all")
private LinkedList items = new LinkedList<>();
private List readonlyItems = Collections.unmodifiableList( items );
private Comparator comparator = null;
private boolean comparatorInheritable = false;
private Menu parent;
private Map attributes = new HashMap<>();
public Menu() {
this.path = "";
}
public Menu( String name ) {
this();
this.name = name;
setPath( name );
}
public Menu( String path, String title ) {
this( path );
setTitle( title );
}
/**
* Copy constructor.
*
* @param original
*/
public Menu( Menu original ) {
merge( original, false );
}
void setParent( Menu parent ) {
this.parent = parent;
}
/**
* @return The direct parent of this menu item or null if it is the root of the tree.
*/
public Menu getParent() {
return parent;
}
/**
* @return The root of the menu tree this item belongs to.
*/
public Menu getRoot() {
return parent != null ? parent.getRoot() : this;
}
/**
* @return True if this menu item has a parent menu item, false if it is the root.
*/
public boolean hasParent() {
return parent != null;
}
/**
* @return True if this menu item is the root of the tree.
*/
public boolean isRoot() {
return !hasParent();
}
/**
* @return The level this item is currently at, level 0 (Menu.ROOT_LEVEL) means it is the root of the menu tree.
*/
public int getLevel() {
int level = 0;
Menu item = this;
while ( item.hasParent() ) {
item = item.getParent();
level++;
}
return level;
}
/**
* @return True if this Menu is in fact a group or items, but should not be treated as a single item in itself.
*/
public boolean isGroup() {
return group;
}
public void setGroup( boolean group ) {
this.group = group;
}
/**
* @return True if this Menu should be treated as disabled.
*/
public boolean isDisabled() {
return disabled;
}
public void setDisabled( boolean disabled ) {
this.disabled = disabled;
}
public String getName() {
return name;
}
public void setName( String name ) {
this.name = name;
}
public boolean isOrdered() {
return ordered;
}
/**
* Determines if the menu is already ordered. If true, calls to sort() will have no effect on the menu.
* The ordered flag is not inheritable but must be set on every Menu explicitly.
*
* @param ordered True if the menu is already ordered.
*/
public void setOrdered( boolean ordered ) {
this.ordered = ordered;
}
/**
* Set the comparator to be used when sorting this Menu. If the comparator is inheritable, it
* will also be used for all submenus in this tree that do not have another comparator set explicitly.
*
* @param comparator Comparator instance.
* @param inheritable True if the comparator should also be used for all submenus.
*/
public void setComparator( Comparator comparator, boolean inheritable ) {
this.comparator = comparator;
comparatorInheritable = inheritable;
}
/**
* @return The comparator instance or null if none attached.
*/
public Comparator getComparator() {
return comparator;
}
/**
* @return True if the comparator will be inherited by submenus. False if no comparator specified
* or it cannot be inherited.
*/
public boolean isComparatorInheritable() {
return comparatorInheritable;
}
public String getPath() {
return path;
}
public void setPath( String path ) {
this.path = path;
}
public String getUrl() {
return hasUrl() ? url : path;
}
public void setUrl( String url ) {
this.url = url;
}
public String getTitle() {
return title;
}
public boolean hasUrl() {
return StringUtils.isNotEmpty( url );
}
public void setTitle( String title ) {
this.title = title;
}
public boolean hasTitle() {
return !StringUtils.isBlank( title );
}
public Map getAttributes() {
return attributes;
}
public void setAttributes( Map attributes ) {
this.attributes = attributes;
}
public void setAttribute( String name, Object value ) {
attributes.put( name, value );
}
@SuppressWarnings("unchecked")
public T getAttribute( String name ) {
return (T) attributes.get( name );
}
public boolean hasAttribute( String name ) {
return attributes.containsKey( name );
}
/**
* @return True if this Menu is selected (can be the lowest selected item or not).
*/
public boolean isSelected() {
return selected;
}
/**
* @return The explicit order assigned to the menu item.
*/
public int getOrder() {
return order;
}
/**
* Assign an explicit order to the menu item. Depending on the comparator this property will be used.
* By default the menu is sorted first on order value and second on name.
*
* @param order Explicit order to assign.
*/
public void setOrder( int order ) {
this.order = order;
}
/**
* Set the selected status of the menu item (and its parents). Selecting a child will automatically
* select its parents. Deselecting it will not deselect its parents, but deselecting a parent will
* deselect the children.
*
* @param selected True if the menu item and its parents should be selected.
*/
public void setSelected( boolean selected ) {
if ( selected && hasParent() ) {
getRoot().setSelected( false );
getParent().setSelected( true );
}
else if ( !selected && isSelected() && hasItems() ) {
for ( Menu item : items ) {
item.setSelected( false );
}
}
this.selected = selected;
}
/**
* Returns the selected direct child of this menu. Will return null if {@link #isSelected()} returns false.
*
* @return Menu or null if none selected.
*/
public Menu getSelectedItem() {
for ( Menu item : items ) {
if ( item.isSelected() ) {
return item;
}
}
return null;
}
/**
* Returns all selected items (including the menu itself) in top-down order.
*
* @return Menu items or empty collection if none selected.
*/
@SuppressWarnings("all")
public List getSelectedItemPath() {
LinkedList selectedItems = new LinkedList<>();
Menu item = getLowestSelectedItem();
// Move up but not past ourselves
while ( item != null && item != this ) {
selectedItems.addFirst( item );
item = item.getParent();
}
if ( isSelected() ) {
selectedItems.addFirst( this );
}
return selectedItems;
}
/**
* Returns the lowest selected item of this menu tree. Will return null if {@link #isSelected()} returns false.
*
* @return Menu or null if none selected.
*/
public Menu getLowestSelectedItem() {
Menu item = getSelectedItem();
if ( item != null && item.hasItems() ) {
Menu child = item.getLowestSelectedItem();
if ( child != null ) {
item = child;
}
}
return item;
}
/**
* Fetches the item that matches the given MenuSelector.
*
* @param selector MenuSelector the item should match.
* @return Menu instance or null if not found.
*/
public Menu getItem( MenuSelector selector ) {
if ( selector != null ) {
return selector.find( this );
}
return null;
}
/**
* Sets the item matching the MenuSelector as as selected.
*
* @param selector MenuSelector the item should match.
* @return True if an item was selected.
*/
public boolean select( MenuSelector selector ) {
Menu item = getItem( selector );
if ( item != null ) {
item.setSelected( true );
}
return item != null;
}
/**
* Fetches the first item with the path specified.
*
* @param path Path of the item.
* @return Menu instance or null if not found.
*/
public Menu getItemWithPath( String path ) {
return getItem( MenuSelector.byPath( path ) );
}
/**
* Fetches the first item with the name specified.
*
* @param name Name of the item.
* @return Menu instance or null if not found.
*/
public Menu getItemWithName( String name ) {
return getItem( MenuSelector.byName( name ) );
}
public List getItems() {
return readonlyItems;
}
public Menu getFirstItem() {
if ( hasItems() ) {
return items.getFirst();
}
return null;
}
public boolean hasItems() {
return !items.isEmpty();
}
public boolean isEmpty() {
return items.isEmpty();
}
public Menu addItem( String path ) {
return addItem( path, path );
}
public Menu addItem( String path, String title ) {
Menu item = new Menu( path );
item.setTitle( title );
return addItem( item );
}
public Menu addItem( String path, String title, String url ) {
Menu item = new Menu( path );
item.setTitle( title );
item.setUrl( url );
return addItem( item );
}
public Menu addItem( Menu item ) {
if ( item.hasParent() ) {
throw new IllegalStateException( "A Menu can only belong to a single parent menu." );
}
items.add( item );
item.setParent( this );
if ( item.isSelected() ) {
item.setSelected( true );
}
return item;
}
/**
* Removes the menu item from the tree - disconnects it from its parent.
*
* @param item Menu to remove.
* @return True if found anywhere in the tree and removed successfully.
*/
public boolean remove( Menu item ) {
if ( !item.hasParent() || item.getRoot() == getRoot() ) {
if ( item.getParent() == this ) {
boolean removed = items.remove( item );
if ( removed ) {
item.setParent( null );
}
return removed;
}
else {
return item.getParent().remove( item );
}
}
return false;
}
/**
* Sorts the items in the menu recursively depending on Comparator specified or Ordered property.
*/
public void sort() {
sort( null );
}
protected void sort( Comparator inheritedComparator ) {
if ( hasItems() ) {
Comparator comparatorToInherit = comparator;
Comparator comparatorToUse = comparator;
boolean inheritable = comparatorInheritable;
if ( comparatorToUse == null ) {
comparatorToUse = inheritedComparator;
comparatorToInherit = inheritedComparator;
inheritable = true;
}
else if ( !inheritable ) {
comparatorToInherit = inheritedComparator;
inheritable = true;
}
if ( comparatorToUse == null ) {
comparatorToUse = SORT_BY_ORDER_AND_TITLE;
inheritable = false;
}
if ( !isOrdered() ) {
Collections.sort( items, comparatorToUse );
}
for ( Menu item : items ) {
item.sort( inheritable ? comparatorToInherit : null );
}
}
}
public int size() {
return items.size();
}
public void clear() {
for ( Menu item : items ) {
item.setParent( null );
}
items.clear();
}
/**
*
* Merges the other menu into this one.
*
* Any item with the same path will be modified
* Any unknown item present in other will be added
* Any item not present in other will be kept
*
* In case an item is modified:
*
* Properties are overwritten with the values from other
* Attributes from other are added or overwritten (merge of attribute map)
* All sub items undergo a merge
*
* A merge only looks downstream, the parent structure does not find modified. The selected item
* however can be modified by the merge.
*
*
* @param other Other menu to merge into the current instance.
* @param ignoreRoot True if the root of the other Menu should be ignored, only children should be merged.
*/
public void merge( Menu other, boolean ignoreRoot ) {
if ( other != null ) {
if ( !ignoreRoot ) {
order = other.order;
ordered = other.ordered;
group = other.group;
disabled = other.disabled;
name = other.name;
path = other.path;
title = other.title;
url = other.url;
comparator = other.comparator;
comparatorInheritable = other.comparatorInheritable;
setSelected( other.selected );
for ( Map.Entry otherAttribute : other.getAttributes().entrySet() ) {
setAttribute( otherAttribute.getKey(), otherAttribute.getValue() );
}
}
// Merge items by path
for ( Menu otherItem : other.getItems() ) {
Menu existingItem = findDirectChildWithPath( otherItem.getPath() );
if ( existingItem != null ) {
existingItem.merge( otherItem, false );
}
else {
Menu duplicate = new Menu( otherItem );
duplicate.setParent( null );
addItem( duplicate );
}
}
}
}
private Menu findDirectChildWithPath( String path ) {
for ( Menu currentItem : items ) {
if ( StringUtils.equals( currentItem.path, path ) ) {
return currentItem;
}
}
return null;
}
@Override
public String toString() {
return "Menu{" +
"name='" + path + '\'' +
", items=" + items +
'}';
}
/**
* Create a {@link PathBasedMenuBuilder} for a new {@code Menu}.
*
* @return builder for a menu
*/
public static PathBasedMenuBuilder builder() {
return new PathBasedMenuBuilder();
}
}