/** * (c) Copyright 2013 WibiData, Inc. * * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * 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 org.kiji.mapreduce.kvstore.lib; import java.io.IOException; import java.util.HashMap; import java.util.Map; import com.google.common.base.Preconditions; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.SerializationException; import org.apache.commons.lang.SerializationUtils; import org.kiji.annotations.ApiAudience; import org.kiji.annotations.ApiStability; import org.kiji.mapreduce.kvstore.KeyValueStore; import org.kiji.mapreduce.kvstore.KeyValueStoreReader; import org.kiji.mapreduce.kvstore.framework.KeyValueStoreConfiguration; /** * KeyValueStore backed entirely by an in-memory map. * * <p>This key-value store provides an encapsulated way to pass information to * all tasks in a KijiMR job via the key-value store system. This can be * useful for things like passing contextual information to Producers that is * too ephemeral to store in a KijiTable or file in HDFS (time of day, for * example.) A typical usage case for this class is to register an * UnconfiguredKeyValueStore in the Producer and mask it with an instance of * InMemoryMapKeyValueStore registered with the job builder.</p> * * <p>Since the entire contents of the map will be stored in the job's * Configuration, copied to all tasks, and held in memory it is not * recommended that this class be used for very large amounts of data.</p> * * <p>The key and value types of this key value store must be * {@link java.io.Serializable}. Ideally, they should be Java primitive wrappers * (e.g. Integer), Strings, or other small objects.</p> * * <p>To create a InMemoryMapKeyValueStore you should use {@link #builder()} to * get a builder object. * This class has one method, * {@link InMemoryMapKeyValueStore.Builder#withMap(Map)} which takes as input the * Map<K, V> to be used to back the InMemoryMapKeyValueStore.</p> * * <p>The contents of this Map are copied when * {@link InMemoryMapKeyValueStore.Builder#build()} is called; modifications to * the map between the calls to {@link Builder#withMap(Map)} and * {@link InMemoryMapKeyValueStore.Builder#build()} will be reflected in the * KeyValueStore. Modifications made after the call to build() will not. Thus, * all data should be stored in the Map passed to {@link Builder#withMap(Map)} * before the call to {@link InMemoryMapKeyValueStore.Builder#build()}, but it * can either be inserted before or between these calls.</p> * * <p>As a shortcut, you may use the static method * {@link #fromMap(Map)} to immediately generate a key * value store with the contents of the Map argument. Note that the Map is copied * immediately and no modifications made to it after the call to get() will be * reflected in the InMemoryMapKeyValueStore.</p> * * <p>In either case, the copy is a <em>shallow</em> copy, i.e. references to the * Objects used as keys and values will be used rather than creating new * Objects. As a result, you should be cautious about mutating the Objects used * as keys or values, as their state is not locked down until the key value store is * serialized.</p> * * @param <K> The type of the key field. Should implement {@link java.io.Serializable}. * @param <V> The type of the value field. Should implement {@link java.io.Serializable}. */ @ApiAudience.Public @ApiStability.Experimental public final class InMemoryMapKeyValueStore<K, V> implements KeyValueStore<K, V> { private static final String CONF_MAP = "map"; /** * The map written to or read from the Configuration object. This must be * HashMap so that it implements serializable. */ private HashMap<K, V> mMap; /** true if the user has called open() on this object. */ private boolean mOpened; /** * A Builder-pattern class that configures and creates new InMemoryMapKeyValueStore * instances. Use this to specify the Map<String, String> for this KeyValueStore * and call build() to return a new instance. */ @ApiAudience.Public @ApiStability.Experimental public static final class Builder<K, V> { private Map<K, V> mMap; /** * Private constructor. Use InMemoryMapKeyValueStore.builder() to get a builder instance. */ private Builder() { } /** * Sets the map containing the keys and values. Its contents will be copied at the call * to build(). This is a shallow copy: Updates to the map will not be reflected after * build(), but mutations to keys and values will until the InMemoryMapKeyValueStore is * serialized into a Configuration. * * @param map the map containing the data backing this key value store. * @return this builder instance. */ public Builder withMap(Map<K, V> map) { mMap = map; return this; } /** * Build a new InMemoryMapKeyValueStore instance. * * @return an initialized KeyValueStore. */ public InMemoryMapKeyValueStore<K, V> build() { Preconditions.checkArgument(mMap != null, "Must specify a non-null map."); return new InMemoryMapKeyValueStore(this); } } /** * Creates a new InMemoryMapKeyValueStore.Builder instance that can be * used to make an InMemoryMapKeyValueStore. * * @return the builder. */ public static Builder builder() { return new Builder(); } /** * Reflection-only constructor. Used only for reflection. You should * create InMemoryMapKeyValueStore instances by using a builder or * the factory method {@link InMemoryMapKeyValueStore#fromMap(Map)}. */ public InMemoryMapKeyValueStore() { mMap = new HashMap<K, V>(); } /** * Constructor that up this KeyValueStore using a builder. * * @param builder the builder instance to read configuration from. */ private InMemoryMapKeyValueStore(Builder builder) { mMap = new HashMap<K, V>(builder.mMap); } /** * Factory method that returns an InMemoryMapKeyValueStore instance. * This makes a shallow copy of the map. Future modifications to the map * passed into this call will <em>not</em> be reflected in the KeyValueStore, * but changes to the key or value Objects will be. * * @param <K> The type of the key field. Should implement {@link java.io.Serializable}. * @param <V> The type of the value field. Should implement {@link java.io.Serializable}. * @param map the map containing the data for the InMemoryMapKeyValueStore. * @return An InMemoryMapKeyValueStore instance. */ public static <K, V> InMemoryMapKeyValueStore<K, V> fromMap(Map<K, V> map) { return builder().withMap(map).build(); } @Override public void storeToConf(KeyValueStoreConfiguration conf) throws IOException { try { conf.set(CONF_MAP, Base64.encodeBase64String(SerializationUtils.serialize(mMap))); } catch (SerializationException se) { // Throw a more helpful error message for the user. throw new SerializationException( "InMemoryKeyValueStore requires that its keys and values are Serializable", se); } } @SuppressWarnings("unchecked") @Override public void initFromConf(KeyValueStoreConfiguration conf) throws IOException { Preconditions.checkState(!mOpened, "Cannot reinitialize; already opened."); mMap = (HashMap<K, V>) SerializationUtils .deserialize(Base64.decodeBase64(conf.get(CONF_MAP))); } @Override public KeyValueStoreReader<K, V> open() throws IOException { mOpened = true; return new Reader(); } /** * A very simple KVStore Reader. It simply wraps access to the mMap of the * outer class's mMap. Because of this, its {@link #close()} and * {@link #isOpen()} methods are somewhat inane. */ @ApiAudience.Private private final class Reader implements KeyValueStoreReader<K, V> { /** Private constructor. */ private Reader() { } @Override public void close() throws IOException { } @Override public V get(K key) throws IOException { return mMap.get(key); } @Override public boolean containsKey(K key) throws IOException { return mMap.containsKey(key); } @Override public boolean isOpen() { return true; } } }