<commit_before>/*
 * This library is part of OpenCms -
 * the Open Source Content Management System
 *
 * Copyright (C) Alkacon Software (http://www.alkacon.com)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * For further information about Alkacon Software, please see the
 * company website: http://www.alkacon.com
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.opencms.ade.configuration;

import org.opencms.ade.configuration.CmsADEConfigDataInternal.AttributeValue;
import org.opencms.ade.configuration.formatters.CmsFormatterBeanParser;
import org.opencms.ade.configuration.formatters.CmsFormatterChangeSet;
import org.opencms.ade.configuration.formatters.CmsFormatterConfigurationCacheState;
import org.opencms.ade.containerpage.shared.CmsContainer;
import org.opencms.ade.containerpage.shared.CmsFormatterConfig;
import org.opencms.ade.detailpage.CmsDetailPageInfo;
import org.opencms.file.CmsObject;
import org.opencms.file.CmsResource;
import org.opencms.file.CmsResourceFilter;
import org.opencms.file.types.CmsResourceTypeFolder;
import org.opencms.file.types.CmsResourceTypeFunctionConfig;
import org.opencms.file.types.CmsResourceTypeXmlContent;
import org.opencms.file.types.I_CmsResourceType;
import org.opencms.gwt.CmsIconUtil;
import org.opencms.jsp.util.CmsFunctionRenderer;
import org.opencms.loader.CmsLoaderException;
import org.opencms.main.CmsException;
import org.opencms.main.CmsLog;
import org.opencms.main.OpenCms;
import org.opencms.main.OpenCmsServlet;
import org.opencms.util.CmsStringUtil;
import org.opencms.util.CmsUUID;
import org.opencms.workplace.editors.directedit.CmsAdvancedDirectEditProvider.SitemapDirectEditPermissions;
import org.opencms.xml.CmsXmlContentDefinition;
import org.opencms.xml.containerpage.CmsFormatterConfiguration;
import org.opencms.xml.containerpage.CmsXmlDynamicFunctionHandler;
import org.opencms.xml.containerpage.I_CmsFormatterBean;
import org.opencms.xml.content.CmsXmlContentFactory;
import org.opencms.xml.content.CmsXmlContentProperty;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.logging.Log;

import com.google.common.base.Optional;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;

/**
 * A class which represents the accessible configuration data at a given point in a sitemap.<p>
 */
public class CmsADEConfigData {

    /**
     * Bean which contains the detail information for a single sub-sitemap and resource type.<p>
     *
     * This includes both  information about the detail page itself, as well as the path of the folder
     * which is used to store that content type in this subsitemap.<p>
     *
     */
    public class DetailInfo {

        /** The base path of the sitemap configuration where this information originates from. */
        private String m_basePath;

        /** The information about the detail page info itself. */
        private CmsDetailPageInfo m_detailPageInfo;

        /** The content folder path. */
        private String m_folderPath;

        /** The detail type. */
        private String m_type;

        /**
         * Creates a new instance.<p>
         *
         * @param folderPath the content folder path
         * @param detailPageInfo the detail page information
         * @param type the detail type
         * @param basePath the base path of the sitemap configuration
         */
        public DetailInfo(String folderPath, CmsDetailPageInfo detailPageInfo, String type, String basePath) {

            m_folderPath = folderPath;
            m_detailPageInfo = detailPageInfo;
            m_type = type;
            m_basePath = basePath;

        }

        /**
         * Gets the base path of the sitemap configuration from which this information is coming.<p>
         *
         * @return the base path
         */
        public String getBasePath() {

            return m_basePath;
        }

        /**
         * Gets the detail page information.<p>
         *
         * @return the detail page information
         */
        public CmsDetailPageInfo getDetailPageInfo() {

            return m_detailPageInfo;
        }

        /**
         * Gets the content folder path.<p>
         *
         * @return the content folder path
         */
        public String getFolderPath() {

            return m_folderPath;
        }

        /**
         * Gets the detail type.<p>
         *
         * @return the detail type
         */
        public String getType() {

            return m_type;
        }

        /**
         * Sets the base path.<p>
         *
         * @param basePath the new base path value
         */
        public void setBasePath(String basePath) {

            m_basePath = basePath;
        }

        /**
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {

            return ReflectionToStringBuilder.toString(this);
        }
    }

    public static final String REQ_LOG_PREFIX = "[CmsADEConfigData] ";

    public static final String REQUEST_LOG_CHANNEL = "org.opencms.ade.configuration.CmsADEConfigData.request";

    /** The log instance for this class. */
    private static final Log LOG = CmsLog.getLog(CmsADEConfigData.class);

    /** The wrapped configuration bean containing the actual data. */
    protected CmsADEConfigDataInternal m_data;

    /** Lazily initialized map of formatters. */
    private Map<CmsUUID, I_CmsFormatterBean> m_activeFormatters;

    /** Lazily initialized cache for active formatters by formatter key. */
    private Multimap<String, I_CmsFormatterBean> m_activeFormattersByKey;

    /** The sitemap attributes (may be null if not yet computed). */
    private Map<String, AttributeValue> m_attributes;

    /** The cache state to which the wrapped configuration bean belongs. */
    private CmsADEConfigCacheState m_cache;

    /** Current formatter configuration. */
    private CmsFormatterConfigurationCacheState m_cachedFormatters;

    /** The configuration sequence (contains the list of all sitemap configuration data beans to be used for inheritance). */
    private CmsADEConfigurationSequence m_configSequence;

    /** Lazily initialized cache for formatters by JSP id. */
    private Multimap<CmsUUID, I_CmsFormatterBean> m_formattersByJspId;

    /** Lazily initialized cache for formatters by formatter key. */
    private Multimap<String, I_CmsFormatterBean> m_formattersByKey;

    /** Set of names of active types.*/
    private Set<String> m_typesActive;

    /** Type names configured in this or ancestor sitemap configurations. */
    private Set<String> m_typesInAncestors;

    /**
     * Creates a new configuration data object, based on an internal configuration data bean and a
     * configuration cache state.<p>
     *
     * @param data the internal configuration data bean
     * @param cache the configuration cache state
     * @param configSequence the configuration sequence
     */
    public CmsADEConfigData(
        CmsADEConfigDataInternal data,
        CmsADEConfigCacheState cache,
        CmsADEConfigurationSequence configSequence) {

        m_data = data;
        m_cache = cache;
        m_configSequence = configSequence;
    }

    /**
     * Generic method to merge lists of named configuration objects.<p>
     *
     * The lists are merged such that the configuration objects from the child list rise to the front of the result list,
     * and two configuration objects will be merged themselves if they share the same name.<p>
     *
     * For example, if we have two lists of configuration objects:<p>
     *
     * parent: A1, B1, C1<p>
     * child: D2, B2<p>
     *
     * then the resulting list will look like:<p>
     *
     * D2, B3, A1, C1<p>
     *
     * where B3 is the result of merging B1 and B2.<p>
     *
     * @param <C> the type of configuration object
     * @param parentConfigs the parent configurations
     * @param childConfigs the child configurations
     * @param preserveDisabled if true, try to merge parents with disabled children instead of discarding them
     *
     * @return the merged configuration object list
     */
    public static <C extends I_CmsConfigurationObject<C>> List<C> combineConfigurationElements(
        List<C> parentConfigs,
        List<C> childConfigs,
        boolean preserveDisabled) {

        List<C> result = new ArrayList<C>();
        Map<String, C> map = new LinkedHashMap<String, C>();
        if (parentConfigs != null) {
            for (C parent : Lists.reverse(parentConfigs)) {
                map.put(parent.getKey(), parent);
            }
        }
        if (childConfigs == null) {
            childConfigs = Collections.emptyList();
        }
        for (C child : Lists.reverse(childConfigs)) {
            String childKey = child.getKey();
            if (child.isDisabled() && !preserveDisabled) {
                map.remove(childKey);
            } else {
                C parent = map.get(childKey);
                map.remove(childKey);
                C newValue;
                if (parent != null) {
                    newValue = parent.merge(child);
                } else {
                    newValue = child;
                }
                map.put(childKey, newValue);
            }
        }
        result = new ArrayList<C>(map.values());
        Collections.reverse(result);
        // those multiple "reverse" calls may a bit confusing. They are there because on the one hand we want to keep the
        // configuration items from one configuration in the same order as they are defined, on the other hand we want
        // configuration items from a child configuration to rise to the top of the configuration items.

        // so for example, if the parent configuration has items with the keys A,B,C,E
        // and the child configuration has items  with the keys C,B,D
        // we want the items of the combined configuration in the order C,B,D,A,E

        return result;
    }

    /**
     * Applies the formatter change sets of this and all parent configurations to a map of external (non-schema) formatters.<p>
     *
     * @param formatters the external formatter map which will be modified
     *
     * @param formatterCacheState the formatter cache state from which new external formatters should be fetched
     */
    public void applyAllFormatterChanges(
        Map<CmsUUID, I_CmsFormatterBean> formatters,
        CmsFormatterConfigurationCacheState formatterCacheState) {

        for (CmsFormatterChangeSet changeSet : getFormatterChangeSets()) {
            changeSet.applyToFormatters(formatters, formatterCacheState);
        }
    }

    /**
     * Gets the 'best' formatter for the given ID.<p>
     *
     * If the formatter with the ID has a key, then the active formatter with the same key is returned.  Otherwise, the
     * formatter matching the ID is returned. So being active and having the same key is prioritized over an exact ID match.
     *
     * @param id the formatter ID
     * @return the best formatter the given ID
     */
    public I_CmsFormatterBean findFormatter(CmsUUID id) {

        if (id == null) {
            return null;
        }

        CmsFormatterConfigurationCacheState formatterState = getCachedFormatters();
        I_CmsFormatterBean result = formatterState.getFormatters().get(id);
        if ((result != null) && (result.getKey() != null)) {
            String key = result.getKey();
            Collection<I_CmsFormatterBean> activeFormattersForKey = getActiveFormattersByKey().get(key);
            if (activeFormattersForKey.size() > 0) {
                if (activeFormattersForKey.size() > 1) {
                    String labels = ""
                        + activeFormattersForKey.stream().map(this::getFormatterLabel).collect(Collectors.toList());
                    String message = "Ambiguous formatter for key '"
                        + key
                        + "' at '"
                        + getBasePath()
                        + "': found "
                        + labels;
                    LOG.warn(message);
                    OpenCmsServlet.withRequestCache(
                        reqCache -> reqCache.addLog(REQUEST_LOG_CHANNEL, "warn", REQ_LOG_PREFIX + message));
                }
                I_CmsFormatterBean original = result;
                result = activeFormattersForKey.iterator().next();
                String message = "Using substitute formatter "
                    + getFormatterLabel(result)
                    + " instead of "
                    + getFormatterLabel(original)
                    + " because of matching key.";
                LOG.debug(message);
                OpenCmsServlet.withRequestCache(
                    reqCache -> reqCache.addLog(REQUEST_LOG_CHANNEL, "debug", REQ_LOG_PREFIX + message));
            }
        }
        return result;
    }

    /**
     * Gets the 'best' formatter for the given name.<p>
     *
     * The name can be either a formatter key, or a formatter UUID. If it's a key, an active formatter with that key is returned.
     * If it's a UUID, and the formatter with that UUID has no key, it will be returned. If it does have a key, the active formatter
     * with that key is returned (so being active and having the same key is prioritized over an exact ID match).
     *
     * @param name a formatter name (key or ID)
     * @return the best formatter for that name, or null if no formatter could be found
     */
    public I_CmsFormatterBean findFormatter(String name) {

        if (name == null) {
            return null;
        }

        if (CmsUUID.isValidUUID(name)) {
            return findFormatter(new CmsUUID(name));
        }

        if (name.startsWith(CmsFormatterConfig.SCHEMA_FORMATTER_ID)) {
            return null;
        }

        Collection<I_CmsFormatterBean> activeForKey = getActiveFormattersByKey().get(name);
        if (activeForKey.size() > 0) {
            if (activeForKey.size() > 1) {
                String labels = "" + activeForKey.stream().map(this::getFormatterLabel).collect(Collectors.toList());
                String message = "Ambiguous formatter for key '"
                    + name
                    + "' at '"
                    + getBasePath()
                    + "': found "
                    + labels;
                LOG.warn(message);
                OpenCmsServlet.withRequestCache(rc -> rc.addLog(REQUEST_LOG_CHANNEL, "warn", REQ_LOG_PREFIX + message));
            }<commit_msg>Removed more 'false alarm' log messages in findFormatter().<commit_after>/*
 * This library is part of OpenCms -
 * the Open Source Content Management System
 *
 * Copyright (C) Alkacon Software (http://www.alkacon.com)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * For further information about Alkacon Software, please see the
 * company website: http://www.alkacon.com
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.opencms.ade.configuration;

import org.opencms.ade.configuration.CmsADEConfigDataInternal.AttributeValue;
import org.opencms.ade.configuration.formatters.CmsFormatterBeanParser;
import org.opencms.ade.configuration.formatters.CmsFormatterChangeSet;
import org.opencms.ade.configuration.formatters.CmsFormatterConfigurationCacheState;
import org.opencms.ade.containerpage.shared.CmsContainer;
import org.opencms.ade.containerpage.shared.CmsContainerElement;
import org.opencms.ade.containerpage.shared.CmsFormatterConfig;
import org.opencms.ade.detailpage.CmsDetailPageInfo;
import org.opencms.file.CmsObject;
import org.opencms.file.CmsResource;
import org.opencms.file.CmsResourceFilter;
import org.opencms.file.types.CmsResourceTypeFolder;
import org.opencms.file.types.CmsResourceTypeFunctionConfig;
import org.opencms.file.types.CmsResourceTypeXmlContent;
import org.opencms.file.types.I_CmsResourceType;
import org.opencms.gwt.CmsIconUtil;
import org.opencms.jsp.util.CmsFunctionRenderer;
import org.opencms.loader.CmsLoaderException;
import org.opencms.main.CmsException;
import org.opencms.main.CmsLog;
import org.opencms.main.OpenCms;
import org.opencms.main.OpenCmsServlet;
import org.opencms.util.CmsStringUtil;
import org.opencms.util.CmsUUID;
import org.opencms.workplace.editors.directedit.CmsAdvancedDirectEditProvider.SitemapDirectEditPermissions;
import org.opencms.xml.CmsXmlContentDefinition;
import org.opencms.xml.containerpage.CmsFormatterConfiguration;
import org.opencms.xml.containerpage.CmsXmlDynamicFunctionHandler;
import org.opencms.xml.containerpage.I_CmsFormatterBean;
import org.opencms.xml.content.CmsXmlContentFactory;
import org.opencms.xml.content.CmsXmlContentProperty;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.logging.Log;

import com.google.common.base.Optional;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;

/**
 * A class which represents the accessible configuration data at a given point in a sitemap.<p>
 */
public class CmsADEConfigData {

    /**
     * Bean which contains the detail information for a single sub-sitemap and resource type.<p>
     *
     * This includes both  information about the detail page itself, as well as the path of the folder
     * which is used to store that content type in this subsitemap.<p>
     *
     */
    public class DetailInfo {

        /** The base path of the sitemap configuration where this information originates from. */
        private String m_basePath;

        /** The information about the detail page info itself. */
        private CmsDetailPageInfo m_detailPageInfo;

        /** The content folder path. */
        private String m_folderPath;

        /** The detail type. */
        private String m_type;

        /**
         * Creates a new instance.<p>
         *
         * @param folderPath the content folder path
         * @param detailPageInfo the detail page information
         * @param type the detail type
         * @param basePath the base path of the sitemap configuration
         */
        public DetailInfo(String folderPath, CmsDetailPageInfo detailPageInfo, String type, String basePath) {

            m_folderPath = folderPath;
            m_detailPageInfo = detailPageInfo;
            m_type = type;
            m_basePath = basePath;

        }

        /**
         * Gets the base path of the sitemap configuration from which this information is coming.<p>
         *
         * @return the base path
         */
        public String getBasePath() {

            return m_basePath;
        }

        /**
         * Gets the detail page information.<p>
         *
         * @return the detail page information
         */
        public CmsDetailPageInfo getDetailPageInfo() {

            return m_detailPageInfo;
        }

        /**
         * Gets the content folder path.<p>
         *
         * @return the content folder path
         */
        public String getFolderPath() {

            return m_folderPath;
        }

        /**
         * Gets the detail type.<p>
         *
         * @return the detail type
         */
        public String getType() {

            return m_type;
        }

        /**
         * Sets the base path.<p>
         *
         * @param basePath the new base path value
         */
        public void setBasePath(String basePath) {

            m_basePath = basePath;
        }

        /**
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {

            return ReflectionToStringBuilder.toString(this);
        }
    }

    public static final String REQ_LOG_PREFIX = "[CmsADEConfigData] ";

    public static final String REQUEST_LOG_CHANNEL = "org.opencms.ade.configuration.CmsADEConfigData.request";

    /** The log instance for this class. */
    private static final Log LOG = CmsLog.getLog(CmsADEConfigData.class);

    /** Prefixes for internal settings which might be passed as formatter keys to findFormatter(). */
    private static final HashSet<String> systemSettingPrefixes = new HashSet<>(
        Arrays.asList("element", "model", "source", "use", "cms", "is"));

    /** The wrapped configuration bean containing the actual data. */
    protected CmsADEConfigDataInternal m_data;

    /** Lazily initialized map of formatters. */
    private Map<CmsUUID, I_CmsFormatterBean> m_activeFormatters;

    /** Lazily initialized cache for active formatters by formatter key. */
    private Multimap<String, I_CmsFormatterBean> m_activeFormattersByKey;

    /** The sitemap attributes (may be null if not yet computed). */
    private Map<String, AttributeValue> m_attributes;

    /** The cache state to which the wrapped configuration bean belongs. */
    private CmsADEConfigCacheState m_cache;

    /** Current formatter configuration. */
    private CmsFormatterConfigurationCacheState m_cachedFormatters;

    /** The configuration sequence (contains the list of all sitemap configuration data beans to be used for inheritance). */
    private CmsADEConfigurationSequence m_configSequence;

    /** Lazily initialized cache for formatters by JSP id. */
    private Multimap<CmsUUID, I_CmsFormatterBean> m_formattersByJspId;

    /** Lazily initialized cache for formatters by formatter key. */
    private Multimap<String, I_CmsFormatterBean> m_formattersByKey;

    /** Set of names of active types.*/
    private Set<String> m_typesActive;

    /** Type names configured in this or ancestor sitemap configurations. */
    private Set<String> m_typesInAncestors;

    /**
     * Creates a new configuration data object, based on an internal configuration data bean and a
     * configuration cache state.<p>
     *
     * @param data the internal configuration data bean
     * @param cache the configuration cache state
     * @param configSequence the configuration sequence
     */
    public CmsADEConfigData(
        CmsADEConfigDataInternal data,
        CmsADEConfigCacheState cache,
        CmsADEConfigurationSequence configSequence) {

        m_data = data;
        m_cache = cache;
        m_configSequence = configSequence;
    }

    /**
     * Generic method to merge lists of named configuration objects.<p>
     *
     * The lists are merged such that the configuration objects from the child list rise to the front of the result list,
     * and two configuration objects will be merged themselves if they share the same name.<p>
     *
     * For example, if we have two lists of configuration objects:<p>
     *
     * parent: A1, B1, C1<p>
     * child: D2, B2<p>
     *
     * then the resulting list will look like:<p>
     *
     * D2, B3, A1, C1<p>
     *
     * where B3 is the result of merging B1 and B2.<p>
     *
     * @param <C> the type of configuration object
     * @param parentConfigs the parent configurations
     * @param childConfigs the child configurations
     * @param preserveDisabled if true, try to merge parents with disabled children instead of discarding them
     *
     * @return the merged configuration object list
     */
    public static <C extends I_CmsConfigurationObject<C>> List<C> combineConfigurationElements(
        List<C> parentConfigs,
        List<C> childConfigs,
        boolean preserveDisabled) {

        List<C> result = new ArrayList<C>();
        Map<String, C> map = new LinkedHashMap<String, C>();
        if (parentConfigs != null) {
            for (C parent : Lists.reverse(parentConfigs)) {
                map.put(parent.getKey(), parent);
            }
        }
        if (childConfigs == null) {
            childConfigs = Collections.emptyList();
        }
        for (C child : Lists.reverse(childConfigs)) {
            String childKey = child.getKey();
            if (child.isDisabled() && !preserveDisabled) {
                map.remove(childKey);
            } else {
                C parent = map.get(childKey);
                map.remove(childKey);
                C newValue;
                if (parent != null) {
                    newValue = parent.merge(child);
                } else {
                    newValue = child;
                }
                map.put(childKey, newValue);
            }
        }
        result = new ArrayList<C>(map.values());
        Collections.reverse(result);
        // those multiple "reverse" calls may a bit confusing. They are there because on the one hand we want to keep the
        // configuration items from one configuration in the same order as they are defined, on the other hand we want
        // configuration items from a child configuration to rise to the top of the configuration items.

        // so for example, if the parent configuration has items with the keys A,B,C,E
        // and the child configuration has items  with the keys C,B,D
        // we want the items of the combined configuration in the order C,B,D,A,E

        return result;
    }

    /**
     * Applies the formatter change sets of this and all parent configurations to a map of external (non-schema) formatters.<p>
     *
     * @param formatters the external formatter map which will be modified
     *
     * @param formatterCacheState the formatter cache state from which new external formatters should be fetched
     */
    public void applyAllFormatterChanges(
        Map<CmsUUID, I_CmsFormatterBean> formatters,
        CmsFormatterConfigurationCacheState formatterCacheState) {

        for (CmsFormatterChangeSet changeSet : getFormatterChangeSets()) {
            changeSet.applyToFormatters(formatters, formatterCacheState);
        }
    }

    /**
     * Gets the 'best' formatter for the given ID.<p>
     *
     * If the formatter with the ID has a key, then the active formatter with the same key is returned.  Otherwise, the
     * formatter matching the ID is returned. So being active and having the same key is prioritized over an exact ID match.
     *
     * @param id the formatter ID
     * @return the best formatter the given ID
     */
    public I_CmsFormatterBean findFormatter(CmsUUID id) {

        if (id == null) {
            return null;
        }

        CmsFormatterConfigurationCacheState formatterState = getCachedFormatters();
        I_CmsFormatterBean result = formatterState.getFormatters().get(id);
        if ((result != null) && (result.getKey() != null)) {
            String key = result.getKey();
            Collection<I_CmsFormatterBean> activeFormattersForKey = getActiveFormattersByKey().get(key);
            if (activeFormattersForKey.size() > 0) {
                if (activeFormattersForKey.size() > 1) {
                    String labels = ""
                        + activeFormattersForKey.stream().map(this::getFormatterLabel).collect(Collectors.toList());
                    String message = "Ambiguous formatter for key '"
                        + key
                        + "' at '"
                        + getBasePath()
                        + "': found "
                        + labels;
                    LOG.warn(message);
                    OpenCmsServlet.withRequestCache(
                        reqCache -> reqCache.addLog(REQUEST_LOG_CHANNEL, "warn", REQ_LOG_PREFIX + message));
                }
                I_CmsFormatterBean original = result;
                result = activeFormattersForKey.iterator().next();
                String message = "Using substitute formatter "
                    + getFormatterLabel(result)
                    + " instead of "
                    + getFormatterLabel(original)
                    + " because of matching key.";
                LOG.debug(message);
                OpenCmsServlet.withRequestCache(
                    reqCache -> reqCache.addLog(REQUEST_LOG_CHANNEL, "debug", REQ_LOG_PREFIX + message));
            }
        }
        return result;
    }

    /**
     * Gets the 'best' formatter for the given name.<p>
     *
     * The name can be either a formatter key, or a formatter UUID. If it's a key, an active formatter with that key is returned.
     * If it's a UUID, and the formatter with that UUID has no key, it will be returned. If it does have a key, the active formatter
     * with that key is returned (so being active and having the same key is prioritized over an exact ID match).
     *
     * @param name a formatter name (key or ID)
     * @return the best formatter for that name, or null if no formatter could be found
     */
    public I_CmsFormatterBean findFormatter(String name) {

        if (name == null) {
            return null;
        }

        if (systemSettingPrefixes.contains(name) || name.startsWith(CmsContainerElement.SYSTEM_SETTING_PREFIX)) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("System setting prefix used: " + name, new Exception());
            }
            return null;
        }

        if (CmsUUID.isValidUUID(name)) {
            return findFormatter(new CmsUUID(name));
        }

        if (name.startsWith(CmsFormatterConfig.SCHEMA_FORMATTER_ID)) {
            return null;
        }

        Collection<I_CmsFormatterBean> activeForKey = getActiveFormattersByKey().get(name);
        if (activeForKey.size() > 0) {
            if (activeForKey.size() > 1) {
                String labels = "" + activeForKey.stream().map(this::getFormatterLabel).collect(Collectors.toList());
                String message = "Ambiguous formatter for key '"
                    + name
                    + "' at '"
                    + getBasePath()
                    + "': found "
                    + labels;
                LOG.warn(message);
                OpenCmsServlet.withRequestCache(rc -> rc.addLog(REQUEST_LOG_CHANNEL, "warn", REQ_LOG_PREFIX + message));
            }
{'fraction_non_alphanumeric': 0.08151229202880557, 'fraction_numerical': 0.002048671467593742, 'mean_word_length': 2.4593578868248684, 'pattern_counts': {'":': 0, '<': 89, '<?xml version=': 0, '>': 103, 'https://': 0, 'lorem ipsum': 0, 'www.': 6, 'xml': 12}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6957167', 'n_tokens_mistral': 8690, 'n_tokens_neox': 8456, 'n_words': 2707}