/* (c) 2016 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.impl;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CatalogException;
import org.geoserver.catalog.CatalogInfo;
import org.geoserver.catalog.LayerGroupInfo;
import org.geoserver.catalog.LayerGroupInfo.Mode;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.PublishedInfo;
import org.geoserver.catalog.ResourceInfo;
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;
/**
* A cache for layer group containment, it speeds up looking up layer groups containing a particular layer (recursively). * The class is thread safe.
*
* @author Andrea Aime - GeoSolutions
*/
class LayerGroupContainmentCache {
/**
* Builds a concurrent set wrapping a {@link ConcurrentHashMap}
*/
static final Function<? super String, ? extends Set<LayerGroupSummary>> CONCURRENT_SET_BUILDER = k -> Collections
.newSetFromMap(new ConcurrentHashMap<LayerGroupSummary, Boolean>());
/**
* Verifies a certain {@link PublishedInfo} is actually a {@link LayerInfo}
*/
static final Predicate<PublishedInfo> IS_LAYER = p -> p.getId() != null
&& p instanceof LayerInfo && ((LayerInfo) p).getResource() != null;
/**
* Verifies a certain {@link PublishedInfo} is actually a {@link LayerGroupInfo}
*/
static final Predicate<PublishedInfo> IS_GROUP = p -> p.getId() != null
&& p instanceof LayerGroupInfo;
/**
* Lookup from layer group id to group parent information
*/
Map<String, LayerGroupSummary> groupCache = new ConcurrentHashMap<>();
/**
* Lookup from {@link ResourceInfo} id to groups directly containing its associated layers (the transitive containment is computed by suing
* {@link LayerGroupSummary}
*/
Map<String, Set<LayerGroupSummary>> resourceContainmentCache = new ConcurrentHashMap<>();
private Catalog catalog;
public LayerGroupContainmentCache(Catalog catalog) {
this.catalog = catalog;
catalog.addListener(new CatalogChangeListener());
buildLayerGroupCaches();
}
private void buildLayerGroupCaches() {
groupCache.clear();
resourceContainmentCache.clear();
List<LayerGroupInfo> groups = catalog.getLayerGroups();
// first populate the basic structure
for (LayerGroupInfo lg : groups) {
addGroupInfo(lg);
}
// now populate the containment structure
for (LayerGroupInfo lg : groups) {
registerContainedGroups(lg);
}
}
private void registerContainedGroups(LayerGroupInfo lg) {
lg.getLayers().stream().filter(IS_GROUP).forEach(p -> {
String containerId = lg.getId();
String containedId = p.getId();
LayerGroupSummary container = groupCache.get(containerId);
LayerGroupSummary contained = groupCache.get(containedId);
if (container != null && contained != null) {
contained.containerGroups.add(container);
}
});
}
private void addGroupInfo(LayerGroupInfo lg) {
LayerGroupSummary groupData = new LayerGroupSummary(lg);
groupCache.put(lg.getId(), groupData);
lg.getLayers().stream().filter(IS_LAYER).forEach(p -> {
String id = ((LayerInfo) p).getResource().getId();
Set<LayerGroupSummary> containers = resourceContainmentCache.computeIfAbsent(id,
CONCURRENT_SET_BUILDER);
containers.add(groupData);
});
}
private void clearGroupInfo(LayerGroupInfo lg) {
LayerGroupSummary data = groupCache.remove(lg.getId());
// clear the resource containment cache
lg.getLayers().stream().filter(IS_LAYER).forEach(p -> {
String rid = ((LayerInfo) p).getResource().getId();
synchronized (rid) {
Set<LayerGroupSummary> containers = resourceContainmentCache.get(rid);
if (containers != null) {
containers.remove(data);
}
}
});
// this group does not contain anything anymore, remove from containment
for (LayerGroupSummary d : groupCache.values()) {
d.containerGroups.remove(lg.getId());
}
}
/**
* Returns all groups containing directly or indirectly containing the resource
*
* @param resource
* @return
*/
public Collection<LayerGroupSummary> getContainerGroupsFor(ResourceInfo resource) {
String id = resource.getId();
Set<LayerGroupSummary> groups = resourceContainmentCache.get(id);
if (groups == null) {
return Collections.emptyList();
}
Set<LayerGroupSummary> result = new HashSet<>();
for (LayerGroupSummary lg : groups) {
collectContainers(lg, result);
}
return result;
}
/**
* Returns all groups containing directly or indirectly the specified group, and relevant for security (e.g., anything but
* {@link LayerGroupInfo.Mode#SINGLE} ones
*
* @param lg
* @return
*/
public Collection<LayerGroupSummary> getContainerGroupsFor(LayerGroupInfo lg) {
String id = lg.getId();
if (id == null) {
return Collections.emptyList();
}
LayerGroupSummary summary = groupCache.get(id);
if (summary == null) {
return Collections.emptyList();
}
Set<LayerGroupSummary> result = new HashSet<>();
for (LayerGroupSummary container : summary.getContainerGroups()) {
collectContainers(container, result);
}
return result;
}
/**
* Recursively collects the group and all its containers in the <data>groups</data> collection
*
* @param lg
* @param groups
*/
private void collectContainers(LayerGroupSummary lg, Set<LayerGroupSummary> groups) {
if (!groups.contains(lg)) {
if (lg.getMode() != LayerGroupInfo.Mode.SINGLE) {
groups.add(lg);
}
for (LayerGroupSummary container : lg.containerGroups) {
collectContainers(container, groups);
}
}
}
/**
* Information summary about a layer group, just enough information to avoid performing linear searches against the catalog to match against rules
* and scan layer containment upwards
*/
static class LayerGroupSummary {
String id;
String workspace;
String name;
LayerGroupInfo.Mode mode;
Set<LayerGroupSummary> containerGroups;
LayerGroupSummary(LayerGroupInfo lg) {
this.id = lg.getId();
this.workspace = lg.getWorkspace() != null ? lg.getWorkspace().getName() : null;
this.name = lg.getName();
this.mode = lg.getMode();
containerGroups = CONCURRENT_SET_BUILDER.apply(null);
}
LayerGroupSummary(LayerGroupSummary other) {
this.id = other.id;
this.workspace = other.workspace;
this.name = other.name;
this.mode = other.mode;
containerGroups = other.containerGroups;
}
public String getId() {
return id;
}
public String getWorkspace() {
return workspace;
}
public String getName() {
return name;
}
public LayerGroupInfo.Mode getMode() {
return mode;
}
public Set<LayerGroupSummary> getContainerGroups() {
return containerGroups;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof LayerGroupSummary)) {
return false;
}
LayerGroupSummary other = (LayerGroupSummary) obj;
return Objects.equals(this.id, other.id);
}
@Override
public int hashCode() {
return Objects.hash(this.id);
}
public String[] getPath() {
if (workspace == null) {
return new String[] { name };
} else {
return new String[] { workspace, name };
}
}
@Override
public String toString() {
return "LayerGroupSummary [id=" + id + ", workspace=" + workspace + ", name=" + name
+ ", mode=" + mode + ", containerGroups=" + containerGroups + "]";
}
public String prefixedName() {
if (workspace == null) {
return name;
} else {
return workspace + ":" + name;
}
}
}
/**
* This listener keeps the "layer group" flags in the authorization tree current, in order to optimize the application of layer group containment
* rules
*/
final class CatalogChangeListener implements CatalogListener {
@Override
public void handleAddEvent(CatalogAddEvent event) throws CatalogException {
if (event.getSource() instanceof LayerGroupInfo) {
LayerGroupInfo lg = (LayerGroupInfo) event.getSource();
addGroupInfo(lg);
registerContainedGroups(lg);
}
}
@Override
public void handleRemoveEvent(CatalogRemoveEvent event) throws CatalogException {
if (event.getSource() instanceof LayerGroupInfo) {
LayerGroupInfo lg = (LayerGroupInfo) event.getSource();
clearGroupInfo(lg);
}
// no need to listen to workspace or layer removal, these will cascade to
// layer groups
}
@Override
public void handleModifyEvent(CatalogModifyEvent event) throws CatalogException {
final CatalogInfo source = event.getSource();
if (source instanceof LayerGroupInfo) {
LayerGroupInfo lg = (LayerGroupInfo) event.getSource();
// was the layer group renamed, moved, or its contents changed?
int nameIdx = event.getPropertyNames().indexOf("name");
if (nameIdx != -1) {
String newName = (String) event.getNewValues().get(nameIdx);
updateGroupName(lg.getId(), newName);
}
int wsIdx = event.getPropertyNames().indexOf("workspace");
if (wsIdx != -1) {
WorkspaceInfo newWorkspace = (WorkspaceInfo) event.getNewValues().get(wsIdx);
updateGroupWorkspace(lg.getId(), newWorkspace);
}
int layerIdx = event.getPropertyNames().indexOf("layers");
if (layerIdx != -1) {
List<PublishedInfo> oldLayers = (List<PublishedInfo>) event.getOldValues()
.get(layerIdx);
List<PublishedInfo> newLayers = (List<PublishedInfo>) event.getNewValues()
.get(layerIdx);
updateContainedLayers(groupCache.get(lg.getId()), oldLayers, newLayers);
}
int modeIdx = event.getPropertyNames().indexOf("mode");
if(modeIdx != -1) {
Mode newMode = (Mode) event.getNewValues().get(modeIdx);
updateGroupMode(lg.getId(), newMode);
}
} else if (source instanceof WorkspaceInfo) {
int nameIdx = event.getPropertyNames().indexOf("name");
if (nameIdx != -1) {
String oldName = (String) event.getOldValues().get(nameIdx);
String newName = (String) event.getNewValues().get(nameIdx);
updateWorkspaceNames(oldName, newName);
}
}
}
private void updateGroupMode(String id, Mode newMode) {
LayerGroupSummary summary = groupCache.get(id);
summary.mode = newMode;
}
private void updateContainedLayers(LayerGroupSummary groupSummary,
List<PublishedInfo> oldLayers, List<PublishedInfo> newLayers) {
// process layers that are no more contained
final HashSet<PublishedInfo> removedLayers = new HashSet<>(oldLayers);
removedLayers.removeAll(newLayers);
for (PublishedInfo removed : removedLayers) {
if (removed instanceof LayerInfo) {
String resourceId = ((LayerInfo) removed).getResource().getId();
Set<LayerGroupSummary> containers = resourceContainmentCache.get(resourceId);
if (containers != null) {
synchronized (resourceId) {
containers.remove(groupSummary);
if (containers.isEmpty()) {
resourceContainmentCache.remove(resourceId, containers);
}
}
}
} else {
LayerGroupInfo child = (LayerGroupInfo) removed;
LayerGroupSummary summary = groupCache.get(child.getId());
if (summary != null) {
summary.containerGroups.remove(groupSummary);
}
}
}
// add the layers that are newly contained
final HashSet<PublishedInfo> addedLayers = new HashSet<>(newLayers);
addedLayers.removeAll(oldLayers);
for (PublishedInfo added : addedLayers) {
if (added instanceof LayerInfo) {
String resourceId = ((LayerInfo) added).getResource().getId();
synchronized (resourceId) {
Set<LayerGroupSummary> containers = resourceContainmentCache
.computeIfAbsent(resourceId, CONCURRENT_SET_BUILDER);
containers.add(groupSummary);
}
} else {
LayerGroupInfo child = (LayerGroupInfo) added;
LayerGroupSummary summary = groupCache.get(child.getId());
if (summary != null) {
summary.containerGroups.add(groupSummary);
}
}
}
}
private void updateGroupWorkspace(String id, WorkspaceInfo newWorkspace) {
LayerGroupSummary summary = groupCache.get(id);
if (summary != null) {
summary.workspace = newWorkspace == null ? null : newWorkspace.getName();
}
}
private void updateGroupName(String id, String newName) {
LayerGroupSummary summary = groupCache.get(id);
if (summary != null) {
summary.name = newName;
}
}
private void updateWorkspaceNames(String oldName, String newName) {
groupCache.values().stream().filter(lg -> Objects.equals(lg.workspace, oldName))
.forEach(lg -> lg.workspace = newName);
}
@Override
public void handlePostModifyEvent(CatalogPostModifyEvent event) throws CatalogException {
// nothing to do here
}
@Override
public void reloaded() {
// rebuild the containment cache
buildLayerGroupCaches();
}
}
}