/**
* 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 com.couchbase.client.core.message.kv.MutationToken;
import com.couchbase.client.java.AsyncBucket;
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.TemporaryLockFailureException;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.query.Statement;
import com.couchbase.client.java.repository.AsyncRepository;
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 AsyncBucket}. It is similar to
* {@link AsyncRepository} 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 AsyncDatastore {
/**
* Result of {@link AsyncDatastore#query(ViewQuery)} contains a list of entities plus totalRows in
* the view.
*
* @author edgar
* @param <T> Entity type.
*/
class AsyncViewQueryResult<T> {
/** Total number of rows in the view. */
private int totalRows;
/** List of rows from current execution. */
private Observable<List<T>> rows;
/**
* Creates a new {@link AsyncViewQueryResult}.
*
* @param totalRows Total number of rows in the view.
* @param rows Resultset from current execution.
*/
public AsyncViewQueryResult(final int totalRows, final Observable<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 Observable<List<T>> getRows() {
return rows;
}
}
/**
* Provides advanced options for couchbase operation.
*
* @author edgar
*/
interface AsyncCommand {
/**
* Set the expiry/ttl entity option.
*
* @param expiry The expiration time expressed as relative seconds from now.
* @return This command.
*/
AsyncCommand expiry(int expiry);
/**
* Set a CAS value for the entity (0 if not set).
*
* @param cas Cas value.
* @return This command.
*/
AsyncCommand cas(long cas);
/**
* 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.
*/
AsyncCommand mutationToken(MutationToken mutationToken);
/**
* Execute this command.
*
* @param entity Entity to use.
* @param <R> Entity type.
* @return Command result.
*/
default <R> Observable<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.
*/
default <R> Observable<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.
*/
default <R> Observable<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.
*/
<R> Observable<R> execute(Object entity, PersistTo persistTo, ReplicateTo replicateTo);
}
/**
* Provides advanced options for couchbase operation.
*
* @author edgar
* @since 1.0.0.CR7
*/
@SuppressWarnings("unchecked")
interface AsyncRemoveCommand extends AsyncCommand {
/**
* Execute a remove document operation
*
* @param entity Entity to remove.
* @param persistTo Persist to option.
* @param replicateTo Replicate to option.
* @return CAS value.
*/
@Override
Observable<Long> execute(Object entity, PersistTo persistTo, ReplicateTo replicateTo);
/**
* Execute a remove document operation
*
* @param entityClass Entity class to remove.
* @param id Entity id to remove.
* @return CAS value.
*/
default Observable<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.
*/
default Observable<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.
*/
default Observable<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.
*/
Observable<Long> execute(final Class<?> entityClass, Object id, PersistTo persistTo,
ReplicateTo replicateTo);
}
/**
* Get an entity/document by ID. The unique ID is constructed via
* {@link N1Q#qualifyId(Class, Object)}.
*
* @param entityClass Entity class.
* @param id Entity id.
* @param <T> Entity type.
* @return An observable entity matching the id or an empty observable.
*/
<T> Observable<T> get(Class<T> entityClass, Object id);
/**
* Get an entity/document by ID. The unique ID is constructed via
* {@link N1Q#qualifyId(Class, Object)}.
*
* @param entityClass Entity class.
* @param id Entity id.
* @param mode Replica mode.
* @param <T> Entity type.
* @return An observable entity matching the id or an empty observable.
*/
<T> Observable<T> getFromReplica(Class<T> entityClass, Object id, ReplicaMode mode);
/**
* Retrieve and lock a entity by its unique ID.
*
* If the entity is found, a entity is returned. If the entity is not found, the
* {@link Observable} completes without an item emitted (empty).
*
* 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}.
*/
<T> Observable<T> getAndLock(Class<T> entityClass, Object id, int lockTime);
/**
* Retrieve and touch an entity by its unique ID.
*
* If the entity is found, an entity is returned. If the entity is not found, the
* {@link Observable} completes without an item emitted (empty).
*
* 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}.
*/
<T> Observable<T> getAndTouch(Class<T> entityClass, Object id, int expiry);
/**
* 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.
*/
Observable<Boolean> exists(Class<?> entityClass, Object id);
/**
* @return A new upsert command.
*/
AsyncCommand 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> Observable<T> upsert(final T entity) {
return upsert().execute(entity);
}
/**
* @return A new insert command.
*/
AsyncCommand 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.
* @param <T> Entity type.
* @return Updated entity.
*/
default <T> Observable<T> insert(final T entity) {
return insert().execute(entity);
}
/**
* Replace an entity if it does exist and watch for durability constraints.
*
* @return A new replace command.
*/
AsyncCommand replace();
/**
* Replace an entity if it does exist and watch for durability constraints.
*
* This method works exactly like {@link AsyncBucket#replace(Document)}, but afterwards 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 An entity to replace.
* @param <T> Entity type.
* @return The replace entity.
*/
default <T> Observable<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.
*/
AsyncRemoveCommand 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.
*
* @param entity Entity to remove.
* @return The cas value.
*/
default Observable<Long> remove(final Object entity) {
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.
*
* @param entityClass Entity class to remove.
* @param id Entity id.
* @return The cas value.
*/
default Observable<Long> remove(final Class<?> entityClass, final Object id) {
return remove().execute(entityClass, id);
}
/**
* 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> Observable<List<T>> query(final Statement statement) {
return query(N1qlQuery.simple(statement));
}
/**
* Run a {@link N1qlQuery} query.
*
* @param query Query.
* @param <T> Entity type.
* @return A list of results.
* @see N1Q#from(Class)
*/
<T> Observable<List<T>> query(N1qlQuery query);
/**
* Run a {@link ViewQuery} query.
*
* @param query View query.
* @param <T> Entity type.
* @return Results.
*/
<T> Observable<AsyncViewQueryResult<T>> query(ViewQuery query);
}