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