/** * 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.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.geowebcache.GeoWebCacheEnvironment; import org.geowebcache.GeoWebCacheException; import org.geowebcache.GeoWebCacheExtensions; import org.geowebcache.config.ConfigurationException; import org.geowebcache.config.ConfigurationResourceProvider; import org.geowebcache.config.XMLFileResourceProvider; import org.geowebcache.diskquota.storage.LayerQuota; import org.geowebcache.diskquota.storage.Quota; import org.geowebcache.diskquota.storage.StorageUnit; import org.geowebcache.io.GeoWebCacheXStream; import org.geowebcache.layer.TileLayer; import org.geowebcache.layer.TileLayerDispatcher; import org.geowebcache.storage.DefaultStorageFinder; import org.geowebcache.storage.StorageException; import org.geowebcache.util.ApplicationContextProvider; import org.springframework.util.Assert; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.XStreamException; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; /** * Utility class to load the disk quota configuration * <p> * An instance of this class is expected to be configured as a spring bean and then passed over as a * constructor parameter to {@link DiskQuotaMonitor}. * </p> * <p> * When {@link #loadConfig()} is called, a file named {@code geowebcache-diskquota.xml} will be * looked up for in the cache directory as specified by * {@link DefaultStorageFinder#getDefaultPath()}. The configuration file must adhere to the * {@code geowebcache-diskquota.xsd} schema. * </p> * * @author Gabriel Roldan * */ public class ConfigLoader { private static final Log log = LogFactory.getLog(ConfigLoader.class); private static final String CONFIGURATION_FILE_NAME = "geowebcache-diskquota.xml"; private final TileLayerDispatcher tileLayerDispatcher; private final ConfigurationResourceProvider resourceProvider; private final DefaultStorageFinder storageFinder; /** * * @param storageFinder * used to get the location of the cache directory * @param contextProvider * used to look up registered instances of {@link ExpirationPolicy} and to aid in * determining the location of the {@code geowebcache-diskquota.xml} configuration * file * @param tld * used only to validate the presence of a layer at {@link #loadConfig()} and ignore * the layer quota definition if the {@link TileLayer} does not exist * @param quotaStore * storage for layer quota information and paged tiles statistics * @throws IOException */ public ConfigLoader(final DefaultStorageFinder storageFinder, final ApplicationContextProvider contextProvider, final TileLayerDispatcher tld) throws ConfigurationException { this(new XMLFileResourceProvider(CONFIGURATION_FILE_NAME, contextProvider, null, storageFinder), storageFinder, tld); } /** * * * @param resourceProvider provides custom configuration resource * @param storageFinder * used to get the location of the cache directory * @param tld * used only to validate the presence of a layer at {@link #loadConfig()} and ignore * the layer quota definition if the {@link TileLayer} does not exist * @param quotaStore * storage for layer quota information and paged tiles statistics * @throws IOException */ public ConfigLoader(final ConfigurationResourceProvider resourceProvider, final DefaultStorageFinder storageFinder, final TileLayerDispatcher tld) throws ConfigurationException { this.resourceProvider = resourceProvider; this.storageFinder = storageFinder; this.tileLayerDispatcher = tld; } /** * Saves the configuration to the root cache directory * * @param config * @throws IOException * @throws ConfigurationException */ public void saveConfig(DiskQuotaConfig config) throws IOException, ConfigurationException { if (!resourceProvider.hasOutput()) { log.error("Unable to save DiskQuota to resource :" + resourceProvider.getLocation()); return; } XStream xStream = getConfiguredXStream(new GeoWebCacheXStream()); log.debug("Saving disk quota config to " + resourceProvider.getLocation()); try (OutputStream configOut = resourceProvider.out()) { xStream.toXML(config, new OutputStreamWriter(configOut, "UTF-8")); } catch (RuntimeException e) { log.error("Error saving DiskQuota config to file :" + resourceProvider.getLocation()); } } public DiskQuotaConfig loadConfig() throws IOException, ConfigurationException { DiskQuotaConfig quotaConfig = null; if (resourceProvider.hasInput()) { log.info("Quota config is: " + resourceProvider.getLocation()); try (InputStream configIn = resourceProvider.in()) { quotaConfig = loadConfiguration(configIn); if (null == quotaConfig) { throw new ConfigurationException("Couldn't parse configuration file " + resourceProvider.getLocation()); } } catch (IOException | RuntimeException e) { log.error( "Error loading DiskQuota configuration from " + resourceProvider.getLocation() + ": " + e.getMessage() + ". Deferring to a default (disabled) configuration", e); } } else { log.info("DiskQuota configuration is not readable: " + resourceProvider.getLocation()); } if (quotaConfig == null) { quotaConfig = new DiskQuotaConfig(); } // set default values quotaConfig.setDefaults(); validateConfig(quotaConfig); return quotaConfig; } private void validateConfig(DiskQuotaConfig quotaConfig) throws ConfigurationException { int cacheCleanUpFrequency = quotaConfig.getCacheCleanUpFrequency(); if (cacheCleanUpFrequency <= 0) { throw new ConfigurationException("cacheCleanUpFrequency shall be a positive integer"); } TimeUnit cacheCleanUpUnits = quotaConfig.getCacheCleanUpUnits(); if (cacheCleanUpUnits == null) { throw new ConfigurationException( "cacheCleanUpUnits shall be specified. Expected one of SECONDS, MINUTES, HOURS, DAYS. Got null"); } int diskBlockSize = quotaConfig.getDiskBlockSize(); if (diskBlockSize <= 0) { throw new ConfigurationException( "Disk block size shall be specified and be a positive integer"); } int maxConcurrentCleanUps = quotaConfig.getMaxConcurrentCleanUps(); if (maxConcurrentCleanUps <= 0) { throw new ConfigurationException( "maxConcurrentCleanUps shall be specified as a positive integer"); } if (null != quotaConfig.getLayerQuotas()) { for (LayerQuota lq : new ArrayList<LayerQuota>(quotaConfig.getLayerQuotas())) { if (null == lq.getQuota()) { log.info("Configured quota for layer " + lq.getLayer() + " is null. Discarding it to be attached to the global quota"); quotaConfig.remove(lq); continue; } validateLayerQuota(quotaConfig, lq); } } } private void validateLayerQuota(DiskQuotaConfig quotaConfig, LayerQuota lq) throws ConfigurationException { String layer = lq.getLayer(); try { tileLayerDispatcher.getTileLayer(layer); } catch (GeoWebCacheException e) { log.error("LayerQuota configuration error: layer " + layer + " does not exist. Removing quota from runtime configuration.", e); quotaConfig.remove(lq); } final ExpirationPolicy expirationPolicyName = lq.getExpirationPolicyName(); if (expirationPolicyName == null) { // if expiration policy is not defined, then there should be no quota defined either, // as it means the layer is managed by the global expiration policy, if any if (lq.getQuota() != null) { throw new ConfigurationException("Layer " + lq.getLayer() + " has no expiration policy, but does have a quota defined. " + "Either both or neither should be present"); } return; } Quota quota = lq.getQuota(); try { validateQuota(quota); } catch (ConfigurationException e) { log.error("LayerQuota configuration error for layer " + layer + ". Error message is: " + e.getMessage() + ". Quota removed from runtime configuration."); quotaConfig.remove(lq); } } private void validateQuota(Quota quota) throws ConfigurationException { if (quota == null) { throw new IllegalArgumentException("No quota defined"); } BigInteger limit = quota.getBytes(); if (limit.compareTo(BigInteger.ZERO) < 0) { throw new ConfigurationException("Limit shall be >= 0: " + limit + ". " + quota); } log.debug("Quota validated: " + quota); } private DiskQuotaConfig loadConfiguration(final InputStream configStream) throws XStreamException { XStream xstream = getConfiguredXStream(new GeoWebCacheXStream()); Reader reader; try { reader = new InputStreamReader(configStream, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } DiskQuotaConfig fromXML = loadConfiguration(reader, xstream); return fromXML; } public static DiskQuotaConfig loadConfiguration(final Reader reader, XStream xstream) { DiskQuotaConfig fromXML = (DiskQuotaConfig) xstream.fromXML(reader); final GeoWebCacheEnvironment gwcEnvironment = GeoWebCacheExtensions.bean(GeoWebCacheEnvironment.class); if (gwcEnvironment != null && GeoWebCacheEnvironment.ALLOW_ENV_PARAMETRIZATION) { fromXML.setQuotaStore((String) gwcEnvironment.resolveValue(fromXML.getQuotaStore())); } return fromXML; } public static XStream getConfiguredXStream(XStream xs) { // Allow anything that's part of GWC Diskquota // TODO: replace this with a more narrow whitelist xs.allowTypesByWildcard(new String[]{"org.geowebcache.**"}); xs.setMode(XStream.NO_REFERENCES); xs.alias("gwcQuotaConfiguration", DiskQuotaConfig.class); xs.alias("layerQuotas", List.class); xs.alias("LayerQuota", LayerQuota.class); xs.alias("Quota", Quota.class); xs.registerConverter(new QuotaXSTreamConverter()); return xs; } /** * Opens an output stream for a file relative to the cache storage folder * * @param fileNameRelPath * @return * @throws IOException * @throws ConfigurationException */ public OutputStream getStorageOutputStream(String... fileNameRelPath) throws IOException, ConfigurationException { File rootCacheDir = getFileStorageDir(fileNameRelPath); String fileName = fileNameRelPath[fileNameRelPath.length - 1]; File configFile = new File(rootCacheDir, fileName); return new FileOutputStream(configFile); } /** * Opens a stream over an existing file relative to the cache storage folder * * @param fileNameRelPath * the file name relative to the cache storage folder to open * @return * @throws IOException * if {@code fileName} doesn't exist * @throws ConfigurationException */ public InputStream getStorageInputStream(String... fileNameRelPath) throws IOException, ConfigurationException { File rootCacheDir = getFileStorageDir(fileNameRelPath); String fileName = fileNameRelPath[fileNameRelPath.length - 1]; File configFile = new File(rootCacheDir, fileName); return new FileInputStream(configFile); } /** * @param fileNameRelPath * file path relative to the cache storage directory, where the last entry is the * file name and any previous one directory names * @return * @throws StorageException */ private File getFileStorageDir(String[] fileNameRelPath) throws ConfigurationException { File parentDir = getRootCacheDir(); for (int i = 0; i < fileNameRelPath.length - 1; i++) { parentDir = new File(parentDir, fileNameRelPath[i]); } parentDir.mkdirs(); return parentDir; } public File getRootCacheDir() throws ConfigurationException { return new File(storageFinder.getDefaultPath()); } /** * Handles XStream conversion of {@link Quota}s to persist them as * {@code <value>value</value><units>StorageUnit</units>} instead of plain byte count. * * @author groldan * */ private static final class QuotaXSTreamConverter implements Converter { /** * @see com.thoughtworks.xstream.converters.ConverterMatcher#canConvert(java.lang.Class) */ public boolean canConvert(Class type) { return Quota.class.equals(type); } /** * @see com.thoughtworks.xstream.converters.Converter#unmarshal(com.thoughtworks.xstream.io.HierarchicalStreamReader, * com.thoughtworks.xstream.converters.UnmarshallingContext) */ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { Quota quota = new Quota(); reader.moveDown(); String nodeName = reader.getNodeName(); Assert.isTrue("value".equals(nodeName)); String nodevalue = reader.getValue(); double value = Double.parseDouble(nodevalue); reader.moveUp(); reader.moveDown(); nodeName = reader.getNodeName(); Assert.isTrue("units".equals(nodeName)); nodevalue = reader.getValue(); StorageUnit unit = StorageUnit.valueOf(nodevalue); reader.moveUp(); quota.setValue(value, unit); return quota; } /** * @see com.thoughtworks.xstream.converters.Converter#marshal(java.lang.Object, * com.thoughtworks.xstream.io.HierarchicalStreamWriter, * com.thoughtworks.xstream.converters.MarshallingContext) */ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { Quota quota = (Quota) source; BigInteger bytes = quota.getBytes(); StorageUnit unit = StorageUnit.bestFit(bytes); BigDecimal value = unit.fromBytes(bytes); writer.startNode("value"); writer.setValue(value.toString()); writer.endNode(); writer.startNode("units"); writer.setValue(unit.toString()); writer.endNode(); } } }