org.locationtech.geogig.storage.fs.FileRefDatabase.java Source code

Java tutorial

Introduction

Here is the source code for org.locationtech.geogig.storage.fs.FileRefDatabase.java

Source

/* 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.fs;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.locationtech.geogig.api.Ref.append;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Map;

import org.locationtech.geogig.api.ObjectId;
import org.locationtech.geogig.api.Platform;
import org.locationtech.geogig.api.Ref;
import org.locationtech.geogig.api.plumbing.ResolveGeogigDir;
import org.locationtech.geogig.repository.RepositoryConnectionException;
import org.locationtech.geogig.storage.AbstractRefDatabase;
import org.locationtech.geogig.storage.ConfigDatabase;

import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
import com.google.inject.Inject;

/**
 * Provides an implementation of a GeoGig ref database that utilizes the file system for the storage
 * of refs.
 */
public class FileRefDatabase extends AbstractRefDatabase {

    private static final Charset CHARSET = Charset.forName("UTF-8");

    private final Platform platform;

    private final ConfigDatabase configDB;

    /**
     * Constructs a new {@code FileRefDatabase} with the given platform.
     * 
     * @param platform the platform to use
     */
    @Inject
    public FileRefDatabase(Platform platform, ConfigDatabase configDB) {
        this.platform = platform;
        this.configDB = configDB;
    }

    /**
     * Creates the reference database.
     */
    @Override
    public void create() {
        Optional<URL> envHome = new ResolveGeogigDir(platform).call();
        checkState(envHome.isPresent(), "Not inside a geogig directory");

        final URL envURL = envHome.get();
        if (!"file".equals(envURL.getProtocol())) {
            throw new UnsupportedOperationException(
                    "This References Database works only against file system repositories. "
                            + "Repository location: " + envURL.toExternalForm());
        }
        File repoDir;
        try {
            repoDir = new File(envURL.toURI());
        } catch (URISyntaxException e) {
            throw Throwables.propagate(e);
        }
        File refs = new File(repoDir, "refs");
        if (!refs.exists() && !refs.mkdir()) {
            throw new IllegalStateException("Cannot create refs directory '" + refs.getAbsolutePath() + "'");
        }
    }

    /**
     * Closes the reference database.
     */
    @Override
    public void close() {
        // nothing to close
    }

    /**
     * @param name the name of the ref (e.g. {@code "refs/remotes/origin"}, etc).
     * @return the ref, or {@code null} if it doesn't exist
     */
    @Override
    public String getRef(String name) {
        checkNotNull(name);

        String value = getInternal(name);
        if (value == null) {
            return null;
        }
        try {
            ObjectId.valueOf(value);
        } catch (IllegalArgumentException e) {
            throw e;
        }
        return value;
    }

    /**
     * @param name the name of the symbolic ref (e.g. {@code "HEAD"}, etc).
     * @return the ref, or {@code null} if it doesn't exist
     */
    @Override
    public String getSymRef(String name) {
        checkNotNull(name);
        String value = getInternal(name);
        if (value == null) {
            return null;
        }
        if (!value.startsWith("ref: ")) {
            throw new IllegalArgumentException(name + " is not a symbolic ref: '" + value + "'");
        }
        value = value.substring("ref: ".length());
        return value;
    }

    private String getInternal(String name) {
        File refFile = toFile(name);
        if (!refFile.exists() || refFile.isDirectory()) {
            return null;
        }
        String value = readRef(refFile);
        return value;
    }

    /**
     * @param refName the name of the ref
     * @param refValue the value of the ref
     * @return {@code null} if the ref didn't exist already, its old value otherwise
     */
    @Override
    public void putRef(String refName, String refValue) {
        checkNotNull(refName);
        checkNotNull(refValue);
        try {
            ObjectId.forString(refValue);
        } catch (IllegalArgumentException e) {
            throw e;
        }
        store(refName, refValue);
    }

    /**
     * @param name the name of the symbolic ref
     * @param val the value of the symbolic ref
     * @return {@code null} if the ref didn't exist already, its old value otherwise
     */
    @Override
    public void putSymRef(String name, String val) {
        checkNotNull(name);
        checkNotNull(val);
        checkArgument(!name.equals(val), "Trying to store cyclic symbolic ref: %s", name);
        checkArgument(!name.startsWith("ref: "), "Wrong value, should not contain 'ref: ': %s -> '%s'", name, val);
        val = "ref: " + val;
        store(name, val);
    }

    /**
     * @param refName the name of the ref to remove (e.g. {@code "HEAD"},
     *        {@code "refs/remotes/origin"}, etc).
     * @return the value of the ref before removing it, or {@code null} if it didn't exist
     */
    @Override
    public String remove(String refName) {
        checkNotNull(refName);
        File refFile = toFile(refName);
        String oldRef;
        if (refFile.exists()) {
            oldRef = readRef(refFile);
            if (!refFile.delete()) {
                throw new RuntimeException("Unable to delete ref file '" + refFile.getAbsolutePath() + "'");
            }
        } else {
            oldRef = null;
        }
        return oldRef;
    }

    /**
     * @param refPath
     * @return
     */
    private File toFile(String refPath) {
        Optional<URL> envHome = new ResolveGeogigDir(platform).call();

        String[] path = refPath.split("/");

        try {
            File file = new File(envHome.get().toURI());
            for (String subpath : path) {
                file = new File(file, subpath);
            }
            return file;
        } catch (Exception e) {
            throw Throwables.propagate(e);
        }
    }

    private String readRef(final File refFile) {
        try {
            // make sure no other thread changes the ref as we read it
            synchronized (refFile.getCanonicalPath().intern()) {
                return Files.readFirstLine(refFile, CHARSET);
            }
        } catch (IOException e) {
            throw Throwables.propagate(e);
        }
    }

    /**
     * @param refName the full name of the ref (e.g.
     *        {@code refs/heads/master, HEAD, transaction/<tx id>/refs/orig/refs/heads/master, etc.}
     * @param refValue
     */
    private void store(String refName, String refValue) {
        final File refFile = toFile(refName);
        try {
            synchronized (refFile.getCanonicalPath().intern()) {
                Files.createParentDirs(refFile);
                checkState(refFile.exists() || refFile.createNewFile(), "Unable to create file for ref %s",
                        refFile);

                FileOutputStream fout = new FileOutputStream(refFile);
                try {
                    FileDescriptor fd = fout.getFD();
                    fout.write((refValue + "\n").getBytes(CHARSET));
                    fout.flush();
                    // force change to be persisted to disk
                    fd.sync();
                } finally {
                    fout.close();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            throw Throwables.propagate(e);
        }
    }

    @Override
    public Map<String, String> getAll() {
        Builder<String, String> builder = ImmutableMap.<String, String>builder();

        builder.putAll(getAll(Ref.HEADS_PREFIX));
        builder.putAll(getAll(Ref.TAGS_PREFIX));
        builder.putAll(getAll(Ref.REMOTES_PREFIX));

        addIfPresent(builder, Ref.CHERRY_PICK_HEAD);
        addIfPresent(builder, Ref.ORIG_HEAD);
        addIfPresent(builder, Ref.HEAD);
        addIfPresent(builder, Ref.WORK_HEAD);
        addIfPresent(builder, Ref.STAGE_HEAD);
        addIfPresent(builder, Ref.MERGE_HEAD);

        ImmutableMap<String, String> all = builder.build();
        return all;
    }

    private void addIfPresent(Builder<String, String> builder, String name) {
        String value = getInternal(name);
        if (value != null) {
            if (value.startsWith("ref: ")) {
                value = value.substring("ref: ".length());
            }
            builder.put(name, value);
        }
    }

    /**
     * @return all references under the specified namespace
     */
    @Override
    public Map<String, String> getAll(String namespace) {
        Preconditions.checkNotNull(namespace);
        File refsRoot;
        try {
            Optional<URL> envHome = new ResolveGeogigDir(platform).call();
            refsRoot = new File(envHome.get().toURI());
        } catch (Exception e) {
            throw Throwables.propagate(e);
        }
        if (namespace.endsWith("/")) {
            namespace = namespace.substring(0, namespace.length() - 1);
        }
        Map<String, String> refs = Maps.newTreeMap();
        findRefs(refsRoot, namespace, refs);
        return ImmutableMap.copyOf(refs);
    }

    private void findRefs(final File refsRoot, final String namespace, final Map<String, String> target) {
        String[] subdirs = namespace.split("/");
        File nsDir = refsRoot;
        for (String subdir : subdirs) {
            nsDir = new File(nsDir, subdir);
            if (!nsDir.exists() || !nsDir.isDirectory()) {
                return;
            }
        }
        addAll(nsDir, namespace, target);
    }

    private void addAll(File nsDir, String prefix, Map<String/* name */, String/* value */> target) {
        File[] children = nsDir.listFiles();
        for (File f : children) {
            final String fileName = f.getName();
            if (f.isDirectory()) {
                String namespace = append(prefix, fileName);
                addAll(f, namespace, target);
            } else if (fileName.length() == 0 || fileName.charAt(0) != '.') {
                String refName = append(prefix, fileName);
                String refValue = readRef(f);
                if (refValue.startsWith("ref: ")) {
                    refValue = refValue.substring("ref: ".length());
                }

                target.put(refName, refValue);
            }
        }
    }

    @Override
    public Map<String, String> removeAll(String namespace) {
        final File file = toFile(namespace);
        if (file.exists() && file.isDirectory()) {
            deleteDir(file);
        }
        return null;
    }

    /**
     * @param directory
     */
    private void deleteDir(final File directory) {
        if (!directory.exists()) {
            return;
        }
        File[] files = directory.listFiles();
        if (files == null) {
            throw new RuntimeException("Unable to list files of " + directory);
        }
        for (File f : files) {
            if (f.isDirectory()) {
                deleteDir(f);
            } else if (!f.delete()) {
                throw new RuntimeException("Unable to delete file " + f.getAbsolutePath());
            }
        }
        if (!directory.delete()) {
            throw new RuntimeException("Unable to delete directory " + directory.getAbsolutePath());
        }
    }

    @Override
    public void configure() throws RepositoryConnectionException {
        RepositoryConnectionException.StorageType.REF.configure(configDB, "file", "1.0");
    }

    @Override
    public void checkConfig() throws RepositoryConnectionException {
        RepositoryConnectionException.StorageType.REF.verify(configDB, "file", "1.0");
    }

    @Override
    public String toString() {
        Optional<URL> envHome = new ResolveGeogigDir(platform).call();
        return String.format("%s[geogig dir: %s]", getClass().getSimpleName(), envHome.orNull());
    }
}