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

com.couchbase.client.java.datastructures.collections.CouchbaseMap 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.AbstractMap;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

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.JsonDocument;
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.error.subdoc.PathNotFoundException;
import com.couchbase.client.java.subdoc.DocumentFragment;

/**
 * A CouchbaseMap is a {@link Map} backed by a {@link Bucket Couchbase} document (more specifically a
 * {@link JsonDocument JSON object}).
 *
 * Null keys are NOT permitted, and keys are restricted to {@link String}.
 *
 * Values in a CouchbaseMap are restricted to the types that a {@link JsonObject JSON objects}
 * can contain. JSON sub-objects and sub-arrays can be represented as {@link JsonObject} and {@link JsonArray}
 * respectively.
 *
 * @param  the type of values in the map (restricted to {@link JsonObject}.
 *
 * @author Simon Baslé
 * @author Subhashni Balakrishnan
 * @since 2.3.6
 */

@InterfaceStability.Committed
@InterfaceAudience.Public
public class CouchbaseMap extends AbstractMap {

    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 CouchbaseMap}, backed by the document identified by id
     * in the given Couchbase 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 map.
     * @param bucket the {@link Bucket} through which to interact with the document.
     */
    public CouchbaseMap(String id, Bucket bucket) {
        this.id = id;
        this.bucket = bucket;

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

    /**
     * Create a new {@link CouchbaseMap}, backed by the document identified by id
     * in the given Couchbase bucket. Note that if the document already exists, its content is
     * reset to the values copied from the given data Map.
     *
     * A null or empty data map will re-initialize any pre-existing document to an empty content.
     *
     * @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 data Map of the elements to initially store in the CouchbaseMap.
     */
    public CouchbaseMap(String id, Bucket bucket, Map data) {
        this.id = id;
        this.bucket = bucket;

        JsonObject content = JsonObject.create();
        if (data != null && !data.isEmpty()) {
            for (Entry entry : data.entrySet()) {
                if (entry.getKey() == null) {
                    throw new NullPointerException("Attempted to create a map with a null key");
                }
                content.put(entry.getKey(), entry.getValue());
            }
        }
        JsonDocument initial = JsonDocument.create(id, content);
        bucket.upsert(initial);
    }

    @Override
    public V put(String key, V value) {
        if (key == null) {
            throw new NullPointerException("Unsupported null key");
        }
        if (!JsonValue.checkType(value)) {
            throw new IllegalArgumentException("Unsupported value type.");
        }

        for(int i = 0; i < MAX_OPTIMISTIC_LOCKING_ATTEMPTS; i++) {
            try {
                DocumentFragment current = bucket.lookupIn(id).get(key).execute();
                long returnCas = current.cas();
                Object result = null;
                if (current.exists(key)) {
                    result = current.content(key);
                }
                bucket.mutateIn(id).upsert(key, value, false).withCas(returnCas).execute();
                return (V) result;
            } catch (CASMismatchException ex) {
                //will need to retry get-and-set
            }
        }
        throw new ConcurrentModificationException("Couldn't perform put in less than " + MAX_OPTIMISTIC_LOCKING_ATTEMPTS + " iterations");
    }

    @Override
    public V get(Object key) {
        if (key == null) {
            throw new NullPointerException("Unsupported null key");
        }
        try {
            return (V) bucket.lookupIn(id)
                    .get(String.valueOf(key))
                    .execute()
                    .content(0);
        } catch (PathNotFoundException e) {
            return null;
        }
    }



    @Override
    public V remove(Object key) {
        if (key == null) {
            throw new NullPointerException("Unsupported null key");
        }
        String idx = String.valueOf(key);
        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 (V) result;
            } catch (CASMismatchException ex) {
                //will have to retry get-and-remove
            } catch (MultiMutationException ex) {
                if (ex.firstFailureStatus() == ResponseStatus.SUBDOC_PATH_NOT_FOUND) {
                    return null;
                }
                throw ex;
            }
        }
        throw new ConcurrentModificationException("Couldn't perform remove in less than " + MAX_OPTIMISTIC_LOCKING_ATTEMPTS + " iterations");
    }

    @Override
    public void clear() {
        //optimized version over AbstractMap's (which uses the entry set)
        bucket.upsert(JsonDocument.create(id, JsonObject.empty()));
    }

    @Override
    public Set> entrySet() {
        return new CouchbaseEntrySet((Map) bucket.get(id).content().toMap());
    }

    @Override
    public boolean containsKey(Object key) {
        return (Boolean) bucket
                .lookupIn(id).exists(String.valueOf(key))
                .execute()
                .content(0);
    }

    @Override
    public boolean containsValue(Object value) {
        return super.containsValue(value); //TODO use ARRAY_CONTAINS subdoc operator when available
    }

    @Override
    public int size() {
        return super.size(); //TODO use COUNT subdoc operator when available
    }

    private class CouchbaseEntrySet implements Set> {

        private final Set> delegate;

        private CouchbaseEntrySet(Map data) {
            this.delegate = data.entrySet();
        }

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

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

        @Override
        public boolean contains(Object o) {
            return delegate.contains(o);
        }

        @Override
        public Iterator> iterator() {
            return new CouchbaseEntrySetIterator(delegate.iterator());
        }

        @Override
        public Object[] toArray() {
            return delegate.toArray();
        }

        @Override
        public  T[] toArray(T[] a) {
            return delegate.toArray(a);
        }

        @Override
        public boolean add(Entry stringVEntry) {
            return delegate.add(stringVEntry);
        }

        @Override
        public boolean remove(Object o) {
            if (delegate.remove(o)) {
                if (o instanceof Map.Entry) {
                    Entry entry = (Entry) o;
                    CouchbaseMap.this.remove(entry.getKey());
                } else {
                    throw new IllegalStateException("Expected entrySet remove() to remove an entry");
                }
                return true;
            } else {
                return false;
            }
        }

        @Override
        public boolean containsAll(Collection c) {
            return delegate.containsAll(c);
        }

        @Override
        public boolean addAll(Collection> c) {
            return delegate.addAll(c);
        }

        @Override
        public boolean retainAll(Collection c) {
            return delegate.retainAll(c);
        }

        @Override
        public boolean removeAll(Collection c) {
            return delegate.removeAll(c);
        }

        @Override
        public void clear() {
            delegate.clear();
            CouchbaseMap.this.clear();
        }

        @Override
        public String toString() {
            return delegate.toString();
        }

        public int hashCode() {
            return delegate.hashCode();
        }

        public boolean equals(Object obj) {
            return delegate.equals(obj);
        }
    }

    private class CouchbaseEntrySetIterator implements Iterator> {

        private final Iterator> delegateItr;
        private Entry lastNext = null;

        public CouchbaseEntrySetIterator(Iterator> iterator) {
            this.delegateItr = iterator;
        }

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

        @Override
        public Entry next() {
            this.lastNext = delegateItr.next();
            return lastNext;
        }

        @Override
        public void remove() {
            if (lastNext == null)
                throw new IllegalStateException("next() hasn't been called before remove()");
            delegateItr.remove();
            CouchbaseMap.this.remove(lastNext.getKey());
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy