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

com.couchbase.client.java.datastructures.collections.CouchbaseArrayList Maven / Gradle / Ivy

There is a newer version: 3.7.7
Show newest version
/*
 * 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.datastructures.collections;

import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

import com.couchbase.client.core.annotations.InterfaceAudience;
import com.couchbase.client.core.annotations.InterfaceStability;
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.Mutation;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.document.JsonArrayDocument;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.document.json.JsonValue;
import com.couchbase.client.java.error.CASMismatchException;
import com.couchbase.client.java.error.DocumentAlreadyExistsException;
import com.couchbase.client.java.error.subdoc.MultiMutationException;
import com.couchbase.client.java.subdoc.DocumentFragment;

/**
 * A CouchbaseArrayList is a {@link List} backed by a {@link Bucket Couchbase} document (more
 * specifically a {@link JsonArrayDocument JSON array}).
 *
 * Note that as such, a CouchbaseArrayList is restricted to the types that a {@link JsonArray JSON array}
 * can contain. JSON objects and sub-arrays can be represented as {@link JsonObject} and {@link JsonArray}
 * respectively.
 *
 * @param  the type of values in the list.
 *
 * @author Simon Baslé
 * @author Subhashni Balakrishnan
 * @since 2.3.6
 */


@InterfaceStability.Committed
@InterfaceAudience.Public
public class CouchbaseArrayList extends AbstractList {

    private static final int MAX_OPTIMISTIC_LOCKING_ATTEMPTS = Integer.parseInt(System.getProperty("com.couchbase.datastructureCASRetryLimit", "10"));
    private final String id;
    private final Bucket bucket;

    /**
     * Create a new {@link Bucket Couchbase-backed} List, backed by the document identified by id
     * in bucket. Note that if the document already exists, its content will be used as initial
     * content for this collection. Otherwise it is created empty.
     *
     * @param id the id of the Couchbase document to back the list.
     * @param bucket the {@link Bucket} through which to interact with the document.
     */
    public CouchbaseArrayList(String id, Bucket bucket) {
        this.bucket = bucket;
        this.id = id;

        try {
            bucket.insert(JsonArrayDocument.create(id, JsonArray.empty()));
        } catch (DocumentAlreadyExistsException ex) {
            // Ignore concurrent creations, keep on moving.
        }
    }

    /**
     * Create a new {@link Bucket Couchbase-backed} List, backed by the document identified by id
     * in bucket. Note that if the document already exists, its content is reset to the values
     * provided.
     *
     * Note that if you don't provide any value as a vararg, the {@link #CouchbaseArrayList(String, Bucket)}
     * constructor will be invoked instead, which will use pre-existing values as content. To create a new
     * List and force it to be empty, use {@link #CouchbaseArrayList(String, Bucket, Collection)} with an empty
     * collection.
     *
     * @param id the id of the Couchbase document to back the list.
     * @param bucket the {@link Bucket} through which to interact with the document.
     * @param content vararg of the elements to initially store in the List.
     */
    public CouchbaseArrayList(String id, Bucket bucket, E... content) {
        this.bucket = bucket;
        this.id = id;

        bucket.upsert(JsonArrayDocument.create(id, JsonArray.from(content)));
    }

    /**
     * Create a new {@link Bucket Couchbase-backed} List, backed by the document identified by id
     * in bucket. Note that if the document already exists, its content is reset to the values
     * provided in the content Collection.
     *
     * @param id the id of the Couchbase document to back the list.
     * @param bucket the {@link Bucket} through which to interact with the document.
     * @param content collection of the elements to initially store in the List.
     */
    public CouchbaseArrayList(String id, Bucket bucket, Collection content) {
        this.bucket = bucket;
        this.id = id;

        JsonArray array;
        if (content instanceof List) {
            array = JsonArray.from((List) content);
        } else {
            array = JsonArray.create();
            for (E e : content) {
                array.add(e);
            }
        }

        bucket.upsert(JsonArrayDocument.create(id, array));
    }

    @Override
    public E get(int index) {
        //fail fast on negative values, as they are interpreted as "starting from the back of the array" otherwise
        if (index < 0) {
            throw new IndexOutOfBoundsException("Index: " + index);
        }
        String idx = "[" + index + "]";

        DocumentFragment result = bucket.lookupIn(id).get(idx).execute();
        if (result.status(idx) == ResponseStatus.SUBDOC_PATH_NOT_FOUND) {
            throw new IndexOutOfBoundsException("Index: " + index);
        }

        return (E) result.content(idx);
    }

    @Override
    public int size() {
        //TODO in Spock, GET_COUNT should be available on subdoc
        JsonArrayDocument current = bucket.get(id, JsonArrayDocument.class);
        return current.content().size();
    }

    @Override
    public boolean isEmpty() {
        DocumentFragment current = bucket.lookupIn(id).exists("[0]").execute();
        return current.status("[0]") == ResponseStatus.SUBDOC_PATH_NOT_FOUND;
    }

    @Override
    public E set(int index, E element) {
        //fail fast on negative values, as they are interpreted as "starting from the back of the array" otherwise
        if (index < 0) {
            throw new IndexOutOfBoundsException("Index: " + index);
        }
        if (!JsonValue.checkType(element)) {
            throw new IllegalArgumentException("Unsupported value type.");
        }
        String idx = "["+index+"]";

        for(int i = 0; i < MAX_OPTIMISTIC_LOCKING_ATTEMPTS; i++) {
            try {
                DocumentFragment current = bucket.lookupIn(id).get(idx).execute();
                long returnCas = current.cas();
                Object result = current.content(idx);
                bucket.mutateIn(id).replace(idx, element).withCas(returnCas).execute();
                return (E) result;
            } catch (CASMismatchException ex) {
                //will need to retry get-and-set
            } catch (MultiMutationException ex) {
                if (ex.firstFailureStatus() == ResponseStatus.SUBDOC_PATH_NOT_FOUND
                        || ex.firstFailureStatus() == ResponseStatus.SUBDOC_PATH_INVALID) {
                    throw new IndexOutOfBoundsException("Index: " + index);
                }
                throw ex;
            }
        }
        throw new ConcurrentModificationException("Couldn't perform set in less than " + MAX_OPTIMISTIC_LOCKING_ATTEMPTS + " iterations");
    }

    @Override
    public void add(int index, E element) {
        //fail fast on negative values, as they are interpreted as "starting from the back of the array" otherwise
        if (index < 0) {
            throw new IndexOutOfBoundsException("Index: " + index);
        }
        if (!JsonValue.checkType(element)) {
            throw new IllegalArgumentException("Unsupported value type.");
        }

        try {
            bucket.mutateIn(id).arrayInsert("["+index+"]", element).execute();
        } catch (MultiMutationException ex) {
            if (ex.firstFailureStatus() == ResponseStatus.SUBDOC_PATH_NOT_FOUND ||
                    ex.firstFailureStatus() == ResponseStatus.SUBDOC_PATH_INVALID) {
                throw new IndexOutOfBoundsException("Index: " + index);
            }
            throw ex;
        }
    }

    @Override
    public E remove(int index) {
        //fail fast on negative values, as they are interpreted as "starting from the back of the array" otherwise
        if (index < 0) {
            throw new IndexOutOfBoundsException("Index: " + index);
        }
        String idx = "[" + index + "]";
        for(int i = 0; i < MAX_OPTIMISTIC_LOCKING_ATTEMPTS; i++) {
            try {
                DocumentFragment current = bucket.lookupIn(id).get(idx).execute();
                long returnCas = current.cas();
                Object result = current.content(idx);
                DocumentFragment updated = bucket.mutateIn(id).remove(idx).withCas(returnCas).execute();
                return (E) result;
            } catch (CASMismatchException ex) {
                //will have to retry get-and-remove
            } catch (MultiMutationException ex) {
                if (ex.firstFailureStatus() == ResponseStatus.SUBDOC_PATH_NOT_FOUND) {
                    throw new IndexOutOfBoundsException("Index: " + index);
                }
                throw ex;
            }
        }
        throw new ConcurrentModificationException("Couldn't perform remove in less than " + MAX_OPTIMISTIC_LOCKING_ATTEMPTS + " iterations");
    }

    @Override
    public boolean contains(Object o) {
        //TODO in Spock subdoc may have ARRAY_CONTAINS which can help implement indexOf, remove(Object) and contains
        return super.contains(o);
    }

    @Override
    public Iterator iterator() {
        return new CouchbaseListIterator(0);
    }

    @Override
    public ListIterator listIterator() {
        return new CouchbaseListIterator(0);
    }

    @Override
    public ListIterator listIterator(int index) {
        return new CouchbaseListIterator(index);
    }

    @Override
    public void clear() {
        //optimized version over AbstractList's (which iterates on all and remove)
        bucket.upsert(JsonArrayDocument.create(id, JsonArray.empty()));
    }

    private class CouchbaseListIterator implements ListIterator {

        private long cas;
        private final ListIterator delegate;

        private int cursor;
        private int lastVisited;

        public CouchbaseListIterator(int index) {
            JsonArrayDocument current = bucket.get(id, JsonArrayDocument.class);
            //Care not to use toList, as it will convert internal JsonObject/JsonArray to Map/List
            List list = new ArrayList(current.content().size());
            for (E value : (Iterable) current.content()) {
                list.add(value);
            }
            this.cas = current.cas();
            this.delegate = list.listIterator(index);
            this.lastVisited = -1;
            this.cursor = index;
        }

        @Override
        public boolean hasNext() {
            return delegate.hasNext();
        }

        @Override
        public E next() {
            E next = delegate.next();
            lastVisited = cursor;
            cursor++;
            return next;
        }

        @Override
        public boolean hasPrevious() {
            return delegate.hasPrevious();
        }

        @Override
        public E previous() {
            E previous = delegate.previous();
            cursor--;
            lastVisited = cursor;
            return previous;
        }

        @Override
        public int nextIndex() {
            return delegate.nextIndex();
        }

        @Override
        public int previousIndex() {
            return delegate.previousIndex();
        }

        @Override
        public void remove() {
            if (lastVisited < 0) {
                throw new IllegalStateException();
            }
            int index = lastVisited;
            String idx = "[" + index + "]";
            try {
                DocumentFragment updated = bucket.mutateIn(id).remove(idx).withCas(this.cas).execute();
                //update the cas so that several removes in a row can work
                this.cas = updated.cas();
                //also correctly reset the state:
                delegate.remove();
                this.cursor = lastVisited;
                this.lastVisited = -1;
            } catch (CASMismatchException ex) {
                throw new ConcurrentModificationException("List was modified since iterator creation: " + ex);
            } catch (MultiMutationException ex) {
                if (ex.firstFailureStatus() == ResponseStatus.SUBDOC_PATH_NOT_FOUND) {
                    throw new ConcurrentModificationException("Element doesn't exist anymore at index: " + index);
                }
                throw ex;
            }
        }

        @Override
        public void set(E e) {
            if (lastVisited < 0) {
                throw new IllegalStateException();
            }
            int index = lastVisited;
            String idx = "[" + index + "]";
            try {
                DocumentFragment updated = bucket.mutateIn(id).replace(idx, e).withCas(this.cas).execute();
                //update the cas so that several mutations in a row can work
                this.cas = updated.cas();
                //also correctly reset the state:
                delegate.set(e);
            } catch (CASMismatchException ex) {
                throw new ConcurrentModificationException("List was modified since iterator creation: " + ex);
            } catch (MultiMutationException ex) {
                if (ex.firstFailureStatus() == ResponseStatus.SUBDOC_PATH_NOT_FOUND) {
                    throw new ConcurrentModificationException("Element doesn't exist anymore at index: " + index);
                }
                throw ex;
            }
        }

        @Override
        public void add(E e) {
            int index = this.cursor;
            String idx = "[" + index + "]";
            try {
                DocumentFragment updated = bucket.mutateIn(id).arrayInsert(idx, e).withCas(this.cas).execute();
                //update the cas so that several mutations in a row can work
                this.cas = updated.cas();
                //also correctly reset the state:
                delegate.add(e);
                this.cursor++;
                this.lastVisited = -1;
            } catch (CASMismatchException ex) {
                //TODO if target switch to Java 7, use ctor with a Cause
                throw new ConcurrentModificationException("List was modified since iterator creation: " + ex);
            } catch (MultiMutationException ex) {
                if (ex.firstFailureStatus() == ResponseStatus.SUBDOC_PATH_NOT_FOUND) {
                    throw new ConcurrentModificationException("Element doesn't exist anymore at index: " + index);
                }
                throw ex;
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy