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}