/* (c) 2014 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.catalog;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.geoserver.catalog.util.CloseableIterator;
import org.opengis.filter.Filter;
import org.opengis.filter.MultiValuedFilter.MatchAction;
/**
* Visits the specified objects cascading down to contained/related objects,
* and collects information about which objects will be removed or modified
* once the root objects are cascade deleted with {@link CascadeDeleteVisitor}
*/
public class CascadeRemovalReporter implements CatalogVisitor {
/**
* The various types of modifications a catalog object can be subjected to in case of cascade
* removal. They are ordered from stronger to weaker.
*/
public enum ModificationType {
DELETE, STYLE_RESET, EXTRA_STYLE_REMOVED, GROUP_CHANGED;
}
/**
* The catalog used to drill down into the containment hierarchy
*/
Catalog catalog;
/**
* The set of objects collected during the scan
*/
Map<CatalogInfo, ModificationType> objects;
/**
* Used to track which layers are going to be removed from a group, if
* we remove them all the group will have to be removed as well
*/
Map<LayerGroupInfo, Set<LayerInfo>> groups;
public CascadeRemovalReporter(Catalog catalog) {
this.catalog = catalog;
reset();
}
public void visit(Catalog catalog) {
}
/**
* Resets the visitor so that it can be reused for another search
*/
public void reset() {
this.objects = new HashMap<CatalogInfo, ModificationType>();
this.groups = new HashMap<LayerGroupInfo, Set<LayerInfo>>();
}
/**
* Returns the objects that will be affected by the removal, filtering them by type and by kind
* of modification they will sustain as a consequence of the removal
*
* @param <T>
* @param catalogClass
* The type of object to be searched for, or null if no type filtering is desired
* @param modification
* The kind of modification to be searched for, or null if no modification type
* filtering is desired
*
*/
public <T> List<T> getObjects(Class<T> catalogClass, ModificationType... modifications) {
List<T> result = new ArrayList<T>();
List<ModificationType> mods = (modifications == null || modifications.length == 0) ?
null : Arrays.asList(modifications);
for (CatalogInfo ci : objects.keySet()) {
if (catalogClass == null || catalogClass.isAssignableFrom(ci.getClass())) {
if (mods == null || mods.contains(objects.get(ci)))
result.add((T) ci);
}
}
return result;
}
/**
* Allows removal of the specified objects from the reachable set (usually, the user
* will not want the roots to be part of the set)
* @param objects
*/
public void removeAll(Collection<? extends CatalogInfo> objects) {
for (CatalogInfo ci : objects) {
this.objects.remove(ci);
}
}
/**
* Adds a CatalogInfo into the objects map, eventually overriding the
* type if the modification is stronger that the one already registered
*/
void add(CatalogInfo ci, ModificationType type) {
ModificationType oldType = objects.get(ci);
if(oldType == null || oldType.compareTo(type) > 0) {
objects.put(ci, type);
}
}
public void visit(WorkspaceInfo workspace) {
// drill down on stores
List<StoreInfo> stores = catalog.getStoresByWorkspace(workspace, StoreInfo.class);
for (StoreInfo storeInfo : stores) {
storeInfo.accept(this);
}
// drill into namespaces
// catalog.getNamespaceByPrefix(workspace.getName()).accept(this);
// drill down into styles
for (StyleInfo style : catalog.getStylesByWorkspace(workspace)) {
style.accept(this);
}
// drill down into groups
for (LayerGroupInfo group : catalog.getLayerGroupsByWorkspace(workspace)) {
group.accept(this);
}
// add self
add(workspace, ModificationType.DELETE);
}
public void visit(NamespaceInfo namespace) {
add(namespace, ModificationType.DELETE);
}
public void visit(DataStoreInfo dataStore) {
visitStore(dataStore);
}
public void visit(CoverageStoreInfo coverageStore) {
visitStore(coverageStore);
}
public void visit(WMSStoreInfo store) {
visitStore(store);
}
void visitStore(StoreInfo dataStore) {
// drill down into layers (into resources since we cannot scan layers)
List<ResourceInfo> resources = catalog.getResourcesByStore(dataStore, ResourceInfo.class);
for (ResourceInfo ri : resources) {
List<LayerInfo> layers = catalog.getLayers(ri);
if (!layers.isEmpty()) {
for (LayerInfo li : layers) {
li.accept(this);
}
}
else {
ri.accept(this);
}
}
add(dataStore, ModificationType.DELETE);
}
public void visit(FeatureTypeInfo featureType) {
add(featureType, ModificationType.DELETE);
}
public void visit(CoverageInfo coverage) {
add(coverage, ModificationType.DELETE);
}
public void visit(WMSLayerInfo wmsLayer) {
add(wmsLayer, ModificationType.DELETE);
}
public void visit(LayerInfo layer) {
// mark layer and resource as removed
add(layer.getResource(), ModificationType.DELETE);
add(layer, ModificationType.DELETE);
// scan the layer groups and find those that do use the
// current layer
Filter groupContainsLayer = Predicates.equal("layers", layer, MatchAction.ANY);
try (CloseableIterator<LayerGroupInfo> it = catalog.list(LayerGroupInfo.class,
groupContainsLayer)) {
while (it.hasNext()) {
LayerGroupInfo group = it.next();
// mark the layer as one that will be removed
Set<LayerInfo> layers = groups.get(group);
if(layers == null) {
layers = new HashSet<LayerInfo>();
groups.put(group, layers);
}
layers.add(layer);
// a group can contain the same layer multiple times. We want to
// make sure to mark the group as removed if all the layers inside of
// it are going to be removed, just changed otherwise
if(layers.size() == new HashSet<PublishedInfo>(group.getLayers()).size()) {
visit(group);
} else {
add(group, ModificationType.GROUP_CHANGED);
}
}
}
}
public void visit(StyleInfo style) {
// find the layers having this style as primary or secondary
Filter anyStyle = Predicates.equal("styles", style, MatchAction.ANY);
Filter layersAssociated = Predicates.or(Predicates.equal("defaultStyle", style), anyStyle);
// remove style references in layers
try (CloseableIterator<LayerInfo> it = catalog.list(LayerInfo.class, layersAssociated)) {
while (it.hasNext()) {
LayerInfo li = it.next();
if (style.equals(li.getDefaultStyle()))
add(li, ModificationType.STYLE_RESET);
else if(li.getStyles().contains(style))
add(li, ModificationType.EXTRA_STYLE_REMOVED);
}
}
// groups can also refer to style, reset each reference to the
// associated layer default style
Filter groupAssociated = Predicates.or(Predicates.equal("rootLayerStyle", style), anyStyle);
try (CloseableIterator<LayerGroupInfo> it = catalog.list(LayerGroupInfo.class,
groupAssociated)) {
while (it.hasNext()) {
LayerGroupInfo group = it.next();
if (style.equals(group.getRootLayerStyle())) {
add(group, ModificationType.GROUP_CHANGED);
}
if (group.getStyles().contains(style)) {
add(group, ModificationType.GROUP_CHANGED);
}
}
}
// add the style
add(style, ModificationType.DELETE);
}
public void visit(LayerGroupInfo layerGroupToRemove) {
Filter associatedTo = Predicates.equal("layers", layerGroupToRemove, MatchAction.ANY);
try (CloseableIterator<LayerGroupInfo> it = catalog
.list(LayerGroupInfo.class, associatedTo)) {
while (it.hasNext()) {
LayerGroupInfo group = it.next();
if (group.getLayers().contains(layerGroupToRemove)) {
final List<PublishedInfo> layers = new ArrayList<>(group.getLayers());
layers.removeAll(objects.keySet());
if (layers.size() == 0) {
visit(group);
} else {
add(group, ModificationType.GROUP_CHANGED);
}
}
}
}
add(layerGroupToRemove, ModificationType.DELETE);
}
}