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}