org.geowebcache.diskquota.LayerCacheInfoBuilder.java Source code

Java tutorial

Introduction

Here is the source code for org.geowebcache.diskquota.LayerCacheInfoBuilder.java

Source

/**
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * @author Gabriel Roldan (OpenGeo) 2010
 *  
 */
package org.geowebcache.diskquota;

import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geowebcache.diskquota.storage.LayerQuota;
import org.geowebcache.diskquota.storage.Quota;
import org.geowebcache.diskquota.storage.TilePage;
import org.geowebcache.diskquota.storage.TileSet;
import org.geowebcache.grid.GridSubset;
import org.geowebcache.layer.TileLayer;
import org.geowebcache.mime.MimeException;
import org.geowebcache.mime.MimeType;
import org.geowebcache.storage.blobstore.file.FilePathUtils;
import org.geowebcache.util.FileUtils;

/**
 * Gathers information about the cache of a layer, such as its size and available {@link TilePage}s.
 * 
 * @author groldan
 */
final class LayerCacheInfoBuilder {

    private static final Log log = LogFactory.getLog(LayerCacheInfoBuilder.class);

    private final File rootCacheDir;

    private final ExecutorService threadPool;

    private final Map<String, List<Future<ZoomLevelVisitor.Stats>>> perLayerRunningTasks;

    private final QuotaUpdatesMonitor quotaUsageMonitor;

    private boolean closed = false;

    public LayerCacheInfoBuilder(final File rootCacheDir, final ExecutorService threadPool,
            QuotaUpdatesMonitor quotaUsageMonitor) {
        this.rootCacheDir = rootCacheDir;
        this.threadPool = threadPool;
        this.quotaUsageMonitor = quotaUsageMonitor;
        this.perLayerRunningTasks = new HashMap<String, List<Future<ZoomLevelVisitor.Stats>>>();
    }

    /**
     * Asynchronously collects cache usage information for the given {@code tileLayer} into the
     * given {@code layerQuota} by using the provided {@link ExecutorService} at construction time.
     * <p>
     * This method discards any {@link LayerQuota#getUsedQuota() used quota} information available
     * for {@code layerQuota} and updates it by collecting the usage information for the layer.
     * </p>
     * <p>
     * In addition to collecting the cache usage information for the layer, the {@code layerQuota}'s
     * {@link ExpirationPolicy expiration policy} will be given the opportunity to gather any
     * additional information by calling the
     * {@link ExpirationPolicy#createInfoFor(LayerQuota, String, long[], File)
     * createInforFor(layerQuota, gridSetId, tileXYZ, file)} method for each available tile on the
     * layer's cache.
     * </p>
     * <p>
     * Note the cache information gathering is performed asynchronously and hence this method
     * returns immediately. To check whether the information collect for a given layer has finished
     * use the {@link #isRunning(String) isRunning(layerName)} method.
     * </p>
     * 
     * @param tileLayer
     */
    public void buildCacheInfo(final TileLayer tileLayer) {

        final String layerName = tileLayer.getName();
        final String layerDirName = FilePathUtils.filteredLayerName(layerName);

        final File layerDir = new File(rootCacheDir, layerDirName);

        if (!layerDir.exists()) {
            return;
        }

        perLayerRunningTasks.put(layerName, new ArrayList<Future<ZoomLevelVisitor.Stats>>());

        final Set<TileSet> onDiskTileSets = findOnDiskTileSets(tileLayer, layerDir);

        for (TileSet tileSet : onDiskTileSets) {
            final String gridSetId = tileSet.getGridsetId();
            // final String blobFormat = tileSet.getBlobFormat();
            final String parametersId = tileSet.getParametersId();
            final GridSubset gs = tileLayer.getGridSubset(gridSetId);
            final int zoomStart = gs.getZoomStart();
            final int zoomStop = gs.getZoomStop();

            for (int zoomLevel = zoomStart; zoomLevel <= zoomStop && !closed; zoomLevel++) {
                String gridsetZLevelParamsDirName;
                gridsetZLevelParamsDirName = FilePathUtils.gridsetZoomLevelDir(gridSetId, zoomLevel);
                if (parametersId != null) {
                    gridsetZLevelParamsDirName += "_" + parametersId;
                }
                final File gridsetZLevelDir = new File(layerDir, gridsetZLevelParamsDirName);

                if (gridsetZLevelDir.exists()) {
                    ZoomLevelVisitor cacheInfoBuilder;
                    cacheInfoBuilder = new ZoomLevelVisitor(layerName, gridsetZLevelDir, gridSetId, zoomLevel,
                            parametersId, quotaUsageMonitor);

                    Future<ZoomLevelVisitor.Stats> cacheTask;
                    cacheTask = threadPool.submit(cacheInfoBuilder);

                    perLayerRunningTasks.get(layerName).add(cacheTask);
                    log.debug("Submitted background task to gather cache info for '" + layerName + "'/" + gridSetId
                            + "/" + zoomLevel);
                }
            }
        }
    }

    private Set<TileSet> findOnDiskTileSets(final TileLayer tileLayer, final File layerDir) {

        final String layerName = tileLayer.getName();
        final Set<String> griSetNames = tileLayer.getGridSubsets();
        Set<TileSet> foundTileSets = new HashSet<TileSet>();
        for (String gridSetName : griSetNames) {
            final String gridSetDirPrefix = FilePathUtils.filteredGridSetId(gridSetName);
            FileFilter prefixFilter = new FileFilter() {
                public boolean accept(File pathname) {
                    if (!pathname.isDirectory()) {
                        return false;
                    }
                    return pathname.getName().startsWith(gridSetDirPrefix + "_");
                }
            };
            File[] thisGridSetDirs = layerDir.listFiles(prefixFilter);
            for (File directory : thisGridSetDirs) {
                // <Filtered gridset id><_zoom level>[_<parametersId>]
                final String dirName = directory.getName();
                final String zlevelAndParamId = dirName.substring(1 + gridSetDirPrefix.length());
                final String[] parts = zlevelAndParamId.split("_");

                final String gridsetId = gridSetName;
                // we don't care here.. format should be part of the top level directory name
                final String blobFormat = null;
                String parametersId = null;
                if (parts.length == 2) {
                    parametersId = parts[1];
                }
                TileSet tileSet = new TileSet(layerName, gridsetId, blobFormat, parametersId);
                foundTileSets.add(tileSet);
            }
        }
        return foundTileSets;
    }

    /**
     * Builds the cache information for a single layer/gridsetId/parametersId/zoomLevel combo
     * 
     * @author groldan
     * 
     */
    private final class ZoomLevelVisitor implements FileFilter, Callable<ZoomLevelVisitor.Stats> {

        private final String gridSetId;

        private int tileZ;

        private final File zoomLevelPath;

        private Stats stats;

        private final QuotaUpdatesMonitor quotaUsageMonitor;

        private final String layerName;

        private final String parametersId;

        private class Stats {
            long runTimeMillis;

            long numTiles;

            Quota collectedQuota = new Quota();
        }

        public ZoomLevelVisitor(final String layerName, final File zoomLevelPath, final String gridsetId,
                final int zoomLevel, String parametersId, final QuotaUpdatesMonitor quotaUsageMonitor) {
            this.layerName = layerName;
            this.zoomLevelPath = zoomLevelPath;
            this.gridSetId = gridsetId;
            this.parametersId = parametersId;
            this.quotaUsageMonitor = quotaUsageMonitor;
            this.tileZ = zoomLevel;
            this.stats = new Stats();
        }

        /**
         * @see java.util.concurrent.Callable#call()
         */
        public Stats call() throws Exception {
            final String zLevelKey = layerName + "'/" + gridSetId + "/paramId:"
                    + (parametersId == null ? "default" : parametersId) + "/zlevel:" + tileZ;
            try {
                log.debug("Gathering cache information for '" + zLevelKey);
                stats.numTiles = 0L;
                stats.runTimeMillis = 0L;
                long runTime = System.currentTimeMillis();
                FileUtils.traverseDepth(zoomLevelPath, this);
                runTime = System.currentTimeMillis() - runTime;
                stats.runTimeMillis = runTime;
            } catch (TraversalCanceledException cancel) {
                log.debug("Gathering cache information for " + zLevelKey + " was canceled.");
                return null;
            } catch (Exception e) {
                e.printStackTrace();
                throw (e);
            }
            log.debug("Cache information for " + zLevelKey + " collected in " + stats.runTimeMillis / 1000D
                    + "s. Counted " + stats.numTiles + " tiles for a storage space of "
                    + stats.collectedQuota.toNiceString());
            return stats;
        }

        /**
         * @see java.io.FileFilter#accept(java.io.File)
         */
        public boolean accept(final File file) {
            if (closed) {
                throw new TraversalCanceledException();
            }
            if (file.isDirectory()) {
                log.trace("Processing files in " + file.getAbsolutePath());
                return true;
            }

            final long length = file.length();

            // we know path is a direct child of processingDir and represents a tile file...
            final String path = file.getPath();

            final int fileNameIdx = 1 + path.lastIndexOf(File.separatorChar);
            final int coordSepIdx = path.lastIndexOf('_');
            final int dotIdx = path.lastIndexOf('.');
            final String extension = FilenameUtils.getExtension(file.getName());
            String blobFormat;
            try {
                blobFormat = MimeType.createFromExtension(extension).getFormat();
            } catch (MimeException e) {
                throw new RuntimeException(e);
            }
            final long x = Long.valueOf(path.substring(fileNameIdx, coordSepIdx));
            final long y = Long.valueOf(path.substring(1 + coordSepIdx, dotIdx));

            this.quotaUsageMonitor.tileStored(layerName, gridSetId, blobFormat, parametersId, x, y, (int) tileZ,
                    length);
            stats.numTiles++;
            stats.collectedQuota.addBytes(length);
            return true;
        }

        /**
         * Used to brute-force cancel a cache inspection (as InterruptedException is checked and
         * hence can't use it in accept(File) above
         * 
         * @author groldan
         * 
         */
        private class TraversalCanceledException extends RuntimeException {
            private static final long serialVersionUID = 1L;
            // doesn't need a body
        }
    }

    /**
     * Returns whether cache information is still being gathered for the layer named after
     * {@code layerName}.
     * 
     * @param layerName
     * @return {@code true} if the cache information gathering for {@code layerName} is not finished
     */
    public boolean isRunning(String layerName) {
        try {
            List<Future<ZoomLevelVisitor.Stats>> layerTasks = perLayerRunningTasks.get(layerName);
            if (layerTasks == null) {
                return false;
            }

            int numRunning = 0;
            Future<ZoomLevelVisitor.Stats> future;
            for (Iterator<Future<ZoomLevelVisitor.Stats>> it = layerTasks.iterator(); it.hasNext();) {
                future = it.next();
                if (future.isDone()) {
                    it.remove();
                } else {
                    numRunning++;
                }
            }
            return numRunning > 0;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    public void shutDown() {
        this.closed = true;
        this.threadPool.shutdownNow();
    }
}