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 org.apache.commons.logging.Log;
118
119import com.google.common.collect.Lists;
120import com.google.common.collect.Maps;
121import com.google.common.collect.Multimap;
122
123import jakarta.servlet.ServletRequest;
124import jakarta.servlet.http.HttpServletRequest;
125import jakarta.servlet.http.HttpServletResponse;
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    public Set<String> getParentFolderTypes(boolean online, String contentRootPath) {
876
877        return getCacheState(online).getParentFolderTypes(contentRootPath);
878    }
879
880    /**
881     * 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.
882     *
883     * <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.
884     * <p>If the given root path is not part of a site, the list of all containing subsite roots is returned.
885     *
886     * @param cms the current CMS context
887     * @param rootPath the root path for which to get the secret lookup paths
888     * @return
889     */
890    public List<String> getPathsForSecretLookup(CmsObject cms, String rootPath) {
891
892        CmsADEConfigCacheState state = getCacheState(cms.getRequestContext().getCurrentProject().isOnlineProject());
893        Set<String> siteConfigPaths = state.getSiteConfigurationPaths(); // has trailing slashes
894        String currentPath = rootPath;
895        currentPath = CmsFileUtil.addTrailingSeparator(currentPath);
896        String siteRoot = OpenCms.getSiteManager().getSiteRoot(rootPath);
897        if (siteRoot != null) {
898            siteRoot = CmsFileUtil.addTrailingSeparator(siteRoot);
899        }
900        List<String> result = new ArrayList<>();
901        while (currentPath != null) {
902            boolean isSiteRoot = currentPath.equals(siteRoot);
903            if (isSiteRoot || siteConfigPaths.contains(currentPath)) {
904                result.add(CmsFileUtil.removeTrailingSeparator(currentPath));
905            }
906            if (isSiteRoot) {
907                break;
908            }
909            currentPath = CmsResource.getParentFolder(currentPath); //  has trailing slash
910        }
911        return result;
912    }
913
914    /**
915     * Returns the permission info for the given resource.<p>
916     *
917     * @param cms the cms context
918     * @param resource the resource
919     * @param contextPath the context path
920     *
921     * @return the permission info
922     *
923     * @throws CmsException if checking the permissions fails
924     */
925    public CmsPermissionInfo getPermissionInfo(CmsObject cms, CmsResource resource, String contextPath)
926    throws CmsException {
927
928        boolean hasView = cms.hasPermissions(
929            resource,
930            CmsPermissionSet.ACCESS_VIEW,
931            false,
932            CmsResourceFilter.ALL.addRequireVisible());
933        boolean hasWrite = false;
934        if (hasView) {
935            try {
936                I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(resource.getTypeId());
937                CmsExplorerTypeSettings settings = OpenCms.getWorkplaceManager().getExplorerTypeSetting(
938                    type.getTypeName());
939                hasView = (settings == null)
940                    || settings.getAccess().getPermissions(cms, resource).requiresViewPermission();
941                if (hasView
942                    && CmsResourceTypeXmlContent.isXmlContent(resource)
943                    && !CmsResourceTypeXmlContainerPage.isContainerPage(resource)) {
944                    if (contextPath == null) {
945                        contextPath = resource.getRootPath();
946                    }
947                    CmsResourceTypeConfig localConfigData = lookupConfigurationWithCache(
948                        cms,
949                        contextPath).getResourceType(type.getTypeName());
950                    if (localConfigData != null) {
951                        Map<CmsUUID, CmsElementView> elementViews = getElementViews(cms);
952                        hasView = elementViews.containsKey(localConfigData.getElementView())
953                            && elementViews.get(localConfigData.getElementView()).hasPermission(cms, resource);
954                    }
955                }
956                // the user may only have write permissions if he is allowed to view the resource
957                hasWrite = hasView
958                    && cms.hasPermissions(
959                        resource,
960                        CmsPermissionSet.ACCESS_WRITE,
961                        false,
962                        CmsResourceFilter.IGNORE_EXPIRATION)
963                    && ((settings == null)
964                        || settings.getAccess().getPermissions(cms, resource).requiresWritePermission());
965            } catch (CmsLoaderException e) {
966                LOG.warn(e.getLocalizedMessage(), e);
967                hasWrite = false;
968            }
969        }
970
971        if (hasWrite && isEditorRestricted(cms, resource)) {
972            hasWrite = false;
973        }
974
975        String noEdit = new CmsResourceUtil(cms, resource).getNoEditReason(
976            OpenCms.getWorkplaceManager().getWorkplaceLocale(cms),
977            true);
978
979        boolean isFunction = false;
980        for (String type : new String[] {"function", CmsResourceTypeFunctionConfig.TYPE_NAME}) {
981            if (OpenCms.getResourceManager().matchResourceType(type, resource.getTypeId())) {
982                isFunction = true;
983                break;
984            }
985        }
986        if (isFunction) {
987            Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms);
988            noEdit = Messages.get().getBundle(locale).key(Messages.GUI_CANT_EDIT_FUNCTIONS_0);
989        }
990
991        return new CmsPermissionInfo(hasView, hasWrite, noEdit);
992    }
993
994    /**
995     * Gets a map of plugin wrappers for the given site path.
996     *
997     * <p>This *only* includes plugins defined in site plugins active on the given path, not those referenced in formatters.
998     *
999     * @param cms the CMS context
1000     * @param path the path for which to get the plugins
1001     *
1002     * @return the map of plugin wrappers, with the plugin groups as keys
1003     */
1004    public Map<String, List<CmsTemplatePluginWrapper>> getPluginsForPath(CmsObject cms, String path) {
1005
1006        CmsADEConfigData config = lookupConfigurationWithCache(cms, cms.getRequestContext().addSiteRoot(path));
1007
1008        Multimap<String, CmsTemplatePlugin> plugins = CmsTemplatePluginFinder.getActiveTemplatePluginsFromSitePlugins(
1009            config);
1010        Map<String, List<CmsTemplatePluginWrapper>> result = new HashMap<>();
1011        for (String key : plugins.keySet()) {
1012            List<CmsTemplatePluginWrapper> wrappers = plugins.get(key).stream().map(
1013                plugin -> new CmsTemplatePluginWrapper(cms, plugin)).collect(Collectors.toList());
1014            result.put(key, Collections.unmodifiableList(wrappers));
1015        }
1016        return Collections.unmodifiableMap(result);
1017
1018    }
1019
1020    /**
1021     * Gets the raw configured detail page information, with no existence checks or path correction.
1022     *
1023     * @param cms the CMS context
1024     * @return the list of raw detail page info beans
1025     */
1026    public List<CmsDetailPageInfo> getRawDetailPages(CmsObject cms) {
1027
1028        return getCache(cms.getRequestContext().getCurrentProject().isOnlineProject()).getRawDetailPages();
1029    }
1030
1031    /**
1032     * Returns the favorite list, or creates it if not available.<p>
1033     *
1034     * @param cms the cms context
1035     *
1036     * @return the favorite list
1037     *
1038     * @throws CmsException if something goes wrong
1039     */
1040    public List<CmsContainerElementBean> getRecentList(CmsObject cms) throws CmsException {
1041
1042        CmsUser user = cms.getRequestContext().getCurrentUser();
1043        Object obj = user.getAdditionalInfo(ADDINFO_ADE_RECENT_LIST);
1044
1045        List<CmsContainerElementBean> recentList = new ArrayList<CmsContainerElementBean>();
1046        if (obj instanceof String) {
1047            try {
1048                JSONArray array = new JSONArray((String)obj);
1049                for (int i = 0; i < array.length(); i++) {
1050                    try {
1051                        recentList.add(elementFromJson(array.getJSONObject(i)));
1052                    } catch (Throwable e) {
1053                        // should never happen, catches wrong or no longer existing values
1054                        LOG.warn(e.getLocalizedMessage());
1055                    }
1056                }
1057            } catch (Throwable e) {
1058                // should never happen, catches json parsing
1059                LOG.warn(e.getLocalizedMessage());
1060            }
1061        } else {
1062            // save to be better next time
1063            saveRecentList(cms, recentList);
1064        }
1065
1066        return recentList;
1067    }
1068
1069    /**
1070     * Gets a map of sitemap attribute values by sitemap path for a given attribute key.
1071     *
1072     * @param cms the current CMS context
1073     * @param attribute the sitemap attribute key
1074     *
1075     * @return the map of attribute values, with the root paths of the corresponding subsitemaps as keys
1076     */
1077    public Map<String, String> getSitemapAttributeValuesByPath(CmsObject cms, String attribute) {
1078
1079        boolean online = (null == cms) || isOnline(cms);
1080        CmsADEConfigCacheState state = getCacheState(online);
1081        return state.getAttributeValuesByPath(attribute);
1082    }
1083
1084    /**
1085     * Gets the sitemap configuration resource type.<p>
1086     *
1087     * @return the resource type for sitemap configurations
1088     */
1089    public I_CmsResourceType getSitemapConfigurationType() {
1090
1091        return m_configType;
1092    }
1093
1094    /**
1095     * Gets the registered sitemap extra info providers.
1096     *
1097     * @return the list of providers
1098     */
1099    public List<I_CmsSitemapExtraInfoProvider> getSitemapExtraInfoProviders() {
1100
1101        return m_sitemapExtraInfoProviders.stream().sorted(
1102            Comparator.comparing(provider -> provider.getOrder())).collect(Collectors.toList());
1103    }
1104
1105    /**
1106     * Returns all sub sites below the given path.<p>
1107     *
1108     * @param cms the cms context
1109     * @param subSiteRoot the sub site root path
1110     *
1111     * @return the sub site root paths
1112     */
1113    public List<String> getSubSitePaths(CmsObject cms, String subSiteRoot) {
1114
1115        List<String> result = new ArrayList<String>();
1116        String normalizedRootPath = CmsStringUtil.joinPaths("/", subSiteRoot, "/");
1117        CmsADEConfigCacheState state = getCacheState(isOnline(cms));
1118        Set<String> siteConfigurationPaths = state.getSiteConfigurationPaths();
1119        for (String path : siteConfigurationPaths) {
1120            if ((path.length() > normalizedRootPath.length()) && path.startsWith(normalizedRootPath)) {
1121                result.add(path);
1122            }
1123        }
1124        return result;
1125    }
1126
1127    /**
1128     * Tries to get the subsite root for a given resource root path.<p>
1129     *
1130     * @param cms the current CMS context
1131     * @param rootPath the root path for which the subsite root should be found
1132     *
1133     * @return the subsite root
1134     */
1135    public String getSubSiteRoot(CmsObject cms, String rootPath) {
1136
1137        CmsADEConfigData configData = lookupConfiguration(cms, rootPath);
1138        String basePath = configData.getBasePath();
1139        String siteRoot = OpenCms.getSiteManager().getSiteRoot(rootPath);
1140        if (siteRoot == null) {
1141            siteRoot = "";
1142        }
1143        if ((basePath == null) || !basePath.startsWith(siteRoot)) {
1144            // the subsite root should always be below the site root
1145            return siteRoot;
1146        } else {
1147            return basePath;
1148        }
1149    }
1150
1151    /**
1152     * Gets the subsites to be displayed in the site selector.
1153     *
1154     * @param online true if we want the subsites for the Online project
1155     *
1156     * @return the subsites to be displayed in the site selector
1157     */
1158    public List<String> getSubsitesForSiteSelector(boolean online) {
1159
1160        return getCacheState(online).getSubsitesForSiteSelector();
1161
1162    }
1163
1164    /**
1165     * Gets the table of upload warnings.
1166     *
1167     * @return the table of upload warnings
1168     */
1169    public CmsUploadWarningTable getUploadWarningTable() {
1170
1171        return m_uploadWarningTable;
1172    }
1173
1174    /**
1175     * Processes a HTML redirect content.<p>
1176     *
1177     * This needs to be in the ADE manager because the user for whom the HTML redirect is being loaded
1178     * does not necessarily have read permissions for the redirect target, so we read the redirect target
1179     * with admin privileges.<p>
1180     *
1181     * @param userCms the CMS context of the current user
1182     * @param request the servlet request
1183     * @param response the servlet response
1184     * @param htmlRedirect the path of the HTML redirect resource
1185     *
1186     * @throws CmsException if something goes wrong
1187     */
1188    public void handleHtmlRedirect(
1189        CmsObject userCms,
1190        HttpServletRequest request,
1191        HttpServletResponse response,
1192        String htmlRedirect)
1193    throws CmsException {
1194
1195        CmsObject cms = OpenCms.initCmsObject(m_offlineCms);
1196        CmsRequestContext userContext = userCms.getRequestContext();
1197        CmsRequestContext currentContext = cms.getRequestContext();
1198        currentContext.setCurrentProject(userContext.getCurrentProject());
1199        currentContext.setSiteRoot(userContext.getSiteRoot());
1200        currentContext.setLocale(userContext.getLocale());
1201        currentContext.setUri(userContext.getUri());
1202
1203        CmsFile file = cms.readFile(htmlRedirect);
1204        CmsXmlContent content = CmsXmlContentFactory.unmarshal(cms, file);
1205
1206        // find out the locale to use for reading values from the redirect
1207        List<Locale> candidates = new ArrayList<Locale>();
1208        candidates.add(currentContext.getLocale());
1209        candidates.add(CmsLocaleManager.getDefaultLocale());
1210        candidates.add(Locale.ENGLISH);
1211        candidates.addAll(content.getLocales());
1212        Locale contentLocale = currentContext.getLocale();
1213        for (Locale candidateLocale : candidates) {
1214            if (content.hasLocale(candidateLocale)) {
1215                contentLocale = candidateLocale;
1216                break;
1217            }
1218        }
1219
1220        String typeValue = content.getValue(N_TYPE, contentLocale).getStringValue(cms);
1221        String lnkUri = "";
1222        Integer errorCode;
1223        if ("sublevel".equals(typeValue)) {
1224            // use the nav builder to get the first sub level entry
1225            CmsJspNavBuilder navBuilder = new CmsJspNavBuilder(cms);
1226            if (navBuilder.getNavigationForFolder().size() > 0) {
1227                CmsJspNavElement target = navBuilder.getNavigationForFolder().get(0);
1228                lnkUri = CmsJspTagLink.linkTagAction(target.getResourceName(), request);
1229                errorCode = Integer.valueOf(HttpServletResponse.SC_MOVED_TEMPORARILY);
1230            } else {
1231                // send error 404 if no sub entry available
1232                errorCode = Integer.valueOf(HttpServletResponse.SC_NOT_FOUND);
1233            }
1234        } else {
1235            I_CmsXmlContentValue contentValue = content.getValue(N_LINK, contentLocale);
1236            if (contentValue != null) {
1237                String linkValue = contentValue.getStringValue(cms);
1238                lnkUri = OpenCms.getLinkManager().substituteLinkForUnknownTarget(cms, linkValue);
1239                try {
1240                    errorCode = Integer.valueOf(typeValue);
1241                } catch (NumberFormatException e) {
1242                    LOG.error(e.getMessage(), e);
1243                    // fall back to default
1244                    errorCode = Integer.valueOf(307);
1245                }
1246            } else {
1247                // send error 404 if no link value is set
1248                errorCode = Integer.valueOf(HttpServletResponse.SC_NOT_FOUND);
1249            }
1250        }
1251        if (!currentContext.getCurrentProject().isOnlineProject()) {
1252            // permanent redirects are confusing and not useful in the Offline project because they are stored
1253            // by the browser based on the host name, not the site the user is working in.
1254            if (errorCode.intValue() == HttpServletResponse.SC_MOVED_PERMANENTLY) {
1255                errorCode = Integer.valueOf(HttpServletResponse.SC_MOVED_TEMPORARILY);
1256            }
1257        }
1258        request.setAttribute(CmsRequestUtil.ATTRIBUTE_ERRORCODE, errorCode);
1259        response.setHeader("Location", CmsEncoder.convertHostToPunycode(lnkUri));
1260        response.setHeader("Connection", "close");
1261        response.setStatus(errorCode.intValue());
1262    }
1263
1264    /**
1265     * Initializes the configuration by reading all configuration files and caching their data.<p>
1266     */
1267    public synchronized void initialize() {
1268
1269        // no need to try initialization in case the 'org.opencms.base' is not present and the contained resource types missing
1270        if ((m_initStatus == Status.notInitialized) && OpenCms.getModuleManager().hasModule(MODULE_NAME_ADE_CONFIG)) {
1271            try {
1272                CmsLog.INIT.info(". Initializing the ADE configuration, this may take a while...");
1273                m_initStatus = Status.initializing;
1274                m_configType = OpenCms.getResourceManager().getResourceType(CONFIG_TYPE);
1275                m_moduleConfigType = OpenCms.getResourceManager().getResourceType(MODULE_CONFIG_TYPE);
1276                m_elementViewType = OpenCms.getResourceManager().getResourceType(ELEMENT_VIEW_TYPE);
1277                CmsProject temp = getTempfileProject(m_onlineCms);
1278                m_offlineCms = OpenCms.initCmsObject(m_onlineCms);
1279                m_offlineCms.getRequestContext().setCurrentProject(temp);
1280                m_onlineCache = new CmsConfigurationCache(
1281                    m_onlineCms,
1282                    m_configType,
1283                    m_moduleConfigType,
1284                    m_elementViewType);
1285                m_offlineCache = new CmsConfigurationCache(
1286                    m_offlineCms,
1287                    m_configType,
1288                    m_moduleConfigType,
1289                    m_elementViewType);
1290                CmsLog.INIT.info(". Reading online configuration...");
1291                m_onlineCache.initialize();
1292                CmsLog.INIT.info(". Reading offline configuration...");
1293                m_offlineCache.initialize();
1294                m_onlineContainerConfigurationCache = new CmsContainerConfigurationCache(
1295                    m_onlineCms,
1296                    "online inheritance groups");
1297                m_offlineContainerConfigurationCache = new CmsContainerConfigurationCache(
1298                    m_offlineCms,
1299                    "offline inheritance groups");
1300                CmsLog.INIT.info(". Reading online inherited container configurations...");
1301                m_onlineContainerConfigurationCache.initialize();
1302                CmsLog.INIT.info(". Reading offline inherited container configurations...");
1303                m_offlineContainerConfigurationCache.initialize();
1304                m_offlineFormatterCache = new CmsFormatterConfigurationCache(m_offlineCms, "offline formatters");
1305                m_onlineFormatterCache = new CmsFormatterConfigurationCache(m_onlineCms, "online formatters");
1306                CmsLog.INIT.info(". Reading online formatter configurations...");
1307                m_onlineFormatterCache.initialize();
1308                CmsLog.INIT.info(". Reading offline formatter configurations...");
1309                m_offlineFormatterCache.initialize();
1310
1311                m_offlineDetailIdCache = new CmsDetailNameCache(m_offlineCms);
1312                m_onlineDetailIdCache = new CmsDetailNameCache(m_onlineCms);
1313                CmsLog.INIT.info(". Initializing online detail name cache...");
1314                m_onlineDetailIdCache.initialize();
1315                CmsLog.INIT.info(". Initializing offline detail name cache...");
1316                m_offlineDetailIdCache.initialize();
1317
1318                CmsGlobalConfigurationCacheEventHandler handler = new CmsGlobalConfigurationCacheEventHandler(
1319                    m_onlineCms);
1320                handler.addCache(m_offlineCache, m_onlineCache, "ADE configuration cache");
1321                handler.addCache(
1322                    m_offlineContainerConfigurationCache,
1323                    m_onlineContainerConfigurationCache,
1324                    "Inherited container cache");
1325                handler.addCache(m_offlineFormatterCache, m_onlineFormatterCache, "formatter configuration cache");
1326                handler.addCache(m_offlineDetailIdCache, m_onlineDetailIdCache, "Detail ID cache");
1327                OpenCms.getEventManager().addCmsEventListener(handler);
1328                CmsLog.INIT.info(". Done initializing the ADE configuration.");
1329                m_initStatus = Status.initialized;
1330            } catch (CmsException e) {
1331                m_initStatus = Status.notInitialized;
1332                LOG.error(e.getLocalizedMessage(), e);
1333            }
1334            m_detailPageHandler.initialize(m_offlineCms, m_onlineCms);
1335        }
1336    }
1337
1338    /**
1339     * Checks whether the given resource is configured as a detail page.<p>
1340     *
1341     * @param cms the current CMS context
1342     * @param resource the resource which should be tested
1343     *
1344     * @return true if the resource is configured as a detail page
1345     */
1346    public boolean isDetailPage(CmsObject cms, CmsResource resource) {
1347
1348        return getCache(isOnline(cms)).isDetailPage(cms, resource);
1349    }
1350
1351    /**
1352     * Checks if the user should be prevented from editing a file.
1353     *
1354     * <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.
1355     *
1356     * @param cms the CMS context
1357     * @param res the resource to check
1358     * @return true if the user should be prevented from editing the file
1359     */
1360    public boolean isEditorRestricted(CmsObject cms, CmsResource res) {
1361
1362        if (OpenCms.getResourceManager().matchResourceType(CONFIG_TYPE, res.getTypeId())) {
1363            CmsRole role = getRoleForSitemapConfigEditing();
1364            if ((role != null) && !OpenCms.getRoleManager().hasRoleForResource(cms, role, res)) {
1365                return true;
1366            }
1367        }
1368        return false;
1369    }
1370
1371    /**
1372     * Checks if an element is reused in a page or group that is not excluded by a given predicate.
1373     *
1374     * @param resource the resource to check
1375     * @param exclude predicate used to ignore reuses which match it
1376     * @return true if the element is reused
1377     */
1378    public boolean isElementReused(CmsResource resource, Predicate<CmsResource> exclude) {
1379
1380        return getOfflineElementUses(resource).anyMatch(source -> !exclude.test(source));
1381    }
1382
1383    /**
1384     * Checks whether the ADE manager is initialized (this should usually be the case except during the setup).<p>
1385     *
1386     * @return true if the ADE manager is initialized
1387     */
1388    public boolean isInitialized() {
1389
1390        return m_initStatus == Status.initialized;
1391    }
1392
1393    /**
1394     * Returns the show editor help flag.<p>
1395     *
1396     * @param cms the cms context
1397     *
1398     * @return the show editor help flag
1399     */
1400    public boolean isShowEditorHelp(CmsObject cms) {
1401
1402        CmsUser user = cms.getRequestContext().getCurrentUser();
1403        String showHelp = (String)user.getAdditionalInfo(ADDINFO_ADE_SHOW_EDITOR_HELP);
1404        return CmsStringUtil.isEmptyOrWhitespaceOnly(showHelp) || Boolean.parseBoolean(showHelp);
1405    }
1406
1407    /**
1408     * Looks up the configuration data for a given sitemap path.<p>
1409     *
1410     * @param cms the current CMS context
1411     * @param rootPath the root path for which the configuration data should be looked up
1412     *
1413     * @return the configuration data
1414     */
1415    public CmsADEConfigData lookupConfiguration(CmsObject cms, String rootPath) {
1416
1417        CmsADEConfigData configData = internalLookupConfiguration(cms, rootPath);
1418        return configData;
1419    }
1420
1421    /**
1422     * Looks up the configuration data for a given sitemap path, but uses a thread-local cache for the current request for efficiency.
1423     *
1424     * @param cms the current CMS context
1425     * @param rootPath the root path for which the configuration data should be looked up
1426     *
1427     * @return the configuration data
1428     */
1429    public CmsADEConfigData lookupConfigurationWithCache(CmsObject cms, String rootPath) {
1430
1431        boolean online = (cms == null) || cms.getRequestContext().getCurrentProject().isOnlineProject();
1432        String cacheKey = "" + online + ":" + rootPath;
1433        OpenCmsServlet.RequestCache context = OpenCmsServlet.getRequestCache();
1434        CmsADEConfigData result = null;
1435        if (context != null) {
1436            result = context.getCachedConfig(cacheKey);
1437        }
1438        if (result == null) {
1439            result = internalLookupConfiguration(cms, rootPath);
1440            if (context != null) {
1441                context.setCachedConfig(cacheKey, result);
1442            }
1443        }
1444        return result;
1445    }
1446
1447    /**
1448     * Reloads the configuration.<p>
1449     *
1450     * Normally you shouldn't call this directly since the event handlers take care of updating the configuration.
1451     */
1452    public void refresh() {
1453
1454        m_onlineCache.initialize();
1455        m_offlineCache.initialize();
1456    }
1457
1458    public void removeSitemapExtraInfoProvider(I_CmsSitemapExtraInfoProvider provider) {
1459
1460        m_sitemapExtraInfoProviders.remove(provider);
1461    }
1462
1463    /**
1464     * Saves a list of detail pages.<p>
1465     * @param cms the cms context
1466     * @param rootPath the root path
1467     * @param detailPages the detail pages
1468     * @param newId the id to use for new detail pages without an id
1469     * @return true if the detail pages could be successfully saved
1470     *
1471     * @throws CmsException if something goes wrong
1472     */
1473    public boolean saveDetailPages(CmsObject cms, String rootPath, List<CmsDetailPageInfo> detailPages, CmsUUID newId)
1474    throws CmsException {
1475
1476        CmsADEConfigData configData = lookupConfiguration(cms, rootPath);
1477        CmsDetailPageConfigurationWriter configWriter;
1478        String originalSiteRoot = cms.getRequestContext().getSiteRoot();
1479        try {
1480            cms.getRequestContext().setSiteRoot("");
1481            if (configData.isModuleConfiguration()) {
1482                return false;
1483            }
1484            CmsResource configFile = configData.getResource();
1485            configWriter = new CmsDetailPageConfigurationWriter(cms, configFile);
1486            configWriter.updateAndSave(detailPages, newId);
1487            return true;
1488        } finally {
1489            cms.getRequestContext().setSiteRoot(originalSiteRoot);
1490        }
1491    }
1492
1493    /**
1494     * Saves the favorite list, user based.<p>
1495     *
1496     * @param cms the cms context
1497     * @param favoriteList the element list
1498     *
1499     * @throws CmsException if something goes wrong
1500     */
1501    public void saveFavoriteList(CmsObject cms, List<CmsContainerElementBean> favoriteList) throws CmsException {
1502
1503        saveElementList(cms, favoriteList, ADDINFO_ADE_FAVORITE_LIST);
1504    }
1505
1506    /**
1507     * Saves the inheritance container information.<p>
1508     *
1509     * @param cms the current cms context
1510     * @param pageResource the resource or parent folder
1511     * @param name the inheritance name
1512     * @param newOrder if the element have been reordered
1513     * @param elements the elements
1514     *
1515     * @throws CmsException if something goes wrong
1516     */
1517    public void saveInheritedContainer(
1518        CmsObject cms,
1519        CmsResource pageResource,
1520        String name,
1521        boolean newOrder,
1522        List<CmsContainerElementBean> elements)
1523    throws CmsException {
1524
1525        CmsContainerConfigurationWriter writer = new CmsContainerConfigurationWriter();
1526        writer.save(cms, name, newOrder, pageResource, elements);
1527
1528        // Inheritance groups are usually reloaded directly after saving them,
1529        // so the cache needs to be up to date after this method is called
1530        m_offlineContainerConfigurationCache.flushUpdates();
1531    }
1532
1533    /**
1534     * Saves the inheritance container information.<p>
1535     *
1536     * @param cms the current cms context
1537     * @param sitePath the site path of the resource or parent folder
1538     * @param name the inheritance name
1539     * @param newOrder if the element have been reordered
1540     * @param elements the elements
1541     *
1542     * @throws CmsException if something goes wrong
1543     */
1544    public void saveInheritedContainer(
1545        CmsObject cms,
1546        String sitePath,
1547        String name,
1548        boolean newOrder,
1549        List<CmsContainerElementBean> elements)
1550    throws CmsException {
1551
1552        saveInheritedContainer(cms, cms.readResource(sitePath), name, newOrder, elements);
1553    }
1554
1555    /**
1556     * Saves the favorite list, user based.<p>
1557     *
1558     * @param cms the cms context
1559     * @param recentList the element list
1560     *
1561     * @throws CmsException if something goes wrong
1562     */
1563    public void saveRecentList(CmsObject cms, List<CmsContainerElementBean> recentList) throws CmsException {
1564
1565        saveElementList(cms, recentList, ADDINFO_ADE_RECENT_LIST);
1566    }
1567
1568    /**
1569     * Sets the show editor help flag.<p>
1570     *
1571     * @param cms the cms context
1572     * @param showHelp the show help flag
1573     * @throws CmsException if writing the user info fails
1574     */
1575    public void setShowEditorHelp(CmsObject cms, boolean showHelp) throws CmsException {
1576
1577        CmsUser user = cms.getRequestContext().getCurrentUser();
1578        user.setAdditionalInfo(ADDINFO_ADE_SHOW_EDITOR_HELP, String.valueOf(showHelp));
1579        cms.writeUser(user);
1580    }
1581
1582    /**
1583     * The method which is called when the OpenCms instance is shut down.<p>
1584     */
1585    public void shutdown() {
1586
1587        // do nothing
1588    }
1589
1590    /**
1591     * Waits until the next time the cache is updated.<p>
1592     *
1593     * @param online true if we want to wait for the online cache, false for the offline cache
1594     */
1595    public void waitForCacheUpdate(boolean online) {
1596
1597        getCache(online).getWaitHandleForUpdateTask().enter(2 * CmsConfigurationCache.TASK_DELAY_MILLIS);
1598    }
1599
1600    /**
1601     * Waits until the formatter cache has finished updating itself.<p>
1602     *
1603     * This method is only intended for use in test cases.
1604     *
1605     * @param online true if we should wait for the online formatter cache,false for the offline cache
1606     */
1607    public void waitForFormatterCache(boolean online) {
1608
1609        CmsFormatterConfigurationCache cache = online ? m_onlineFormatterCache : m_offlineFormatterCache;
1610        cache.waitForUpdate();
1611    }
1612
1613    /**
1614     * Creates an element from its serialized data.<p>
1615     *
1616     * @param data the serialized data
1617     *
1618     * @return the restored element bean
1619     *
1620     * @throws JSONException if the serialized data got corrupted
1621     */
1622    protected CmsContainerElementBean elementFromJson(JSONObject data) throws JSONException {
1623
1624        CmsUUID element = new CmsUUID(data.getString(FavListProp.ELEMENT.name().toLowerCase()));
1625        CmsUUID formatter = null;
1626        if (data.has(FavListProp.FORMATTER.name().toLowerCase())) {
1627            formatter = new CmsUUID(data.getString(FavListProp.FORMATTER.name().toLowerCase()));
1628        }
1629        Map<String, String> properties = new HashMap<String, String>();
1630
1631        JSONObject props = data.getJSONObject(FavListProp.PROPERTIES.name().toLowerCase());
1632        Iterator<String> keys = props.keys();
1633        while (keys.hasNext()) {
1634            String key = keys.next();
1635            properties.put(key, props.getString(key));
1636        }
1637
1638        return new CmsContainerElementBean(element, formatter, properties, false);
1639    }
1640
1641    /**
1642     * Converts the given element to JSON.<p>
1643     *
1644     * @param element the element to convert
1645     * @param excludeSettings the keys of settings which should not be written to the JSON
1646     *
1647     * @return the JSON representation
1648     */
1649    protected JSONObject elementToJson(CmsContainerElementBean element, Set<String> excludeSettings) {
1650
1651        JSONObject data = null;
1652        try {
1653            data = new JSONObject();
1654            data.put(FavListProp.ELEMENT.name().toLowerCase(), element.getId().toString());
1655            if (element.getFormatterId() != null) {
1656                data.put(FavListProp.FORMATTER.name().toLowerCase(), element.getFormatterId().toString());
1657            }
1658            JSONObject properties = new JSONObject();
1659            for (Map.Entry<String, String> entry : element.getIndividualSettings().entrySet()) {
1660                String settingKey = entry.getKey();
1661                if (!excludeSettings.contains(settingKey)) {
1662                    properties.put(entry.getKey(), entry.getValue());
1663                }
1664            }
1665            data.put(FavListProp.PROPERTIES.name().toLowerCase(), properties);
1666        } catch (JSONException e) {
1667            // should never happen
1668            if (!LOG.isDebugEnabled()) {
1669                LOG.warn(e.getLocalizedMessage());
1670            }
1671            LOG.debug(e.getLocalizedMessage(), e);
1672            return null;
1673        }
1674        return data;
1675    }
1676
1677    /**
1678     * Gets the configuration cache instance.<p>
1679     *
1680     * @param online true if you want the online cache, false for the offline cache
1681     *
1682     * @return the ADE configuration cache instance
1683     */
1684    protected CmsConfigurationCache getCache(boolean online) {
1685
1686        return online ? m_onlineCache : m_offlineCache;
1687    }
1688
1689    /**
1690     * Gets the offline cache.<p>
1691     *
1692     * @return the offline configuration cache
1693     */
1694    protected CmsConfigurationCache getOfflineCache() {
1695
1696        return m_offlineCache;
1697    }
1698
1699    /**
1700     * Gets the online cache.<p>
1701     *
1702     * @return the online configuration cache
1703     */
1704    protected CmsConfigurationCache getOnlineCache() {
1705
1706        return m_onlineCache;
1707    }
1708
1709    /**
1710     * Gets the role necessary to edit sitemap configuration files.
1711     *
1712     * @return the role needed for editing sitemap configurations
1713     */
1714    protected CmsRole getRoleForSitemapConfigEditing() {
1715
1716        String roleName = OpenCms.getWorkplaceManager().getSitemapConfigEditRole();
1717        if (roleName == null) {
1718            return null;
1719        } else {
1720            if (roleName.indexOf("/") == -1) {
1721                return CmsRole.valueOfRoleName(roleName).forOrgUnit(null);
1722            } else {
1723                return CmsRole.valueOfRoleName(roleName);
1724            }
1725        }
1726    }
1727
1728    /**
1729     * Gets the root path for a given resource structure id.<p>
1730     *
1731     * @param structureId the structure id
1732     * @param online if true, the resource will be looked up in the online project ,else in the offline project
1733     *
1734     * @return the root path for the given structure id
1735     *
1736     * @throws CmsException if something goes wrong
1737     */
1738    protected String getRootPath(CmsUUID structureId, boolean online) throws CmsException {
1739
1740        CmsConfigurationCache cache = online ? m_onlineCache : m_offlineCache;
1741        return cache.getPathForStructureId(structureId);
1742    }
1743
1744    /**
1745     * Gets a tempfile project, creating one if it doesn't exist already.<p>
1746     *
1747     * @param cms the CMS context to use
1748     * @return the tempfile project
1749     *
1750     * @throws CmsException if something goes wrong
1751     */
1752    protected CmsProject getTempfileProject(CmsObject cms) throws CmsException {
1753
1754        try {
1755            return cms.readProject(I_CmsProjectDriver.TEMP_FILE_PROJECT_NAME);
1756        } catch (CmsException e) {
1757            return cms.createTempfileProject();
1758        }
1759    }
1760
1761    /**
1762     * Internal configuration lookup method.<p>
1763     *
1764     * @param cms the cms context
1765     * @param rootPath the root path for which to look up the configuration
1766     *
1767     * @return the configuration for the given path
1768     */
1769    protected CmsADEConfigData internalLookupConfiguration(CmsObject cms, String rootPath) {
1770
1771        boolean online = (null == cms) || isOnline(cms);
1772        CmsADEConfigCacheState state = getCacheState(online);
1773        return state.lookupConfiguration(rootPath);
1774    }
1775
1776    /**
1777     * Returns true if the project set in the CmsObject is the Online project.<p>
1778     *
1779     * @param cms the CMS context to check
1780     *
1781     * @return true if the project set in the CMS context is the Online project
1782     */
1783    private boolean isOnline(CmsObject cms) {
1784
1785        return cms.getRequestContext().getCurrentProject().isOnlineProject();
1786    }
1787
1788    /**
1789     * Saves an element list to the user additional infos.<p>
1790     *
1791     * @param cms the cms context
1792     * @param elementList the element list
1793     * @param listKey the list key
1794     *
1795     * @throws CmsException if something goes wrong
1796     */
1797    private void saveElementList(CmsObject cms, List<CmsContainerElementBean> elementList, String listKey)
1798    throws CmsException {
1799
1800        // limit the favorite list size to avoid the additional info size limit
1801        if (elementList.size() > DEFAULT_ELEMENT_LIST_SIZE) {
1802            elementList = elementList.subList(0, DEFAULT_ELEMENT_LIST_SIZE);
1803        }
1804
1805        JSONArray data = new JSONArray();
1806
1807        Set<String> excludedSettings = new HashSet<String>();
1808        // do not store the template contexts, since dragging an element into the page which might be invisible
1809        // doesn't make sense
1810        excludedSettings.add(CmsTemplateContextInfo.SETTING);
1811
1812        for (CmsContainerElementBean element : elementList) {
1813            data.put(elementToJson(element, excludedSettings));
1814        }
1815        CmsUser user = cms.getRequestContext().getCurrentUser();
1816        user.setAdditionalInfo(listKey, data.toString());
1817        cms.writeUser(user);
1818    }
1819}