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

com.basho.riak.client.api.commands.kv.UpdateValue Maven / Gradle / Ivy

There is a newer version: 2.1.1
Show newest version
/*
 * Copyright 2013 Basho Technologies 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.basho.riak.client.api.commands.kv;

import com.basho.riak.client.core.RiakCluster;
import com.basho.riak.client.api.RiakCommand;
import com.basho.riak.client.api.cap.UnresolvedConflictException;
import com.basho.riak.client.api.cap.VClock;
import com.basho.riak.client.api.convert.ConversionException;
import com.basho.riak.client.api.convert.reflection.AnnotationUtil;
import com.basho.riak.client.core.RiakFuture;
import com.basho.riak.client.core.RiakFutureListener;
import com.basho.riak.client.api.commands.ListenableFuture;
import com.basho.riak.client.core.query.Location;
import com.basho.riak.client.core.query.RiakObject;
import com.fasterxml.jackson.core.type.TypeReference;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * Perform an full cycle update of a Riak value: fetch, resolve, modify, store.
 * @author Dave Rusek 
 * @since 2.0
 */
public final class UpdateValue extends RiakCommand
{
    private final Location location;
    private final Update update;
    private final TypeReference typeReference;
    private final Map, Object> fetchOptions =
	    new HashMap, Object>();
    private final Map, Object> storeOptions =
	    new HashMap, Object>();

    UpdateValue(Builder builder)
    {
        this.location = builder.location;
        this.update = builder.update;
        this.typeReference = builder.typeReference;
	    this.fetchOptions.putAll(builder.fetchOptions);
	    this.storeOptions.putAll(builder.storeOptions);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected RiakFuture executeAsync(final RiakCluster cluster)
    {
        final UpdateValueFuture updateFuture = new UpdateValueFuture(location);
        
        FetchValue.Builder fetchBuilder = new FetchValue.Builder(location);
        for (Map.Entry, Object> optPair : fetchOptions.entrySet())
        {
            fetchBuilder.withOption((FetchValue.Option) optPair.getKey(), optPair.getValue());
        }

        RiakFuture fetchFuture =
            fetchBuilder.build().executeAsync(cluster);
        
        // Anonymous listener that will do the work
        RiakFutureListener fetchListener =
            new RiakFutureListener()
            {
                @Override
                public void handle(RiakFuture f)
                {
                    if (f.isSuccess())
                    {
                        FetchValue.Response fetchResponse;
                        try 
                        {
                            fetchResponse = f.get();
                            Object resolved = null;
                            VClock vclock = null;
                            
                            if (!fetchResponse.isNotFound())
                            {
                                if (typeReference == null)
                                {
                                    // Steal the type from the Update. Yes, Really.
                                    ParameterizedType pType = (ParameterizedType)update.getClass().getGenericSuperclass();
                                    Type t = pType.getActualTypeArguments()[0];
                                    if (t instanceof ParameterizedType)
                                    {
                                        t = ((ParameterizedType)t).getRawType();
                                    }

                                    resolved = fetchResponse.getValue((Class) t);
                                }
                                else
                                {
                                    resolved = fetchResponse.getValue(typeReference);
                                }

                                // We get the vclock so we can inject it into the updated object. 
                                // This is so the end user doesn't have to worry about vclocks
                                // in the Update.
                                vclock = fetchResponse.getVectorClock();
                            }
                            
                            Object updated = ((Update)update).apply(resolved);

                            if (update.isModified())
                            {
                                AnnotationUtil.setVClock(updated, vclock);
                                
                                StoreValue.Builder store = 
                                    new StoreValue.Builder(updated, typeReference)
                                        .withLocation(location)
                                        .withVectorClock(vclock);

                                for (Map.Entry, Object> optPair : storeOptions.entrySet())
                                {
                                    store.withOption((StoreValue.Option) optPair.getKey(), optPair.getValue());
                                }
                                RiakFuture storeFuture = 
                                    store.build().executeAsync(cluster);
                                storeFuture.addListener(updateFuture);
                            }
                            else
                            {
                                Response updateResponse = new Response.Builder()
                                    .withLocation(f.getQueryInfo())
                                    .withUpdated(false)
                                    .build();
                                updateFuture.setResponse(updateResponse);
                            }
                            
                            
                        }
                        catch (InterruptedException ex) 
                        {
                            updateFuture.setException(ex);
                        }
                        catch (UnresolvedConflictException ex)
                        {
                            updateFuture.setException(ex);
                        }
                        catch (ConversionException ex)
                        {
                            updateFuture.setException(ex);
                        }
                    }
                    else
                    {
                        updateFuture.setException(f.cause().getCause());
                    }
                }
            };
        
        fetchFuture.addListener(fetchListener);
        return updateFuture;
    }
    
    /**
     *
     */
    public static class Response extends KvResponseBase
    {
        private final boolean wasUpdated;

        Response(Init builder)
        {
            super(builder);
            this.wasUpdated = builder.wasUpdated;
        }

        /**
         * Determine if an update occurred.
         * 

* The supplied {@code Update} indicates if a modification was made. If * no modification was made, no store operation is performed and this * will return false. *

* @return true if the supplied {@code Update} modified the retrieved object, * false otherwise. */ public boolean wasUpdated() { return wasUpdated; } /** * @ExcludeFromJavadoc */ protected static abstract class Init> extends KvResponseBase.Init { private boolean wasUpdated; T withUpdated(boolean updated) { this.wasUpdated = updated; return self(); } } static class Builder extends Init { @Override protected Builder self() { return this; } @Override Response build() { return new Response(this); } } } /** * An update on a Riak object * * @param */ public abstract static class Update { private boolean modified = true; /** * Modify the input value and return the modification. It is OK to * modify the input value in-place and return it. * * @param original the resolved value * @return a modified value */ public abstract T apply(T original); /** * Set the modification status of this update, defaults to {@code true} * * @param modified true if modified */ protected void setModified(boolean modified) { this.modified = modified; } /** * true if this Update has modified the input value and requires a store, * defaults to {@code true} * * @return true if modified */ public boolean isModified() { return modified; } /** * Returns a no-op Update instance. *

* This can be used to simply resolve siblings that exist * in Riak without modifying the resolved object. *

* * @return An {@code Update} instance the does not modify anything. */ public static Update noopUpdate() { return new Update() { @Override public T apply(T original) { return original; } }; } /** * Returns an Update that replaces whatever is in Riak. *

* This Update simply returns the supplied value, thus replacing * anything currently in Riak with that value. *

* @param newValue the obj to store in Riak. * @return an Update instance. */ public static Update clobberUpdate(final T newValue) { return new Update() { @Override public T apply(T original) { return newValue; } }; } } public static class Builder { private final Location location; private Update update; private TypeReference typeReference; private final Map, Object> fetchOptions = new HashMap, Object>(); private final Map, Object> storeOptions = new HashMap, Object>(); public Builder(Location location) { this.location = location; } /** * Add an option for the fetch phase of the update * * @param option the option * @param value the option's value * @param the type of the option's value * @return this */ public Builder withFetchOption(FetchValue.Option option, U value) { fetchOptions.put(option, value); return this; } /** * Add an option for the store phase of the update * * @param option the option * @param value the option's value * @param the type of the option's value * @return this */ public Builder withStoreOption(StoreValue.Option option, U value) { storeOptions.put(option, value); return this; } /** * Supply the Update. *

* During the update operation, the fetched value needs to be converted * before being passed to the {@code ConflictResolver} and the {@code Update} * method. *

*

* Supplying only an {@code Update} means the raw type of {@code T} * will be used to retrieve the {@code Converter} and {@code ConflictResolver} * to be used. *

* @param update The {@code Update} instance * @return a reference to this object. * @see com.basho.riak.client.convert.Converter * @see com.basho.riak.client.convert.ConverterFactory * @see com.basho.riak.client.cap.ConflictResolver * @see com.basho.riak.client.cap.ConflictResolverFactory */ public Builder withUpdate(Update update) { this.update = update; return this; } /** * Supply the Update with a TypeReference. *

* During the update operation, the fetched value needs to be converted * before being passed to the {@code ConflictResolver} and the {@code Update} * method. If your domain object is a parameterized type you will need to supply * a {@code TypeReference} so the appropriate {@code ConflictResolver} * and {@code Converter} can be found. *

* @param update The {@code Update} instance * @param typeReference the {@code TypeReference} for the class used for conversion. * @return a reference to this object. * @see com.basho.riak.client.convert.Converter * @see com.basho.riak.client.convert.ConverterFactory * @see com.basho.riak.client.cap.ConflictResolver * @see com.basho.riak.client.cap.ConflictResolverFactory */ public Builder withUpdate(Update update, TypeReference typeReference) { this.update = update; this.typeReference = typeReference; return this; } /** * Set the Riak-side timeout value. *

* By default, riak has a 60s timeout for operations. Setting * this value will override that default for both the * fetch and store operation. *

* @param timeout the timeout in milliseconds to be sent to riak. * @return a reference to this object. */ public Builder withTimeout(int timeout) { withFetchOption(FetchValue.Option.TIMEOUT, timeout); withStoreOption(StoreValue.Option.TIMEOUT, timeout); return this; } public UpdateValue build() { return new UpdateValue(this); } } private class UpdateValueFuture extends ListenableFuture implements RiakFutureListener { private final Location location; private final CountDownLatch latch = new CountDownLatch(1); private volatile Throwable exception; private volatile Response updateResponse; private UpdateValueFuture(Location location) { this.location = location; } @Override public boolean cancel(boolean mayInterruptIfRunning) { return false; } @Override public Response get() throws InterruptedException { latch.await(); return updateResponse; } @Override public Response get(long timeout, TimeUnit unit) throws InterruptedException { latch.await(timeout, unit); return updateResponse; } @Override public boolean isCancelled() { return false; } @Override public boolean isDone() { return latch.getCount() != 1; } @Override public void await() throws InterruptedException { latch.await(); } @Override public void await(long timeout, TimeUnit unit) throws InterruptedException { latch.await(timeout, unit); } @Override public boolean isSuccess() { return isDone() && exception == null; } @Override public Throwable cause() { return exception; } private void setResponse(Response response) { this.updateResponse = response; latch.countDown(); notifyListeners(); } private void setException(Throwable t) { this.exception = t; latch.countDown(); notifyListeners(); } @Override public void handle(RiakFuture f) { if (f.isSuccess()) { StoreValue.Response storeResponse; try { storeResponse = f.get(); Response response = new Response.Builder() .withValues(storeResponse.getValues(RiakObject.class)) .withLocation(f.getQueryInfo()) .withUpdated(true) .build(); setResponse(response); } catch (InterruptedException ex) { setException(ex); } } else { setException(f.cause().getCause()); } } @Override public Location getQueryInfo() { return location; } } }