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}