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.ui.apps.cacheadmin; 029 030import org.opencms.file.CmsObject; 031import org.opencms.file.CmsProject; 032import org.opencms.file.CmsResource; 033import org.opencms.file.CmsResourceFilter; 034import org.opencms.loader.CmsImageLoader; 035import org.opencms.loader.I_CmsImageCache; 036import org.opencms.loader.imagecache.CmsImageCacheMaintenanceService; 037import org.opencms.main.CmsException; 038import org.opencms.main.CmsLog; 039import org.opencms.main.OpenCms; 040import org.opencms.ui.A_CmsUI; 041import org.opencms.util.CmsFileUtil; 042import org.opencms.util.CmsStringUtil; 043 044import java.io.File; 045import java.io.FilenameFilter; 046import java.util.ArrayList; 047import java.util.Collections; 048import java.util.HashMap; 049import java.util.List; 050import java.util.Map; 051 052import org.apache.commons.logging.Log; 053 054/** 055 * Helper class for getting information about cached images.<p> 056 */ 057public class CmsImageCacheHolder { 058 059 /** The logger for this class. */ 060 private static final Log LOG = CmsLog.getLog(CmsImageCacheHolder.class); 061 062 /** Maps cache file names and their intermediate names to VFS names for this listing operation. */ 063 private Map<String, String> m_pathToVfsName = new HashMap<String, String>(); 064 065 /**File path map. */ 066 private Map<String, List<CmsVariationBean>> m_filePaths = new HashMap<>(); 067 068 /** Lengths map. */ 069 private Map m_lengths = new HashMap(); 070 071 /** Sizes map. */ 072 private Map m_sizes = new HashMap(); 073 074 /** Variations map. */ 075 private Map m_variations = new HashMap(); 076 077 /**Filter for files (and dictionaries). */ 078 private FilenameFilter m_filter; 079 080 /**Cms Object. */ 081 protected CmsObject m_clonedCms; 082 083 /** 084 * public constructor.<p> 085 * @param search 086 */ 087 public CmsImageCacheHolder(final String search) { 088 089 try { 090 m_clonedCms = getClonedCmsObject(A_CmsUI.getCmsObject()); 091 } catch (CmsException e) { 092 m_clonedCms = A_CmsUI.getCmsObject(); 093 } 094 I_CmsImageCache imageCache = CmsImageLoader.getImageCache(); 095 if (imageCache != null) { 096 readExternalImageCache(imageCache, search); 097 freezeMaps(); 098 return; 099 } 100 if (CmsImageLoader.getImageRepositoryPath() == null) { 101 freezeMaps(); 102 return; 103 } 104 m_filter = new FilenameFilter() { 105 106 public boolean accept(File dir, String name) { 107 108 String spatt = search.replace("*", ""); 109 if (new File(dir, name).isDirectory()) { 110 return true; 111 } 112 113 String fullPath = dir.getAbsolutePath() + "/" + name; 114 115 fullPath = fullPath.substring(CmsImageLoader.getImageRepositoryPath().length() - 1); 116 return getVFSName(m_clonedCms, fullPath).contains(spatt); 117 118 } 119 }; 120 if (!search.startsWith("*") & search.startsWith("/")) { 121 String root = getRootFromPattern(search); 122 if (root.length() > 1) { 123 readAllImagesAndVariations(root.substring(1).replace("*", "")); 124 } else { 125 readAllImagesAndVariations(""); 126 } 127 } else { 128 readAllImagesAndVariations(""); 129 } 130 131 } 132 133 /** 134 * Gets a cache key prefix which can safely be applied before resolving cache keys to VFS paths.<p> 135 * 136 * The file name is deliberately excluded because image cache keys contain hashes before the extension. 137 * 138 * @param search the search expression 139 * @return the cache key prefix 140 */ 141 static String getCacheKeyPrefix(String search) { 142 143 if (CmsStringUtil.isEmptyOrWhitespaceOnly(search) || search.startsWith("*") || !search.startsWith("/")) { 144 return ""; 145 } 146 int firstWildcard = search.indexOf('*'); 147 String fixedPrefix = firstWildcard < 0 ? search : search.substring(0, firstWildcard); 148 int lastSlash = fixedPrefix.lastIndexOf('/'); 149 if (lastSlash <= 0) { 150 return ""; 151 } 152 return fixedPrefix.substring(1, lastSlash + 1); 153 } 154 155 /** 156 * Gets the original VFS path encoded in a current external image cache key.<p> 157 * 158 * @param key the image cache key 159 * @return the original VFS path, or an empty string if the key does not use the expected format 160 */ 161 static String getVfsNameFromExternalCacheKey(String key) { 162 163 if (CmsStringUtil.isEmptyOrWhitespaceOnly(key)) { 164 return ""; 165 } 166 String normalizedKey = CmsStringUtil.substitute(key, "\\", "/"); 167 if (!normalizedKey.startsWith("/")) { 168 normalizedKey = "/" + normalizedKey; 169 } 170 String extension = CmsFileUtil.getExtension(normalizedKey); 171 String nameWithoutExtension = normalizedKey.substring(0, normalizedKey.length() - extension.length()); 172 int pathEnd = nameWithoutExtension.lastIndexOf('/'); 173 int parameterSeparator = nameWithoutExtension.lastIndexOf('_'); 174 if (parameterSeparator <= pathEnd) { 175 return ""; 176 } 177 String parameterHash = nameWithoutExtension.substring(parameterSeparator + 1); 178 if (!isLowerCaseHexHash(parameterHash)) { 179 return ""; 180 } 181 int resourceSeparator = nameWithoutExtension.lastIndexOf('_', parameterSeparator - 1); 182 if (resourceSeparator <= pathEnd) { 183 return ""; 184 } 185 String resourceHash = nameWithoutExtension.substring(resourceSeparator + 1, parameterSeparator); 186 if (!isInteger(resourceHash)) { 187 return ""; 188 } 189 return nameWithoutExtension.substring(0, resourceSeparator) + extension; 190 } 191 192 /** Checks whether a value represents an integer. */ 193 private static boolean isInteger(String value) { 194 195 if (CmsStringUtil.isEmpty(value)) { 196 return false; 197 } 198 int start = value.charAt(0) == '-' ? 1 : 0; 199 if (start == value.length()) { 200 return false; 201 } 202 for (int i = start; i < value.length(); i++) { 203 char character = value.charAt(i); 204 if ((character < '0') || (character > '9')) { 205 return false; 206 } 207 } 208 return true; 209 } 210 211 /** Checks whether a value is a 32 character lower-case hexadecimal hash. */ 212 private static boolean isLowerCaseHexHash(String value) { 213 214 if (value.length() != 32) { 215 return false; 216 } 217 for (int i = 0; i < value.length(); i++) { 218 char character = value.charAt(i); 219 if (!(((character >= '0') && (character <= '9')) || ((character >= 'a') && (character <= 'f')))) { 220 return false; 221 } 222 } 223 return true; 224 } 225 226 /** 227 * Returns all cached images.<p> 228 * 229 * @return a list of root paths 230 */ 231 public List<String> getAllCachedImages() { 232 233 List<String> ret = new ArrayList<String>(m_variations.keySet()); 234 Collections.sort(ret); 235 return ret; 236 } 237 238 /** 239 * Get variations of resource.<p> 240 * 241 * @param resource to get variations for 242 * @return list of CmsVariationBean 243 */ 244 public List<CmsVariationBean> getVariations(String resource) { 245 246 List<CmsVariationBean> ret = m_filePaths.get(resource); 247 if (ret == null) { 248 return new ArrayList<CmsVariationBean>(); 249 } 250 return new ArrayList<CmsVariationBean>(ret); 251 } 252 253 /** 254 * Get the amount of variations for resource.<p> 255 * 256 * @param resource to get variations for 257 * @return amount of variations 258 */ 259 public int getVariationsCount(String resource) { 260 261 return ((List<String>)m_variations.get(resource)).size(); 262 } 263 264 /** 265 * Get name of image in the VFS.<p> 266 * 267 * @param cms CmsObject 268 * @param oName Name of cached image file 269 * @return vfs resource name (root path) 270 */ 271 String getVFSName(CmsObject cms, String oName) { 272 273 oName = CmsStringUtil.substitute(oName, "\\", "/"); 274 if (!oName.startsWith("/")) { 275 oName = "/" + oName; 276 } 277 278 if (m_pathToVfsName.containsKey(oName)) { 279 return m_pathToVfsName.get(oName); 280 } 281 282 String imgName = oName; 283 List<String> attemptedNames = new ArrayList<String>(); 284 while (true) { 285 String path = CmsResource.getParentFolder(imgName); 286 String name = imgName.substring(path.length()); 287 String ext = CmsFileUtil.getExtension(imgName); 288 String nameWoExt = name.substring(0, name.length() - ext.length()); 289 int pos = nameWoExt.lastIndexOf("_"); 290 String newName = path; 291 if (pos >= 0) { 292 newName += nameWoExt.substring(0, pos); 293 } else { 294 newName += nameWoExt; 295 } 296 newName += ext; 297 attemptedNames.add(newName); 298 if (m_pathToVfsName.containsKey(newName)) { 299 String result = m_pathToVfsName.get(newName); 300 cacheVfsName(attemptedNames, oName, result); 301 return result; 302 } 303 try { 304 CmsResource res = cms.readResource(newName, CmsResourceFilter.ALL); 305 String result = res.getRootPath(); 306 cacheVfsName(attemptedNames, oName, result); 307 return result; 308 } catch (Exception e) { 309 // it could be a variation 310 } 311 if (pos < 0) { 312 cacheVfsName(attemptedNames, oName, ""); 313 return ""; 314 } 315 imgName = newName; 316 } 317 } 318 319 /** 320 * Adds a variation description.<p> 321 * 322 * @param vfsName the VFS resource name 323 * @param length the cache entry length 324 */ 325 private void addVariation(String vfsName, long length) { 326 327 List variations = (List)m_variations.get(vfsName); 328 if (variations == null) { 329 variations = new ArrayList(); 330 m_variations.put(vfsName, variations); 331 332 } 333 variations.add(vfsName + " (" + length + " Bytes)"); 334 } 335 336 /** Caches a resolved VFS name for the cache key and all names inspected while resolving it. */ 337 private void cacheVfsName(List<String> attemptedNames, String cacheKey, String vfsName) { 338 339 m_pathToVfsName.put(cacheKey, vfsName); 340 for (String attemptedName : attemptedNames) { 341 m_pathToVfsName.put(attemptedName, vfsName); 342 } 343 } 344 345 /** Freezes the result maps. */ 346 private void freezeMaps() { 347 348 m_variations = Collections.unmodifiableMap(m_variations); 349 m_sizes = Collections.unmodifiableMap(m_sizes); 350 m_lengths = Collections.unmodifiableMap(m_lengths); 351 m_filePaths = Collections.unmodifiableMap(m_filePaths); 352 } 353 354 /** 355 * Clones a CmsObject.<p> 356 * 357 * @param cms the CmsObject to be cloned. 358 * @return a clones CmsObject 359 * @throws CmsException if something goes wrong 360 */ 361 private CmsObject getClonedCmsObject(CmsObject cms) throws CmsException { 362 363 CmsObject clonedCms = OpenCms.initCmsObject(cms); 364 // only online images get caches 365 clonedCms.getRequestContext().setCurrentProject(clonedCms.readProject(CmsProject.ONLINE_PROJECT_ID)); 366 // paths are always root path 367 clonedCms.getRequestContext().setSiteRoot(""); 368 369 return clonedCms; 370 } 371 372 private String getRootFromPattern(String pattern) { 373 374 String res = pattern.substring(0, pattern.lastIndexOf("/")); 375 376 return res; 377 } 378 379 /** 380 * Fille the list m_variations and m_filePaths.<p> 381 */ 382 private void readAllImagesAndVariations(String root) { 383 384 String repositoryPath = CmsImageLoader.getImageRepositoryPath(); 385 if (repositoryPath == null) { 386 return; 387 } 388 File basedir = new File(repositoryPath + root); 389 visitImages(m_clonedCms, basedir); 390 freezeMaps(); 391 392 } 393 394 /** 395 * Reads all entries from an external image cache.<p> 396 * 397 * @param imageCache the external image cache 398 * @param search the search expression 399 */ 400 private void readExternalImageCache(I_CmsImageCache imageCache, String search) { 401 402 String pattern = search.replace("*", ""); 403 String cacheKeyPrefix = getCacheKeyPrefix(search); 404 try { 405 CmsImageCacheMaintenanceService maintenance = CmsImageCacheMaintenanceService.create( 406 imageCache, 407 OpenCms.getImageCacheConfiguration()); 408 imageCache.visitEntries(cacheKeyPrefix, (key, length) -> { 409 String vfsName = getVfsNameFromExternalCacheKey(key); 410 if (CmsStringUtil.isEmptyOrWhitespaceOnly(vfsName)) { 411 vfsName = getVFSName(m_clonedCms, key); 412 } 413 if (CmsStringUtil.isEmptyOrWhitespaceOnly(vfsName)) { 414 vfsName = key.startsWith("/") ? key : "/" + key; 415 } 416 if (vfsName.contains(pattern)) { 417 visitImage(vfsName, new CmsVariationBean(imageCache, maintenance, key, length), length); 418 } 419 }); 420 } catch (Exception e) { 421 LOG.warn("Unable to list the configured image cache.", e); 422 } 423 } 424 425 /** 426 * Visits a single image.<p> 427 * 428 * @param cms CmsObject 429 * @param f a File to be read out 430 */ 431 private void visitImage(CmsObject cms, File f) { 432 433 f.length(); 434 String oName = f.getAbsolutePath().substring(CmsImageLoader.getImageRepositoryPath().length()); 435 oName = getVFSName(cms, oName); 436 437 List<CmsVariationBean> files = m_filePaths.get(oName); 438 if (files == null) { 439 files = new ArrayList<CmsVariationBean>(); 440 m_filePaths.put(oName, files); 441 } 442 files.add(new CmsVariationBean(f.getAbsolutePath())); 443 444 addVariation(oName, f.length()); 445 } 446 447 /** 448 * Adds an external image cache entry.<p> 449 * 450 * @param vfsName the VFS resource name 451 * @param variation the variation bean 452 * @param length the cache entry length 453 */ 454 private void visitImage(String vfsName, CmsVariationBean variation, long length) { 455 456 List<CmsVariationBean> files = m_filePaths.get(vfsName); 457 if (files == null) { 458 files = new ArrayList<CmsVariationBean>(); 459 m_filePaths.put(vfsName, files); 460 } 461 files.add(variation); 462 addVariation(vfsName, length); 463 } 464 465 /** 466 * Visits all cached images in the given directory.<p> 467 * 468 * @param cms the cms context 469 * @param directory the directory to visit 470 */ 471 private void visitImages(CmsObject cms, File directory) { 472 473 if (!directory.canRead() || !directory.isDirectory()) { 474 return; 475 } 476 File[] files = directory.listFiles(m_filter); 477 for (int i = 0; i < files.length; i++) { 478 File f = files[i]; 479 if (f.isDirectory()) { 480 visitImages(cms, f); 481 continue; 482 } 483 visitImage(cms, f); 484 } 485 } 486}