/* 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: * Johnathan Garrett (LMN Solutions) - initial implementation */ package org.locationtech.geogig.storage; import static com.google.common.base.Preconditions.checkArgument; import static org.locationtech.geogig.api.Ref.TRANSACTIONS_PREFIX; import static org.locationtech.geogig.api.Ref.append; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; import java.util.concurrent.TimeoutException; import org.locationtech.geogig.api.Context; import org.locationtech.geogig.api.GeogigTransaction; import org.locationtech.geogig.api.Ref; import org.locationtech.geogig.api.plumbing.TransactionBegin; import org.locationtech.geogig.api.plumbing.TransactionEnd; import org.locationtech.geogig.repository.Index; import org.locationtech.geogig.repository.WorkingTree; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.MapDifference; import com.google.common.collect.MapDifference.ValueDifference; import com.google.common.collect.Maps; /** * A {@link RefDatabase} decorator for a specific {@link GeogigTransaction transaction}. * <p> * This decorator creates a transaction specific namespace under the * {@code transactions/<transaction id>} path, and maps all query and storage methods to that * namespace. * <p> * This is so that every command created through the {@link GeogigTransaction transaction} used as a * {@link Context}, as well as the transaction specific {@link Index} and {@link WorkingTree} , are * given this instance of {@code RefDatabase} and can do its work without ever noticing its * "running inside a transaction". For the command nothing changes. * <p> * {@link TransactionRefDatabase#create() create()} shall be called before this decorator gets used * in order for the transaction refs namespace to be created and all original references copied in * there, and {@link TransactionRefDatabase#close() close()} for the transaction refs namespace to * be deleted. * * @see GeogigTransaction * @see TransactionBegin * @see TransactionEnd */ public class TransactionRefDatabase implements RefDatabase { private static final Logger LOGGER = LoggerFactory.getLogger(TransactionRefDatabase.class); private RefDatabase refDb; private final String txRootNamespace; private final String txNamespace; private final String txOrigNamespace; public TransactionRefDatabase(final RefDatabase refDb, final UUID transactionId) { this.refDb = refDb; this.txRootNamespace = append(TRANSACTIONS_PREFIX, transactionId.toString()); this.txNamespace = append(txRootNamespace, "changed"); this.txOrigNamespace = append(txRootNamespace, "orig"); } @Override public void lock() throws TimeoutException { refDb.lock(); } @Override public void unlock() { refDb.unlock(); } @Override public void create() { refDb.create(); // copy HEADS copyIfPresent(Ref.HEAD, Ref.WORK_HEAD, Ref.STAGE_HEAD, Ref.CHERRY_PICK_HEAD, Ref.MERGE_HEAD, Ref.ORIG_HEAD); copyAll(refDb.getAll(Ref.HEADS_PREFIX)); copyAll(refDb.getAll(Ref.REMOTES_PREFIX)); copyAll(refDb.getAll(Ref.TAGS_PREFIX)); } private void copyIfPresent(String... refNames) { for (String refName : refNames) { String origValue = readRef(refName); if (origValue != null) { String origInternal = toOrigInternal(refName); LOGGER.debug("copy {} as {}", refName, origInternal); insertRef(origInternal, origValue); } } } private String readRef(String name) { String value = null; try { value = refDb.getRef(name); } catch (IllegalArgumentException e) { value = refDb.getSymRef(name); } return value; } private void copyAll(Map<String, String> origRefs) { Map<String, String> thisTxRefs = toOrigInternal(origRefs); for (Entry<String, String> entry : thisTxRefs.entrySet()) { insertRef(entry.getKey(), entry.getValue()); } } private void insertRef(String name, String value) { if (value.contains("/")) { refDb.putSymRef(name, value); } else { refDb.putRef(name, value); } } /** * Releases all the references for this transaction, but does not close the original * {@link RefDatabase} */ @Override public void close() { refDb.removeAll(this.txRootNamespace); } /** * Gets the requested ref value from {@code transactions/<tx id>/<name>} */ @Override public String getRef(final String name) { String internalName; String value; if (name.startsWith("changed") || name.startsWith("orig")) { internalName = append(txRootNamespace, name); value = refDb.getRef(internalName); } else { internalName = toInternal(name); value = refDb.getRef(internalName); if (value == null) { internalName = toOrigInternal(name); value = refDb.getRef(internalName); } } return value; } @Override public String getSymRef(final String name) { String internalName; String value; if (name.startsWith("changed") || name.startsWith("orig")) { internalName = append(txRootNamespace, name); value = refDb.getSymRef(internalName); } else { internalName = toInternal(name); value = refDb.getSymRef(internalName); if (value == null) { internalName = toOrigInternal(name); value = refDb.getSymRef(internalName); } } return value; } @Override public void putRef(final String refName, final String refValue) { String internalName = toInternal(refName); LOGGER.debug("update {} as {}", refName, internalName); refDb.putRef(internalName, refValue); } @Override public void putSymRef(final String name, final String val) { checkArgument(!name.startsWith("ref: "), "Wrong value, should not contain 'ref: ': %s -> '%s'", name, val); String internalName = toInternal(name); LOGGER.debug("update {} as {}", name, internalName); refDb.putSymRef(internalName, val); } @Override public String remove(final String refName) { return refDb.remove(toInternal(refName)); } @Override public Map<String, String> getAll() { return getAll(""); } @Override public Map<String, String> getAll(final String prefix) { Map<String, String> originals = refDb.getAll(append(this.txOrigNamespace, prefix)); Map<String, String> changed = refDb.getAll(append(this.txNamespace, prefix)); Map<String, String> externalOriginals = toExternal(originals); Map<String, String> externalChanged = toExternal(changed); Map<String, String> composite = Maps.newHashMap(externalOriginals); // Overwrite originals composite.putAll(externalChanged); return composite; } /** * The names of the refs that either have changed from their original value or didn't exist at * the time this method is called */ public ImmutableSet<String> getChangedRefs() { Map<String, String> externalOriginals; Map<String, String> externalChanged; { Map<String, String> originals = refDb.getAll(this.txOrigNamespace); Map<String, String> changed = refDb.getAll(this.txNamespace); externalOriginals = toExternal(originals); externalChanged = toExternal(changed); } MapDifference<String, String> difference; difference = Maps.difference(externalOriginals, externalChanged); Map<String, String> changes = new HashMap<>(); // include all new refs changes.putAll(difference.entriesOnlyOnRight()); // include all changed refs, with the new values for (Map.Entry<String, ValueDifference<String>> e : difference.entriesDiffering() .entrySet()) { String name = e.getKey(); ValueDifference<String> valueDifference = e.getValue(); String newValue = valueDifference.rightValue(); changes.put(name, newValue); } return ImmutableSet.copyOf(changes.keySet()); } @Override public Map<String, String> removeAll(String namespace) { final String txMappedNamespace = toInternal(namespace); Map<String, String> removed = refDb.removeAll(txMappedNamespace); Map<String, String> external = toExternal(removed); return external; } private Map<String, String> toExternal(final Map<String, String> transactionEntries) { Map<String, String> transformed = Maps.newHashMap(); for (Entry<String, String> entry : transactionEntries.entrySet()) { String txName = entry.getKey(); String txValue = entry.getValue(); String transformedName = toExternal(txName); String transformedValue = toExternalValue(txValue); transformed.put(transformedName, transformedValue); } return ImmutableMap.copyOf(transformed); } private String toInternal(String name) { return append(txNamespace, name); } private String toExternal(String name) { if (name.startsWith(this.txNamespace)) { return Ref.child(this.txNamespace, name); } else if (name.startsWith(this.txOrigNamespace)) { return Ref.child(this.txOrigNamespace, name); } return name; } private String toOrigInternal(String name) { String origName = append(txOrigNamespace, name); return origName; } private Map<String, String> toOrigInternal(final Map<String, String> orig) { Map<String, String> transformed = Maps.newHashMap(); for (Entry<String, String> entry : orig.entrySet()) { String origName = entry.getKey(); String origValue = entry.getValue(); String transformedName = toOrigInternal(origName); String transformedValue = origValue; LOGGER.debug("copy {} as {}", origName, transformedName); transformed.put(transformedName, transformedValue); } return ImmutableMap.copyOf(transformed); } private String toExternalValue(String origValue) { String txValue = origValue; boolean isSymRef = origValue.startsWith("ref: "); if (isSymRef) { String val = origValue.substring("ref: ".length()); if (val.startsWith(this.txNamespace)) { val = val.substring(this.txNamespace.length()); if (val.length() > 0 && val.charAt(0) == '/') { val = val.substring(1); } } txValue = "ref: " + val; } return txValue; } @Override public void configure() { // No-op } @Override public void checkConfig() { // No-op } }