/* Copyright (c) 2011 TOPP - www.openplans.org. All rights reserved.
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.gwc.layer;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
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.LayerGroupInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.NamespaceInfo;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.catalog.StyleInfo;
import org.geoserver.catalog.WMSLayerInfo;
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.gwc.GWC;
import org.geoserver.gwc.config.GWCConfig;
import org.geotools.util.logging.Logging;
import org.geowebcache.filter.parameters.StringParameterFilter;
import org.geowebcache.storage.StorageBroker;
/**
* Listens to {@link Catalog}'s layer 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>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#getPrefixedName() 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#getLayers() layers} or
* {@link LayerGroupInfo#getStyles() 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#getCachedStyles() cached styles}. For any <b>removed</b> style, if
* it was one of the {@link GeoServerTileLayerInfo#getCachedStyles() 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 CatalogConfiguration catalogConfig;
/**
* 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>();
public CatalogLayerEventListener(final CatalogConfiguration catalogConfiguration) {
this.catalogConfig = catalogConfiguration;
}
/**
* If either a {@link LayerInfo} or {@link LayerGroupInfo} has been added to the {@link Catalog}
* , create a corresponding GWC TileLayer.
*
* @see org.geoserver.catalog.event.CatalogListener#handleAddEvent
* @see GWC#createLayer(LayerInfo)
* @see GWC#createLayer(LayerGroupInfo)
*/
public void handleAddEvent(CatalogAddEvent event) throws CatalogException {
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;
catalogConfig.createLayer(layerInfo);
} else if (obj instanceof LayerGroupInfo) {
LayerGroupInfo lgi = (LayerGroupInfo) obj;
catalogConfig.createLayer(lgi);
}
}
/**
* @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) {
PRE_MODIFY_EVENT.set(event);
}
}
/**
* 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 Object source = event.getSource();
if (!(source instanceof LayerInfo || source instanceof LayerGroupInfo
|| source instanceof FeatureTypeInfo || source instanceof CoverageInfo || source instanceof WMSLayerInfo)) {
return;
}
final CatalogModifyEvent preModifyEvent = PRE_MODIFY_EVENT.get();
if (preModifyEvent == null) {
throw new IllegalStateException(
"PostModifyEvent called without having called handlePreModify first?");
}
PRE_MODIFY_EVENT.remove();
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 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 the group itself
*/
if (changedProperties.contains("name") || changedProperties.contains("namespace")) {
handleRename(source, changedProperties, oldValues, newValues);
}
}
if (source instanceof LayerInfo) {
if (changedProperties.contains("defaultStyle") || changedProperties.contains("styles")) {
// REVISIT: what about truncating the LayerGroups containing the modified layer?
// checking the style applies of course
final LayerInfo li = (LayerInfo) source;
handleLayerInfo(changedProperties, oldValues, newValues, li);
}
} else if (source instanceof LayerGroupInfo) {
if (changedProperties.contains("layers") || changedProperties.contains("styles")) {
LayerGroupInfo lgInfo = (LayerGroupInfo) source;
handleLayerGroupInfo(changedProperties, oldValues, newValues, lgInfo);
}
}
}
private void handleLayerGroupInfo(final List<String> changedProperties,
final List<Object> oldValues, final List<Object> newValues, LayerGroupInfo 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 '" + lgInfo.getName()
+ "' due to a change in its layers or styles");
String layerName = lgInfo.getName();
catalogConfig.truncate(layerName);
}
}
@SuppressWarnings("unchecked")
private void handleLayerInfo(final List<String> changedProperties,
final List<Object> oldValues, final List<Object> newValues, final LayerInfo li) {
final String layerName = li.getResource().getPrefixedName();
final GeoServerTileLayer tileLayer;
tileLayer = (GeoServerTileLayer) catalogConfig.getTileLayer(layerName);
boolean save = false;
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.getName();
final String newStyleName = newStyle.getName();
if (!oldStyleName.equals(newStyleName)) {
save = true;
catalogConfig.truncate(layerName, oldStyleName);
}
}
if (changedProperties.contains("styles")) {
final GeoServerTileLayerInfo info = tileLayer.getInfo();
final int propIndex = changedProperties.indexOf("styles");
final Set<StyleInfo> oldStyles = (Set<StyleInfo>) oldValues.get(propIndex);
final Set<StyleInfo> currentStyles = (Set<StyleInfo>) newValues.get(propIndex);
Set<String> newStyleSet = new HashSet<String>(info.getCachedStyles());
if (!oldStyles.equals(currentStyles)) {
Set<StyleInfo> removed = new HashSet<StyleInfo>(oldStyles);
removed.removeAll(currentStyles);
// remove any style detacched from the layer
for (StyleInfo deletedStyle : removed) {
String styleName = deletedStyle.getName();
newStyleSet.remove(styleName);
catalogConfig.truncate(layerName, styleName);
}
// add new cached styles if tilelayer is configured to do so
if (info.isAutoCacheStyles()) {
Set<StyleInfo> added = new HashSet<StyleInfo>(currentStyles);
added.removeAll(oldStyles);
for (StyleInfo addedStyle : added) {
String styleName = addedStyle.getName();
newStyleSet.add(styleName);
}
}
}
// prune any tangling style from info
Set<String> currentStyleNames = new HashSet<String>();
for (StyleInfo current : currentStyles) {
currentStyleNames.add(current.getName());
}
newStyleSet.retainAll(currentStyleNames);
// recreate parameter filters if need be
if (!newStyleSet.equals(info.getCachedStyles())) {
save = true;
info.setCachedStyles(newStyleSet);
tileLayer.resetParameterFilters();
}
}
if (save) {
catalogConfig.save(tileLayer);
}
}
private void handleRename(final Object 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.getPrefixedName();
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 = (String) oldValues.get(nameIndex);
newLayerName = (String) newValues.get(nameIndex);
}
if (!oldLayerName.equals(newLayerName)) {
catalogConfig.renameTileLayer(oldLayerName, newLayerName);
}
}
/**
*
* @see org.geoserver.catalog.event.CatalogListener#handleRemoveEvent(org.geoserver.catalog.event.CatalogRemoveEvent)
* @see GWC#removeLayer(String)
*/
public void handleRemoveEvent(CatalogRemoveEvent event) throws CatalogException {
Object obj = event.getSource();
String prefixedName = null;
if (obj instanceof LayerGroupInfo) {
LayerGroupInfo lgInfo = (LayerGroupInfo) obj;
prefixedName = lgInfo.getName();
} else if (obj instanceof LayerInfo) {
LayerInfo layerInfo = (LayerInfo) obj;
prefixedName = layerInfo.getResource().getPrefixedName();
}
if (null != prefixedName) {
catalogConfig.removeLayer(prefixedName);
}
}
/**
*
* @see org.geoserver.catalog.event.CatalogListener#reloaded()
*/
public void reloaded() {
//
}
}