com.orangerhymelabs.helenus.cassandra.document.AbstractDocumentRepository.java Source code

Java tutorial

Introduction

Here is the source code for com.orangerhymelabs.helenus.cassandra.document.AbstractDocumentRepository.java

Source

/*
Copyright 2015, Strategic Gains, Inc.
    
   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.orangerhymelabs.helenus.cassandra.document;

import java.nio.ByteBuffer;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;

import org.bson.BSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.datastax.driver.core.BoundStatement;
import com.datastax.driver.core.PreparedStatement;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.exceptions.CodecNotFoundException;
import com.datastax.driver.core.exceptions.InvalidTypeException;
import com.google.common.base.Function;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.orangerhymelabs.helenus.cassandra.AbstractCassandraRepository;
import com.orangerhymelabs.helenus.cassandra.document.AbstractDocumentRepository.DocumentStatements;
import com.orangerhymelabs.helenus.cassandra.table.key.KeyComponent;
import com.orangerhymelabs.helenus.cassandra.table.key.KeyDefinition;
import com.orangerhymelabs.helenus.cassandra.table.key.KeyDefinitionException;
import com.orangerhymelabs.helenus.cassandra.table.key.KeyDefinitionParser;
import com.orangerhymelabs.helenus.exception.InvalidIdentifierException;
import com.orangerhymelabs.helenus.exception.StorageException;
import com.orangerhymelabs.helenus.persistence.Identifier;
import com.orangerhymelabs.helenus.persistence.StatementFactory;

/**
 * Document repositories are unique per document/table and therefore must be cached by table.
 * 
 * @author tfredrich
 * @since Jun 8, 2015
 */
public abstract class AbstractDocumentRepository extends AbstractCassandraRepository<Document, DocumentStatements> {
    private static final Logger LOG = LoggerFactory.getLogger(AbstractDocumentRepository.class);

    private class Columns {
        static final String OBJECT = "object";
        static final String CREATED_AT = "created_at";
        static final String UPDATED_AT = "updated_at";
    }

    public static class Schema {
        private static final String DROP_TABLE = "drop table if exists %s.%s;";
        private static final String CREATE_TABLE = "create table if not exists %s.%s" + "(" + "%s," + // identifying properties
                Columns.OBJECT + " blob," +
                // TODO: Add Location details to Document.
                // TODO: Add Lucene index capability to Document.
                Columns.CREATED_AT + " timestamp," + Columns.UPDATED_AT + " timestamp," + "%s" + // primary key
                ")" + " %s"; // clustering order (optional)

        public boolean drop(Session session, String keyspace, String table) {
            ResultSetFuture rs = session.executeAsync(String.format(DROP_TABLE, keyspace, table));
            try {
                return rs.get().wasApplied();
            } catch (InterruptedException | ExecutionException e) {
                LOG.error("Document schema drop failed", e);
            }

            return false;
        }

        public boolean create(Session session, String keyspace, String table, KeyDefinition key) {
            ResultSetFuture rs = session.executeAsync(String.format(CREATE_TABLE, keyspace, table, key.asColumns(),
                    key.asPrimaryKey(), key.asClusteringKey()));
            try {
                return rs.get().wasApplied();
            } catch (InterruptedException | ExecutionException e) {
                LOG.error("ViewDocument schema create failed", e);
            }

            return false;
        }
    }

    public static class DocumentStatements implements StatementFactory {
        private static final String CREATE = "create";
        private static final String DELETE = "delete";
        private static final String EXISTS = "exists";
        private static final String READ = "read";
        private static final String READ_ALL = "readAll";
        private static final String UPDATE = "update";
        private static final String UPSERT = "upsert";

        private KeyDefinition keys;
        private Session session;
        private String keyspace;
        private String tableName;
        private Map<String, PreparedStatement> statements = new ConcurrentHashMap<>();

        public DocumentStatements(Session session, String keyspace, String tableName, KeyDefinition keys)
                throws KeyDefinitionException {
            super();
            this.session = session;
            this.keyspace = keyspace;
            this.tableName = tableName;
            this.keys = keys;
        }

        @Override
        public PreparedStatement create() {
            PreparedStatement ps = statements.get(CREATE);

            if (ps == null) {
                try {
                    ps = session.prepareAsync(
                            String.format("insert into %s.%s (%s, %s, %s, %s) values (%s) if not exists", keyspace,
                                    tableName, keys.asSelectProperties(), Columns.OBJECT, Columns.CREATED_AT,
                                    Columns.UPDATED_AT, keys.asQuestionMarks(3)))
                            .get();
                    statements.put(CREATE, ps);
                } catch (InterruptedException | ExecutionException e) {
                    LOG.error("Error preparing create() statement", e);
                }
            }

            return ps;
        }

        @Override
        public PreparedStatement delete() {
            PreparedStatement ps = statements.get(DELETE);

            if (ps == null) {
                try {
                    ps = session.prepareAsync(String.format("delete from %s.%s where %s", keyspace, tableName,
                            keys.asIdentityClause())).get();
                    statements.put(DELETE, ps);
                } catch (InterruptedException | ExecutionException e) {
                    LOG.error("Error preparing delete() statement", e);
                }
            }

            return ps;
        }

        public PreparedStatement exists() {
            PreparedStatement ps = statements.get(EXISTS);

            if (ps == null) {
                try {
                    ps = session.prepareAsync(String.format("select count(*) from %s.%s  where %s limit 1",
                            keyspace, tableName, keys.asIdentityClause())).get();
                    statements.put(EXISTS, ps);
                } catch (InterruptedException | ExecutionException e) {
                    LOG.error("Error preparing exists() statement", e);
                }
            }

            return ps;
        }

        @Override
        public PreparedStatement update() {
            PreparedStatement ps = statements.get(UPDATE);

            if (ps == null) {
                try {
                    ps = session
                            .prepareAsync(
                                    String.format("update %s.%s set %s = ?, %s = ? where %s if exists", keyspace,
                                            tableName, Columns.OBJECT, Columns.UPDATED_AT, keys.asIdentityClause()))
                            .get();
                    statements.put(UPDATE, ps);
                } catch (InterruptedException | ExecutionException e) {
                    LOG.error("Error preparing update() statement", e);
                }
            }

            return ps;
        }

        public PreparedStatement upsert() {
            PreparedStatement ps = statements.get(UPSERT);

            if (ps == null) {
                try {
                    ps = session.prepareAsync(String.format("insert into %s.%s (%s, %s, %s, %s) values (%s)",
                            keyspace, tableName, keys.asSelectProperties(), Columns.OBJECT, Columns.CREATED_AT,
                            Columns.UPDATED_AT, keys.asQuestionMarks(3))).get();
                    statements.put(UPSERT, ps);
                } catch (InterruptedException | ExecutionException e) {
                    LOG.error("Error preparing upsert() statement", e);
                }
            }

            return ps;
        }

        @Override
        public PreparedStatement read() {
            PreparedStatement ps = statements.get(READ);

            if (ps == null) {
                try {
                    ps = session.prepareAsync(String.format("select * from %s.%s where %s limit 1", keyspace,
                            tableName, keys.asIdentityClause())).get();
                    statements.put(READ, ps);
                } catch (InterruptedException | ExecutionException e) {
                    LOG.error("Error preparing read() statement", e);
                }
            }

            return ps;
        }

        @Override
        public PreparedStatement readAll() {
            PreparedStatement ps = statements.get(READ_ALL);

            if (ps == null) {
                try {
                    ps = session.prepareAsync(String.format("select * from %s.%s where %s", keyspace, tableName,
                            keys.asPartitionIdentityClause())).get();
                    statements.put(READ_ALL, ps);
                } catch (InterruptedException | ExecutionException e) {
                    LOG.error("Error preparing readAll() statement", e);
                }
            }

            return ps;
        }
    }

    private String tableName;
    private KeyDefinition keyDefinition;

    public AbstractDocumentRepository(Session session, String keyspace, String tableName, String keys)
            throws KeyDefinitionException {
        super(session, keyspace);
        this.keyDefinition = new KeyDefinitionParser().parse(keys);
        this.tableName = tableName;
        statementFactory(new DocumentStatements(session, keyspace, tableName, keyDefinition));
    }

    public ListenableFuture<Boolean> exists(Identifier id) {
        ListenableFuture<ResultSet> future = submitExists(id);
        return Futures.transform(future, new Function<ResultSet, Boolean>() {
            @Override
            public Boolean apply(ResultSet result) {
                return result.one().getLong(0) > 0;
            }
        }, MoreExecutors.directExecutor());
    }

    public ListenableFuture<Document> upsert(Document entity) {
        ListenableFuture<ResultSet> future = submitUpsert(entity);
        return Futures.transformAsync(future, new AsyncFunction<ResultSet, Document>() {
            @Override
            public ListenableFuture<Document> apply(ResultSet result) throws Exception {
                if (result.wasApplied()) {
                    return Futures.immediateFuture(entity);
                }

                //TODO: This doesn't provide any informational value... what should it be?
                return Futures.immediateFailedFuture(new StorageException(
                        String.format("Table %s failed to store document: %s", tableName, entity.toString())));
            }
        }, MoreExecutors.directExecutor());
    }

    @Override
    protected ResultSetFuture submitCreate(Document document) {
        BoundStatement create = new BoundStatement(statementFactory().create());
        bindCreate(create, document);
        return session().executeAsync(create);
    }

    @Override
    protected ResultSetFuture submitDelete(Identifier id) {
        BoundStatement delete = new BoundStatement(statementFactory().delete());
        bindIdentity(delete, id);
        return session().executeAsync(delete);
    }

    protected ListenableFuture<ResultSet> submitExists(Identifier id) {
        BoundStatement bs = new BoundStatement(statementFactory().exists());
        bindIdentity(bs, id);
        return session().executeAsync(bs);
    }

    @Override
    protected ResultSetFuture submitUpdate(Document document) {
        BoundStatement update = new BoundStatement(statementFactory().update());
        bindUpdate(update, document);
        return session().executeAsync(update);
    }

    protected ResultSetFuture submitUpsert(Document document) {
        BoundStatement upsert = new BoundStatement(statementFactory().upsert());
        bindCreate(upsert, document);
        return session().executeAsync(upsert);
    }

    @Override
    protected void bindCreate(BoundStatement bs, Document document) {
        Date now = new Date();
        document.createdAt(now);
        document.updatedAt(now);
        Identifier id = document.identifier();
        Object[] values = new Object[id.size() + 3]; // Identifier + object + createdAt + updatedAt.

        try {
            fill(values, 0, id.components().toArray());
            fill(values, id.size(), (document.hasObject() ? ByteBuffer.wrap(BSON.encode(document.object())) : null),
                    document.createdAt(), document.updatedAt());
            bs.bind(values);
        } catch (InvalidTypeException | CodecNotFoundException e) {
            throw new InvalidIdentifierException(e);
        }
    }

    @Override
    protected void bindUpdate(BoundStatement bs, Document document) {
        document.updatedAt(new Date());
        Identifier id = document.identifier();
        Object[] values = new Object[id.size() + 2];

        try {
            if (document.hasObject()) {
                fill(values, 0, ByteBuffer.wrap(BSON.encode(document.object())), document.updatedAt());
                fill(values, 2, id.components().toArray());
            } else {
                fill(values, 0, null, document.updatedAt());
                fill(values, 2, id.components().toArray());
            }

            bs.bind(values);
        } catch (InvalidTypeException | CodecNotFoundException e) {
            throw new InvalidIdentifierException(e);
        }
    }

    private void fill(Object[] array, int offset, Object... values) {
        for (int i = offset; i < values.length + offset; i++) {
            array[i] = values[i - offset];
        }
    }

    @Override
    protected Document marshalRow(Row row) {
        if (row == null) {
            return null;
        }

        Document d = new Document();
        d.identifier(marshalId(keyDefinition, row));
        ByteBuffer b = row.getBytes(Columns.OBJECT);

        if (b != null && b.hasArray()) {
            byte[] result = new byte[b.remaining()];
            b.get(result);
            d.object(BSON.decode(result));
        }

        d.createdAt(row.getTimestamp(Columns.CREATED_AT));
        d.updatedAt(row.getTimestamp(Columns.UPDATED_AT));
        return d;
    }

    private Identifier marshalId(KeyDefinition keyDefinition, Row row) {
        Identifier id = new Identifier();
        keyDefinition.components().forEach(new Consumer<KeyComponent>() {
            @Override
            public void accept(KeyComponent t) {
                id.add(IdPropertyConverter.marshal(t.property(), t.type(), row));
            }
        });
        return id;
    }
}