/* Copyright (c) 2013-2014 Boundless and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/edl-v10.html * * Contributors: * Gabriel Roldan (Boundless) - initial implementation */ package org.locationtech.geogig.geotools.data; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; import javax.annotation.Nullable; import org.geotools.data.DataStore; import org.geotools.data.Transaction; import org.geotools.data.simple.SimpleFeatureSource; import org.geotools.data.store.ContentDataStore; import org.geotools.data.store.ContentEntry; import org.geotools.data.store.ContentState; import org.geotools.feature.NameImpl; import org.locationtech.geogig.api.Context; import org.locationtech.geogig.api.GeoGIG; import org.locationtech.geogig.api.GeogigTransaction; import org.locationtech.geogig.api.NodeRef; import org.locationtech.geogig.api.ObjectId; import org.locationtech.geogig.api.Ref; import org.locationtech.geogig.api.RevObject.TYPE; import org.locationtech.geogig.api.RevTree; import org.locationtech.geogig.api.SymRef; import org.locationtech.geogig.api.data.FindFeatureTypeTrees; import org.locationtech.geogig.api.plumbing.ForEachRef; import org.locationtech.geogig.api.plumbing.RefParse; import org.locationtech.geogig.api.plumbing.RevParse; import org.locationtech.geogig.api.plumbing.TransactionBegin; import org.locationtech.geogig.api.porcelain.AddOp; import org.locationtech.geogig.api.porcelain.CheckoutOp; import org.locationtech.geogig.api.porcelain.CommitOp; import org.locationtech.geogig.repository.WorkingTree; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.Name; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Throwables; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; /** * A GeoTools {@link DataStore} that serves and edits {@link SimpleFeature}s in a geogig repository. * <p> * Multiple instances of this kind of data store may be created against the same repository, * possibly working against different {@link #setHead(String) heads}. * <p> * A head is any commit in GeoGig. If a head has a branch pointing at it then the store allows * transactions, otherwise no data modifications may be made. * * A branch in Geogig is a separate line of history that may or may not share a common ancestor with * another branch. In the later case the branch is called "orphan" and by convention the default * branch is called "master", which is created when the geogig repo is first initialized, but does * not necessarily exist. * <p> * Every read operation (like in {@link #getFeatureSource(Name)}) reads committed features out of * the configured "head" branch. Write operations are only supported if a {@link Transaction} is * set, in which case a {@link GeogigTransaction} is tied to the geotools transaction by means of a * {@link GeogigTransactionState}. During the transaction, read operations will read from the * transaction's {@link WorkingTree} in order to "see" features that haven't been committed yet. * <p> * When the transaction is committed, the changes made inside that transaction are merged onto the * actual repository. If any other change was made to the repository meanwhile, a rebase will be * attempted, and the transaction commit will fail if the rebase operation finds any conflict. This * provides for optimistic locking and reduces thread contention. * */ public class GeoGigDataStore extends ContentDataStore implements DataStore { private final GeoGIG geogig; /** @see #setHead(String) */ private String refspec; /** When the configured head is not a branch, we disallow transactions */ private boolean allowTransactions = true; public GeoGigDataStore(GeoGIG geogig) { super(); Preconditions.checkNotNull(geogig); Preconditions.checkNotNull(geogig.getRepository(), "No repository exists at %s", geogig .getPlatform().pwd()); this.geogig = geogig; } @Override public void dispose() { super.dispose(); geogig.close(); } /** * @deprecated Use {@link setHead(String)} instead */ @Deprecated public void setBranch(@Nullable final String branchName) throws IllegalArgumentException { setHead(branchName); } /** * Instructs the datastore to operate against the specified refspec, or against the checked out * branch, whatever it is, if the argument is {@code null}. * * Editing capabilities are disabled if the refspec is not a local branch. * * @param refspec the name of the branch to work against, or {@code null} to default to the * currently checked out branch * @see #getConfiguredBranch() * @see #getOrFigureOutBranch() * @throws IllegalArgumentException if {@code refspec} is not null and no such commit exists in * the repository */ public void setHead(@Nullable final String refspec) throws IllegalArgumentException { if (refspec != null) { Optional<ObjectId> rev = getCommandLocator(null).command(RevParse.class) .setRefSpec(refspec).call(); if (!rev.isPresent()) { throw new IllegalArgumentException("Bad ref spec: " + refspec); } Optional<Ref> branchRef = getCommandLocator(null).command(RefParse.class) .setName(refspec).call(); if (branchRef.isPresent() && branchRef.get().getName().startsWith(Ref.HEADS_PREFIX)) { allowTransactions = true; } else { allowTransactions = false; } } else { allowTransactions = true; // when no branch name is set we assume we should make // transactions against the current HEAD } this.refspec = refspec; } /** * @deprecated Use getOrFigureOutHead instead. */ @Deprecated public String getOrFigureOutBranch() { return getOrFigureOutHead(); } public String getOrFigureOutHead() { String branch = getConfiguredHead(); if (branch != null) { return branch; } return getCheckedOutBranch(); } public GeoGIG getGeogig() { return geogig; } /** * @deprecated Use getConfiguredHead instead. */ @Deprecated public String getConfiguredBranch() { return getConfiguredHead(); } /** * @return the configured refspec of the commit this datastore works against, or {@code null} if * no head in particular has been set, meaning the data store works against whatever the * currently checked out branch is. */ @Nullable public String getConfiguredHead() { return this.refspec; } /** * @return whether or not we can support transactions against the configured head */ public boolean isAllowTransactions() { return this.allowTransactions; } /** * @return the name of the currently checked out branch in the repository, not necessarily equal * to {@link #getConfiguredBranch()}, or {@code null} in the (improbable) case HEAD is * on a dettached state (i.e. no local branch is currently checked out) */ @Nullable public String getCheckedOutBranch() { Optional<Ref> head = getCommandLocator(null).command(RefParse.class).setName(Ref.HEAD) .call(); if (!head.isPresent()) { return null; } Ref headRef = head.get(); if (!(headRef instanceof SymRef)) { return null; } String refName = ((SymRef) headRef).getTarget(); Preconditions.checkState(refName.startsWith(Ref.HEADS_PREFIX)); String branchName = refName.substring(Ref.HEADS_PREFIX.length()); return branchName; } public ImmutableList<String> getAvailableBranches() { ImmutableSet<Ref> heads = getCommandLocator(null).command(ForEachRef.class) .setPrefixFilter(Ref.HEADS_PREFIX).call(); List<String> list = Lists.newArrayList(Collections2.transform(heads, new Function<Ref, String>() { @Override public String apply(Ref ref) { String branchName = ref.getName().substring(Ref.HEADS_PREFIX.length()); return branchName; } })); Collections.sort(list); return ImmutableList.copyOf(list); } public Context getCommandLocator(@Nullable Transaction transaction) { Context commandLocator = null; if (transaction != null && !Transaction.AUTO_COMMIT.equals(transaction)) { GeogigTransactionState state; state = (GeogigTransactionState) transaction.getState(GeogigTransactionState.class); Optional<GeogigTransaction> geogigTransaction = state.getGeogigTransaction(); if (geogigTransaction.isPresent()) { commandLocator = geogigTransaction.get(); } } if (commandLocator == null) { commandLocator = geogig.getContext(); } return commandLocator; } public Name getDescriptorName(NodeRef treeRef) { Preconditions.checkNotNull(treeRef); Preconditions.checkArgument(TYPE.TREE.equals(treeRef.getType())); Preconditions.checkArgument(!treeRef.getMetadataId().isNull(), "NodeRef '%s' is not a feature type reference", treeRef.path()); return new NameImpl(getNamespaceURI(), NodeRef.nodeFromPath(treeRef.path())); } public NodeRef findTypeRef(Name typeName, @Nullable Transaction tx) { Preconditions.checkNotNull(typeName); final String localName = typeName.getLocalPart(); final List<NodeRef> typeRefs = findTypeRefs(tx); Collection<NodeRef> matches = Collections2.filter(typeRefs, new Predicate<NodeRef>() { @Override public boolean apply(NodeRef input) { return NodeRef.nodeFromPath(input.path()).equals(localName); } }); switch (matches.size()) { case 0: throw new NoSuchElementException(String.format("No tree ref matched the name: %s", localName)); case 1: return matches.iterator().next(); default: throw new IllegalArgumentException(String.format( "More than one tree ref matches the name %s: %s", localName, matches)); } } @Override protected ContentState createContentState(ContentEntry entry) { return new ContentState(entry); } @Override protected ImmutableList<Name> createTypeNames() throws IOException { List<NodeRef> typeTrees = findTypeRefs(Transaction.AUTO_COMMIT); Function<NodeRef, Name> function = new Function<NodeRef, Name>() { @Override public Name apply(NodeRef treeRef) { return getDescriptorName(treeRef); } }; return ImmutableList.copyOf(Collections2.transform(typeTrees, function)); } private List<NodeRef> findTypeRefs(@Nullable Transaction tx) { final String rootRef = getRootRef(tx); Context commandLocator = getCommandLocator(tx); List<NodeRef> typeTrees = commandLocator.command(FindFeatureTypeTrees.class) .setRootTreeRef(rootRef).call(); return typeTrees; } String getRootRef(@Nullable Transaction tx) { final String rootRef; if (null == tx || Transaction.AUTO_COMMIT.equals(tx)) { rootRef = getOrFigureOutBranch(); } else { rootRef = Ref.WORK_HEAD; } return rootRef; } @Override protected GeogigFeatureStore createFeatureSource(ContentEntry entry) throws IOException { return new GeogigFeatureStore(entry); } /** * Creates a new feature type tree on the {@link #getOrFigureOutBranch() current branch}. * <p> * Implementation detail: the operation is the homologous to starting a transaction, checking * out the current/configured branch, creating the type tree inside the transaction, issueing a * geogig commit, and committing the transaction for the created tree to be merged onto the * configured branch. */ @Override public void createSchema(SimpleFeatureType featureType) throws IOException { if (!allowTransactions) { throw new IllegalStateException("Configured head " + refspec + " is not a branch; transactions are not supported."); } GeogigTransaction tx = getCommandLocator(null).command(TransactionBegin.class).call(); boolean abort = false; try { String treePath = featureType.getName().getLocalPart(); // check out the datastore branch on the transaction space final String branch = getOrFigureOutBranch(); tx.command(CheckoutOp.class).setForce(true).setSource(branch).call(); // now we can use the transaction working tree with the correct branch checked out WorkingTree workingTree = tx.workingTree(); workingTree.createTypeTree(treePath, featureType); tx.command(AddOp.class).addPattern(treePath).call(); tx.command(CommitOp.class).setMessage("Created feature type tree " + treePath).call(); tx.commit(); } catch (IllegalArgumentException alreadyExists) { abort = true; throw new IOException(alreadyExists.getMessage(), alreadyExists); } catch (Exception e) { abort = true; throw Throwables.propagate(e); } finally { if (abort) { tx.abort(); } } } // Deliberately leaving the @Override annotation commented out so that the class builds // both against GeoTools 10.x and 11.x (as the method was added to DataStore in 11.x) // @Override public void removeSchema(Name name) throws IOException { throw new UnsupportedOperationException( "removeSchema not yet supported by geogig DataStore"); } // Deliberately leaving the @Override annotation commented out so that the class builds // both against GeoTools 10.x and 11.x (as the method was added to DataStore in 11.x) // @Override public void removeSchema(String name) throws IOException { throw new UnsupportedOperationException( "removeSchema not yet supported by geogig DataStore"); } public static enum ChangeType { ADDED, REMOVED, CHANGED_NEW, CHANGED_OLD; } /** * Builds a FeatureSource (read-only) that fetches features out of the differences between two * root trees: a provided tree-ish as the left side of the comparison, and the datastore's * configured HEAD as the right side of the comparison. * <p> * E.g., to get all features of a given feature type that were removed between a given commit * and its parent: * * <pre> * <code> * String commitId = ... * GeoGigDataStore store = new GeoGigDataStore(geogig); * store.setHead(commitId); * FeatureSource removed = store.getDiffFeatureSource("roads", commitId + "~1", ChangeType.REMOVED); * </code> * </pre> * * @param typeName the feature type name to look up a type tree for in the datastore's current * {@link #getOrFigureOutHead() HEAD} * @param oldRoot a tree-ish string that resolves to the ROOT tree to be used as the left side * of the diff * @param changeType the type of change between {@code oldRoot} and * {@link #getOrFigureOutHead() head} to pick as the features to return. * @return a feature source whose features are computed out of the diff between the feature type * diffs between the given {@code oldRoot} and the datastore's * {@link #getOrFigureOutHead() HEAD}. */ public SimpleFeatureSource getDiffFeatureSource(final String typeName, final String oldRoot, final ChangeType changeType) throws IOException { Preconditions.checkNotNull(typeName, "typeName"); Preconditions.checkNotNull(oldRoot, "oldRoot"); Preconditions.checkNotNull(changeType, "changeType"); final Name name = name(typeName); final ContentEntry entry = ensureEntry(name); GeogigFeatureSource featureSource = new GeogigFeatureSource(entry); featureSource.setTransaction(Transaction.AUTO_COMMIT); featureSource.setChangeType(changeType); if (ObjectId.NULL.toString().equals(oldRoot) || RevTree.EMPTY_TREE_ID.toString().equals(oldRoot)) { featureSource.setOldRoot(null); } else { featureSource.setOldRoot(oldRoot); } return featureSource; } }