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

com.couchbase.client.java.subdoc.AsyncLookupInBuilder 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.subdoc;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.couchbase.client.core.ClusterFacade;
import com.couchbase.client.core.annotations.InterfaceAudience;
import com.couchbase.client.core.annotations.InterfaceStability;
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.kv.subdoc.multi.Lookup;
import com.couchbase.client.core.message.kv.subdoc.multi.MultiLookupResponse;
import com.couchbase.client.core.message.kv.subdoc.multi.MultiResult;
import com.couchbase.client.core.message.kv.subdoc.multi.SubMultiLookupRequest;
import com.couchbase.client.core.message.kv.subdoc.simple.SimpleSubdocResponse;
import com.couchbase.client.core.message.kv.subdoc.simple.SubExistRequest;
import com.couchbase.client.core.message.kv.subdoc.simple.SubGetRequest;
import com.couchbase.client.deps.io.netty.util.internal.StringUtil;
import com.couchbase.client.java.AsyncBucket;
import com.couchbase.client.java.document.JsonDocument;
import com.couchbase.client.java.env.CouchbaseEnvironment;
import com.couchbase.client.java.error.DocumentDoesNotExistException;
import com.couchbase.client.java.error.TranscodingException;
import com.couchbase.client.java.error.subdoc.DocumentNotJsonException;
import com.couchbase.client.java.error.subdoc.SubDocumentException;
import com.couchbase.client.java.transcoder.subdoc.FragmentTranscoder;
import rx.Observable;
import rx.functions.Func0;
import rx.functions.Func1;

/**
 * A builder for subdocument lookups. In order to perform the final set of operations, use the
 * {@link #execute()} method. Operations are performed asynchronously (see {@link LookupInBuilder} for a synchronous
 * version).
 *
 * Instances of this builder should be obtained through {@link AsyncBucket#lookupIn(String)} rather than directly
 * constructed.
 *
 * @author Simon Baslé
 * @since 2.2
 */
@InterfaceStability.Committed
@InterfaceAudience.Public
public class AsyncLookupInBuilder {

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

    private final ClusterFacade core;
    private final CouchbaseEnvironment environment;
    private final String bucketName;
    private final FragmentTranscoder subdocumentTranscoder;

    private final String docId;

    private final List specs;

    /**
     * Instances of this builder should be obtained through {@link AsyncBucket#lookupIn(String)} rather than directly
     * constructed.
    */
    @InterfaceAudience.Private
    public AsyncLookupInBuilder(ClusterFacade core, String bucketName, CouchbaseEnvironment environment,
            FragmentTranscoder transcoder, String docId) {
        if (docId == null || docId.isEmpty()) {
            throw new IllegalArgumentException("The document ID must not be null or empty.");
        }
        if (docId.getBytes().length > 250) {
            throw new IllegalArgumentException("The document ID must not be larger than 250 bytes");
        }

        this.core = core;
        this.bucketName = bucketName;
        this.environment = environment;
        this.subdocumentTranscoder = transcoder;
        this.docId = docId;
        this.specs = new ArrayList();
    }

    /**
     * Perform several {@link Lookup lookup} operations inside a single existing {@link JsonDocument JSON document}.
     * The list of path to look for inside the JSON is constructed through builder methods {@link #get(String...)} and
     * {@link #exists(String...)}.
     *
     * The subdocument API has the benefit of only transmitting the fragment of the document you work with
     * on the wire, instead of the whole document.
     *
     * If multiple operations are specified, each spec will receive an answer, overall contained in a
     * {@link DocumentFragment}, meaning that if sub-document level error conditions happen (like the path is malformed
     * or doesn't exist), the whole operation still succeeds.
     *
     * If a single operation is specified, then any error other that a path not found will cause the Observable to
     * fail with the corresponding {@link SubDocumentException}. Otherwise a {@link DocumentFragment} is returned.
     *
     * Calling {@link DocumentFragment#content(String)} or one of its variants on a failed spec/path will throw the
     * corresponding {@link SubDocumentException}. For successful gets, it will return the value (or null in the case
     * of a path not found, and only in this case). For exists, it will return true (or false for a path not found).
     *
     * To check for any error without throwing an exception, use {@link DocumentFragment#status(String)}
     * (or its index-based variant).
     *
     * To check that a given path (or index) is valid for calling {@link DocumentFragment#content(String) content()} on
     * it without raising an Exception, use {@link DocumentFragment#exists(String)}.
     *
     * One special fatal error can also happen, when the value couldn't be decoded from JSON. In that case,
     * the ResponseStatus for the path is {@link ResponseStatus#FAILURE} and the content(path) will throw a
     * {@link TranscodingException}.
     *
     * This Observable most notable error conditions are:
     *
     *  - The enclosing document does not exist: {@link DocumentDoesNotExistException}
     *  - The enclosing document is not JSON: {@link DocumentNotJsonException}
     *  - No lookup was defined through the builder API: {@link IllegalArgumentException}
     *
     * Other document-level error conditions are similar to those encountered during a document-level {@link AsyncBucket#get(String)}.
     *
     * @return an {@link Observable} of a single {@link DocumentFragment} representing the whole list of results (1 for
     *        each spec), unless a document-level error happened (in which case an exception is propagated).
     */
    public Observable> execute() {
        if (specs.isEmpty()) {
            throw new IllegalArgumentException("Execution of a subdoc lookup requires at least one operation");
        } else if (specs.size() == 1) {
            //single path optimization
            return doSingleLookup(specs.get(0));
        } else {
            return doMultiLookup();
        }
    }

    /**
     * Get a value inside the JSON document.
     *
     * @param paths the path inside the document where to get the value from.
     * @return this builder for chaining.
     */
    public AsyncLookupInBuilder get(String... paths) {
        if (paths == null || paths.length == 0) {
            throw new IllegalArgumentException("Path is mandatory for subdoc get");
        }
        for (String path : paths) {
            if (StringUtil.isNullOrEmpty(path)) {
                throw new IllegalArgumentException("Path is mandatory for subdoc get");
            }
            this.specs.add(new LookupSpec(Lookup.GET, path));
        }
        return this;
    }

    /**
     * Check if a value exists inside the document (if it does not, attempting to get the
     * {@link DocumentFragment#content(int)} will raise an error).
     * This doesn't transmit the value on the wire if it exists, saving the corresponding byte overhead.
     *
     * @param paths the path inside the document to check for existence.
     * @return this builder for chaining.
     */
    public AsyncLookupInBuilder exists(String... paths) {
        if (paths == null || paths.length == 0) {
            throw new IllegalArgumentException("Path is mandatory for subdoc exists");
        }
        for (String path : paths) {
            if (StringUtil.isNullOrEmpty(path)) {
                throw new IllegalArgumentException("Path is mandatory for subdoc exists");
            }
            this.specs.add(new LookupSpec(Lookup.EXIST, path));
        }
        return this;
    }

    protected Observable> doSingleLookup(LookupSpec spec) {
        if (spec.lookup() == Lookup.GET) {
            return getIn(docId, spec.path(), Object.class);
        } else if (spec.lookup() == Lookup.EXIST) {
            return existsIn(docId, spec.path());
        }
        return Observable.error(new UnsupportedOperationException("Lookup type " + spec.lookup() + " unknown"));
    }

    private final Func1, SubdocOperationResult> multiCoreResultToLookupResult
        = new Func1, SubdocOperationResult>() {
        @Override
        public SubdocOperationResult call(MultiResult lookupResult) {
            String path = lookupResult.path();
            Lookup operation = lookupResult.operation();
            ResponseStatus status = lookupResult.status();
            boolean isExist = operation == Lookup.EXIST;
            boolean isSuccess = status.isSuccess();
            boolean isNotFound = status == ResponseStatus.SUBDOC_PATH_NOT_FOUND;

            try {
                if (isExist && isSuccess) {
                    return SubdocOperationResult.createResult(path, operation, status, true);
                } else if (isExist && isNotFound) {
                    return SubdocOperationResult.createResult(path, operation, status, false);
                } else if (!isExist && isSuccess) {
                    try {
                        //generic, so will transform dictionaries into JsonObject and arrays into JsonArray
                        Object content = subdocumentTranscoder.decode(lookupResult.value(), Object.class);
                        return SubdocOperationResult.createResult(path, operation, status, content);
                    } catch (TranscodingException e) {
                        LOGGER.error("Couldn't decode multi-lookup " + operation + " for " + docId + "/" + path, e);
                        return SubdocOperationResult.createFatal(path, operation, e);
                    }
                } else if (!isExist && isNotFound) {
                    return SubdocOperationResult.createResult(path, operation, status, null);
                } else {
                    return SubdocOperationResult
                            .createError(path, operation, status, SubdocHelper.commonSubdocErrors(status, docId, path));
                }
            } finally {
                if (lookupResult.value() != null) {
                    lookupResult.value().release();
                }
            }
        }
    };

    protected Observable> doMultiLookup() {
        if (specs.isEmpty()) {
            throw new IllegalArgumentException("At least one Lookup Command is necessary for lookupIn");
        }
        final LookupSpec[] lookupSpecs = specs.toArray(new LookupSpec[specs.size()]);

        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                return core.send(new SubMultiLookupRequest(docId, bucketName, lookupSpecs));
            }
        }).filter(new Func1() {
            @Override
            public Boolean call(MultiLookupResponse response) {
                if (response.status().isSuccess() || response.status() == ResponseStatus.SUBDOC_MULTI_PATH_FAILURE) {
                    return true;
                }

                if (response.content() != null && response.content().refCnt() > 0) {
                    response.content().release();
                }

                throw SubdocHelper.commonSubdocErrors(response.status(), docId, "MULTI-LOOKUP");
            }
        }).flatMap(new Func1>>() {
            @Override
            public Observable> call(final MultiLookupResponse mlr) {
                return Observable
                    .from(mlr.responses()).map(multiCoreResultToLookupResult)
                    .toList()
                    .map(new Func1>, DocumentFragment>() {
                        @Override
                        public DocumentFragment call(List> lookupResults) {
                            return new DocumentFragment(docId, mlr.cas(), null, lookupResults);
                        }
                    });
            }
        });
    }


    private  Observable> getIn(final String id, final String path, final Class fragmentType) {
        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                SubGetRequest request = new SubGetRequest(id, path, bucketName);
                return core.send(request);
            }
        }).map(new Func1>() {
            @Override
            public DocumentFragment call(SimpleSubdocResponse response) {
                if (response.status().isSuccess()) {
                    try {
                        T content = subdocumentTranscoder.decodeWithMessage(response.content(), fragmentType,
                                "Couldn't decode subget fragment for " + id + "/" + path);
                        SubdocOperationResult single = SubdocOperationResult
                                .createResult(path, Lookup.GET, response.status(), content);
                        return new DocumentFragment(id, response.cas(), response.mutationToken(),
                                Collections.singletonList(single));
                    } finally {
                        if (response.content() != null) {
                            response.content().release();
                        }
                    }
                } else {
                    if (response.content() != null && response.content().refCnt() > 0) {
                        response.content().release();
                    }

                    if (response.status() == ResponseStatus.SUBDOC_PATH_NOT_FOUND) {
                        SubdocOperationResult single = SubdocOperationResult
                                .createResult(path, Lookup.GET, response.status(), null);
                        return new DocumentFragment(id, response.cas(), response.mutationToken(), Collections.singletonList(single));
                    } else {
                        throw SubdocHelper.commonSubdocErrors(response.status(), id, path);
                    }
                }
            }
        });
    }

    private Observable> existsIn(final String id, final String path) {
        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                SubExistRequest request = new SubExistRequest(id, path, bucketName);
                return core.send(request);
            }
        }).map(new Func1>() {
            @Override
            public DocumentFragment call(SimpleSubdocResponse response) {
                if (response.content() != null && response.content().refCnt() > 0) {
                    response.content().release();
                }

                if (response.status().isSuccess()) {
                    SubdocOperationResult result = SubdocOperationResult
                            .createResult(path, Lookup.EXIST, response.status(), true);
                    return new DocumentFragment(docId, response.cas(), response.mutationToken(), Collections.singletonList(result));
                } else if (response.status() == ResponseStatus.SUBDOC_PATH_NOT_FOUND) {
                    SubdocOperationResult result = SubdocOperationResult
                            .createResult(path, Lookup.EXIST, response.status(), false);
                    return new DocumentFragment(docId, response.cas(), response.mutationToken(), Collections.singletonList(result));
                }

                throw SubdocHelper.commonSubdocErrors(response.status(), id, path);
            }
        });
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("lookupIn(").append(docId).append(")[");
        int pos = sb.length();
        for (LookupSpec spec : specs) {
            sb.append(", ").append(spec);
        }
        sb.delete(pos, pos+2);
        sb.append(']');
        return sb.toString();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy