/** * 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(); } }