All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.couchbase.client.java.CouchbaseAsyncBucket Maven / Gradle / Ivy

/*
 * Copyright (c) 2016 Couchbase, 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.couchbase.client.java;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import com.couchbase.client.core.ClusterFacade;
import com.couchbase.client.core.CouchbaseException;
import com.couchbase.client.core.lang.Tuple2;
import com.couchbase.client.core.logging.CouchbaseLogger;
import com.couchbase.client.core.logging.CouchbaseLoggerFactory;
import com.couchbase.client.core.message.ResponseStatus;
import com.couchbase.client.core.message.cluster.CloseBucketRequest;
import com.couchbase.client.core.message.cluster.CloseBucketResponse;
import com.couchbase.client.core.message.kv.AppendRequest;
import com.couchbase.client.core.message.kv.AppendResponse;
import com.couchbase.client.core.message.kv.CounterRequest;
import com.couchbase.client.core.message.kv.CounterResponse;
import com.couchbase.client.core.message.kv.GetRequest;
import com.couchbase.client.core.message.kv.GetResponse;
import com.couchbase.client.core.message.kv.InsertRequest;
import com.couchbase.client.core.message.kv.InsertResponse;
import com.couchbase.client.core.message.kv.ObserveRequest;
import com.couchbase.client.core.message.kv.ObserveResponse;
import com.couchbase.client.core.message.kv.PrependRequest;
import com.couchbase.client.core.message.kv.PrependResponse;
import com.couchbase.client.core.message.kv.RemoveRequest;
import com.couchbase.client.core.message.kv.RemoveResponse;
import com.couchbase.client.core.message.kv.ReplaceRequest;
import com.couchbase.client.core.message.kv.ReplaceResponse;
import com.couchbase.client.core.message.kv.TouchRequest;
import com.couchbase.client.core.message.kv.TouchResponse;
import com.couchbase.client.core.message.kv.UnlockRequest;
import com.couchbase.client.core.message.kv.UnlockResponse;
import com.couchbase.client.core.message.kv.UpsertRequest;
import com.couchbase.client.core.message.kv.UpsertResponse;
import com.couchbase.client.core.message.observe.Observe;
import com.couchbase.client.core.message.search.SearchQueryRequest;
import com.couchbase.client.core.message.search.SearchQueryResponse;
import com.couchbase.client.core.message.view.ViewQueryRequest;
import com.couchbase.client.core.message.view.ViewQueryResponse;
import com.couchbase.client.deps.io.netty.buffer.ByteBuf;
import com.couchbase.client.java.bucket.AsyncBucketManager;
import com.couchbase.client.java.bucket.DefaultAsyncBucketManager;
import com.couchbase.client.java.bucket.ReplicaReader;
import com.couchbase.client.java.document.Document;
import com.couchbase.client.java.document.JsonDocument;
import com.couchbase.client.java.document.JsonLongDocument;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.env.CouchbaseEnvironment;
import com.couchbase.client.java.error.CASMismatchException;
import com.couchbase.client.java.error.CouchbaseOutOfMemoryException;
import com.couchbase.client.java.error.DocumentAlreadyExistsException;
import com.couchbase.client.java.error.DocumentDoesNotExistException;
import com.couchbase.client.java.error.DurabilityException;
import com.couchbase.client.java.error.RequestTooBigException;
import com.couchbase.client.java.error.TemporaryFailureException;
import com.couchbase.client.java.error.TemporaryLockFailureException;
import com.couchbase.client.java.query.AsyncN1qlQueryResult;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.query.Statement;
import com.couchbase.client.java.query.core.N1qlQueryExecutor;
import com.couchbase.client.java.repository.AsyncRepository;
import com.couchbase.client.java.repository.CouchbaseAsyncRepository;
import com.couchbase.client.java.search.SearchQuery;
import com.couchbase.client.java.search.queries.AbstractFtsQuery;
import com.couchbase.client.java.search.result.AsyncSearchQueryResult;
import com.couchbase.client.java.search.result.impl.DefaultAsyncSearchQueryResult;
import com.couchbase.client.java.subdoc.AsyncLookupInBuilder;
import com.couchbase.client.java.subdoc.AsyncMutateInBuilder;
import com.couchbase.client.java.transcoder.BinaryTranscoder;
import com.couchbase.client.java.transcoder.JacksonTransformers;
import com.couchbase.client.java.transcoder.JsonArrayTranscoder;
import com.couchbase.client.java.transcoder.JsonBooleanTranscoder;
import com.couchbase.client.java.transcoder.JsonDoubleTranscoder;
import com.couchbase.client.java.transcoder.JsonLongTranscoder;
import com.couchbase.client.java.transcoder.JsonStringTranscoder;
import com.couchbase.client.java.transcoder.JsonTranscoder;
import com.couchbase.client.java.transcoder.LegacyTranscoder;
import com.couchbase.client.java.transcoder.RawJsonTranscoder;
import com.couchbase.client.java.transcoder.SerializableTranscoder;
import com.couchbase.client.java.transcoder.StringTranscoder;
import com.couchbase.client.java.transcoder.Transcoder;
import com.couchbase.client.java.transcoder.subdoc.FragmentTranscoder;
import com.couchbase.client.java.transcoder.subdoc.JacksonFragmentTranscoder;
import com.couchbase.client.java.view.AsyncSpatialViewResult;
import com.couchbase.client.java.view.AsyncViewResult;
import com.couchbase.client.java.view.SpatialViewQuery;
import com.couchbase.client.java.view.ViewQuery;
import com.couchbase.client.java.view.ViewQueryResponseMapper;
import com.couchbase.client.java.view.ViewRetryHandler;
import rx.Observable;
import rx.functions.Func0;
import rx.functions.Func1;

public class CouchbaseAsyncBucket implements AsyncBucket {

    private static final CouchbaseLogger LOGGER = CouchbaseLoggerFactory.getInstance(CouchbaseAsyncBucket.class);

    private static final int COUNTER_NOT_EXISTS_EXPIRY = 0xffffffff;

    public static final String CURRENT_BUCKET_IDENTIFIER = "#CURRENT_BUCKET#";

    public static final JsonTranscoder JSON_OBJECT_TRANSCODER = new JsonTranscoder();
    public static final JsonArrayTranscoder JSON_ARRAY_TRANSCODER = new JsonArrayTranscoder();
    public static final JsonBooleanTranscoder JSON_BOOLEAN_TRANSCODER = new JsonBooleanTranscoder();
    public static final JsonDoubleTranscoder JSON_DOUBLE_TRANSCODER = new JsonDoubleTranscoder();
    public static final JsonLongTranscoder JSON_LONG_TRANSCODER = new JsonLongTranscoder();
    public static final JsonStringTranscoder JSON_STRING_TRANSCODER = new JsonStringTranscoder();
    public static final RawJsonTranscoder RAW_JSON_TRANSCODER = new RawJsonTranscoder();

    public static final LegacyTranscoder LEGACY_TRANSCODER = new LegacyTranscoder();
    public static final BinaryTranscoder BINARY_TRANSCODER = new BinaryTranscoder();
    public static final StringTranscoder STRING_TRANSCODER = new StringTranscoder();
    public static final SerializableTranscoder SERIALIZABLE_TRANSCODER = new SerializableTranscoder();

    private final String bucket;
    private final String password;
    private final ClusterFacade core;
    private final Map, Transcoder> transcoders;
    //TODO this could be opened for customization like with transcoders
    private final FragmentTranscoder subdocumentTranscoder = new JacksonFragmentTranscoder(JacksonTransformers.MAPPER);
    private final AsyncBucketManager bucketManager;
    private final CouchbaseEnvironment environment;
    /** the bucket's {@link N1qlQueryExecutor}. Prefer using {@link #n1qlQueryExecutor()} since it allows mocking and testing */
    private final N1qlQueryExecutor n1qlQueryExecutor;

    private volatile boolean closed;


    public CouchbaseAsyncBucket(final ClusterFacade core, final CouchbaseEnvironment environment, final String name,
        final String password, final List> customTranscoders) {
        bucket = name;
        this.password = password;
        this.core = core;
        this.environment = environment;
        this.closed = false;

        transcoders = new ConcurrentHashMap, Transcoder>();
        transcoders.put(JSON_OBJECT_TRANSCODER.documentType(), JSON_OBJECT_TRANSCODER);
        transcoders.put(JSON_ARRAY_TRANSCODER.documentType(), JSON_ARRAY_TRANSCODER);
        transcoders.put(JSON_BOOLEAN_TRANSCODER.documentType(), JSON_BOOLEAN_TRANSCODER);
        transcoders.put(JSON_DOUBLE_TRANSCODER.documentType(), JSON_DOUBLE_TRANSCODER);
        transcoders.put(JSON_LONG_TRANSCODER.documentType(), JSON_LONG_TRANSCODER);
        transcoders.put(JSON_STRING_TRANSCODER.documentType(), JSON_STRING_TRANSCODER);
        transcoders.put(RAW_JSON_TRANSCODER.documentType(), RAW_JSON_TRANSCODER);
        transcoders.put(LEGACY_TRANSCODER.documentType(), LEGACY_TRANSCODER);
        transcoders.put(BINARY_TRANSCODER.documentType(), BINARY_TRANSCODER);
        transcoders.put(STRING_TRANSCODER.documentType(), STRING_TRANSCODER);
        transcoders.put(SERIALIZABLE_TRANSCODER.documentType(), SERIALIZABLE_TRANSCODER);

        for (Transcoder custom : customTranscoders) {
            transcoders.put(custom.documentType(), custom);
        }

        bucketManager = DefaultAsyncBucketManager.create(bucket, password, core);

        boolean n1qlPreparedEncodedPlanEnabled = "true".equalsIgnoreCase(System.getProperty(N1qlQueryExecutor.ENCODED_PLAN_ENABLED_PROPERTY, "true")); //active by default
        n1qlQueryExecutor = new N1qlQueryExecutor(core, bucket, password, n1qlPreparedEncodedPlanEnabled);
    }

    @Override
    public String name() {
        return bucket;
    }

    @Override
    public Observable core() {
        return Observable.just(core);
    }

    @Override
    public FragmentTranscoder subdocumentTranscoder() {
        return subdocumentTranscoder;
    }

    /**
     * Returns the underlying {@link N1qlQueryExecutor} used to perform N1QL queries.
     *
     * Handle with care since all additional checks that are normally performed by this library may be skipped (hence
     * the protected visibility).
     */
    protected N1qlQueryExecutor n1qlQueryExecutor() {
        return this.n1qlQueryExecutor;
    }

    @Override
    public CouchbaseEnvironment environment() {
        return environment;
    }

    @Override
    public Observable repository() {
        return Observable.just((AsyncRepository) new CouchbaseAsyncRepository(this));
    }

    @Override
    public Observable get(final String id) {
        return get(id, JsonDocument.class);
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable get(D document) {
        return (Observable) get(document.id(), document.getClass());
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable get(final String id, final Class target) {
        return Observable.defer(new Func0>() {
                @Override
                public Observable call() {
                    return core.send(new GetRequest(id, bucket));
                }
            })
            .filter(new Func1() {
                @Override
                public Boolean call(GetResponse response) {
                    if (response.status().isSuccess()) {
                        return true;
                    }
                    ByteBuf content = response.content();
                    if (content != null && content.refCnt() > 0) {
                        content.release();
                    }

                    switch(response.status()) {
                        case NOT_EXISTS:
                            return false;
                        case TEMPORARY_FAILURE:
                        case SERVER_BUSY:
                            throw new TemporaryFailureException();
                        case OUT_OF_MEMORY:
                            throw new CouchbaseOutOfMemoryException();
                        default:
                            throw new CouchbaseException(response.status().toString());
                    }
                }
            })
            .map(new Func1() {
                @Override
                public D call(final GetResponse response) {
                    Transcoder transcoder = (Transcoder) transcoders.get(target);
                    return (D) transcoder.decode(id, response.content(), response.cas(), 0, response.flags(),
                        response.status());
                }
            });
    }

    @Override
    public Observable exists(final String id) {
        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                return core.send(new ObserveRequest(id, 0, true, (short) 0, bucket));
            }
        })
            .map(new Func1() {
                @Override
                public Boolean call(ObserveResponse response) {
                    ByteBuf content = response.content();
                    if (content != null && content.refCnt() > 0) {
                        content.release();
                    }

                    ObserveResponse.ObserveStatus foundStatus = response.observeStatus();
                    if (foundStatus == ObserveResponse.ObserveStatus.FOUND_PERSISTED
                        || foundStatus == ObserveResponse.ObserveStatus.FOUND_NOT_PERSISTED) {
                        return true;
                    }

                    return false;
                }
            });
    }

    @Override
    public > Observable exists(D document) {
        return exists(document.id());
    }

    @Override
    public Observable getAndLock(String id, int lockTime) {
        return getAndLock(id, lockTime, JsonDocument.class);
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable getAndLock(D document, int lockTime) {
        return (Observable) getAndLock(document.id(), lockTime, document.getClass());
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable getAndLock(final String id, final int lockTime, final Class target) {
        return Observable.defer(new Func0>() {
                @Override
                public Observable call() {
                    return core.send(new GetRequest(id, bucket, true, false, lockTime));
                }
            })
            .filter(new Func1() {
                @Override
                public Boolean call(GetResponse response) {
                    if (response.status().isSuccess()) {
                        return true;
                    }
                    ByteBuf content = response.content();
                    if (content != null && content.refCnt() > 0) {
                        content.release();
                    }

                    switch (response.status()) {
                        case NOT_EXISTS:
                            return false;
                        case TEMPORARY_FAILURE:
                            throw new TemporaryLockFailureException();
                        case SERVER_BUSY:
                            throw new TemporaryFailureException();
                        case OUT_OF_MEMORY:
                            throw new CouchbaseOutOfMemoryException();
                        default:
                            throw new CouchbaseException(response.status().toString());
                    }
                }
            })
            .map(new Func1() {
                @Override
                public D call(final GetResponse response) {
                    Transcoder transcoder = (Transcoder) transcoders.get(target);
                    return (D) transcoder.decode(id, response.content(), response.cas(), 0, response.flags(),
                        response.status());
                }
            });
    }

    @Override
    public Observable getAndTouch(String id, int expiry) {
        return getAndTouch(id, expiry, JsonDocument.class);
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable getAndTouch(D document) {
        return (Observable) getAndTouch(document.id(), document.expiry(), document.getClass());
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable getAndTouch(final String id, final int expiry, final Class target) {
        return Observable.defer(new Func0>() {
                @Override
                public Observable call() {
                    return core.send(new GetRequest(id, bucket, false, true, expiry));
                }
            })
            .filter(new Func1() {
                @Override
                public Boolean call(GetResponse response) {
                    if (response.status().isSuccess()) {
                        return true;
                    }
                    ByteBuf content = response.content();
                    if (content != null && content.refCnt() > 0) {
                        content.release();
                    }

                    switch (response.status()) {
                        case NOT_EXISTS:
                            return false;
                        case TEMPORARY_FAILURE:
                        case SERVER_BUSY:
                            throw new TemporaryFailureException();
                        case OUT_OF_MEMORY:
                            throw new CouchbaseOutOfMemoryException();
                        default:
                            throw new CouchbaseException(response.status().toString());
                    }
                }
            })
            .map(new Func1() {
                @Override
                public D call(final GetResponse response) {
                    Transcoder transcoder = (Transcoder) transcoders.get(target);
                    return (D) transcoder.decode(id, response.content(), response.cas(), 0, response.flags(),
                        response.status());
                }
            });
    }

    @Override
    public Observable getFromReplica(final String id, final ReplicaMode type) {
        return getFromReplica(id, type, JsonDocument.class);
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable getFromReplica(final D document, final ReplicaMode type) {
        return (Observable) getFromReplica(document.id(), type, document.getClass());
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable getFromReplica(final String id, final ReplicaMode type,
        final Class target) {
        return ReplicaReader
            .read(core, id, type, bucket)
            .map(new Func1() {
                @Override
                public D call(final GetResponse response) {
                    Transcoder transcoder = (Transcoder) transcoders.get(target);
                    return (D) transcoder.decode(id, response.content(), response.cas(), 0, response.flags(),
                        response.status());
                }
            })
            .cache(type.maxAffectedNodes());
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable insert(final D document) {
        final  Transcoder, Object> transcoder = (Transcoder, Object>) transcoders.get(document.getClass());

        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                Tuple2 encoded = transcoder.encode((Document) document);
                return core.send(new InsertRequest(document.id(), encoded.value1(), document.expiry(), encoded.value2(), bucket));
            }
        }).map(new Func1() {
            @Override
            public D call(InsertResponse response) {
                if (response.content() != null && response.content().refCnt() > 0) {
                    response.content().release();
                }

                if (response.status().isSuccess()) {
                    return (D) transcoder.newDocument(document.id(), document.expiry(),
                        document.content(), response.cas(), response.mutationToken());
                }

                switch (response.status()) {
                    case TOO_BIG:
                        throw new RequestTooBigException();
                    case EXISTS:
                        throw new DocumentAlreadyExistsException();
                    case TEMPORARY_FAILURE:
                    case SERVER_BUSY:
                        throw new TemporaryFailureException();
                    case OUT_OF_MEMORY:
                        throw new CouchbaseOutOfMemoryException();
                    default:
                        throw new CouchbaseException(response.status().toString());
                }
            }
        });
    }

    @Override
    public > Observable insert(final D document, final PersistTo persistTo,
        final ReplicateTo replicateTo) {
        Observable insertResult = insert(document);

        if (persistTo == PersistTo.NONE && replicateTo == ReplicateTo.NONE) {
            return insertResult;
        }

        return insertResult.flatMap(new Func1>() {
            @Override
            public Observable call(final D doc) {
                return Observe
                    .call(core, bucket, doc.id(), doc.cas(), false, doc.mutationToken(), persistTo.value(), replicateTo.value(),
                        environment.observeIntervalDelay(), environment.retryStrategy())
                    .map(new Func1() {
                        @Override
                        public D call(Boolean aBoolean) {
                            return doc;
                        }
                    }).onErrorResumeNext(new Func1>() {
                        @Override
                        public Observable call(Throwable throwable) {
                            return Observable.error(new DurabilityException(
                                "Durability requirement failed: " + throwable.getMessage(),
                                throwable));
                        }
                    });
            }
        });
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable upsert(final D document) {
        final  Transcoder, Object> transcoder = (Transcoder, Object>) transcoders.get(document.getClass());

        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                Tuple2 encoded = transcoder.encode((Document) document);
                return core.send(new UpsertRequest(document.id(), encoded.value1(), document.expiry(), encoded.value2(), bucket));
            }
        }).map(new Func1() {
            @Override
            public D call(UpsertResponse response) {
                if (response.content() != null && response.content().refCnt() > 0) {
                    response.content().release();
                }

                if (response.status().isSuccess()) {
                    return (D) transcoder.newDocument(document.id(), document.expiry(),
                        document.content(), response.cas(), response.mutationToken());
                }

                switch (response.status()) {
                    case TOO_BIG:
                        throw new RequestTooBigException();
                    case EXISTS:
                        throw new CASMismatchException();
                    case TEMPORARY_FAILURE:
                    case SERVER_BUSY:
                        throw new TemporaryFailureException();
                    case OUT_OF_MEMORY:
                        throw new CouchbaseOutOfMemoryException();
                    default:
                        throw new CouchbaseException(response.status().toString());
                }
            }
        });
    }

    @Override
    public > Observable upsert(final D document, final PersistTo persistTo,
        final ReplicateTo replicateTo) {
        Observable upsertResult = upsert(document);

        if (persistTo == PersistTo.NONE && replicateTo == ReplicateTo.NONE) {
            return upsertResult;
        }

        return upsertResult.flatMap(new Func1>() {
            @Override
            public Observable call(final D doc) {
                return Observe
                    .call(core, bucket, doc.id(), doc.cas(), false, doc.mutationToken(), persistTo.value(), replicateTo.value(),
                        environment.observeIntervalDelay(), environment.retryStrategy())
                    .map(new Func1() {
                        @Override
                        public D call(Boolean aBoolean) {
                            return doc;
                        }
                    })
                    .onErrorResumeNext(new Func1>() {
                        @Override
                        public Observable call(Throwable throwable) {
                            return Observable.error(new DurabilityException(
                                "Durability requirement failed: " + throwable.getMessage(),
                                throwable));
                        }
                    });
            }
        });
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable replace(final D document) {
        final  Transcoder, Object> transcoder = (Transcoder, Object>) transcoders.get(document.getClass());

        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                Tuple2 encoded = transcoder.encode((Document) document);
                return core.send(new ReplaceRequest(document.id(), encoded.value1(), document.cas(), document.expiry(), encoded.value2(), bucket));
            }
        }).map(new Func1() {
            @Override
            public D call(ReplaceResponse response) {
                if (response.content() != null && response.content().refCnt() > 0) {
                    response.content().release();
                }

                if (response.status().isSuccess()) {
                    return (D) transcoder.newDocument(document.id(), document.expiry(),
                        document.content(), response.cas(), response.mutationToken());
                }

                switch (response.status()) {
                    case TOO_BIG:
                        throw new RequestTooBigException();
                    case NOT_EXISTS:
                        throw new DocumentDoesNotExistException();
                    case EXISTS:
                        throw new CASMismatchException();
                    case TEMPORARY_FAILURE:
                    case SERVER_BUSY:
                        throw new TemporaryFailureException();
                    case OUT_OF_MEMORY:
                        throw new CouchbaseOutOfMemoryException();
                    default:
                        throw new CouchbaseException(response.status().toString());
                }
            }
        });
    }

    @Override
    public > Observable replace(final D document, final PersistTo persistTo,
        final ReplicateTo replicateTo) {
        Observable replaceResult = replace(document);

        if (persistTo == PersistTo.NONE && replicateTo == ReplicateTo.NONE) {
            return replaceResult;
        }

        return replaceResult.flatMap(new Func1>() {
            @Override
            public Observable call(final D doc) {
                return Observe
                    .call(core, bucket, doc.id(), doc.cas(), false, doc.mutationToken(), persistTo.value(), replicateTo.value(),
                        environment.observeIntervalDelay(), environment.retryStrategy())
                    .map(new Func1() {
                        @Override
                        public D call(Boolean aBoolean) {
                            return doc;
                        }
                    }).onErrorResumeNext(new Func1>() {
                        @Override
                        public Observable call(Throwable throwable) {
                            return Observable.error(new DurabilityException(
                                "Durability requirement failed: " + throwable.getMessage(),
                                throwable));
                        }
                    });
            }
        });
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable remove(final D document) {
        final  Transcoder, Object> transcoder = (Transcoder, Object>) transcoders.get(document.getClass());
        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                return core.send(new RemoveRequest(document.id(), document.cas(), bucket));
            }
        }).map(new Func1() {
            @Override
            public D call(final RemoveResponse response) {
                if (response.content() != null && response.content().refCnt() > 0) {
                    response.content().release();
                }

                if (response.status().isSuccess()) {
                    return (D) transcoder.newDocument(document.id(), 0, null, response.cas(), response.mutationToken());
                }

                switch (response.status()) {
                    case NOT_EXISTS:
                        throw new DocumentDoesNotExistException();
                    case EXISTS:
                        throw new CASMismatchException();
                    case TEMPORARY_FAILURE:
                    case SERVER_BUSY:
                        throw new TemporaryFailureException();
                    case OUT_OF_MEMORY:
                        throw new CouchbaseOutOfMemoryException();
                    default:
                        throw new CouchbaseException(response.status().toString());
                }
            }
        });
    }

    @Override
    public Observable remove(final String id) {
        return remove(id, JsonDocument.class);
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable remove(final String id, final Class target) {
        final  Transcoder, Object> transcoder = (Transcoder, Object>) transcoders.get(target);
        return remove((D) transcoder.newDocument(id, 0, null, 0));
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable remove(D document, final PersistTo persistTo, final ReplicateTo replicateTo) {
        return observeRemove(remove(document), persistTo, replicateTo);
    }

    @Override
    public Observable remove(String id, PersistTo persistTo, ReplicateTo replicateTo) {
        return remove(id, persistTo, replicateTo, JsonDocument.class);
    }

    @Override
    public > Observable remove(String id, final PersistTo persistTo,
        final ReplicateTo replicateTo, Class target) {
        return observeRemove(remove(id, target), persistTo, replicateTo);
    }

    /**
     * Helper method to observe the result of a remove operation with the given durability
     * requirements.
     *
     * @param removeResult the original result of the actual remove operation.
     * @param persistTo the persistence requirement given.
     * @param replicateTo the replication requirement given.
     * @return an observable reporting success or error of the observe operation.
     */
    private > Observable observeRemove(Observable removeResult,
        final PersistTo persistTo, final ReplicateTo replicateTo) {
        if (persistTo == PersistTo.NONE && replicateTo == ReplicateTo.NONE) {
            return removeResult;
        }

        return removeResult.flatMap(new Func1>() {
            @Override
            public Observable call(final D doc) {
                return Observe
                    .call(core, bucket, doc.id(), doc.cas(), true, doc.mutationToken(),
                        persistTo.value(), replicateTo.value(),
                        environment.observeIntervalDelay(), environment.retryStrategy())
                    .map(new Func1() {
                        @Override
                        public D call(Boolean aBoolean) {
                            return doc;
                        }
                    }).onErrorResumeNext(new Func1>() {
                        @Override
                        public Observable call(Throwable throwable) {
                            return Observable.error(new DurabilityException(
                                "Durability requirement failed: " + throwable.getMessage(),
                                throwable));
                        }
                    });
            }
        });
    }

    @Override
    public Observable query(final ViewQuery query) {
        Observable source = Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                final ViewQueryRequest request = new ViewQueryRequest(query.getDesign(), query.getView(),
                    query.isDevelopment(), query.toQueryString(), query.getKeys(), bucket, password);
                return core.send(request);
            }
        });

        return ViewRetryHandler
            .retryOnCondition(source)
            .flatMap(new Func1>() {
                @Override
                public Observable call(final ViewQueryResponse response) {
                    return ViewQueryResponseMapper.mapToViewResult(CouchbaseAsyncBucket.this, query, response);
                }
            });
    }

    @Override
    public Observable query(final SearchQuery query) {
        final String indexName = query.indexName();
        final AbstractFtsQuery queryPart = query.query();

        //always set a server side timeout. if not explicit, set it to the client side timeout
        if (query.getServerSideTimeout() == null) {
            query.serverSideTimeout(environment().searchTimeout(), TimeUnit.MILLISECONDS);
        }

        Observable source = Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                final SearchQueryRequest request =
                    new SearchQueryRequest(indexName, query.export().toString(), bucket, password);
                return core.send(request);
            }
        });

        return source.map(new Func1() {
            @Override
            public AsyncSearchQueryResult call(SearchQueryResponse response) {
                if (response.status().isSuccess()) {
                    JsonObject json = JsonObject.fromJson(response.payload());
                    return DefaultAsyncSearchQueryResult.fromJson(json);
                } else if (response.status() == ResponseStatus.INVALID_ARGUMENTS) {
                    return DefaultAsyncSearchQueryResult.fromHttp400(response.payload());
                } else {
                    throw new CouchbaseException("Could not query search index, " + response.status() + ": " + response.payload());
                }
            }
        });
    }

    @Override
    public Observable query(final SpatialViewQuery query) {
        Observable source = Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                final ViewQueryRequest request = new ViewQueryRequest(query.getDesign(), query.getView(),
                    query.isDevelopment(), true, query.toString(), null, bucket, password);
                return core.send(request);
            }
        });

        return ViewRetryHandler
            .retryOnCondition(source)
            .flatMap(new Func1>() {
                @Override
                public Observable call(final ViewQueryResponse response) {
                    return ViewQueryResponseMapper.mapToSpatialViewResult(CouchbaseAsyncBucket.this, query, response);
                }
            });
    }

    @Override
    public Observable query(final Statement statement) {
        return query(N1qlQuery.simple(statement));
    }

    @Override
    public Observable query(final N1qlQuery query) {
        if (!query.params().hasServerSideTimeout()) {
            query.params().serverSideTimeout(environment().queryTimeout(), TimeUnit.MILLISECONDS);
        }
        return n1qlQueryExecutor.execute(query);
    }

    @Override
    public Observable counter(String id, long delta) {
        return counter(id, delta, 0, COUNTER_NOT_EXISTS_EXPIRY);
    }

    @Override
    public Observable counter(String id, long delta, long initial) {
        return counter(id, delta, initial, 0);
    }

    @Override
    public Observable counter(final String id, final long delta, final long initial, final int expiry) {
        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                return core.send(new CounterRequest(id, initial, delta, expiry, bucket));
            }
        }).map(new Func1() {
            @Override
            public JsonLongDocument call(CounterResponse response) {
                if (response.content() != null && response.content().refCnt() > 0) {
                    response.content().release();
                }

                if (response.status().isSuccess()) {
                    int returnedExpiry = expiry == COUNTER_NOT_EXISTS_EXPIRY ? 0 : expiry;
                    return JsonLongDocument.create(id, returnedExpiry, response.value(),
                        response.cas(), response.mutationToken());
                }

                switch (response.status()) {
                    case NOT_EXISTS:
                        throw new DocumentDoesNotExistException();
                    case TEMPORARY_FAILURE:
                    case SERVER_BUSY:
                        throw new TemporaryFailureException();
                    case OUT_OF_MEMORY:
                        throw new CouchbaseOutOfMemoryException();
                    default:
                        throw new CouchbaseException(response.status().toString());
                }
            }
        });
    }

    @Override
    public Observable unlock(final String id, final long cas) {
        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                return core.send(new UnlockRequest(id, cas, bucket));
            }
        }).map(new Func1() {
            @Override
            public Boolean call(UnlockResponse response) {
                if (response.content() != null && response.content().refCnt() > 0) {
                    response.content().release();
                }

                if (response.status().isSuccess()) {
                    return true;
                }

                switch (response.status()) {
                    case NOT_EXISTS:
                        throw new DocumentDoesNotExistException();
                    case TEMPORARY_FAILURE:
                        throw new TemporaryLockFailureException();
                    case SERVER_BUSY:
                        throw new TemporaryFailureException();
                    case OUT_OF_MEMORY:
                        throw new CouchbaseOutOfMemoryException();
                    default:
                        throw new CouchbaseException(response.status().toString());
                }
            }
        });
    }

    @Override
    public > Observable unlock(D document) {
        return unlock(document.id(), document.cas());
    }

    @Override
    public Observable touch(final String id, final int expiry) {
        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                return core.send(new TouchRequest(id, expiry, bucket));
            }
        }).map(new Func1() {
            @Override
            public Boolean call(TouchResponse response) {
                if (response.content() != null && response.content().refCnt() > 0) {
                    response.content().release();
                }

                if (response.status().isSuccess()) {
                    return true;
                }

                switch (response.status()) {
                    case NOT_EXISTS:
                        throw new DocumentDoesNotExistException();
                    case TEMPORARY_FAILURE:
                    case SERVER_BUSY:
                        throw new TemporaryFailureException();
                    case OUT_OF_MEMORY:
                        throw new CouchbaseOutOfMemoryException();
                    default:
                        throw new CouchbaseException(response.status().toString());
                }
            }
        });
    }

    @Override
    public > Observable touch(D document) {
        return touch(document.id(), document.expiry());
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable append(final D document) {
        final  Transcoder, Object> transcoder = (Transcoder, Object>) transcoders.get(document.getClass());

        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                Tuple2 encoded = transcoder.encode((Document) document);
                return core.send(new AppendRequest(document.id(), document.cas(), encoded.value1(), bucket));
            }
        }).map(new Func1() {
            @Override
            public D call(final AppendResponse response) {
                if (response.content() != null && response.content().refCnt() > 0) {
                    response.content().release();
                }

                if (response.status().isSuccess()) {
                    return (D) transcoder.newDocument(document.id(), 0, null, response.cas(), response.mutationToken());
                }

                switch (response.status()) {
                    case TOO_BIG:
                        throw new RequestTooBigException();
                    case NOT_STORED:
                        throw new DocumentDoesNotExistException();
                    case TEMPORARY_FAILURE:
                    case SERVER_BUSY:
                        throw new TemporaryFailureException();
                    case OUT_OF_MEMORY:
                        throw new CouchbaseOutOfMemoryException();
                    case EXISTS:
                        throw new CASMismatchException();
                    default:
                        throw new CouchbaseException(response.status().toString());
                }
            }
        });
    }

    @Override
    @SuppressWarnings("unchecked")
    public > Observable prepend(final D document) {
        final  Transcoder, Object> transcoder = (Transcoder, Object>) transcoders.get(document.getClass());
        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                Tuple2 encoded = transcoder.encode((Document) document);
                return core.send(new PrependRequest(document.id(), document.cas(), encoded.value1(), bucket));
            }
        }).map(new Func1() {
            @Override
            public D call(final PrependResponse response) {
                if (response.content() != null && response.content().refCnt() > 0) {
                    response.content().release();
                }

                if (response.status().isSuccess()) {
                    return (D) transcoder.newDocument(document.id(), 0, null, response.cas(), response.mutationToken());
                }

                switch (response.status()) {
                    case TOO_BIG:
                        throw new RequestTooBigException();
                    case NOT_STORED:
                        throw new DocumentDoesNotExistException();
                    case TEMPORARY_FAILURE:
                    case SERVER_BUSY:
                        throw new TemporaryFailureException();
                    case OUT_OF_MEMORY:
                        throw new CouchbaseOutOfMemoryException();
                    case EXISTS:
                        throw new CASMismatchException();
                    default:
                        throw new CouchbaseException(response.status().toString());
                }
            }
        });
    }

    @Override
    public Observable bucketManager() {
        return Observable.just(bucketManager);
    }

    @Override
    public > Observable insert(D document, PersistTo persistTo) {
        return insert(document, persistTo, ReplicateTo.NONE);
    }

    @Override
    public > Observable insert(D document, ReplicateTo replicateTo) {
        return insert(document, PersistTo.NONE, replicateTo);
    }

    @Override
    public > Observable upsert(D document, PersistTo persistTo) {
        return upsert(document, persistTo, ReplicateTo.NONE);
    }

    @Override
    public > Observable upsert(D document, ReplicateTo replicateTo) {
        return upsert(document, PersistTo.NONE, replicateTo);
    }

    @Override
    public > Observable replace(D document, PersistTo persistTo) {
        return replace(document, persistTo, ReplicateTo.NONE);
    }

    @Override
    public > Observable replace(D document, ReplicateTo replicateTo) {
        return replace(document, PersistTo.NONE, replicateTo);
    }

    @Override
    public > Observable remove(D document, PersistTo persistTo) {
        return remove(document, persistTo, ReplicateTo.NONE);
    }

    @Override
    public > Observable remove(D document, ReplicateTo replicateTo) {
        return remove(document, PersistTo.NONE, replicateTo);
    }

    @Override
    public Observable remove(String id, PersistTo persistTo) {
        return remove(id, persistTo, ReplicateTo.NONE);
    }

    @Override
    public Observable remove(String id, ReplicateTo replicateTo) {
        return remove(id, PersistTo.NONE, replicateTo);
    }

    @Override
    public > Observable remove(String id, PersistTo persistTo, Class target) {
        return remove(id, persistTo, ReplicateTo.NONE, target);
    }

    @Override
    public > Observable remove(String id, ReplicateTo replicateTo, Class target) {
        return remove(id, PersistTo.NONE, replicateTo, target);
    }

    @Override
    public Observable counter(String id, long delta, PersistTo persistTo) {
        return counter(id, delta, persistTo, ReplicateTo.NONE);
    }

    @Override
    public Observable counter(String id, long delta, ReplicateTo replicateTo) {
        return counter(id, delta, PersistTo.NONE, replicateTo);
    }

    @Override
    public Observable counter(String id, long delta, long initial, PersistTo persistTo) {
        return counter(id, delta, initial, persistTo, ReplicateTo.NONE);
    }

    @Override
    public Observable counter(String id, long delta, long initial, ReplicateTo replicateTo) {
        return counter(id, delta, initial, PersistTo.NONE, replicateTo);
    }

    @Override
    public Observable counter(String id, long delta, long initial, int expiry, PersistTo persistTo) {
        return counter(id, delta, initial, expiry, persistTo, ReplicateTo.NONE);
    }

    @Override
    public Observable counter(String id, long delta, long initial, int expiry, ReplicateTo replicateTo) {
        return counter(id, delta, initial, expiry, PersistTo.NONE, replicateTo);
    }

    @Override
    public Observable counter(String id, long delta, long initial, PersistTo persistTo, ReplicateTo replicateTo) {
        return counter(id, delta, initial, 0, persistTo, replicateTo);
    }

    @Override
    public Observable counter(String id, long delta, PersistTo persistTo, ReplicateTo replicateTo) {
        return counter(id, delta, 0, COUNTER_NOT_EXISTS_EXPIRY, persistTo, replicateTo);
    }

    @Override
    public Observable counter(String id, long delta, long initial, int expiry, final PersistTo persistTo, final ReplicateTo replicateTo) {

        Observable counterResult = counter(id, delta, initial, expiry);

        if (persistTo == PersistTo.NONE && replicateTo == ReplicateTo.NONE) {
            return counterResult;
        }

        return counterResult.flatMap(new Func1>() {
            @Override
            public Observable call(final JsonLongDocument doc) {
                return Observe
                    .call(core, bucket, doc.id(), doc.cas(), false, doc.mutationToken(),
                        persistTo.value(), replicateTo.value(),
                        environment.observeIntervalDelay(), environment.retryStrategy())
                    .map(new Func1() {
                        @Override
                        public JsonLongDocument call(Boolean aBoolean) {
                            return doc;
                        }
                    })
                    .onErrorResumeNext(new Func1>() {
                        @Override
                        public Observable call(Throwable throwable) {
                            return Observable.error(new DurabilityException(
                                "Durability requirement failed: " + throwable.getMessage(),
                                throwable));
                        }
                    });
            }
        });
    }

    @Override
    public > Observable append(D document, PersistTo persistTo) {
        return append(document, persistTo, ReplicateTo.NONE);
    }

    @Override
    public > Observable append(D document, ReplicateTo replicateTo) {
        return append(document, PersistTo.NONE, replicateTo);
    }

    @Override
    public > Observable append(D document, final PersistTo persistTo, final ReplicateTo replicateTo) {
        Observable appendResult = append(document);

        if (persistTo == PersistTo.NONE && replicateTo == ReplicateTo.NONE) {
            return appendResult;
        }

        return appendResult.flatMap(new Func1>() {
            @Override
            public Observable call(final D doc) {
                return Observe
                    .call(core, bucket, doc.id(), doc.cas(), false, doc.mutationToken(), persistTo.value(), replicateTo.value(),
                        environment.observeIntervalDelay(), environment.retryStrategy())
                    .map(new Func1() {
                        @Override
                        public D call(Boolean aBoolean) {
                            return doc;
                        }
                    }).onErrorResumeNext(new Func1>() {
                        @Override
                        public Observable call(Throwable throwable) {
                            return Observable.error(new DurabilityException(
                                "Durability requirement failed: " + throwable.getMessage(),
                                throwable));
                        }
                    });
            }
        });
    }

    @Override
    public > Observable prepend(D document, PersistTo persistTo) {
        return prepend(document, persistTo, ReplicateTo.NONE);
    }

    @Override
    public > Observable prepend(D document, ReplicateTo replicateTo) {
        return prepend(document, PersistTo.NONE, replicateTo);
    }

    @Override
    public > Observable prepend(D document, final PersistTo persistTo, final ReplicateTo replicateTo) {
        Observable prependResult = prepend(document);

        if (persistTo == PersistTo.NONE && replicateTo == ReplicateTo.NONE) {
            return prependResult;
        }

        return prependResult.flatMap(new Func1>() {
            @Override
            public Observable call(final D doc) {
                return Observe
                    .call(core, bucket, doc.id(), doc.cas(), false, doc.mutationToken(), persistTo.value(), replicateTo.value(),
                        environment.observeIntervalDelay(), environment.retryStrategy())
                    .map(new Func1() {
                        @Override
                        public D call(Boolean aBoolean) {
                            return doc;
                        }
                    }).onErrorResumeNext(new Func1>() {
                        @Override
                        public Observable call(Throwable throwable) {
                            return Observable.error(new DurabilityException(
                                "Durability requirement failed: " + throwable.getMessage(),
                                throwable));
                        }
                    });
            }
        });
    }

    /*---------------------------*
     * START OF SUB-DOCUMENT API *
     *---------------------------*/

    @Override
    public AsyncLookupInBuilder lookupIn(String docId) {
        return new AsyncLookupInBuilder(core, bucket, environment, subdocumentTranscoder, docId);
    }

    @Override
    public AsyncMutateInBuilder mutateIn(String docId) {
        return new AsyncMutateInBuilder(core, bucket, environment, subdocumentTranscoder, docId);
    }

    /*-------------------------*
     * END OF SUB-DOCUMENT API *
     *-------------------------*/

    @Override
    public Observable close() {
        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                return core.send(new CloseBucketRequest(bucket));
            }
        }).map(new Func1() {
            @Override
            public Boolean call(CloseBucketResponse response) {
                closed = true;
                return response.status().isSuccess();
            }
        });
    }

    @Override
    public boolean isClosed() {
        return closed;
    }

    @Override
    public String toString() {
        return "AsyncBucket[" + name() + "]";
    }

    @Override
    public Observable invalidateQueryCache() {
        return Observable.just(n1qlQueryExecutor.invalidateQueryCache());
    }
}