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 GmbH & Co. KG, 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 com.alkacon.simapi.Simapi;
031
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsProject;
034import org.opencms.file.CmsResource;
035import org.opencms.file.CmsResourceFilter;
036import org.opencms.loader.CmsImageLoader;
037import org.opencms.loader.I_CmsImageCache;
038import org.opencms.main.CmsException;
039import org.opencms.main.OpenCms;
040import org.opencms.util.CmsFileUtil;
041import org.opencms.util.CmsStringUtil;
042
043import java.awt.image.BufferedImage;
044import java.io.ByteArrayOutputStream;
045import java.io.File;
046import java.io.IOException;
047import java.util.ArrayList;
048import java.util.Collections;
049import java.util.HashMap;
050import java.util.List;
051import java.util.Map;
052
053/**
054 * Image Cache helper.<p>
055 *
056 * @since 7.0.5
057 */
058public class CmsImageCacheHelper {
059
060    /**File path map. */
061    private Map<String, String> m_filePaths = new HashMap<String, String>();
062
063    /** Lengths map. */
064    private Map m_lengths = new HashMap();
065
066    /** Sizes map. */
067    private Map m_sizes = new HashMap();
068
069    /** Variations map. */
070    private Map m_variations = new HashMap();
071
072    /** The total number of variations. */
073    private int m_variationsCount;
074
075    /** The total size of all variations. */
076    private int m_variationsSize;
077
078    /**
079     * Default constructor.<p>
080     *
081     * @param cms the cms context
082     * @param withVariations if also variations should be read
083     * @param showSize if it is needed to compute the image size
084     * @param statsOnly if only statistical information should be retrieved
085     */
086    public CmsImageCacheHelper(CmsObject cms, boolean withVariations, boolean showSize, boolean statsOnly) {
087
088        init(cms, withVariations, showSize, statsOnly);
089    }
090
091    /**
092     * Returns all cached images.<p>
093     *
094     * @return a list of root paths
095     */
096    public List getAllCachedImages() {
097
098        List ret = new ArrayList(m_variations.keySet());
099        Collections.sort(ret);
100        return ret;
101    }
102
103    /**
104     * Returns the total number of files.<p>
105     *
106     * @return the total number of files
107     */
108    public int getFilesCount() {
109
110        return m_variations.keySet().size();
111    }
112
113    /**
114     * Returns the length of the given image.<p>
115     *
116     * @param imgName the image name
117     *
118     * @return the length of the given image
119     */
120    public String getLength(String imgName) {
121
122        String ret = (String)m_lengths.get(imgName);
123        if (ret == null) {
124            return "";
125        }
126        return ret;
127    }
128
129    /**
130     * Reads the size of a single image.<p>
131     *
132     * @param cms CmsObejct
133     * @param resPath Path of image (uri)
134     * @return a String representation of the dimension of the given image
135     * @throws CmsException if something goes wrong
136     */
137    public String getSingleSize(CmsObject cms, String resPath) throws CmsException {
138
139        CmsResource res = getClonedCmsObject(cms).readResource(resPath);
140        return getSingleSize(cms, res);
141    }
142
143    /**
144     * Returns the size of the given image.<p>
145     *
146     * @param imgName the image name
147     *
148     * @return the size of the given image
149     */
150    public String getSize(String imgName) {
151
152        String ret = (String)m_sizes.get(imgName);
153        if (ret == null) {
154            return "";
155        }
156        return ret;
157    }
158
159    /**
160     * Returns the variations for the given image.<p>
161     *
162     * @param imgName the image name
163     *
164     * @return the variations for the given image
165     */
166    public List getVariations(String imgName) {
167
168        List ret = (List)m_variations.get(imgName);
169        if (ret == null) {
170            return new ArrayList();
171        }
172        Collections.sort(ret);
173        return ret;
174    }
175
176    /**
177     * Returns the total number of variations.<p>
178     *
179     * @return the total number of variations
180     */
181    public int getVariationsCount() {
182
183        return m_variationsCount;
184    }
185
186    /**
187     * Returns the total size of all variations.<p>
188     *
189     * @return the total size of all variations
190     */
191    public int getVariationsSize() {
192
193        return m_variationsSize;
194    }
195
196    /**
197     * Reads out the path of a given image (by its name).<p>
198     *
199     * @param resName name of image
200     * @return path to it on Server
201     */
202    String getFilePath(String resName) {
203
204        return m_filePaths.get(resName);
205    }
206
207    /**
208     * Finds the original VFS image resource for a cache entry name.<p>
209     *
210     * @param cms the CMS context
211     * @param imageName the image cache entry name
212     * @return the original resource, or <code>null</code>
213     */
214    private CmsResource findOriginalResource(CmsObject cms, String imageName) {
215
216        String candidate = imageName;
217        boolean found = false;
218        while (!found) {
219            String path = CmsResource.getParentFolder(candidate);
220            String name = candidate.substring(path.length());
221            String ext = CmsFileUtil.getExtension(candidate);
222            String nameWoExt = name.substring(0, name.length() - ext.length());
223            int pos = nameWoExt.lastIndexOf("_");
224            String newName = path;
225            found = (pos < 0);
226            if (!found) {
227                newName += nameWoExt.substring(0, pos);
228            } else {
229                newName += nameWoExt;
230            }
231            newName += ext;
232            try {
233                return cms.readResource(newName, CmsResourceFilter.ALL);
234            } catch (Exception e) {
235                // it could be a variation
236            }
237            candidate = newName;
238        }
239        return null;
240    }
241
242    /**
243     * Clones a CmsObject.<p>
244     *
245     * @param cms the CmsObject to be cloned.
246     * @return a clones CmsObject
247     * @throws CmsException if something goes wrong
248     */
249    private CmsObject getClonedCmsObject(CmsObject cms) throws CmsException {
250
251        CmsObject clonedCms = OpenCms.initCmsObject(cms);
252        // only online images get caches
253        clonedCms.getRequestContext().setCurrentProject(clonedCms.readProject(CmsProject.ONLINE_PROJECT_ID));
254        // paths are always root path
255        clonedCms.getRequestContext().setSiteRoot("");
256
257        return clonedCms;
258    }
259
260    /**
261     * Reads image dimensions from an external image cache entry.<p>
262     *
263     * @param imageCache the external image cache
264     * @param key the image cache key
265     * @return the formatted image dimensions, or an empty string
266     */
267    private String getImageDimensions(I_CmsImageCache imageCache, String key) {
268
269        try {
270            ByteArrayOutputStream out = new ByteArrayOutputStream();
271            imageCache.writeTo(key, out);
272            BufferedImage image = Simapi.read(out.toByteArray());
273            return "" + image.getWidth() + " x " + image.getHeight() + "px";
274        } catch (Exception e) {
275            return "";
276        }
277    }
278
279    /**
280     * Reads the size of a single image.<p>
281     *
282     * @param cms CmsObejct
283     * @param res CmsResource to be read
284     * @return a String representation of the dimension of the given image
285     */
286    private String getSingleSize(CmsObject cms, CmsResource res) {
287
288        try {
289            BufferedImage img = Simapi.read(cms.readFile(res).getContents());
290            return "" + img.getWidth() + " x " + img.getHeight() + "px";
291        } catch (CmsException | IOException e) {
292            return "";
293        }
294    }
295
296    /**
297     * Reads all cached images.<p>
298     *
299     * @param cms the cms context
300     * @param withVariations if also variations should be read
301     * @param showSize if it is needed to compute the image size
302     * @param statsOnly if only statistical information should be retrieved
303     */
304    private void init(CmsObject cms, boolean withVariations, boolean showSize, boolean statsOnly) {
305
306        String repositoryPath = CmsImageLoader.getImageRepositoryPath();
307        try {
308            CmsObject clonedCms = getClonedCmsObject(cms);
309            I_CmsImageCache imageCache = CmsImageLoader.getImageCache();
310            if (imageCache != null) {
311                imageCache.visitEntries(
312                    (
313                        key,
314                        length) -> visitImage(clonedCms, imageCache, key, length, withVariations, showSize, statsOnly));
315            } else if (repositoryPath != null) {
316                visitImages(clonedCms, new File(repositoryPath), withVariations, showSize, statsOnly);
317            }
318        } catch (Exception e) {
319            // should never happen
320        }
321        m_variations = Collections.unmodifiableMap(m_variations);
322        m_sizes = Collections.unmodifiableMap(m_sizes);
323        m_lengths = Collections.unmodifiableMap(m_lengths);
324    }
325
326    /**
327     * Visits a single image.<p>
328     *
329     * @param cms CmsObject
330     * @param f a File to be read out
331     * @param withVariations boolean
332     * @param showSize boolean
333     * @param statsOnly boolean
334     */
335    private void visitImage(CmsObject cms, File f, boolean withVariations, boolean showSize, boolean statsOnly) {
336
337        m_variationsCount++;
338        m_variationsSize += f.length();
339        String oName = f.getAbsolutePath().substring(CmsImageLoader.getImageRepositoryPath().length());
340        oName = CmsStringUtil.substitute(oName, "\\", "/");
341        if (!oName.startsWith("/")) {
342            oName = "/" + oName;
343        }
344        CmsResource res = findOriginalResource(cms, oName);
345
346        if (res != null) {
347            oName = res.getRootPath();
348        }
349        m_filePaths.put(oName, f.getAbsolutePath());
350        List variations = (List)m_variations.get(oName);
351        if (variations == null) {
352            variations = new ArrayList();
353            m_variations.put(oName, variations);
354            if (statsOnly) {
355                return;
356            }
357            if (res != null) {
358                m_lengths.put(oName, "" + res.getLength() + " Bytes");
359                if (showSize) {
360                    m_sizes.put(oName, getSingleSize(cms, res));
361                }
362            } else {
363                m_lengths.put(oName, "" + f.length() + " Bytes");
364                if (showSize) {
365                    try {
366                        BufferedImage img = Simapi.read(f);
367                        m_sizes.put(oName, "" + img.getWidth() + " x " + img.getHeight() + "px");
368                    } catch (Throwable e) {
369                        // ignore
370                    }
371                }
372            }
373        }
374        if (!withVariations) {
375            return;
376        }
377        oName += " (";
378        if (showSize) {
379            try {
380                BufferedImage img = Simapi.read(f);
381                oName += "" + img.getWidth() + " x " + img.getHeight() + "px - ";
382            } catch (Throwable e) {
383                // ignore
384            }
385        }
386        oName += f.length() + " Bytes)";
387        variations.add(oName);
388    }
389
390    /**
391     * Visits a single image in an external image cache.<p>
392     *
393     * @param cms the CMS context
394     * @param imageCache the external image cache
395     * @param key the image cache key
396     * @param length the image cache entry length
397     * @param withVariations whether variations should be collected
398     * @param showSize whether image dimensions should be read
399     * @param statsOnly whether only statistics should be collected
400     */
401    private void visitImage(
402        CmsObject cms,
403        I_CmsImageCache imageCache,
404        String key,
405        long length,
406        boolean withVariations,
407        boolean showSize,
408        boolean statsOnly) {
409
410        m_variationsCount++;
411        m_variationsSize += length;
412        String cacheName = CmsStringUtil.substitute(key, "\\", "/");
413        if (!cacheName.startsWith("/")) {
414            cacheName = "/" + cacheName;
415        }
416        CmsResource res = findOriginalResource(cms, cacheName);
417        String imageName = (res != null) ? res.getRootPath() : cacheName;
418        m_filePaths.put(imageName, key);
419        List variations = (List)m_variations.get(imageName);
420        if (variations == null) {
421            variations = new ArrayList();
422            m_variations.put(imageName, variations);
423            if (statsOnly) {
424                return;
425            }
426            if (res != null) {
427                m_lengths.put(imageName, "" + res.getLength() + " Bytes");
428                if (showSize) {
429                    m_sizes.put(imageName, getSingleSize(cms, res));
430                }
431            } else {
432                m_lengths.put(imageName, "" + length + " Bytes");
433                if (showSize) {
434                    m_sizes.put(imageName, getImageDimensions(imageCache, key));
435                }
436            }
437        }
438        if (!withVariations) {
439            return;
440        }
441        StringBuilder variation = new StringBuilder(imageName).append(" (");
442        if (showSize) {
443            String dimensions = getImageDimensions(imageCache, key);
444            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(dimensions)) {
445                variation.append(dimensions).append(" - ");
446            }
447        }
448        variation.append(length).append(" Bytes)");
449        variations.add(variation.toString());
450    }
451
452    /**
453    * Visits all cached images in the given directory.<p>
454    *
455    * @param cms the cms context
456    * @param directory the directory to visit
457    * @param withVariations if also variations should be read
458    * @param showSize if it is needed to compute the image size
459    * @param statsOnly if only statistical information should be retrieved
460    */
461    private void visitImages(
462        CmsObject cms,
463        File directory,
464        boolean withVariations,
465        boolean showSize,
466        boolean statsOnly) {
467
468        if (!directory.canRead() || !directory.isDirectory()) {
469            return;
470        }
471        File[] files = directory.listFiles();
472        for (int i = 0; i < files.length; i++) {
473            File f = files[i];
474            if (f.isDirectory()) {
475                visitImages(cms, f, withVariations, showSize, statsOnly);
476                continue;
477            }
478            visitImage(cms, f, withVariations, showSize, statsOnly);
479        }
480    }
481
482}