View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.jetspeed.portalsite.impl;
18  
19  import java.util.ArrayList;
20  import java.util.HashSet;
21  import java.util.Iterator;
22  import java.util.List;
23  import java.util.ListIterator;
24  import java.util.Locale;
25  import java.util.Set;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import org.apache.jetspeed.om.common.GenericMetadata;
30  import org.apache.jetspeed.om.folder.Folder;
31  import org.apache.jetspeed.om.folder.MenuDefinition;
32  import org.apache.jetspeed.om.folder.MenuExcludeDefinition;
33  import org.apache.jetspeed.om.folder.MenuIncludeDefinition;
34  import org.apache.jetspeed.om.folder.MenuOptionsDefinition;
35  import org.apache.jetspeed.om.folder.MenuSeparatorDefinition;
36  import org.apache.jetspeed.om.page.Page;
37  import org.apache.jetspeed.page.document.Node;
38  import org.apache.jetspeed.page.document.NodeNotFoundException;
39  import org.apache.jetspeed.portalsite.Menu;
40  import org.apache.jetspeed.portalsite.MenuElement;
41  import org.apache.jetspeed.portalsite.MenuOption;
42  import org.apache.jetspeed.portalsite.PortalSiteRequestContext;
43  import org.apache.jetspeed.portalsite.menu.DefaultMenuDefinition;
44  import org.apache.jetspeed.portalsite.menu.DefaultMenuOptionsDefinition;
45  import org.apache.jetspeed.portalsite.view.SiteView;
46  
47  /***
48   * This class implements the portal-site menu elements
49   * constructed and returned to decorators.
50   * 
51   * @author <a href="mailto:rwatler@apache.org">Randy Watler</a>
52   * @version $Id: MenuImpl.java 516448 2007-03-09 16:25:47Z ate $
53   */
54  public class MenuImpl extends MenuElementImpl implements Menu, Cloneable
55  {
56      /***
57       * definition - menu definition
58       */
59      private MenuDefinition definition;
60  
61      /***
62       * elements - ordered list of menu elements that
63       *            make up this instaniated menu
64       */
65      private List elements;
66  
67      /***
68       * elementRelative - flag that indicates whether any relative paths
69       *                   dependent on the current page in context were
70       *                   referenced while constructing menu elements:
71       *                   requires request, not session, caching
72       */
73      private boolean elementRelative;
74  
75      /***
76       * MenuImpl - request/session context dependent constructor
77       *
78       * @param parent containing menu implementation
79       * @param definition menu definition
80       * @param context request context
81       * @param menus related menu definition names set
82       */
83      public MenuImpl(MenuImpl parent, MenuDefinition definition, PortalSiteRequestContextImpl context, Set menus)
84      {
85          super(parent);
86          this.definition = definition;
87  
88          // get site view from context
89          SiteView view = ((PortalSiteSessionContextImpl)context.getSessionContext()).getSiteView();
90          if (view != null)
91          {
92              // define menu node for titles and metadata if options
93              // specifies a single visible page or folder proxy
94              String options = definition.getOptions();
95              Node optionProxy = null;
96              if ((options != null) && (options.indexOf(',') == -1))
97              {
98                  try
99                  {
100                     optionProxy = view.getNodeProxy(options, context.getPage(), true, true);
101                 }
102                 catch (NodeNotFoundException nnfe)
103                 {
104                 }
105                 catch (SecurityException se)
106                 {
107                 }
108                 if (optionProxy != null)
109                 {
110                     setNode(optionProxy);
111                 }
112             }
113 
114             // construct menu elements from menu definition
115             // or nested menu definition elements; note that
116             // menu elements override menu options attribute
117             if ((definition.getMenuElements() == null) || definition.getMenuElements().isEmpty())
118             {
119                 // if options optionProxy is a single folder, force
120                 // options to include all folder children if not to
121                 // be expanded with paths and depth inclusion is
122                 // specified
123                 List overrideOptionProxies = null;
124                 if (optionProxy != null)
125                 {
126                     if ((optionProxy instanceof Folder) && !definition.isPaths() && (definition.getDepth() != 0))
127                     {
128                         // assemble folder children path using wildcard
129                         String folderChildrenPath = null;
130                         if (!options.endsWith(Folder.PATH_SEPARATOR))
131                         {
132                             folderChildrenPath = options + Folder.PATH_SEPARATOR + "*";
133                         }
134                         else
135                         {
136                             folderChildrenPath = options + "*";
137                         }
138 
139                         // override menu options with visible folder contents
140                         // or create empty menu if no contents exist
141                         List folderChildren = null;
142                         try
143                         {
144                             folderChildren = view.getNodeProxies(folderChildrenPath, context.getPage(), true, true);
145                         }
146                         catch (NodeNotFoundException nnfe)
147                         {
148                         }
149                         catch (SecurityException se)
150                         {
151                         }
152                         if ((folderChildren != null) && !folderChildren.isEmpty())
153                         {
154                             overrideOptionProxies = folderChildren;
155                         }
156                         else
157                         {
158                             return;
159                         }
160                     }
161                     else
162                     {
163                         // override menu options with single folder/page/link
164                         overrideOptionProxies = new ArrayList(1);
165                         overrideOptionProxies.add(optionProxy);
166                     }
167                     
168                     // set relative element flag if options path is relative
169                     this.elementRelative = (this.elementRelative || !options.startsWith(Folder.PATH_SEPARATOR));
170                 }
171 
172                 // menu defined only with menu definition options
173                 this.elements = constructMenuElements(context, view, options, overrideOptionProxies, definition.getDepth(), definition.isPaths(), definition.isRegexp(), definition.getProfile(), definition.getOrder());
174             }
175             else
176             {
177                 // limit cyclic references to this menu if named and
178                 // referencable as root menu instance
179                 boolean menuNameReferenced = false;
180                 if ((definition.getName() != null) && (parent == null))
181                 {
182                     if (menus == null)
183                     {
184                         menus = new HashSet(4);
185                     }
186                     menuNameReferenced = menus.add(definition.getName());
187                 }
188                 
189                 // process menu elements in chunks between separators:
190                 // separators are included only if menu options are
191                 // generated after the separator and include/exclude
192                 // merge/filter operations apply to options bounded
193                 // by separators
194                 MenuSeparatorImpl separator = null;
195                 List separatedElements = null;
196 
197                 // process each defined menu element
198                 Iterator menuElementsIter = definition.getMenuElements().iterator();
199                 while (menuElementsIter.hasNext())
200                 {
201                     Object menuElement = menuElementsIter.next();
202                     if (menuElement instanceof MenuOptionsDefinition)
203                     {
204                         // construct menu option elements from definition using
205                         // defaults from menu definition as appropriate
206                         MenuOptionsDefinition optionDefinition = (MenuOptionsDefinition)menuElement;
207                         String locatorName = optionDefinition.getProfile();
208                         if (locatorName == null)
209                         {
210                             locatorName = definition.getProfile();
211                         }
212                         String order = optionDefinition.getOrder();
213                         if (order == null)
214                         {
215                             order = definition.getOrder();
216                         }
217                         List optionsAndMenus = constructMenuElements(context, view, optionDefinition.getOptions(), null, optionDefinition.getDepth(), optionDefinition.isPaths(), optionDefinition.isRegexp(), locatorName, order);
218 
219                         // append option and menu elements to current separator
220                         // elements list
221                         if (optionsAndMenus != null)
222                         {
223                             if (separatedElements == null)
224                             {
225                                 separatedElements = optionsAndMenus;
226                             }
227                             else
228                             {
229                                 appendMenuElements(optionsAndMenus, separatedElements);
230                             }
231                         }
232                     }
233                     else if (menuElement instanceof MenuSeparatorDefinition)
234                     {
235                         // append current separator and separated option/menu elements
236                         // to menu elements list if at least one option/menu
237                         // element exists: do not include disassociated separators in menu
238                         if ((separatedElements != null) && !separatedElements.isEmpty())
239                         {
240                             if (this.elements == null)
241                             {
242                                 int initialSize = separatedElements.size();
243                                 if (separator != null)
244                                 {
245                                     initialSize++;
246                                 }
247                                 this.elements = new ArrayList(initialSize);
248                             }
249                             if (separator != null)
250                             {
251                                 this.elements.add(separator);
252                             }
253                             this.elements.addAll(separatedElements);
254                         }
255 
256                         // construct new separator and reset separator
257                         // and separator option/menu elements list
258                         MenuSeparatorDefinition separatorDefinition = (MenuSeparatorDefinition)menuElement;
259                         separator = new MenuSeparatorImpl(this, separatorDefinition);
260                         if (separatedElements != null)
261                         {
262                             separatedElements.clear();
263                         }
264                     }
265                     else if (menuElement instanceof MenuDefinition)
266                     {
267                         // construct nested menu element from definition
268                         MenuDefinition menuDefinition = (MenuDefinition)menuElement;
269                         MenuImpl nestedMenu = new MenuImpl(this, menuDefinition, context, menus);
270 
271                         // append menu element to current separated elements list
272                         if (separatedElements == null)
273                         {
274                             separatedElements = new ArrayList(1);
275                         }
276                         appendMenuElement(nestedMenu, separatedElements);
277 
278                         // set relative element flag if nested menu is relative
279                         this.elementRelative = (this.elementRelative || nestedMenu.isElementRelative());
280                     }
281                     else if (menuElement instanceof MenuIncludeDefinition)
282                     {
283                         // include or nest referenced menu definition
284                         // assuming reference to menu is not cyclic
285                         MenuIncludeDefinition includeDefinition = (MenuIncludeDefinition)menuElement;
286                         if ((menus == null) || !menus.contains(includeDefinition.getName()))
287                         {
288                             // get named root menu from context, (menu may
289                             // not exist in this context so failure to
290                             // access menu is ignored)
291                             MenuImpl includeMenu = null;
292                             try
293                             {
294                                 includeMenu = (MenuImpl)context.getMenu(includeDefinition.getName());
295                             }
296                             catch (NodeNotFoundException nnfe)
297                             {
298                             }
299                             catch (SecurityException se)
300                             {
301                             }
302                             if (includeMenu != null)
303                             {
304                                 // nest menu or include elements, clone required
305                                 // to support reparenting to this menu
306                                 if (includeDefinition.isNest())
307                                 {
308                                     // nest menu instance
309                                     try
310                                     {
311                                         // clone menu and reparent
312                                         includeMenu = (MenuImpl)includeMenu.clone();
313                                         includeMenu.setParentMenu(this);
314 
315                                         // append menu element to current separated elements list
316                                         if (separatedElements == null)
317                                         {
318                                             separatedElements = new ArrayList(1);
319                                         }
320                                         appendMenuElement(includeMenu, separatedElements);
321                                     }
322                                     catch (CloneNotSupportedException cnse)
323                                     {
324                                     }
325                                 }
326                                 else
327                                 {
328                                     // include menu elements
329                                     if (!includeMenu.isEmpty())
330                                     {
331                                         Iterator elementsIter = includeMenu.getElements().iterator();
332                                         while (elementsIter.hasNext())
333                                         {
334                                             MenuElementImpl includeElement = (MenuElementImpl)elementsIter.next();
335                                             try
336                                             {
337                                                 // clone menu element and reparent
338                                                 includeElement = (MenuElementImpl)includeElement.clone();
339                                                 includeElement.setParentMenu(this);
340                                                 
341                                                 // insert separators or options and menus
342                                                 if (includeElement instanceof MenuSeparatorImpl)
343                                                 {
344                                                     // append current separator and separated option/menu elements
345                                                     if ((separatedElements != null) && !separatedElements.isEmpty())
346                                                     {
347                                                         if (this.elements == null)
348                                                         {
349                                                             int initialSize = separatedElements.size();
350                                                             if (separator != null)
351                                                             {
352                                                                 initialSize++;
353                                                             }
354                                                             this.elements = new ArrayList(initialSize);
355                                                         }
356                                                         if (separator != null)
357                                                         {
358                                                             this.elements.add(separator);
359                                                         }
360                                                         this.elements.addAll(separatedElements);
361                                                     }
362 
363                                                     // reset separator and separator option/menu elements list
364                                                     // using separator menu element
365                                                     separator = (MenuSeparatorImpl)includeElement;
366                                                     if (separatedElements != null)
367                                                     {
368                                                         separatedElements.clear();
369                                                     }
370                                                 }
371                                                 else
372                                                 {
373                                                     // append menu element to current separated elements list
374                                                     if (separatedElements == null)
375                                                     {
376                                                         separatedElements = new ArrayList(includeMenu.getElements().size());
377                                                     }
378                                                     appendMenuElement(includeElement, separatedElements);
379                                                 }
380                                             }
381                                             catch (CloneNotSupportedException cnse)
382                                             {
383                                             }
384                                         }
385                                     }
386                                 }
387 
388                                 // set relative element flag if included menu is relative
389                                 this.elementRelative = (this.elementRelative || includeMenu.isElementRelative());
390                             }
391                         }
392                     }
393                     else if (menuElement instanceof MenuExcludeDefinition)
394                     {
395                         // exclusion requires current separated elements
396                         if ((separatedElements != null) && !separatedElements.isEmpty())
397                         {
398                             // exclude top level referenced menu definition
399                             // options assuming reference to menu is not cyclic
400                             MenuExcludeDefinition excludeDefinition = (MenuExcludeDefinition)menuElement;
401                             if ((menus == null) || !menus.contains(excludeDefinition.getName()))
402                             {
403                                 // get named root menu from context, (menu may
404                                 // not exist in this context so failure to
405                                 // access menu is ignored)
406                                 MenuImpl excludeMenu = null;
407                                 try
408                                 {
409                                     excludeMenu = (MenuImpl)context.getMenu(excludeDefinition.getName());
410                                 }
411                                 catch (NodeNotFoundException nnfe)
412                                 {
413                                 }
414                                 catch (SecurityException se)
415                                 {
416                                 }
417                                 if (excludeMenu != null)
418                                 {
419                                     // remove referenced menu options from current
420                                     // separated elements list
421                                     removeMenuElements(excludeMenu.getElements(), separatedElements);
422 
423                                     // set relative element flag if excluded menu is relative
424                                     this.elementRelative = (this.elementRelative || excludeMenu.isElementRelative());
425                                 }
426                             }
427                         }
428                     }
429                 }
430 
431                 // append last separator and separated option/menu elements
432                 // to menu elements list if at least one option/menu
433                 // element exists: do not include trailing separators
434                 if ((separatedElements != null) && !separatedElements.isEmpty())
435                 {
436                     if (this.elements == null)
437                     {
438                         // use the separated elements as the menu elements
439                         // collection and insert the separator
440                         this.elements = separatedElements;
441                         if (separator != null)
442                         {
443                             this.elements.add(0, separator);
444                         }
445                     }
446                     else
447                     {
448                         // copy into existing menu elements collection
449                         if (separator != null)
450                         {
451                             this.elements.add(separator);
452                         }
453                         this.elements.addAll(separatedElements);
454                     }
455                 }
456 
457                 // restore referencing for this menu if limited
458                 if (menuNameReferenced)
459                 {
460                     menus.remove(definition.getName());
461                 }
462             }
463         }
464     }
465 
466     /***
467      * MenuImpl - request/session context dependent constructor
468      *
469      * @param definition menu definition
470      * @param context request context
471      * @param menus related menu definition names set
472      */
473     public MenuImpl(MenuDefinition definition, PortalSiteRequestContextImpl context, Set menus)
474     {
475         this(null, definition, context, menus);
476     }
477 
478     /***
479      * appendMenuElement - append to ordered list of unique menu
480      *                     option/menu elements
481      * 
482      * @param appendMenuElement option/menu element to append
483      * @param menuElements option/menu element list
484      */
485     private void appendMenuElement(MenuElementImpl appendMenuElement, List menuElements)
486     {
487         // make sure new menu element is unique and
488         // add to menu element list
489         if (appendMenuElement != null)
490         {
491             if (!menuElements.contains(appendMenuElement))
492             {
493                 menuElements.add(appendMenuElement);
494             }
495         }
496     }
497     
498     /***
499      * appendMenuElements - append to ordered list of unique menu
500      *                      option/menu elements
501      * 
502      * @param appendMenuElements option/menu element list to append
503      * @param menuElements option/menu element list
504      */
505     private void appendMenuElements(List appendMenuElements, List menuElements)
506     {
507         // make sure new menu elements are unique and
508         // add to menu element list
509         if (appendMenuElements != null)
510         {
511             Iterator elementsIter = appendMenuElements.iterator();
512             while (elementsIter.hasNext())
513             {
514                 appendMenuElement((MenuElementImpl)elementsIter.next(), menuElements);
515             }
516         }
517     }
518     
519     /***
520      * removeMenuElements - remove from ordered list of unique menu
521      *                      option/menu elements
522      * 
523      * @param removeMenuElements option/menu element list to remove
524      * @param menuElements option/menu element list
525      */
526     private void removeMenuElements(List removeMenuElements, List menuElements)
527     {
528         // remove equivalent menu elements from menu
529         // element list
530         if (removeMenuElements != null)
531         {
532             menuElements.removeAll(removeMenuElements);
533         }
534     }
535 
536     /***
537      * constructMenuElements - construct ordered list of menu elements in
538      *                         context/site view using specified element
539      *                         selection parameters; also sets up the
540      *                         elementRelative flag while constructing the
541      *                         menu elements
542      * 
543      * @param context request context
544      * @param view context site view
545      * @param options option paths specification
546      * @param overrideElementProxies override menu element node proxies
547      * @param depth inclusion depth
548      * @param paths paths elements flag
549      * @param regexp regexp flag
550      * @param locatorName profile locator name
551      * @param order ordering patterns list
552      */
553     private List constructMenuElements(PortalSiteRequestContextImpl context, SiteView view, String options, List overrideElementProxies, int depth, boolean paths, boolean regexp, String locatorName, String order)
554     {
555         if (options != null)
556         {
557             // use override element proxies if specified; otherwise
558             // compute proxy list using specified menu options
559             List elementProxies = overrideElementProxies;
560             if (elementProxies == null)
561             {
562                 // split multiple comma separated option paths from specified options 
563                 String [] optionPaths = options.split(",");
564                 
565                 // use regexp processing if specified or simple
566                 // path evaluation to retrieve list of proxies from
567                 // the site view for the specified options
568                 for (int i = 0; (i < optionPaths.length); i++)
569                 {
570                     String optionPath = optionPaths[i].trim();
571                     if (optionPath.length() > 0)
572                     {
573                         // get proxies/proxy for path
574                         if (regexp)
575                         {
576                             // get list of visible proxies for path from view and append
577                             // to list if unique and pass profile locator name filter
578                             List pathProxies = null;
579                             try
580                             {
581                                 pathProxies = view.getNodeProxies(optionPath, context.getPage(), true, true);
582                             }
583                             catch (NodeNotFoundException nnfe)
584                             {
585                             }
586                             catch (SecurityException se)
587                             {
588                             }
589                             if (pathProxies != null)
590                             {
591                                 Iterator pathProxiesIter = pathProxies.iterator();
592                                 while (pathProxiesIter.hasNext())
593                                 {
594                                     Node pathProxy = (Node)pathProxiesIter.next();
595                                     if ((locatorName == null) || locatorName.equals(MenuOptionsDefinition.ANY_PROFILE_LOCATOR) ||
596                                         locatorName.equals(view.getProfileLocatorName(pathProxy)))
597                                     {
598                                         if (elementProxies == null)
599                                         {
600                                             elementProxies = new ArrayList();
601                                         }
602                                         appendMenuElementProxies(pathProxy, elementProxies);
603                                     }
604                                 }
605                             }
606                         }
607                         else
608                         {
609                             // get visible proxy for path from view and append to
610                             // list if unique and pass profile locator name filter
611                             Node pathProxy = null;
612                             try
613                             {
614                                 pathProxy = view.getNodeProxy(optionPath, context.getPage(), true, true);
615                             }
616                             catch (NodeNotFoundException nnfe)
617                             {
618                             }
619                             catch (SecurityException se)
620                             {
621                             }
622                             if ((pathProxy != null) &&
623                                 ((locatorName == null) || locatorName.equals(MenuOptionsDefinition.ANY_PROFILE_LOCATOR) ||
624                                  locatorName.equals(view.getProfileLocatorName(pathProxy))))
625                             {
626                                 if (elementProxies == null)
627                                 {
628                                     elementProxies = new ArrayList();
629                                 }
630                                 appendMenuElementProxies(pathProxy, elementProxies);
631                             }
632                         }
633 
634                         // set relative element flag if path is relative
635                         elementRelative = (elementRelative || !optionPath.startsWith(Folder.PATH_SEPARATOR));
636                     }
637                 }
638 
639                 // return if no proxies available
640                 if (elementProxies == null)
641                 {
642                     return null;
643                 }
644             }
645             
646             // sort elements proxies using url and/or names if order
647             // specified and more than one element proxy in list
648             if ((order != null) && (elementProxies.size() > 1))
649             {
650                 // create ordered element proxies
651                 List orderedElementProxies = new ArrayList(elementProxies.size());
652                 
653                 // split multiple comma separated elements orderings
654                 // after converted to regexp pattern
655                 String [] orderings = orderRegexpPattern(order).split(",");
656                 
657                 // copy ordered proxies per ordering
658                 for (int i=0; ((i < orderings.length) && (elementProxies.size() > 1)); i++)
659                 {
660                     String ordering = orderings[i].trim();
661                     if (ordering.length() > 0)
662                     {
663                         // get ordering pattern and matcher
664                         Pattern pattern = Pattern.compile(ordering);
665                         Matcher matcher = null;
666                         
667                         // use regular expression to match urls or names of
668                         // element proxies; matched proxies are removed and
669                         // placed in the ordered elements proxies list
670                         Iterator elementProxiesIter = elementProxies.iterator();
671                         while (elementProxiesIter.hasNext())
672                         {
673                             Node elementProxy = (Node)elementProxiesIter.next(); 
674                             
675                             // get url or name to test ordering match against
676                             String test = null;
677                             if (ordering.charAt(0) == Folder.PATH_SEPARATOR_CHAR)
678                             {
679                                 test = elementProxy.getUrl();
680                             }
681                             else
682                             {
683                                 test = elementProxy.getName();
684                             }
685                             
686                             // construct or reset ordering matcher
687                             if (matcher == null)
688                             {
689                                 matcher = pattern.matcher(test);
690                             }
691                             else
692                             {
693                                 matcher.reset(test);
694                             }
695                             
696                             // move proxy to ordered list if matched
697                             if (matcher.matches())
698                             {
699                                 orderedElementProxies.add(elementProxy);
700                                 elementProxiesIter.remove();
701                             }
702                         }
703                     }
704                 }
705                 
706                 // copy remaining unordered proxies
707                 orderedElementProxies.addAll(elementProxies);
708                 
709                 // replace element proxies with ordered list
710                 elementProxies = orderedElementProxies;
711             }
712             
713             // expand paths if single page or folder element proxy
714             // has been specified in elements with no depth expansion
715             if (paths && (depth == 0) && (elementProxies.size() == 1) &&
716                 ((elementProxies.get(0) instanceof Folder) || (elementProxies.get(0) instanceof Page)))
717             {
718                 Node parentNode = ((Node)elementProxies.get(0)).getParent();
719                 while (parentNode != null)
720                 {
721                     elementProxies.add(0, parentNode);
722                     parentNode = parentNode.getParent();
723                 }
724             }
725             
726             // convert elements proxies into menu elements
727             DefaultMenuOptionsDefinition defaultMenuOptionsDefinition = null;
728             ListIterator elementProxiesIter = elementProxies.listIterator();
729             while (elementProxiesIter.hasNext())
730             {
731                 Node elementProxy = (Node)elementProxiesIter.next();
732                 MenuElement menuElement = null;
733 
734                 // convert folders into nested menus if depth specified
735                 // with no paths expansion, (negative depth values are
736                 // interpreted as complete menu expansion)
737                 if ((elementProxy instanceof Folder) && ((depth < 0) || (depth > 1)) && !paths)
738                 {
739                     // construct menu definition and associated menu
740                     MenuDefinition nestedMenuDefinition = new DefaultMenuDefinition(elementProxy.getUrl(), depth - 1, locatorName);
741                     menuElement = new MenuImpl(this, nestedMenuDefinition, context, null);
742                 }
743                 else
744                 {
745                     // construct shared default menu option definition and menu option
746                     if (defaultMenuOptionsDefinition == null)
747                     {
748                         defaultMenuOptionsDefinition = new DefaultMenuOptionsDefinition(options, depth, paths, regexp, locatorName, order);
749                     }
750                     menuElement = new MenuOptionImpl(this, elementProxy, defaultMenuOptionsDefinition);
751                 }
752 
753                 // replace element proxy with menu element
754                 elementProxiesIter.set(menuElement);
755             }
756             List menuElements = elementProxies;
757 
758             // return list of menu elements constructed from element proxies
759             return menuElements;
760         }
761 
762         // no options specified
763         return null;
764     }
765 
766     /***
767      * appendMenuElementProxies - append to ordered list of unique menu
768      *                            element proxies
769      * 
770      * @param pathProxy menu element page, folder, or link proxy at path
771      * @param elementProxies element proxies list
772      */
773     private void appendMenuElementProxies(Node pathProxy, List elementProxies)
774     {
775         // make sure new proxy is unique and add
776         // to element proxies list
777         if (!elementProxies.contains(pathProxy))
778         {
779             elementProxies.add(pathProxy);
780         }
781     }
782     
783     /***
784      * clone - clone this instance
785      *
786      * @return unparented deep copy
787      */
788     public Object clone() throws CloneNotSupportedException
789     {
790         // clone this object
791         MenuImpl copy = (MenuImpl)super.clone();
792 
793         // clone and reparent copy elements
794         if (copy.elements != null)
795         {
796             Iterator elementsIter = copy.elements.iterator();
797             copy.elements = new ArrayList(copy.elements.size());
798             while (elementsIter.hasNext())
799             {
800                 MenuElementImpl elementCopy = (MenuElementImpl)((MenuElementImpl)elementsIter.next()).clone();
801                 elementCopy.setParentMenu(copy);
802                 copy.elements.add(elementCopy);
803             }
804         }
805         return copy;
806     }
807 
808     /***
809      * getElementType - get type of menu element
810      *
811      * @return MENU_ELEMENT_TYPE
812      */
813     public String getElementType()
814     {
815         return MENU_ELEMENT_TYPE;
816     }
817 
818     /***
819      * getName - get name of menu
820      *
821      * @return menu name
822      */
823     public String getName()
824     {
825         return definition.getName();
826     }
827 
828     /***
829      * getTitle - get default title for menu element
830      *
831      * @return title text
832      */
833     public String getTitle()
834     {
835         // return definition title
836         String title = definition.getTitle();
837         if (title != null)
838         {
839             return title;
840         }
841         // return node or default title
842         return super.getTitle();
843     }
844 
845     /***
846      * getShortTitle - get default short title for menu element
847      *
848      * @return short title text
849      */
850     public String getShortTitle()
851     {
852         // return definition short title
853         String title = definition.getShortTitle();
854         if (title != null)
855         {
856             return title;
857         }
858 
859         // return node or default short title
860         return super.getShortTitle();
861     }
862 
863     /***
864      * getTitle - get locale specific title for menu element
865      *            from metadata
866      *
867      * @param locale preferred locale
868      * @return title text
869      */
870     public String getTitle(Locale locale)
871     {
872         // return definition short title for preferred locale
873         String title = definition.getTitle(locale);
874         if (title != null)
875         {
876             return title;
877         }
878 
879         // return node or default title for preferred locale
880         return super.getTitle(locale);
881     }
882 
883     /***
884      * getShortTitle - get locale specific short title for menu
885      *                 element from metadata
886      *
887      * @param locale preferred locale
888      * @return short title text
889      */
890     public String getShortTitle(Locale locale)
891     {
892         // return definition short title for preferred locale
893         String title = definition.getShortTitle(locale);
894         if (title != null)
895         {
896             return title;
897         }
898 
899         // return node or default short title for preferred locale
900         return super.getShortTitle(locale);
901     }
902 
903     /***
904      * getMetadata - get generic metadata for menu element
905      *
906      * @return metadata
907      */    
908     public GenericMetadata getMetadata()
909     {
910         // return definition metadata
911         GenericMetadata metadata = definition.getMetadata();
912         if ((metadata != null) && (metadata.getFields() != null) && !metadata.getFields().isEmpty())
913         {
914             return metadata;
915         }
916 
917         // return node metadata
918         return super.getMetadata();
919     }
920 
921     /***
922      * getSkin - get skin name for menu element
923      *
924      * @return skin name
925      */
926     public String getSkin()
927     {
928         // get skin from definition or inherit from parent menu
929         String skin = definition.getSkin();
930         if (skin == null)
931         {
932             skin = super.getSkin();
933         }
934         return skin;
935     }
936 
937     /***
938      * getUrl - get url of top level folder that defined
939      *          menu options; only available for menus
940      *          defined without multiple options, nested
941      *          menus, or separators
942      *
943      * @return folder url
944      */
945     public String getUrl()
946     {
947         // return url of node associated with menu
948         // option if defined
949         if (getNode() != null)
950         {
951             return getNode().getUrl();
952         }
953         return null;
954     }
955 
956     /***
957      * isHidden - get hidden state of folder that defined
958      *            menu options; only available for menus
959      *            defined without multiple options, nested
960      *            menus, or separators
961      *
962      * @return hidden state
963      */
964     public boolean isHidden()
965     {
966         // return hidden state of node associated with
967         // menu option if defined
968         if (getNode() != null)
969         {
970             return getNode().isHidden();
971         }
972         return false;
973     }
974 
975     /***
976      * isSelected - return true if an option or nested
977      *              menu within this menu are selected by
978      *              the specified request context
979      *
980      * @param context request context
981      * @return selected state
982      */
983     public boolean isSelected(PortalSiteRequestContext context)
984     {
985         // menu is selected if a selected element exists
986         return (getSelectedElement(context) != null);
987     }
988 
989     /***
990      * getElements - get ordered list of menu elements that
991      *               are members of this menu; possibly contains
992      *               options, nested menus, or separators
993      *
994      * @return menu elements list
995      */
996     public List getElements()
997     {
998         return elements;
999     }
1000 
1001     /***
1002      * isEmpty - get empty state of list of menu elements
1003      *
1004      * @return menu elements list empty state
1005      */
1006     public boolean isEmpty()
1007     {
1008         return ((elements == null) || elements.isEmpty());
1009     }
1010 
1011     /***
1012      * isElementRelative - get flag that indicates whether any relative paths
1013      *                     dependent on the current page in context were
1014      *                     referenced while constructing menu elements
1015      *
1016      * @return relative element status
1017      */
1018     public boolean isElementRelative()
1019     {
1020         return elementRelative;
1021     }
1022 
1023     /***
1024      * getSelectedElement - return selected option or nested
1025      *                      menu within this menu selected by
1026      *                      the specified request context
1027      *
1028      * @return selected menu element
1029      */
1030     public MenuElement getSelectedElement(PortalSiteRequestContext context)
1031     {
1032         // test nested menu and option menu
1033         // elements for selected status
1034         if (elements != null)
1035         {
1036             Iterator elementsIter = elements.iterator();
1037             while (elementsIter.hasNext())
1038             {
1039                 MenuElement element = (MenuElement)elementsIter.next();
1040                 
1041                 // test element selected
1042                 boolean selected = false;
1043                 if (element instanceof MenuOption)
1044                 {
1045                     selected = ((MenuOption)element).isSelected(context);
1046                 }
1047                 else if (element instanceof Menu)
1048                 {
1049                     selected = ((Menu)element).isSelected(context);
1050                 }
1051                 
1052                 // return selected element
1053                 if (selected)
1054                 {
1055                     return element;
1056                 }
1057             }
1058         }
1059         return null;
1060     }
1061 
1062     /***
1063      * orderRegexpPattern - tests for and converts simple order wildcard
1064      *                      and character class regular exressions to
1065      *                      perl5/standard java pattern syntax
1066      *
1067      * @param regexp - candidate order regular expression
1068      * @return - converted pattern
1069      */
1070     private static String orderRegexpPattern(String regexp)
1071     {
1072         // convert expression to pattern
1073         StringBuffer pattern = null;
1074         for (int i = 0, limit = regexp.length(); (i < limit); i++)
1075         {
1076             char regexpChar = regexp.charAt(i);
1077             switch (regexpChar)
1078             {
1079                 case '*':
1080                 case '.':
1081                 case '?':
1082                 case '[':
1083                     if (pattern == null)
1084                     {
1085                         pattern = new StringBuffer(regexp.length()*2);
1086                         pattern.append(regexp.substring(0, i));
1087                     }
1088                     switch (regexpChar)
1089                     {
1090                         case '*':
1091                             pattern.append("[^"+Folder.PATH_SEPARATOR+"]*");
1092                             break;
1093                         case '.':
1094                             pattern.append("//.");
1095                             break;
1096                         case '?':
1097                             pattern.append("[^"+Folder.PATH_SEPARATOR+"]");
1098                             break;
1099                         case '[':
1100                             pattern.append('[');
1101                             break;
1102                     }
1103                     break;
1104                 default:
1105                     if (pattern != null)
1106                     {
1107                         pattern.append(regexpChar);
1108                     }
1109                     break;
1110             }
1111         }
1112 
1113         // return converted pattern
1114         if (pattern != null)
1115             return pattern.toString();
1116         return regexp;
1117     }
1118 }