/*==========================================================================*\ | $Id: TabDescriptor.java,v 1.1 2010/05/11 14:51:55 aallowat Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2009 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU Affero General Public License as published | by the Free Software Foundation; either version 3 of the License, or | (at your option) any later version. | | Web-CAT is distributed in the hope that it will be useful, | but WITHOUT ANY WARRANTY; without even the implied warranty of | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | GNU General Public License for more details. | | You should have received a copy of the GNU Affero General Public License | along with Web-CAT; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package org.webcat.core; import org.apache.log4j.Logger; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSComparator; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSPropertyListSerialization; import er.extensions.foundation.ERXValueUtilities; // ------------------------------------------------------------------------- /** * A class used to describe individual tabs in a hierarchical navigation * scheme. Note that although such a niavigational element may nominally * be called a "tab", it may be rendered in different ways depending on * what level it is at in hierarchical navigation. * * @author Stephen Edwards * @author Last changed by $Author: aallowat $ * @version $Revision: 1.1 $, $Date: 2010/05/11 14:51:55 $ */ public class TabDescriptor { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Constructs a new <code>TabDescriptor</code> object. * * @param pageName The class name for the page represented by this tab * @param label The text label (description) associated with this tab * @param accessLevel The accessLevel needed to access this tab * @param priority The order priority for this tab (larger numbers come * later in the sequence of sibling tabs) * @param wantsStart True if this tab wants to be the default starting * subtab for its parent * @param children An array of subtab elements * @param id A unique identifier for this tab (optional) * @param config An optional dictionary of configuration parameters; * if the actual is mutable, it will be taken and owned; * if not, a mutable clone will be created */ public TabDescriptor( String pageName, String label, int accessLevel, int priority, boolean wantsStart, NSArray<TabDescriptor> children, String id, NSDictionary<String, Object> config ) { this.pageName = pageName; this.label = label; this.accessLevel = accessLevel; this.priority = priority; this.wantsStart = wantsStart; this.children = new NSMutableArray<TabDescriptor>(); this.id = id; if (config != null) { if (config instanceof NSMutableDictionary) { this.config = (NSMutableDictionary<String, Object>)config; } else { this.config = config.mutableClone(); } } if (children != null) { addChildren(children); } // log.debug( "created, before selection:" + this ); // selectDefault(); // log.debug( "created, after selection:" + this ); } // ---------------------------------------------------------- /** * Constructs a new <code>TabDescriptor</code> object. * * @param pageName The class name for the page represented by this tab * @param label The text label (description) associated with this tab * @param accessLevel The accessLevel needed to access this tab * @param priority The order priority for this tab (larger numbers come * later in the sequence of sibling tabs) * @param wantsStart True if this tab wants to be the default starting * subtab for its parent */ public TabDescriptor( String pageName, String label, int accessLevel, int priority, boolean wantsStart ) { this( pageName, label, accessLevel, priority, wantsStart, null, null, null ); } // ---------------------------------------------------------- /** * Constructs a new <code>TabDescriptor</code> object. * * @param pageName The class name for the page represented by this tab * @param label The text label (description) associated with this tab * @param accessLevel The accessLevel needed to access this tab * @param priority The order priority for this tab (larger numbers come * later in the sequence of sibling tabs) */ public TabDescriptor( String pageName, String label, int accessLevel, int priority ) { this( pageName, label, accessLevel, priority, false, null, null, null ); } // ---------------------------------------------------------- /** * Constructs a new <code>TabDescriptor</code> object. * * @param pageName The class name for the page represented by this tab * @param label The text label (description) associated with this tab * @param accessLevel The accessLevel needed to access this tab */ public TabDescriptor( String pageName, String label, int accessLevel ) { this( pageName, label, accessLevel, 1000, false, null, null, null ); } // ---------------------------------------------------------- /** * Constructs a new <code>TabDescriptor</code> object. * * @param pageName The class name for the page represented by this tab * @param label The text label (description) associated with this tab */ public TabDescriptor( String pageName, String label ) { this( pageName, label, 0, 1000, false, null, null, null ); } //~ Methods ............................................................... // ---------------------------------------------------------- /** * Retrieve the class name for the page this tab represents. * * @return The page class name */ public String pageName() { return pageName; } // ---------------------------------------------------------- /** * Retrieve the class name for the page of the currently selected * descendant tab. * * @return The page class name */ public String selectedPageName() { return selectedDescendant().pageName(); } // ---------------------------------------------------------- /** * Retrieve the text label to use when rendering this tab. * * @return The tab label */ public String label() { return label; } // ---------------------------------------------------------- /** * Retrieve the sentence-case version of the text label to use when * rendering this tab. * * @return The sentence-case tab label (only first char capitalized) */ public String lcLabel() { if ( lcLabel == null ) { lcLabel = lowerCaseAfterFirst( label() ); } return lcLabel; } // ---------------------------------------------------------- /** * Retrieve the accessLevel a user must have to use this tab. * * @return The tab access level */ public int accessLevel() { return accessLevel; } // ---------------------------------------------------------- /** * Retrieve the priority for ordering this tab. * * @return The tab priority */ public int priority() { return priority; } // ---------------------------------------------------------- /** * Determine whether this tab wants to be the default tab for its parent. * * @return True if this tab wants to be the default tab for parent */ public boolean wantsStart() { return wantsStart; } // ---------------------------------------------------------- /** * Retrieve the list of children possessed by this tab. * * @return The tab label */ public NSMutableArray<TabDescriptor> children() { return children; } // ---------------------------------------------------------- /** * Retrieve the ID for the page this tab represents. * * @return The page ID */ public String id() { return id; } // ---------------------------------------------------------- /** * Retrieve the config settings dictionary for this tab. * * @return The dictionary of config settings (possibly null) */ public NSMutableDictionary<String, Object> config() { return config; } // ---------------------------------------------------------- /** * Retrieve this tab's CSS class, based on its accessLevel. * * @return The CSS class associated with this tab */ public String cssClass() { if ( accessLevel < 25 ) return "user"; else if ( accessLevel < 75 ) return "staff"; else return "admin"; } // ---------------------------------------------------------- /** * This operation does nothing, since the cssClass is determined by the * accessLevel. It is provided only to prevent certain KVC exceptions * from arising because of how this attribute is typically bound in pages. * @param value ignored */ public void setCssClass( String value ) { // Does nothing } // ---------------------------------------------------------- /** * Find out if this tab is the currently selected tab. * * @return True if this tab is selected */ public boolean isSelected() { return isSelected; } // ---------------------------------------------------------- /** * Find out if this subtab has a sibling before it. * * @return True if this tab has a previous sibling */ public boolean hasPreviousSibling() { return ( parent != null && myIndex > 0 ); } // ---------------------------------------------------------- /** * Retrieve this subtab's previous sibling, if any. * * @return The previous sibling */ public TabDescriptor previousSibling() { return hasPreviousSibling() ? parent.children.objectAtIndex(myIndex - 1) : null; } // ---------------------------------------------------------- /** * Find out if this subtab has a sibling after it. * * @return True if this tab has a next sibling */ public boolean hasNextSibling() { return ( parent != null && myIndex < parent.children.count() - 1 ); } // ---------------------------------------------------------- /** * Retrieve this subtab's next sibling, if any. * * @return The next sibling */ public TabDescriptor nextSibling() { return hasNextSibling() ? parent.children.objectAtIndex(myIndex + 1) : null; } // ---------------------------------------------------------- /** * Retrieve a subtab by position. * * @param pos The index of the child to retrieve * @return The child tab at the specified position */ public TabDescriptor childAt(int pos) { if (pos >= 0 && pos < children.count()) { return children.objectAtIndex(pos); } else { return null; } } // ---------------------------------------------------------- /** * Retrieve a subtab's parent. * @return The parent tab of this tab */ public TabDescriptor parent() { return parent; } // ---------------------------------------------------------- /** * Access the position of the currently selected subtab. * * @return The selected child tab's index, or -1 if none is selected */ public int selectedChildIndex() { return getSelectedChild(); } // ---------------------------------------------------------- /** * Access the currently selected subtab. * * @return The child tab that is selected */ public TabDescriptor selectedChild() { TabDescriptor result = childAt( getSelectedChild() ); // if ( result == null ) // { // log.error( "null selected child", // new RuntimeException( "here") ); // log.error( "tab = " + this ); // } return result; } // ---------------------------------------------------------- /** * Access the currently selected subtab. * * @return The child tab that is selected */ public TabDescriptor selectedDescendant() { TabDescriptor chosen = selectedChild(); if ( chosen != null ) return chosen.selectedDescendant(); else if ( isSelected ) return this; else return null; } // ---------------------------------------------------------- /** * Select this tab. * @return the selected tab */ public TabDescriptor select() { if ( config != null ) { String target = (String)config.objectForKey( "jumpTo" ); if ( target != null ) { return selectById( target ); } } if ( parent != null ) { parent.select(); deselectSibling(); parent.setSelectedChild( myIndex ); } isSelected = true; return this; } // ---------------------------------------------------------- /** * Unselect this tab. */ protected void deselect() { isSelected = false; if ( parent != null ) { parent.setSelectedChild( -1 ); } } // ---------------------------------------------------------- /** * Unselect this tab. */ protected void deselectSibling() { if ( parent != null ) { TabDescriptor sibling = parent.selectedChild(); if ( sibling != null ) { sibling.deselect(); } } } // ---------------------------------------------------------- /** * Select the first tab that wants to be the default. * @return the selected tab */ public TabDescriptor selectDefault() { if ( children.count() > 0 ) { for (TabDescriptor child : children) { if ( child.wantsStart ) { return child.selectDefault(); } } return children.objectAtIndex(0).selectDefault(); } else { return select(); } } // ---------------------------------------------------------- /** * Select the descendant tab that has the given ID. * @param targetId the ID to search for * @return the selected tab */ private TabDescriptor selectDownwardById( String targetId ) { if ( targetId.equals( id ) ) { return select(); } else if ( children.count() > 0 ) { for (TabDescriptor child : children) { child = child.selectDownwardById( targetId ); if ( child != null ) { return child; } } } return null; } // ---------------------------------------------------------- /** * Select the tab (anywhere in the entire hierarchy of tabs) that has the * given ID. * @param targetId the ID to search for * @return the selected tab */ public TabDescriptor selectById( String targetId ) { TabDescriptor root = this; while ( root.parent != null ) { root = root.parent; } TabDescriptor result = root.selectDownwardById( targetId ); if ( result == null ) { log.error( "no matching child for id '" + targetId + "'", new RuntimeException( "here") ); log.error( "root tab = " + root ); } return result; } // ---------------------------------------------------------- /** * Select the first sibling tab that wants to be the default. * @return the selected tab */ public TabDescriptor selectDefaultSibling() { if ( parent == null ) return select(); else return parent.selectDefault(); } // ---------------------------------------------------------- /** * Add a list of additional children to this tab. * @param moreChildren the new subtabs to add */ public void addChildren( NSArray<TabDescriptor> moreChildren ) { if ( moreChildren == null || moreChildren.count() == 0 ) return; TabDescriptor selectedChildTab = selectedChild(); children.addObjectsFromArray( moreChildren ); if ( this.children != null ) { try { this.children.sortUsingComparator( priorityOrder ); } catch ( NSComparator.ComparisonException e ) { log.error( "Exception sorting tab chilren:", e ); } } resetChildIndices( selectedChildTab ); } // ---------------------------------------------------------- /** * Reset a the indices of child tabs. * @param selectedChildTab the child selection to maintain */ private void resetChildIndices( TabDescriptor selectedChildTab ) { for ( int i = 0; i < children.count(); i++ ) { TabDescriptor child = children.objectAtIndex( i ); child.myIndex = i; child.parent = this; if ( child == selectedChildTab ) { child.isSelected = true; setSelectedChild( i ); } else { child.isSelected = false; } } } // ---------------------------------------------------------- /** * Merge a list of additional children into this tab. * @param moreChildren the new subtabs to add */ public void mergeClonedChildren(NSArray<TabDescriptor> moreChildren) { if ( moreChildren == null || moreChildren.count() == 0 ) return; NSMutableArray<TabDescriptor> newChildren = new NSMutableArray<TabDescriptor>(); tabSearch: for (TabDescriptor newTab : moreChildren) { for (TabDescriptor oldTab : children) { if ( oldTab.label().equals( newTab.label() ) ) { // Found old tab already present // First, override any settings, as appropriate if (newTab.pageName() != null) { oldTab.pageName = newTab.pageName(); } if (newTab.accessLevel() > 0 || (newTab.config() != null && newTab.config().containsKey("overrideAccessLevel"))) { oldTab.accessLevel = newTab.accessLevel(); } if (newTab.priority() > 0) { oldTab.priority = newTab.priority(); } if (newTab.wantsStart()) { oldTab.wantsStart = newTab.wantsStart(); } if (newTab.id() != null) { oldTab.id = newTab.id(); } if (newTab.config() != null && newTab.config().count() > 0) { if (oldTab.config() == null) { oldTab.config = new NSMutableDictionary<String, Object>(); } oldTab.config().addEntriesFromDictionary( newTab.config()); } // Now, recursively merge its children oldTab.mergeClonedChildren( newTab.children() ); continue tabSearch; } } newChildren.addObject( newTab.clone() ); } addChildren( newChildren ); } // ---------------------------------------------------------- /** * Recursively remove any tabs with an access level greater than * a given cutoff. Calling this method necessarily forces all * tabs to be deselected. * @param aLevel The access level to use to limit tabs */ public void filterByAccessLevel( int aLevel ) { TabDescriptor selected = selectedChild(); if ( selected != null ) selected.deselect(); else setSelectedChild( -1 ); for ( int i = 0; i < children.count(); i++ ) { TabDescriptor child = childAt( i ); if ( child.accessLevel > aLevel ) { children.removeObjectAtIndex( i ); i--; } else child.filterByAccessLevel( aLevel ); } resetChildIndices( null ); } // ---------------------------------------------------------- /** * Generate a string representation of this tab's class and address. * @return A human-readable description of this tab */ private String addressId() { return super.toString(); } // ---------------------------------------------------------- /** * Generate a string representation of this tab's state. * @param buffer the StringBuffer to append the description to */ public void appendToStringBuffer( StringBuffer buffer ) { buffer.append( label ); } // ---------------------------------------------------------- /** * Generate a string representation of this tab's state. * @param buffer the StringBuffer to append the description to * @param appendChildren if true, then recursively append all * child tab descriptions as well * @param indentLevel the number of spaces to indent this description */ public void appendDetailsToStringBuffer( StringBuffer buffer, boolean appendChildren, int indentLevel ) { StringBuffer indent = new StringBuffer( indentLevel + 4 ); for ( int i = 0; i < indentLevel; i++ ) { indent.append( ' ' ); } buffer.append( indent ); buffer.append( addressId() ); indent.append( " " ); buffer.append( " = {\n" ); buffer.append( indent ); buffer.append( "label = " ); buffer.append( label ); buffer.append( ";\n" ); buffer.append( indent ); buffer.append( "pageName = " ); buffer.append( pageName ); buffer.append( ";\n" ); buffer.append( indent ); buffer.append( "parent = " ); buffer.append( ( parent == null ) ? "null" : parent.addressId() ); buffer.append( ";\n" ); buffer.append( indent ); buffer.append( "myIndex = " ); buffer.append( myIndex ); buffer.append( ";\n" ); buffer.append( indent ); buffer.append( "isSelected = " ); buffer.append( isSelected ); buffer.append( ";\n" ); buffer.append( indent ); buffer.append( "selectedChild = " ); buffer.append( getSelectedChild() ); buffer.append( ";\n" ); buffer.append( indent ); buffer.append( "accessLevel = " ); buffer.append( accessLevel ); buffer.append( ";\n" ); buffer.append( indent ); buffer.append( "priority = " ); buffer.append( priority ); buffer.append( ";\n" ); buffer.append( indent ); buffer.append( "wantsStart = " ); buffer.append( wantsStart ); buffer.append( ";\n" ); buffer.append( indent ); buffer.append( "parent = " ); buffer.append( ( parent == null ) ? "null" : parent.label ); buffer.append( ";\n" ); if ( appendChildren ) { buffer.append( indent ); buffer.append( "children = {\n" ); for ( int i = 0; i < children.count(); i++ ) { TabDescriptor child = children.objectAtIndex(i); child.appendDetailsToStringBuffer( buffer, appendChildren, indent.length() ); if ( i == children.count() - 1) { buffer.append( '\n' ); } else { buffer.append( ",\n" ); } } buffer.append( indent ); buffer.append( "};\n" ); } indent.replace( indentLevel, indentLevel + 3, "" ); buffer.append( indent ); buffer.append( "}" ); } // ---------------------------------------------------------- /** * Generate a string representation of this tab's state. * @return A human-readable description of this tab */ public String toString() { StringBuffer buffer = new StringBuffer( 200 ); appendToStringBuffer( buffer ); return buffer.toString(); } // ---------------------------------------------------------- /** * Generate a string representation of this tab's state, including * all its ancestor tabs. * @return A human-readable description of this tab */ public String printableTabLocation() { StringBuffer buffer = new StringBuffer( 200 ); appendToStringBuffer( buffer ); TabDescriptor p = parent; while ( p != null ) { buffer.append( " <-- " ); p.appendToStringBuffer( buffer ); p = p.parent; } return buffer.toString(); } // ---------------------------------------------------------- /** * Generate a string representation of this tab's state, including * all its ancestor tabs. * @return A human-readable description of this tab */ public String printableTabLocationDetails() { StringBuffer buffer = new StringBuffer( 200 ); appendDetailsToStringBuffer( buffer, true, 0 ); buffer.append( '\n' ); TabDescriptor p = parent; while ( p != null ) { buffer.append( "---------------------\n" ); p.appendDetailsToStringBuffer( buffer, false, 0 ); buffer.append( '\n' ); p = p.parent; } return buffer.toString(); } // ---------------------------------------------------------- /** * Generate tab descriptors from an dictionary of property values. * @param dict the properties to use * @return An array of the new tab descriptors */ public static NSArray<TabDescriptor> tabsFromDictionary( NSDictionary<String, NSDictionary<String, Object>> dict) { NSMutableArray<TabDescriptor> tabs = new NSMutableArray<TabDescriptor>(); for (String label : dict.keySet()) { NSDictionary<String, Object> settings = dict.get(label); String pageName = (String)settings.objectForKey( "pageName" ); int accessLevel = ERXValueUtilities.intValueWithDefault( settings.objectForKey( "accessLevel" ), 0 ); int priority = ERXValueUtilities.intValueWithDefault( settings.objectForKey( "priority" ), 0 ); boolean wantsStart = ERXValueUtilities.booleanValueWithDefault( settings.objectForKey( "wantsStart" ), false ); @SuppressWarnings("unchecked") NSDictionary<String, NSDictionary<String, Object>> children = (NSDictionary<String, NSDictionary<String, Object>>)settings .objectForKey( "children" ); String overridingLabel = (String)settings.objectForKey("label"); if (overridingLabel != null) { label = overridingLabel; } @SuppressWarnings("unchecked") NSDictionary<String, Object> tabConfig = (NSDictionary<String, Object>)settings.objectForKey("config"); tabs.addObject( new TabDescriptor( pageName, label, accessLevel, priority, wantsStart, ( children == null ) ? null : tabsFromDictionary( children ), (String)settings.objectForKey( "id" ), tabConfig ) ); } return tabs; } // ---------------------------------------------------------- /** * Generate tab descriptors from a property list description. * @param data the raw bytes from the property list * @return An array of the new tab descriptors */ public static NSArray<TabDescriptor> tabsFromPropertyList(NSData data) { @SuppressWarnings("unchecked") NSDictionary<String, NSDictionary<String, Object>> dict = (NSDictionary<String, NSDictionary<String, Object>>) NSPropertyListSerialization .propertyListFromData(data, "UTF-8"); log.debug("tabFromPropertyList(): dict = " + dict); return tabsFromDictionary(dict); } // ---------------------------------------------------------- /** * Convert a mixed-case prose-style string into a string where * only the first character (at most) is capitalized. This method * is typically used to convert a "title case" label into a * "sentence case" string. * @param title the string to convert * @return The string with all characters after the first one * converted to lower case */ public static String lowerCaseAfterFirst( String title ) { String result = null; if ( title != null ) { if ( title.length() < 2 ) { result = title; } else { result = title.substring( 0, 1 ) + title.substring( 1 ).toLowerCase(); } } return result; } // ---------------------------------------------------------- /** * Generate an independent copy of this tab. * @return A clone of this tab */ public Object clone() { TabDescriptor result = new TabDescriptor( pageName, label, accessLevel, priority, wantsStart, children.mutableClone(), id, ( config == null ) ? null : config.mutableClone() ); result.myIndex = myIndex; result.isSelected = isSelected; result.setSelectedChild( getSelectedChild() ); for ( int i = 0; i < children.count(); i++ ) { TabDescriptor thisTab = (TabDescriptor) result.children.objectAtIndex(i).clone(); thisTab.parent = result; result.children.replaceObjectAtIndex( thisTab, i ); } return result; } // ---------------------------------------------------------- /** * @param selectedChild The selectedChild to set. */ protected void setSelectedChild( int selectedChild ) { // if ( selectedChild == 5 ) // { // log.error( "setting selected child = " + selectedChild, // new Exception( "here" ) ); // log.error( "tabs = " + this ); // } this.theSelectedChild = selectedChild; } // ---------------------------------------------------------- /** * @return Returns the selectedChild. */ protected int getSelectedChild() { return theSelectedChild; } // ---------------------------------------------------------- /** * A custom comparator class used to sort tab arrays by priority. */ protected class PriorityComparator extends NSComparator { // ---------------------------------------------------------- /* (non-Javadoc) * @see com.webobjects.foundation.NSComparator#compare(java.lang.Object, java.lang.Object) */ public int compare( Object left, Object right ) throws ComparisonException { if ( ! ( left instanceof TabDescriptor && right instanceof TabDescriptor ) ) { throw new ComparisonException( "incomparable TabDescriptors" ); } int leftp = ( (TabDescriptor)left ).priority; int rightp = ( (TabDescriptor)right ).priority; if ( leftp < rightp ) return OrderedAscending; else if ( leftp == rightp ) return OrderedSame; else return OrderedDescending; } } //~ Instance/static variables ............................................. public static final String TAB_DEFINITIONS = "Tabs.plist"; private String pageName; private String label; private String lcLabel; private int accessLevel; private int priority; private boolean wantsStart; private NSMutableArray<TabDescriptor> children; private String id; private NSMutableDictionary<String, Object> config; private TabDescriptor parent = null; private int myIndex = -1; private boolean isSelected = false; private int theSelectedChild = -1; protected final NSComparator priorityOrder = new PriorityComparator(); static Logger log = Logger.getLogger( TabDescriptor.class ); }