1.9.5
[gae.git] / java / src / main / com / google / appengine / api / memcache / MemcacheSerialization.java
blob35bfabada1f82712cc609bc9b9f8dc3a0df7c98c
1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com.google.appengine.api.memcache;
5 import static com.google.common.io.BaseEncoding.base64;
6 import static java.nio.charset.StandardCharsets.US_ASCII;
7 import static java.nio.charset.StandardCharsets.UTF_8;
9 import com.google.common.primitives.Bytes;
11 import java.io.ByteArrayInputStream;
12 import java.io.ByteArrayOutputStream;
13 import java.io.IOException;
14 import java.io.ObjectInputStream;
15 import java.io.ObjectOutputStream;
16 import java.io.ObjectStreamClass;
17 import java.io.Serializable;
18 import java.math.BigInteger;
19 import java.security.MessageDigest;
20 import java.security.NoSuchAlgorithmException;
21 import java.util.logging.Level;
22 import java.util.logging.Logger;
24 /**
25 * Static serialization helpers shared by {@link MemcacheServiceImpl} and
26 * {@link com.google.appengine.api.memcache.dev.LocalMemcacheService}.
28 * This class is thread-safe.
32 public class MemcacheSerialization {
34 /**
35 * Values used as flags on the MemcacheService's values.
37 public enum Flag {
38 BYTES,
39 UTF8,
40 OBJECT,
41 INTEGER,
42 LONG,
43 BOOLEAN,
44 BYTE,
45 SHORT;
47 private static final Flag[] VALUES = Flag.values();
49 /**
50 * While the enum is convenient, the implementation wants {@code int}s...
51 * this factory converts {@code int} value to Flag value.
53 public static Flag fromInt(int i) {
54 if (i < 0 || i >= VALUES.length) {
55 throw new IllegalArgumentException();
57 return VALUES[i];
61 /**
62 * Tuple of a serialized byte array value and associated flags to interpret
63 * that value. Used as the return from {@link MemcacheSerialization#serialize}.
65 public static class ValueAndFlags {
66 public final byte[] value;
67 public final Flag flags;
69 private ValueAndFlags(byte[] value, Flag flags) {
70 this.value = value;
71 this.flags = flags;
75 /** Limit determined by memcache backend. */
76 static final int MAX_KEY_BYTE_COUNT = 250;
78 private static final byte FALSE_VALUE = '0';
79 private static final byte TRUE_VALUE = '1';
80 private static final String MYCLASSNAME = MemcacheSerialization.class.getName();
82 /**
83 * The SHA1 checksum engine, cached for reuse. We did test the hashing time
84 * was negligible (17us/kb, linear); we don't "need" crypto-secure, but it's
85 * a good way to minimize collisions.
87 private static final MessageDigest SHA1;
89 static {
90 try {
91 SHA1 = MessageDigest.getInstance("SHA-1");
92 } catch (NoSuchAlgorithmException ex) {
93 Logger.getLogger(MYCLASSNAME).log(Level.SEVERE,
94 "Can't load SHA-1 MessageDigest!", ex);
95 throw new MemcacheServiceException("No SHA-1 algorithm, cannot hash keys for memcache", ex);
99 public static final String USE_THREAD_CONTEXT_CLASSLOADER_PROPERTY =
100 "appengine.api.memcache.useThreadContextClassLoader";
102 private static class UseThreadContextClassLoaderHolder {
103 static final boolean INSTANCE =
104 Boolean.getBoolean(USE_THREAD_CONTEXT_CLASSLOADER_PROPERTY);
107 private MemcacheSerialization() {
111 * Deserialize the object, according to its flags. This would have private
112 * visibility, but is also used by LocalMemcacheService for the increment
113 * operation.
115 * @param value
116 * @param flags
117 * @return the Object originally stored
118 * @throws ClassNotFoundException if the object can't be re-instantiated due
119 * to being an unlocatable type
120 * @throws IOException if the object can't be re-instantiated for some other
121 * reason
123 public static Object deserialize(byte[] value, int flags)
124 throws ClassNotFoundException, IOException {
125 Flag flagval = Flag.fromInt(flags);
127 switch (flagval) {
128 case BYTES:
129 return value;
131 case BOOLEAN:
132 if (value.length != 1) {
133 throw new InvalidValueException("Cannot deserialize Boolean: bad length", null);
135 switch (value[0]) {
136 case TRUE_VALUE:
137 return Boolean.TRUE;
138 case FALSE_VALUE:
139 return Boolean.FALSE;
141 throw new InvalidValueException("Cannot deserialize Boolean: bad contents", null);
143 case BYTE:
144 case SHORT:
145 case INTEGER:
146 case LONG:
147 long val = new BigInteger(new String(value, US_ASCII)).longValue();
148 switch (flagval) {
149 case BYTE:
150 return (byte) val;
151 case SHORT:
152 return (short) val;
153 case INTEGER:
154 return (int) val;
155 case LONG:
156 return val;
157 default:
158 throw new InvalidValueException("Cannot deserialize number: bad contents", null);
161 case UTF8:
162 return new String(value, UTF_8);
164 case OBJECT:
165 if (value.length == 0) {
166 return null;
168 ByteArrayInputStream baos = new ByteArrayInputStream(value);
170 ObjectInputStream objIn = null;
171 if (UseThreadContextClassLoaderHolder.INSTANCE) {
172 objIn = new ObjectInputStream(baos) {
173 @Override
174 protected Class<?> resolveClass(ObjectStreamClass objectStreamClass)
175 throws ClassNotFoundException, IOException {
176 ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader();
177 if (threadClassLoader == null) {
178 return super.resolveClass(objectStreamClass);
181 try {
182 return Class.forName(objectStreamClass.getName(), false, threadClassLoader);
183 } catch (ClassNotFoundException ex) {
184 return super.resolveClass(objectStreamClass);
188 } else {
189 objIn = new ObjectInputStream(baos);
192 Object response = objIn.readObject();
193 objIn.close();
194 return response;
196 default:
197 assert false;
199 return null;
202 private static boolean noEmbeddedNulls(byte[] key) {
203 return !Bytes.contains(key, (byte) 0);
206 /** TODO(user) remove all stuttering code after transitional release. */
207 static final byte[] STUTTER_PREFIX = "|R172eYPQ9M|".getBytes();
208 private static final int HASH_START = STUTTER_PREFIX.length;
209 private static final int HASH_LEN = 28;
210 static final int RAW_START = HASH_START + HASH_LEN + 1;
213 * Converts the user's key Object into a byte[] for the MemcacheGetRequest.
214 * Because the underlying service has a length limit, we actually use the
215 * SHA1 hash of the serialized object as its key if it's not a basic type.
216 * For the basic types (that is, {@code String}, {@code Boolean}, and the
217 * fixed-point numbers), we use a human-readable representation.
219 * @param key
220 * @return hash result. For the key {@code null}, the hash is also
221 * {@code null}.
223 public static byte[] makePbKey(Object key) throws IOException {
224 if (key == null) {
225 return new byte[0];
227 if (key instanceof String && ((String) key).length() <= MAX_KEY_BYTE_COUNT - 2) {
228 return ("\"" + key + "\"").getBytes(UTF_8);
229 } else if (key instanceof byte[]) {
230 byte[] bytes = (byte[]) key;
232 if (bytes.length <= MAX_KEY_BYTE_COUNT - RAW_START && noEmbeddedNulls(bytes)) {
233 int bytesLen = bytes.length;
234 byte[] stutteredBytes = new byte[RAW_START + bytesLen];
235 System.arraycopy(STUTTER_PREFIX, 0, stutteredBytes, 0, STUTTER_PREFIX.length);
236 System.arraycopy(hash(key), 0, stutteredBytes, HASH_START, HASH_LEN);
237 stutteredBytes[RAW_START - 1] = (byte) '|';
238 System.arraycopy(bytes, 0, stutteredBytes, RAW_START, bytesLen);
239 return stutteredBytes;
242 } else if (key instanceof Long || key instanceof Integer
243 || key instanceof Short || key instanceof Byte) {
244 return (key.getClass().getName() + ":" + key).getBytes(UTF_8);
246 } else if (key instanceof Boolean) {
247 return (((Boolean) key) ? "true" : "false").getBytes(UTF_8);
251 return hash(key);
254 private static final byte[] hash(Object key) throws IOException {
255 ValueAndFlags vaf = serialize(key);
256 byte sha1hash[];
257 synchronized (SHA1) {
258 SHA1.update(vaf.value);
259 sha1hash = SHA1.digest();
261 return base64().encode(sha1hash).getBytes(UTF_8);
265 * @param value
266 * @return the ValueAndFlags containing a serialized representation of the
267 * Object and the flags to hint deserialization.
268 * @throws IOException for serialization errors, normally due to a
269 * non-serializable object type
271 public static ValueAndFlags serialize(Object value)
272 throws IOException {
273 Flag flags;
274 byte[] bytes;
276 if (value == null) {
277 bytes = new byte[0];
278 flags = Flag.OBJECT;
280 } else if (value instanceof byte[]) {
281 flags = Flag.BYTES;
282 bytes = (byte[]) value;
284 } else if (value instanceof Boolean) {
285 flags = Flag.BOOLEAN;
286 bytes = new byte[1];
287 bytes[0] = ((Boolean) value) ? TRUE_VALUE : FALSE_VALUE;
289 } else if (value instanceof Integer || value instanceof Long
290 || value instanceof Byte || value instanceof Short) {
291 bytes = value.toString().getBytes(US_ASCII);
292 if (value instanceof Integer) {
293 flags = Flag.INTEGER;
294 } else if (value instanceof Long) {
295 flags = Flag.LONG;
296 } else if (value instanceof Byte) {
297 flags = Flag.BYTE;
298 } else {
299 flags = Flag.SHORT;
302 } else if (value instanceof String) {
303 flags = Flag.UTF8;
304 bytes = ((String) value).getBytes(UTF_8);
306 } else if (value instanceof Serializable) {
307 flags = Flag.OBJECT;
308 ByteArrayOutputStream baos = new ByteArrayOutputStream();
309 ObjectOutputStream objOut = new ObjectOutputStream(baos);
310 objOut.writeObject(value);
311 objOut.close();
312 bytes = baos.toByteArray();
314 } else {
315 throw new IllegalArgumentException("can't accept " + value.getClass()
316 + " as a memcache entity");
318 return new ValueAndFlags(bytes, flags);