/* (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.geogig.geoserver.gwc; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static org.geoserver.catalog.Predicates.and; import static org.geoserver.catalog.Predicates.equal; import java.io.Serializable; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.DataStoreInfo; import org.geoserver.catalog.LayerInfo; import org.geoserver.catalog.StoreInfo; import org.geoserver.catalog.util.CloseableIterator; import org.geoserver.gwc.GWC; import org.geoserver.gwc.layer.GeoServerTileLayer; import org.geowebcache.GeoWebCacheExtensions; import org.geowebcache.seed.TileBreeder; import org.locationtech.geogig.geotools.data.GeoGigDataStore; import org.locationtech.geogig.geotools.data.GeoGigDataStoreFactory; import org.locationtech.geogig.hooks.CannotRunGeogigOperationException; import org.locationtech.geogig.hooks.CommandHook; import org.locationtech.geogig.hooks.Scripting; import org.locationtech.geogig.model.Ref; import org.locationtech.geogig.model.SymRef; import org.locationtech.geogig.plumbing.RefParse; import org.locationtech.geogig.plumbing.UpdateRef; import org.locationtech.geogig.repository.AbstractGeoGigOp; import org.locationtech.geogig.repository.Context; import org.opengis.filter.Filter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Optional; import com.google.common.base.Stopwatch; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; /** * A "classpath" command hook that hooks onto the {@link UpdateRef} command and truncates GWC tiles * for any {@link GeoGigDataStore} configured in geoserver that's affected by the ref update. * <p> * Ref updates may come from remote repositories pushing changes to the geogig web api as exposed by * the {@code /geogig/<workspace>:<datastore>} repository entry points. * <p> * When such geogig command is caught, this hook looks for GWC tile layers configured so that the * change may affect them, figures out the "minimal bounds" geometry of the changeset, and issues * GWC truncate tasks appropriately. */ public class TruncateTilesOnUpdateRefHook implements CommandHook { private static final Logger LOGGER = LoggerFactory .getLogger(TruncateTilesOnUpdateRefHook.class); /** * {@link Catalog} filter to retrieve enabled layer infos backed by a geogig datastore */ private static final Filter GEOGIG_LAYERINFO_FILTER = and(equal("enabled", Boolean.TRUE), equal("resource.store.type", GeoGigDataStoreFactory.DISPLAY_NAME)); @Override public boolean appliesTo(Class<? extends AbstractGeoGigOp<?>> clazz) { return UpdateRef.class.equals(clazz); } @Override public <C extends AbstractGeoGigOp<?>> C pre(C command) throws CannotRunGeogigOperationException { /* * Store the ref name and its old value in the command's user properties to be used in the * post-hook if the operation was successful */ Map<String, Object> params = Scripting.getParamMap(command); String refName = (String) params.get("name"); command.getClientData().put("name", refName); if (Ref.WORK_HEAD.equals(refName) || Ref.STAGE_HEAD.equals(refName)) { command.getClientData().put("ignore", Boolean.TRUE); // ignore updates to work/stage heads, we only care of updates to branches return command; } Optional<Ref> currentValue = command.command(RefParse.class).setName(refName).call(); command.getClientData().put("oldValue", currentValue); LOGGER.debug("GWC geogig truncate pre-hook engaged for ref '{}'", refName); return command; } @SuppressWarnings("unchecked") @Override public <T> T post(AbstractGeoGigOp<T> command, @Nullable Object retVal, @Nullable RuntimeException exception) throws Exception { checkArgument(command instanceof UpdateRef); final UpdateRef cmd = (UpdateRef) command; final String refName = (String) cmd.getClientData().get("name"); checkState(refName != null, "refName not captured in pre-hook"); if (Boolean.TRUE.equals(cmd.getClientData().get("ignore"))) { LOGGER.debug("GWC geogig truncate post-hook returning, ref '{}' is ignored.", refName); return (T) retVal; } boolean success = exception == null; if (!success) { LOGGER.info( "GWC geogig truncate post-hook returning, UpdateRef operation failed on ref '{}'.", refName); return (T) retVal; } final GWC mediator = GWC.get(); if (mediator == null) { LOGGER.debug("GWC geogig truncate post-hook returning, GWC mediator not installed?."); return (T) retVal; } final Optional<Ref> oldValue = (Optional<Ref>) cmd.getClientData().get("oldValue"); final Optional<Ref> newValue = (Optional<Ref>) retVal;// == oldValue if the ref was deleted checkState(oldValue != null, "oldValue not captured in pre-hook"); if (oldValue.equals(newValue)) { LOGGER.debug("GWC geogig truncate post-hook returning, ref '{}' didn't change ({}).", refName, oldValue); return (T) retVal; } List<LayerInfo> affectedLayers; final String newRefName = newValue.get().getName(); Stopwatch sw = Stopwatch.createStarted(); affectedLayers = findAffectedLayers(mediator, command.context(), newRefName); LOGGER.debug(String.format( "GWC geogig truncate post-hook found %s affected layers on branch %s in %s.", affectedLayers.size(), refName, sw.stop())); for (LayerInfo layer : affectedLayers) { truncate(mediator, command.context(), layer, oldValue, newValue); } return (T) retVal; } private void truncate(GWC mediator, Context geogigContext, LayerInfo layer, Optional<Ref> oldValue, Optional<Ref> newValue) { GeoServerTileLayer tileLayer = mediator.getTileLayer(layer); if (tileLayer == null) { return; } TileBreeder breeder = GeoWebCacheExtensions.bean(TileBreeder.class); checkState(breeder != null);// if GWC wasn't installed it should have returned earlier TruncateHelper.issueTruncateTasks(geogigContext, oldValue, newValue, tileLayer, breeder); } private List<LayerInfo> findAffectedLayers(GWC mediator, Context context, String newRefName) { final Catalog catalog = mediator.getCatalog(); ListMultimap<StoreInfo, LayerInfo> affectedLayers = LinkedListMultimap.create(); CloseableIterator<LayerInfo> geogigLayers; geogigLayers = catalog.list(LayerInfo.class, GEOGIG_LAYERINFO_FILTER); try { while (geogigLayers.hasNext()) { LayerInfo layerInfo = geogigLayers.next(); // re check for the cascaded enabled() property if (!layerInfo.enabled()) { continue; } // now we're sure the layer info is enabled and so is its store // ignore if there's no tile layer for it if (!mediator.hasTileLayer(layerInfo)) { continue; } final DataStoreInfo store = (DataStoreInfo) layerInfo.getResource().getStore(); if (affectedLayers.containsKey(store)) { affectedLayers.put(store, layerInfo); } else { @Nullable final String dataStoreHead = findDataStoreHeadRefName(store, context); if (newRefName.equals(dataStoreHead)) { affectedLayers.put(store, layerInfo); } } } } finally { geogigLayers.close(); } return (List<LayerInfo>) affectedLayers.values(); } private String findDataStoreHeadRefName(DataStoreInfo store, Context context) { String dataStoreHead; Map<String, Serializable> storeParams = store.getConnectionParameters(); final String branch = (String) storeParams.get(GeoGigDataStoreFactory.BRANCH.key); final String head = (String) storeParams.get(GeoGigDataStoreFactory.HEAD.key); dataStoreHead = (head == null) ? branch : head; if (dataStoreHead == null) { Optional<Ref> currHead = context.command(RefParse.class).setName(Ref.HEAD).call(); if (!currHead.isPresent() || !(currHead.get() instanceof SymRef)) { // can't figure out the current branch, ignore? return null; } dataStoreHead = ((SymRef) currHead.get()).getTarget(); } else { Optional<Ref> storeRef = context.command(RefParse.class).setName(dataStoreHead).call(); if (storeRef.isPresent()) { Ref ref = storeRef.get(); if (ref instanceof SymRef) { dataStoreHead = ((SymRef) ref).getTarget(); } else { dataStoreHead = ref.getName(); } } else { LOGGER.info("HEAD '{}' configured for store '{}' does not resolve to a ref", dataStoreHead, store.getName()); dataStoreHead = null; } } return dataStoreHead; } }