001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.configuration;
029
030import org.opencms.cache.CmsVfsMemoryObjectCache;
031import org.opencms.file.CmsFile;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsResourceFilter;
034import org.opencms.main.CmsException;
035import org.opencms.main.CmsLog;
036import org.opencms.main.OpenCms;
037import org.opencms.module.CmsModule;
038import org.opencms.xml.CmsXmlEntityResolver;
039import org.opencms.xml.CmsXmlException;
040import org.opencms.xml.CmsXmlUtils;
041
042import java.util.ArrayList;
043import java.util.HashMap;
044import java.util.List;
045import java.util.Map;
046
047import org.apache.commons.logging.Log;
048
049import org.dom4j.Document;
050import org.dom4j.Element;
051import org.dom4j.Node;
052
053import com.google.common.primitives.Doubles;
054
055/**
056 * Class for accessing global 'weighted' configuration parameters defined in parameter files in the VFS. Used as a singleton.
057 *
058 * <p>
059 * Parameter files are XML configuration files that contain a list of named, string-valued configuration parameters, optionally with a numeric weight. The weight can be set
060 * individually for each parameter, or globally for a whole parameter file, but individual weights override parameter file weights. The schema for these is defined in org/opencm/configuration/paramfile.dtd.
061 * <p>
062 * To register a parameter file in OpenCms, its path must be listed as a value  of the 'paramfile' module parameter for an installed module. The module parameter
063 * can be set on multiple modules, and may also contain multiple paths separated by commas.
064 * <p>
065 * When retrieving a value that is defined in multiple parameter files, the one with the highest weight wins. If there are multiple instances with the same weight, which one of them wins is implementation dependent.
066 */
067public class CmsParameterStore {
068
069    /**
070     * An individual weighted parameter value, with a 'source' attribute for better debuggability.
071     */
072    public static class WeightedValue {
073
074        /** The source where the value comes from. */
075        private String m_source;
076
077        /** The actual value. */
078        private String m_value;
079
080        /** The weight of the value. */
081        private double m_weight;
082
083        /**
084         * Creates a new weighted value.
085         *
086         * @param value the actual value
087         * @param weight the weight
088         * @param source the source
089         */
090        public WeightedValue(String value, double weight, String source) {
091
092            m_value = value;
093            m_source = source;
094            m_weight = weight;
095        }
096
097        /**
098         * Gets the source of the value (for debugging).
099         *
100         * @return the source of the value
101         */
102        public String getSource() {
103
104            return m_source;
105        }
106
107        /**
108         * Gets the value.
109         *
110         * @return value
111         */
112        public String getValue() {
113
114            return m_value;
115        }
116
117        /**
118         * Gets the weight of the value.
119         *
120         * @return the weight of the value
121         */
122        public double getWeight() {
123
124            return m_weight;
125        }
126    }
127
128    /** XML attribute name. */
129    public static final String A_NAME = "name";
130
131    /** XML attribute name. */
132    public static final String A_WEIGHT = "weight";
133
134    /** Default weight, if not defined in parameter file. */
135    public static final double DEFAULT_WEIGHT = 100.0;
136
137    /** XML node name. */
138    public static final String N_PARAM = "param";
139
140    /** Module parameter for registering parameter files. */
141    public static final String PARAM_PARAMFILE = "paramfile";
142
143    /** The global parameter store instance. */
144    private static final CmsParameterStore INSTANCE = new CmsParameterStore();
145
146    /** Logger instance for this class. */
147    private static final Log LOG = CmsLog.getLog(CmsParameterStore.class);
148
149    /**
150     * Gets the global instance.
151     *
152     * @return the global instance
153     */
154    public static CmsParameterStore getInstance() {
155
156        return INSTANCE;
157    }
158
159    /**
160     * Helper method for parsing a parameter file from a byte array.
161     *
162     * @param data the binary data for the parameter file
163     * @param source the source identifier
164     * @return the of parameters
165     * @throws CmsXmlException if something goes wrong
166     */
167    public static Map<String, WeightedValue> parse(byte[] data, String source) throws CmsXmlException {
168
169        CmsXmlEntityResolver resolver = new CmsXmlEntityResolver(null);
170        Document doc;
171        // don't want missing DTD reference to cause a hard error, so we just warn on validation
172        // errors and reparse without validation
173        try {
174            doc = CmsXmlUtils.unmarshalHelper(data, resolver, /*validate=*/true);
175        } catch (CmsXmlException e) {
176            LOG.warn(e.getLocalizedMessage(), e);
177            doc = CmsXmlUtils.unmarshalHelper(data, resolver, false);
178        }
179        return parse(doc.getRootElement(), source);
180    }
181
182    /**
183     * Helper method for parsing a parameter file from a VFS resource.
184     *
185     * @param cms the CmsObject
186     * @param path the path of the resource
187     * @return the map of parameters
188     *
189     * @throws CmsException if something goes wrong
190     */
191    public static Map<String, WeightedValue> parse(CmsObject cms, String path) throws CmsException {
192
193        CmsFile file = cms.readFile(path, CmsResourceFilter.IGNORE_EXPIRATION);
194        return parse(file.getContents(), file.getRootPath());
195
196    }
197
198    /**
199     * Parses a parameter file from an XML element.
200     *
201     * @param rootElem the root element of the XML
202     * @param source the source identifier
203     *
204     * @return the parameter map
205     */
206    public static Map<String, WeightedValue> parse(Element rootElem, String source) {
207
208        double defaultWeight = DEFAULT_WEIGHT;
209        String defaultWeightStr = rootElem.attributeValue(A_WEIGHT);
210        Map<String, WeightedValue> result = new HashMap<>();
211
212        if (defaultWeightStr != null) {
213            try {
214                defaultWeight = Double.parseDouble(defaultWeightStr);
215            } catch (NumberFormatException e) {
216                LOG.error(source + ":" + e.getLocalizedMessage(), e);
217            }
218        }
219        for (Node node : rootElem.selectNodes(N_PARAM)) {
220            Element paramElem = (Element)node;
221            String nameStr = paramElem.attributeValue(A_NAME);
222            String weightStr = paramElem.attributeValue(A_WEIGHT);
223            String content = paramElem.getText();
224            if (nameStr == null) {
225                LOG.error("Missing name attribute in " + source);
226                continue;
227            }
228            double weight = defaultWeight;
229            if (weightStr != null) {
230                Double weightObj = Doubles.tryParse(weightStr);
231                if (weightObj != null) {
232                    weight = weightObj.doubleValue();
233                }
234            }
235            WeightedValue val = new WeightedValue(content, weight, source);
236            result.put(nameStr, val);
237        }
238        return result;
239    }
240
241    /**
242     * Gets the string value with the maximal weight for the parameter with the given key.
243     *
244     * @param cms the CMS context
245     * @param key the key
246     *
247     * @return the string value with maximal weight
248     */
249    public String getValue(CmsObject cms, String key) {
250
251        WeightedValue val = getWeightedValue(cms, key);
252        return val == null ? null : val.getValue();
253    }
254
255    /**
256     * Finds the value with the maximal weight for the given key.
257     *
258     * @param cms the CMS context
259     * @param key the parameter key
260     *
261     * @return the value with the maximal weight
262     */
263    public WeightedValue getWeightedValue(CmsObject cms, String key) {
264
265        return getConfigurations(cms).stream().map(m -> m.get(key)).filter(val -> val != null).max(
266            (v1, v2) -> Double.compare(v1.getWeight(), v2.getWeight())).orElse(null);
267    }
268
269    /**
270     * Retrieves data for all registered parameter files.
271     *
272     * @param cms the CMS context
273     * @return the list of parameter maps from all registered files
274     */
275    private List<Map<String, WeightedValue>> getConfigurations(CmsObject cms) {
276
277        List<CmsModule> modules = OpenCms.getModuleManager().getAllInstalledModules();
278        List<Map<String, WeightedValue>> result = new ArrayList<>();
279        for (CmsModule module : modules) {
280            String paramConfigsStr = module.getParameter(PARAM_PARAMFILE);
281            if (paramConfigsStr != null) {
282                String[] paths = paramConfigsStr.trim().split(" *, *");
283                for (String path : paths) {
284                    result.add(getConfigurationWithCache(cms, path));
285                }
286            }
287        }
288        return result;
289    }
290
291    /**
292     * Gets the configuration for a VFS path, and uses a cache for the results.
293     *
294     * @param cms the CMS context
295     * @param path the path
296     * @return the parameter map
297     */
298    @SuppressWarnings("unchecked")
299    private Map<String, WeightedValue> getConfigurationWithCache(CmsObject cms, String path) {
300
301        String rootPath = cms.getRequestContext().addSiteRoot(path);
302        Map<String, WeightedValue> result;
303        result = (Map<String, WeightedValue>)CmsVfsMemoryObjectCache.getVfsMemoryObjectCache().getCachedObject(
304            cms,
305            rootPath);
306        if (result == null) {
307            try {
308                result = parse(cms, path);
309            } catch (CmsException e) {
310                LOG.info(path + ": " + e.getLocalizedMessage(), e);
311                result = new HashMap<>();
312            }
313            CmsVfsMemoryObjectCache.getVfsMemoryObjectCache().putCachedObject(cms, rootPath, result);
314        }
315        return result;
316
317    }
318
319}