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