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.flex;
029
030import org.opencms.db.CmsPublishedResource;
031import org.opencms.file.CmsFile;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsResource;
034import org.opencms.main.CmsException;
035import org.opencms.main.CmsLog;
036import org.opencms.util.CmsFileUtil;
037import org.opencms.util.CmsStringUtil;
038
039import java.io.ByteArrayInputStream;
040import java.io.IOException;
041import java.io.InputStreamReader;
042import java.util.Arrays;
043import java.util.BitSet;
044import java.util.Collections;
045import java.util.List;
046import java.util.Properties;
047import java.util.Set;
048
049import org.apache.commons.logging.Log;
050
051import com.google.common.collect.ArrayListMultimap;
052import com.google.common.collect.Lists;
053import com.google.common.collect.Sets;
054
055/**
056 * Represents a Flex bucket configuration.<p>
057 *
058 * This consists of a list of flex bucket definitions, and a 'clear all' list.<p>
059 *
060 * Each flex bucket definition consists of a name and a list of paths. If any resources from the given list of paths or their
061 * descendants is published, the corresponding Flex bucket's contents should be removed from the Flex cache.
062 *
063 * If a resource with its path below one of the paths from the 'clear all' list is published, the complete Flex cache should be
064 * cleared.
065 *
066 */
067public class CmsFlexBucketConfiguration {
068
069    /**
070     * A data structure representing a set of Flex cache buckets.<p>
071     */
072    public class BucketSet {
073
074        /** A bit set, with each bit index representing a bucket. */
075        private BitSet m_bits = new BitSet();
076
077        /**
078         * Creates a new instance from a set of bucket names.<p>
079         *
080         * @param buckets the bucket names
081         */
082        protected BucketSet(Set<String> buckets) {
083            for (String bucket : buckets) {
084                int index = getBucketIndex(bucket);
085                if (index >= 0) {
086                    m_bits.set(index);
087                }
088            }
089        }
090
091        /**
092         * Computes the list of bucket names for this instance.<p>
093         *
094         * @return the list of bucket names
095         */
096        public List<String> getBucketNames() {
097
098            List<String> result = Lists.newArrayList();
099            for (int i = m_bits.nextSetBit(0); i >= 0; i = m_bits.nextSetBit(i + 1)) {
100                result.add(getBucketName(i));
101            }
102            return result;
103        }
104
105        /**
106         * If this entry is the bucket set created from a publish list, and the argument is the bucket list
107         * of a flex cache entry, then the result of this method determines whether the flex cache entry for which
108         * the argument bucket set was created should be removed.<p>
109         *
110         * @param flexEntryBucketSet the bucket set for the flex cache entry
111         *
112         * @return true if the flex cache entry from which argument flex bucket set was generated should be removed.<p>
113         */
114        public boolean matchForDeletion(BucketSet flexEntryBucketSet) {
115
116            if (flexEntryBucketSet == null) {
117                return true;
118            }
119            BitSet otherBits = flexEntryBucketSet.m_bits;
120            BitSet commonBits = (BitSet)(m_bits.clone());
121            commonBits.and(otherBits);
122            return !(commonBits.isEmpty());
123        }
124
125        /**
126         * @see java.lang.Object#toString()
127         */
128        @Override
129        public String toString() {
130
131            return "[BucketSet:" + getBucketNames().toString() + "]";
132        }
133
134    }
135
136    /** Special bucket name for everything that doesn't belong in any other bucket. */
137    public static final String BUCKET_OTHER = "OTHER";
138
139    /** Configuration key for the list of folders for which the whole flex cache should be purged when a resource in them is published. */
140    public static final String KEY_CLEAR_ALL = "clearAll";
141
142    /** The configuration key prefix used to define a bucket. */
143    public static final String KEY_PREFIX_BUCKET = "bucket.";
144
145    /** Logger instance for this class. */
146    private static final Log LOG = CmsLog.getLog(CmsFlexBucketConfiguration.class);
147
148    /** The list of bucket names. */
149    private List<String> m_bucketNames = Lists.newArrayList();
150
151    /** The list of bucket paths for the bucket at the corresponding list in m_bucketNames. */
152    private List<List<String>> m_bucketPathLists = Lists.newArrayList();
153
154    /** A list of paths for which the flex cache should be cleared completely if any resources below them are published. */
155    private List<String> m_clearAll = Lists.newArrayList("/system/modules");
156
157    /** Flag which, when set, prevents further modification of this configuration object. */
158    private boolean m_frozen;
159
160    /**
161     * Loads the flex bucket configuration from a java.util.Properties instance.<p>
162     *
163     * @param properties the properties from which to load the configuration
164     * @return the configuration
165     */
166    public static CmsFlexBucketConfiguration loadFromProperties(Properties properties) {
167
168        ArrayListMultimap<String, String> multimap = ArrayListMultimap.create();
169        List<String> clearAll = Lists.newArrayList();
170        for (Object keyObj : properties.keySet()) {
171            String key = (String)keyObj;
172            key = key.trim();
173            String value = (String)(properties.get(key));
174            value = value.trim();
175            if (key.startsWith(KEY_PREFIX_BUCKET)) {
176                String bucketName = key.substring(KEY_PREFIX_BUCKET.length());
177                multimap.putAll(bucketName, Arrays.asList(value.trim().split(" *, *")));
178            } else if (KEY_CLEAR_ALL.equals(key)) {
179                clearAll = Arrays.asList(value.trim().split(" *, *"));
180            }
181        }
182        CmsFlexBucketConfiguration result = new CmsFlexBucketConfiguration();
183        if (!clearAll.isEmpty()) {
184            result.setClearAll(clearAll);
185        }
186        for (String key : multimap.keySet()) {
187            result.add(key, multimap.get(key));
188        }
189        result.freeze();
190        return result;
191    }
192
193    /**
194     * Loads a flex bucket configuration from the OpenCms VFS.<p>
195     *
196     * @param cms the CMS context to use for VFS operations
197     * @param path the path of the resource
198     *
199     * @return the flex bucket configuration
200     *
201     * @throws CmsException if something goes wrong
202     */
203    public static CmsFlexBucketConfiguration loadFromVfsFile(CmsObject cms, String path) throws CmsException {
204
205        if (!cms.existsResource(path)) {
206            return null;
207        }
208        CmsResource configRes = cms.readResource(path);
209        if (configRes.isFolder()) {
210            return null;
211        }
212        CmsFile configFile = cms.readFile(configRes);
213        String encoding = CmsFileUtil.getEncoding(cms, configRes);
214        Properties props = new Properties();
215        try {
216            props.load(new InputStreamReader(new ByteArrayInputStream(configFile.getContents()), encoding));
217            return loadFromProperties(props);
218        } catch (IOException e) {
219            LOG.error(e.getLocalizedMessage(), e);
220        }
221        return null;
222    }
223
224    /**
225     * Adds a flex bucket definition, consisting of a flex bucket name and a list of paths.<p>
226     *
227     * @param key the flex bucket name
228     * @param values the flex bucket paths
229     */
230    public void add(String key, List<String> values) {
231
232        if (m_frozen) {
233            throw new IllegalStateException("Can not modify frozen CmsFlexBucketConfiguration");
234        }
235        m_bucketNames.add(key);
236        m_bucketPathLists.add(values);
237    }
238
239    /**
240     * Freeze the bucket configuration, i.e. make it non-modifiable.<p>
241     */
242    public void freeze() {
243
244        m_frozen = true;
245    }
246
247    /**
248     * Gets the bucket name for the given bit index.<p>
249     *
250     * @param bitIndex the bit index for the bucket in the bit set representation.<p>
251     *
252     * @return the name of the bucket
253     */
254    public String getBucketName(int bitIndex) {
255
256        if (bitIndex == 0) {
257            return BUCKET_OTHER;
258        } else {
259            String result = null;
260            if ((bitIndex - 1) < m_bucketNames.size()) {
261                result = m_bucketNames.get(bitIndex - 1);
262            }
263            if (result == null) {
264                return "??? " + bitIndex;
265            } else {
266                return result;
267            }
268        }
269    }
270
271    /**
272     * Computes the bucket set for a set of paths based on this configuration.<p>
273     *
274     * The resulting bucket set contains all buckets for which one of the given paths is below the
275     * configured roots of that bucket.
276     *
277     * @param paths a list of root paths
278     *
279     * @return the bucket set for the input paths
280     */
281    public BucketSet getBucketSet(Iterable<String> paths) {
282
283        Set<String> bucketNames = Sets.newHashSet();
284        for (String path : paths) {
285            bucketNames.addAll(getBucketsForPath(path));
286        }
287        if (LOG.isDebugEnabled()) {
288            LOG.debug("Determined bucket set " + bucketNames.toString() + " for path set " + paths);
289        }
290        return new BucketSet(bucketNames);
291    }
292
293    /**
294     * Sets the 'clear all' list, a list of paths for which the complete Flex cache should be cleared if any resource
295     * below them is published.<p>
296     *
297     * @param clearAll a list of paths
298     */
299    public void setClearAll(List<String> clearAll) {
300
301        if (m_frozen) {
302            throw new IllegalStateException("Can not modify frozen CmsFlexBucketConfiguration");
303        }
304        m_clearAll = Collections.unmodifiableList(clearAll);
305    }
306
307    /**
308     * Returns true if for the given publish list, the complete Flex cache should be cleared based on this configuration.<p>
309     *
310     * @param publishedResources a publish list
311     * @return true if the complete Flex cache should be cleared
312     */
313    public boolean shouldClearAll(List<CmsPublishedResource> publishedResources) {
314
315        for (CmsPublishedResource pubRes : publishedResources) {
316            for (String clearPath : m_clearAll) {
317                if (CmsStringUtil.isPrefixPath(clearPath, pubRes.getRootPath())) {
318                    return true;
319                }
320            }
321        }
322        return false;
323    }
324
325    /**
326     * Gets the bucket bit index for the given bucket name.<p>
327     *
328     * @param bucketName a bucket name
329     * @return the bit index for the bucket
330     */
331    int getBucketIndex(String bucketName) {
332
333        if (bucketName.equals(BUCKET_OTHER)) {
334            return 0;
335        }
336        for (int i = 0; i < m_bucketNames.size(); i++) {
337            if (m_bucketNames.get(i).equals(bucketName)) {
338                return 1 + i;
339            }
340        }
341        return -1;
342    }
343
344    /**
345     * Gets the bucket of which the given path is a part.<p>
346     *
347     * @param path a root path
348     * @return the set of buckets for the given path
349     */
350    private Set<String> getBucketsForPath(String path) {
351
352        Set<String> result = Sets.newHashSet();
353        boolean foundBucket = false;
354        for (int i = 0; i < m_bucketNames.size(); i++) {
355            for (String bucketPath : m_bucketPathLists.get(i)) {
356                if (CmsStringUtil.isPrefixPath(bucketPath, path)) {
357                    String bucketName = m_bucketNames.get(i);
358                    result.add(bucketName);
359                    if (!BUCKET_OTHER.equals(bucketName)) {
360                        foundBucket = true;
361                    }
362                }
363            }
364        }
365        if (!foundBucket) {
366            result.add(BUCKET_OTHER);
367        }
368        return result;
369    }
370}