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, 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.ui.apps;
029
030import org.opencms.ade.configuration.CmsADEConfigData;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsProperty;
033import org.opencms.file.CmsPropertyDefinition;
034import org.opencms.file.CmsResource;
035import org.opencms.file.CmsResourceFilter;
036import org.opencms.file.CmsVfsResourceNotFoundException;
037import org.opencms.file.types.CmsResourceTypeXmlContainerPage;
038import org.opencms.file.types.I_CmsResourceType;
039import org.opencms.jsp.CmsJspNavBuilder;
040import org.opencms.jsp.CmsJspNavBuilder.Visibility;
041import org.opencms.jsp.CmsJspNavElement;
042import org.opencms.main.CmsException;
043import org.opencms.main.CmsLog;
044import org.opencms.main.OpenCms;
045import org.opencms.site.CmsSite;
046import org.opencms.util.CmsStringUtil;
047
048import java.io.Serializable;
049import java.util.ArrayList;
050import java.util.Collections;
051import java.util.HashMap;
052import java.util.List;
053import java.util.Locale;
054import java.util.Map;
055import java.util.concurrent.ConcurrentHashMap;
056
057import javax.servlet.http.HttpSession;
058
059import org.apache.commons.logging.Log;
060
061/**
062 * Stores the last opened locations for file explorer, page editor and sitemap editor.<p>
063 */
064public class CmsQuickLaunchLocationCache implements Serializable {
065
066    /**
067     * Contains contextual information for the last edited page, to find a page "near" it to edit in case the original page was deleted.
068     */
069    static class PageLocationWithContext {
070
071        /** The original page. */
072        private CmsResource m_resource;
073
074        /** True if this was a non-container page resource. */
075        private boolean m_isNotPage;
076
077        /** The original navigation position. */
078        private float m_navPos;
079
080        /** The ancestor folders, if available. */
081        private List<CmsResource> m_ancestors = new ArrayList<>();
082
083        /** The navigation start resource, if available. */
084        private CmsResource m_navResource;
085
086        public PageLocationWithContext(CmsObject cms, CmsResource resource) {
087
088            m_resource = resource;
089            if (!CmsResourceTypeXmlContainerPage.isContainerPage(resource)) {
090                m_isNotPage = true;
091            } else {
092                try {
093                    Map<String, CmsProperty> props = CmsProperty.getPropertyMap(
094                        cms.readPropertyObjects(resource, false));
095                    if (hasNavigationProps(props)) {
096                        initNavigationData(cms, resource, props);
097                    } else {
098                        CmsResource parent = cms.readParentFolder(m_resource.getStructureId());
099                        Map<String, CmsProperty> parentProps = CmsProperty.getPropertyMap(
100                            cms.readPropertyObjects(parent, false));
101                        if (hasNavigationProps(parentProps)) {
102                            initNavigationData(cms, parent, parentProps);
103                        }
104                    }
105                } catch (Exception e) {
106                    LOG.error(e.getLocalizedMessage(), e);
107                }
108            }
109        }
110
111        /**
112         * Does the same thing as getNearestPageInternal, but as an additional fallback, tries to read any
113         * container page of the subsitemap if the former returns null.
114         *
115         * @param cms the current CMS context
116         * @return the 'nearest' page to the original page
117         */
118        public CmsResource getNearestPage(CmsObject cms) {
119
120            CmsResource result = getNearestPageInternal(cms);
121            if (result == null) {
122                try {
123                    CmsADEConfigData config = OpenCms.getADEManager().lookupConfiguration(
124                        cms,
125                        m_resource.getRootPath());
126                    if (CmsStringUtil.isPrefixPath(cms.getRequestContext().getSiteRoot(), config.getBasePath())) {
127                        I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(
128                            CmsResourceTypeXmlContainerPage.RESOURCE_TYPE_NAME);
129                        List<CmsResource> resources = cms.readResources(
130                            cms.getRequestContext().removeSiteRoot(config.getBasePath()),
131                            CmsResourceFilter.IGNORE_EXPIRATION.addRequireType(type),
132                            true);
133                        for (CmsResource resource : resources) {
134                            if (resource.getRootPath().endsWith("/index.html")) {
135                                return resource;
136                            }
137                        }
138                        for (CmsResource resource : resources) {
139                            return resource;
140                        }
141                    }
142
143                } catch (Exception e) {
144                    LOG.debug(e.getLocalizedMessage(), e);
145                }
146            }
147            return result;
148        }
149
150        /**
151         * Gets the navigation position from a map of properties.
152         *
153         * @param props the map of properties
154         * @return the navigation position
155         */
156        private float getNavPos(Map<String, CmsProperty> props) {
157
158            float result = Float.MAX_VALUE;
159
160            CmsProperty prop = props.get(CmsPropertyDefinition.PROPERTY_NAVPOS);
161            if (prop != null) {
162                try {
163                    result = Float.parseFloat(prop.getValue());
164                } catch (Exception e) {
165                    /*ignore*/
166                }
167            }
168            return result;
169        }
170
171        /**
172         * Gets the 'nearest' page to the original resource.
173         *
174         *  <ul>
175         *  <li>check if the original resource exists and return it if so
176         *  <li>try to find the default file of the parent folder of the original resource, and return it if it exists
177         *  <li>try the page with the smallest navigation position greater than the original resource's navigation position in its original folder
178         *  <li>try the page with the greatest navigation position less than  the original resource's navigation position in its original folder
179         *  </ul>
180         *
181         * @param cms the CMS context
182         * @return
183         */
184        private CmsResource getNearestPageInternal(CmsObject cms) {
185
186            if (cms.existsResource(m_resource.getStructureId(), CmsResourceFilter.IGNORE_EXPIRATION)) {
187                return m_resource;
188            }
189            if (m_isNotPage) {
190                return null;
191            }
192            if ((m_navResource != m_resource)
193                && (m_navResource != null)
194                && cms.existsResource(m_navResource.getStructureId(), CmsResourceFilter.IGNORE_EXPIRATION)) {
195                try {
196                    CmsResource defaultFile = cms.readDefaultFile(
197                        cms.getRequestContext().getSitePath(m_navResource),
198                        CmsResourceFilter.IGNORE_EXPIRATION);
199                    if (defaultFile != null) {
200                        return defaultFile;
201                    }
202                } catch (Exception e) {
203                    LOG.debug(e.getLocalizedMessage(), e);
204                }
205            }
206            CmsJspNavBuilder builder = new CmsJspNavBuilder();
207            builder.init(cms, Locale.ENGLISH);
208            for (int ancestorIndex = 0; ancestorIndex < m_ancestors.size(); ancestorIndex++) {
209                try {
210                    // re-read to ensure path is correct
211                    CmsResource ancestor = cms.readResource(
212                        m_ancestors.get(ancestorIndex).getStructureId(),
213                        CmsResourceFilter.IGNORE_EXPIRATION);
214                    if (!CmsStringUtil.isPrefixPath(cms.getRequestContext().getSiteRoot(), ancestor.getRootPath())) {
215                        return null;
216                    }
217                    if (ancestorIndex == 0) {
218                        List<CmsJspNavElement> navigation = builder.getNavigationForFolder(
219                            cms.getRequestContext().getSitePath(ancestor),
220                            Visibility.navigation,
221                            CmsResourceFilter.IGNORE_EXPIRATION);
222                        List<CmsJspNavElement> before = new ArrayList<>();
223                        List<CmsJspNavElement> after = new ArrayList<>();
224                        for (CmsJspNavElement elem : navigation) {
225                            if (elem.getNavPosition() < m_navPos) {
226                                before.add(elem);
227                            } else {
228                                after.add(elem);
229                            }
230                        }
231
232                        for (CmsJspNavElement afterElem : after) {
233                            try {
234                                // this is *not* always the same as afterElem.getResource() !
235                                CmsResource candidate = cms.readResource(
236                                    afterElem.getResourceName(),
237                                    CmsResourceFilter.IGNORE_EXPIRATION);
238                                return candidate;
239                            } catch (CmsException e) {
240                                LOG.debug(e.getLocalizedMessage(), e);
241                            }
242                        }
243                        Collections.reverse(before);
244                        for (CmsJspNavElement beforeElem : before) {
245                            try {
246                                // this is *not* always the same as beforeElem.getResource() !
247                                CmsResource candidate = cms.readResource(
248                                    beforeElem.getResourceName(),
249                                    CmsResourceFilter.IGNORE_EXPIRATION);
250                                return candidate;
251                            } catch (CmsException e) {
252                                LOG.debug(e.getLocalizedMessage(), e);
253                            }
254                        }
255                    }
256                    CmsJspNavElement ancestorElem = builder.getNavigationForResource(
257                        cms.getRequestContext().getSitePath(ancestor),
258                        CmsResourceFilter.IGNORE_EXPIRATION);
259                    if ((ancestorElem != null) && ancestorElem.isInNavigation()) {
260                        try {
261                            CmsResource candidate = cms.readResource(
262                                ancestorElem.getResourceName(),
263                                CmsResourceFilter.IGNORE_EXPIRATION);
264                            return candidate;
265                        } catch (CmsException e) {
266                            LOG.debug(e.getLocalizedMessage(), e);
267                        }
268                    }
269
270                } catch (Exception e) {
271                    LOG.debug(e.getLocalizedMessage(), e);
272                }
273            }
274            return null;
275        }
276
277        /**
278         * Checks if the property map contains navigation properties.
279         *
280         * @param props the navigation properties
281         * @return true if navigation properties exist in the property map
282         */
283        private boolean hasNavigationProps(Map<String, CmsProperty> props) {
284
285            return props.containsKey(CmsPropertyDefinition.PROPERTY_NAVTEXT)
286                || props.containsKey(CmsPropertyDefinition.PROPERTY_NAVPOS);
287        }
288
289        /**
290         * Initializes the context information for the given navigation resource.
291         * @param cms the CMS context
292         * @param navResource a resource in the navigation
293         * @param props the properties of the navigation resource
294         */
295        private void initNavigationData(CmsObject cms, CmsResource navResource, Map<String, CmsProperty> props) {
296
297            m_navPos = getNavPos(props);
298            m_navResource = navResource;
299            CmsResource currentResource = navResource;
300            while (true) {
301                if (cms.getRequestContext().getSitePath(currentResource).equals("/")) {
302                    break;
303                }
304                try {
305                    currentResource = cms.readParentFolder(currentResource.getStructureId());
306                    if (currentResource != null) {
307                        m_ancestors.add(currentResource);
308                    } else {
309                        break;
310                    }
311                } catch (Exception e) {
312                    break;
313                }
314            }
315        }
316    }
317
318    /** Logger instance for this class. */
319    private static final Log LOG = CmsLog.getLog(CmsQuickLaunchLocationCache.class);
320
321    /** The serial version id. */
322    private static final long serialVersionUID = -6144984854691623070L;
323
324    private Map<String, PageLocationWithContext> m_pageEditorLocations = new ConcurrentHashMap<String, CmsQuickLaunchLocationCache.PageLocationWithContext>();
325
326    /** The sitemap editor locations. */
327    private Map<String, String> m_sitemapEditorLocations;
328
329    /** The file explorer locations. */
330    private Map<String, String> m_fileExplorerLocations;
331
332    /**
333     * Constructor.<p>
334     */
335    public CmsQuickLaunchLocationCache() {
336
337        m_sitemapEditorLocations = new HashMap<String, String>();
338        m_fileExplorerLocations = new HashMap<String, String>();
339    }
340
341    /**
342     * Returns the location cache from the user session.<p>
343     *
344     * @param session the session
345     *
346     * @return the location cache
347     */
348    public static CmsQuickLaunchLocationCache getLocationCache(HttpSession session) {
349
350        CmsQuickLaunchLocationCache cache = (CmsQuickLaunchLocationCache)session.getAttribute(
351            CmsQuickLaunchLocationCache.class.getName());
352        if (cache == null) {
353            cache = new CmsQuickLaunchLocationCache();
354            session.setAttribute(CmsQuickLaunchLocationCache.class.getName(), cache);
355        }
356        return cache;
357    }
358
359    /**
360     * Returns the file explorer location for the given site root.<p>
361     *
362     * @param siteRoot the site root
363     *
364     * @return the location
365     */
366    public String getFileExplorerLocation(String siteRoot) {
367
368        return m_fileExplorerLocations.get(siteRoot);
369    }
370
371    /**
372     * Returns the page editor location for the given site root.<p>
373     *
374     * @param cms the current CMS context
375     * @param siteRoot the site root
376     *
377     * @return the location
378     */
379    public String getPageEditorLocation(CmsObject cms, String siteRoot) {
380
381        PageLocationWithContext location = m_pageEditorLocations.get(siteRoot);
382        CmsResource res = null;
383        if (location != null) {
384            res = location.getNearestPage(cms);
385        }
386
387        if (res == null) {
388            return null;
389        }
390        try {
391            String sitePath = cms.getSitePath(res);
392            cms.readResource(sitePath, CmsResourceFilter.ONLY_VISIBLE_NO_DELETED);
393            return sitePath;
394        } catch (CmsVfsResourceNotFoundException e) {
395            try {
396                CmsResource newRes = cms.readResource(res.getStructureId(), CmsResourceFilter.ONLY_VISIBLE_NO_DELETED);
397                CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(newRes.getRootPath());
398                if (site == null) {
399                    return null;
400                }
401                if (normalizePath(site.getSiteRoot()).equals(normalizePath(siteRoot))) {
402                    return cms.getSitePath(newRes);
403                } else {
404                    return null;
405                }
406
407            } catch (CmsVfsResourceNotFoundException e2) {
408                return null;
409            } catch (CmsException e2) {
410                LOG.error(e.getLocalizedMessage(), e2);
411                return null;
412            }
413        } catch (CmsException e) {
414            LOG.error(e.getLocalizedMessage(), e);
415            return null;
416        }
417
418    }
419
420    /**
421     * Returns the sitemap editor location for the given site root.<p>
422     *
423     * @param siteRoot the site root
424     *
425     * @return the location
426     */
427    public String getSitemapEditorLocation(String siteRoot) {
428
429        return m_sitemapEditorLocations.get(siteRoot);
430    }
431
432    /**
433     * Sets the latest file explorer location for the given site.<p>
434     *
435     * @param siteRoot the site root
436     * @param location the location
437     */
438    public void setFileExplorerLocation(String siteRoot, String location) {
439
440        m_fileExplorerLocations.put(siteRoot, location);
441    }
442
443    /**
444     * Sets the latest page editor location for the given site.<p>
445     *
446     * @param siteRoot the site root
447     * @param resource the location resource
448     */
449    public void setPageEditorResource(CmsObject cms, String siteRoot, CmsResource resource) {
450
451        PageLocationWithContext location = new PageLocationWithContext(cms, resource);
452        m_pageEditorLocations.put(siteRoot, location);
453    }
454
455    /**
456     * Sets the latest sitemap editor location for the given site.<p>
457     *
458     * @param siteRoot the site root
459     * @param location the location
460     */
461    public void setSitemapEditorLocation(String siteRoot, String location) {
462
463        m_sitemapEditorLocations.put(siteRoot, location);
464    }
465
466    /**
467     * Ensures the given path begins and ends with a slash.
468     *
469     * @param path the path
470     * @return the normalized path
471     */
472    private String normalizePath(String path) {
473
474        return CmsStringUtil.joinPaths("/", path, "/");
475    }
476}