/* (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.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static org.geoserver.gwc.GWC.tileLayerName; import java.io.IOException; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.httpclient.util.LangUtils; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.CatalogException; import org.geoserver.catalog.CatalogInfo; import org.geoserver.catalog.CoverageInfo; import org.geoserver.catalog.FeatureTypeInfo; import org.geoserver.catalog.LayerGroupHelper; import org.geoserver.catalog.LayerGroupInfo; import org.geoserver.catalog.LayerInfo; import org.geoserver.catalog.MetadataMap; import org.geoserver.catalog.NamespaceInfo; import org.geoserver.catalog.Predicates; import org.geoserver.catalog.PublishedType; import org.geoserver.catalog.ResourceInfo; import org.geoserver.catalog.StyleInfo; import org.geoserver.catalog.WMSLayerInfo; import org.geoserver.catalog.WorkspaceInfo; import org.geoserver.catalog.event.CatalogAddEvent; import org.geoserver.catalog.event.CatalogListener; import org.geoserver.catalog.event.CatalogModifyEvent; import org.geoserver.catalog.event.CatalogPostModifyEvent; import org.geoserver.catalog.event.CatalogRemoveEvent; import org.geoserver.catalog.util.CloseableIterator; import org.geoserver.gwc.GWC; import org.geoserver.gwc.config.GWCConfig; import org.geotools.util.logging.Logging; import org.geowebcache.filter.parameters.StringParameterFilter; import org.geowebcache.grid.GridSetBroker; import org.geowebcache.layer.TileLayer; import org.geowebcache.storage.StorageBroker; import com.google.common.base.Objects; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; /** * Listens to {@link Catalog} layers added/removed events and adds/removes * {@link GeoServerTileLayer}s to/from the {@link CatalogConfiguration} * <p> * Handles the following cases: * <ul> * <li><b>Layer added</b>: a {@link LayerInfo} or {@link LayerGroupInfo} has been added. A * {@link GeoServerTileLayer} is {@link CatalogConfiguration#createLayer is created} with the * {@link GWCConfig default settings} only if the integrated GWC configuration is set to * {@link GWCConfig#isCacheLayersByDefault() cache layers by default}.</li> * <li><b>Layer removed</b>: a {@code LayerInfo} or {@code LayerGroupInfo} has been removed. GWC is * instructed to remove the layer, deleting it's cache and any other associated information * completely (for example, the disk quota information for the layer is also deleted and the global * usage updated accordingly). * <li><b>Layer renamed</b>: a {@link LayerInfo} or {@link LayerGroupInfo} has been renamed. GWC is * {@link StorageBroker#rename instructed to rename} the corresponding tile layer preserving the * cache and any other information (usage statistics, disk quota usage, etc).</li> * <li><b>Workspace renamed</b>: a {@link WorkspaceInfo} as been renamed. GWC is * {@link StorageBroker#rename instructed to rename} all the corresponding tile layer associated to * the workspace, preserving the cache and any other information (usage statistics, disk quota usage, etc).</li> * <li><b>Namespace changed</b>: a {@link ResourceInfo} has been assigned to a different * {@link NamespaceInfo namespace}. As the GWC tile layers are named after the resource's * {@link ResourceInfo#prefixedName() prefixed name} and not only after the * {@link LayerInfo#getName()} (at least until GeoServer separates out data from publication - the * famous data/publish split), GWC is instructed to rename the layer preserving the cache and any * other information for the layer.</li> * <li><b>LayerGroupInfo modified</b>: either the {@link LayerGroupInfo#layers() layers} or * {@link LayerGroupInfo#styles() styles} changed for a {@code LayerGroupInfo}. It's cache is * truncated.</li> * <li><b>LayerInfo default style replaced</b>: a {@code LayerInfo} has been assigned a different * {@link LayerInfo#getDefaultStyle() default style}. The corresponding tile layer's cache is * truncated for the default style.</li> * <li><b>LayerInfo alternate styles changed</b> the set of a {@code LayerInfo}'s * {@link LayerInfo#getStyles() alternate styles} has been modified. For any added style, if the * {@link GeoServerTileLayer} is configured to {@link GeoServerTileLayerInfo#isAutoCacheStyles() * automatically cache all styles}, the style name is added to the set of * {@link GeoServerTileLayerInfo#cachedStyles() cached styles}. For any <b>removed</b> style, if it * was one of the {@link GeoServerTileLayerInfo#cachedStyles() cached styles}, the layer's cache for * that style is truncated, and it's removed from the tile layer's set of cached styles. * Subsequently, the {@link GeoServerTileLayer} will create a {@link StringParameterFilter "STYLES" * parameter filter} for all the cached styles on demand</li> * </ul> * </p> * * @author Arne Kepp * @author Gabriel Roldan */ public class CatalogLayerEventListener implements CatalogListener { private static Logger log = Logging.getLogger(CatalogLayerEventListener.class); private final GWC mediator; private final Catalog catalog; /** * Holds the CatalogModifyEvent from {@link #handleModifyEvent} to be taken after the change was * applied to the {@link Catalog} at {@link #handlePostModifyEvent} and check whether it is * necessary to perform any action on the cache based on the changed properties */ private static ThreadLocal<CatalogModifyEvent> PRE_MODIFY_EVENT = new ThreadLocal<CatalogModifyEvent>(); private static ThreadLocal<GeoServerTileLayerInfo> PRE_MODIFY_TILELAYER = new ThreadLocal<GeoServerTileLayerInfo>(); public CatalogLayerEventListener(final GWC mediator, Catalog catalog) { this.mediator = mediator; this.catalog = catalog; } /** * If either a {@link LayerInfo} or {@link LayerGroupInfo} has been added to the {@link Catalog} * , create a corresponding GWC TileLayer depending on the value of * {@link GWCConfig#isCacheLayersByDefault()}. * * @see org.geoserver.catalog.event.CatalogListener#handleAddEvent * @see GWC#createLayer(LayerInfo) * @see GWC#createLayer(LayerGroupInfo) */ public void handleAddEvent(CatalogAddEvent event) throws CatalogException { GWCConfig config = mediator.getConfig(); boolean sane = config.isSane(); boolean cacheLayersByDefault = config.isCacheLayersByDefault(); if (!cacheLayersByDefault) { return; } if (!sane) { log.info("Ignoring auto-creation of tile layer for " + event.getSource() + ": global gwc settings are not sane"); } Object obj = event.getSource(); // We only handle layers here. Layer groups are initially empty if (obj instanceof LayerInfo) { log.finer("Handling add event: " + obj); LayerInfo layerInfo = (LayerInfo) obj; createTileLayer(layerInfo); } else if (obj instanceof LayerGroupInfo) { LayerGroupInfo lgi = (LayerGroupInfo) obj; createTileLayer(lgi); } } /** * LayerInfo has been created, add a matching {@link GeoServerTileLayer} * * @see CatalogLayerEventListener#handleAddEvent * @see GWC#add(GeoServerTileLayer) */ void createTileLayer(final LayerInfo layerInfo) { GWCConfig defaults = mediator.getConfig(); if (defaults.isSane() && defaults.isCacheLayersByDefault()) { GridSetBroker gridSetBroker = mediator.getGridSetBroker(); GeoServerTileLayer tileLayer = new GeoServerTileLayer(layerInfo, defaults, gridSetBroker); mediator.add(tileLayer); } } /** * LayerGroupInfo has been created, add a matching {@link GeoServerTileLayer} * * @see CatalogLayerEventListener#handleAddEvent * @see GWC#add(GeoServerTileLayer) */ public void createTileLayer(LayerGroupInfo lgi) { GWCConfig defaults = mediator.getConfig(); GridSetBroker gridSetBroker = mediator.getGridSetBroker(); GeoServerTileLayer tileLayer = new GeoServerTileLayer(lgi, defaults, gridSetBroker); mediator.add(tileLayer); } /** * @see org.geoserver.catalog.event.CatalogListener#handleModifyEvent(org.geoserver.catalog.event.CatalogModifyEvent) * @see #handlePostModifyEvent */ public void handleModifyEvent(CatalogModifyEvent event) throws CatalogException { CatalogInfo source = event.getSource(); if (source instanceof LayerInfo || source instanceof LayerGroupInfo || source instanceof FeatureTypeInfo || source instanceof CoverageInfo || source instanceof WMSLayerInfo || source instanceof WorkspaceInfo) { PRE_MODIFY_EVENT.set(event); if (mediator.hasTileLayer(source)) { try { GeoServerTileLayer tileLayer = mediator.getTileLayer(source); GeoServerTileLayerInfo tileLayerInfo = tileLayer.getInfo(); PRE_MODIFY_TILELAYER.set(tileLayerInfo); } catch (RuntimeException e) { log.info("Ignoring misconfigured tile layer info for " + source); } } } } /** * In case the event refers to the addition or removal of a {@link LayerInfo} or * {@link LayerGroupInfo} adds or removes the corresponding {@link GeoServerTileLayer} through * {@link GWC#createLayer}. * <p> * Note this method does not discriminate whether the change in the layer or layergroup deserves * a change in its matching TileLayer, it just re-creates the TileLayer * </p> * * @see org.geoserver.catalog.event.CatalogListener#handlePostModifyEvent(org.geoserver.catalog.event.CatalogPostModifyEvent) */ public void handlePostModifyEvent(final CatalogPostModifyEvent event) throws CatalogException { final CatalogInfo source = event.getSource(); if (!(source instanceof LayerInfo || source instanceof LayerGroupInfo || source instanceof FeatureTypeInfo || source instanceof CoverageInfo || source instanceof WMSLayerInfo || source instanceof WorkspaceInfo)) { return; } final GeoServerTileLayerInfo tileLayerInfo = PRE_MODIFY_TILELAYER.get(); PRE_MODIFY_TILELAYER.remove(); final CatalogModifyEvent preModifyEvent = PRE_MODIFY_EVENT.get(); PRE_MODIFY_EVENT.remove(); if (tileLayerInfo == null && !(source instanceof WorkspaceInfo)) { return;// no tile layer associated, no need to continue } if (preModifyEvent == null) { throw new IllegalStateException( "PostModifyEvent called without having called handlePreModify first?"); } final List<String> changedProperties = preModifyEvent.getPropertyNames(); final List<Object> oldValues = preModifyEvent.getOldValues(); final List<Object> newValues = preModifyEvent.getNewValues(); log.finer("Handling modify event for " + source); if (source instanceof FeatureTypeInfo || source instanceof CoverageInfo || source instanceof WMSLayerInfo || source instanceof LayerGroupInfo) { /* * Handle changing the filter definition, this is the kind of change that affects the * full output contents */ if (changedProperties.contains("cqlFilter") && source instanceof FeatureTypeInfo) { mediator.truncate(((FeatureTypeInfo) source).prefixedName()); } /* * Handle the rename case. For LayerInfos it's actually the related ResourceInfo what * gets renamed, at least until the data/publish split is implemented in GeoServer. For * LayerGroupInfo it's either the group name itself or its workspace */ if (changedProperties.contains("name") || changedProperties.contains("namespace") || changedProperties.contains("workspace")) { handleRename(tileLayerInfo, source, changedProperties, oldValues, newValues); } } else if(source instanceof WorkspaceInfo) { if (changedProperties.contains("name")) { handleWorkspaceRename(source, changedProperties, oldValues, newValues); } } if (source instanceof LayerInfo) { final LayerInfo li = (LayerInfo) source; handleLayerInfoChange(changedProperties, oldValues, newValues, li, tileLayerInfo); } else if (source instanceof LayerGroupInfo) { LayerGroupInfo lgInfo = (LayerGroupInfo) source; handleLayerGroupInfoChange(changedProperties, oldValues, newValues, lgInfo, tileLayerInfo); } } private void handleLayerGroupInfoChange(final List<String> changedProperties, final List<Object> oldValues, final List<Object> newValues, final LayerGroupInfo lgInfo, final GeoServerTileLayerInfo tileLayerInfo) { checkNotNull(lgInfo); checkNotNull(tileLayerInfo); final String layerName = tileLayerName(lgInfo); boolean truncate = false; if (changedProperties.contains("layers")) { final int layersIndex = changedProperties.indexOf("layers"); Object oldLayers = oldValues.get(layersIndex); Object newLayers = newValues.get(layersIndex); truncate = !oldLayers.equals(newLayers); } if (!truncate && changedProperties.contains("styles")) { final int stylesIndex = changedProperties.indexOf("styles"); Object oldStyles = oldValues.get(stylesIndex); Object newStyles = newValues.get(stylesIndex); truncate = !oldStyles.equals(newStyles); } if (truncate) { log.info("Truncating TileLayer for layer group '" + layerName + "' due to a change in its layers or styles"); mediator.truncate(layerName); } } /** * Handles changes of interest to GWC on a {@link LayerInfo}. * <ul> * <li>If the name of the default style changed, then the layer's cache for the default style is * truncated. This method doesn't check if the contents of the styles are equal. That is handled * by {@link CatalogStyleChangeListener} whenever a style is modified. * <li>If the tile layer is {@link GeoServerTileLayerInfo#isAutoCacheStyles() auto caching * styles} and the layerinfo's "styles" list changed, the tile layer's STYLE parameter filter is * updated to match the actual list of layer styles and any removed style is truncated. * </ul> * * @param changedProperties * @param oldValues * @param newValues * @param li * @param tileLayerInfo */ private void handleLayerInfoChange(final List<String> changedProperties, final List<Object> oldValues, final List<Object> newValues, final LayerInfo li, final GeoServerTileLayerInfo tileLayerInfo) { checkNotNull(tileLayerInfo); final String layerName = tileLayerName(li); boolean save = false; boolean defaultStyleChanged = false; final String defaultStyle; /* * If default style name changed */ if (changedProperties.contains("defaultStyle")) { final int propIndex = changedProperties.indexOf("defaultStyle"); final StyleInfo oldStyle = (StyleInfo) oldValues.get(propIndex); final StyleInfo newStyle = (StyleInfo) newValues.get(propIndex); final String oldStyleName = oldStyle.prefixedName(); defaultStyle = newStyle.prefixedName(); if (!Objects.equal(oldStyleName, defaultStyle)) { save = true; defaultStyleChanged = true; log.info("Truncating default style for layer " + layerName + ", as it changed from " + oldStyleName + " to " + defaultStyle); mediator.truncateByLayerAndStyle(layerName, oldStyleName); } } else { StyleInfo styleInfo = li.getDefaultStyle(); defaultStyle = styleInfo == null ? null : styleInfo.prefixedName(); } if (tileLayerInfo.isAutoCacheStyles()) { Set<String> styles = new HashSet<String>(); for (StyleInfo s : li.getStyles()) { styles.add(s.prefixedName()); } ImmutableSet<String> cachedStyles = tileLayerInfo.cachedStyles(); if (!styles.equals(cachedStyles)) { // truncate no longer existing cached styles Set<String> notCachedAnyMore = Sets.difference(cachedStyles, styles); for (String oldCachedStyle : notCachedAnyMore) { log.info("Truncating cached style " + oldCachedStyle + " of layer " + layerName + " as it's no longer one of the layer's styles"); mediator.truncateByLayerAndStyle(layerName, oldCachedStyle); } // reset STYLES parameter filter final boolean createParamIfNotExists = true; TileLayerInfoUtil.updateStringParameterFilter(tileLayerInfo, "STYLES", createParamIfNotExists, defaultStyle, styles); save = true; } } // check the caching settings, have they changed? boolean cachingInfoChanged = false; int metadataIdx = changedProperties.indexOf("metadata"); if(metadataIdx >= 0) { MetadataMap oldMetadata = (MetadataMap) oldValues.get(metadataIdx); MetadataMap newMetadata = (MetadataMap) newValues.get(metadataIdx); boolean cachingEnabledChanged = LangUtils.equals(oldMetadata.get(ResourceInfo.CACHING_ENABLED, Boolean.class), newMetadata.get(ResourceInfo.CACHING_ENABLED, Boolean.class)); boolean cachingMaxAgeChanged = LangUtils.equals(oldMetadata.get(ResourceInfo.CACHE_AGE_MAX, Boolean.class), newMetadata.get(ResourceInfo.CACHE_AGE_MAX, Boolean.class)); // we do we don't need to truncate the layer, but we need to update // its LayerInfo so that the resulting caching headers get updated if(cachingEnabledChanged || cachingMaxAgeChanged) { cachingInfoChanged = true; save = true; } } if (save) { GridSetBroker gridSetBroker = mediator.getGridSetBroker(); GeoServerTileLayer tileLayer = new GeoServerTileLayer(li, gridSetBroker, tileLayerInfo); mediator.save(tileLayer); } // caching info and default style changes affect also the layer groups containing the layer if( cachingInfoChanged || defaultStyleChanged) { List<LayerGroupInfo> groups = catalog.getLayerGroups(); for (LayerGroupInfo lg : groups) { GeoServerTileLayer tileLayer = mediator.getTileLayer(lg); if(tileLayer != null) { LayerGroupHelper helper = new LayerGroupHelper(lg); int idx = helper.allLayers().indexOf(li); if(idx >= 0) { // we need to save in case something changed in one of the layer GridSetBroker gridSetBroker = mediator.getGridSetBroker(); GeoServerTileLayerInfo groupTileLayerInfo = tileLayer.getInfo(); GeoServerTileLayer newTileLayer = new GeoServerTileLayer(lg, gridSetBroker, groupTileLayerInfo); mediator.save(newTileLayer); // we also need to truncate the group if the layer default style changed, // and the layer group was using if(defaultStyleChanged && lg.getStyles().get(idx) == null) { mediator.truncate(groupTileLayerInfo.getName()); } } } } } } private void handleWorkspaceRename(final CatalogInfo source, final List<String> changedProperties, final List<Object> oldValues, final List<Object> newValues) { final int nameIndex = changedProperties.indexOf("name"); final String oldWorkspaceName = (String) oldValues.get(nameIndex); final String newWorkspaceName = (String) newValues.get(nameIndex); // handle layers rename CloseableIterator<LayerInfo> layers = catalog.list(LayerInfo.class, Predicates.equal("resource.store.workspace.name", newWorkspaceName)); try { while(layers.hasNext()) { LayerInfo layer = layers.next(); String oldName = oldWorkspaceName + ":" + layer.getName(); String newName = newWorkspaceName + ":" + layer.getName(); // see if the tile layer existed and it is one that we can rename (admin // could have overwritten it with a direct layer in geowebcache.xml) TileLayer tl; try { tl = mediator.getTileLayerByName(oldName); if(!(tl instanceof GeoServerTileLayer)) { continue; } } catch(IllegalArgumentException e) { // this happens if the layer is not there, move on continue; } try { if(layer.getType() == PublishedType.VECTOR && ((FeatureTypeInfo) layer.getResource()).getFeatureType().getGeometryDescriptor() == null) { // skip geometryless layers continue; } } catch(IOException e) { // this should not happen... log.log(Level.FINE, "Failed to determine if layer" + layer + " is geometryless while renaming tile layers for workspace name change " + oldName + " -> " + newName, e); } try { if(tl instanceof GeoServerTileLayer) { GeoServerTileLayer gstl = (GeoServerTileLayer) tl; renameTileLayer(gstl.getInfo(), oldName, newName); } } catch(Exception e) { // this should not happen, but we don't want to log.log(Level.WARNING, "Failed to rename tile layer for geoserver layer " + layer + " while renaming tile layers for workspace name change " + oldName + " -> " + newName, e); } } } finally { layers.close(); } // handle layer group renames CloseableIterator<LayerGroupInfo> groups = catalog.list(LayerGroupInfo.class, Predicates.equal("workspace.name", newWorkspaceName)); try { while (groups.hasNext()) { LayerGroupInfo group = groups.next(); String oldName = oldWorkspaceName + ":" + group.getName(); String newName = newWorkspaceName + ":" + group.getName(); // see if the tile layer existed and it is one that we can rename (admin // could have overwritten it with a direct layer in geowebcache.xml) TileLayer tl; try { tl = mediator.getTileLayerByName(oldName); if (!(tl instanceof GeoServerTileLayer)) { continue; } } catch (IllegalArgumentException e) { // this happens if the layer is not there, move on continue; } try { if (tl instanceof GeoServerTileLayer) { GeoServerTileLayer gstl = (GeoServerTileLayer) tl; renameTileLayer(gstl.getInfo(), oldName, newName); } } catch (Exception e) { // this should not happen, but we don't want to log.log(Level.WARNING, "Failed to rename tile layer for geoserver group " + group + " while renaming tile layers for workspace name change " + oldName + " -> " + newName, e); } } } finally { groups.close(); } } private void handleRename(final GeoServerTileLayerInfo tileLayerInfo, final CatalogInfo source, final List<String> changedProperties, final List<Object> oldValues, final List<Object> newValues) { final int nameIndex = changedProperties.indexOf("name"); final int namespaceIndex = changedProperties.indexOf("namespace"); String oldLayerName; String newLayerName; if (source instanceof ResourceInfo) {// covers LayerInfo, CoverageInfo, and WMSLayerInfo // must cover prefix:name final ResourceInfo resourceInfo = (ResourceInfo) source; final NamespaceInfo currNamespace = resourceInfo.getNamespace(); final NamespaceInfo oldNamespace; if (namespaceIndex > -1) { // namespace changed oldNamespace = (NamespaceInfo) oldValues.get(namespaceIndex); } else { oldNamespace = currNamespace; } newLayerName = resourceInfo.prefixedName(); if (nameIndex > -1) { oldLayerName = (String) oldValues.get(nameIndex); } else { oldLayerName = resourceInfo.getName(); } oldLayerName = oldNamespace.getPrefix() + ":" + oldLayerName; } else { // it's a layer group, no need to worry about namespace oldLayerName = tileLayerInfo.getName(); newLayerName = tileLayerName((LayerGroupInfo) source); } if (!oldLayerName.equals(newLayerName)) { renameTileLayer(tileLayerInfo, oldLayerName, newLayerName); } } private void renameTileLayer(final GeoServerTileLayerInfo tileLayerInfo, String oldLayerName, String newLayerName) { tileLayerInfo.setName(newLayerName); // notify the mediator of the rename so it changes the name of the layer in GWC without // affecting its caches GridSetBroker gridSetBroker = mediator.getGridSetBroker(); final GeoServerTileLayer oldTileLayer = (GeoServerTileLayer) mediator .getTileLayerByName(oldLayerName); checkState(null != oldTileLayer, "handleRename: old tile layer not found: '" + oldLayerName + "'. New name: '" + newLayerName + "'"); final GeoServerTileLayer modifiedTileLayer; if (oldTileLayer.getLayerInfo() != null) { LayerInfo layerInfo = oldTileLayer.getLayerInfo(); modifiedTileLayer = new GeoServerTileLayer(layerInfo, gridSetBroker, tileLayerInfo); } else { LayerGroupInfo layerGroup = oldTileLayer.getLayerGroupInfo(); modifiedTileLayer = new GeoServerTileLayer(layerGroup, gridSetBroker, tileLayerInfo); } mediator.save(modifiedTileLayer); } /** * * @see org.geoserver.catalog.event.CatalogListener#handleRemoveEvent(org.geoserver.catalog.event.CatalogRemoveEvent) * @see GWC#removeTileLayers(List) */ public void handleRemoveEvent(CatalogRemoveEvent event) throws CatalogException { CatalogInfo obj = event.getSource(); if (!(obj instanceof LayerInfo || obj instanceof LayerGroupInfo)) { return; } if (!mediator.hasTileLayer(obj)) { return; } String prefixedName = null; if (obj instanceof LayerGroupInfo) { LayerGroupInfo lgInfo = (LayerGroupInfo) obj; prefixedName = tileLayerName(lgInfo); } else if (obj instanceof LayerInfo) { LayerInfo layerInfo = (LayerInfo) obj; prefixedName = tileLayerName(layerInfo); } if (null != prefixedName) { // notify the layer has been removed mediator.removeTileLayers(Arrays.asList(prefixedName)); } } /** * * @see org.geoserver.catalog.event.CatalogListener#reloaded() */ public void reloaded() { // } }