View Javadoc
1   /**
2    * This file Copyright (c) 2003-2014 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.cms.gui.control;
35  
36  import info.magnolia.cms.core.Content;
37  import info.magnolia.cms.core.HierarchyManager;
38  import info.magnolia.cms.core.ItemType;
39  import info.magnolia.cms.core.MgnlNodeType;
40  import info.magnolia.cms.core.NodeData;
41  import info.magnolia.cms.core.Path;
42  import info.magnolia.cms.util.MetaDataUtil;
43  import info.magnolia.cms.util.NodeDataUtil;
44  import info.magnolia.context.MgnlContext;
45  import info.magnolia.freemarker.FreemarkerUtil;
46  
47  import java.util.ArrayList;
48  import java.util.Calendar;
49  import java.util.Collection;
50  import java.util.Collections;
51  import java.util.Comparator;
52  import java.util.HashMap;
53  import java.util.HashSet;
54  import java.util.Iterator;
55  import java.util.List;
56  import java.util.Map;
57  import java.util.Set;
58  import java.util.TreeSet;
59  
60  import javax.jcr.PropertyType;
61  import javax.jcr.RepositoryException;
62  import javax.jcr.Value;
63  import javax.jcr.ValueFactory;
64  import javax.jcr.ValueFormatException;
65  import javax.servlet.http.HttpServletRequest;
66  
67  //import org.apache.commons.httpclient.util.DateUtil;
68  import org.apache.commons.lang.StringEscapeUtils;
69  import org.apache.commons.lang.StringUtils;
70  import org.slf4j.Logger;
71  import org.slf4j.LoggerFactory;
72  
73  /**
74   * @author Vinzenz Wyser
75   * @version 2.0
76   */
77  public class Tree extends ControlImpl {
78  
79      public static final String DOCROOT = "/.resources/controls/tree/"; //$NON-NLS-1$
80  
81      public static final String ICONDOCROOT = "/.resources/icons/16/"; //$NON-NLS-1$
82  
83      public static final String DEFAULT_ICON = ICONDOCROOT + "cubes.gif";
84  
85      public static final String DEFAULT_ICON_CONTENT = ICONDOCROOT + "document_plain_earth.gif";
86  
87      public static final String DEFAULT_ICON_CONTENTNODE = DEFAULT_ICON;
88  
89      public static final String DEFAULT_ICON_NODEDATA = ICONDOCROOT + "cube_green.gif";
90  
91      public static final String DEFAULT_ICON_DELETED = ICONDOCROOT + "document_deleted.gif";
92  
93      /**
94       * @deprecated since 4.4, use ItemType.MGNL_NODE_DATA instead
95       */
96      @Deprecated
97      public static final String ITEM_TYPE_NODEDATA = "mgnl:nodeData";
98  
99      public static final int ACTION_MOVE = 0;
100 
101     public static final int ACTION_COPY = 1;
102 
103     public static final int ACTION_ACTIVATE = 2;
104 
105     public static final int ACTION_DEACTIVATE = 3;
106 
107     public static final int PASTETYPE_ABOVE = 0;
108 
109     public static final int PASTETYPE_BELOW = 1;
110 
111     public static final int PASTETYPE_SUB = 2;
112 
113     public static final int PASTETYPE_LAST = 3;
114 
115     /**
116      * Logger.
117      */
118     private static Logger log = LoggerFactory.getLogger(Tree.class);
119 
120     private String repository;
121 
122     private String pathOpen;
123 
124     private String pathCurrent;
125 
126     private String pathSelected;
127 
128     private String rootPath;
129 
130     private int indentionWidth = 15;
131 
132     private final List<String> itemTypes = new ArrayList<String>();
133 
134     private final Set<String> strictTypes = new HashSet<String>();
135 
136     private int height = 400;
137 
138     private final Map<String, String> icons = new HashMap<String, String>();
139 
140     private String iconOndblclick;
141 
142     private String shifterExpand = DOCROOT + "shifter_EXPAND.gif"; //$NON-NLS-1$
143 
144     private String shifterCollaspe = DOCROOT + "shifter_COLLAPSE.gif"; //$NON-NLS-1$
145 
146     private String shifterEmpty = DOCROOT + "shifter_EMPTY.gif"; //$NON-NLS-1$
147 
148     private boolean drawShifter = true;
149 
150     private String javascriptTree = "mgnlTreeControl"; //$NON-NLS-1$
151 
152     private List columns = new ArrayList();
153 
154     private ContextMenu menu;
155 
156     private Comparator sortComparator;
157 
158     /**
159      * the bar at the bottom of the page holding some function buttons
160      */
161     private FunctionBar functionBar;
162 
163     private boolean snippetMode = true;
164 
165     private String columnResizer = DOCROOT + "columnResizer.gif"; //$NON-NLS-1$
166 
167     private boolean browseMode;
168 
169     /**
170      * Use the getter method to access this HierarchyManager.
171      */
172     private HierarchyManager hm;
173 
174     /**
175      * @deprecated don't pass the request
176      */
177     @Deprecated
178     public Tree(String name, String repository, HttpServletRequest request) {
179         this(name, repository);
180     }
181 
182     /**
183      * Constructor.
184      * 
185      * @param name name of the tree (name of the treehandler)
186      * @param repository name of the repository (i.e. "website", "users")
187      */
188     public Tree(String name, String repository) {
189         this.setName(name);
190         this.setRepository(repository);
191         this.setMenu(new ContextMenu(this.getJavascriptTree()));
192         this.setFunctionBar(new FunctionBar(this.getJavascriptTree()));
193 
194         this.setHierarchyManager(MgnlContext.getHierarchyManager(this.getRepository()));
195 
196         // set default icons
197         this.addIcon(MgnlNodeType.NT_CONTENT, DEFAULT_ICON_CONTENT);
198         this.addIcon(MgnlNodeType.NT_CONTENTNODE, DEFAULT_ICON_CONTENTNODE);
199         this.addIcon(MgnlNodeType.MGNL_NODE_DATA, DEFAULT_ICON_NODEDATA);
200         this.addIcon(MgnlNodeType.MIX_DELETED, DEFAULT_ICON_DELETED);
201     }
202 
203     /**
204      * Constructor: the name of the tree is the same as the name of the repository
205      * 
206      * @param repository
207      * @param request
208      * @deprecated use Tree(name, repository) instead
209      */
210     @Deprecated
211     public Tree(String repository, HttpServletRequest request) {
212         this(repository, repository, request);
213     }
214 
215     public void setRepository(String s) {
216         this.repository = s;
217     }
218 
219     public String getRepository() {
220         return this.repository;
221     }
222 
223     public void setPathOpen(String s) {
224         this.pathOpen = s;
225     }
226 
227     public String getPathOpen() {
228         return this.pathOpen;
229     }
230 
231     /**
232      * Sets which path will be selected (and opened - overwrites pathOpen).
233      * 
234      * @param s
235      */
236     public void setPathSelected(String s) {
237         if (StringUtils.isNotEmpty(s)) {
238             if (StringUtils.isEmpty(this.getPathOpen())) {
239                 this.setPathOpen(StringUtils.substringBeforeLast(s, "/")); //$NON-NLS-1$
240             }
241         }
242         this.pathSelected = s;
243     }
244 
245     public String getPathSelected() {
246         return this.pathSelected;
247     }
248 
249     @Override
250     public String getPath() {
251         if (super.getPath() != null) {
252             return super.getPath();
253         }
254 
255         return ("/"); //$NON-NLS-1$
256     }
257 
258     protected void setPathCurrent(String s) {
259         this.pathCurrent = s;
260     }
261 
262     protected String getPathCurrent() {
263         return this.pathCurrent;
264     }
265 
266     public void setIndentionWidth(int i) {
267         this.indentionWidth = i;
268     }
269 
270     public int getIndentionWidth() {
271         return this.indentionWidth;
272     }
273 
274     public List getItemTypes() {
275         return this.itemTypes;
276     }
277 
278     /**
279      * Add a itemType to the itemTypes that will be shown in this branch.
280      * 
281      * @deprecated pass the icon to use as a second parameter
282      */
283     @Deprecated
284     public void addItemType(String s) {
285         addItemType(s, null);
286     }
287 
288     /**
289      * Adds an itemType to the list of existing types and associates an icon with it.
290      * This method will cause only items of given type to be displayed. The items of subtype of given type will not be displayed.
291      * To display also subtypes see {@link Tree#addItemType(String, String, boolean)}
292      * 
293      * @param type The item type
294      * @param icon The icon used to display items of given type
295      * @see #addItemType(String, String, boolean)
296      */
297     public void addItemType(String type, String icon) {
298         addItemType(type, icon, true);
299     }
300 
301     public void addItemType(String type, boolean strict) {
302         addItemType(type, null, strict);
303     }
304 
305     /**
306      * Adds an itemType to the list of existing types and associates an icon with it.
307      * The type can be specified as a strict, in which case only the items of given type will be displayed, but sub types will be ignored,
308      * or non-strict in which case the items of sub types of given type will also be rendered.
309      * 
310      * @param type The item type
311      * @param icon The icon used to display items of given type
312      * @param strict Flag specifying whether or not to show also items of sub type of given type
313      */
314     public void addItemType(String type, String icon, boolean strict) {
315         this.itemTypes.add(type);
316         if (icon != null) {
317             setIcon(type, icon);
318         }
319         if (strict) {
320             strictTypes.add(type);
321         } else {
322             strictTypes.remove(type);
323         }
324     }
325 
326     /**
327      * Add a itemType to the itemTypes that will be shown in this branch.
328      * 
329      * @param s itemType (one of: ItemType.CONTENT, ItemType.CONTENTNODE)
330      */
331     public void addItemType(ItemType s) {
332         addItemType(s.getSystemName(), null);
333     }
334 
335     /**
336      * Set the icon of pages.
337      * 
338      * @param src source of the image
339      * @deprecated since 4.4, use addIcon(ItemType.CONTENT.getSystemName(), src) instead
340      */
341     @Deprecated
342     public void setIconPage(String src) {
343         this.setIcon(ItemType.CONTENT.getSystemName(), src);
344     }
345 
346     public String getIconPage() {
347         return this.getIcon(ItemType.CONTENT.getSystemName());
348     }
349 
350     /**
351      * Set the icon of content nodes.
352      * 
353      * @param src source of the image
354      * @deprecated since 4.4, use addIcon(ItemType.CONTENTNODE.getSystemName(), src) instead
355      */
356     @Deprecated
357     public void setIconContentNode(String src) {
358         this.setIcon(ItemType.CONTENTNODE.getSystemName(), src);
359     }
360 
361     public String getIconContentNode() {
362         return this.getIcon(ItemType.CONTENTNODE.getSystemName());
363     }
364 
365     /**
366      * Set the icon of node data.
367      * 
368      * @param src source of the image
369      * @deprecated since 4.4, use addIcon(ItemType.MGNL_NODE_DATA, src) instead
370      */
371     @Deprecated
372     public void setIconNodeData(String src) {
373         this.setIcon(ITEM_TYPE_NODEDATA, src);
374     }
375 
376     public String getIconNodeData() {
377         return this.getIcon(ITEM_TYPE_NODEDATA);
378     }
379 
380     protected String getIcon(String itemType) {
381         return icons.get(itemType);
382     }
383 
384     protected String getIcon(Content node, NodeData nodedata, String itemType) {
385         // deleted icon trumps them all.
386         try {
387             if (node != null && node.hasMixin(ItemType.DELETED_NODE_MIXIN)) {
388                 return icons.get(ItemType.DELETED_NODE_MIXIN);
389             }
390         } catch (RepositoryException e) {
391             log.error("Failed to read content of " + node.getHandle());
392         }
393         if (icons.containsKey(itemType)) {
394             return icons.get(itemType);
395         }
396         return DEFAULT_ICON;
397     }
398 
399     /**
400      * @deprecated since 4.4 use {@link #addIcon(String, String)} instead.
401      */
402     @Deprecated
403     public void setIcon(String typeName, String icon) {
404         addIcon(typeName, icon);
405     }
406 
407     public void addIcon(String typeName, String icon) {
408         icons.put(typeName, icon);
409     }
410 
411     /**
412      * Set the double click event of the icon.
413      * 
414      * @param s javascriopt method
415      */
416     public void setIconOndblclick(String s) {
417         this.iconOndblclick = s;
418     }
419 
420     public String getIconOndblclick() {
421         return this.iconOndblclick;
422     }
423 
424     /**
425      * Set the shifter image (expand branch). "_EXPAND" in file name will be replaced by "_COLLAPSE" after expanding
426      * e.g. myShifterIcon_EXPAND.gif
427      * 
428      * @param src source of the image
429      */
430     public void setShifterExpand(String src) {
431         this.shifterExpand = src;
432     }
433 
434     public String getShifterExpand() {
435         return this.shifterExpand;
436     }
437 
438     /**
439      * Set the shifter image (collapse branch). "_COLLAPSE" in file name will be replaced by "_EXPAND" after collapsing
440      * e.g. myShifterIcon_COLLAPSE.gif
441      * 
442      * @param src source of the image
443      */
444     public void setShifterCollapse(String src) {
445         this.shifterCollaspe = src;
446     }
447 
448     public String getShifterCollapse() {
449         return this.shifterCollaspe;
450     }
451 
452     /**
453      * Set the shifter image when no children are available (not expandable). "_EMPTY" in the file name will be replaced
454      * when children are available e.g. myShifterIcon_EMPTY.gif
455      * 
456      * @param src source of the image
457      */
458     public void setShifterEmpty(String src) {
459         this.shifterEmpty = src;
460     }
461 
462     public String getShifterEmpty() {
463         return this.shifterEmpty;
464     }
465 
466     public void setDrawShifter(boolean b) {
467         this.drawShifter = b;
468     }
469 
470     public boolean getDrawShifter() {
471         return this.drawShifter;
472     }
473 
474     public void setHeight(int i) {
475         this.height = i;
476     }
477 
478     public int getHeight() {
479         return this.height;
480     }
481 
482     /**
483      * Set the columns (for pages and content nodes only).
484      * 
485      * @param al list of TreeColumns
486      */
487     public void setColums(List al) {
488         this.columns = al;
489     }
490 
491     public List getColumns() {
492         return this.columns;
493     }
494 
495     public TreeColumn getColumns(int col) {
496         return (TreeColumn) this.getColumns().get(col);
497     }
498 
499     public void addColumn(TreeColumn tc) {
500         tc.setJavascriptTree(this.getJavascriptTree());
501         this.getColumns().add(tc);
502     }
503 
504     /**
505      * Set the name of the javascript tree object.
506      * 
507      * @param variableName
508      */
509     public void setJavascriptTree(String variableName) {
510         this.javascriptTree = variableName;
511         this.menu.setName(variableName + "Menu"); //$NON-NLS-1$
512     }
513 
514     public String getJavascriptTree() {
515         return this.javascriptTree;
516     }
517 
518     /**
519      * Sets if only a snippet (requested branch) shall be returnde or including the surounding html (tree header, js/css
520      * links etc).
521      * 
522      * @param b true: snippet only
523      */
524     public void setSnippetMode(boolean b) {
525         this.snippetMode = b;
526     }
527 
528     public boolean getSnippetMode() {
529         return this.snippetMode;
530     }
531 
532     // @todo: set size of column resizer gif and pass it to js object
533     public void setColumnResizer(String src) {
534         this.columnResizer = src;
535     }
536 
537     public String getColumnResizer() {
538         return this.columnResizer;
539     }
540 
541     public String createNode(String itemType) {
542         return this.createNode("untitled", itemType); //$NON-NLS-1$
543     }
544 
545     /**
546      * Creates a new node (either <code>NodeData</code> or <code>Content</code>) with the specified name (<tt>label</tt>)
547      * and type.
548      * 
549      * @param label new node name
550      * @param itemType new node type
551      */
552     public String createNode(String label, String itemType) {
553         try {
554             Content parentNode = getHierarchyManager().getContent(this.getPath());
555             String name = getUniqueLabel(label);
556             if (itemType.equals(ITEM_TYPE_NODEDATA)) {
557                 parentNode.createNodeData(name);
558             } else {
559                 Content newNode;
560                 newNode = parentNode.createContent(name, itemType);
561                 newNode.getMetaData().setAuthorId(MgnlContext.getUser().getName());
562                 newNode.getMetaData().setCreationDate();
563                 newNode.getMetaData().setModificationDate();
564             }
565             parentNode.save();
566             return name;
567         } catch (Exception e) {
568             log.error(e.getMessage(), e);
569             return StringUtils.EMPTY; // reset the name, so that you can check if the node was created
570         }
571 
572     }
573 
574     public String saveNodeData(String nodeDataName, String value, boolean isMeta) {
575         String returnValue = StringUtils.EMPTY;
576         try {
577             Content content = getHierarchyManager().getContent(this.getPath());
578             if (!isMeta) {
579                 NodeData node;
580                 int type = PropertyType.STRING;
581                 if (!content.getNodeData(nodeDataName).isExist()) {
582                     node = content.createNodeData(nodeDataName);
583                 }
584                 else {
585                     node = content.getNodeData(nodeDataName);
586                     type = node.getType();
587                 }
588                 // todo: share with Contorol.Save
589                 if (node.isMultiValue() != NodeData.MULTIVALUE_TRUE) {
590                     switch (type) {
591                     case PropertyType.STRING:
592                         node.setValue(value);
593                         break;
594                     case PropertyType.BOOLEAN:
595                         if (value.equals("true")) { //$NON-NLS-1$
596                             node.setValue(true);
597                         }
598                         else {
599                             node.setValue(false);
600                         }
601                         break;
602                     case PropertyType.DOUBLE:
603                         try {
604                             node.setValue(Double.valueOf(value).doubleValue());
605                         } catch (Exception e) {
606                             node.setValue(0);
607                         }
608                         break;
609                     case PropertyType.LONG:
610                         try {
611                             node.setValue(Long.valueOf(value).longValue());
612                         } catch (Exception e) {
613                             node.setValue(0);
614                         }
615                         break;
616                     case PropertyType.DATE:
617                         this.setDate(node, value);
618                         break;
619                     case PropertyType.BINARY: // not editable
620                         break;
621                     }
622                 }
623                 content.updateMetaData();
624                 content.save();
625                 returnValue = NodeDataUtil.getValueString(node);
626             }
627             else {
628                 content.getMetaData().setProperty(nodeDataName, value);
629                 content.updateMetaData();
630                 content.save();
631                 returnValue = MetaDataUtil.getPropertyValueString(content, nodeDataName);
632             }
633         } catch (Exception e) {
634             log.warn("Exception caught: " + e.getMessage(), e);
635         }
636         return returnValue;
637     }
638 
639     public String saveNodeDataType(String nodeDataName, int type) {
640         try {
641             Content content = getHierarchyManager().getContent(this.getPath());
642             Value value = null;
643             if (content.getNodeData(nodeDataName).isExist()) {
644                 value = content.getNodeData(nodeDataName).getValue();
645                 content.deleteNodeData(nodeDataName);
646             }
647             NodeData node = content.createNodeData(nodeDataName, type);
648 
649             if (value != null && node.isMultiValue() != NodeData.MULTIVALUE_TRUE) {
650                 switch (type) {
651                 case PropertyType.STRING:
652                     node.setValue(value.getString());
653                     break;
654                 case PropertyType.BOOLEAN:
655                     if (value != null && value.getBoolean()) {
656                         node.setValue(true);
657                     }
658                     else {
659                         node.setValue(false);
660                     }
661                     break;
662                 case PropertyType.DOUBLE:
663                     try {
664                         node.setValue(value.getDouble());
665                     } catch (Exception e) {
666                         node.setValue(0);
667                     }
668                     break;
669                 case PropertyType.LONG:
670                     try {
671                         node.setValue(value.getLong());
672                     } catch (Exception e) {
673                         node.setValue(0);
674                     }
675                     break;
676                 case PropertyType.DATE:
677                     this.setDate(node, value.getString());
678                     break;
679                 case PropertyType.BINARY: // cannot change to binary type
680                     break;
681                 }
682             }
683             content.updateMetaData();
684             content.save();
685             return PropertyType.nameFromValue(content.getNodeData(nodeDataName).getType());
686         } catch (Exception e) {
687             log.warn("Exception caught: " + e.getMessage(), e);
688         }
689         return StringUtils.EMPTY;
690     }
691 
692     private void setDate(NodeData node, String value) throws RepositoryException {
693         Value calendar = null;
694         ValueFactory factory = node.getHierarchyManager().getWorkspace().getSession().getValueFactory();
695         try {
696             calendar = factory.createValue(value, PropertyType.DATE);
697         } catch (ValueFormatException e) { // cannot be converted
698             log.debug("Cannot convert to date: {}", value);
699             calendar = factory.createValue(Calendar.getInstance());
700         }
701         node.setValue(calendar);
702     }
703 
704     /**
705      * @param label
706      * @return unique label
707      */
708     protected String getUniqueLabel(String label) {
709         String slash = "/"; //$NON-NLS-1$
710         boolean isRoot = false;
711         if ("/".equals(getPath())) { //$NON-NLS-1$
712             isRoot = true;
713             slash = StringUtils.EMPTY;
714         }
715         if (getHierarchyManager().isExist(this.getPath() + slash + label)) {
716             // todo: bugfix getUniqueLabel???
717             if (isRoot) {
718                 label = Path.getUniqueLabel(getHierarchyManager(), StringUtils.EMPTY, label);
719             } else {
720                 label = Path.getUniqueLabel(getHierarchyManager(), this.getPath(), label);
721             }
722         }
723         return label;
724     }
725 
726     @Override
727     public String getHtml() {
728         StringBuffer html = new StringBuffer();
729         html.append(this.getHtmlPre());
730 
731         if (!this.getSnippetMode()) {
732             html.append(this.getHtmlHeader());
733         }
734         this.setPathCurrent(this.getPath());
735         html.append(this.getHtmlChildren());
736         if (!this.getSnippetMode()) {
737             html.append(this.getHtmlFooter());
738         }
739         html.append(this.getHtmlPost());
740         return html.toString();
741     }
742 
743     public String getHtmlHeader() {
744 
745         StringBuffer str = new StringBuffer();
746         try {
747             Map params = populateTemplateParameters();
748             str.append(FreemarkerUtil.process("info/magnolia/cms/gui/control/TreeHeader.ftl", params));
749         } catch (Exception e) {
750             log.error("can't render tree header", e);
751         }
752         return str.toString();
753     }
754 
755     public String getHtmlFooter() {
756         StringBuffer html = new StringBuffer();
757         html.append("</div>"); //$NON-NLS-1$
758 
759         Map params = populateTemplateParameters();
760 
761         // include the tree footer / menu divs
762         html.append(FreemarkerUtil.process("info/magnolia/cms/gui/control/TreeFooter.ftl", params));
763 
764         return html.toString();
765     }
766 
767     protected Map populateTemplateParameters() {
768         boolean permissionWrite = true;
769         try {
770             Content root = getHierarchyManager().getContent(this.getPath());
771             permissionWrite = root.isGranted(info.magnolia.cms.security.Permission.WRITE);
772         } catch (RepositoryException e) {
773             if (log.isDebugEnabled()) {
774                 log.debug("Exception caught: " + e.getMessage(), e); //$NON-NLS-1$
775             }
776         }
777 
778         // lineInter: line between nodes, to allow set cursor between nodes
779         // line to place a very last position
780         String lineId = this.getJavascriptTree() + "_" + this.getPath() + "_LineInter"; //$NON-NLS-1$ //$NON-NLS-2$
781 
782         // prepare the data for the templates
783         Map<String, Object> params = new HashMap<String, Object>();
784         params.put("tree", this);
785         params.put("PASTETYPE_SUB", new Integer(Tree.PASTETYPE_SUB));
786         params.put("DOCROOT", Tree.DOCROOT);
787         params.put("lineId", lineId);
788         params.put("permissionWrite", Boolean.valueOf(permissionWrite));
789         params.put("columns", this.getColumns());
790         params.put("menu", this.getMenu());
791         params.put("treeCssClass", "mgnlTreeDiv");
792         if (this.isBrowseMode()) {
793             params.put("treeCssClass", "mgnlTreeBrowseModeDiv mgnlTreeDiv");
794         }
795         return params;
796     }
797 
798     public String getHtmlBranch() {
799         return StringUtils.EMPTY;
800     }
801 
802     public String getHtmlChildren() {
803         StringBuffer html = new StringBuffer();
804         Content parentNode;
805         try {
806             parentNode = getHierarchyManager().getContent(this.getPathCurrent());
807             // loop the children of the different item types
808             for (int i = 0; i < this.getItemTypes().size(); i++) {
809                 String type = (String) this.getItemTypes().get(i);
810                 if (hasSub(parentNode, type)) {
811                     html.append(this.getHtmlChildrenOfOneType(parentNode, type));
812                 }
813             }
814         } catch (RepositoryException e) {
815             if (log.isDebugEnabled()) {
816                 log.debug("Exception caught: " + e.getMessage(), e); //$NON-NLS-1$
817             }
818         }
819         return html.toString();
820     }
821 
822     public String getHtmlChildrenOfOneType(Content parentNode, String itemType) {
823         StringBuffer html = new StringBuffer();
824         try {
825             Iterator it = collectRenderedItems(parentNode, itemType);
826             while (it.hasNext()) {
827                 Object maybeContent = it.next();
828                 if (maybeContent instanceof Content) {
829                     Content c = (Content) maybeContent;
830                     // ensure no subtypes of strict types are included by mistake
831                     if (strictTypes.contains(itemType) && !c.getItemType().getSystemName().equals(itemType)) {
832                         continue;
833                     }
834                 }
835                 getHtmlOfSingleItem(html, parentNode, itemType, maybeContent);
836             }
837         } catch (RepositoryException e) {
838             if (log.isDebugEnabled()) {
839                 log.debug("Exception caught: " + e.getMessage(), e); //$NON-NLS-1$
840             }
841         }
842         return html.toString();
843     }
844 
845     protected void getHtmlOfSingleItem(StringBuffer html, Content parentNode, String itemType, Object item) throws RepositoryException {
846         Content c = null;
847         NodeData d = null;
848         String handle;
849         String name;
850         boolean hasSub = false;
851         boolean showSub = false;
852         boolean isActivated = false;
853         boolean isDeleted = false;
854         boolean permissionWrite = false;
855         boolean permissionWriteParent = false;
856         if (itemType.equals(ITEM_TYPE_NODEDATA)) {
857             d = (NodeData) item;
858             handle = d.getHandle();
859             name = d.getName();
860 
861             if (d.isGranted(info.magnolia.cms.security.Permission.WRITE) && d.isMultiValue() != NodeData.MULTIVALUE_TRUE) {
862                 permissionWrite = true;
863             }
864         }
865         else {
866             c = (Content) item;
867 
868             handle = c.getHandle();
869             if (this.getColumns().size() == 0) {
870                 name = c.getName();
871             }
872             else {
873                 this.getColumns(0).setWebsiteNode(c);
874                 name = this.getColumns(0).getHtml();
875             }
876             if (c.isGranted(info.magnolia.cms.security.Permission.WRITE)) {
877                 permissionWrite = true;
878             }
879             if (c.hasMixin(ItemType.DELETED_NODE_MIXIN)) {
880                 isDeleted = true;
881             }
882             if (c.getAncestor(c.getLevel() - 1).isGranted(info.magnolia.cms.security.Permission.WRITE)) {
883                 permissionWriteParent = true;
884             }
885             isActivated = c.getMetaData().getIsActivated();
886             for (int i = 0; i < this.getItemTypes().size(); i++) {
887                 String type = (String) this.getItemTypes().get(i);
888 
889                 hasSub = hasSub(c, type);
890 
891                 if (hasSub) {
892                     if (this.getPathOpen() != null
893                             && (this.getPathOpen().indexOf(handle + "/") == 0 || this.getPathOpen().equals(handle))) { //$NON-NLS-1$
894                         showSub = true;
895                     }
896                     break;
897                 }
898             }
899         }
900 
901         // get next if this node is not shown
902         if (!showNode(c, d, itemType)) {
903             return;
904         }
905 
906         String icon = getIcon(c, d, itemType);
907 
908         String idPre = this.javascriptTree + "_" + handle; //$NON-NLS-1$
909         String jsHighlightNode = this.javascriptTree + ".nodeHighlight(this,'" //$NON-NLS-1$
910                 + handle
911                 + "'," //$NON-NLS-1$
912                 + Boolean.toString(permissionWrite)
913                 + ");"; //$NON-NLS-1$
914         String jsResetNode = this.javascriptTree + ".nodeReset(this,'" + handle + "');"; //$NON-NLS-1$ //$NON-NLS-2$
915         String jsSelectNode = this.javascriptTree + ".selectNode('" //$NON-NLS-1$
916                 + handle
917                 + "'," //$NON-NLS-1$
918                 + Boolean.toString(permissionWrite)
919                 + ",'" //$NON-NLS-1$
920                 + itemType
921                 + "');"; //$NON-NLS-1$
922         String jsExpandNode;
923         if (this.getDrawShifter()) {
924             jsExpandNode = this.javascriptTree + ".expandNode('" + handle + "');"; //$NON-NLS-1$ //$NON-NLS-2$
925         }
926         else {
927             jsExpandNode = jsSelectNode;
928         }
929         String jsHighlightLine = this.javascriptTree + ".moveNodeHighlightLine('" + idPre + "_LineInter');"; //$NON-NLS-1$ //$NON-NLS-2$
930         String jsResetLine = this.javascriptTree + ".moveNodeResetLine('" + idPre + "_LineInter');"; //$NON-NLS-1$ //$NON-NLS-2$
931 
932         // lineInter: line between nodes, to allow set cursor between nodes
933         // try to avoid blank images, setting js actions on divs should be ok
934         if (permissionWriteParent) {
935             html.append("<div id=\"");
936             html.append(idPre);
937             html.append("_LineInter\" class=\"mgnlTreeLineInter mgnlLineEnabled\" onmouseover=\"");
938             html.append(jsHighlightLine);
939             html.append("\" onmouseout=\"");
940             html.append(jsResetLine);
941             html.append("\" onmousedown=\"");
942             html.append(this.javascriptTree);
943             html.append(".pasteNode('");
944             html.append(handle);
945             html.append("'," + Tree.PASTETYPE_ABOVE + ",true);\" ></div>");
946         }
947         else {
948             html.append("<div id=\"");
949             html.append(idPre);
950             html.append("_LineInter\" class=\"mgnlTreeLineInter mgnlLineDisabled\"></div>");
951         }
952 
953         html.append("<div id=\"");
954         html.append(idPre);
955         html.append("_DivMain\" style=\"position:relative;top:0;left:0;width:100%;height:18px;\">");
956         html.append("&nbsp;"); // do not remove! //$NON-NLS-1$
957 
958         html.append("<span id=\"");
959         html.append(idPre);
960         html.append("_Column0Outer\" class=\"mgnlTreeColumn ");
961         html.append(this.javascriptTree);
962         html.append("CssClassColumn0\" style=\"padding-left:");
963 
964         html.append(getPaddingLeft(parentNode));
965         html.append("px;\">");
966         if (this.getDrawShifter()) {
967             String shifter = StringUtils.EMPTY;
968             if (hasSub) {
969                 if (showSub) {
970                     if (this.getShifterCollapse() != null) {
971                         shifter = this.getShifterCollapse();
972                     }
973                 }
974                 else {
975                     if (this.getShifterExpand() != null) {
976                         shifter = this.getShifterExpand();
977                     }
978                 }
979             }
980             else {
981                 if (this.getShifterEmpty() != null) {
982                     shifter = this.getShifterEmpty();
983                 }
984             }
985             if (StringUtils.isNotEmpty(shifter)) {
986                 html.append("<img id=\"");
987                 html.append(idPre);
988                 html.append("_Shifter\" onmousedown=\"");
989                 html.append(this.javascriptTree);
990                 html.append(".shifterDown('");
991                 html.append(handle);
992                 html.append("');\" onmouseout=\"");
993                 html.append(this.javascriptTree);
994                 html.append(".shifterOut();\" class=\"mgnlTreeShifter\" src=\"");
995                 html.append(this.getRequest().getContextPath());
996                 html.append(shifter);
997                 html.append("\" />");
998             }
999         }
1000         html.append("<span id=\"");
1001         html.append(idPre);
1002         html.append("_Name\" onmouseover=\"");
1003         html.append(jsHighlightNode);
1004         html.append("\" onmouseout=\"");
1005         html.append(jsResetNode);
1006         html.append("\" onmousedown=\"");
1007         html.append(jsSelectNode);
1008         html.append(this.javascriptTree);
1009         html.append(".pasteNode('");
1010         html.append(handle);
1011         html.append("'," + Tree.PASTETYPE_SUB + ",");
1012         html.append(permissionWrite);
1013         html.append(");\">");
1014         if (StringUtils.isNotEmpty(icon)) {
1015             html.append("<img id=\"");
1016             html.append(idPre);
1017             html.append("_Icon\" class=\"mgnlTreeIcon\" src=\"");
1018             html.append(this.getRequest().getContextPath());
1019             html.append(icon);
1020             html.append("\" onmousedown=\"");
1021             html.append(jsExpandNode);
1022             html.append("\"");
1023             if (this.getIconOndblclick() != null) {
1024                 html.append(" ondblclick=\"");
1025                 html.append(this.getIconOndblclick());
1026                 html.append("\"");
1027             }
1028             html.append(" />"); //$NON-NLS-1$
1029         }
1030         String dblclick = StringUtils.EMPTY;
1031         String htmlEdit = this.getColumns(0).getHtmlEdit();
1032         if (permissionWrite && StringUtils.isNotEmpty(htmlEdit)) {
1033             dblclick = " ondblclick=\"" + this.javascriptTree + ".editNodeData(this,'" + handle + "',0,'" + StringUtils.replace(htmlEdit, "\"", "&quot;") + "');\""; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
1034         }
1035         html.append("<span class=\"mgnlTreeText\" id=\"");
1036         html.append(idPre);
1037         html.append("_Column0Main\"");
1038         html.append(dblclick);
1039         html.append(">");
1040         html.append(name);
1041         html.append("</span></span></span>"); //$NON-NLS-1$
1042 
1043         // this is done because js is not executed when you get it with ajax
1044         html.append(new Hidden(idPre + "_PermissionWrite", Boolean.toString(permissionWrite), false).getHtml()); //$NON-NLS-1$
1045         html.append(new Hidden(idPre + "_IsDeleted", Boolean.toString(isDeleted), false).getHtml()); //$NON-NLS-1$
1046         html.append(new Hidden(idPre + "_ItemType", itemType, false).getHtml()); //$NON-NLS-1$
1047         html.append(new Hidden(idPre + "_IsActivated", Boolean.toString(isActivated), false).getHtml()); //$NON-NLS-1$
1048 
1049         // Put your own stuff here. Good luck!
1050         onGetHtmlOfSingleItem(html, parentNode, itemType, item, idPre);
1051 
1052         for (int i = 1; i < this.getColumns().size(); i++) {
1053             String str = StringUtils.EMPTY;
1054             TreeColumn tc = this.getColumns(i);
1055             if (!itemType.equals(ITEM_TYPE_NODEDATA)) {
1056                 // content node ItemType.NT_CONTENTNODE and ItemType.NT_CONTENT
1057                 if (!tc.getIsNodeDataType() && !tc.getIsNodeDataValue()) {
1058                     tc.setWebsiteNode(c);
1059                     tc.setId(handle);
1060                     str = tc.getHtml();
1061                 }
1062             }
1063             else {
1064                 if (tc.getIsNodeDataType()) {
1065                     str = NodeDataUtil.getTypeName(d);
1066                 }
1067                 else if (tc.getIsNodeDataValue()) {
1068                     if (PropertyType.DATE == d.getType()) { // don't reformat date to make it easily editable
1069                         str = d.getString();
1070                     } else {
1071                         final String stringValue = NodeDataUtil.getValueString(d);
1072                         str = StringEscapeUtils.escapeXml(stringValue);
1073                     }
1074                 }
1075                 if (StringUtils.isEmpty(str)) {
1076                     str = TreeColumn.EMPTY;
1077                 }
1078                 tc.setName(name); // workaround, will be passed to js TreeColumn object
1079             }
1080             tc.setEvent("onmouseover", jsHighlightNode, true); //$NON-NLS-1$
1081             tc.setEvent("onmouseout", jsResetNode, true); //$NON-NLS-1$
1082             tc.setEvent("onmousedown", jsSelectNode, true); //$NON-NLS-1$
1083             html.append("<span class=\"mgnlTreeColumn ");
1084             html.append(this.javascriptTree);
1085             html.append("CssClassColumn");
1086             html.append(i);
1087             html.append("\"><span id=\"");
1088             html.append(idPre);
1089             html.append("_Column");
1090             html.append(i);
1091             html.append("Main\"");
1092             html.append(tc.getHtmlCssClass());
1093             html.append(tc.getHtmlEvents());
1094             htmlEdit = tc.getHtmlEdit();
1095             if (permissionWrite && StringUtils.isNotEmpty(htmlEdit) && (d == null || d.getType() != PropertyType.BINARY)) { // binary is noneditable
1096                 html.append(" ondblclick=\"");
1097                 html.append(this.javascriptTree);
1098                 html.append(".editNodeData(this,'");
1099                 html.append(handle);
1100                 html.append("',");
1101                 html.append(i);
1102                 html.append(",'");
1103                 html.append(StringUtils.replace(htmlEdit, "\"", "&quot;"));
1104                 html.append("');\"");
1105                 html.append(",'");
1106 
1107             }
1108             html.append(">");
1109             html.append(str);
1110             html.append("</span></span>");
1111         }
1112         html.append("</div>"); //$NON-NLS-1$
1113         String display = "none"; //$NON-NLS-1$
1114         if (showSub) {
1115             display = "block"; //$NON-NLS-1$
1116         }
1117         html.append("<div id=\"");
1118         html.append(idPre);
1119         html.append("_DivSub\" style=\"display:");
1120         html.append(display);
1121         html.append(";\">");
1122         if (hasSub) {
1123             if (showSub) {
1124                 String pathRemaining = this.getPathOpen().substring(this.getPathCurrent().length());
1125                 if (pathRemaining.length() > 0) {
1126                     // get rid of first slash (/people/franz -> people/franz)
1127                     String slash = "/"; //$NON-NLS-1$
1128                     if (this.getPathCurrent().equals("/")) { //$NON-NLS-1$
1129                         // first slash already removed
1130                         slash = StringUtils.EMPTY; // no slash needed between pathCurrent and nextChunk
1131                     }
1132                     else {
1133                         pathRemaining = pathRemaining.substring(1);
1134                     }
1135                     String nextChunk = StringUtils.substringBefore(pathRemaining, "/"); //$NON-NLS-1$
1136 
1137                     String pathNext = this.getPathCurrent() + slash + nextChunk;
1138                     this.setPathCurrent(pathNext);
1139                     html.append(this.getHtmlChildren());
1140                 }
1141             }
1142         }
1143         html.append("</div>\n"); //$NON-NLS-1$
1144 
1145     }
1146 
1147     protected void onGetHtmlOfSingleItem(StringBuffer html, Content parentNode, String itemType, Object item, String idPre) {
1148     }
1149 
1150     protected Iterator collectRenderedItems(Content parentNode, String itemType) {
1151         Iterator it;
1152         if (itemType.equalsIgnoreCase(ITEM_TYPE_NODEDATA)) {
1153             List nodeDatas = new ArrayList(parentNode.getNodeDataCollection());
1154             // order them alphabetically
1155             Collections.sort(nodeDatas, new Comparator() {
1156 
1157                 @Override
1158                 public int compare(Object arg0, Object arg1) {
1159                     return ((NodeData) arg0).getName().compareTo(((NodeData) arg1).getName());
1160                 }
1161             });
1162             it = nodeDatas.iterator();
1163         }
1164         else {
1165             Collection nodes = parentNode.getChildren(itemType);
1166             Comparator comp = this.getSortComparator();
1167             if (comp != null) {
1168                 Collection sortedNodes = new TreeSet(comp);
1169                 sortedNodes.addAll(nodes);
1170                 nodes = sortedNodes;
1171             }
1172             it = nodes.iterator();
1173         }
1174         return it;
1175     }
1176 
1177     protected int getPaddingLeft(Content parentNode) throws RepositoryException {
1178         int left = (parentNode.getLevel() - StringUtils.countMatches(getRootPath(), "/")) * this.getIndentionWidth();
1179         int paddingLeft = left + 8;
1180         if (paddingLeft < 8) {
1181             paddingLeft = 8;
1182         }
1183         return paddingLeft;
1184     }
1185 
1186     protected boolean hasSub(Content c, String type) {
1187         int size;
1188         if (type.equalsIgnoreCase(ITEM_TYPE_NODEDATA)) {
1189             size = c.getNodeDataCollection().size();
1190         }
1191         else {
1192             size = c.getChildren(type).size();
1193         }
1194         return size > 0;
1195     }
1196 
1197     /**
1198      * Override to make special exclusions. The current nodedata or node is passed.
1199      * 
1200      * @param node
1201      * @param nodedata
1202      * @param itemType
1203      * @return true if the node is shown
1204      */
1205     protected boolean showNode(Content node, NodeData nodedata, String itemType) {
1206         return true;
1207     }
1208 
1209     /**
1210      * @param item ContextMenuItem
1211      */
1212     public void addMenuItem(ContextMenuItem item) {
1213         menu.addMenuItem(item);
1214     }
1215 
1216     /**
1217      * @param item FunctionBarItem
1218      */
1219     public void addFunctionBarItem(FunctionBarItem item) {
1220         if (item != null) {
1221             item.setJavascriptMenuName(functionBar.getJavascriptName());
1222         }
1223         functionBar.addMenuItem(item);
1224     }
1225 
1226     /**
1227      * Convenience method to add a function bar item that already exists in the context menu.
1228      */
1229     public void addFunctionBarItemFromContextMenu(String itemName) {
1230         final ContextMenuItem menuItem = getMenu().getMenuItemByName(itemName);
1231         if (menuItem != null) {
1232             addFunctionBarItem(new FunctionBarItem(menuItem));
1233         }
1234     }
1235 
1236     /**
1237      * Add a separator line between context menu items.
1238      */
1239     public void addSeparator() {
1240         menu.addMenuItem(null);
1241     }
1242 
1243     public ContextMenu getMenu() {
1244         return this.menu;
1245     }
1246 
1247     /**
1248      * @return the function bar object
1249      */
1250     public FunctionBar getFunctionBar() {
1251         return this.functionBar;
1252     }
1253 
1254     public void setMenu(ContextMenu menu) {
1255         this.menu = menu;
1256     }
1257 
1258     /**
1259      * @param functionBar the function bar object
1260      */
1261     public void setFunctionBar(FunctionBar functionBar) {
1262         this.functionBar = functionBar;
1263     }
1264 
1265     /**
1266      * @return Returns the browseMode.
1267      */
1268     public boolean isBrowseMode() {
1269         return browseMode;
1270     }
1271 
1272     /**
1273      * @param browseMode The browseMode to set.
1274      */
1275     public void setBrowseMode(boolean browseMode) {
1276         this.browseMode = browseMode;
1277     }
1278 
1279     /**
1280      * @return the current HierarchyManager
1281      */
1282     public HierarchyManager getHierarchyManager() {
1283         return hm;
1284     }
1285 
1286     /**
1287      * @param hm The HierarchyManager to set.
1288      */
1289     private void setHierarchyManager(HierarchyManager hm) {
1290         this.hm = hm;
1291     }
1292 
1293     public Comparator getSortComparator() {
1294         return sortComparator;
1295     }
1296 
1297     public void setSortComparator(Comparator sortComperator) {
1298         this.sortComparator = sortComperator;
1299     }
1300 
1301     public String getRootPath() {
1302         return rootPath;
1303     }
1304 
1305     public void setRootPath(String rootPath) {
1306         this.rootPath = rootPath;
1307     }
1308 
1309 }