Java tutorial
/* * This file is provided to you 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; import java.io.InputStream; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; import org.apache.commons.httpclient.methods.EntityEnclosingMethod; import org.apache.commons.httpclient.methods.InputStreamRequestEntity; import org.apache.commons.httpclient.util.DateParseException; import org.apache.commons.httpclient.util.DateUtil; import com.basho.riak.client.request.RequestMeta; import com.basho.riak.client.request.RiakWalkSpec; import com.basho.riak.client.response.FetchResponse; import com.basho.riak.client.response.HttpResponse; import com.basho.riak.client.response.RiakIORuntimeException; import com.basho.riak.client.response.RiakResponseRuntimeException; import com.basho.riak.client.response.StoreResponse; import com.basho.riak.client.response.WalkResponse; import com.basho.riak.client.util.Constants; /** * A Riak object. */ public class RiakObject { private RiakClient riak; private String bucket; private String key; private byte[] value; private List<RiakLink> links; private Map<String, String> userMetaData; private String contentType; private String vclock; private String lastmod; private String vtag; private InputStream valueStream; private Long valueStreamLength; /** * Create an empty object. The content type defaults to * application/octet-stream. * * @param riak * Riak instance this object is associated with, which is used by * the convenience methods in this class (e.g. * {@link RiakObject#store()}). * @param bucket * The object's bucket * @param key * The object's key * @param value * The object's value * @param contentType * The object's content type which defaults to * application/octet-stream if null. * @param links * Links to other objects * @param userMetaData * Custom metadata key-value pairs for this object * @param vclock * An opaque vclock assigned by Riak * @param lastmod * The last time this object was modified according to Riak * @param vtag * This object's entity tag assigned by Riak */ public RiakObject(RiakClient riak, String bucket, String key, byte[] value, String contentType, List<RiakLink> links, Map<String, String> userMetaData, String vclock, String lastmod, String vtag) { this.riak = riak; this.bucket = bucket; this.key = key; this.vclock = vclock; this.lastmod = lastmod; this.vtag = vtag; safeSetValue(value); this.contentType = contentType == null ? Constants.CTYPE_OCTET_STREAM : contentType; safeSetLinks(links); safeSetUsermetaData(userMetaData); } public RiakObject(RiakClient riak, String bucket, String key) { this(riak, bucket, key, null, null, null, null, null, null, null); } public RiakObject(RiakClient riak, String bucket, String key, byte[] value) { this(riak, bucket, key, value, null, null, null, null, null, null); } public RiakObject(RiakClient riak, String bucket, String key, byte[] value, String contentType) { this(riak, bucket, key, value, contentType, null, null, null, null, null); } public RiakObject(RiakClient riak, String bucket, String key, byte[] value, String contentType, List<RiakLink> links) { this(riak, bucket, key, value, contentType, links, null, null, null, null); } public RiakObject(RiakClient riak, String bucket, String key, byte[] value, String contentType, List<RiakLink> links, Map<String, String> userMetaData) { this(riak, bucket, key, value, contentType, links, userMetaData, null, null, null); } public RiakObject(String bucket, String key) { this(null, bucket, key, null, null, null, null, null, null, null); } public RiakObject(String bucket, String key, byte[] value) { this(null, bucket, key, value, null, null, null, null, null, null); } public RiakObject(String bucket, String key, byte[] value, String contentType) { this(null, bucket, key, value, contentType, null, null, null, null, null); } public RiakObject(String bucket, String key, byte[] value, String contentType, List<RiakLink> links) { this(null, bucket, key, value, contentType, links, null, null, null, null); } public RiakObject(String bucket, String key, byte[] value, String contentType, List<RiakLink> links, Map<String, String> userMetaData) { this(null, bucket, key, value, contentType, links, userMetaData, null, null, null); } public RiakObject(String bucket, String key, byte[] value, String contentType, List<RiakLink> links, Map<String, String> userMetaData, String vclock, String lastmod, String vtag) { this(null, bucket, key, value, contentType, links, userMetaData, vclock, lastmod, vtag); } /** * A {@link RiakObject} can be loosely attached to the {@link RiakClient} * from which retrieve it was retrieved. Calling convenience methods like * {@link RiakObject#store()} will store this object use that client. */ public RiakClient getRiakClient() { return riak; } public RiakObject setRiakClient(RiakClient client) { riak = client; return this; } /** * Copy the metadata and value from <code>object</code>. The bucket and key * are not copied. * * @param object * The source object to copy from */ public void copyData(RiakObject object) { if (object == null) return; if (object.value != null) { value = object.value.clone(); } else { value = null; } valueStream = object.valueStream; valueStreamLength = object.valueStreamLength; setLinks(object.links); userMetaData = new HashMap<String, String>(); if (object.userMetaData != null) { userMetaData.putAll(object.userMetaData); } contentType = object.contentType; vclock = object.vclock; lastmod = object.lastmod; vtag = object.vtag; } /** * Perform a shallow copy of the object */ void shallowCopy(RiakObject object) { value = object.value; this.links = object.links; userMetaData = object.userMetaData; contentType = object.contentType; vclock = object.vclock; lastmod = object.lastmod; vtag = object.vtag; valueStream = object.valueStream; valueStreamLength = object.valueStreamLength; } /** * Update the object's metadata. This usually happens when Riak returns * updated metadata from a store operation. * * @param response * Response from a store operation containing an updated vclock, * last modified date, and vtag */ public void updateMeta(StoreResponse response) { if (response == null) { vclock = null; lastmod = null; vtag = null; } else { vclock = response.getVclock(); lastmod = response.getLastmod(); vtag = response.getVtag(); } } /** * Update the object's metadata from a fetch or fetchMeta operation * * @param response * Response from a fetch or fetchMeta operation containing a * vclock, last modified date, and vtag */ public void updateMeta(FetchResponse response) { if (response == null || response.getObject() == null) { vclock = null; lastmod = null; vtag = null; } else { vclock = response.getObject().getVclock(); lastmod = response.getObject().getLastmod(); vtag = response.getObject().getVtag(); } } /** * The object's bucket */ public String getBucket() { return bucket; } /** * The object's key */ public String getKey() { return key; } /** * The object's value */ public String getValue() { return (value == null ? null : new String(value)); } public byte[] getValueAsBytes() { return value == null ? value : value.clone(); } public void setValue(String value) { if (value != null) { this.value = value.getBytes(); } else { this.value = null; } } public void setValue(byte[] value) { safeSetValue(value); } /** * * @param value */ private void safeSetValue(final byte[] value) { if (value != null) { this.value = value.clone(); } else { this.value = null; } } /** * Set the object's value as a stream. A value set here is independent of * and has precedent over any value set using setValue(): * {@link RiakObject#writeToHttpMethod(HttpMethod)} will always write the * value from getValueStream() if it is not null. Calling getValue() will * always return values set via setValue(), and calling getValueStream() * will always return the stream set via setValueStream. * * @param in * Input stream representing the object's value * @param len * Length of the InputStream or null if unknown. If null, the * value will be buffered in memory to determine its size before * sending to the server. */ public void setValueStream(InputStream in, Long len) { valueStream = in; valueStreamLength = len; } public void setValueStream(InputStream in) { valueStream = in; } public InputStream getValueStream() { return valueStream; } public void setValueStreamLength(Long len) { valueStreamLength = len; } public Long getValueStreamLength() { return valueStreamLength; } /** * The object's links -- may be empty, but never be null. * * @see {@link RiakObject#addLink()}, {@link RiakObject#removeLink()}, {@link RiakObject#iterator()}, {@link RiakObject#hasLinks()} and , {@link RiakObject#numLinks()} * * @return the list of {@link RiakLink}s for this * RiakObject * @deprecated please use {@link RiakObject#iterableLinks())} to iterate over the * collection of {@link RiakLink}s. Attempting to mutate the * collection will result in UnsupportedOperationException in * future versions. Use {@link RiakObject#addLink()} and {@link RiakObject#removeLink()} instead. * Use {@link RiakObject#hasLinks()}, {@link RiakObject#numLinks()} and {@link RiakObject#hasLink(RiakLink)} * to query state of links. */ @Deprecated public List<RiakLink> getLinks() { return this.links; } /** * Makes a *deep* copy of links. * * Changes made to the original collection and its contents will not be reflected * in this RiakObject's links. Use {@link RiakObject#addLink(RiakLink)}, * {@link RiakObject#removeLink(RiakLink)} and {@link RiakObject#setLinks(List)} to alter the collection. * @param links a List of {@link RiakLink} */ public void setLinks(List<RiakLink> links) { safeSetLinks(links); } private void safeSetLinks(final List<RiakLink> links) { if (links == null) { this.links = new CopyOnWriteArrayList<RiakLink>(); } else { this.links = new CopyOnWriteArrayList<RiakLink>(deepCopy(links)); } } /** * Creates a new RiakLink for each RiakLink in links and adds it to a new List. * * @param links a List of {@link RiakLink}s * @return a deep copy of List. */ private List<RiakLink> deepCopy(List<RiakLink> links) { final ArrayList<RiakLink> copyLinks = new ArrayList<RiakLink>(); for (RiakLink link : links) { copyLinks.add(new RiakLink(link)); } return copyLinks; } /** * Add link to this RiakObject's links. * @param link a {@link RiakLink} to add. * @return this RiakObject. */ public RiakObject addLink(RiakLink link) { if (link != null) { links.add(link); } return this; } /** * Remove a {@link RiakLink} from this RiakObject. * @param link the {@link RiakLink} to remove * @return this RiakObject */ public RiakObject removeLink(final RiakLink link) { this.links.remove(link); return this; } /** * Does this RiakObject have any {@link RiakLink}s? * @return true if there are links, false otherwise */ public boolean hasLinks() { return !links.isEmpty(); } /** * How many {@link RiakLink}s does this RiakObject have? * @return the number of {@link RiakLink}s this object has. */ public int numLinks() { return links.size(); } /** * Checks if the collection of RiakLinks contains the one passed in. * @param riakLink a RiakLink * @return true if the RiakObject's link collection contains riakLink. */ public boolean hasLink(final RiakLink riakLink) { return links.contains(riakLink); } /** * User-specified metadata for the object in the form of key-value pairs -- * may be empty, but never be null. New key-value pairs can be added using * addUsermeta() * * @deprecated Future versions will return an unmodifiable view of the user meta. Please use * {@link RiakObject#addUsermeta(String, String)}, * {@link RiakObject#removeUsermetaItem(String)}, * {@link RiakObject#setUsermeta(Map)}, * {@link RiakObject#hasUsermetaItem(String)}, * {@link RiakObject#hasUsermeta()} and * {@link RiakObject#getUsermetaItem(String)} to mutate and query the User meta collection */ @Deprecated public Map<String, String> getUsermeta() { return userMetaData; } /** * Creates a copy of userMetaData. Changes made to the original collection will not be * reflected in the RiakObject's state. * @param userMetaData */ public void setUsermeta(final Map<String, String> userMetaData) { safeSetUsermetaData(userMetaData); } private void safeSetUsermetaData(final Map<String, String> userMetaData) { if (userMetaData == null) { this.userMetaData = new ConcurrentHashMap<String, String>(); } else { this.userMetaData = new ConcurrentHashMap<String, String>(userMetaData); } } /** * Adds the key, value to the collection of user meta for this object. * @param key * @param value * @return this RiakObject. */ public RiakObject addUsermetaItem(String key, String value) { userMetaData.put(key, value); return this; } /** * @return true if there are any user meta data set on this RiakObject. */ public boolean hasUsermeta() { return !userMetaData.isEmpty(); } /** * @return how many user meta data items this RiakObject has. */ public int numUsermetaItems() { return userMetaData.size(); } /** * @param key * @return */ public boolean hasUsermetaItem(String key) { return userMetaData.containsKey(key); } /** * Get an item of user meta data. * @param key the user meta data item key * @return The value for the given key or null. */ public String getUsermetaItem(String key) { return userMetaData.get(key); } /** * @param key the key of the item to remove */ public void removeUsermetaItem(String key) { userMetaData.remove(key); } public Iterable<String> usermetaKeys() { return userMetaData.keySet(); } /** * The object's content type as a MIME type */ public String getContentType() { return contentType; } public void setContentType(String contentType) { if (contentType != null) { this.contentType = contentType; } else { this.contentType = Constants.CTYPE_OCTET_STREAM; } } /** * The object's opaque vclock assigned by Riak */ public String getVclock() { return vclock; } /** * The modification date of the object determined by Riak */ public String getLastmod() { return lastmod; } /** * Convenience method to get the last modified header parsed into a Date * object. Returns null if header is null, malformed, or cannot be parsed. */ public Date getLastmodAsDate() { try { return DateUtil.parseDate(lastmod); } catch (DateParseException e) { return null; } } /** * An entity tag for the object assigned by Riak */ public String getVtag() { return vtag; } /** * Convenience method for calling * {@link RiakClient#store(RiakObject, RequestMeta)} followed by * {@link RiakObject#updateMeta(StoreResponse)} * * @throws IllegalStateException * if this object was not fetched from a Riak instance, so there * is not associated server to store it with. */ public StoreResponse store(RequestMeta meta) { return store(riak, meta); } public StoreResponse store() { return store(riak, null); } /** * Store this object to a different Riak instance. * * @param riak * Riak instance to store this object to * @param meta * Same as {@link RiakClient#store(RiakObject, RequestMeta)} * @throws IllegalStateException * if this object was not fetched from a Riak instance, so there * is not associated server to store it with. */ public StoreResponse store(RiakClient riak, RequestMeta meta) { if (riak == null) throw new IllegalStateException("Cannot store an object without a RiakClient"); StoreResponse r = riak.store(this, meta); if (r.isSuccess()) { this.updateMeta(r); } return r; } /** * Convenience method for calling {@link RiakClient#fetch(String, String)} * followed by {@link RiakObject#copyData(RiakObject)} * * @param meta * Same as {@link RiakClient#fetch(String, String, RequestMeta)} * @throws IllegalStateException * if this object was not fetched from a Riak instance, so there * is not associated server to refetch it from. */ public FetchResponse fetch(RequestMeta meta) { if (riak == null) throw new IllegalStateException("Cannot fetch an object without a RiakClient"); FetchResponse r = riak.fetch(bucket, key, meta); if (r.getObject() != null) { RiakObject other = r.getObject(); shallowCopy(other); r.setObject(this); } return r; } public FetchResponse fetch() { return fetch(null); } /** * Convenience method for calling * {@link RiakClient#fetchMeta(String, String, RequestMeta)} followed by * {@link RiakObject#updateMeta(FetchResponse)} * * @throws IllegalStateException * if this object was not fetched from a Riak instance, so there * is not associated server to refetch meta from. */ public FetchResponse fetchMeta(RequestMeta meta) { if (riak == null) throw new IllegalStateException("Cannot fetch meta for an object without a RiakClient"); FetchResponse r = riak.fetchMeta(bucket, key, meta); if (r.isSuccess()) { this.updateMeta(r); } return r; } public FetchResponse fetchMeta() { return fetchMeta(null); } /** * Convenience method for calling * {@link RiakClient#delete(String, String, RequestMeta)}. * * @throws IllegalStateException * if this object was not fetched from a Riak instance, so there * is not associated server to delete from. */ public HttpResponse delete(RequestMeta meta) { if (riak == null) throw new IllegalStateException("Cannot delete an object without a RiakClient"); return riak.delete(bucket, key, meta); } public HttpResponse delete() { return delete(null); } /** * Convenience methods for building a link walk specification starting from * this object and calling * {@link RiakClient#walk(String, String, RiakWalkSpec)} * * @param bucket * The bucket to follow object links to * @param tag * The link tags to follow from this object * @param keep * Whether to keep the output from this link walking step. If not * specified, then the output is only kept from the last step. * @return A {@link LinkBuilder} object to continue building the walk query * or to run it. */ public LinkBuilder walk(String bucket, String tag, boolean keep) { return new LinkBuilder().walk(bucket, tag, keep); } public LinkBuilder walk(String bucket, String tag) { return new LinkBuilder().walk(bucket, tag); } public LinkBuilder walk(String bucket, boolean keep) { return new LinkBuilder().walk(bucket, keep); } public LinkBuilder walk(String bucket) { return new LinkBuilder().walk(bucket); } public LinkBuilder walk() { return new LinkBuilder().walk(); } public LinkBuilder walk(boolean keep) { return new LinkBuilder().walk(keep); } /** * Serializes this object to an existing {@link HttpMethod} which can be * sent as an HTTP request. Specifically, sends the object's link, * user-defined metadata and vclock as HTTP headers and the value as the * body. Used by {@link RiakClient} to create PUT requests. */ public void writeToHttpMethod(HttpMethod httpMethod) { // Serialize headers String basePath = getBasePathFromHttpMethod(httpMethod); writeLinks(httpMethod, basePath); for (String name : userMetaData.keySet()) { httpMethod.setRequestHeader(Constants.HDR_USERMETA_REQ_PREFIX + name, userMetaData.get(name)); } if (vclock != null) { httpMethod.setRequestHeader(Constants.HDR_VCLOCK, vclock); } // Serialize body if (httpMethod instanceof EntityEnclosingMethod) { EntityEnclosingMethod entityEnclosingMethod = (EntityEnclosingMethod) httpMethod; // Any value set using setValueAsStream() has precedent over value // set using setValue() if (valueStream != null) { if (valueStreamLength != null && valueStreamLength >= 0) { entityEnclosingMethod.setRequestEntity( new InputStreamRequestEntity(valueStream, valueStreamLength, contentType)); } else { entityEnclosingMethod.setRequestEntity(new InputStreamRequestEntity(valueStream, contentType)); } } else if (value != null) { entityEnclosingMethod.setRequestEntity(new ByteArrayRequestEntity(value, contentType)); } else { entityEnclosingMethod.setRequestEntity(new ByteArrayRequestEntity("".getBytes(), contentType)); } } } private void writeLinks(HttpMethod httpMethod, String basePath) { StringBuilder linkHeader = new StringBuilder(); for (RiakLink link : this.links) { if (linkHeader.length() > 0) { linkHeader.append(", "); } linkHeader.append("<"); linkHeader.append(basePath); linkHeader.append("/"); linkHeader.append(link.getBucket()); linkHeader.append("/"); linkHeader.append(link.getKey()); linkHeader.append(">; "); linkHeader.append(Constants.LINK_TAG); linkHeader.append("=\""); linkHeader.append(link.getTag()); linkHeader.append("\""); // To avoid (MochiWeb) problems with too long headers, flush if // it grows too big: if (linkHeader.length() > 2000) { httpMethod.addRequestHeader(Constants.HDR_LINK, linkHeader.toString()); linkHeader = new StringBuilder(); } } if (linkHeader.length() > 0) { httpMethod.addRequestHeader(Constants.HDR_LINK, linkHeader.toString()); } } String getBasePathFromHttpMethod(HttpMethod httpMethod) { if (httpMethod == null || httpMethod.getPath() == null) return ""; String path = httpMethod.getPath(); int idx = path.length() - 1; // ignore any trailing slash if (path.endsWith("/")) { idx--; } // trim off last two path components idx = path.lastIndexOf('/', idx); idx = path.lastIndexOf('/', idx - 1); if (idx <= 0) return ""; return path.substring(0, idx); } /** * Created by links() as a convenient way to build up link walking queries */ public class LinkBuilder { private RiakWalkSpec walkSpec = new RiakWalkSpec(); public LinkBuilder walk() { walkSpec.addStep(RiakWalkSpec.WILDCARD, RiakWalkSpec.WILDCARD); return this; } public LinkBuilder walk(boolean keep) { walkSpec.addStep(RiakWalkSpec.WILDCARD, RiakWalkSpec.WILDCARD, keep); return this; } public LinkBuilder walk(String bucket) { walkSpec.addStep(bucket, RiakWalkSpec.WILDCARD); return this; } public LinkBuilder walk(String bucket, boolean keep) { walkSpec.addStep(bucket, RiakWalkSpec.WILDCARD, keep); return this; } public LinkBuilder walk(String bucket, String tag) { walkSpec.addStep(bucket, tag); return this; } public LinkBuilder walk(String bucket, String tag, boolean keep) { walkSpec.addStep(bucket, tag, keep); return this; } public String getWalkSpec() { return walkSpec.toString(); } /** * Execute the link walking query by calling * {@link RiakClient#walk(String, String, String, RequestMeta)}. * * @param meta * Extra metadata to attach to the request such as HTTP * headers or query parameters. * @return See * {@link RiakClient#walk(String, String, String, RequestMeta)}. * * @throws RiakIORuntimeException * If an error occurs during communication with the Riak * server. * @throws RiakResponseRuntimeException * If the Riak server returns a malformed response. */ public WalkResponse run(RequestMeta meta) { if (riak == null) throw new IllegalStateException("Cannot perform object link walk without a RiakClient"); return riak.walk(bucket, key, getWalkSpec(), meta); } public WalkResponse run() { return run(null); } } /** * A thread safe, snapshot Iterable view of the state of this RiakObject's {@link RiakLink}s at call time. * Modifications are *NOT* supported. * @return Iterable<RiakLink> for this RiakObject's {@link RiakLink}s */ public Iterable<RiakLink> iterableLinks() { return new Iterable<RiakLink>() { public Iterator<RiakLink> iterator() { return links.iterator(); } }; } }