001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (C) Alkacon Software (https://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: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://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.ade.configuration;
029
030import org.opencms.ade.configuration.CmsADEConfigData.DetailInfo;
031import org.opencms.ade.configuration.CmsElementView.ElementViewComparator;
032import org.opencms.ade.configuration.formatters.CmsFormatterConfigurationCache;
033import org.opencms.ade.configuration.formatters.CmsFormatterConfigurationCacheState;
034import org.opencms.ade.configuration.plugins.CmsTemplatePlugin;
035import org.opencms.ade.configuration.plugins.CmsTemplatePluginFinder;
036import org.opencms.ade.containerpage.inherited.CmsContainerConfigurationCache;
037import org.opencms.ade.containerpage.inherited.CmsContainerConfigurationWriter;
038import org.opencms.ade.containerpage.inherited.CmsInheritedContainerState;
039import org.opencms.ade.detailpage.CmsDetailPageConfigurationWriter;
040import org.opencms.ade.detailpage.CmsDetailPageInfo;
041import org.opencms.ade.detailpage.I_CmsDetailPageHandler;
042import org.opencms.ade.upload.CmsUploadWarningTable;
043import org.opencms.configuration.CmsSystemConfiguration;
044import org.opencms.db.I_CmsProjectDriver;
045import org.opencms.file.CmsFile;
046import org.opencms.file.CmsObject;
047import org.opencms.file.CmsProject;
048import org.opencms.file.CmsRequestContext;
049import org.opencms.file.CmsResource;
050import org.opencms.file.CmsResourceFilter;
051import org.opencms.file.CmsUser;
052import org.opencms.file.types.CmsResourceTypeFunctionConfig;
053import org.opencms.file.types.CmsResourceTypeXmlContainerPage;
054import org.opencms.file.types.CmsResourceTypeXmlContent;
055import org.opencms.file.types.I_CmsResourceType;
056import org.opencms.gwt.shared.CmsGwtConstants;
057import org.opencms.gwt.shared.CmsPermissionInfo;
058import org.opencms.gwt.shared.CmsTemplateContextInfo;
059import org.opencms.i18n.CmsEncoder;
060import org.opencms.i18n.CmsLocaleManager;
061import org.opencms.json.JSONArray;
062import org.opencms.json.JSONException;
063import org.opencms.json.JSONObject;
064import org.opencms.jsp.CmsJspNavBuilder;
065import org.opencms.jsp.CmsJspNavElement;
066import org.opencms.jsp.CmsJspTagLink;
067import org.opencms.jsp.util.CmsJspStandardContextBean;
068import org.opencms.jsp.util.CmsTemplatePluginWrapper;
069import org.opencms.loader.CmsLoaderException;
070import org.opencms.main.CmsException;
071import org.opencms.main.CmsLog;
072import org.opencms.main.OpenCms;
073import org.opencms.main.OpenCmsServlet;
074import org.opencms.monitor.CmsMemoryMonitor;
075import org.opencms.relations.CmsRelation;
076import org.opencms.relations.CmsRelationFilter;
077import org.opencms.security.CmsPermissionSet;
078import org.opencms.security.CmsRole;
079import org.opencms.util.CmsFileUtil;
080import org.opencms.util.CmsRequestUtil;
081import org.opencms.util.CmsStringUtil;
082import org.opencms.util.CmsUUID;
083import org.opencms.util.CmsWaitHandle;
084import org.opencms.workplace.explorer.CmsExplorerTypeSettings;
085import org.opencms.workplace.explorer.CmsResourceUtil;
086import org.opencms.xml.CmsXmlContentDefinition;
087import org.opencms.xml.CmsXmlException;
088import org.opencms.xml.containerpage.CmsADECache;
089import org.opencms.xml.containerpage.CmsADECacheSettings;
090import org.opencms.xml.containerpage.CmsContainerElementBean;
091import org.opencms.xml.containerpage.I_CmsFormatterBean;
092import org.opencms.xml.containerpage.Messages;
093import org.opencms.xml.content.CmsXmlContent;
094import org.opencms.xml.content.CmsXmlContentFactory;
095import org.opencms.xml.content.CmsXmlContentProperty;
096import org.opencms.xml.content.CmsXmlContentProperty.Visibility;
097import org.opencms.xml.content.CmsXmlContentPropertyHelper;
098import org.opencms.xml.content.I_CmsXmlContentHandler;
099import org.opencms.xml.types.I_CmsXmlContentValue;
100
101import java.util.ArrayList;
102import java.util.Collections;
103import java.util.Comparator;
104import java.util.HashMap;
105import java.util.HashSet;
106import java.util.Iterator;
107import java.util.LinkedHashMap;
108import java.util.List;
109import java.util.Locale;
110import java.util.Map;
111import java.util.Map.Entry;
112import java.util.Set;
113import java.util.function.Predicate;
114import java.util.stream.Collectors;
115import java.util.stream.Stream;
116
117import javax.servlet.ServletRequest;
118import javax.servlet.http.HttpServletRequest;
119import javax.servlet.http.HttpServletResponse;
120
121import org.apache.commons.logging.Log;
122
123import com.google.common.collect.Lists;
124import com.google.common.collect.Maps;
125import com.google.common.collect.Multimap;
126
127/**
128 * This is the main class used to access the ADE configuration and also accomplish some other related tasks
129 * like loading/saving favorite and recent lists.<p>
130 */
131public class CmsADEManager {
132
133    /** JSON property name constant. */
134    protected enum FavListProp {
135        /** element property. */
136        ELEMENT,
137        /** formatter property. */
138        FORMATTER,
139        /** properties property. */
140        PROPERTIES;
141    }
142
143    /**
144     * A status enum for the initialization status.<p>
145     */
146    protected enum Status {
147        /** already initialized. */
148        initialized,
149        /** currently initializing. */
150        initializing,
151        /** not initialized. */
152        notInitialized
153    }
154
155    /** The client id separator. */
156    public static final String CLIENT_ID_SEPERATOR = "#";
157
158    /** The configuration file name. */
159    public static final String CONFIG_FILE_NAME = ".config";
160
161    /** The name of the sitemap configuration file type. */
162    public static final String CONFIG_FOLDER_TYPE = "content_folder";
163
164    /** The path for sitemap configuration files relative from the base path. */
165    public static final String CONFIG_SUFFIX = "/"
166        + CmsADEManager.CONTENT_FOLDER_NAME
167        + "/"
168        + CmsADEManager.CONFIG_FILE_NAME;
169
170    /** The name of the sitemap configuration file type. */
171    public static final String CONFIG_TYPE = "sitemap_config";
172
173    /** The content folder name. */
174    public static final String CONTENT_FOLDER_NAME = ".content";
175
176    /** The default detail page type name. */
177    public static final String DEFAULT_DETAILPAGE_TYPE = CmsGwtConstants.DEFAULT_DETAILPAGE_TYPE;
178
179    /** Default favorite/recent list size constant. */
180    public static final int DEFAULT_ELEMENT_LIST_SIZE = 10;
181
182    /** The name of the element view configuration file type. */
183    public static final String ELEMENT_VIEW_TYPE = "elementview";
184
185    /** The name of the module configuration file type. */
186    public static final String MODULE_CONFIG_TYPE = "module_config";
187
188    /** The aADE configuration module name. */
189    public static final String MODULE_NAME_ADE_CONFIG = "org.opencms.base";
190
191    /** Node name for the nav level link value. */
192    public static final String N_LINK = "Link";
193
194    /** Node name for the nav level type value. */
195    public static final String N_TYPE = "Type";
196
197    /** The path to the sitemap editor JSP. */
198    public static final String PATH_SITEMAP_EDITOR_JSP = "/system/workplace/commons/sitemap.jsp";
199
200    /** User additional info key constant. */
201    protected static final String ADDINFO_ADE_FAVORITE_LIST = "ADE_FAVORITE_LIST";
202
203    /** User additional info key constant. */
204    protected static final String ADDINFO_ADE_RECENT_LIST = "ADE_RECENT_LIST";
205
206    /** User additional info key constant. */
207    protected static final String ADDINFO_ADE_SHOW_EDITOR_HELP = "ADE_SHOW_EDITOR_HELP";
208
209    /** The logger instance for this class. */
210    private static final Log LOG = CmsLog.getLog(CmsADEManager.class);
211
212    /** The cache instance. */
213    private CmsADECache m_cache;
214
215    /** The sitemap configuration file type. */
216    private I_CmsResourceType m_configType;
217
218    /** The detail page handler. */
219    private I_CmsDetailPageHandler m_detailPageHandler;
220
221    /** The element view configuration file type. */
222    private I_CmsResourceType m_elementViewType;
223
224    /** The initialization status. */
225    private Status m_initStatus = Status.notInitialized;
226
227    /** The module configuration file type. */
228    private I_CmsResourceType m_moduleConfigType;
229
230    /** The online cache instance. */
231    private CmsConfigurationCache m_offlineCache;
232
233    /** The offline CMS context. */
234    private CmsObject m_offlineCms;
235
236    /** The offline inherited container configuration cache. */
237    private CmsContainerConfigurationCache m_offlineContainerConfigurationCache;
238
239    /** The detail id cache for the Offline project. */
240    private CmsDetailNameCache m_offlineDetailIdCache;
241
242    /** The offline formatter bean cache. */
243    private CmsFormatterConfigurationCache m_offlineFormatterCache;
244
245    /** The offline cache instance. */
246    private CmsConfigurationCache m_onlineCache;
247
248    /** The online CMS context. */
249    private CmsObject m_onlineCms;
250
251    /** The online inherited container configuration cache. */
252    private CmsContainerConfigurationCache m_onlineContainerConfigurationCache;
253
254    /** The Online project detail id cache. */
255    private CmsDetailNameCache m_onlineDetailIdCache;
256
257    /** The online formatter bean cache. */
258    private CmsFormatterConfigurationCache m_onlineFormatterCache;
259
260    /** ADE parameters. */
261    private Map<String, String> m_parameters;
262
263    /** The table of upload warnings. */
264    private CmsUploadWarningTable m_uploadWarningTable = new CmsUploadWarningTable();
265
266    /** The providers used to inject additional configuration details into the sitemap configuration. */
267    private List<I_CmsSitemapExtraInfoProvider> m_sitemapExtraInfoProviders = new ArrayList<>();
268
269    /**
270     * Creates a new ADE manager.<p>
271     *
272     * @param adminCms a CMS context with admin privileges
273     * @param memoryMonitor the memory monitor instance
274     * @param systemConfiguration the system configuration
275     */
276    public CmsADEManager(
277        CmsObject adminCms,
278        CmsMemoryMonitor memoryMonitor,
279        CmsSystemConfiguration systemConfiguration) {
280
281        // initialize the ade cache
282        CmsADECacheSettings cacheSettings = systemConfiguration.getAdeCacheSettings();
283        if (cacheSettings == null) {
284            cacheSettings = new CmsADECacheSettings();
285        }
286        m_onlineCms = adminCms;
287        m_cache = new CmsADECache(memoryMonitor, cacheSettings);
288        m_parameters = new LinkedHashMap<String, String>(systemConfiguration.getAdeParameters());
289        m_detailPageHandler = systemConfiguration.getDetailPageHandler();
290        // further initialization is done by the initialize() method. We don't do that in the constructor,
291        // because during the setup the configuration resource types don't exist yet.
292    }
293
294    /**
295     * Adds a wait handle for the next cache update to a formatter configuration.<p>
296     *
297     * @param online true if we want to add a wait handle to the online cache, else the offline cache
298     * @return the wait handle that has been added
299     */
300    public CmsWaitHandle addFormatterCacheWaitHandle(boolean online) {
301
302        CmsWaitHandle handle = new CmsWaitHandle(true); // single use wait handle
303        CmsFormatterConfigurationCache cache = online ? m_onlineFormatterCache : m_offlineFormatterCache;
304        cache.addWaitHandle(handle);
305        return handle;
306    }
307
308    /**
309     * Adds a sitemap extra info provider.
310     *
311     * @param provider the provider to add
312     */
313    public void addSitemapExtraInfoProvider(I_CmsSitemapExtraInfoProvider provider) {
314
315        m_sitemapExtraInfoProviders.add(provider);
316    }
317
318    /**
319     * Checks if the sitemap config can be edited by the user in the given CMS context.
320     *
321     * <p>Note: Even if this returns true, there may be other reasons preventing the sitemap configuration from being edited by the user.
322     *
323     * @param cms the CMS context to check
324     * @return false if the user should not be able to edit the sitemap configuration
325     */
326    public boolean canEditSitemapConfiguration(CmsObject cms) {
327
328        CmsRole role = getRoleForSitemapConfigEditing();
329        if (role == null) {
330            return true;
331        }
332        return OpenCms.getRoleManager().hasRole(cms, role);
333    }
334
335    /**
336     * Finds the entry point to a sitemap.<p>
337     *
338     * @param cms the CMS context
339     * @param openPath the resource path to find the sitemap to
340     *
341     * @return the sitemap entry point
342     */
343    public String findEntryPoint(CmsObject cms, String openPath) {
344
345        CmsADEConfigData configData = lookupConfiguration(cms, openPath);
346        String result = configData.getBasePath();
347        if (result == null) {
348            return cms.getRequestContext().addSiteRoot("/");
349        }
350        return result;
351    }
352
353    /**
354     * Flushes inheritance group changes so the cache is updated.<p>
355     *
356     * This is useful for test cases.
357     */
358    public void flushInheritanceGroupChanges() {
359
360        m_offlineContainerConfigurationCache.flushUpdates();
361        m_onlineContainerConfigurationCache.flushUpdates();
362    }
363
364    /**
365     * Gets the complete list of beans for the currently configured detail pages.<p>
366     *
367     * @param cms the CMS context to use
368     *
369     * @return the list of detail page infos
370     */
371    public List<CmsDetailPageInfo> getAllDetailPages(CmsObject cms) {
372
373        return getCacheState(isOnline(cms)).getAllDetailPages();
374    }
375
376    /**
377     * Gets the containerpage cache instance.<p>
378     *
379     * @return the containerpage cache instance
380     */
381    public CmsADECache getCache() {
382
383        return m_cache;
384    }
385
386    /**
387     * Gets the cached formatter beans.<p>
388     *
389     * @param online true if the Online project formatters should be returned, false for the Offline formatters
390     *
391     * @return the formatter configuration cache state
392     */
393    public CmsFormatterConfigurationCacheState getCachedFormatters(boolean online) {
394
395        CmsFormatterConfigurationCache cache = online ? m_onlineFormatterCache : m_offlineFormatterCache;
396        return cache.getState();
397    }
398
399    /**
400     * Gets the current ADE configuration cache state.<p>
401     *
402     * @param online true if you want the online state, false for the offline state
403     *
404     * @return the configuration cache state
405     */
406    public CmsADEConfigCacheState getCacheState(boolean online) {
407
408        return (online ? m_onlineCache : m_offlineCache).getState();
409    }
410
411    /**
412     * Gets the configuration file type.<p>
413     *
414     * @return the configuration file type
415     */
416    public I_CmsResourceType getConfigurationType() {
417
418        return m_configType;
419    }
420
421    /**
422     * Returns the names of the bundles configured as workplace bundles in any module configuration.
423     * @return the names of the bundles configured as workplace bundles in any module configuration.
424     */
425    public Set<String> getConfiguredWorkplaceBundles() {
426
427        CmsADEConfigData configData = internalLookupConfiguration(null, null);
428        return configData.getConfiguredWorkplaceBundles();
429    }
430
431    /**
432     * Gets the content types configured in any sitemap configuations.
433     *
434     * @param online true if the types for the Online project should be fetched
435     * @return the set of content types
436     */
437    public Set<String> getContentTypeNames(boolean online) {
438
439        CmsConfigurationCache cache = online ? m_onlineCache : m_offlineCache;
440        return cache.getState().getContentTypes();
441
442    }
443
444    /**
445     * Reads the current element bean from the request.<p>
446     *
447     * @param req the servlet request
448     *
449     * @return the element bean
450     *
451     * @throws CmsException if no current element is set
452     */
453    public CmsContainerElementBean getCurrentElement(ServletRequest req) throws CmsException {
454
455        CmsJspStandardContextBean sCBean = CmsJspStandardContextBean.getInstance(req);
456        CmsContainerElementBean element = sCBean.getElement();
457        if (element == null) {
458            throw new CmsException(
459                Messages.get().container(
460                    Messages.ERR_READING_ELEMENT_FROM_REQUEST_1,
461                    sCBean.getRequestContext().getUri()));
462        }
463        return element;
464    }
465
466    /**
467     * Gets the detail id cache for the Online or Offline projects.<p>
468     *
469     * @param online if true, gets the Online project detail id
470     *
471     * @return the detail name cache
472     */
473    public CmsDetailNameCache getDetailIdCache(boolean online) {
474
475        return online ? m_onlineDetailIdCache : m_offlineDetailIdCache;
476    }
477
478    /**
479     * Gets the detail page information for  everything.<p>
480     *
481     * @param cms the current CMS context
482     *
483     * @return the list with all the detail page information
484     */
485    public List<DetailInfo> getDetailInfo(CmsObject cms) {
486
487        return getCacheState(isOnline(cms)).getDetailInfosForSubsites(cms);
488    }
489
490    /**
491     * Gets the detail page for a content element.<p>
492     *
493     * @param cms the CMS context
494     * @param pageRootPath the element's root path
495     * @param originPath the path in which the the detail page is being requested
496     *
497     * @return the detail page for the content element
498     */
499    public String getDetailPage(CmsObject cms, String pageRootPath, String originPath) {
500
501        return getDetailPage(cms, pageRootPath, originPath, null);
502    }
503
504    /**
505     * Gets the detail page for a content element.<p>
506     *
507     * @param cms the CMS context
508     * @param rootPath the element's root path
509     * @param linkSource the path in which the the detail page is being requested
510     * @param targetDetailPage the target detail page to use
511     *
512     * @return the detail page for the content element
513     */
514    public String getDetailPage(CmsObject cms, String rootPath, String linkSource, String targetDetailPage) {
515
516        return getDetailPageHandler().getDetailPage(cms, rootPath, linkSource, targetDetailPage);
517    }
518
519    /**
520     * Gets the detail page finder.<p>
521     *
522     * @return the detail page finder
523     */
524    public I_CmsDetailPageHandler getDetailPageHandler() {
525
526        return m_detailPageHandler;
527    }
528
529    /**
530     * Returns the main detail pages for a type in all of the VFS tree.<p>
531     *
532     * @param cms the current CMS context
533     * @param type the resource type name
534     * @return a list of detail page root paths
535     */
536    public List<String> getDetailPages(CmsObject cms, String type) {
537
538        CmsConfigurationCache cache = isOnline(cms) ? m_onlineCache : m_offlineCache;
539        return cache.getState().getDetailPages(type);
540    }
541
542    /**
543     * Gets the set of types for which detail pages are defined.<p>
544     *
545     * @param cms the current CMS context
546     *
547     * @return the set of types for which detail pages are defined
548     */
549    public Set<String> getDetailPageTypes(CmsObject cms) {
550
551        return getCacheState(isOnline(cms)).getDetailPageTypes();
552    }
553
554    /**
555     * Returns the element settings for a given resource.<p>
556     *
557     * @param cms the current cms context
558     * @param resource the resource
559     *
560     * @return the element settings for a given resource
561     *
562     * @throws CmsException if something goes wrong
563     */
564    public Map<String, CmsXmlContentProperty> getElementSettings(CmsObject cms, CmsResource resource)
565    throws CmsException {
566
567        if (CmsResourceTypeXmlContent.isXmlContent(resource)) {
568            Map<String, CmsXmlContentProperty> result = new LinkedHashMap<String, CmsXmlContentProperty>();
569            Map<String, CmsXmlContentProperty> settings = CmsXmlContentDefinition.getContentHandlerForResource(
570                cms,
571                resource).getSettings(cms, resource);
572            result.putAll(settings);
573            return CmsXmlContentPropertyHelper.copyPropertyConfiguration(result);
574        }
575        return Collections.<String, CmsXmlContentProperty> emptyMap();
576    }
577
578    /**
579     * Returns the available element views.<p>
580     *
581     * @param cms the cms context
582     *
583     * @return the element views
584     */
585    public Map<CmsUUID, CmsElementView> getElementViews(CmsObject cms) {
586
587        CmsConfigurationCache cache = getCache(isOnline(cms));
588        List<CmsElementView> viewList = Lists.newArrayList();
589        viewList.addAll(cache.getState().getElementViews().values());
590        viewList.addAll(OpenCms.getWorkplaceManager().getExplorerTypeViews().values());
591        Collections.sort(viewList, new ElementViewComparator());
592        Map<CmsUUID, CmsElementView> result = Maps.newLinkedHashMap();
593        for (CmsElementView viewValue : viewList) {
594            result.put(viewValue.getId(), viewValue);
595        }
596        return result;
597    }
598
599    /**
600     * Gets the element view configuration resource type.<p>
601     *
602     * @return the element view configuration resource type
603     */
604    public I_CmsResourceType getElementViewType() {
605
606        return m_elementViewType;
607    }
608
609    /**
610     * Returns the favorite list, or creates it if not available.<p>
611     *
612     * @param cms the cms context
613     *
614     * @return the favorite list
615     *
616     * @throws CmsException if something goes wrong
617     */
618    public List<CmsContainerElementBean> getFavoriteList(CmsObject cms) throws CmsException {
619
620        CmsUser user = cms.getRequestContext().getCurrentUser();
621        Object obj = user.getAdditionalInfo(ADDINFO_ADE_FAVORITE_LIST);
622
623        List<CmsContainerElementBean> favList = new ArrayList<CmsContainerElementBean>();
624        if (obj instanceof String) {
625            try {
626                JSONArray array = new JSONArray((String)obj);
627                for (int i = 0; i < array.length(); i++) {
628                    try {
629                        favList.add(elementFromJson(array.getJSONObject(i)));
630                    } catch (Throwable e) {
631                        // should never happen, catches wrong or no longer existing values
632                        LOG.warn(e.getLocalizedMessage());
633                    }
634                }
635            } catch (Throwable e) {
636                // should never happen, catches json parsing
637                LOG.warn(e.getLocalizedMessage());
638            }
639        } else {
640            // save to be better next time
641            saveFavoriteList(cms, favList);
642        }
643
644        return favList;
645    }
646
647    /**
648     * Returns the settings configured for the given formatter which should be editable via ADE.<p>
649     *
650     * @param cms the cms context
651     * @param config the sitemap configuration
652     * @param mainFormatter the formatter
653     * @param res the element resource
654     * @param locale the content locale
655     * @param req the current request, if available
656     *
657     * @return the settings configured for the given formatter
658     */
659    public Map<String, CmsXmlContentProperty> getFormatterSettings(
660        CmsObject cms,
661        CmsADEConfigData config,
662        I_CmsFormatterBean mainFormatter,
663        CmsResource res,
664        Locale locale,
665        ServletRequest req) {
666
667        Map<String, CmsXmlContentProperty> result = new LinkedHashMap<String, CmsXmlContentProperty>();
668        Visibility defaultVisibility = Visibility.elementAndParentIndividual;
669        if (mainFormatter != null) {
670            for (Entry<String, CmsXmlContentProperty> entry : mainFormatter.getSettings(config).entrySet()) {
671                Visibility visibility = entry.getValue().getVisibility(defaultVisibility);
672                if (visibility.isVisibleOnElement()) {
673                    result.put(entry.getKey(), entry.getValue());
674                }
675            }
676            if (mainFormatter.hasNestedFormatterSettings()) {
677                List<I_CmsFormatterBean> nestedFormatters = getNestedFormatters(cms, config, res, locale, req);
678                if (nestedFormatters != null) {
679                    for (I_CmsFormatterBean formatter : nestedFormatters) {
680                        for (Entry<String, CmsXmlContentProperty> entry : formatter.getSettings(config).entrySet()) {
681                            Visibility visibility = entry.getValue().getVisibility(defaultVisibility);
682                            switch (visibility) {
683                                case parentShared:
684                                case elementAndParentShared:
685                                    result.put(entry.getKey(), entry.getValue());
686                                    break;
687                                case elementAndParentIndividual:
688                                case parentIndividual:
689                                    String settingName = formatter.getKeyOrId() + "_" + entry.getKey();
690                                    CmsXmlContentProperty settingConf = entry.getValue().withName(settingName);
691                                    result.put(settingName, settingConf);
692                                    break;
693                                default:
694                                    break;
695                            }
696                        }
697                    }
698                }
699            }
700        }
701        return result;
702    }
703
704    /**
705     * Returns the inheritance state for the given inheritance name and resource.<p>
706     *
707     * @param cms the current cms context
708     * @param resource the resource
709     * @param name the inheritance name
710     *
711     * @return the inheritance state
712     */
713    public CmsInheritedContainerState getInheritedContainerState(CmsObject cms, CmsResource resource, String name) {
714
715        String rootPath = resource.getRootPath();
716        if (!resource.isFolder()) {
717            rootPath = CmsResource.getParentFolder(rootPath);
718        }
719        CmsInheritedContainerState result = new CmsInheritedContainerState();
720        boolean online = isOnline(cms);
721        CmsContainerConfigurationCache cache = online
722        ? m_onlineContainerConfigurationCache
723        : m_offlineContainerConfigurationCache;
724        result.addConfigurations(cache, rootPath, name);
725        return result;
726
727    }
728
729    /**
730     * Returns the inheritance state for the given inheritance name and root path.<p>
731     *
732     * @param cms the current cms context
733     * @param rootPath the root path
734     * @param name the inheritance name
735     *
736     * @return the inheritance state
737     *
738     * @throws CmsException if something goes wrong
739     */
740    public CmsInheritedContainerState getInheritedContainerState(CmsObject cms, String rootPath, String name)
741    throws CmsException {
742
743        String oldSiteRoot = cms.getRequestContext().getSiteRoot();
744        try {
745            cms.getRequestContext().setSiteRoot("");
746            CmsResource resource = cms.readResource(rootPath);
747            return getInheritedContainerState(cms, resource, name);
748        } finally {
749            cms.getRequestContext().setSiteRoot(oldSiteRoot);
750        }
751    }
752
753    /**
754     * Gets the maximum sitemap depth.<p>
755     *
756     * @return the maximum sitemap depth
757     */
758    public int getMaxSitemapDepth() {
759
760        return 20;
761    }
762
763    /**
764     * Gets the module configuration resource type.<p>
765     *
766     * @return the module configuration resource type
767     */
768    public I_CmsResourceType getModuleConfigurationType() {
769
770        return m_moduleConfigType;
771    }
772
773    /**
774     * Returns the nested formatters of the given resource.<p>
775     *
776     * @param cms the cms context
777     * @param config the sitemap configuration
778     * @param res the resource
779     * @param locale the content locale
780     * @param req the request, if available
781     *
782     * @return the nested formatters
783     */
784    public List<I_CmsFormatterBean> getNestedFormatters(
785        CmsObject cms,
786        CmsADEConfigData config,
787        CmsResource res,
788        Locale locale,
789        ServletRequest req) {
790
791        List<I_CmsFormatterBean> result = null;
792        if (CmsResourceTypeXmlContent.isXmlContent(res)) {
793            CmsResourceTypeXmlContent type = (CmsResourceTypeXmlContent)OpenCms.getResourceManager().getResourceType(
794                res);
795            String schema = type.getSchema();
796            try {
797                CmsXmlContentDefinition contentDefinition = CmsXmlContentDefinition.unmarshal(cms, schema);
798                // get the content handler for the resource type to create
799                I_CmsXmlContentHandler handler = contentDefinition.getContentHandler();
800                if (handler.hasNestedFormatters()) {
801                    result = new ArrayList<I_CmsFormatterBean>();
802                    for (String formatterId : handler.getNestedFormatters(cms, res, locale, req)) {
803                        I_CmsFormatterBean formatter = config.findFormatter(formatterId);
804                        if (formatter != null) {
805                            result.add(formatter);
806                        }
807                    }
808                }
809            } catch (CmsXmlException e) {
810                LOG.error(e.getMessage(), e);
811            }
812        }
813        return result;
814    }
815
816    /**
817     * Creates a stream that produces the pages/groups referencing a given element.
818     *
819     *  <p>Note that this method doesn't take a CmsObject and just generates all resources referencing the element regardless of whether
820     *  the current user can read them. So readResource calls for the ids of these resources may fail.
821     *
822     * @param resource the element resource
823     * @return the stream of resources which use the element
824     */
825    public Stream<CmsResource> getOfflineElementUses(CmsResource resource) {
826
827        if ((resource == null) || resource.getStructureId().isNullUUID()) {
828            return Stream.of();
829        }
830        try {
831            List<CmsRelation> relations = m_offlineCms.readRelations(
832                CmsRelationFilter.relationsToStructureId(resource.getStructureId()));
833
834            return relations.stream().flatMap(rel -> {
835                try {
836                    CmsResource source = rel.getSource(m_offlineCms, CmsResourceFilter.ALL);
837                    return Stream.of(source);
838                } catch (Exception e) {
839                    LOG.debug(e.getLocalizedMessage(), e);
840                    return Stream.of();
841                }
842            }).filter(source -> {
843                return (CmsResourceTypeXmlContainerPage.isContainerPage(source)
844                    || CmsResourceTypeXmlContainerPage.isModelGroup(source)
845                    || OpenCms.getResourceManager().matchResourceType(
846                        CmsResourceTypeXmlContainerPage.GROUP_CONTAINER_TYPE_NAME,
847                        source.getTypeId()));
848            });
849        } catch (CmsException e) {
850            LOG.error(e.getLocalizedMessage(), e);
851            return Stream.of();
852        }
853    }
854
855    /**
856     * Gets ADE parameters.<p>
857     *
858     * @param cms the current CMS context
859     * @return the ADE parameters for the current user
860     */
861    public Map<String, String> getParameters(CmsObject cms) {
862
863        Map<String, String> result = new LinkedHashMap<String, String>(m_parameters);
864        if (cms != null) {
865            String userParamsStr = (String)(cms.getRequestContext().getCurrentUser().getAdditionalInfo().get(
866                "ADE_PARAMS"));
867            if (userParamsStr != null) {
868                Map<String, String> userParams = CmsStringUtil.splitAsMap(userParamsStr, "|", ":");
869                result.putAll(userParams);
870            }
871        }
872        return result;
873    }
874
875    /**
876     * Gets the content element type for the given path's parent folder.
877     *
878     * @param online true if we want to use the Online project's configuration
879     * @param rootPath the root path of a content
880     *
881     * @return the parent folder type name, or null if none is defined
882     */
883    public String getParentFolderType(boolean online, String rootPath) {
884
885        return getCacheState(online).getParentFolderType(rootPath);
886
887    }
888
889    /**
890     * Gets the root paths to use for secret lookup in the current project and based on the given root path, ordered from most deeply nested to least deeply nested.
891     *
892     * <p>These consist of all subsite paths, up to and including the site root, even if the site root does not have a .content/.config file.
893     * <p>If the given root path is not part of a site, the list of all containing subsite roots is returned.
894     *
895     * @param cms the current CMS context
896     * @param rootPath the root path for which to get the secret lookup paths
897     * @return
898     */
899    public List<String> getPathsForSecretLookup(CmsObject cms, String rootPath) {
900
901        CmsADEConfigCacheState state = getCacheState(cms.getRequestContext().getCurrentProject().isOnlineProject());
902        Set<String> siteConfigPaths = state.getSiteConfigurationPaths(); // has trailing slashes
903        String currentPath = rootPath;
904        currentPath = CmsFileUtil.addTrailingSeparator(currentPath);
905        String siteRoot = OpenCms.getSiteManager().getSiteRoot(rootPath);
906        if (siteRoot != null) {
907            siteRoot = CmsFileUtil.addTrailingSeparator(siteRoot);
908        }
909        List<String> result = new ArrayList<>();
910        while (currentPath != null) {
911            boolean isSiteRoot = currentPath.equals(siteRoot);
912            if (isSiteRoot || siteConfigPaths.contains(currentPath)) {
913                result.add(CmsFileUtil.removeTrailingSeparator(currentPath));
914            }
915            if (isSiteRoot) {
916                break;
917            }
918            currentPath = CmsResource.getParentFolder(currentPath); //  has trailing slash
919        }
920        return result;
921    }
922
923    /**
924     * Returns the permission info for the given resource.<p>
925     *
926     * @param cms the cms context
927     * @param resource the resource
928     * @param contextPath the context path
929     *
930     * @return the permission info
931     *
932     * @throws CmsException if checking the permissions fails
933     */
934    public CmsPermissionInfo getPermissionInfo(CmsObject cms, CmsResource resource, String contextPath)
935    throws CmsException {
936
937        boolean hasView = cms.hasPermissions(
938            resource,
939            CmsPermissionSet.ACCESS_VIEW,
940            false,
941            CmsResourceFilter.ALL.addRequireVisible());
942        boolean hasWrite = false;
943        if (hasView) {
944            try {
945                I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(resource.getTypeId());
946                CmsExplorerTypeSettings settings = OpenCms.getWorkplaceManager().getExplorerTypeSetting(
947                    type.getTypeName());
948                hasView = (settings == null)
949                    || settings.getAccess().getPermissions(cms, resource).requiresViewPermission();
950                if (hasView
951                    && CmsResourceTypeXmlContent.isXmlContent(resource)
952                    && !CmsResourceTypeXmlContainerPage.isContainerPage(resource)) {
953                    if (contextPath == null) {
954                        contextPath = resource.getRootPath();
955                    }
956                    CmsResourceTypeConfig localConfigData = lookupConfigurationWithCache(
957                        cms,
958                        contextPath).getResourceType(type.getTypeName());
959                    if (localConfigData != null) {
960                        Map<CmsUUID, CmsElementView> elementViews = getElementViews(cms);
961                        hasView = elementViews.containsKey(localConfigData.getElementView())
962                            && elementViews.get(localConfigData.getElementView()).hasPermission(cms, resource);
963                    }
964                }
965                // the user may only have write permissions if he is allowed to view the resource
966                hasWrite = hasView
967                    && cms.hasPermissions(
968                        resource,
969                        CmsPermissionSet.ACCESS_WRITE,
970                        false,
971                        CmsResourceFilter.IGNORE_EXPIRATION)
972                    && ((settings == null)
973                        || settings.getAccess().getPermissions(cms, resource).requiresWritePermission());
974            } catch (CmsLoaderException e) {
975                LOG.warn(e.getLocalizedMessage(), e);
976                hasWrite = false;
977            }
978        }
979
980        if (hasWrite && isEditorRestricted(cms, resource)) {
981            hasWrite = false;
982        }
983
984        String noEdit = new CmsResourceUtil(cms, resource).getNoEditReason(
985            OpenCms.getWorkplaceManager().getWorkplaceLocale(cms),
986            true);
987
988        boolean isFunction = false;
989        for (String type : new String[] {"function", CmsResourceTypeFunctionConfig.TYPE_NAME}) {
990            if (OpenCms.getResourceManager().matchResourceType(type, resource.getTypeId())) {
991                isFunction = true;
992                break;
993            }
994        }
995        if (isFunction) {
996            Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms);
997            noEdit = Messages.get().getBundle(locale).key(Messages.GUI_CANT_EDIT_FUNCTIONS_0);
998        }
999
1000        return new CmsPermissionInfo(hasView, hasWrite, noEdit);
1001    }
1002
1003    /**
1004     * Gets a map of plugin wrappers for the given site path.
1005     *
1006     * <p>This *only* includes plugins defined in site plugins active on the given path, not those referenced in formatters.
1007     *
1008     * @param cms the CMS context
1009     * @param path the path for which to get the plugins
1010     *
1011     * @return the map of plugin wrappers, with the plugin groups as keys
1012     */
1013    public Map<String, List<CmsTemplatePluginWrapper>> getPluginsForPath(CmsObject cms, String path) {
1014
1015        CmsADEConfigData config = lookupConfigurationWithCache(cms, cms.getRequestContext().addSiteRoot(path));
1016
1017        Multimap<String, CmsTemplatePlugin> plugins = CmsTemplatePluginFinder.getActiveTemplatePluginsFromSitePlugins(
1018            config);
1019        Map<String, List<CmsTemplatePluginWrapper>> result = new HashMap<>();
1020        for (String key : plugins.keySet()) {
1021            List<CmsTemplatePluginWrapper> wrappers = plugins.get(key).stream().map(
1022                plugin -> new CmsTemplatePluginWrapper(cms, plugin)).collect(Collectors.toList());
1023            result.put(key, Collections.unmodifiableList(wrappers));
1024        }
1025        return Collections.unmodifiableMap(result);
1026
1027    }
1028
1029    /**
1030     * Gets the raw configured detail page information, with no existence checks or path correction.
1031     *
1032     * @param cms the CMS context
1033     * @return the list of raw detail page info beans
1034     */
1035    public List<CmsDetailPageInfo> getRawDetailPages(CmsObject cms) {
1036
1037        return getCache(cms.getRequestContext().getCurrentProject().isOnlineProject()).getRawDetailPages();
1038    }
1039
1040    /**
1041     * Returns the favorite list, or creates it if not available.<p>
1042     *
1043     * @param cms the cms context
1044     *
1045     * @return the favorite list
1046     *
1047     * @throws CmsException if something goes wrong
1048     */
1049    public List<CmsContainerElementBean> getRecentList(CmsObject cms) throws CmsException {
1050
1051        CmsUser user = cms.getRequestContext().getCurrentUser();
1052        Object obj = user.getAdditionalInfo(ADDINFO_ADE_RECENT_LIST);
1053
1054        List<CmsContainerElementBean> recentList = new ArrayList<CmsContainerElementBean>();
1055        if (obj instanceof String) {
1056            try {
1057                JSONArray array = new JSONArray((String)obj);
1058                for (int i = 0; i < array.length(); i++) {
1059                    try {
1060                        recentList.add(elementFromJson(array.getJSONObject(i)));
1061                    } catch (Throwable e) {
1062                        // should never happen, catches wrong or no longer existing values
1063                        LOG.warn(e.getLocalizedMessage());
1064                    }
1065                }
1066            } catch (Throwable e) {
1067                // should never happen, catches json parsing
1068                LOG.warn(e.getLocalizedMessage());
1069            }
1070        } else {
1071            // save to be better next time
1072            saveRecentList(cms, recentList);
1073        }
1074
1075        return recentList;
1076    }
1077
1078    /**
1079     * Gets a map of sitemap attribute values by sitemap path for a given attribute key.
1080     *
1081     * @param cms the current CMS context
1082     * @param attribute the sitemap attribute key
1083     *
1084     * @return the map of attribute values, with the root paths of the corresponding subsitemaps as keys
1085     */
1086    public Map<String, String> getSitemapAttributeValuesByPath(CmsObject cms, String attribute) {
1087
1088        boolean online = (null == cms) || isOnline(cms);
1089        CmsADEConfigCacheState state = getCacheState(online);
1090        return state.getAttributeValuesByPath(attribute);
1091    }
1092
1093    /**
1094     * Gets the sitemap configuration resource type.<p>
1095     *
1096     * @return the resource type for sitemap configurations
1097     */
1098    public I_CmsResourceType getSitemapConfigurationType() {
1099
1100        return m_configType;
1101    }
1102
1103    /**
1104     * Gets the registered sitemap extra info providers.
1105     *
1106     * @return the list of providers
1107     */
1108    public List<I_CmsSitemapExtraInfoProvider> getSitemapExtraInfoProviders() {
1109
1110        return m_sitemapExtraInfoProviders.stream().sorted(
1111            Comparator.comparing(provider -> provider.getOrder())).collect(Collectors.toList());
1112    }
1113
1114    /**
1115     * Returns all sub sites below the given path.<p>
1116     *
1117     * @param cms the cms context
1118     * @param subSiteRoot the sub site root path
1119     *
1120     * @return the sub site root paths
1121     */
1122    public List<String> getSubSitePaths(CmsObject cms, String subSiteRoot) {
1123
1124        List<String> result = new ArrayList<String>();
1125        String normalizedRootPath = CmsStringUtil.joinPaths("/", subSiteRoot, "/");
1126        CmsADEConfigCacheState state = getCacheState(isOnline(cms));
1127        Set<String> siteConfigurationPaths = state.getSiteConfigurationPaths();
1128        for (String path : siteConfigurationPaths) {
1129            if ((path.length() > normalizedRootPath.length()) && path.startsWith(normalizedRootPath)) {
1130                result.add(path);
1131            }
1132        }
1133        return result;
1134    }
1135
1136    /**
1137     * Tries to get the subsite root for a given resource root path.<p>
1138     *
1139     * @param cms the current CMS context
1140     * @param rootPath the root path for which the subsite root should be found
1141     *
1142     * @return the subsite root
1143     */
1144    public String getSubSiteRoot(CmsObject cms, String rootPath) {
1145
1146        CmsADEConfigData configData = lookupConfiguration(cms, rootPath);
1147        String basePath = configData.getBasePath();
1148        String siteRoot = OpenCms.getSiteManager().getSiteRoot(rootPath);
1149        if (siteRoot == null) {
1150            siteRoot = "";
1151        }
1152        if ((basePath == null) || !basePath.startsWith(siteRoot)) {
1153            // the subsite root should always be below the site root
1154            return siteRoot;
1155        } else {
1156            return basePath;
1157        }
1158    }
1159
1160    /**
1161     * Gets the subsites to be displayed in the site selector.
1162     *
1163     * @param online true if we want the subsites for the Online project
1164     *
1165     * @return the subsites to be displayed in the site selector
1166     */
1167    public List<String> getSubsitesForSiteSelector(boolean online) {
1168
1169        return getCacheState(online).getSubsitesForSiteSelector();
1170
1171    }
1172
1173    /**
1174     * Gets the table of upload warnings.
1175     *
1176     * @return the table of upload warnings
1177     */
1178    public CmsUploadWarningTable getUploadWarningTable() {
1179
1180        return m_uploadWarningTable;
1181    }
1182
1183    /**
1184     * Processes a HTML redirect content.<p>
1185     *
1186     * This needs to be in the ADE manager because the user for whom the HTML redirect is being loaded
1187     * does not necessarily have read permissions for the redirect target, so we read the redirect target
1188     * with admin privileges.<p>
1189     *
1190     * @param userCms the CMS context of the current user
1191     * @param request the servlet request
1192     * @param response the servlet response
1193     * @param htmlRedirect the path of the HTML redirect resource
1194     *
1195     * @throws CmsException if something goes wrong
1196     */
1197    public void handleHtmlRedirect(
1198        CmsObject userCms,
1199        HttpServletRequest request,
1200        HttpServletResponse response,
1201        String htmlRedirect)
1202    throws CmsException {
1203
1204        CmsObject cms = OpenCms.initCmsObject(m_offlineCms);
1205        CmsRequestContext userContext = userCms.getRequestContext();
1206        CmsRequestContext currentContext = cms.getRequestContext();
1207        currentContext.setCurrentProject(userContext.getCurrentProject());
1208        currentContext.setSiteRoot(userContext.getSiteRoot());
1209        currentContext.setLocale(userContext.getLocale());
1210        currentContext.setUri(userContext.getUri());
1211
1212        CmsFile file = cms.readFile(htmlRedirect);
1213        CmsXmlContent content = CmsXmlContentFactory.unmarshal(cms, file);
1214
1215        // find out the locale to use for reading values from the redirect
1216        List<Locale> candidates = new ArrayList<Locale>();
1217        candidates.add(currentContext.getLocale());
1218        candidates.add(CmsLocaleManager.getDefaultLocale());
1219        candidates.add(Locale.ENGLISH);
1220        candidates.addAll(content.getLocales());
1221        Locale contentLocale = currentContext.getLocale();
1222        for (Locale candidateLocale : candidates) {
1223            if (content.hasLocale(candidateLocale)) {
1224                contentLocale = candidateLocale;
1225                break;
1226            }
1227        }
1228
1229        String typeValue = content.getValue(N_TYPE, contentLocale).getStringValue(cms);
1230        String lnkUri = "";
1231        Integer errorCode;
1232        if ("sublevel".equals(typeValue)) {
1233            // use the nav builder to get the first sub level entry
1234            CmsJspNavBuilder navBuilder = new CmsJspNavBuilder(cms);
1235            if (navBuilder.getNavigationForFolder().size() > 0) {
1236                CmsJspNavElement target = navBuilder.getNavigationForFolder().get(0);
1237                lnkUri = CmsJspTagLink.linkTagAction(target.getResourceName(), request);
1238                errorCode = Integer.valueOf(HttpServletResponse.SC_MOVED_TEMPORARILY);
1239            } else {
1240                // send error 404 if no sub entry available
1241                errorCode = Integer.valueOf(HttpServletResponse.SC_NOT_FOUND);
1242            }
1243        } else {
1244            I_CmsXmlContentValue contentValue = content.getValue(N_LINK, contentLocale);
1245            if (contentValue != null) {
1246                String linkValue = contentValue.getStringValue(cms);
1247                lnkUri = OpenCms.getLinkManager().substituteLinkForUnknownTarget(cms, linkValue);
1248                try {
1249                    errorCode = Integer.valueOf(typeValue);
1250                } catch (NumberFormatException e) {
1251                    LOG.error(e.getMessage(), e);
1252                    // fall back to default
1253                    errorCode = Integer.valueOf(307);
1254                }
1255            } else {
1256                // send error 404 if no link value is set
1257                errorCode = Integer.valueOf(HttpServletResponse.SC_NOT_FOUND);
1258            }
1259        }
1260        if (!currentContext.getCurrentProject().isOnlineProject()) {
1261            // permanent redirects are confusing and not useful in the Offline project because they are stored
1262            // by the browser based on the host name, not the site the user is working in.
1263            if (errorCode.intValue() == HttpServletResponse.SC_MOVED_PERMANENTLY) {
1264                errorCode = Integer.valueOf(HttpServletResponse.SC_MOVED_TEMPORARILY);
1265            }
1266        }
1267        request.setAttribute(CmsRequestUtil.ATTRIBUTE_ERRORCODE, errorCode);
1268        response.setHeader("Location", CmsEncoder.convertHostToPunycode(lnkUri));
1269        response.setHeader("Connection", "close");
1270        response.setStatus(errorCode.intValue());
1271    }
1272
1273    /**
1274     * Initializes the configuration by reading all configuration files and caching their data.<p>
1275     */
1276    public synchronized void initialize() {
1277
1278        // no need to try initialization in case the 'org.opencms.base' is not present and the contained resource types missing
1279        if ((m_initStatus == Status.notInitialized) && OpenCms.getModuleManager().hasModule(MODULE_NAME_ADE_CONFIG)) {
1280            try {
1281                CmsLog.INIT.info(". Initializing the ADE configuration, this may take a while...");
1282                m_initStatus = Status.initializing;
1283                m_configType = OpenCms.getResourceManager().getResourceType(CONFIG_TYPE);
1284                m_moduleConfigType = OpenCms.getResourceManager().getResourceType(MODULE_CONFIG_TYPE);
1285                m_elementViewType = OpenCms.getResourceManager().getResourceType(ELEMENT_VIEW_TYPE);
1286                CmsProject temp = getTempfileProject(m_onlineCms);
1287                m_offlineCms = OpenCms.initCmsObject(m_onlineCms);
1288                m_offlineCms.getRequestContext().setCurrentProject(temp);
1289                m_onlineCache = new CmsConfigurationCache(
1290                    m_onlineCms,
1291                    m_configType,
1292                    m_moduleConfigType,
1293                    m_elementViewType);
1294                m_offlineCache = new CmsConfigurationCache(
1295                    m_offlineCms,
1296                    m_configType,
1297                    m_moduleConfigType,
1298                    m_elementViewType);
1299                CmsLog.INIT.info(". Reading online configuration...");
1300                m_onlineCache.initialize();
1301                CmsLog.INIT.info(". Reading offline configuration...");
1302                m_offlineCache.initialize();
1303                m_onlineContainerConfigurationCache = new CmsContainerConfigurationCache(
1304                    m_onlineCms,
1305                    "online inheritance groups");
1306                m_offlineContainerConfigurationCache = new CmsContainerConfigurationCache(
1307                    m_offlineCms,
1308                    "offline inheritance groups");
1309                CmsLog.INIT.info(". Reading online inherited container configurations...");
1310                m_onlineContainerConfigurationCache.initialize();
1311                CmsLog.INIT.info(". Reading offline inherited container configurations...");
1312                m_offlineContainerConfigurationCache.initialize();
1313                m_offlineFormatterCache = new CmsFormatterConfigurationCache(m_offlineCms, "offline formatters");
1314                m_onlineFormatterCache = new CmsFormatterConfigurationCache(m_onlineCms, "online formatters");
1315                CmsLog.INIT.info(". Reading online formatter configurations...");
1316                m_onlineFormatterCache.initialize();
1317                CmsLog.INIT.info(". Reading offline formatter configurations...");
1318                m_offlineFormatterCache.initialize();
1319
1320                m_offlineDetailIdCache = new CmsDetailNameCache(m_offlineCms);
1321                m_onlineDetailIdCache = new CmsDetailNameCache(m_onlineCms);
1322                CmsLog.INIT.info(". Initializing online detail name cache...");
1323                m_onlineDetailIdCache.initialize();
1324                CmsLog.INIT.info(". Initializing offline detail name cache...");
1325                m_offlineDetailIdCache.initialize();
1326
1327                CmsGlobalConfigurationCacheEventHandler handler = new CmsGlobalConfigurationCacheEventHandler(
1328                    m_onlineCms);
1329                handler.addCache(m_offlineCache, m_onlineCache, "ADE configuration cache");
1330                handler.addCache(
1331                    m_offlineContainerConfigurationCache,
1332                    m_onlineContainerConfigurationCache,
1333                    "Inherited container cache");
1334                handler.addCache(m_offlineFormatterCache, m_onlineFormatterCache, "formatter configuration cache");
1335                handler.addCache(m_offlineDetailIdCache, m_onlineDetailIdCache, "Detail ID cache");
1336                OpenCms.getEventManager().addCmsEventListener(handler);
1337                CmsLog.INIT.info(". Done initializing the ADE configuration.");
1338                m_initStatus = Status.initialized;
1339            } catch (CmsException e) {
1340                m_initStatus = Status.notInitialized;
1341                LOG.error(e.getLocalizedMessage(), e);
1342            }
1343            m_detailPageHandler.initialize(m_offlineCms, m_onlineCms);
1344        }
1345    }
1346
1347    /**
1348     * Checks whether the given resource is configured as a detail page.<p>
1349     *
1350     * @param cms the current CMS context
1351     * @param resource the resource which should be tested
1352     *
1353     * @return true if the resource is configured as a detail page
1354     */
1355    public boolean isDetailPage(CmsObject cms, CmsResource resource) {
1356
1357        return getCache(isOnline(cms)).isDetailPage(cms, resource);
1358    }
1359
1360    /**
1361     * Checks if the user should be prevented from editing a file.
1362     *
1363     * <p>This is not a permission check, but an additional mechanism to prevent users from editing configuration files even if they technically need or have write permissions for these files.
1364     *
1365     * @param cms the CMS context
1366     * @param res the resource to check
1367     * @return true if the user should be prevented from editing the file
1368     */
1369    public boolean isEditorRestricted(CmsObject cms, CmsResource res) {
1370
1371        if (OpenCms.getResourceManager().matchResourceType(CONFIG_TYPE, res.getTypeId())) {
1372            CmsRole role = getRoleForSitemapConfigEditing();
1373            if ((role != null) && !OpenCms.getRoleManager().hasRoleForResource(cms, role, res)) {
1374                return true;
1375            }
1376        }
1377        return false;
1378    }
1379
1380    /**
1381     * Checks if an element is reused in a page or group that is not excluded by a given predicate.
1382     *
1383     * @param resource the resource to check
1384     * @param exclude predicate used to ignore reuses which match it
1385     * @return true if the element is reused
1386     */
1387    public boolean isElementReused(CmsResource resource, Predicate<CmsResource> exclude) {
1388
1389        return getOfflineElementUses(resource).anyMatch(source -> !exclude.test(source));
1390    }
1391
1392    /**
1393     * Checks whether the ADE manager is initialized (this should usually be the case except during the setup).<p>
1394     *
1395     * @return true if the ADE manager is initialized
1396     */
1397    public boolean isInitialized() {
1398
1399        return m_initStatus == Status.initialized;
1400    }
1401
1402    /**
1403     * Returns the show editor help flag.<p>
1404     *
1405     * @param cms the cms context
1406     *
1407     * @return the show editor help flag
1408     */
1409    public boolean isShowEditorHelp(CmsObject cms) {
1410
1411        CmsUser user = cms.getRequestContext().getCurrentUser();
1412        String showHelp = (String)user.getAdditionalInfo(ADDINFO_ADE_SHOW_EDITOR_HELP);
1413        return CmsStringUtil.isEmptyOrWhitespaceOnly(showHelp) || Boolean.parseBoolean(showHelp);
1414    }
1415
1416    /**
1417     * Looks up the configuration data for a given sitemap path.<p>
1418     *
1419     * @param cms the current CMS context
1420     * @param rootPath the root path for which the configuration data should be looked up
1421     *
1422     * @return the configuration data
1423     */
1424    public CmsADEConfigData lookupConfiguration(CmsObject cms, String rootPath) {
1425
1426        CmsADEConfigData configData = internalLookupConfiguration(cms, rootPath);
1427        return configData;
1428    }
1429
1430    /**
1431     * Looks up the configuration data for a given sitemap path, but uses a thread-local cache for the current request for efficiency.
1432     *
1433     * @param cms the current CMS context
1434     * @param rootPath the root path for which the configuration data should be looked up
1435     *
1436     * @return the configuration data
1437     */
1438    public CmsADEConfigData lookupConfigurationWithCache(CmsObject cms, String rootPath) {
1439
1440        boolean online = (cms == null) || cms.getRequestContext().getCurrentProject().isOnlineProject();
1441        String cacheKey = "" + online + ":" + rootPath;
1442        OpenCmsServlet.RequestCache context = OpenCmsServlet.getRequestCache();
1443        CmsADEConfigData result = null;
1444        if (context != null) {
1445            result = context.getCachedConfig(cacheKey);
1446        }
1447        if (result == null) {
1448            result = internalLookupConfiguration(cms, rootPath);
1449            if (context != null) {
1450                context.setCachedConfig(cacheKey, result);
1451            }
1452        }
1453        return result;
1454    }
1455
1456    /**
1457     * Reloads the configuration.<p>
1458     *
1459     * Normally you shouldn't call this directly since the event handlers take care of updating the configuration.
1460     */
1461    public void refresh() {
1462
1463        m_onlineCache.initialize();
1464        m_offlineCache.initialize();
1465    }
1466
1467    public void removeSitemapExtraInfoProvider(I_CmsSitemapExtraInfoProvider provider) {
1468
1469        m_sitemapExtraInfoProviders.remove(provider);
1470    }
1471
1472    /**
1473     * Saves a list of detail pages.<p>
1474     * @param cms the cms context
1475     * @param rootPath the root path
1476     * @param detailPages the detail pages
1477     * @param newId the id to use for new detail pages without an id
1478     * @return true if the detail pages could be successfully saved
1479     *
1480     * @throws CmsException if something goes wrong
1481     */
1482    public boolean saveDetailPages(CmsObject cms, String rootPath, List<CmsDetailPageInfo> detailPages, CmsUUID newId)
1483    throws CmsException {
1484
1485        CmsADEConfigData configData = lookupConfiguration(cms, rootPath);
1486        CmsDetailPageConfigurationWriter configWriter;
1487        String originalSiteRoot = cms.getRequestContext().getSiteRoot();
1488        try {
1489            cms.getRequestContext().setSiteRoot("");
1490            if (configData.isModuleConfiguration()) {
1491                return false;
1492            }
1493            CmsResource configFile = configData.getResource();
1494            configWriter = new CmsDetailPageConfigurationWriter(cms, configFile);
1495            configWriter.updateAndSave(detailPages, newId);
1496            return true;
1497        } finally {
1498            cms.getRequestContext().setSiteRoot(originalSiteRoot);
1499        }
1500    }
1501
1502    /**
1503     * Saves the favorite list, user based.<p>
1504     *
1505     * @param cms the cms context
1506     * @param favoriteList the element list
1507     *
1508     * @throws CmsException if something goes wrong
1509     */
1510    public void saveFavoriteList(CmsObject cms, List<CmsContainerElementBean> favoriteList) throws CmsException {
1511
1512        saveElementList(cms, favoriteList, ADDINFO_ADE_FAVORITE_LIST);
1513    }
1514
1515    /**
1516     * Saves the inheritance container information.<p>
1517     *
1518     * @param cms the current cms context
1519     * @param pageResource the resource or parent folder
1520     * @param name the inheritance name
1521     * @param newOrder if the element have been reordered
1522     * @param elements the elements
1523     *
1524     * @throws CmsException if something goes wrong
1525     */
1526    public void saveInheritedContainer(
1527        CmsObject cms,
1528        CmsResource pageResource,
1529        String name,
1530        boolean newOrder,
1531        List<CmsContainerElementBean> elements)
1532    throws CmsException {
1533
1534        CmsContainerConfigurationWriter writer = new CmsContainerConfigurationWriter();
1535        writer.save(cms, name, newOrder, pageResource, elements);
1536
1537        // Inheritance groups are usually reloaded directly after saving them,
1538        // so the cache needs to be up to date after this method is called
1539        m_offlineContainerConfigurationCache.flushUpdates();
1540    }
1541
1542    /**
1543     * Saves the inheritance container information.<p>
1544     *
1545     * @param cms the current cms context
1546     * @param sitePath the site path of the resource or parent folder
1547     * @param name the inheritance name
1548     * @param newOrder if the element have been reordered
1549     * @param elements the elements
1550     *
1551     * @throws CmsException if something goes wrong
1552     */
1553    public void saveInheritedContainer(
1554        CmsObject cms,
1555        String sitePath,
1556        String name,
1557        boolean newOrder,
1558        List<CmsContainerElementBean> elements)
1559    throws CmsException {
1560
1561        saveInheritedContainer(cms, cms.readResource(sitePath), name, newOrder, elements);
1562    }
1563
1564    /**
1565     * Saves the favorite list, user based.<p>
1566     *
1567     * @param cms the cms context
1568     * @param recentList the element list
1569     *
1570     * @throws CmsException if something goes wrong
1571     */
1572    public void saveRecentList(CmsObject cms, List<CmsContainerElementBean> recentList) throws CmsException {
1573
1574        saveElementList(cms, recentList, ADDINFO_ADE_RECENT_LIST);
1575    }
1576
1577    /**
1578     * Sets the show editor help flag.<p>
1579     *
1580     * @param cms the cms context
1581     * @param showHelp the show help flag
1582     * @throws CmsException if writing the user info fails
1583     */
1584    public void setShowEditorHelp(CmsObject cms, boolean showHelp) throws CmsException {
1585
1586        CmsUser user = cms.getRequestContext().getCurrentUser();
1587        user.setAdditionalInfo(ADDINFO_ADE_SHOW_EDITOR_HELP, String.valueOf(showHelp));
1588        cms.writeUser(user);
1589    }
1590
1591    /**
1592     * The method which is called when the OpenCms instance is shut down.<p>
1593     */
1594    public void shutdown() {
1595
1596        // do nothing
1597    }
1598
1599    /**
1600     * Waits until the next time the cache is updated.<p>
1601     *
1602     * @param online true if we want to wait for the online cache, false for the offline cache
1603     */
1604    public void waitForCacheUpdate(boolean online) {
1605
1606        getCache(online).getWaitHandleForUpdateTask().enter(2 * CmsConfigurationCache.TASK_DELAY_MILLIS);
1607    }
1608
1609    /**
1610     * Waits until the formatter cache has finished updating itself.<p>
1611     *
1612     * This method is only intended for use in test cases.
1613     *
1614     * @param online true if we should wait for the online formatter cache,false for the offline cache
1615     */
1616    public void waitForFormatterCache(boolean online) {
1617
1618        CmsFormatterConfigurationCache cache = online ? m_onlineFormatterCache : m_offlineFormatterCache;
1619        cache.waitForUpdate();
1620    }
1621
1622    /**
1623     * Creates an element from its serialized data.<p>
1624     *
1625     * @param data the serialized data
1626     *
1627     * @return the restored element bean
1628     *
1629     * @throws JSONException if the serialized data got corrupted
1630     */
1631    protected CmsContainerElementBean elementFromJson(JSONObject data) throws JSONException {
1632
1633        CmsUUID element = new CmsUUID(data.getString(FavListProp.ELEMENT.name().toLowerCase()));
1634        CmsUUID formatter = null;
1635        if (data.has(FavListProp.FORMATTER.name().toLowerCase())) {
1636            formatter = new CmsUUID(data.getString(FavListProp.FORMATTER.name().toLowerCase()));
1637        }
1638        Map<String, String> properties = new HashMap<String, String>();
1639
1640        JSONObject props = data.getJSONObject(FavListProp.PROPERTIES.name().toLowerCase());
1641        Iterator<String> keys = props.keys();
1642        while (keys.hasNext()) {
1643            String key = keys.next();
1644            properties.put(key, props.getString(key));
1645        }
1646
1647        return new CmsContainerElementBean(element, formatter, properties, false);
1648    }
1649
1650    /**
1651     * Converts the given element to JSON.<p>
1652     *
1653     * @param element the element to convert
1654     * @param excludeSettings the keys of settings which should not be written to the JSON
1655     *
1656     * @return the JSON representation
1657     */
1658    protected JSONObject elementToJson(CmsContainerElementBean element, Set<String> excludeSettings) {
1659
1660        JSONObject data = null;
1661        try {
1662            data = new JSONObject();
1663            data.put(FavListProp.ELEMENT.name().toLowerCase(), element.getId().toString());
1664            if (element.getFormatterId() != null) {
1665                data.put(FavListProp.FORMATTER.name().toLowerCase(), element.getFormatterId().toString());
1666            }
1667            JSONObject properties = new JSONObject();
1668            for (Map.Entry<String, String> entry : element.getIndividualSettings().entrySet()) {
1669                String settingKey = entry.getKey();
1670                if (!excludeSettings.contains(settingKey)) {
1671                    properties.put(entry.getKey(), entry.getValue());
1672                }
1673            }
1674            data.put(FavListProp.PROPERTIES.name().toLowerCase(), properties);
1675        } catch (JSONException e) {
1676            // should never happen
1677            if (!LOG.isDebugEnabled()) {
1678                LOG.warn(e.getLocalizedMessage());
1679            }
1680            LOG.debug(e.getLocalizedMessage(), e);
1681            return null;
1682        }
1683        return data;
1684    }
1685
1686    /**
1687     * Gets the configuration cache instance.<p>
1688     *
1689     * @param online true if you want the online cache, false for the offline cache
1690     *
1691     * @return the ADE configuration cache instance
1692     */
1693    protected CmsConfigurationCache getCache(boolean online) {
1694
1695        return online ? m_onlineCache : m_offlineCache;
1696    }
1697
1698    /**
1699     * Gets the offline cache.<p>
1700     *
1701     * @return the offline configuration cache
1702     */
1703    protected CmsConfigurationCache getOfflineCache() {
1704
1705        return m_offlineCache;
1706    }
1707
1708    /**
1709     * Gets the online cache.<p>
1710     *
1711     * @return the online configuration cache
1712     */
1713    protected CmsConfigurationCache getOnlineCache() {
1714
1715        return m_onlineCache;
1716    }
1717
1718    /**
1719     * Gets the role necessary to edit sitemap configuration files.
1720     *
1721     * @return the role needed for editing sitemap configurations
1722     */
1723    protected CmsRole getRoleForSitemapConfigEditing() {
1724
1725        String roleName = OpenCms.getWorkplaceManager().getSitemapConfigEditRole();
1726        if (roleName == null) {
1727            return null;
1728        } else {
1729            if (roleName.indexOf("/") == -1) {
1730                return CmsRole.valueOfRoleName(roleName).forOrgUnit(null);
1731            } else {
1732                return CmsRole.valueOfRoleName(roleName);
1733            }
1734        }
1735    }
1736
1737    /**
1738     * Gets the root path for a given resource structure id.<p>
1739     *
1740     * @param structureId the structure id
1741     * @param online if true, the resource will be looked up in the online project ,else in the offline project
1742     *
1743     * @return the root path for the given structure id
1744     *
1745     * @throws CmsException if something goes wrong
1746     */
1747    protected String getRootPath(CmsUUID structureId, boolean online) throws CmsException {
1748
1749        CmsConfigurationCache cache = online ? m_onlineCache : m_offlineCache;
1750        return cache.getPathForStructureId(structureId);
1751    }
1752
1753    /**
1754     * Gets a tempfile project, creating one if it doesn't exist already.<p>
1755     *
1756     * @param cms the CMS context to use
1757     * @return the tempfile project
1758     *
1759     * @throws CmsException if something goes wrong
1760     */
1761    protected CmsProject getTempfileProject(CmsObject cms) throws CmsException {
1762
1763        try {
1764            return cms.readProject(I_CmsProjectDriver.TEMP_FILE_PROJECT_NAME);
1765        } catch (CmsException e) {
1766            return cms.createTempfileProject();
1767        }
1768    }
1769
1770    /**
1771     * Internal configuration lookup method.<p>
1772     *
1773     * @param cms the cms context
1774     * @param rootPath the root path for which to look up the configuration
1775     *
1776     * @return the configuration for the given path
1777     */
1778    protected CmsADEConfigData internalLookupConfiguration(CmsObject cms, String rootPath) {
1779
1780        boolean online = (null == cms) || isOnline(cms);
1781        CmsADEConfigCacheState state = getCacheState(online);
1782        return state.lookupConfiguration(rootPath);
1783    }
1784
1785    /**
1786     * Returns true if the project set in the CmsObject is the Online project.<p>
1787     *
1788     * @param cms the CMS context to check
1789     *
1790     * @return true if the project set in the CMS context is the Online project
1791     */
1792    private boolean isOnline(CmsObject cms) {
1793
1794        return cms.getRequestContext().getCurrentProject().isOnlineProject();
1795    }
1796
1797    /**
1798     * Saves an element list to the user additional infos.<p>
1799     *
1800     * @param cms the cms context
1801     * @param elementList the element list
1802     * @param listKey the list key
1803     *
1804     * @throws CmsException if something goes wrong
1805     */
1806    private void saveElementList(CmsObject cms, List<CmsContainerElementBean> elementList, String listKey)
1807    throws CmsException {
1808
1809        // limit the favorite list size to avoid the additional info size limit
1810        if (elementList.size() > DEFAULT_ELEMENT_LIST_SIZE) {
1811            elementList = elementList.subList(0, DEFAULT_ELEMENT_LIST_SIZE);
1812        }
1813
1814        JSONArray data = new JSONArray();
1815
1816        Set<String> excludedSettings = new HashSet<String>();
1817        // do not store the template contexts, since dragging an element into the page which might be invisible
1818        // doesn't make sense
1819        excludedSettings.add(CmsTemplateContextInfo.SETTING);
1820
1821        for (CmsContainerElementBean element : elementList) {
1822            data.put(elementToJson(element, excludedSettings));
1823        }
1824        CmsUser user = cms.getRequestContext().getCurrentUser();
1825        user.setAdditionalInfo(listKey, data.toString());
1826        cms.writeUser(user);
1827    }
1828}