001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (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.formatters;
029
030import org.opencms.gwt.shared.CmsGwtConstants;
031import org.opencms.main.CmsLog;
032import org.opencms.util.CmsUUID;
033import org.opencms.xml.content.CmsXmlContentProperty;
034
035import java.util.ArrayList;
036import java.util.Collections;
037import java.util.HashMap;
038import java.util.LinkedHashMap;
039import java.util.List;
040import java.util.Map;
041import java.util.concurrent.ExecutionException;
042
043import org.apache.commons.logging.Log;
044
045import com.google.common.cache.CacheBuilder;
046import com.google.common.cache.CacheLoader;
047import com.google.common.cache.LoadingCache;
048import com.google.common.collect.ImmutableList;
049
050/**
051 * Contains the setting-related data for a formatter bean.
052 */
053public class CmsSettingConfiguration {
054
055    /**
056     * The shared setting configuration file that defines a setting, for configuration origin discovery:
057     * the file's structure id, whether the matching definition is formatter-specific, and whether it
058     * comes from a sitemap setting override (rather than the formatter's own shared settings).
059     */
060    public static class CmsSettingDefiningFile {
061
062        /** The structure id of the shared setting configuration file. */
063        private final CmsUUID m_fileId;
064
065        /** Whether the matching definition is keyed to the formatter (as opposed to global). */
066        private final boolean m_formatterScoped;
067
068        /** Whether the file is a sitemap setting override (as opposed to a formatter shared setting). */
069        private final boolean m_fromOverride;
070
071        /**
072         * Creates a new instance.<p>
073         *
074         * @param fileId the file structure id
075         * @param formatterScoped whether the matching definition is formatter-specific
076         * @param fromOverride whether the file is a sitemap setting override
077         */
078        public CmsSettingDefiningFile(CmsUUID fileId, boolean formatterScoped, boolean fromOverride) {
079
080            m_fileId = fileId;
081            m_formatterScoped = formatterScoped;
082            m_fromOverride = fromOverride;
083        }
084
085        /**
086         * Gets the structure id of the defining shared setting configuration file.<p>
087         *
088         * @return the file structure id
089         */
090        public CmsUUID getFileId() {
091
092            return m_fileId;
093        }
094
095        /**
096         * Returns whether the matching definition is keyed to the formatter (vs. the global setting).<p>
097         *
098         * @return <code>true</code> if the definition is formatter-specific
099         */
100        public boolean isFormatterScoped() {
101
102            return m_formatterScoped;
103        }
104
105        /**
106         * Returns whether the file is a sitemap setting override (vs. a formatter shared setting).<p>
107         *
108         * @return <code>true</code> if the file is a sitemap setting override
109         */
110        public boolean isFromOverride() {
111
112            return m_fromOverride;
113        }
114    }
115
116    /** The logger instance for this class. */
117    private static final Log LOG = CmsLog.getLog(CmsSettingConfiguration.class);
118
119    /** Cache for calculating and storing the setting definition maps for various combinations of override shared setting configuration file ids. */
120    private LoadingCache<ImmutableList<CmsUUID>, Map<String, CmsXmlContentProperty>> m_cache = CacheBuilder.newBuilder().concurrencyLevel(
121        4).build(new CacheLoader<ImmutableList<CmsUUID>, Map<String, CmsXmlContentProperty>>() {
122
123            @SuppressWarnings("synthetic-access")
124            @Override
125            public Map<String, CmsXmlContentProperty> load(ImmutableList<CmsUUID> sharedSettingOverrides)
126            throws Exception {
127
128                return resolveSettings(sharedSettingOverrides);
129
130            }
131
132        });
133
134    /** The display type. */
135    private String m_displayType;
136
137    /** The key of the formatter using this configuration (may be null). */
138    private String m_formatterKey;
139
140    /** The settings configured in the formatter configuration. */
141    private List<CmsXmlContentProperty> m_listedSettings;
142
143    /** A map of all shared setting configurations in the system, with their structure ids as keys. */
144    private Map<CmsUUID, Map<CmsSharedSettingKey, CmsXmlContentProperty>> m_sharedSettingConfigsById;
145
146    /** The list of structure ids of shared settings files configured in the formatter. The last entry has the highest priority. */
147    private List<CmsUUID> m_sharedSettingsIdsFromFormatter;
148
149    /**
150     * Creates an empty configuration.
151     */
152    public CmsSettingConfiguration() {
153
154        m_listedSettings = new ArrayList<>();
155        m_sharedSettingConfigsById = new HashMap<>();
156        m_displayType = null;
157        m_sharedSettingsIdsFromFormatter = new ArrayList<>();
158    }
159
160    /**
161     *
162     * @param listedSettings  the setting entries configured in the formatter configuration
163     * @param sharedSettingConfigsById the map of shared setting configurations, with their structure ids as keys
164     * @param includeIds the list of structure ids of shared setting configurations referenced from the formatter configuration
165     * @param formatterKey the key of the formatter using this setting configuration (may be null)
166     * @param displayType the display type
167     */
168    public CmsSettingConfiguration(
169        List<CmsXmlContentProperty> listedSettings,
170        Map<CmsUUID, Map<CmsSharedSettingKey, CmsXmlContentProperty>> sharedSettingConfigsById,
171        List<CmsUUID> includeIds,
172        String formatterKey,
173        String displayType) {
174
175        m_listedSettings = listedSettings;
176        m_sharedSettingConfigsById = sharedSettingConfigsById;
177        m_displayType = displayType;
178        m_formatterKey = formatterKey;
179        m_sharedSettingsIdsFromFormatter = new ArrayList<>(includeIds);
180    }
181
182    /**
183     * Finds the shared setting configuration file that defines the given setting, searching the
184     * sitemap setting overrides first (highest priority) and then the formatter's own shared setting
185     * files, in order of decreasing specificity. Read-only, used for configuration origin discovery.<p>
186     *
187     * @param overrideIds the sitemap setting override file ids in ascending specificity, e.g. from
188     *            {@link org.opencms.ade.configuration.CmsADEConfigData#getSharedSettingOverrides()}
189     * @param settingName the setting name (the property name of the setting)
190     *
191     * @return the file defining the setting, or <code>null</code> if no shared setting file defines it
192     */
193    public CmsSettingDefiningFile findDefiningFile(List<CmsUUID> overrideIds, String settingName) {
194
195        // shared settings are keyed by include name, so resolve the requested setting name back to the
196        // include name the formatter actually references before searching the files
197        String includeName = resolveIncludeName(overrideIds, settingName);
198        // sitemap overrides win over the formatter's own shared settings; within each group the last
199        // entry is the most specific, so search from the most specific downwards
200        CmsSettingDefiningFile fromOverride = findInFiles(overrideIds, includeName, true);
201        if (fromOverride != null) {
202            return fromOverride;
203        }
204        return findInFiles(m_sharedSettingsIdsFromFormatter, includeName, false);
205    }
206
207    /**
208     * Gets the setting map by looking up the configured settings' include names in either the shared settings files
209     * configured in the formatter configuration, or the override shared settings files whose ids are passed as parameter.
210     *
211     *  <p>
212     *  Setting definitions from Override shared settings files have higher priority than those referenced in the formatter
213     *  configuration, and later entries in both lists have higher prioritiy than earlier ones.
214     *
215     * @param sharedSettingOverrides the structure ids of Override shared setting configurations (highest priority last)
216     *
217     * @return the setting definition map
218     */
219    public Map<String, CmsXmlContentProperty> getSettings(ImmutableList<CmsUUID> sharedSettingOverrides) {
220
221        try {
222            return m_cache.get(sharedSettingOverrides);
223        } catch (ExecutionException e) {
224            LOG.error(e.getLocalizedMessage(), e);
225            return Collections.emptyMap();
226        }
227    }
228
229    /**
230     * Combines shard setting definitions from multiple shared setting files into a single map.
231     *
232     * @param ids the structure ids of shared setting files, in order of increasing specificity
233     *
234     * @return the combined map of shared setting definitions
235     */
236    private Map<CmsSharedSettingKey, CmsXmlContentProperty> combineSharedSettingDefinitionMaps(List<CmsUUID> ids) {
237
238        Map<CmsSharedSettingKey, CmsXmlContentProperty> result = new HashMap<>();
239        for (CmsUUID settingFileId : ids) {
240            Map<CmsSharedSettingKey, CmsXmlContentProperty> sharedSettingsForLevel = m_sharedSettingConfigsById.get(
241                settingFileId);
242            if (sharedSettingsForLevel != null) {
243                // since we have different map keys for each (includeName, formatterKey) combination,
244                // putAll does the right thing here, i.e. setting definitions are overridden for their individual formatter keys
245                result.putAll(sharedSettingsForLevel);
246            } else {
247                LOG.warn("Shared setting reference not found: " + settingFileId);
248            }
249        }
250        return result;
251    }
252
253    /**
254     * Searches a list of shared setting files (in ascending specificity) for a definition with the
255     * given include name, most specific file first, preferring a formatter-specific entry over the
256     * global one.<p>
257     *
258     * @param ids the shared setting file ids in ascending specificity
259     * @param includeName the include name of the setting to look for
260     * @param fromOverride whether the files are sitemap setting overrides
261     *
262     * @return the defining file, or <code>null</code>
263     */
264    private CmsSettingDefiningFile findInFiles(List<CmsUUID> ids, String includeName, boolean fromOverride) {
265
266        for (int i = ids.size() - 1; i >= 0; i--) {
267            Map<CmsSharedSettingKey, CmsXmlContentProperty> map = m_sharedSettingConfigsById.get(ids.get(i));
268            if (map == null) {
269                continue;
270            }
271            // the shared setting maps are keyed by (include name, formatter key); match the resolved
272            // include name and prefer a formatter-specific entry over the global one
273            if ((m_formatterKey != null) && map.containsKey(new CmsSharedSettingKey(includeName, m_formatterKey))) {
274                return new CmsSettingDefiningFile(ids.get(i), true, fromOverride);
275            }
276            if (map.containsKey(new CmsSharedSettingKey(includeName, null))) {
277                return new CmsSettingDefiningFile(ids.get(i), false, fromOverride);
278            }
279        }
280        return null;
281    }
282
283    /**
284     * Helper method to get a shared setting for this formatter.
285     *
286     *  <p>Prioritizes shared settings with a formatter key matching this formatter's key.
287     *
288     * @param map the map of shared settings
289     * @param includeName the effective include name of the setting definition to find
290     *
291     * @return the shared setting definition
292     */
293    private CmsXmlContentProperty getSharedSetting(
294        Map<CmsSharedSettingKey, CmsXmlContentProperty> map,
295        String includeName) {
296
297        CmsXmlContentProperty result = null;
298
299        // try formatter key specific entry first, if not found try the general entry
300
301        if (m_formatterKey != null) {
302            result = map.get(new CmsSharedSettingKey(includeName, m_formatterKey));
303        }
304        if (result == null) {
305            result = map.get(new CmsSharedSettingKey(includeName, null));
306        }
307        return result;
308
309    }
310
311    /**
312     * Merges a setting listed in the formatter with its matching shared and override definitions, pulling
313     * each field value from the first of [override, formatter, shared] where it is defined.
314     *
315     * @param settingDef the setting definition listed in the formatter
316     * @param includeName the effective include name of the setting definition
317     * @param sharedSettingDefinitions the shared setting definitions referenced from the formatter
318     * @param overrideSettingDefinitions the sitemap override setting definitions active in the current context
319     *
320     * @return the merged setting definition
321     */
322    private CmsXmlContentProperty mergeListedSetting(
323        CmsXmlContentProperty settingDef,
324        String includeName,
325        Map<CmsSharedSettingKey, CmsXmlContentProperty> sharedSettingDefinitions,
326        Map<CmsSharedSettingKey, CmsXmlContentProperty> overrideSettingDefinitions) {
327
328        CmsXmlContentProperty defaultSetting = getSharedSetting(sharedSettingDefinitions, includeName);
329        CmsXmlContentProperty overrideSetting = getSharedSetting(overrideSettingDefinitions, includeName);
330        CmsXmlContentProperty mergedSetting = settingDef;
331        if (defaultSetting != null) {
332            mergedSetting = mergedSetting.mergeDefaults(defaultSetting);
333        }
334        if (overrideSetting != null) {
335            mergedSetting = overrideSetting.mergeDefaults(mergedSetting);
336        }
337        return mergedSetting;
338    }
339
340    /**
341     * Resolves a setting's effective (property) name back to the include name that the formatter
342     * references it by, mirroring how {@link #resolveSettings} derives the effective settings. Read-only,
343     * used for configuration origin discovery.<p>
344     *
345     * @param overrideIds the sitemap setting override file ids active in the current context
346     * @param settingName the effective (property) name of the setting
347     *
348     * @return the include name the formatter references the setting by, or <code>settingName</code> if no
349     *         listed setting resolves to it
350     */
351    private String resolveIncludeName(List<CmsUUID> overrideIds, String settingName) {
352
353        Map<CmsSharedSettingKey, CmsXmlContentProperty> sharedSettingDefinitions = combineSharedSettingDefinitionMaps(
354            m_sharedSettingsIdsFromFormatter);
355        Map<CmsSharedSettingKey, CmsXmlContentProperty> overrideSettingDefinitions = combineSharedSettingDefinitionMaps(
356            overrideIds);
357        String result = settingName;
358        // later listed settings win, mirroring the last-wins put in resolveSettings
359        for (CmsXmlContentProperty settingDef : m_listedSettings) {
360            String includeName = settingDef.getIncludeName(settingDef.getName());
361            if (includeName == null) {
362                continue;
363            }
364            CmsXmlContentProperty mergedSetting = mergeListedSetting(
365                settingDef,
366                includeName,
367                sharedSettingDefinitions,
368                overrideSettingDefinitions);
369            if (settingName.equals(mergedSetting.getName())) {
370                result = includeName;
371            }
372        }
373        return result;
374    }
375
376    /**
377     * Computes the finished map of settings for the given combination of shared setting overrides.+
378     *
379     * @param overrideSharedSettingsIds the structure ids of shared setting overrides active in the current context, with the most specific override last
380     *
381     * @return the finished map of settings
382     */
383    private Map<String, CmsXmlContentProperty> resolveSettings(ImmutableList<CmsUUID> overrideSharedSettingsIds) {
384
385        Map<String, CmsXmlContentProperty> result = new LinkedHashMap<>();
386
387        Map<CmsSharedSettingKey, CmsXmlContentProperty> sharedSettingDefinitions = combineSharedSettingDefinitionMaps(
388            m_sharedSettingsIdsFromFormatter);
389        Map<CmsSharedSettingKey, CmsXmlContentProperty> overrideSettingDefinitions = combineSharedSettingDefinitionMaps(
390            overrideSharedSettingsIds);
391
392        /*
393         * For each setting listed in the formatter, try to find a matching setting definition in both the shared settings (referenced from the formatter)
394         * and the setting overrides (configured in the sitemap/master configuration). These three setting definition objects (or less, if the setting override or shared
395         * setting doesn't exist) are then merged, such that each individual field value for the setting definition is pulled from the first entry in the list
396         * [overrideSetting, settingFromFormatter, settingFromSharedSettings] where it is defined. I.e. the field values defined in setting overrides have the highest
397         * priority.
398         */
399        for (CmsXmlContentProperty settingDef : m_listedSettings) {
400            String includeName = settingDef.getIncludeName(settingDef.getName());
401            if (includeName == null) {
402                continue;
403            }
404
405            CmsXmlContentProperty mergedSetting = mergeListedSetting(
406                settingDef,
407                includeName,
408                sharedSettingDefinitions,
409                overrideSettingDefinitions);
410            if (mergedSetting.getName() == null) {
411                continue;
412            }
413            result.put(mergedSetting.getName(), mergedSetting);
414        }
415        if ((m_displayType != null) && !result.containsKey(CmsFormatterBeanParser.SETTING_DISPLAY_TYPE)) {
416            CmsXmlContentProperty displayType = new CmsXmlContentProperty(
417                CmsFormatterBeanParser.SETTING_DISPLAY_TYPE,
418                "string",
419                CmsGwtConstants.HIDDEN_SETTINGS_WIDGET_NAME,
420                null,
421                null,
422                null,
423                m_displayType,
424                null,
425                null,
426                null,
427                null);
428            result.put(displayType.getName(), displayType);
429        }
430        return Collections.unmodifiableMap(result);
431    }
432}