com.benhumphreys.jgitcassandra.store.RefStore.java Source code

Java tutorial

Introduction

Here is the source code for com.benhumphreys.jgitcassandra.store.RefStore.java

Source

/*
 * A Cassandra backend for JGit
 * Copyright 2014-2015 Ben Humphreys
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.benhumphreys.jgitcassandra.store;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdRef;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.SymbolicRef;

import com.benhumphreys.jgitcassandra.Utils;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.Statement;
import com.datastax.driver.core.querybuilder.QueryBuilder;

/**
 * Provides access to the Ref store.
 * <p/>
 * This class provides map (i.e. key/value) semantics, mapping a "name" to
 * a Ref. The map exists within a namespace identified by the "keyspace".
 * Key/value pairs are distinct within a keyspace.
 */
public class RefStore {
    /**
     * Cassandra fetch size. This won't limit the size of a query result set, but
     * a large result set will be broken up into multiple fetches if the result set
     * size exceeds FETCH_SIZE
     */
    private static final int FETCH_SIZE = 100;

    /**
     * Refs table name
     */
    private static final String TABLE_NAME = "refs";

    /**
     * The keyspace acts as a namespace
     */
    private final String keyspace;

    /**
     * A Cassandra session instance
     */
    private final Session session;

    /**
     * Constructor
     *
     * @param keyspace the Cassandra keyspace
     * @param session  a Cassandra session instance
     * @throws NullPointerException if either of the parameters are null
     * @throws IOException          if an exception occurs when communicating to the
     *                              database
     */
    public RefStore(String keyspace, Session session) throws IOException {
        if (keyspace == null || session == null) {
            throw new NullPointerException();
        }
        this.keyspace = keyspace;
        this.session = session;
        createSchemaIfNotExist();
    }

    /**
     * Returns the Ref to which the specified name is mapped
     *
     * @param name the name whose associated value is to be returned
     * @return the Ref to which the specified name is mapped, or null if
     * the store contains no mapping for the name
     * @throws IOException if an exception occurs when communicating to the
     *                     database
     */
    public Ref get(String name) throws IOException {
        try {
            Statement stmt = QueryBuilder.select().all().from(keyspace, TABLE_NAME)
                    .where(QueryBuilder.eq("name", name));
            ResultSet results = session.execute(stmt);
            Ref r = rowToRef(results.one());
            if (!results.isExhausted()) {
                throw new IllegalStateException("Multiple rows for a single ref: " + name);
            }
            return r;
        } catch (RuntimeException e) {
            e.printStackTrace();
            throw new IOException(e);
        }
    }

    /**
     * @return a Collection view of all refs in the store
     * @throws IOException if an exception occurs when communicating to the
     *                     database
     */
    public Collection<Ref> values() throws IOException {
        try {
            List<Ref> refs = new ArrayList<Ref>();
            Statement stmt = QueryBuilder.select().all().from(keyspace, TABLE_NAME);
            stmt.setFetchSize(FETCH_SIZE);
            ResultSet results = session.execute(stmt);
            for (Row row : results) {
                refs.add(rowToRef(row));
            }
            return refs;
        } catch (RuntimeException e) {
            e.printStackTrace();
            throw new IOException(e);
        }
    }

    /**
     * If the specified "name" is not already associated with a "Ref",
     * associate it with the given "Ref".
     *
     * @param name   the name (i.e. key) with which the specified value is
     *               to be associated
     * @param newRef the Ref to be associated with the specified name
     * @return the previous Ref associated with the specified name, or null if
     * there was no mapping for the name.
     * @throws IOException if an exception occurs when communicating to the
     *                     database
     */
    public Ref putIfAbsent(String name, Ref newRef) throws IOException {
        final Ref cur = get(name);
        if (cur == null) {
            putRef(name, newRef);
        }
        return cur;
    }

    /**
     * Replaces the entry for a name only if currently mapped to a given Ref.
     *
     * @param name   name  which the specified value is associated
     * @param cur    Ref expected to be currently associated with the
     *               specified key
     * @param newRef Ref to be associated with the specified key
     * @return true if the value was replaced
     * @throws IOException if an exception occurs when communicating to the
     *                     database
     */
    public boolean replace(String name, Ref cur, Ref newRef) throws IOException {
        final Ref curInStore = get(name);
        if (curInStore != null && Utils.refsHaveEqualObjectId(curInStore, cur)) {
            putRef(name, newRef);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Removes the entry for a name only if currently mapped to a given Ref
     *
     * @param name name with which the specified value is associated
     * @param cur  Ref expected to be associated with the specified key
     * @return true if the value was removed
     * @throws IOException if an exception occurs when communicating to the
     *                     database
     */
    public boolean remove(String name, Ref cur) throws IOException {
        final Ref curInStore = get(name);
        if (curInStore != null && Utils.refsHaveEqualObjectId(curInStore, cur)) {
            removeRef(name);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Creates the Cassandra keyspace and refs table if it does
     * not already exist.
     *
     * @throws IOException if an exception occurs when communicating to the
     *                     database
     */
    private void createSchemaIfNotExist() throws IOException {
        try {
            session.execute("CREATE KEYSPACE IF NOT EXISTS " + keyspace
                    + " WITH replication = {'class':'SimpleStrategy'," + " 'replication_factor':1};");

            session.execute("CREATE TABLE IF NOT EXISTS " + keyspace + "." + TABLE_NAME
                    + " (name varchar PRIMARY KEY, type int, value varchar, " + "aux_value varchar);");
        } catch (RuntimeException e) {
            e.printStackTrace();
            throw new IOException(e);
        }
    }

    /**
     * Parses a Cassandra refs table row and converts it to a Ref
     *
     * @param row a single Cassandra row to parse
     * @return a ref, or null if the "row" parameter is null
     * @throws IOException           if an exception occurs when communicating to the
     *                               database
     * @throws IllegalStateException if the "type" field read back from the
     *                               database is not one of the four handled
     *                               types (@see RefType).
     */
    private Ref rowToRef(Row row) throws IOException {
        if (row == null) {
            return null;
        }

        final String name = row.getString("name");
        final String value = row.getString("value");
        final int refType = row.getInt("type");

        if (refType == RefType.PEELED_NONTAG.getValue()) {
            return new ObjectIdRef.PeeledNonTag(Ref.Storage.NETWORK, name, ObjectId.fromString(value));
        } else if (refType == RefType.PEELED_TAG.getValue()) {
            final String auxValue = row.getString("aux_value");
            return new ObjectIdRef.PeeledTag(Ref.Storage.NETWORK, name, ObjectId.fromString(value),
                    ObjectId.fromString(auxValue));
        } else if (refType == RefType.UNPEELED.getValue()) {
            return new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, name, ObjectId.fromString(value));
        } else if (refType == RefType.SYMBOLIC.getValue()) {
            return new SymbolicRef(name, get(value));
        } else {
            throw new IllegalStateException("Unhandled ref type: " + refType);
        }
    }

    /**
     * Inserts a single ref into the database
     *
     * @throws IllegalStateException if the reference concrete type is not
     *                               one of the four handled classes
     *                               (@see RefType).
     */
    private void putRef(String name, Ref r) throws IOException {
        if (r instanceof SymbolicRef) {
            putRow(name, RefType.SYMBOLIC, r.getTarget().getName(), "");
        } else if (r instanceof ObjectIdRef.PeeledNonTag) {
            putRow(name, RefType.PEELED_NONTAG, r.getObjectId().name(), "");
        } else if (r instanceof ObjectIdRef.PeeledTag) {
            putRow(name, RefType.PEELED_TAG, r.getObjectId().name(), r.getPeeledObjectId().toString());
        } else if (r instanceof ObjectIdRef.Unpeeled) {
            putRow(name, RefType.UNPEELED, r.getObjectId().name(), "");
        } else {
            throw new IllegalStateException("Unhandled ref type: " + r);
        }
    }

    /**
     * Inserts a row into the refs table. This works for both insertion of a
     * new row, and updating an existing row.
     *
     * @param name     the primary key
     * @param type     a type where the value is mapped to an integer through
     *                 the RefType enum
     * @param value    the value, either a commit id or in the case of a
     *                 symbolic reference, the target name
     * @param auxValue an additional value, either the peeled object id in the
     *                 case of a peeled tag ref, or an empty string for all
     *                 other types of commits
     * @throws IOException if an exception occurs when communicating to the
     *                     database
     */
    private void putRow(String name, RefType type, String value, String auxValue) throws IOException {
        try {
            Statement stmt = QueryBuilder.insertInto(keyspace, TABLE_NAME).value("name", name)
                    .value("type", type.getValue()).value("value", value).value("aux_value", auxValue);

            session.execute(stmt);
        } catch (RuntimeException e) {
            e.printStackTrace();
            throw new IOException(e);
        }
    }

    /**
     * Removes the ref row given by "name".
     * <p/>
     * Given name is the primary key (and unique) only a single row will be
     * removed.
     *
     * @throws IOException if an exception occurs when communicating to the
     *                     database
     */
    private void removeRef(String name) throws IOException {
        try {
            Statement stmt = QueryBuilder.delete().from(keyspace, TABLE_NAME).where(QueryBuilder.eq("name", name));
            session.execute(stmt);
        } catch (RuntimeException e) {
            e.printStackTrace();
            throw new IOException(e);
        }
    }
}