/* (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;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.Nullable;
import org.geogig.geoserver.config.GeoServerGeoGigRepositoryResolver;
import org.geogig.geoserver.config.RepositoryInfo;
import org.geogig.geoserver.config.RepositoryManager;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.DataStoreInfo;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.NamespaceInfo;
import org.geoserver.catalog.ProjectionPolicy;
import org.geoserver.catalog.WorkspaceInfo;
import org.geotools.data.DataAccess;
import org.geotools.data.DataUtilities;
import org.geotools.feature.SchemaException;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.util.Converters;
import org.junit.Assert;
import org.junit.rules.ExternalResource;
import org.junit.rules.TemporaryFolder;
import org.locationtech.geogig.cli.test.functional.CLITestContextBuilder;
import org.locationtech.geogig.data.FeatureBuilder;
import org.locationtech.geogig.geotools.data.GeoGigDataStore;
import org.locationtech.geogig.geotools.data.GeoGigDataStoreFactory;
import org.locationtech.geogig.model.NodeRef;
import org.locationtech.geogig.model.ObjectId;
import org.locationtech.geogig.model.RevFeature;
import org.locationtech.geogig.model.RevFeatureType;
import org.locationtech.geogig.model.RevTree;
import org.locationtech.geogig.model.impl.RevFeatureBuilder;
import org.locationtech.geogig.model.impl.RevFeatureTypeBuilder;
import org.locationtech.geogig.plumbing.FindTreeChild;
import org.locationtech.geogig.plumbing.ResolveGeogigDir;
import org.locationtech.geogig.plumbing.RevObjectParse;
import org.locationtech.geogig.porcelain.AddOp;
import org.locationtech.geogig.porcelain.BranchCreateOp;
import org.locationtech.geogig.porcelain.CheckoutOp;
import org.locationtech.geogig.porcelain.CommitOp;
import org.locationtech.geogig.porcelain.ConfigOp;
import org.locationtech.geogig.porcelain.ConfigOp.ConfigAction;
import org.locationtech.geogig.porcelain.InitOp;
import org.locationtech.geogig.repository.AbstractGeoGigOp;
import org.locationtech.geogig.repository.Context;
import org.locationtech.geogig.repository.FeatureInfo;
import org.locationtech.geogig.repository.WorkingTree;
import org.locationtech.geogig.repository.impl.GeoGIG;
import org.locationtech.geogig.repository.impl.GlobalContextBuilder;
import org.locationtech.geogig.test.TestPlatform;
import org.opengis.feature.Feature;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.FeatureType;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
public class GeoGigTestData extends ExternalResource {
private static final Logger LOGGER = LoggerFactory.getLogger(GeoGigTestData.class);
private final TemporaryFolder tmpFolder;
private GeoGIG geogig;
private File repoDir;
public GeoGigTestData(TemporaryFolder tmpFolder) {
if (tmpFolder == null) {
this.tmpFolder = new TemporaryFolder();
try {
this.tmpFolder.create();
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
} else {
this.tmpFolder = tmpFolder;
}
}
public GeoGigTestData() {
this(null);
}
@Override
protected void before() throws Throwable {
setUp("testrepo");
}
public void setUp(String repoName) throws Exception {
this.geogig = createRepository(repoName);
}
@Override
protected void after() {
tearDown();
}
public void tearDown() {
try {
if (geogig != null) {
geogig.close();
geogig = null;
}
} finally {
RepositoryManager.close();
if (tmpFolder != null) {
tmpFolder.delete();
}
}
}
protected GeoGIG createGeogig() throws IOException {
return createRepository("testrepo");
}
public GeoGIG createRepository(String name) {
File dataDirectory = tmpFolder.getRoot();
repoDir = new File(dataDirectory, name);
Assert.assertTrue(repoDir.mkdir());
TestPlatform testPlatform = new TestPlatform(repoDir);
testPlatform.setUserHome(dataDirectory);
GlobalContextBuilder.builder(new CLITestContextBuilder(testPlatform));
Context context = GlobalContextBuilder.builder().build();
GeoGIG Geogig = new GeoGIG(context);
return Geogig;
}
public TemporaryFolder tmpFolder() {
return this.tmpFolder;
}
public File repoDirectory() {
return repoDir;
}
public GeoGIG getGeogig() {
return geogig;
}
private Object lastCommandResult;
@SuppressWarnings("unchecked")
public <T> T get() {
return (T) lastCommandResult;
}
public GeoGigTestData config(String key, String value) {
run(geogig.command(ConfigOp.class).setAction(ConfigAction.CONFIG_SET).setName(key)
.setValue(value));
return this;
}
public GeoGigTestData init() {
run(geogig.command(InitOp.class));
return this;
}
public GeoGigTestData add(@Nullable String... paths) {
AddOp command = geogig.command(AddOp.class);
if (paths != null) {
for (String path : paths) {
command.addPattern(path);
}
}
run(command);
return this;
}
public GeoGigTestData commit(@Nullable String message) {
run(geogig.command(CommitOp.class).setMessage(message).setAllowEmpty(message == null));
return this;
}
private Object run(AbstractGeoGigOp<?> cmd) {
Object result = cmd.call();
LOGGER.debug("ran cmd '{}', returned '{}'", cmd.getClass().getSimpleName(), result);
lastCommandResult = result;
return result;
}
public GeoGigTestData createTypeTree(String typeName, String typeSpec) {
FeatureType type;
try {
type = DataUtilities.createType(typeName, typeSpec);
} catch (SchemaException e) {
throw Throwables.propagate(e);
}
return createTypeTree(typeName, type);
}
public GeoGigTestData createTypeTree(String treePath, FeatureType type) {
geogig.getRepository().workingTree().createTypeTree(treePath, type);
return this;
}
public GeoGigTestData branch(String branchName) {
run(geogig.command(BranchCreateOp.class).setName(branchName));
return this;
}
public GeoGigTestData checkout(String branchOrCommit) {
run(geogig.command(CheckoutOp.class).setSource(branchOrCommit));
return this;
}
/**
* Inserts features in the working tree under the given parent tree path.
* <p>
* The parent tree must exist. The {@code featureSpecs} are of the form {@code featureSpec :=
* <id>=<attname>:<value>[;<attname>:<value>]+} . The parsing routine is as naive as it can be
* so do not use any '=', ':', or ';' in the values.
* <p>
* An empty value string is assumed to mean {@code null}. Any {@code <attname>} not provided
* (wrt. its type) will be left as {@code null} in the built feature.
* <p>
* An {@code <attname>} that doesn't exist in the feature type throws an unchecked exception.
*/
public GeoGigTestData insert(String parentTreePath, String... featureSpecs) {
SimpleFeatureType type = getType(parentTreePath);
SimpleFeatureBuilder fb = new SimpleFeatureBuilder(type);
Map<String, Map<String, String>> specs = parseFeatureSpecs(featureSpecs);
List<Feature> features = Lists.newArrayList();
for (Map.Entry<String, Map<String, String>> spec : specs.entrySet()) {
String fid = spec.getKey();
Map<String, String> attributes = spec.getValue();
fb.reset();
for (Map.Entry<String, String> e : attributes.entrySet()) {
String att = e.getKey();
String sval = e.getValue();
AttributeDescriptor descriptor = type.getDescriptor(att);
Class<?> binding = descriptor.getType().getBinding();
Object value = Converters.convert(sval, binding);
checkArgument(sval == null || value != null, "Unable to convert value '%s' to %s",
sval, binding.getName());
fb.set(att, value);
}
SimpleFeature feature = fb.buildFeature(fid);
features.add(feature);
}
return insert(parentTreePath, features.toArray(new Feature[features.size()]));
}
public GeoGigTestData insert(String parentTreePath, Feature... features) {
WorkingTree workingTree = geogig.getContext().workingTree();
for (Feature feature : features) {
RevFeatureType type = RevFeatureTypeBuilder.build(feature.getType());
geogig.getRepository().objectDatabase().put(type);
String path = NodeRef.appendChild(parentTreePath, feature.getIdentifier().getID());
FeatureInfo info = FeatureInfo.insert(RevFeatureBuilder.build(feature), type.getId(), path);
workingTree.insert(info);
}
return this;
}
private Map<String, Map<String, String>> parseFeatureSpecs(String[] featureSpecs) {
final String format = "<id>=<attname>:<value>[;<attname>:<value>]+";
Map<String, Map<String, String>> specs = Maps.newHashMap();
for (String spec : featureSpecs) {
String[] split = spec.split("=");
checkArgument(split.length == 2, "invalid feature spec. Expected '%s', got '%s'",
format, spec);
String fid = split[0];
checkArgument(!isNullOrEmpty(fid), "invalid feature fid. Expected '%s', got '%s'",
format, spec);
checkArgument(!specs.containsKey(fid), "Duplicate fid '%s' in feature spec '%s'", fid,
Arrays.asList(featureSpecs));
String atts = split[1];
String[] attSpecs = atts.split(";");
Map<String, String> attributes = Maps.newHashMap();
for (String attSpec : attSpecs) {
String[] attval = attSpec.split(":");
checkArgument(attval.length == 2,
"invalid attribute spec '%s'. Expected '%s', got '%s'", attSpec, format,
spec);
String attName = attval[0];
checkArgument(!isNullOrEmpty(attName),
"empty attribute name in attribute spec '%s'. Expected '%s', got '%s'",
attSpec, format, spec);
String attValue = attval[1];
if (isNullOrEmpty(attValue)) {
attValue = null;
}
attributes.put(attName, attValue);
}
specs.put(fid, attributes);
}
return specs;
}
@SuppressWarnings("unchecked")
private SimpleFeatureType getType(String parentTreePath) {
Context context = geogig.getContext();
List<NodeRef> featureTypeTrees = context.workingTree().getFeatureTypeTrees();
List<String> treeNames = Lists.transform(featureTypeTrees, new Function<NodeRef, String>() {
@Override
public String apply(NodeRef input) {
return input.path();
}
});
for (int i = 0; i < treeNames.size(); i++) {
String treeName = treeNames.get(i);
if (treeName.equals(parentTreePath)) {
ObjectId metadataId = featureTypeTrees.get(i).getMetadataId();
RevFeatureType revType = ((Optional<RevFeatureType>) run(
geogig.command(RevObjectParse.class).setObjectId(metadataId))).get();
SimpleFeatureType featureType = (SimpleFeatureType) revType.type();
return featureType;
}
}
throw new IllegalArgumentException(
String.format("No tree path named '%s' exists: %s", parentTreePath, treeNames));
}
public GeoGigTestData update(String featurePath, String attributeName, @Nullable Object value) {
SimpleFeature feature = getFeature(featurePath);
SimpleFeatureType featureType = feature.getFeatureType();
AttributeDescriptor descriptor = featureType.getDescriptor(attributeName);
Class<?> binding = descriptor.getType().getBinding();
Object actualValue = Converters.convert(value, binding);
checkArgument(value == null || actualValue != null, "Unable to convert value '%s' to %s",
value, binding.getName());
feature.setAttribute(attributeName, actualValue);
Context context = geogig.getContext();
WorkingTree workingTree = context.workingTree();
RevFeatureType type = RevFeatureTypeBuilder.build(featureType);
FeatureInfo info = FeatureInfo.insert(RevFeatureBuilder.build(feature), type.getId(), featurePath);
workingTree.insert(info);
return this;
}
public SimpleFeature getFeature(String featurePath) {
Context context = geogig.getContext();
WorkingTree workingTree = context.workingTree();
RevTree rootWorkingTree = workingTree.getTree();
@SuppressWarnings("unchecked")
Optional<NodeRef> ref = (Optional<NodeRef>) run(context.command(FindTreeChild.class)
.setParent(rootWorkingTree).setChildPath(featurePath));
checkArgument(ref.isPresent(), "No feature ref found: '%s'", featurePath);
NodeRef featureRef = ref.get();
SimpleFeatureType type = getType(featureRef.getParentPath());
@SuppressWarnings("unchecked")
Optional<RevFeature> revFeature = (Optional<RevFeature>) run(
context.command(RevObjectParse.class).setObjectId(featureRef.getObjectId()));
String id = featureRef.name();
Feature feature = new FeatureBuilder(RevFeatureTypeBuilder.build(type)).build(id, revFeature.get());
return (SimpleFeature) feature;
}
public CatalogBuilder newCatalogBuilder(Catalog catalog) {
return new CatalogBuilder(catalog);
}
public class CatalogBuilder {
public static final String NAMESPACE = "http://Geogig.org";
public static final String WORKSPACE = "Geogigtest";
public static final String STORE = "Geogigstore";
private String workspace = WORKSPACE;
private String nsUri = NAMESPACE;
private String storeName = STORE;
private Set<String> layerNames = new TreeSet<String>();
private Catalog catalog;
private CatalogBuilder(Catalog catalog) {
this.catalog = catalog;
}
public String workspaceName() {
return workspace;
}
public String namespaceUri() {
return nsUri;
}
public String storeName() {
return storeName;
}
public CatalogBuilder workspace(String workspace) {
this.workspace = workspace;
return this;
}
public CatalogBuilder namespace(String nsUri) {
this.nsUri = nsUri;
return this;
}
public CatalogBuilder store(String storeName) {
this.storeName = storeName;
return this;
}
public CatalogBuilder layer(String treeName) {
this.layerNames.add(treeName);
return this;
}
public CatalogBuilder addAllRepoLayers() {
List<NodeRef> featureTypeTrees = geogig.getContext().workingTree()
.getFeatureTypeTrees();
for (NodeRef ref : featureTypeTrees) {
layer(ref.name());
}
return this;
}
public Catalog build() {
NamespaceInfo ns = setUpNamespace(workspace, nsUri);
WorkspaceInfo ws = setUpWorkspace(workspace);
DataStoreInfo ds = setUpDataStore(ns, ws, storeName);
for (String layerName : layerNames) {
setUpLayer(ds, layerName);
}
return catalog;
}
private LayerInfo setUpLayer(DataStoreInfo ds, String layerName) {
FeatureTypeInfo ft = setUpFeatureType(ds, layerName);
LayerInfo li = catalog.getFactory().createLayer();
li.setResource(ft);
li.setEnabled(true);
li.setAdvertised(true);
String resourceName = ft.getName();
li.setName(resourceName);
catalog.add(li);
return catalog.getLayerByName(li.prefixedName());
}
private FeatureTypeInfo setUpFeatureType(DataStoreInfo ds, String layerName) {
FeatureTypeInfo ft = catalog.getFactory().createFeatureType();
ft.setStore(ds);
ft.setAdvertised(true);
ft.setEnabled(true);
ft.setName(layerName);
ft.setNativeName(layerName);
NamespaceInfo namespaceInfo = catalog.getNamespaceByPrefix(ds.getWorkspace().getName());
ft.setNamespace(namespaceInfo);
ft.setProjectionPolicy(ProjectionPolicy.FORCE_DECLARED);
WorkingTree workingTree = geogig.getRepository().workingTree();
Map<String, NodeRef> trees = Maps.uniqueIndex(workingTree.getFeatureTypeTrees(),
new Function<NodeRef, String>() {
@Override
public String apply(NodeRef treeRef) {
return treeRef.name();
}
});
NodeRef treeRef = trees.get(layerName);
FeatureType featureType = getType(treeRef.path());
CoordinateReferenceSystem nativeCRS = featureType.getCoordinateReferenceSystem();
ft.setNativeCRS(nativeCRS);
String srs = CRS.toSRS(nativeCRS);
ft.setSRS(srs);
ReferencedEnvelope box = new ReferencedEnvelope(nativeCRS);
treeRef.expand(box);
ft.setNativeBoundingBox(box);
ReferencedEnvelope latLonBounds;
try {
latLonBounds = box.transform(DefaultGeographicCRS.WGS84, true);
} catch (Exception e) {
throw Throwables.propagate(e);
}
ft.setLatLonBoundingBox(latLonBounds);
catalog.add(ft);
ft = catalog.getFeatureTypeByName(ft.prefixedName());
checkNotNull(ft);
return ft;
}
public DataStoreInfo setUpDataStore(NamespaceInfo ns, WorkspaceInfo ws, String storeName) {
DataStoreInfo ds = catalog.getFactory().createDataStore();
ds.setEnabled(true);
ds.setDescription("Test Geogig DataStore");
ds.setName(storeName);
ds.setType(GeoGigDataStoreFactory.DISPLAY_NAME);
ds.setWorkspace(ws);
Map<String, Serializable> connParams = ds.getConnectionParameters();
Optional<URL> GeogigDir = geogig.command(ResolveGeogigDir.class).call();
File repositoryUrl;
try {
repositoryUrl = new File(GeogigDir.get().toURI()).getParentFile();
} catch (URISyntaxException e) {
throw Throwables.propagate(e);
}
assertTrue(repositoryUrl.exists() && repositoryUrl.isDirectory());
// make sure the Repository is in the Repo Manager
RepositoryInfo info = new RepositoryInfo();
info.setLocation(geogig.getRepository().getLocation());
RepositoryManager.get().save(info);
connParams.put(GeoGigDataStoreFactory.REPOSITORY.key,
GeoServerGeoGigRepositoryResolver.getURI(info.getRepoName()));
connParams.put(GeoGigDataStoreFactory.DEFAULT_NAMESPACE.key, ns.getURI());
catalog.add(ds);
DataStoreInfo dsInfo = catalog.getDataStoreByName(ws.getName(), storeName);
assertNotNull(dsInfo);
assertEquals(GeoGigDataStoreFactory.DISPLAY_NAME, dsInfo.getType());
DataAccess<? extends FeatureType, ? extends Feature> dataStore;
try {
dataStore = dsInfo.getDataStore(null);
} catch (IOException e) {
throw Throwables.propagate(e);
}
assertNotNull(dataStore);
assertTrue(dataStore instanceof GeoGigDataStore);
ds = catalog.getDataStoreByName(ds.getWorkspace(), ds.getName());
checkNotNull(ds);
return ds;
}
public WorkspaceInfo setUpWorkspace(String workspace) {
WorkspaceInfo ws = catalog.getFactory().createWorkspace();
ws.setName(workspace);
catalog.add(ws);
ws = catalog.getWorkspaceByName(workspace);
checkNotNull(ws);
return ws;
}
public NamespaceInfo setUpNamespace(String workspace, String nsUri) {
NamespaceInfo ns = catalog.getFactory().createNamespace();
ns.setPrefix(workspace);
ns.setURI(nsUri);
catalog.add(ns);
ns = catalog.getNamespaceByPrefix(workspace);
checkNotNull(ns);
return ns;
}
}
}