/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.jooby.couchbase;
import java.util.List;
import org.jooby.couchbase.AsyncDatastore.AsyncCommand;
import org.jooby.couchbase.AsyncDatastore.AsyncRemoveCommand;
import com.couchbase.client.core.message.kv.MutationToken;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.PersistTo;
import com.couchbase.client.java.ReplicaMode;
import com.couchbase.client.java.ReplicateTo;
import com.couchbase.client.java.document.Document;
import com.couchbase.client.java.document.JsonDocument;
import com.couchbase.client.java.error.DocumentDoesNotExistException;
import com.couchbase.client.java.error.TemporaryLockFailureException;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.query.Statement;
import com.couchbase.client.java.repository.Repository;
import com.couchbase.client.java.repository.annotation.Id;
import com.couchbase.client.java.view.ViewQuery;
import rx.Observable;
/**
* <h1>datastore</h1>
* <p>
* Create, read, update and delete entities from an {@link Bucket}. It is similar to
* {@link Repository} but less verbose or more ready to use, but also add support for
* {@link ViewQuery} and {@link N1qlQuery}.
* </p>
*
* <h2>design/implementation choices</h2>
* <p>
* In order to abstract developers from doing basic CRUD operations, the following design has been
* made:
* </p>
*
* <ul>
* <li>IDs look like: <code>classname::id</code>.</li>
* <li>ID is also stored within the document as an attribute</li>
* <li>A <code>class</code> attribute is also stored within the document</li>
* </ul>
*
* <p>
* Here is an example of a document for a class: <code>model.Beer</code>:
* </p>
* <pre>{@code
* {
* "model.Beer::678": {
* "name": "IPA",
* "id": 678,
* "class": "model.Beer"
* }
* }
* }</pre>
*
* <h2>ID selection</h2>
* <p>
* In order to mark a class field as document ID, you must:
* </p>
* <ul>
* <li>Name the field: <code>id</code>, or</li>
* <li>Annotated the field with {@link Id}</li>
* </ul>
*
* <p>
* Auto-increment ID are supported by annotating the field with {@link GeneratedValue} and declare
* the field as {@link Long}. Then whenever you make a call to {@link #insert(Object)} or
* {@link #upsert(Object)} a new ID will be generated if need it.
* </p>
*
* @author edgar
* @since 1.0.0.CR7
*/
public interface Datastore {
/**
* Result of {@link Datastore#query(ViewQuery)} contains a list of entities plus totalRows in
* the view.
*
* @author edgar
* @param <T> Entity type.
*/
class ViewQueryResult<T> {
/** Total number of rows in the view. */
private int totalRows;
/** List of rows from current execution. */
private List<T> rows;
/**
* Creates a new {@link ViewQueryResult}.
*
* @param totalRows Total number of rows in the view.
* @param rows Resultset from current execution.
*/
ViewQueryResult(final int totalRows, final List<T> rows) {
this.totalRows = totalRows;
this.rows = rows;
}
/**
* @return Total number of rows in the view.
*/
public int getTotalRows() {
return totalRows;
}
/**
* @return Resultset from current execution.
*/
public List<T> getRows() {
return rows;
}
}
/**
* Provides advanced options for couchbase operation.
*
* @author edgar
*/
class Command {
protected final AsyncCommand cmd;
Command(final AsyncCommand cmd) {
this.cmd = cmd;
}
/**
* Set the expiry/ttl entity option.
*
* @param expiry The expiration time expressed as relative seconds from now.
* @return This command.
*/
public Command expiry(final int expiry) {
cmd.expiry(expiry);
return this;
}
/**
* Set a CAS value for the entity (0 if not set).
*
* @param cas Cas value.
* @return This command.
*/
public Command cas(final long cas) {
cmd.cas(cas);
return this;
}
/**
* The optional, opaque mutation token set after a successful mutation and if enabled on
* the environment.
*
* Note that the mutation token is always null, unless they are explicitly enabled on the
* environment, the server version is supported (>= 4.0.0) and the mutation operation succeeded.
*
* If set, it can be used for enhanced durability requirements, as well as optimized consistency
* for N1QL queries.
*
* @param mutationToken the mutation token if set, otherwise null.
* @return This command.
*/
public Command mutationToken(final MutationToken mutationToken) {
cmd.mutationToken(mutationToken);
return this;
}
/**
* Execute this command.
*
* @param entity Entity to use.
* @param <R> Entity type.
* @return Command result.
*/
public <R> R execute(final Object entity) {
return execute(entity, PersistTo.NONE);
}
/**
* Execute this command.
*
* @param entity Entity to use.
* @param persistTo Persist to value.
* @param <R> Entity type.
* @return Command result.
*/
public <R> R execute(final Object entity, final PersistTo persistTo) {
return execute(entity, persistTo, ReplicateTo.NONE);
}
/**
* Execute this command.
*
* @param entity Entity to use.
* @param replicateTo Replicate to value.
* @param <R> Entity type.
* @return Command result.
*/
public <R> R execute(final Object entity, final ReplicateTo replicateTo) {
return execute(entity, PersistTo.NONE, replicateTo);
}
/**
* Execute this command.
*
* @param entity Entity to use.
* @param persistTo Persist to value.
* @param replicateTo Replicate to value.
* @param <R> Entity type.
* @return Command result.
*/
public <R> R execute(final Object entity, final PersistTo persistTo,
final ReplicateTo replicateTo) {
Observable<R> result = cmd.execute(entity, persistTo, replicateTo);
return result.toBlocking().single();
}
}
/**
* Provides advanced options for couchbase operation.
*
* @author edgar
*/
class RemoveCommand extends Command {
/**
* Creates a new remove command.
*
* @param cmd Delegate to async version.
*/
RemoveCommand(final AsyncRemoveCommand cmd) {
super(cmd);
}
/**
* Execute a remove document operation
*
* @param entity Entity to remove.
* @param persistTo Persist to option.
* @param replicateTo Replicate to option.
* @return CAS value.
*/
@SuppressWarnings("unchecked")
@Override
public Long execute(final Object entity, final PersistTo persistTo,
final ReplicateTo replicateTo) {
Observable<Long> result = cmd.execute(entity, persistTo, replicateTo);
return result.toBlocking().single();
}
/**
* Execute a remove document operation
*
* @param entityClass Entity class to remove.
* @param id Entity id to remove.
* @return CAS value.
*/
public Long execute(final Class<?> entityClass, final Object id) {
return execute(entityClass, id, PersistTo.NONE);
}
/**
* Execute a remove document operation
*
* @param entityClass Entity class to remove.
* @param id Entity id to remove.
* @param persistTo Persist to option.
* @return CAS value.
*/
public Long execute(final Class<?> entityClass, final Object id,
final PersistTo persistTo) {
return execute(entityClass, id, persistTo, ReplicateTo.NONE);
}
/**
* Execute a remove document operation
*
* @param entityClass Entity class to remove.
* @param id Entity id to remove.
* @param replicateTo Replicate to option.
* @return CAS value.
*/
public Long execute(final Class<?> entityClass, final Object id,
final ReplicateTo replicateTo) {
return execute(entityClass, id, PersistTo.NONE, replicateTo);
}
/**
* Execute a remove document operation
*
* @param entityClass Entity class to remove.
* @param id Entity id to remove.
* @param persistTo Persist to option.
* @param replicateTo Replicate to option.
* @return CAS value.
*/
public Long execute(final Class<?> entityClass, final Object id, final PersistTo persistTo,
final ReplicateTo replicateTo) {
return ((AsyncRemoveCommand) cmd).execute(entityClass, id, persistTo, replicateTo)
.toBlocking().single();
}
}
/**
* Produces an observable that throws a {@link DocumentDoesNotExistException}.
*
* @param entityClass Entity class.
* @param id Entity id.
* @param <T> Entity type.
* @return An observable that throws a {@link DocumentDoesNotExistException}.
*/
@SuppressWarnings("rawtypes")
static <T> Observable<T> notFound(final Class entityClass, final Object id) {
return Observable.create(s -> {
s.onError(new DocumentDoesNotExistException(N1Q.qualifyId(entityClass, id)));
});
}
/**
* @return An {@link AsyncDatastore}.
*/
AsyncDatastore async();
/**
* Get an entity/document by ID. The unique ID is constructed via
* {@link N1Q#qualifyId(Class, Object)}.
*
* If the entity is found, a entity is returned. Otherwise a
* {@link DocumentDoesNotExistException}.
*
* @param entityClass Entity class.
* @param id Entity id.
* @param <T> Entity type.
* @return An entity matching the id or an empty observable.
*/
default <T> T get(final Class<T> entityClass, final Object id)
throws DocumentDoesNotExistException {
return async().<T> get(entityClass, id)
.switchIfEmpty(notFound(entityClass, id))
.toBlocking().single();
}
/**
* Get an entity/document by ID. The unique ID is constructed via
* {@link N1Q#qualifyId(Class, Object)}.
*
* If the entity is found, a entity is returned. Otherwise a
* {@link DocumentDoesNotExistException}.
*
* @param entityClass Entity class.
* @param id Entity id.
* @param mode Replica mode.
* @param <T> Entity type.
* @return An entity matching the id or an empty observable.
*/
default <T> T getFromReplica(final Class<T> entityClass, final Object id,
final ReplicaMode mode) throws DocumentDoesNotExistException {
return async().<T> getFromReplica(entityClass, id, mode)
.switchIfEmpty(notFound(entityClass, id))
.toBlocking().single();
}
/**
* Retrieve and lock a entity by its unique ID.
*
* If the entity is found, a entity is returned. Otherwise a
* {@link DocumentDoesNotExistException}.
*
* This method works similar to {@link #get(Class, Object)}, but in addition it (write) locks the
* entity for the given lock time interval. Note that this lock time is hard capped to 30
* seconds, even if provided with a higher value and is not configurable. The entity will unlock
* afterwards automatically.
*
* Detecting an already locked entity is done by checking for
* {@link TemporaryLockFailureException}. Note that this exception can also be raised in other
* conditions, always when the error is transient and retrying may help.
*
* @param entityClass Entity class.
* @param id id the unique ID of the entity.
* @param lockTime the time to write lock the entity (max. 30 seconds).
* @param <T> Entity type.
* @return an {@link Observable} eventually containing the found {@link JsonDocument}.
*/
default <T> T getAndLock(final Class<T> entityClass, final Object id, final int lockTime)
throws DocumentDoesNotExistException {
return async().<T> getAndLock(entityClass, id, lockTime)
.switchIfEmpty(notFound(entityClass, id))
.toBlocking().single();
}
/**
* Retrieve and touch an entity by its unique ID.
*
* If the entity is found, an entity is returned. Otherwise a
* {@link DocumentDoesNotExistException}.
*
* This method works similar to {@link #get(Class, Object)}, but in addition it touches the
* entity, which will reset its configured expiration time to the value provided.
*
* @param entityClass Entity class.
* @param id id the unique ID of the entity.
* @param expiry the new expiration time for the entity (in seconds).
* @param <T> Entity type.
* @return an {@link Observable} eventually containing the found {@link JsonDocument}.
*/
default <T> T getAndTouch(final Class<T> entityClass, final Object id, final int expiry)
throws DocumentDoesNotExistException {
return async().<T> getAndTouch(entityClass, id, expiry)
.switchIfEmpty(notFound(entityClass, id))
.toBlocking().single();
}
/**
* Check whether a entity with the given ID does exist in the bucket.
*
* @param entityClass Entity class.
* @param id Entity id.
* @return True, if exists.
*/
default boolean exists(final Class<?> entityClass, final Object id) {
return async().exists(entityClass, id).toBlocking().single();
}
/**
* @return A new upsert command.
*/
default Command upsert() {
return new Command(async().upsert());
}
/**
* Insert or overwrite an entity. If the entity already has an ID, then that ID is selected. If
* the entity doesn't have an ID and the field is annotated with {@link GeneratedValue} this
* method will generate a new ID and insert the entity.
*
* @param entity Entity to insert or overwrite.
* @param <T> Entity type.
* @return Updated entity.
*/
default <T> T upsert(final T entity) {
return upsert().execute(entity);
}
/**
* @return A new insert command.
*/
default Command insert() {
return new Command(async().insert());
}
/**
* Insert an entity. If the entity already has an ID, then that ID is selected. If
* the entity doesn't have an ID and the field is annotated with {@link GeneratedValue} this
* method will generate a new ID and insert the entity.
*
* @param entity Entity to insert or overwrite.
* @param <T> Entity type.
* @return Updated entity.
*/
default <T> T insert(final T entity) {
return insert().execute(entity);
}
/**
* @return A new replace command.
*/
default Command replace() {
return new Command(async().replace());
}
/**
* Replace a {@link Document} if it does exist and watch for durability constraints.
*
* It watches the server states if the given durability constraints are met. If this is the case,
* a new document is returned which contains the original properties, but has the refreshed CAS
* value set.
*
* @param entity Entity to replace.
* @param <T> Entity type.
* @return A new replace command.
*/
default <T> T replace(final T entity) {
return replace().execute(entity);
}
/**
* Removes an entity from the Server.
*
* The an entity returned just has the document ID and its CAS value set, since the value and all
* other
* associated properties have been removed from the server.
*
* @return A new remove command.
*/
default RemoveCommand remove() {
return new RemoveCommand(async().remove());
}
/**
* Removes an entity from the Server.
*
* The an entity returned just has the document ID and its CAS value set, since the value and all
* other associated properties have been removed from the server.
*
* Throws a {@link DocumentDoesNotExistException} if entity doesn't exist.
*
* @param entity Entity to remove.
* @return The cas value.
*/
default long remove(final Object entity) throws DocumentDoesNotExistException {
return remove().execute(entity);
}
/**
* Removes an entity from the Server.
*
* The an entity returned just has the document ID and its CAS value set, since the value and all
* other associated properties have been removed from the server.
*
* Throws a {@link DocumentDoesNotExistException} if entity doesn't exist.
*
* @param entityClass Entity class to remove.
* @param id Entity id.
* @return The cas value.
*/
default long remove(final Class<?> entityClass, final Object id)
throws DocumentDoesNotExistException {
return remove().execute(entityClass, id);
}
/**
* Run a {@link N1qlQuery#simple(Statement)} query.
*
* @param query N1qlQuery.
* @param <T> Entity type.
* @return A list of results.
* @see N1Q#from(Class)
*/
default <T> List<T> query(final N1qlQuery query) {
return async().<T> query(query).toBlocking().single();
}
/**
* Run a {@link N1qlQuery#simple(Statement)} query.
*
* @param statement Statement.
* @param <T> Entity type.
* @return A list of results.
* @see N1Q#from(Class)
*/
default <T> List<T> query(final Statement statement) {
return async().<T> query(statement).toBlocking().single();
}
/**
* Run a {@link ViewQuery} query.
*
* @param query View query.
* @param <T> Entity type.
* @return Results.
*/
default <T> ViewQueryResult<T> query(final ViewQuery query) {
return async().<T> query(query)
.map(r -> new ViewQueryResult<>(r.getTotalRows(), r.getRows().toBlocking().single()))
.toBlocking().single();
}
}