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.ade.configuration.formatters;
029
030import org.opencms.ade.configuration.CmsConfigurationReader;
031import org.opencms.ade.configuration.I_CmsGlobalConfigurationCache;
032import org.opencms.db.CmsPublishedResource;
033import org.opencms.file.CmsFile;
034import org.opencms.file.CmsObject;
035import org.opencms.file.CmsResource;
036import org.opencms.file.CmsResourceFilter;
037import org.opencms.file.types.CmsResourceTypeFunctionConfig;
038import org.opencms.file.types.I_CmsResourceType;
039import org.opencms.loader.CmsResourceManager;
040import org.opencms.main.CmsException;
041import org.opencms.main.CmsLog;
042import org.opencms.main.OpenCms;
043import org.opencms.util.CmsUUID;
044import org.opencms.util.CmsWaitHandle;
045import org.opencms.xml.containerpage.I_CmsFormatterBean;
046import org.opencms.xml.content.CmsXmlContent;
047import org.opencms.xml.content.CmsXmlContentFactory;
048import org.opencms.xml.content.CmsXmlContentProperty;
049import org.opencms.xml.content.CmsXmlContentRootLocation;
050import org.opencms.xml.content.I_CmsXmlContentValueLocation;
051
052import java.util.ArrayList;
053import java.util.Collections;
054import java.util.HashMap;
055import java.util.HashSet;
056import java.util.List;
057import java.util.Locale;
058import java.util.Map;
059import java.util.Set;
060import java.util.concurrent.LinkedBlockingQueue;
061import java.util.concurrent.ScheduledFuture;
062import java.util.concurrent.TimeUnit;
063
064import org.apache.commons.logging.Log;
065
066import com.google.common.collect.Maps;
067
068/**
069 * A cache object which holds a collection of formatter configuration beans read from the VFS.<p>
070 *
071 * This class does not immediately update the cached formatter collection when changes in the VFS occur, but instead
072 * schedules an update action with a slight delay, so that if many formatters are changed in a short time, only one update
073 * operation is needed.<p>
074 *
075 * Two instances of this cache are needed, one for the Online project and one for Offline projects.<p>
076 **/
077public class CmsFormatterConfigurationCache implements I_CmsGlobalConfigurationCache {
078
079    /** A UUID which is used to mark the configuration cache for complete reloading. */
080    public static final CmsUUID RELOAD_MARKER = CmsUUID.getNullUUID();
081
082    /** The resource type for macro formatters. */
083    public static final String TYPE_FLEX_FORMATTER = "flex_formatter";
084
085    /** The resource type for formatter configurations. */
086    public static final String TYPE_FORMATTER_CONFIG = "formatter_config";
087
088    /** The resource type for macro formatters. */
089    public static final String TYPE_MACRO_FORMATTER = "macro_formatter";
090
091    /** Type name for setting configurations. */
092    public static final String TYPE_SETTINGS_CONFIG = "settings_config";
093
094    /** The delay to use for updating the formatter cache, in seconds. */
095    protected static int UPDATE_DELAY_MILLIS = 500;
096
097    /** The logger for this class. */
098    private static final Log LOG = CmsLog.getLog(CmsFormatterConfigurationCache.class);
099
100    /** The future for the scheduled task. */
101    private volatile ScheduledFuture<?> m_taskFuture;
102
103    /** The work queue to keep track of what needs to be done during the next cache update. */
104    private LinkedBlockingQueue<Object> m_workQueue = new LinkedBlockingQueue<>();
105
106    /** The CMS context used by this cache. */
107    private CmsObject m_cms;
108
109    /** The cache name. */
110    private String m_name;
111
112    /** Additional setting configurations. */
113    private volatile Map<CmsUUID, List<CmsXmlContentProperty>> m_settingConfigs;
114
115    /** The current data contained in the formatter cache.<p> This field is reassigned when formatters are changed, but the objects pointed to by this  field are immutable.<p> **/
116    private volatile CmsFormatterConfigurationCacheState m_state = new CmsFormatterConfigurationCacheState(
117        Collections.<CmsUUID, I_CmsFormatterBean> emptyMap());
118
119    /**
120     * Creates a new formatter configuration cache instance.<p>
121     *
122     * @param cms the CMS context to use
123     * @param name the cache name
124     *
125     * @throws CmsException if something goes wrong
126     */
127    public CmsFormatterConfigurationCache(CmsObject cms, String name)
128    throws CmsException {
129
130        m_cms = OpenCms.initCmsObject(cms);
131        Map<CmsUUID, I_CmsFormatterBean> noFormatters = Collections.emptyMap();
132        m_state = new CmsFormatterConfigurationCacheState(noFormatters);
133        m_name = name;
134    }
135
136    /**
137     * Adds a wait handle to the list of wait handles.<p>
138     *
139     * @param handle the handle to add
140     */
141    public void addWaitHandle(CmsWaitHandle handle) {
142
143        m_workQueue.add(handle);
144    }
145
146    /**
147     * @see org.opencms.ade.configuration.I_CmsGlobalConfigurationCache#clear()
148     */
149    public void clear() {
150
151        markForUpdate(RELOAD_MARKER);
152    }
153
154    /**
155     * Gets the cache instance name.<p>
156     *
157     * @return the cache instance name
158     */
159    public String getName() {
160
161        return m_name;
162    }
163
164    /**
165     * Gets the collection of cached formatters.<p>
166     *
167     * @return the collection of cached formatters
168     */
169    public CmsFormatterConfigurationCacheState getState() {
170
171        return m_state;
172    }
173
174    /**
175     * Initializes the cache and installs the update task.<p>
176     */
177    public void initialize() {
178
179        if (m_taskFuture != null) {
180            m_taskFuture.cancel(false);
181            m_taskFuture = null;
182        }
183        reload();
184        m_taskFuture = OpenCms.getExecutor().scheduleWithFixedDelay(
185            this::performUpdate,
186            UPDATE_DELAY_MILLIS,
187            UPDATE_DELAY_MILLIS,
188            TimeUnit.MILLISECONDS);
189
190    }
191
192    /**
193     * The method called by the scheduled update action to update the cache.<p>
194     */
195    public void performUpdate() {
196
197        // Wrap everything in try-catch because we don't want to leak an exception out of a scheduled task
198        try {
199            ArrayList<Object> work = new ArrayList<>();
200            m_workQueue.drainTo(work);
201            Set<CmsUUID> copiedIds = new HashSet<CmsUUID>();
202            List<CmsWaitHandle> waitHandles = new ArrayList<>();
203            for (Object o : work) {
204                if (o instanceof CmsUUID) {
205                    copiedIds.add((CmsUUID)o);
206                } else if (o instanceof CmsWaitHandle) {
207                    waitHandles.add((CmsWaitHandle)o);
208                }
209            }
210            if (copiedIds.contains(RELOAD_MARKER)) {
211                // clear cache event, reload all formatter configurations
212                reload();
213            } else {
214                // normal case: incremental update
215                Map<CmsUUID, I_CmsFormatterBean> formattersToUpdate = Maps.newHashMap();
216                for (CmsUUID structureId : copiedIds) {
217                    I_CmsFormatterBean formatterBean = readFormatter(structureId);
218                    // formatterBean may be null here
219                    formattersToUpdate.put(structureId, formatterBean);
220                }
221                m_state = m_state.createUpdatedCopy(formattersToUpdate);
222            }
223            for (CmsWaitHandle handle : waitHandles) {
224                handle.release();
225            }
226        } catch (Exception e) {
227            LOG.error(e.getLocalizedMessage(), e);
228        }
229    }
230
231    /**
232     * Reloads the formatter cache.<p>
233     */
234    public void reload() {
235
236        List<CmsResource> settingConfigResources = new ArrayList<>();
237        try {
238            I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(TYPE_SETTINGS_CONFIG);
239            CmsResourceFilter filter = CmsResourceFilter.ONLY_VISIBLE_NO_DELETED.addRequireType(type);
240            settingConfigResources.addAll(m_cms.readResources("/", filter));
241        } catch (CmsException e) {
242            LOG.warn(e.getLocalizedMessage(), e);
243        }
244        Map<CmsUUID, List<CmsXmlContentProperty>> settingConfigs = new HashMap<>();
245        for (CmsResource resource : settingConfigResources) {
246            parseSettingsConfig(resource, settingConfigs);
247        }
248        m_settingConfigs = settingConfigs;
249
250        List<CmsResource> formatterResources = new ArrayList<CmsResource>();
251        try {
252            I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(TYPE_FORMATTER_CONFIG);
253            CmsResourceFilter filter = CmsResourceFilter.ONLY_VISIBLE_NO_DELETED.addRequireType(type);
254            formatterResources.addAll(m_cms.readResources("/", filter));
255        } catch (CmsException e) {
256            LOG.warn(e.getLocalizedMessage(), e);
257        }
258        try {
259            I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(TYPE_MACRO_FORMATTER);
260            CmsResourceFilter filter = CmsResourceFilter.ONLY_VISIBLE_NO_DELETED.addRequireType(type);
261            formatterResources.addAll(m_cms.readResources("/", filter));
262            I_CmsResourceType typeFlex = OpenCms.getResourceManager().getResourceType(TYPE_FLEX_FORMATTER);
263            CmsResourceFilter filterFlex = CmsResourceFilter.ONLY_VISIBLE_NO_DELETED.addRequireType(typeFlex);
264            formatterResources.addAll(m_cms.readResources("/", filterFlex));
265            I_CmsResourceType typeFunction = OpenCms.getResourceManager().getResourceType(
266                CmsResourceTypeFunctionConfig.TYPE_NAME);
267            CmsResourceFilter filterFunction = CmsResourceFilter.ONLY_VISIBLE_NO_DELETED.addRequireType(typeFunction);
268            formatterResources.addAll(m_cms.readResources("/", filterFunction));
269        } catch (CmsException e) {
270            LOG.warn(e.getLocalizedMessage(), e);
271        }
272        Map<CmsUUID, I_CmsFormatterBean> newFormatters = Maps.newHashMap();
273        for (CmsResource formatterResource : formatterResources) {
274            I_CmsFormatterBean formatterBean = readFormatter(formatterResource.getStructureId());
275            if (formatterBean != null) {
276                newFormatters.put(formatterResource.getStructureId(), formatterBean);
277            }
278        }
279        m_state = new CmsFormatterConfigurationCacheState(newFormatters);
280
281    }
282
283    /**
284     * @see org.opencms.ade.configuration.I_CmsGlobalConfigurationCache#remove(org.opencms.db.CmsPublishedResource)
285     */
286    public void remove(CmsPublishedResource pubRes) {
287
288        checkIfUpdateIsNeeded(pubRes.getStructureId(), pubRes.getRootPath(), pubRes.getType());
289    }
290
291    /**
292     * @see org.opencms.ade.configuration.I_CmsGlobalConfigurationCache#remove(org.opencms.file.CmsResource)
293     */
294    public void remove(CmsResource resource) {
295
296        checkIfUpdateIsNeeded(resource.getStructureId(), resource.getRootPath(), resource.getTypeId());
297    }
298
299    /**
300     * @see org.opencms.ade.configuration.I_CmsGlobalConfigurationCache#update(org.opencms.db.CmsPublishedResource)
301     */
302    public void update(CmsPublishedResource pubRes) {
303
304        checkIfUpdateIsNeeded(pubRes.getStructureId(), pubRes.getRootPath(), pubRes.getType());
305    }
306
307    /**
308     * @see org.opencms.ade.configuration.I_CmsGlobalConfigurationCache#update(org.opencms.file.CmsResource)
309     */
310    public void update(CmsResource resource) {
311
312        checkIfUpdateIsNeeded(resource.getStructureId(), resource.getRootPath(), resource.getTypeId());
313    }
314
315    /**
316     * Waits until no update action is scheduled.<p>
317     *
318     * Should only be used in tests.<p>
319     */
320    public void waitForUpdate() {
321
322        CmsWaitHandle handle = new CmsWaitHandle(true);
323        addWaitHandle(handle);
324        handle.enter(Long.MAX_VALUE);
325    }
326
327    /**
328     * Reads a formatter given its structure id and returns it, or null if the formatter couldn't be read.<p>
329     *
330     * @param structureId the structure id of the formatter configuration
331     *
332     * @return the formatter bean, or null if no formatter could be read for some reason
333     */
334    protected I_CmsFormatterBean readFormatter(CmsUUID structureId) {
335
336        I_CmsFormatterBean formatterBean = null;
337        CmsResource formatterRes = null;
338        try {
339            formatterRes = m_cms.readResource(structureId);
340            CmsFile formatterFile = m_cms.readFile(formatterRes);
341            CmsFormatterBeanParser parser = new CmsFormatterBeanParser(m_cms, m_settingConfigs);
342            CmsXmlContent content = CmsXmlContentFactory.unmarshal(m_cms, formatterFile);
343            formatterBean = parser.parse(content, formatterRes.getRootPath(), "" + formatterRes.getStructureId());
344        } catch (Exception e) {
345
346            if (formatterRes == null) {
347                // normal case if resources get deleted, should not be written to the error channel
348                LOG.info("Could not read formatter with id " + structureId);
349            } else {
350                LOG.error(
351                    "Error while trying to read formatter configuration "
352                        + formatterRes.getRootPath()
353                        + ":    "
354                        + e.getLocalizedMessage(),
355                    e);
356            }
357        }
358        return formatterBean;
359    }
360
361    /**
362     * Checks if an update of the formatter is needed and if so, adds its structure id to the update set.<p>
363     *
364     * @param structureId the structure id of the formatter
365     * @param path the path of the formatter
366     * @param resourceType the resource type
367     */
368    private void checkIfUpdateIsNeeded(CmsUUID structureId, String path, int resourceType) {
369
370        if (CmsResource.isTemporaryFileName(path)) {
371            return;
372        }
373        CmsResourceManager manager = OpenCms.getResourceManager();
374
375        if (manager.matchResourceType(TYPE_SETTINGS_CONFIG, resourceType)) {
376            // for each formatter configuration, only the combined settings are stored, not
377            // the reference to the settings config. So we need to reload everything when a setting configuration
378            // changes.
379            markForUpdate(RELOAD_MARKER);
380            return;
381        }
382
383        if (manager.matchResourceType(TYPE_FORMATTER_CONFIG, resourceType)
384            || manager.matchResourceType(TYPE_MACRO_FORMATTER, resourceType)
385            || manager.matchResourceType(TYPE_FLEX_FORMATTER, resourceType)
386            || manager.matchResourceType(CmsResourceTypeFunctionConfig.TYPE_NAME, resourceType)) {
387            markForUpdate(structureId);
388        }
389    }
390
391    /**
392     * Adds a formatter structure id to the update set, and schedule an update task unless one is already scheduled.<p>
393     *
394     * @param structureId the structure id of the formatter configuration
395     */
396    private void markForUpdate(CmsUUID structureId) {
397
398        m_workQueue.add(structureId);
399    }
400
401    /**
402     * Helper method for parsing a settings configuration file.<p>
403     *
404     * @param resource the resource to parse
405     * @param settingConfigs the map in which the result should be stored, with the structure id of the resource as the key
406     */
407    private void parseSettingsConfig(CmsResource resource, Map<CmsUUID, List<CmsXmlContentProperty>> settingConfigs) {
408
409        List<CmsXmlContentProperty> settingConfig = new ArrayList<>();
410
411        try {
412            CmsFile settingFile = m_cms.readFile(resource);
413            CmsXmlContent settingContent = CmsXmlContentFactory.unmarshal(m_cms, settingFile);
414            CmsXmlContentRootLocation location = new CmsXmlContentRootLocation(settingContent, Locale.ENGLISH);
415            for (I_CmsXmlContentValueLocation settingLoc : location.getSubValues(CmsFormatterBeanParser.N_SETTING)) {
416                CmsXmlContentProperty setting = CmsConfigurationReader.parseProperty(
417                    m_cms,
418                    settingLoc).getPropertyData();
419                settingConfig.add(setting);
420            }
421            settingConfigs.put(resource.getStructureId(), settingConfig);
422        } catch (Exception e) {
423            LOG.error(e.getLocalizedMessage(), e);
424        }
425    }
426}