/* Copyright (c) 2012-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.storage.bdbje; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import java.util.List; import javax.annotation.Nullable; import org.locationtech.geogig.api.Platform; import org.locationtech.geogig.api.RevTree; import org.locationtech.geogig.api.plumbing.ResolveGeogigDir; import org.locationtech.geogig.api.plumbing.merge.Conflict; import org.locationtech.geogig.storage.AbstractStagingDatabase; import org.locationtech.geogig.storage.ConfigDatabase; import org.locationtech.geogig.storage.ObjectDatabase; import com.google.common.base.Charsets; import com.google.common.base.Optional; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.io.Files; import com.google.common.io.LineProcessor; /** * The Index (or Staging Area) object database. * <p> * This is a composite object database holding a reference to the actual repository object database * and a separate object database for the staging area itself. * <p> * Object look ups are first performed against the staging area database. If the object is not * found, then the look up is deferred to the actual repository database. * <p> * Object writes are always performed against the staging area object database. * <p> * The staging area database holds references to two root {@link RevTree trees}, one for the staged * objects and another one for the unstaged objects. When objects are added/changed/deleted to/from * the index, those modifications are written to the unstaged root tree. When objects are staged to * be committed, the unstaged objects are moved to the staged root tree. * <p> * A diff operation between the repository root tree and the index unstaged root tree results in the * list of unstaged objects. * <p> * A diff operation between the repository root tree and the index staged root tree results in the * list of staged objects. * */ abstract class JEStagingDatabase extends AbstractStagingDatabase { /** * Name of the BDB JE environment inside the .geogig folder used for the staging database */ static final String ENVIRONMENT_NAME = "index"; private Platform platform; protected final ConfigDatabase configDB; private File repositoryDirectory; public JEStagingDatabase(final ObjectDatabase repositoryDb, final Supplier<JEObjectDatabase> stagingDbSupplier, final Platform platform, final ConfigDatabase configDB) { super(Suppliers.ofInstance(repositoryDb), stagingDbSupplier); this.platform = platform; this.configDB = configDB; } @Override public void open() { super.open(); Optional<URL> repoPath = new ResolveGeogigDir(platform).call(); try { File repoLocation = new File(repoPath.get().toURI()); this.repositoryDirectory = repoLocation; } catch (URISyntaxException e1) { Throwables.propagate(e1); } } // TODO: // ***************************************************************************************** // The following methods are a temporary implementation of conflict storage that relies on a // conflict file in the index folder // ***************************************************************************************** @Override public boolean hasConflicts(String namespace) { final Object monitor = resolveConflictsMonitor(namespace); if (monitor == null) { return false; } synchronized (monitor) { final File file = resolveConflictsFile(namespace); return file.exists() && file.length() > 0; } } /** * Gets all conflicts that match the specified path filter. * * @param namespace the namespace of the conflict * @param pathFilter the path filter, if this is not defined, all conflicts will be returned * @return the list of conflicts */ @Override public List<Conflict> getConflicts(@Nullable String namespace, @Nullable final String pathFilter) { final Object monitor = resolveConflictsMonitor(namespace); if (null == monitor) { return ImmutableList.of(); } synchronized (monitor) { final File file = resolveConflictsFile(namespace); if (null == file || !file.exists() || file.length() == 0) { return ImmutableList.of(); } List<Conflict> conflicts; try { conflicts = Files.readLines(file, Charsets.UTF_8, new LineProcessor<List<Conflict>>() { List<Conflict> conflicts = Lists.newArrayList(); @Override public List<Conflict> getResult() { return conflicts; } @Override public boolean processLine(String s) throws IOException { Conflict c = Conflict.valueOf(s); if (pathFilter == null) { conflicts.add(c); } else if (c.getPath().startsWith(pathFilter)) { conflicts.add(c); } return true; } }); } catch (IOException e) { throw Throwables.propagate(e); } return conflicts; } } /** * Adds a conflict to the database. * * @param namespace the namespace of the conflict * @param conflict the conflict to add */ @Override public void addConflict(@Nullable String namespace, Conflict conflict) { final Object monitor = resolveConflictsMonitor(namespace); checkState(monitor != null, "Either not inside a repository directory or the staging area is closed"); synchronized (monitor) { Optional<File> fileOp = findOrCreateConflictsFile(namespace); checkState(fileOp.isPresent()); try { final File file = fileOp.get(); Files.append(conflict.toString() + "\n", file, Charsets.UTF_8); } catch (IOException e) { throw Throwables.propagate(e); } } } /** * @return the object to synchronize on, or null if not inside a geogig repository */ @Nullable private Object resolveConflictsMonitor(@Nullable final String namespace) { final File file = resolveConflictsFile(namespace); Object monitor = null; if (file != null) { try { monitor = file.getCanonicalPath().intern(); } catch (IOException e) { throw Throwables.propagate(e); } } return monitor; } /** * Removes a conflict from the database. * * @param namespace the namespace of the conflict * @param path the path of feature whose conflict should be removed */ @Override public void removeConflict(@Nullable String namespace, final String path) { checkNotNull(path, "path is null"); final Object monitor = resolveConflictsMonitor(namespace); checkState(monitor != null, "Either not inside a repository directory or the staging area is closed"); synchronized (monitor) { final File file = resolveConflictsFile(namespace); if (file == null || !file.exists()) { return; } try { List<Conflict> conflicts = getConflicts(namespace, null); StringBuilder sb = new StringBuilder(); for (Conflict conflict : conflicts) { if (!path.equals(conflict.getPath())) { sb.append(conflict.toString() + "\n"); } } String s = sb.toString(); if (s.isEmpty()) { file.delete(); } else { Files.write(s, file, Charsets.UTF_8); } } catch (IOException e) { throw Throwables.propagate(e); } } } /** * Gets the specified conflict from the database. * * @param namespace the namespace of the conflict * @param path the conflict to retrieve * @return the conflict, or {@link Optional#absent()} if it was not found */ @Override public Optional<Conflict> getConflict(@Nullable String namespace, final String path) { final Object monitor = resolveConflictsMonitor(namespace); if (null == monitor) { return Optional.absent(); } synchronized (monitor) { File file = resolveConflictsFile(namespace); if (file == null || !file.exists()) { return Optional.absent(); } Conflict conflict = null; try { conflict = Files.readLines(file, Charsets.UTF_8, new LineProcessor<Conflict>() { Conflict conflict = null; @Override public Conflict getResult() { return conflict; } @Override public boolean processLine(String s) throws IOException { Conflict c = Conflict.valueOf(s); if (c.getPath().equals(path)) { conflict = c; return false; } else { return true; } } }); } catch (IOException e) { throw Throwables.propagate(e); } return Optional.fromNullable(conflict); } } private Optional<File> findOrCreateConflictsFile(@Nullable String namespace) { final Object monitor = resolveConflictsMonitor(namespace); checkState(Thread.holdsLock(monitor)); final File file = resolveConflictsFile(namespace); if (null == file) { return Optional.absent(); } if (!file.exists()) { try { Files.createParentDirs(file); file.createNewFile(); } catch (IOException e) { throw Throwables.propagate(e); } } return Optional.of(file); } /** * @return {@code null} if the database is closed or its location cannot be determined, the * conflicts file that belongs to the given namespace otherwise, which may or may not * exist */ @Nullable private File resolveConflictsFile(@Nullable String namespace) { if (namespace == null) { namespace = "conflicts"; } File file = null; if (isOpen()) { file = new File(repositoryDirectory, namespace); } return file; } /** * Removes all conflicts from the database. * * @param namespace the namespace of the conflicts to remove */ @Override public void removeConflicts(@Nullable String namespace) { final Object monitor = resolveConflictsMonitor(namespace); checkState(monitor != null, "Either not inside a repository directory or the staging area is closed"); synchronized (monitor) { File file = resolveConflictsFile(namespace); if (file != null && file.exists()) { checkState(file.delete(), "Unable to delete conflicts file %s", file); } } } }