001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.jsp;
029
030import org.opencms.ade.sitemap.shared.CmsClientSitemapEntry;
031import org.opencms.file.CmsProperty;
032import org.opencms.file.CmsPropertyDefinition;
033import org.opencms.file.CmsResource;
034import org.opencms.i18n.CmsMessages;
035import org.opencms.jsp.CmsJspNavBuilder.NavContext;
036import org.opencms.main.CmsLog;
037import org.opencms.util.CmsCollectionsGenericWrapper;
038
039import java.util.Collections;
040import java.util.List;
041import java.util.Locale;
042import java.util.Map;
043
044import org.apache.commons.logging.Log;
045
046/**
047 * Bean to collect navigation information from a resource in the OpenCms VFS.<p>
048 *
049 * Each navigation element contains a number of information about a VFS resource,
050 * obtained either from the resources properties or attributes.
051 * You can use this information to generate a HTML navigation for
052 * files in the VFS in your template.<p>
053 *
054 * Note: this class has a natural ordering that is inconsistent with equals.<p>
055 *
056 * @since 6.0.0
057 *
058 * @see org.opencms.jsp.CmsJspNavBuilder
059 */
060public class CmsJspNavElement implements Comparable<CmsJspNavElement> {
061
062    /** The log instance for  this class. */
063    private static final Log LOG = CmsLog.getLog(CmsJspNavElement.class);
064
065    /** The locale for which the property should be read. */
066    protected Locale m_locale;
067
068    /** The navigation context. */
069    protected CmsJspNavBuilder.NavContext m_navContext;
070
071    /** The navigation position has changed flag. */
072    private boolean m_changedNavPos;
073
074    /** The file name. */
075    private String m_fileName;
076
077    /** The has navigation flag. */
078    private Boolean m_hasNav;
079
080    /** Flag indicating whether this is a hidden navigation entry. */
081    private Boolean m_isHiddenNavigationEntry;
082
083    /** The properties accessed according to the chosen locale. */
084    private Map<String, String> m_localeProperties;
085
086    /** The navigation tree level. */
087    private int m_navTreeLevel = Integer.MIN_VALUE;
088
089    /** The navigation position. */
090    private float m_position;
091
092    /** The properties. */
093    private Map<String, String> m_properties;
094
095    /** The resource. */
096    private CmsResource m_resource;
097
098    /** The site path. */
099    private String m_sitePath;
100
101    /** The navigation text. */
102    private String m_text;
103
104    private List<CmsJspNavElement> m_subNavigation;
105
106    /**
107     * Empty constructor required for every JavaBean, does nothing.<p>
108     *
109     * Call one of the init methods after you have created an instance
110     * of the bean. Instead of using the constructor you should use
111     * the static factory methods provided by this class to create
112     * navigation beans that are properly initialized with current
113     * OpenCms context.<p>
114     *
115     * @see CmsJspNavBuilder#getNavigationForResource()
116     * @see CmsJspNavBuilder#getNavigationForFolder()
117     * @see CmsJspNavBuilder#getNavigationTreeForFolder(int, int)
118     */
119    public CmsJspNavElement() {
120
121        // empty
122    }
123
124    /**
125     * Create a new instance of the bean and calls the init method
126     * with the provided parameters.<p>
127     *
128     * @param sitePath will be passed to <code>init</code>
129     * @param resource the resource
130     * @param properties will be passed to <code>init</code>
131     */
132    public CmsJspNavElement(String sitePath, CmsResource resource, Map<String, String> properties) {
133
134        setResource(resource);
135        init(sitePath, properties);
136    }
137
138    /**
139     * Create a new instance of the bean and calls the init method
140     * with the provided parameters.<p>
141     *
142     * @param sitePath will be passed to <code>init</code>
143     * @param resource the resource
144     * @param properties will be passed to <code>init</code>
145     * @param navTreeLevel will be passed to <code>init</code>
146     *
147     * @see #init(String, Map, int, Locale)
148     */
149    public CmsJspNavElement(String sitePath, CmsResource resource, Map<String, String> properties, int navTreeLevel) {
150
151        this(sitePath, resource, properties, navTreeLevel, null);
152    }
153
154    /**
155     * Create a new instance of the bean and calls the init method
156     * with the provided parameters.<p>
157     *
158     * @param sitePath will be passed to <code>init</code>
159     * @param resource the resource
160     * @param properties will be passed to <code>init</code>
161     * @param navTreeLevel will be passed to <code>init</code>
162     * @param locale the locale for which properties should be accessed.
163     *
164     * @see #init(String, Map, int, Locale)
165     */
166    public CmsJspNavElement(
167        String sitePath,
168        CmsResource resource,
169        Map<String, String> properties,
170        int navTreeLevel,
171        Locale locale) {
172
173        setResource(resource);
174        init(sitePath, properties, navTreeLevel, locale);
175    }
176
177    /**
178     * Create a new instance of the bean and calls the init method
179     * with the provided parameters.<p>
180     *
181     * @param sitePath will be passed to <code>init</code>
182     * @param properties will be passed to <code>init</code>
183     *
184     * @see #init(String, Map)
185     *
186     * @deprecated use {@link #CmsJspNavElement(String, CmsResource, Map)}
187     */
188    @Deprecated
189    public CmsJspNavElement(String sitePath, Map<String, String> properties) {
190
191        init(sitePath, properties, -1, null);
192    }
193
194    /**
195     * Create a new instance of the bean and calls the init method
196     * with the provided parameters.<p>
197     *
198     * @param sitePath will be passed to <code>init</code>
199     * @param properties will be passed to <code>init</code>
200     * @param navTreeLevel will be passed to <code>init</code>
201     *
202     * @see #init(String, Map, int, Locale)
203     *
204     * @deprecated use {@link #CmsJspNavElement(String, CmsResource, Map, int)}
205     */
206    @Deprecated
207    public CmsJspNavElement(String sitePath, Map<String, String> properties, int navTreeLevel) {
208
209        init(sitePath, properties, navTreeLevel, null);
210    }
211
212    /**
213     * Note: this class has a natural ordering that is inconsistent with equals.<p>
214     *
215     * @see java.lang.Comparable#compareTo(Object)
216     */
217    public int compareTo(CmsJspNavElement obj) {
218
219        if (obj == this) {
220            return 0;
221        }
222        float pos = obj.getNavPosition();
223        // please note: can't just subtract and cast to int here because of float precision loss
224        if (m_position == pos) {
225            return 0;
226        }
227        return (m_position < pos) ? -1 : 1;
228    }
229
230    /**
231     * Note: this class has a natural ordering that is inconsistent with equals.<p>
232     *
233     * @see java.lang.Object#equals(Object)
234     */
235    @Override
236    public boolean equals(Object obj) {
237
238        if (obj == this) {
239            return true;
240        }
241        if (obj instanceof CmsJspNavElement) {
242            return ((CmsJspNavElement)obj).m_sitePath.equals(m_sitePath);
243        }
244        return false;
245    }
246
247    /**
248     * Returns the value of the property PROPERTY_DESCRIPTION of this navigation element,
249     * or <code>null</code> if this property is not set.<p>
250     *
251     * @return the value of the property PROPERTY_DESCRIPTION of this navigation element
252     *          or <code>null</code> if this property is not set
253     */
254    public String getDescription() {
255
256        return getProperties().get(CmsPropertyDefinition.PROPERTY_DESCRIPTION);
257    }
258
259    /**
260     * Returns the filename of the navigation element, i.e.
261     * the name of the navigation resource without any path information.<p>
262     *
263     * @return the filename of the navigation element, i.e.
264     *          the name of the navigation resource without any path information
265     */
266    public String getFileName() {
267
268        if (m_fileName == null) {
269            // use "lazy initializing"
270            if (!m_sitePath.endsWith("/")) {
271                m_fileName = m_sitePath.substring(m_sitePath.lastIndexOf("/") + 1, m_sitePath.length());
272            } else {
273                m_fileName = m_sitePath.substring(
274                    m_sitePath.substring(0, m_sitePath.length() - 1).lastIndexOf("/") + 1,
275                    m_sitePath.length());
276            }
277        }
278        return m_fileName;
279    }
280
281    /**
282     * Returns the value of the property <code>{@link CmsPropertyDefinition#PROPERTY_NAVINFO}</code> of this
283     * navigation element, or <code>null</code> if this property is not set.<p>
284     *
285     * @return the value of the property or <code>null</code> if this property is not set
286     */
287    public String getInfo() {
288
289        return getProperties().get(CmsPropertyDefinition.PROPERTY_NAVINFO);
290    }
291
292    /**
293     * Returns the value of the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> of this
294     * navigation element, or <code>null</code> if this property is not set.<p>
295     *
296     * @return the value of the property or <code>null</code> if this property is not set
297     */
298    public String getLocale() {
299
300        return getProperties().get(CmsPropertyDefinition.PROPERTY_LOCALE);
301    }
302
303    /**
304     * Returns the navigation builder context.
305     *
306     * @return the navigation builder context
307     */
308    public NavContext getNavContext() {
309
310        return m_navContext;
311    }
312
313    /**
314     * Returns the value of the property <code>{@link CmsPropertyDefinition#PROPERTY_NAVIMAGE}</code> of this
315     * navigation element, or <code>null</code> if this property is not set.<p>
316     *
317     * @return the value of the property or <code>null</code> if this property is not set
318     */
319    public String getNavImage() {
320
321        return getProperties().get(CmsPropertyDefinition.PROPERTY_NAVIMAGE);
322    }
323
324    /**
325     * Returns the value of the property C_PROPERTY_NAVPOS converted to a <code>float</code>,
326     * or a value of <code>Float.MAX_VALUE</code> if the navigation position property is not
327     * set (or not a valid number) for this resource.<p>
328     *
329     * @return float the value of the property C_PROPERTY_NAVPOS converted to a <code>float</code>,
330     *          or a value of <code>Float.MAX_VALUE</code> if the navigation position property is not
331     *          set (or not a valid number) for this resource
332     */
333    public float getNavPosition() {
334
335        return m_position;
336    }
337
338    /**
339     * Returns the value of the property PROPERTY_NAVTEXT of this navigation element,
340     * or a warning message if this property is not set
341     * (this method will never return <code>null</code>).<p>
342     *
343     * @return the value of the property PROPERTY_NAVTEXT of this navigation element,
344     *          or a warning message if this property is not set
345     *          (this method will never return <code>null</code>)
346     */
347    public String getNavText() {
348
349        if (m_text == null) {
350            // use "lazy initializing"
351            m_text = getProperties().get(CmsPropertyDefinition.PROPERTY_NAVTEXT);
352            if (m_text == null) {
353                m_text = CmsMessages.formatUnknownKey(CmsPropertyDefinition.PROPERTY_NAVTEXT);
354            }
355        }
356        return m_text;
357    }
358
359    /**
360     * Returns the navigation tree level of this resource.<p>
361     *
362     * @return the navigation tree level of this resource
363     */
364    public int getNavTreeLevel() {
365
366        if (m_navTreeLevel == Integer.MIN_VALUE) {
367            // use "lazy initializing"
368            m_navTreeLevel = CmsResource.getPathLevel(m_sitePath);
369        }
370        return m_navTreeLevel;
371    }
372
373    /**
374     * Returns the name of the parent folder of the resource of this navigation element.<p>
375     *
376     * @return the name of the parent folder of the resource of this navigation element
377     */
378    public String getParentFolderName() {
379
380        return CmsResource.getParentFolder(m_sitePath);
381    }
382
383    /**
384     * Returns the original map of all file properties of the resource that
385     * the navigation element belongs to.<p>
386     *
387     * Please note that the original reference is returned, so be careful when making
388     * changes to the map.<p>
389     *
390     * @return the original map of all file properties of the resource that
391     *          the navigation element belongs to
392     */
393    public Map<String, String> getProperties() {
394
395        if (null == m_locale) {
396            return m_properties;
397        } else {
398            return getLocaleProperties();
399        }
400    }
401
402    /**
403     * Returns the value of the selected property from this navigation element.<p>
404     *
405     * The navigation element contains a hash of all file properties of the resource that
406     * the navigation element belongs to.<p>
407     *
408     * @param key the property name to look up
409     *
410     * @return the value of the selected property
411     */
412    public String getProperty(String key) {
413
414        return getProperties().get(key);
415    }
416
417    /**
418     * Returns the resource.<p>
419     *
420     * @return the resource
421     */
422    public CmsResource getResource() {
423
424        return m_resource;
425    }
426
427    /**
428     * Returns the resource name this navigation element was initialized with.<p>
429     *
430     * @return the resource name this navigation element was initialized with
431     */
432    public String getResourceName() {
433
434        return m_sitePath;
435    }
436
437    /**
438     * Gets the sub-entries of the navigation entry.
439     *
440     * @return the sub-entries
441     */
442    public List<CmsJspNavElement> getSubNavigation() {
443
444        if (m_subNavigation == null) {
445            if (m_resource.isFile()) {
446                m_subNavigation = Collections.emptyList();
447            } else if (m_navContext == null) {
448                try {
449                    throw new Exception("Can not get subnavigation because navigation context is not set.");
450                } catch (Exception e) {
451                    LOG.warn(e.getLocalizedMessage(), e);
452                    m_subNavigation = Collections.emptyList();
453                }
454            } else {
455                CmsJspNavBuilder navBuilder = m_navContext.getNavBuilder();
456                m_subNavigation = navBuilder.getNavigationForFolder(
457                    navBuilder.getCmsObject().getSitePath(m_resource),
458                    m_navContext.getVisibility(),
459                    m_navContext.getFilter());
460            }
461
462        }
463        return m_subNavigation;
464
465    }
466
467    /**
468     * Returns the value of the property PROPERTY_TITLE of this navigation element,
469     * or <code>null</code> if this property is not set.<p>
470     *
471     * @return the value of the property PROPERTY_TITLE of this navigation element
472     *          or <code>null</code> if this property is not set
473     */
474    public String getTitle() {
475
476        return getProperties().get(CmsPropertyDefinition.PROPERTY_TITLE);
477    }
478
479    /**
480     * Returns if the navigation position has been changed since initialization.<p>
481     *
482     * @return <code>true</code> if the navigation position has been changed since initialization
483     */
484    public boolean hasChangedNavPosition() {
485
486        return m_changedNavPos;
487    }
488
489    /**
490     * Note: this class has a natural ordering that is inconsistent with equals.<p>
491     *
492     * @see java.lang.Object#hashCode()
493     */
494    @Override
495    public int hashCode() {
496
497        return m_sitePath.hashCode();
498    }
499
500    /**
501     * Same as calling {@link #init(String, Map, int, Locale)
502     * init(String, Hashtable, -1, null)}.<p>
503     *
504     * @param resource the name of the resource to extract the navigation
505     *     information from
506     * @param properties the properties of the resource read from the vfs
507     */
508    public void init(String resource, Map<String, String> properties) {
509
510        init(resource, properties, -1, null);
511    }
512
513    /**
514     * Initialized the member variables of this bean with the values
515     * provided.<p>
516     *
517     * A resource will be in the navigation if at least one of the two properties
518     * <code>I_CmsConstants.PROPERTY_NAVTEXT</code> or
519     * <code>I_CmsConstants.PROPERTY_NAVPOS</code> is set. Otherwise
520     * it will be ignored.<p>
521     *
522     * This bean does provides static methods to create a new instance
523     * from the context of a current CmsObject. Call these static methods
524     * in order to get a properly initialized bean.<p>
525     *
526     * @param resource the name of the resource to extract the navigation
527     *     information from
528     * @param properties the properties of the resource read from the vfs
529     * @param navTreeLevel tree level of this resource, for building
530     *     navigation trees
531     *
532     * @see CmsJspNavBuilder#getNavigationForResource()
533     */
534    public void init(String resource, Map<String, String> properties, int navTreeLevel) {
535
536        init(resource, properties, navTreeLevel, null);
537    }
538
539    /**
540     * Initialized the member variables of this bean with the values
541     * provided.<p>
542     *
543     * A resource will be in the navigation if at least one of the two properties
544     * <code>I_CmsConstants.PROPERTY_NAVTEXT</code> or
545     * <code>I_CmsConstants.PROPERTY_NAVPOS</code> is set. Otherwise
546     * it will be ignored.<p>
547     *
548     * This bean does provides static methods to create a new instance
549     * from the context of a current CmsObject. Call these static methods
550     * in order to get a properly initialized bean.<p>
551     *
552     * @param resource the name of the resource to extract the navigation
553     *     information from
554     * @param properties the properties of the resource read from the vfs
555     * @param navTreeLevel tree level of this resource, for building
556     *     navigation trees
557     * @param locale The locale for which properties should be accessed.
558     *
559     * @see CmsJspNavBuilder#getNavigationForResource()
560     */
561    public void init(String resource, Map<String, String> properties, int navTreeLevel, Locale locale) {
562
563        m_sitePath = resource;
564        m_properties = properties;
565        m_navTreeLevel = navTreeLevel;
566        m_locale = locale;
567        // init the position value
568        m_position = Float.MAX_VALUE;
569        try {
570            m_position = Float.parseFloat(getProperties().get(CmsPropertyDefinition.PROPERTY_NAVPOS));
571        } catch (@SuppressWarnings("unused") Exception e) {
572            // m_position will have Float.MAX_VALUE, so navigation element will
573            // appear last in navigation
574        }
575    }
576
577    /**
578     * Returns <code>true</code> if this navigation element describes a folder,
579     * <code>false</code> otherwise.<p>
580     *
581     * @return <code>true</code> if this navigation element describes a folder,
582     *          <code>false</code> otherwise.<p>
583     */
584    public boolean isFolderLink() {
585
586        return m_sitePath.endsWith("/");
587    }
588
589    /**
590     * Returns if this is a hidden navigation entry.<p>
591     *
592     * @return <code>true</code> if this is a hidden navigation entry
593     */
594    public boolean isHiddenNavigationEntry() {
595
596        if (m_isHiddenNavigationEntry == null) {
597            // use "lazy initializing"
598            String navInfo = getProperties().get(CmsPropertyDefinition.PROPERTY_NAVINFO);
599            m_isHiddenNavigationEntry = Boolean.valueOf(CmsClientSitemapEntry.HIDDEN_NAVIGATION_ENTRY.equals(navInfo));
600        }
601        return m_isHiddenNavigationEntry.booleanValue();
602    }
603
604    /**
605     * Returns <code>true</code> if this navigation element is in the navigation,
606     * <code>false</code> otherwise.<p>
607     *
608     * A resource is considered to be in the navigation, if <ol>
609     * <li>it has the property PROPERTY_NAVTEXT set
610     * <li><em>or</em> it has the property PROPERTY_NAVPOS set
611     * <li><em>and</em> it is not a temporary file as defined by {@link CmsResource#isTemporaryFileName(String)}.</ol>
612     *
613     * @return <code>true</code> if this navigation element is in the navigation, <code>false</code> otherwise
614     */
615    public boolean isInNavigation() {
616
617        if (m_hasNav == null) {
618            // use "lazy initializing"
619            Object o1 = getProperties().get(CmsPropertyDefinition.PROPERTY_NAVTEXT);
620            Object o2 = getProperties().get(CmsPropertyDefinition.PROPERTY_NAVPOS);
621            m_hasNav = Boolean.valueOf(((o1 != null) || (o2 != null)) && !CmsResource.isTemporaryFileName(m_sitePath));
622        }
623        return m_hasNav.booleanValue();
624    }
625
626    /**
627     * Returns if the navigation element represents a navigation level, linking to it's first sub-element.<p>
628     *
629     * @return <code>true</code> if the navigation element represents a navigation level
630     */
631    public boolean isNavigationLevel() {
632
633        return CmsJspNavBuilder.NAVIGATION_LEVEL_FOLDER.equals(
634            getProperties().get(CmsPropertyDefinition.PROPERTY_DEFAULT_FILE));
635    }
636
637    /**
638     * Sets the navigation builder context.
639     *
640     * @param navContext the navigation builder context
641     */
642    public void setNavContext(NavContext navContext) {
643
644        m_navContext = navContext;
645    }
646
647    /**
648     * Sets the value that will be returned by the {@link #getNavPosition()}
649     * method of this class.<p>
650     *
651     * @param value the value to set
652     */
653    public void setNavPosition(float value) {
654
655        m_position = value;
656        m_changedNavPos = true;
657    }
658
659    /**
660     * Sets the navigation text.<p>
661     *
662     * @param text the text to set
663     */
664    public void setNavText(String text) {
665
666        m_text = text;
667    }
668
669    /**
670     * Sets the navigation tree level.<p>
671     *
672     * @param navTreeLevel the navigation tree level to set
673     */
674    public void setNavTreeLevel(int navTreeLevel) {
675
676        m_navTreeLevel = navTreeLevel;
677    }
678
679    /**
680     * @see java.lang.Object#toString()
681     */
682    @Override
683    public String toString() {
684
685        StringBuffer result = new StringBuffer();
686
687        result.append("[");
688        result.append(this.getClass().getName());
689        result.append(", sitePath: ");
690        result.append(m_sitePath);
691        result.append(", navPosition: ");
692        result.append(getNavPosition());
693        result.append(", navText ");
694        result.append(getNavText());
695        result.append(", navTreeLevel: ");
696        result.append(getNavTreeLevel());
697        result.append("]");
698
699        return result.toString();
700    }
701
702    /**
703     * Returns the site path of the target resource.<p>
704     *
705     * This may not be the same as the navigation resource.<p>
706     *
707     * @return the target resource site path
708     */
709    protected String getSitePath() {
710
711        return m_sitePath;
712    }
713
714    /**
715     * Sets the resource.<p>
716     *
717     * @param resource the resource to set
718     */
719    protected void setResource(CmsResource resource) {
720
721        m_resource = resource;
722    }
723
724    /**
725     * Helper to get locale specific properties.
726     *
727     * @return the locale specific properties map.
728     */
729    private Map<String, String> getLocaleProperties() {
730
731        if (m_localeProperties == null) {
732            m_localeProperties = CmsCollectionsGenericWrapper.createLazyMap(
733                new CmsProperty.CmsPropertyLocaleTransformer(m_properties, m_locale));
734        }
735        return m_localeProperties;
736    }
737
738}