/* (c) 2014 - 2015 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.gwc.layer; import static com.google.common.base.Throwables.propagate; import static com.google.common.base.Throwables.propagateIfInstanceOf; import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; import org.geoserver.config.AsynchResourceIterator; import org.geoserver.platform.GeoServerResourceLoader; import org.geoserver.platform.resource.Resource; import org.geoserver.platform.resource.Resource.Type; import org.geoserver.platform.resource.Resources; import org.geoserver.platform.resource.Resources.ExtensionFilter; import org.geoserver.util.Filter; import org.geotools.util.logging.Logging; import org.geowebcache.config.ContextualConfigurationProvider.Context; import org.geowebcache.config.XMLConfiguration; import org.geowebcache.storage.blobstore.file.FilePathUtils; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.thoughtworks.xstream.XStream; public class DefaultTileLayerCatalog implements TileLayerCatalog { private static final Logger LOGGER = Logging.getLogger(DefaultTileLayerCatalog.class); private static final String LAYERINFO_DIRECTORY = "gwc-layers"; private Map<String, GeoServerTileLayerInfo> layersById; /** * View of layer ids by name */ private Map<String, String> layersByName; private final XStream serializer; private final GeoServerResourceLoader resourceLoader; private final String baseDirectory; private volatile boolean initialized; public DefaultTileLayerCatalog(GeoServerResourceLoader resourceLoader, XMLConfiguration xmlPersisterFactory) throws IOException { this(resourceLoader, // this configures security back in GWC, no point in using SecureXStream here xmlPersisterFactory.getConfiguredXStreamWithContext(new XStream(), Context.PERSIST)); } DefaultTileLayerCatalog(GeoServerResourceLoader resourceLoader, XStream configuredXstream) throws IOException { this.resourceLoader = resourceLoader; this.baseDirectory = LAYERINFO_DIRECTORY; this.layersByName = new ConcurrentHashMap<>(); this.layersById = new ConcurrentHashMap<>(); this.initialized = false; // setup xstream security for local classes this.serializer = configuredXstream; this.serializer.allowTypeHierarchy(GeoServerTileLayerInfo.class); this.serializer.allowTypeHierarchy(SortedSet.class); } @Override public void reset() { layersById.clear(); layersByName.clear(); this.initialized = false; } @Override public void initialize() { reset(); Resource baseDir = resourceLoader.get(baseDirectory); LOGGER.info("GeoServer TileLayer store base directory is: " + baseDir.path()); LOGGER.info("Loading tile layers from " + baseDir.path()); ExtensionFilter xmlFilter = new Resources.ExtensionFilter("XML"); baseDir.list().parallelStream().filter(r -> xmlFilter.accept(r)).forEach(res -> { GeoServerTileLayerInfoImpl info; try { info = depersist(res); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Error depersisting tile layer information from file " + res.name(), e); return; } layersByName.put(info.getName(), info.getId()); layersById.put(info.getId(), info); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("Loaded tile layer '" + info.getName() + "'"); } }); this.initialized = true; } @Override public GeoServerTileLayerInfo getLayerById(final String id) { checkInitialized(); GeoServerTileLayerInfo layer = layersById.get(id); return layer == null ? null : layer.clone(); } private synchronized void checkInitialized() { if (!initialized) { initialize(); } } @Override public GeoServerTileLayerInfo getLayerByName(String layerName) { checkInitialized(); String id = layersByName.get(layerName); if (id == null) { return null; } return getLayerById(id); } @Override public Set<String> getLayerIds() { checkInitialized(); return ImmutableSet.copyOf(layersById.keySet()); } @Override public boolean exists(String layerId) { checkInitialized(); return layersById.containsKey(layerId); } @Override public Set<String> getLayerNames() { checkInitialized(); return ImmutableSet.copyOf(layersByName.keySet()); } @Override public GeoServerTileLayerInfo delete(final String tileLayerId) { checkInitialized(); try { GeoServerTileLayerInfo info = getLayerById(tileLayerId); if (info != null) { Resource file = getFile(tileLayerId); layersById.remove(tileLayerId); layersByName.remove(info.getName()); file.delete(); } return info; } catch (IOException notFound) { LOGGER.log(Level.FINEST, "Deleting " + tileLayerId, notFound); return null; } } @Override public GeoServerTileLayerInfo save(final GeoServerTileLayerInfo newValue) { checkInitialized(); GeoServerTileLayerInfoImpl oldValue = null; final String tileLayerId = newValue.getId(); Preconditions.checkNotNull(tileLayerId); try { try { oldValue = loadInternal(tileLayerId); } catch (FileNotFoundException ignore) { // ok } catch (Exception other) { throw propagate(other); } if (oldValue == null) { final String duplicateNameId = layersByName.get(newValue.getName()); if (null != duplicateNameId) { throw new IllegalArgumentException("TileLayer with same name already exists: " + newValue.getName() + ": <" + duplicateNameId + ">"); } } else { layersByName.remove(oldValue.getName()); } persist(newValue); layersByName.put(newValue.getName(), newValue.getId()); layersById.put(newValue.getId(), newValue.clone()); } catch (Exception e) { if (e instanceof ExecutionException) { propagate(((ExecutionException) e).getCause()); } propagate(e); } return oldValue; } private void persist(GeoServerTileLayerInfo real) throws IOException { final String tileLayerId = real.getId(); Resource file = getFile(tileLayerId); boolean cleanup = false; if (file.getType() == Type.UNDEFINED) { cleanup = true; } final Resource tmp = file.parent().get(file.name() + ".tmp"); try { final Writer writer = new OutputStreamWriter(tmp.out(), "UTF-8"); try { serializer.toXML(real, writer); } finally { writer.close(); } } catch (Exception e) { tmp.delete(); if (cleanup) { file.delete(); } propagateIfInstanceOf(e, IOException.class); throw propagate(e); } // sanity check try { depersist(tmp); } catch (Exception e) { LOGGER.log(Level.WARNING, "Persisted version of tile layer " + real.getName() + " can't be loaded back", e); propagateIfInstanceOf(e, IOException.class); throw propagate(e); } rename(tmp, file); } private GeoServerTileLayerInfoImpl loadInternal(final String tileLayerId) throws FileNotFoundException, IOException { final Resource file = getFile(tileLayerId); if (file.getType() == Type.UNDEFINED) { throw new FileNotFoundException(tileLayerId); } return depersist(file); } private Resource getFile(final String tileLayerId) throws IOException { final String fileName = FilePathUtils.filteredLayerName(tileLayerId) + ".xml"; final Resource base = resourceLoader.get(baseDirectory); return base.get(fileName); } private GeoServerTileLayerInfoImpl depersist(final Resource res) throws IOException { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Depersisting GeoServerTileLayerInfo from " + res.path()); } GeoServerTileLayerInfoImpl info; try(Reader reader = new InputStreamReader(new ByteArrayInputStream(res.getContents()), "UTF-8")) { info = (GeoServerTileLayerInfoImpl) serializer.fromXML(reader); } return info; } private GeoServerTileLayerInfoImpl depersist(final byte[] contents) throws IOException { GeoServerTileLayerInfoImpl info; try(Reader reader = new InputStreamReader(new ByteArrayInputStream(contents), "UTF-8")) { info = (GeoServerTileLayerInfoImpl) serializer.fromXML(reader); } return info; } private void rename(Resource source, Resource dest) throws IOException { // same resource? Do nothing if (source.equals(dest)) return; // different resource boolean win = System.getProperty("os.name").startsWith("Windows"); if (win && Resources.exists(dest)) { // windows does not do atomic renames, and can not rename a file if the dest file // exists if (!dest.delete()) { throw new IOException("Could not delete: " + dest.path()); } source.renameTo(dest); } else { source.renameTo(dest); } } @Override public String getLayerId(String layerName) { checkInitialized(); return layersByName.get(layerName); } @Override public String getLayerName(String layerId) { checkInitialized(); return layersById.get(layerId).getName(); } }