From 79cebfadf8703afe9bf28786bc4df1af348c876e Mon Sep 17 00:00:00 2001 From: "Brian S. O'Neill" Date: Thu, 5 Oct 2006 03:54:20 +0000 Subject: Moved raw package out of spi package. --- .../amazon/carbonado/raw/CustomStorableCodec.java | 332 ++++ .../carbonado/raw/CustomStorableCodecFactory.java | 70 + .../java/com/amazon/carbonado/raw/DataDecoder.java | 567 ++++++ .../java/com/amazon/carbonado/raw/DataEncoder.java | 595 ++++++ .../carbonado/raw/GenericEncodingStrategy.java | 1963 ++++++++++++++++++++ .../carbonado/raw/GenericInstanceFactory.java | 36 + .../amazon/carbonado/raw/GenericPropertyInfo.java | 60 + .../amazon/carbonado/raw/GenericStorableCodec.java | 813 ++++++++ .../carbonado/raw/GenericStorableCodecFactory.java | 76 + .../java/com/amazon/carbonado/raw/KeyDecoder.java | 646 +++++++ .../java/com/amazon/carbonado/raw/KeyEncoder.java | 741 ++++++++ .../amazon/carbonado/raw/LayoutPropertyInfo.java | 86 + .../java/com/amazon/carbonado/raw/RawCursor.java | 743 ++++++++ .../amazon/carbonado/raw/RawStorableGenerator.java | 355 ++++ .../java/com/amazon/carbonado/raw/RawSupport.java | 97 + .../java/com/amazon/carbonado/raw/RawUtil.java | 66 + .../com/amazon/carbonado/raw/StorableCodec.java | 118 ++ .../amazon/carbonado/raw/StorableCodecFactory.java | 54 + .../amazon/carbonado/raw/StorablePropertyInfo.java | 132 ++ .../com/amazon/carbonado/raw/package-info.java | 23 + 20 files changed, 7573 insertions(+) create mode 100644 src/main/java/com/amazon/carbonado/raw/CustomStorableCodec.java create mode 100644 src/main/java/com/amazon/carbonado/raw/CustomStorableCodecFactory.java create mode 100644 src/main/java/com/amazon/carbonado/raw/DataDecoder.java create mode 100644 src/main/java/com/amazon/carbonado/raw/DataEncoder.java create mode 100644 src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java create mode 100644 src/main/java/com/amazon/carbonado/raw/GenericInstanceFactory.java create mode 100644 src/main/java/com/amazon/carbonado/raw/GenericPropertyInfo.java create mode 100644 src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java create mode 100644 src/main/java/com/amazon/carbonado/raw/GenericStorableCodecFactory.java create mode 100644 src/main/java/com/amazon/carbonado/raw/KeyDecoder.java create mode 100644 src/main/java/com/amazon/carbonado/raw/KeyEncoder.java create mode 100644 src/main/java/com/amazon/carbonado/raw/LayoutPropertyInfo.java create mode 100644 src/main/java/com/amazon/carbonado/raw/RawCursor.java create mode 100644 src/main/java/com/amazon/carbonado/raw/RawStorableGenerator.java create mode 100644 src/main/java/com/amazon/carbonado/raw/RawSupport.java create mode 100644 src/main/java/com/amazon/carbonado/raw/RawUtil.java create mode 100644 src/main/java/com/amazon/carbonado/raw/StorableCodec.java create mode 100644 src/main/java/com/amazon/carbonado/raw/StorableCodecFactory.java create mode 100644 src/main/java/com/amazon/carbonado/raw/StorablePropertyInfo.java create mode 100644 src/main/java/com/amazon/carbonado/raw/package-info.java (limited to 'src/main/java/com/amazon/carbonado/raw') diff --git a/src/main/java/com/amazon/carbonado/raw/CustomStorableCodec.java b/src/main/java/com/amazon/carbonado/raw/CustomStorableCodec.java new file mode 100644 index 0000000..e319e7c --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/CustomStorableCodec.java @@ -0,0 +1,332 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import java.util.Map; + +import org.cojen.classfile.ClassFile; +import org.cojen.classfile.CodeBuilder; +import org.cojen.classfile.MethodInfo; +import org.cojen.classfile.Modifiers; +import org.cojen.classfile.TypeDesc; +import org.cojen.util.ClassInjector; +import org.cojen.util.WeakIdentityMap; + +import com.amazon.carbonado.CorruptEncodingException; +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.SupportException; + +import com.amazon.carbonado.info.Direction; +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.info.StorableIntrospector; +import com.amazon.carbonado.info.StorableProperty; + +import com.amazon.carbonado.util.QuickConstructorGenerator; + +/** + * Allows codecs to be defined for storables that have a custom encoding. + * + * @author Brian S O'Neill + * @see CustomStorableCodecFactory + */ +public abstract class CustomStorableCodec implements StorableCodec { + // Generated storable instances maintain a reference to user-defined + // concrete subclass of this class. + private static final String CUSTOM_STORABLE_CODEC_FIELD_NAME = "customStorableCodec$"; + + @SuppressWarnings("unchecked") + private static Map> cCache = + new WeakIdentityMap(); + + /** + * Returns a storable implementation that calls into CustomStorableCodec + * implementation for encoding and decoding. + */ + @SuppressWarnings("unchecked") + static Class + getStorableClass(Class type, boolean isMaster) + throws SupportException + { + synchronized (cCache) { + Class storableClass; + + RawStorableGenerator.Flavors flavors = + (RawStorableGenerator.Flavors) cCache.get(type); + + if (flavors == null) { + flavors = new RawStorableGenerator.Flavors(); + cCache.put(type, flavors); + } else if ((storableClass = flavors.getClass(isMaster)) != null) { + return storableClass; + } + + storableClass = generateStorableClass(type, isMaster); + flavors.setClass(storableClass, isMaster); + + return storableClass; + } + } + + @SuppressWarnings("unchecked") + private static Class + generateStorableClass(Class type, boolean isMaster) + throws SupportException + { + final Class abstractClass = + RawStorableGenerator.getAbstractClass(type, isMaster); + + ClassInjector ci = ClassInjector.create + (type.getName(), abstractClass.getClassLoader()); + + ClassFile cf = new ClassFile(ci.getClassName(), abstractClass); + cf.markSynthetic(); + cf.setSourceFile(CustomStorableCodec.class.getName()); + cf.setTarget("1.5"); + + // Declare some types. + final TypeDesc rawSupportType = TypeDesc.forClass(RawSupport.class); + final TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + final TypeDesc[] byteArrayParam = {byteArrayType}; + final TypeDesc customStorableCodecType = TypeDesc.forClass(CustomStorableCodec.class); + + // Add field for saving reference to concrete CustomStorableCodec. + cf.addField(Modifiers.PRIVATE.toFinal(true), + CUSTOM_STORABLE_CODEC_FIELD_NAME, customStorableCodecType); + + // Add constructor that accepts a RawSupport and a CustomStorableCodec. + { + TypeDesc[] params = {rawSupportType, customStorableCodecType}; + MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + + // Call super class constructor. + b.loadThis(); + b.loadLocal(b.getParameter(0)); + params = new TypeDesc[] {rawSupportType}; + b.invokeSuperConstructor(params); + + // Set private reference to customStorableCodec. + b.loadThis(); + b.loadLocal(b.getParameter(1)); + b.storeField(CUSTOM_STORABLE_CODEC_FIELD_NAME, customStorableCodecType); + + b.returnVoid(); + } + + // Add constructor that accepts a RawSupport, an encoded key, an + // encoded data, and a CustomStorableCodec. + { + TypeDesc[] params = {rawSupportType, byteArrayType, byteArrayType, + customStorableCodecType}; + MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + + // Set private reference to customStorableCodec before calling + // super constructor. This is necessary because super class + // constructor will call our decode methods, which need the + // customStorableCodec. This trick is not allowed in Java, but the + // virtual machine verifier allows it. + b.loadThis(); + b.loadLocal(b.getParameter(3)); + b.storeField(CUSTOM_STORABLE_CODEC_FIELD_NAME, customStorableCodecType); + + // Now call super class constructor. + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.loadLocal(b.getParameter(1)); + b.loadLocal(b.getParameter(2)); + params = new TypeDesc[] {rawSupportType, byteArrayType, byteArrayType}; + b.invokeSuperConstructor(params); + + b.returnVoid(); + } + + // Implement protected abstract methods inherited from parent class. + + // byte[] encodeKey() + { + // Encode the primary key into a byte array that supports correct + // ordering. No special key comparator is needed. + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.ENCODE_KEY_METHOD_NAME, + byteArrayType, null); + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadField(CUSTOM_STORABLE_CODEC_FIELD_NAME, customStorableCodecType); + TypeDesc[] params = {TypeDesc.forClass(Storable.class)}; + b.loadThis(); + b.invokeVirtual(customStorableCodecType, "encodePrimaryKey", byteArrayType, params); + b.returnValue(byteArrayType); + } + + // byte[] encodeData() + { + // Encoding non-primary key data properties. + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.ENCODE_DATA_METHOD_NAME, + byteArrayType, null); + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadField(CUSTOM_STORABLE_CODEC_FIELD_NAME, customStorableCodecType); + TypeDesc[] params = {TypeDesc.forClass(Storable.class)}; + b.loadThis(); + b.invokeVirtual(customStorableCodecType, "encodeData", byteArrayType, params); + b.returnValue(byteArrayType); + } + + // void decodeKey(byte[]) + { + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.DECODE_KEY_METHOD_NAME, + null, byteArrayParam); + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadField(CUSTOM_STORABLE_CODEC_FIELD_NAME, customStorableCodecType); + TypeDesc[] params = {TypeDesc.forClass(Storable.class), byteArrayType}; + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.invokeVirtual(customStorableCodecType, "decodePrimaryKey", null, params); + b.returnVoid(); + } + + // void decodeData(byte[]) + { + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.DECODE_DATA_METHOD_NAME, + null, byteArrayParam); + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadField(CUSTOM_STORABLE_CODEC_FIELD_NAME, customStorableCodecType); + TypeDesc[] params = {TypeDesc.forClass(Storable.class), byteArrayType}; + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.invokeVirtual(customStorableCodecType, "decodeData", null, params); + b.returnVoid(); + } + + return ci.defineClass(cf); + } + + private final Class mType; + private final int mPkPropertyCount; + private final InstanceFactory mInstanceFactory; + + public interface InstanceFactory { + Storable instantiate(RawSupport support, CustomStorableCodec codec); + + Storable instantiate(RawSupport support, byte[] key, byte[] value, + CustomStorableCodec codec) + throws FetchException; + } + + /** + * @param isMaster when true, version properties and sequences are managed + * @throws SupportException if Storable is not supported + */ + public CustomStorableCodec(Class type, boolean isMaster) throws SupportException { + mType = type; + mPkPropertyCount = getPrimaryKeyIndex().getPropertyCount(); + Class storableClass = getStorableClass(type, isMaster); + mInstanceFactory = QuickConstructorGenerator + .getInstance(storableClass, InstanceFactory.class); + } + + public Class getStorableType() { + return mType; + } + + @SuppressWarnings("unchecked") + public S instantiate(RawSupport support) { + return (S) mInstanceFactory.instantiate(support, this); + } + + @SuppressWarnings("unchecked") + public S instantiate(RawSupport support, byte[] key, byte[] value) + throws FetchException + { + return (S) mInstanceFactory.instantiate(support, key, value, this); + } + + public byte[] encodePrimaryKey(S storable) { + return encodePrimaryKey(storable, 0, mPkPropertyCount); + } + + public byte[] encodePrimaryKey(Object[] values) { + return encodePrimaryKey(values, 0, mPkPropertyCount); + } + + /** + * Convenient access to all the storable properties. + */ + public Map> getAllProperties() { + return StorableIntrospector.examine(getStorableType()).getAllProperties(); + } + + /** + * Convenient way to define the clustered primary key index + * descriptor. Direction can be specified by prefixing the property name + * with a '+' or '-'. If unspecified, direction is assumed to be ascending. + */ + @SuppressWarnings("unchecked") + public StorableIndex buildPkIndex(String... propertyNames) { + Map> map = getAllProperties(); + int length = propertyNames.length; + StorableProperty[] properties = new StorableProperty[length]; + Direction[] directions = new Direction[length]; + for (int i=0; i(properties, directions, true, true); + } + + /** + * Decode the primary key into properties of the storable. + */ + public abstract void decodePrimaryKey(S storable, byte[] bytes) + throws CorruptEncodingException; + + /** + * Encode all properties of the storable excluding the primary key. + */ + public abstract byte[] encodeData(S storable); + + /** + * Decode the data into properties of the storable. + */ + public abstract void decodeData(S storable, byte[] bytes) + throws CorruptEncodingException; +} diff --git a/src/main/java/com/amazon/carbonado/raw/CustomStorableCodecFactory.java b/src/main/java/com/amazon/carbonado/raw/CustomStorableCodecFactory.java new file mode 100644 index 0000000..bfba733 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/CustomStorableCodecFactory.java @@ -0,0 +1,70 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.SupportException; + +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.layout.Layout; + +/** + * Factory for custom storable codecs. + * + * @author Brian S O'Neill + */ +public abstract class CustomStorableCodecFactory implements StorableCodecFactory { + public CustomStorableCodecFactory() { + } + + /** + * Returns null to let repository decide what the name should be. + */ + public String getStorageName(Class type) throws SupportException { + return null; + } + + /** + * @param type type of storable to create codec for + * @param pkIndex ignored + * @param isMaster when true, version properties and sequences are managed + * @param layout when non-null, attempt to encode a storable layout + * generation value in each storable + * @throws SupportException if type is not supported + */ + public CustomStorableCodec createCodec(Class type, + StorableIndex pkIndex, + boolean isMaster, + Layout layout) + throws SupportException + { + return createCodec(type, isMaster, layout); + } + + /** + * @param type type of storable to create codec for + * @param isMaster when true, version properties and sequences are managed + * @param layout when non-null, attempt to encode a storable layout + * generation value in each storable + * @throws SupportException if type is not supported + */ + protected abstract CustomStorableCodec + createCodec(Class type, boolean isMaster, Layout layout) + throws SupportException; +} diff --git a/src/main/java/com/amazon/carbonado/raw/DataDecoder.java b/src/main/java/com/amazon/carbonado/raw/DataDecoder.java new file mode 100644 index 0000000..485ce82 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/DataDecoder.java @@ -0,0 +1,567 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import com.amazon.carbonado.CorruptEncodingException; + +import static com.amazon.carbonado.raw.DataEncoder.*; + +/** + * A very low-level class that decodes key components encoded by methods of + * {@link DataEncoder}. + * + * @author Brian S O'Neill + */ +public class DataDecoder { + static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + /** + * Decodes a signed integer from exactly 4 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed integer value + */ + public static int decodeInt(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int value = (src[srcOffset] << 24) | ((src[srcOffset + 1] & 0xff) << 16) | + ((src[srcOffset + 2] & 0xff) << 8) | (src[srcOffset + 3] & 0xff); + return value ^ 0x80000000; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed Integer object from exactly 1 or 5 bytes. If null is + * returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Integer object or null + */ + public static Integer decodeIntegerObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeInt(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed long from exactly 8 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed long value + */ + public static long decodeLong(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return + (((long)(((src[srcOffset ] ) << 24) | + ((src[srcOffset + 1] & 0xff) << 16) | + ((src[srcOffset + 2] & 0xff) << 8 ) | + ((src[srcOffset + 3] & 0xff) )) ^ 0x80000000 ) << 32) | + (((long)(((src[srcOffset + 4] ) << 24) | + ((src[srcOffset + 5] & 0xff) << 16) | + ((src[srcOffset + 6] & 0xff) << 8 ) | + ((src[srcOffset + 7] & 0xff) )) & 0xffffffffL) ); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed Long object from exactly 1 or 9 bytes. If null is + * returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Long object or null + */ + public static Long decodeLongObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeLong(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed byte from exactly 1 byte. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed byte value + */ + public static byte decodeByte(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return (byte)(src[srcOffset] ^ 0x80); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed Byte object from exactly 1 or 2 bytes. If null is + * returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Byte object or null + */ + public static Byte decodeByteObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeByte(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed short from exactly 2 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed short value + */ + public static short decodeShort(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return (short)(((src[srcOffset] << 8) | (src[srcOffset + 1] & 0xff)) ^ 0x8000); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed Short object from exactly 1 or 3 bytes. If null is + * returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Short object or null + */ + public static Short decodeShortObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeShort(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a char from exactly 2 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return char value + */ + public static char decodeChar(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return (char)((src[srcOffset] << 8) | (src[srcOffset + 1] & 0xff)); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a Character object from exactly 1 or 3 bytes. If null is + * returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Character object or null + */ + public static Character decodeCharacterObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeChar(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a boolean from exactly 1 byte. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return boolean value + */ + public static boolean decodeBoolean(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return src[srcOffset] == (byte)128; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a Boolean object from exactly 1 byte. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Boolean object or null + */ + public static Boolean decodeBooleanObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + switch (src[srcOffset]) { + case NULL_BYTE_LOW: case NULL_BYTE_HIGH: + return null; + case (byte)128: + return Boolean.TRUE; + default: + return Boolean.FALSE; + } + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a float from exactly 4 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return float value + */ + public static float decodeFloat(byte[] src, int srcOffset) + throws CorruptEncodingException + { + int bits = decodeFloatBits(src, srcOffset); + bits ^= (bits < 0) ? 0x80000000 : 0xffffffff; + return Float.intBitsToFloat(bits); + } + + /** + * Decodes a Float object from exactly 4 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Float object or null + */ + public static Float decodeFloatObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + int bits = decodeFloatBits(src, srcOffset); + bits ^= (bits < 0) ? 0x80000000 : 0xffffffff; + return bits == 0x7fffffff ? null : Float.intBitsToFloat(bits); + } + + protected static int decodeFloatBits(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return (src[srcOffset] << 24) | ((src[srcOffset + 1] & 0xff) << 16) | + ((src[srcOffset + 2] & 0xff) << 8) | (src[srcOffset + 3] & 0xff); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a double from exactly 8 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return double value + */ + public static double decodeDouble(byte[] src, int srcOffset) + throws CorruptEncodingException + { + long bits = decodeDoubleBits(src, srcOffset); + bits ^= (bits < 0) ? 0x8000000000000000L : 0xffffffffffffffffL; + return Double.longBitsToDouble(bits); + } + + /** + * Decodes a Double object from exactly 8 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Double object or null + */ + public static Double decodeDoubleObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + long bits = decodeDoubleBits(src, srcOffset); + bits ^= (bits < 0) ? 0x8000000000000000L : 0xffffffffffffffffL; + return bits == 0x7fffffffffffffffL ? null : Double.longBitsToDouble(bits); + } + + protected static long decodeDoubleBits(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return + (((long)(((src[srcOffset ] ) << 24) | + ((src[srcOffset + 1] & 0xff) << 16) | + ((src[srcOffset + 2] & 0xff) << 8 ) | + ((src[srcOffset + 3] & 0xff) )) ) << 32) | + (((long)(((src[srcOffset + 4] ) << 24) | + ((src[srcOffset + 5] & 0xff) << 16) | + ((src[srcOffset + 6] & 0xff) << 8 ) | + ((src[srcOffset + 7] & 0xff) )) & 0xffffffffL)); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes the given byte array. + * + * @param src source of encoded data + * @param srcOffset offset into encoded data + * @param valueRef decoded byte array is stored in element 0, which may be null + * @return amount of bytes read from source + * @throws CorruptEncodingException if source data is corrupt + */ + public static int decode(byte[] src, int srcOffset, byte[][] valueRef) + throws CorruptEncodingException + { + try { + final int originalOffset = srcOffset; + + int b = src[srcOffset++] & 0xff; + if (b >= 0xf8) { + valueRef[0] = null; + return 1; + } + + int valueLength; + if (b <= 0x7f) { + valueLength = b; + } else if (b <= 0xbf) { + valueLength = ((b & 0x3f) << 8) | (src[srcOffset++] & 0xff); + } else if (b <= 0xdf) { + valueLength = ((b & 0x1f) << 16) | ((src[srcOffset++] & 0xff) << 8) | + (src[srcOffset++] & 0xff); + } else if (b <= 0xef) { + valueLength = ((b & 0x0f) << 24) | ((src[srcOffset++] & 0xff) << 16) | + ((src[srcOffset++] & 0xff) << 8) | (src[srcOffset++] & 0xff); + } else { + valueLength = ((b & 0x07) << 24) | ((src[srcOffset++] & 0xff) << 16) | + ((src[srcOffset++] & 0xff) << 8) | (src[srcOffset++] & 0xff); + } + + if (valueLength == 0) { + valueRef[0] = EMPTY_BYTE_ARRAY; + } else { + byte[] value = new byte[valueLength]; + System.arraycopy(src, srcOffset, value, 0, valueLength); + valueRef[0]= value; + } + + return srcOffset - originalOffset + valueLength; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes an encoded string from the given byte array. + * + * @param src source of encoded data + * @param srcOffset offset into encoded data + * @param valueRef decoded string is stored in element 0, which may be null + * @return amount of bytes read from source + * @throws CorruptEncodingException if source data is corrupt + */ + public static int decodeString(byte[] src, int srcOffset, String[] valueRef) + throws CorruptEncodingException + { + try { + final int originalOffset = srcOffset; + + int b = src[srcOffset++] & 0xff; + if (b >= 0xf8) { + valueRef[0] = null; + return 1; + } + + int valueLength; + if (b <= 0x7f) { + valueLength = b; + } else if (b <= 0xbf) { + valueLength = ((b & 0x3f) << 8) | (src[srcOffset++] & 0xff); + } else if (b <= 0xdf) { + valueLength = ((b & 0x1f) << 16) | ((src[srcOffset++] & 0xff) << 8) | + (src[srcOffset++] & 0xff); + } else if (b <= 0xef) { + valueLength = ((b & 0x0f) << 24) | ((src[srcOffset++] & 0xff) << 16) | + ((src[srcOffset++] & 0xff) << 8) | (src[srcOffset++] & 0xff); + } else { + valueLength = ((src[srcOffset++] & 0xff) << 24) | + ((src[srcOffset++] & 0xff) << 16) | + ((src[srcOffset++] & 0xff) << 8) | (src[srcOffset++] & 0xff); + } + + if (valueLength == 0) { + valueRef[0] = ""; + return srcOffset - originalOffset; + } + + char[] value = new char[valueLength]; + int valueOffset = 0; + + while (valueOffset < valueLength) { + int c = src[srcOffset++] & 0xff; + switch (c >> 5) { + case 0: case 1: case 2: case 3: + // 0xxxxxxx + value[valueOffset++] = (char)c; + break; + case 4: case 5: + // 10xxxxxx xxxxxxxx + value[valueOffset++] = (char)(((c & 0x3f) << 8) | (src[srcOffset++] & 0xff)); + break; + case 6: + // 110xxxxx xxxxxxxx xxxxxxxx + c = ((c & 0x1f) << 16) | ((src[srcOffset++] & 0xff) << 8) + | (src[srcOffset++] & 0xff); + if (c >= 0x10000) { + // Split into surrogate pair. + c -= 0x10000; + value[valueOffset++] = (char)(0xd800 | ((c >> 10) & 0x3ff)); + value[valueOffset++] = (char)(0xdc00 | (c & 0x3ff)); + } else { + value[valueOffset++] = (char)c; + } + break; + default: + // 111xxxxx + // Illegal. + throw new CorruptEncodingException + ("Corrupt encoded string data (source offset = " + + (srcOffset - 1) + ')'); + } + } + + valueRef[0] = new String(value); + + return srcOffset - originalOffset; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes the given byte array which was encoded by {@link + * DataEncoder#encodeSingle}. + * + * @param prefixPadding amount of extra bytes to skip from start of encoded byte array + * @param suffixPadding amount of extra bytes to skip at end of encoded byte array + */ + public static byte[] decodeSingle(byte[] src, int prefixPadding, int suffixPadding) + throws CorruptEncodingException + { + try { + int length = src.length - suffixPadding - prefixPadding; + if (length == 0) { + return EMPTY_BYTE_ARRAY; + } + if (prefixPadding <= 0 && suffixPadding <= 0) { + return src; + } + byte[] dst = new byte[length]; + System.arraycopy(src, prefixPadding, dst, 0, length); + return dst; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes the given byte array which was encoded by {@link + * DataEncoder#encodeSingleNullable}. + */ + public static byte[] decodeSingleNullable(byte[] src) throws CorruptEncodingException { + return decodeSingleNullable(src, 0, 0); + } + + /** + * Decodes the given byte array which was encoded by {@link + * DataEncoder#encodeSingleNullable}. + * + * @param prefixPadding amount of extra bytes to skip from start of encoded byte array + * @param suffixPadding amount of extra bytes to skip at end of encoded byte array + */ + public static byte[] decodeSingleNullable(byte[] src, int prefixPadding, int suffixPadding) + throws CorruptEncodingException + { + try { + byte b = src[prefixPadding]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + int length = src.length - suffixPadding - 1 - prefixPadding; + if (length == 0) { + return EMPTY_BYTE_ARRAY; + } + byte[] value = new byte[length]; + System.arraycopy(src, 1 + prefixPadding, value, 0, length); + return value; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } +} diff --git a/src/main/java/com/amazon/carbonado/raw/DataEncoder.java b/src/main/java/com/amazon/carbonado/raw/DataEncoder.java new file mode 100644 index 0000000..54846ce --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/DataEncoder.java @@ -0,0 +1,595 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +/** + * A very low-level class that supports encoding of primitive data. For + * encoding data into keys, see {@link KeyEncoder}. + * + * @author Brian S O'Neill + * @see DataDecoder + */ +public class DataEncoder { + // Note: Most of these methods are inherited by KeyEncoder, which is why + // they are encoded for supporting proper ordering. + + /** Byte to use for null, low ordering */ + static final byte NULL_BYTE_LOW = 0; + + /** Byte to use for null, high ordering */ + static final byte NULL_BYTE_HIGH = (byte)~NULL_BYTE_LOW; + + /** Byte to use for not-null, low ordering */ + static final byte NOT_NULL_BYTE_HIGH = (byte)128; + + /** Byte to use for not-null, high ordering */ + static final byte NOT_NULL_BYTE_LOW = (byte)~NOT_NULL_BYTE_HIGH; + + static final byte[] NULL_BYTE_ARRAY_HIGH = {NULL_BYTE_HIGH}; + static final byte[] NULL_BYTE_ARRAY_LOW = {NULL_BYTE_LOW}; + static final byte[] NOT_NULL_BYTE_ARRAY_HIGH = {NOT_NULL_BYTE_HIGH}; + static final byte[] NOT_NULL_BYTE_ARRAY_LOW = {NOT_NULL_BYTE_LOW}; + + /** + * Encodes the given signed integer into exactly 4 bytes. + * + * @param value signed integer value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(int value, byte[] dst, int dstOffset) { + value ^= 0x80000000; + dst[dstOffset ] = (byte)(value >> 24); + dst[dstOffset + 1] = (byte)(value >> 16); + dst[dstOffset + 2] = (byte)(value >> 8); + dst[dstOffset + 3] = (byte)value; + } + + /** + * Encodes the given signed Integer object into exactly 1 or 5 bytes. If + * the Integer object is never expected to be null, consider encoding as an + * int primitive. + * + * @param value optional signed Integer value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(Integer value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_HIGH; + encode(value.intValue(), dst, dstOffset + 1); + return 5; + } + } + + /** + * Encodes the given signed long into exactly 8 bytes. + * + * @param value signed long value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(long value, byte[] dst, int dstOffset) { + int w = ((int)(value >> 32)) ^ 0x80000000; + dst[dstOffset ] = (byte)(w >> 24); + dst[dstOffset + 1] = (byte)(w >> 16); + dst[dstOffset + 2] = (byte)(w >> 8); + dst[dstOffset + 3] = (byte)w; + w = (int)value; + dst[dstOffset + 4] = (byte)(w >> 24); + dst[dstOffset + 5] = (byte)(w >> 16); + dst[dstOffset + 6] = (byte)(w >> 8); + dst[dstOffset + 7] = (byte)w; + } + + /** + * Encodes the given signed Long object into exactly 1 or 9 bytes. If the + * Long object is never expected to be null, consider encoding as a long + * primitive. + * + * @param value optional signed Long value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(Long value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_HIGH; + encode(value.longValue(), dst, dstOffset + 1); + return 9; + } + } + + /** + * Encodes the given signed byte into exactly 1 byte. + * + * @param value signed byte value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(byte value, byte[] dst, int dstOffset) { + dst[dstOffset] = (byte)(value ^ 0x80); + } + + /** + * Encodes the given signed Byte object into exactly 1 or 2 bytes. If the + * Byte object is never expected to be null, consider encoding as a byte + * primitive. + * + * @param value optional signed Byte value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(Byte value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_HIGH; + dst[dstOffset + 1] = (byte)(value ^ 0x80); + return 2; + } + } + + /** + * Encodes the given signed short into exactly 2 bytes. + * + * @param value signed short value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(short value, byte[] dst, int dstOffset) { + value ^= 0x8000; + dst[dstOffset ] = (byte)(value >> 8); + dst[dstOffset + 1] = (byte)value; + } + + /** + * Encodes the given signed Short object into exactly 1 or 3 bytes. If the + * Short object is never expected to be null, consider encoding as a short + * primitive. + * + * @param value optional signed Short value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(Short value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_HIGH; + encode(value.shortValue(), dst, dstOffset + 1); + return 3; + } + } + + /** + * Encodes the given character into exactly 2 bytes. + * + * @param value character value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(char value, byte[] dst, int dstOffset) { + dst[dstOffset ] = (byte)(value >> 8); + dst[dstOffset + 1] = (byte)value; + } + + /** + * Encodes the given Character object into exactly 1 or 3 bytes. If the + * Character object is never expected to be null, consider encoding as a + * char primitive. + * + * @param value optional Character value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(Character value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_HIGH; + encode(value.charValue(), dst, dstOffset + 1); + return 3; + } + } + + /** + * Encodes the given boolean into exactly 1 byte. + * + * @param value boolean value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(boolean value, byte[] dst, int dstOffset) { + dst[dstOffset] = value ? (byte)128 : (byte)127; + } + + /** + * Encodes the given Boolean object into exactly 1 byte. + * + * @param value optional Boolean value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(Boolean value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + } else { + dst[dstOffset] = value.booleanValue() ? (byte)128 : (byte)127; + } + } + + /** + * Encodes the given float into exactly 4 bytes. + * + * @param value float value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(float value, byte[] dst, int dstOffset) { + int bits = Float.floatToIntBits(value); + bits ^= (bits < 0) ? 0xffffffff : 0x80000000; + dst[dstOffset ] = (byte)(bits >> 24); + dst[dstOffset + 1] = (byte)(bits >> 16); + dst[dstOffset + 2] = (byte)(bits >> 8); + dst[dstOffset + 3] = (byte)bits; + } + + /** + * Encodes the given Float object into exactly 4 bytes. A non-canonical NaN + * value is used to represent null. + * + * @param value optional Float value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(Float value, byte[] dst, int dstOffset) { + if (value == null) { + encode(0x7fffffff, dst, dstOffset); + } else { + encode(value.floatValue(), dst, dstOffset); + } + } + + /** + * Encodes the given double into exactly 8 bytes. + * + * @param value double value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(double value, byte[] dst, int dstOffset) { + long bits = Double.doubleToLongBits(value); + bits ^= (bits < 0) ? 0xffffffffffffffffL : 0x8000000000000000L; + int w = (int)(bits >> 32); + dst[dstOffset ] = (byte)(w >> 24); + dst[dstOffset + 1] = (byte)(w >> 16); + dst[dstOffset + 2] = (byte)(w >> 8); + dst[dstOffset + 3] = (byte)w; + w = (int)bits; + dst[dstOffset + 4] = (byte)(w >> 24); + dst[dstOffset + 5] = (byte)(w >> 16); + dst[dstOffset + 6] = (byte)(w >> 8); + dst[dstOffset + 7] = (byte)w; + } + + /** + * Encodes the given Double object into exactly 8 bytes. A non-canonical + * NaN value is used to represent null. + * + * @param value optional Double value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(Double value, byte[] dst, int dstOffset) { + if (value == null) { + encode(0x7fffffffffffffffL, dst, dstOffset); + } else { + encode(value.doubleValue(), dst, dstOffset); + } + } + + /** + * Encodes the given optional byte array into a variable amount of + * bytes. If the byte array is null, exactly 1 byte is written. Otherwise, + * the amount written can be determined by calling calculateEncodedLength. + * + * @param value byte array value to encode, may be null + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(byte[] value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } + return encode(value, 0, value.length, dst, dstOffset); + } + + /** + * Encodes the given optional byte array into a variable amount of + * bytes. If the byte array is null, exactly 1 byte is written. Otherwise, + * the amount written can be determined by calling calculateEncodedLength. + * + * @param value byte array value to encode, may be null + * @param valueOffset offset into byte array + * @param valueLength length of data in byte array + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(byte[] value, int valueOffset, int valueLength, + byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } + + // Write the value length first, in a variable amount of bytes. + int amt = writeLength(valueLength, dst, dstOffset); + + // Now write the value. + System.arraycopy(value, valueOffset, dst, dstOffset + amt, valueLength); + + return amt + valueLength; + } + + /** + * Returns the amount of bytes required to encode the given byte array. + * + * @param value byte array value to encode, may be null + * @return amount of bytes needed to encode + */ + public static int calculateEncodedLength(byte[] value) { + return value == null ? 1 : calculateEncodedLength(value, 0, value.length); + } + + /** + * Returns the amount of bytes required to encode the given byte array. + * + * @param value byte array value to encode, may be null + * @param valueOffset offset into byte array + * @param valueLength length of data in byte array + * @return amount of bytes needed to encode + */ + public static int calculateEncodedLength(byte[] value, int valueOffset, int valueLength) { + if (value == null) { + return 1; + } else if (valueLength < 128) { + return 1 + valueLength; + } else if (valueLength < 16384) { + return 2 + valueLength; + } else if (valueLength < 2097152) { + return 3 + valueLength; + } else if (valueLength < 268435456) { + return 4 + valueLength; + } else { + return 5 + valueLength; + } + } + + /** + * Encodes the given optional String into a variable amount of bytes. The + * amount written can be determined by calling + * calculateEncodedStringLength. + *

+ * Strings are encoded in a fashion similar to UTF-8, in that ASCII + * characters are written in one byte. This encoding is more efficient than + * UTF-8, but it isn't compatible with UTF-8. + * + * @param value String value to encode, may be null + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(String value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } + final int originalOffset = dstOffset; + + int valueLength = value.length(); + + // Write the value length first, in a variable amount of bytes. + dstOffset += writeLength(valueLength, dst, dstOffset); + + for (int i = 0; i < valueLength; i++) { + int c = value.charAt(i); + if (c <= 0x7f) { + dst[dstOffset++] = (byte)c; + } else if (c <= 0x3fff) { + dst[dstOffset++] = (byte)(0x80 | (c >> 8)); + dst[dstOffset++] = (byte)(c & 0xff); + } else { + if (c >= 0xd800 && c <= 0xdbff) { + // Found a high surrogate. Verify that surrogate pair is + // well-formed. Low surrogate must follow high surrogate. + if (i + 1 < valueLength) { + int c2 = value.charAt(i + 1); + if (c2 >= 0xdc00 && c2 <= 0xdfff) { + c = 0x10000 + (((c & 0x3ff) << 10) | (c2 & 0x3ff)); + i++; + } + } + } + dst[dstOffset++] = (byte)(0xc0 | (c >> 16)); + dst[dstOffset++] = (byte)((c >> 8) & 0xff); + dst[dstOffset++] = (byte)(c & 0xff); + } + } + + return dstOffset - originalOffset; + } + + /** + * Returns the amount of bytes required to encode the given String. + * + * @param value String to encode, may be null + */ + public static int calculateEncodedStringLength(String value) { + if (value == null) { + return 1; + } + + int valueLength = value.length(); + int encodedLen; + + if (valueLength < 128) { + encodedLen = 1; + } else if (valueLength < 16384) { + encodedLen = 2; + } else if (valueLength < 2097152) { + encodedLen = 3; + } else if (valueLength < 268435456) { + encodedLen = 4; + } else { + encodedLen = 5; + } + + for (int i = 0; i < valueLength; i++) { + int c = value.charAt(i); + if (c <= 0x7f) { + encodedLen++; + } else if (c <= 0x3fff) { + encodedLen += 2; + } else { + if (c >= 0xd800 && c <= 0xdbff) { + // Found a high surrogate. Verify that surrogate pair is + // well-formed. Low surrogate must follow high surrogate. + if (i + 1 < valueLength) { + int c2 = value.charAt(i + 1); + if (c2 >= 0xdc00 && c2 <= 0xdfff) { + i++; + } + } + } + encodedLen += 3; + } + } + + return encodedLen; + } + + private static int writeLength(int valueLength, byte[] dst, int dstOffset) { + if (valueLength < 128) { + dst[dstOffset] = (byte)valueLength; + return 1; + } else if (valueLength < 16384) { + dst[dstOffset++] = (byte)((valueLength >> 8) | 0x80); + dst[dstOffset] = (byte)valueLength; + return 2; + } else if (valueLength < 2097152) { + dst[dstOffset++] = (byte)((valueLength >> 16) | 0xc0); + dst[dstOffset++] = (byte)(valueLength >> 8); + dst[dstOffset] = (byte)valueLength; + return 3; + } else if (valueLength < 268435456) { + dst[dstOffset++] = (byte)((valueLength >> 24) | 0xe0); + dst[dstOffset++] = (byte)(valueLength >> 16); + dst[dstOffset++] = (byte)(valueLength >> 8); + dst[dstOffset] = (byte)valueLength; + return 4; + } else { + dst[dstOffset++] = (byte)0xf0; + dst[dstOffset++] = (byte)(valueLength >> 24); + dst[dstOffset++] = (byte)(valueLength >> 16); + dst[dstOffset++] = (byte)(valueLength >> 8); + dst[dstOffset] = (byte)valueLength; + return 5; + } + } + + /** + * Encodes the given byte array for use when there is only a single + * property, whose type is a byte array. The original byte array is + * returned if the padding lengths are zero. + * + * @param prefixPadding amount of extra bytes to allocate at start of encoded byte array + * @param suffixPadding amount of extra bytes to allocate at end of encoded byte array + */ + public static byte[] encodeSingle(byte[] value, int prefixPadding, int suffixPadding) { + if (prefixPadding <= 0 && suffixPadding <= 0) { + return value; + } + int length = value.length; + byte[] dst = new byte[prefixPadding + length + suffixPadding]; + System.arraycopy(value, 0, dst, prefixPadding, length); + return dst; + } + + /** + * Encodes the given byte array for use when there is only a single + * nullable property, whose type is a byte array. + */ + public static byte[] encodeSingleNullable(byte[] value) { + return encodeSingleNullable(value, 0, 0); + } + + /** + * Encodes the given byte array for use when there is only a single + * nullable property, whose type is a byte array. + * + * @param prefixPadding amount of extra bytes to allocate at start of encoded byte array + * @param suffixPadding amount of extra bytes to allocate at end of encoded byte array + */ + public static byte[] encodeSingleNullable(byte[] value, int prefixPadding, int suffixPadding) { + if (prefixPadding <= 0 && suffixPadding <= 0) { + if (value == null) { + return NULL_BYTE_ARRAY_HIGH; + } + + int length = value.length; + if (length == 0) { + return NOT_NULL_BYTE_ARRAY_HIGH; + } + + byte[] dst = new byte[1 + length]; + dst[0] = NOT_NULL_BYTE_HIGH; + System.arraycopy(value, 0, dst, 1, length); + return dst; + } + + if (value == null) { + byte[] dst = new byte[prefixPadding + 1 + suffixPadding]; + dst[prefixPadding] = NULL_BYTE_HIGH; + return dst; + } + + int length = value.length; + byte[] dst = new byte[prefixPadding + 1 + length + suffixPadding]; + dst[prefixPadding] = NOT_NULL_BYTE_HIGH; + System.arraycopy(value, 0, dst, prefixPadding + 1, length); + return dst; + } +} diff --git a/src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java b/src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java new file mode 100644 index 0000000..2cd0807 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java @@ -0,0 +1,1963 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Map; + +import org.cojen.classfile.CodeAssembler; +import org.cojen.classfile.Label; +import org.cojen.classfile.LocalVariable; +import org.cojen.classfile.Opcode; +import org.cojen.classfile.TypeDesc; +import org.cojen.util.BeanIntrospector; +import org.cojen.util.BeanProperty; + +import com.amazon.carbonado.CorruptEncodingException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.SupportException; + +import com.amazon.carbonado.lob.Blob; +import com.amazon.carbonado.lob.Clob; +import com.amazon.carbonado.lob.Lob; + +import com.amazon.carbonado.spi.StorableGenerator; +import com.amazon.carbonado.spi.TriggerSupport; + +import com.amazon.carbonado.info.ChainedProperty; +import com.amazon.carbonado.info.Direction; +import com.amazon.carbonado.info.OrderedProperty; +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.info.StorableIntrospector; +import com.amazon.carbonado.info.StorableProperty; +import com.amazon.carbonado.info.StorablePropertyAdapter; + +/** + * Generates bytecode instructions for encoding/decoding Storable properties + * to/from raw bytes. + * + *

Note: subclasses must override and specialize the hashCode and equals + * methods. Failure to do so interferes with {@link StorableCodecFactory}'s + * generated code cache. + * + * @author Brian S O'Neill + */ +public class GenericEncodingStrategy { + private final Class mType; + private final StorableIndex mPkIndex; + + private final int mKeyPrefixPadding; + private final int mKeySuffixPadding; + private final int mDataPrefixPadding; + private final int mDataSuffixPadding; + + /** + * @param type type of Storable to generate code for + * @param pkIndex specifies sequence and ordering of key properties (optional) + */ + public GenericEncodingStrategy(Class type, StorableIndex pkIndex) { + this(type, pkIndex, 0, 0, 0, 0); + } + + /** + * @param type type of Storable to generate code for + * @param pkIndex specifies sequence and ordering of key properties (optional) + * @param keyPrefixPadding amount of padding bytes at start of keys + * @param keySuffixPadding amount of padding bytes at end of keys + * @param dataPrefixPadding amount of padding bytes at start of data values + * @param dataSuffixPadding amount of padding bytes at end of data values + */ + @SuppressWarnings("unchecked") + public GenericEncodingStrategy(Class type, StorableIndex pkIndex, + int keyPrefixPadding, int keySuffixPadding, + int dataPrefixPadding, int dataSuffixPadding) { + mType = type; + + if (keyPrefixPadding < 0 || keySuffixPadding < 0 || + dataPrefixPadding < 0 || dataSuffixPadding < 0) { + throw new IllegalArgumentException(); + } + mKeyPrefixPadding = keyPrefixPadding; + mKeySuffixPadding = keySuffixPadding; + mDataPrefixPadding = dataPrefixPadding; + mDataSuffixPadding = dataSuffixPadding; + + if (pkIndex == null) { + Map> map = + StorableIntrospector.examine(mType).getPrimaryKeyProperties(); + + StorableProperty[] properties = new StorableProperty[map.size()]; + map.values().toArray(properties); + + Direction[] directions = new Direction[map.size()]; + Arrays.fill(directions, Direction.UNSPECIFIED); + + pkIndex = new StorableIndex(properties, directions, true); + } + + mPkIndex = pkIndex; + } + + /** + * Generates bytecode instructions to encode properties. The encoding is + * suitable for "key" encoding, which means it is correctly comparable. + * + *

Note: if a partialStartVar is provided and this strategy has a key + * prefix, the prefix is allocated only if the runtime value of + * partialStartVar is zero. Likewise, if a partialEndVar is provided and + * this strategy has a key suffix, the suffix is allocated only of the + * runtime value of partialEndVar is one less than the property count. + * + * @param assembler code assembler to receive bytecode instructions + * @param properties specific properties to encode, defaults to all key + * properties if null + * @param instanceVar local variable referencing Storable instance, + * defaults to "this" if null. If variable type is an Object array, then + * property values are read from the runtime value of this array instead + * of a Storable instance. + * @param adapterInstanceClass class containing static references to + * adapter instances - defaults to instanceVar + * @param useReadMethods when true, access properties by public read + * methods instead of protected fields - should be used if class being + * generated doesn't have access to these fields + * @param partialStartVar optional variable for supporting partial key + * generation. It must be an int, whose runtime value must be less than the + * properties array length. It marks the range start of the partial + * property range. + * @param partialEndVar optional variable for supporting partial key + * generation. It must be an int, whose runtime value must be less than or + * equal to the properties array length. It marks the range end (exclusive) + * of the partial property range. + * + * @return local variable referencing a byte array with encoded key + * + * @throws SupportException if any property type is not supported + * @throws IllegalArgumentException if assembler is null, or if instanceVar + * is not the correct instance type, or if partial variable types are not + * ints + */ + public LocalVariable buildKeyEncoding(CodeAssembler assembler, + OrderedProperty[] properties, + LocalVariable instanceVar, + Class adapterInstanceClass, + boolean useReadMethods, + LocalVariable partialStartVar, + LocalVariable partialEndVar) + throws SupportException + { + properties = ensureKeyProperties(properties); + return buildEncoding(true, assembler, + extractProperties(properties), extractDirections(properties), + instanceVar, adapterInstanceClass, + useReadMethods, + -1, // no generation support + partialStartVar, partialEndVar); + } + + /** + * Generates bytecode instructions to decode properties. A + * CorruptEncodingException may be thrown from generated code. + * + * @param assembler code assembler to receive bytecode instructions + * @param properties specific properties to decode, defaults to all key + * properties if null + * @param instanceVar local variable referencing Storable instance, + * defaults to "this" if null. If variable type is an Object array, then + * property values are placed into the runtime value of this array instead + * of a Storable instance. + * @param adapterInstanceClass class containing static references to + * adapter instances - defaults to instanceVar + * @param useWriteMethods when true, set properties by public write + * methods instead of protected fields - should be used if class being + * generated doesn't have access to these fields + * @param encodedVar required variable, which must be a byte array. At + * runtime, it references an encoded key. + * + * @throws SupportException if any property type is not supported + * @throws IllegalArgumentException if assembler is null, or if instanceVar + * is not the correct instance type, or if encodedVar is not a byte array + */ + public void buildKeyDecoding(CodeAssembler assembler, + OrderedProperty[] properties, + LocalVariable instanceVar, + Class adapterInstanceClass, + boolean useWriteMethods, + LocalVariable encodedVar) + throws SupportException + { + properties = ensureKeyProperties(properties); + buildDecoding(true, assembler, + extractProperties(properties), extractDirections(properties), + instanceVar, adapterInstanceClass, useWriteMethods, + -1, null, // no generation support + encodedVar); + } + + /** + * Generates bytecode instructions to encode properties. The encoding is + * suitable for "data" encoding, which means it is not correctly + * comparable, but it is more efficient than key encoding. Partial encoding + * is not supported. + * + * @param assembler code assembler to receive bytecode instructions + * @param properties specific properties to encode, defaults to all non-key + * properties if null + * @param instanceVar local variable referencing Storable instance, + * defaults to "this" if null. If variable type is an Object array, then + * property values are read from the runtime value of this array instead + * of a Storable instance. + * @param adapterInstanceClass class containing static references to + * adapter instances - defaults to instanceVar + * @param useReadMethods when true, access properties by public read + * methods instead of protected fields + * @param generation when non-negative, write a storable layout generation + * value in one or four bytes. Generation 0..127 is encoded in one byte, and + * 128..max is encoded in four bytes, with the most significant bit set. + * + * @return local variable referencing a byte array with encoded data + * + * @throws SupportException if any property type is not supported + * @throws IllegalArgumentException if assembler is null, or if instanceVar + * is not the correct instance type + */ + public LocalVariable buildDataEncoding(CodeAssembler assembler, + StorableProperty[] properties, + LocalVariable instanceVar, + Class adapterInstanceClass, + boolean useReadMethods, + int generation) + throws SupportException + { + properties = ensureDataProperties(properties); + return buildEncoding(false, assembler, + properties, null, + instanceVar, adapterInstanceClass, + useReadMethods, generation, null, null); + } + + /** + * Generates bytecode instructions to decode properties. A + * CorruptEncodingException may be thrown from generated code. + * + * @param assembler code assembler to receive bytecode instructions + * @param properties specific properties to decode, defaults to all non-key + * properties if null + * @param instanceVar local variable referencing Storable instance, + * defaults to "this" if null. If variable type is an Object array, then + * property values are placed into the runtime value of this array instead + * of a Storable instance. + * @param adapterInstanceClass class containing static references to + * adapter instances - defaults to instanceVar + * @param useWriteMethods when true, set properties by public write + * methods instead of protected fields - should be used if class being + * generated doesn't have access to these fields + * @param generation when non-negative, decoder expects a storable layout + * generation value to match this value. Otherwise, it throws a + * CorruptEncodingException. + * @param altGenerationHandler if non-null and a generation is provided, + * this label defines an alternate generation handler. It is executed + * instead of throwing a CorruptEncodingException if the generation doesn't + * match. The actual generation is available on the top of the stack for + * the handler to consume. + * @param encodedVar required variable, which must be a byte array. At + * runtime, it references encoded data. + * + * @throws SupportException if any property type is not supported + * @throws IllegalArgumentException if assembler is null, or if instanceVar + * is not the correct instance type, or if encodedVar is not a byte array + */ + public void buildDataDecoding(CodeAssembler assembler, + StorableProperty[] properties, + LocalVariable instanceVar, + Class adapterInstanceClass, + boolean useWriteMethods, + int generation, + Label altGenerationHandler, + LocalVariable encodedVar) + throws SupportException + { + properties = ensureDataProperties(properties); + buildDecoding(false, assembler, properties, null, + instanceVar, adapterInstanceClass, useWriteMethods, + generation, altGenerationHandler, encodedVar); + } + + /** + * Returns the type of Storable that code is generated for. + */ + public final Class getType() { + return mType; + } + + /** + * Returns true if the type of the given property type is supported. The + * types currently supported are primitives, primitive wrapper objects, + * Strings, and byte arrays. + */ + public boolean isSupported(Class propertyType) { + return isSupported(TypeDesc.forClass(propertyType)); + } + + /** + * Returns true if the type of the given property type is supported. The + * types currently supported are primitives, primitive wrapper objects, + * Strings, byte arrays and Lobs. + */ + public boolean isSupported(TypeDesc propertyType) { + if (propertyType.toPrimitiveType() != null) { + return true; + } + return propertyType == TypeDesc.STRING || + propertyType == TypeDesc.forClass(byte[].class) || + propertyType.toClass() != null && Lob.class.isAssignableFrom(propertyType.toClass()); + } + + public int getKeyPrefixPadding() { + return mKeyPrefixPadding; + } + + public int getKeySuffixPadding() { + return mKeySuffixPadding; + } + + public int getDataPrefixPadding() { + return mDataPrefixPadding; + } + + public int getDataSuffixPadding() { + return mDataSuffixPadding; + } + + /** + * Returns amount of prefix key bytes that encoding strategy instance + * produces which are always the same. Default implementation returns 0. + */ + public int getConstantKeyPrefixLength() { + return 0; + } + + @Override + public int hashCode() { + return mType.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof GenericEncodingStrategy) { + GenericEncodingStrategy other = (GenericEncodingStrategy) obj; + return mType == other.mType + && mKeyPrefixPadding == other.mKeyPrefixPadding + && mKeySuffixPadding == other.mKeySuffixPadding + && mDataPrefixPadding == other.mDataPrefixPadding + && mDataSuffixPadding == other.mDataSuffixPadding; + } + return false; + } + + /** + * Returns all key properties in the form of an index. + */ + protected StorableIndex getPrimaryKeyIndex() { + return mPkIndex; + } + + /** + * Returns all key properties as ordered properties, possibly with + * unspecified directions. + */ + protected OrderedProperty[] gatherAllKeyProperties() { + return mPkIndex.getOrderedProperties(); + } + + /** + * Returns all data properties for storable. + */ + @SuppressWarnings("unchecked") + protected StorableProperty[] gatherAllDataProperties() { + Map> map = + StorableIntrospector.examine(mType).getDataProperties(); + + StorableProperty[] properties = new StorableProperty[map.size()]; + + int ordinal = 0; + for (StorableProperty property : map.values()) { + properties[ordinal++] = property; + } + + return properties; + } + + protected StorablePropertyInfo checkSupport(StorableProperty property) + throws SupportException + { + if (isSupported(property.getType())) { + return new StorablePropertyInfo(property); + } + + // Look for an adapter that will allow this property to be supported. + if (property.getAdapter() != null) { + StorablePropertyAdapter adapter = property.getAdapter(); + for (Class storageType : adapter.getStorageTypePreferences()) { + if (!isSupported(storageType)) { + continue; + } + + if (property.isNullable() && storageType.isPrimitive()) { + continue; + } + + Method fromStorage, toStorage; + fromStorage = adapter.findAdaptMethod(storageType, property.getType()); + if (fromStorage == null) { + continue; + } + toStorage = adapter.findAdaptMethod(property.getType(), storageType); + if (toStorage != null) { + return new StorablePropertyInfo(property, storageType, fromStorage, toStorage); + } + } + } + + throw notSupported(property); + } + + @SuppressWarnings("unchecked") + protected StorablePropertyInfo[] checkSupport(StorableProperty[] properties) + throws SupportException + { + int length = properties.length; + StorablePropertyInfo[] infos = new StorablePropertyInfo[length]; + for (int i=0; i property) { + return notSupported(property.getName(), property.getType().getName()); + } + + private SupportException notSupported(String propertyName, String typeName) { + return new SupportException + ("Type \"" + typeName + + "\" not supported for property \"" + propertyName + '"'); + } + + private OrderedProperty[] ensureKeyProperties(OrderedProperty[] properties) { + if (properties == null) { + properties = gatherAllKeyProperties(); + } else { + for (Object prop : properties) { + if (prop == null) { + throw new IllegalArgumentException(); + } + } + } + return properties; + } + + @SuppressWarnings("unchecked") + private StorableProperty[] extractProperties(OrderedProperty[] ordered) { + StorableProperty[] properties = new StorableProperty[ordered.length]; + for (int i=0; i 0) { + throw new IllegalArgumentException(); + } + properties[i] = chained.getPrimeProperty(); + } + return properties; + } + + private Direction[] extractDirections(OrderedProperty[] ordered) { + Direction[] directions = new Direction[ordered.length]; + for (int i=0; i[] ensureDataProperties(StorableProperty[] properties) { + if (properties == null) { + properties = gatherAllDataProperties(); + } else { + for (Object prop : properties) { + if (prop == null) { + throw new IllegalArgumentException(); + } + } + } + return properties; + } + + ///////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////// + + private LocalVariable buildEncoding(boolean forKey, + CodeAssembler a, + StorableProperty[] properties, + Direction[] directions, + LocalVariable instanceVar, + Class adapterInstanceClass, + boolean useReadMethods, + int generation, + LocalVariable partialStartVar, + LocalVariable partialEndVar) + throws SupportException + { + if (a == null) { + throw new IllegalArgumentException(); + } + if (partialStartVar != null && partialStartVar.getType() != TypeDesc.INT) { + throw new IllegalArgumentException(); + } + if (partialEndVar != null && partialEndVar.getType() != TypeDesc.INT) { + throw new IllegalArgumentException(); + } + + // Encoding order is: + // + // 1. Prefix + // 2. Generation prefix + // 3. Properties + // 4. Suffix + + final int prefix = forKey ? mKeyPrefixPadding : mDataPrefixPadding; + + final int generationPrefix; + if (generation < 0) { + generationPrefix = 0; + } else if (generation < 128) { + generationPrefix = 1; + } else { + generationPrefix = 4; + } + + final int suffix = forKey ? mKeySuffixPadding : mDataSuffixPadding; + + final TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + final LocalVariable encodedVar = a.createLocalVariable(null, byteArrayType); + + StorablePropertyInfo[] infos = checkSupport(properties); + + if (properties.length == 1) { + // Ignore partial key encoding variables, since there can't be a + // partial of one property. + partialStartVar = null; + partialEndVar = null; + + StorableProperty property = properties[0]; + StorablePropertyInfo info = infos[0]; + + if (info.getStorageType().toClass() == byte[].class) { + // Since there is only one property, and it is just a byte + // array, optimize by not doing any fancy encoding. If the + // property is optional, then a byte prefix is needed to + // identify a null reference. + + loadPropertyValue(a, info, 0, useReadMethods, + instanceVar, adapterInstanceClass, partialStartVar); + + boolean descending = + forKey && directions != null && directions[0] == Direction.DESCENDING; + + TypeDesc[] params; + if (prefix > 0 || generationPrefix > 0 || suffix > 0) { + a.loadConstant(prefix + generationPrefix); + a.loadConstant(suffix); + params = new TypeDesc[] {byteArrayType, TypeDesc.INT, TypeDesc.INT}; + } else { + params = new TypeDesc[] {byteArrayType}; + } + + if (property.isNullable()) { + if (descending) { + a.invokeStatic(KeyEncoder.class.getName(), "encodeSingleNullableDesc", + byteArrayType, params); + } else { + a.invokeStatic(DataEncoder.class.getName(), "encodeSingleNullable", + byteArrayType, params); + } + } else if (descending) { + a.invokeStatic(KeyEncoder.class.getName(), "encodeSingleDesc", + byteArrayType, params); + } else if (prefix > 0 || generationPrefix > 0 || suffix > 0) { + a.invokeStatic(DataEncoder.class.getName(), "encodeSingle", + byteArrayType, params); + } else { + // Just return raw property value - no need to cache it either. + } + + a.storeLocal(encodedVar); + + encodeGeneration(a, encodedVar, prefix, generation); + + return encodedVar; + } + } + + boolean doPartial = forKey && (partialStartVar != null || partialEndVar != null); + + // Calculate exactly how many bytes are needed to encode. The length + // is composed of a static and a variable amount. The variable amount + // is determined at runtime. + + int staticLength = 0; + if (!forKey || partialStartVar == null) { + // Only include prefix as static if no runtime check is needed + // against runtime partial start value. + staticLength += prefix + generationPrefix; + } + if (!forKey || partialEndVar == null) { + // Only include suffix as static if no runtime check is needed + // against runtime partial end value. + staticLength += suffix; + } + + boolean hasVariableLength; + if (doPartial) { + hasVariableLength = true; + } else { + hasVariableLength = false; + for (GenericPropertyInfo info : infos) { + int len = staticEncodingLength(info); + if (len >= 0) { + staticLength += len; + } else { + staticLength += ~len; + hasVariableLength = true; + } + } + } + + // Generate code that loops over all the properties that have a + // variable length. Load each property and perform the necessary + // tests to determine the exact encoding length. + + boolean hasStackVar = false; + if (hasVariableLength) { + Label[] entryPoints = null; + + if (partialStartVar != null) { + // Will jump into an arbitrary location, so always have a stack + // variable available. + a.loadConstant(0); + hasStackVar = true; + + entryPoints = jumpToPartialEntryPoints(a, partialStartVar, properties.length); + } + + Label exitPoint = a.createLabel(); + + for (int i=0; i property = properties[i]; + StorablePropertyInfo info = infos[i]; + + if (doPartial) { + if (entryPoints != null) { + entryPoints[i].setLocation(); + } + if (partialEndVar != null) { + // Add code to jump out of partial. + a.loadConstant(i); + a.loadLocal(partialEndVar); + a.ifComparisonBranch(exitPoint, ">="); + } + } else if (staticEncodingLength(info) >= 0) { + continue; + } + + TypeDesc propType = info.getStorageType(); + + if (propType.isPrimitive()) { + // This should only ever get executed if implementing + // partial support. Otherwise, the static encoding length + // would have been already calculated. + a.loadConstant(staticEncodingLength(info)); + if (hasStackVar) { + a.math(Opcode.IADD); + } else { + hasStackVar = true; + } + } else if (propType.toPrimitiveType() != null) { + int amt = 0; + switch (propType.toPrimitiveType().getTypeCode()) { + case TypeDesc.BYTE_CODE: + case TypeDesc.BOOLEAN_CODE: + amt = 1; + break; + case TypeDesc.SHORT_CODE: + case TypeDesc.CHAR_CODE: + amt = 2; + break; + case TypeDesc.INT_CODE: + case TypeDesc.FLOAT_CODE: + amt = 4; + break; + case TypeDesc.LONG_CODE: + case TypeDesc.DOUBLE_CODE: + amt = 8; + break; + } + + int extra = 0; + if (doPartial) { + // If value is null, then there may be a one byte size + // adjust for the null value. Otherwise it is the extra + // amount plus the size to encode the raw primitive + // value. If doPartial is false, then this extra amount + // was already accounted for in the static encoding + // length. + + switch (propType.toPrimitiveType().getTypeCode()) { + case TypeDesc.BYTE_CODE: + case TypeDesc.SHORT_CODE: + case TypeDesc.CHAR_CODE: + case TypeDesc.INT_CODE: + case TypeDesc.LONG_CODE: + extra = 1; + } + } + + if (!property.isNullable() || (doPartial && extra == 0)) { + a.loadConstant(amt); + if (hasStackVar) { + a.math(Opcode.IADD); + } + hasStackVar = true; + } else { + // Load property to test for null. + loadPropertyValue(a, info, i, useReadMethods, + instanceVar, adapterInstanceClass, partialStartVar); + + Label isNull = a.createLabel(); + a.ifNullBranch(isNull, true); + + a.loadConstant(amt); + + if (hasStackVar) { + a.math(Opcode.IADD); + isNull.setLocation(); + if (extra > 0) { + a.loadConstant(extra); + a.math(Opcode.IADD); + } + } else { + hasStackVar = true; + // Make sure that there is a zero (or extra) value on + // the stack if the isNull branch is taken. + Label notNull = a.createLabel(); + a.branch(notNull); + isNull.setLocation(); + a.loadConstant(extra); + notNull.setLocation(); + } + } + } else if (propType == TypeDesc.STRING) { + // Load property to test for null. + loadPropertyValue(a, info, i, useReadMethods, + instanceVar, adapterInstanceClass, partialStartVar); + + String className = + (forKey ? KeyEncoder.class : DataEncoder.class).getName(); + a.invokeStatic(className, "calculateEncodedStringLength", + TypeDesc.INT, new TypeDesc[] {TypeDesc.STRING}); + if (hasStackVar) { + a.math(Opcode.IADD); + } else { + hasStackVar = true; + } + } else if (propType.toClass() == byte[].class) { + // Load property to test for null. + loadPropertyValue(a, info, i, useReadMethods, + instanceVar, adapterInstanceClass, partialStartVar); + + String className = + (forKey ? KeyEncoder.class : DataEncoder.class).getName(); + a.invokeStatic(className, "calculateEncodedLength", + TypeDesc.INT, new TypeDesc[] {byteArrayType}); + if (hasStackVar) { + a.math(Opcode.IADD); + } else { + hasStackVar = true; + } + } else if (info.isLob()) { + // Lob locator is a long, or 8 bytes. + a.loadConstant(8); + if (hasStackVar) { + a.math(Opcode.IADD); + } else { + hasStackVar = true; + } + } else { + throw notSupported(property); + } + } + + exitPoint.setLocation(); + + if (forKey && partialStartVar != null && (prefix > 0 || generationPrefix > 0)) { + // Prefix must be allocated only if runtime value of + // partialStartVar is zero. + a.loadLocal(partialStartVar); + Label noPrefix = a.createLabel(); + a.ifZeroComparisonBranch(noPrefix, "!="); + a.loadConstant(prefix + generationPrefix); + if (hasStackVar) { + a.math(Opcode.IADD); + } else { + hasStackVar = true; + } + noPrefix.setLocation(); + } + + if (forKey && partialEndVar != null && suffix > 0) { + // Suffix must be allocated only if runtime value of + // partialEndVar is equal to property count. + a.loadLocal(partialEndVar); + Label noSuffix = a.createLabel(); + a.loadConstant(properties.length); + a.ifComparisonBranch(noSuffix, "!="); + a.loadConstant(suffix); + if (hasStackVar) { + a.math(Opcode.IADD); + } else { + hasStackVar = true; + } + noSuffix.setLocation(); + } + } + + // Allocate a byte array of the exact size. + if (hasStackVar) { + if (staticLength > 0) { + a.loadConstant(staticLength); + a.math(Opcode.IADD); + } + } else { + a.loadConstant(staticLength); + } + a.newObject(byteArrayType); + a.storeLocal(encodedVar); + + // Now encode into the byte array. + + int constantOffset = 0; + LocalVariable offset = null; + + if (!forKey || partialStartVar == null) { + // Only include prefix as constant offset if no runtime check is + // needed against runtime partial start value. + constantOffset += prefix + generationPrefix; + encodeGeneration(a, encodedVar, prefix, generation); + } + + Label[] entryPoints = null; + + if (forKey && partialStartVar != null) { + // Will jump into an arbitrary location, so put an initial value + // into offset variable. + + offset = a.createLocalVariable(null, TypeDesc.INT); + a.loadConstant(0); + if (prefix > 0) { + // Prefix is allocated only if partial start is zero. Check if + // offset should be adjusted to skip over it. + a.loadLocal(partialStartVar); + Label noPrefix = a.createLabel(); + a.ifZeroComparisonBranch(noPrefix, "!="); + a.loadConstant(prefix + generationPrefix); + a.math(Opcode.IADD); + encodeGeneration(a, encodedVar, prefix, generation); + noPrefix.setLocation(); + } + a.storeLocal(offset); + + entryPoints = jumpToPartialEntryPoints(a, partialStartVar, properties.length); + } + + Label exitPoint = a.createLabel(); + + for (int i=0; i property = properties[i]; + StorablePropertyInfo info = infos[i]; + + if (doPartial) { + if (entryPoints != null) { + entryPoints[i].setLocation(); + } + if (partialEndVar != null) { + // Add code to jump out of partial. + a.loadConstant(i); + a.loadLocal(partialEndVar); + a.ifComparisonBranch(exitPoint, ">="); + } + } + + if (info.isLob()) { + // Need RawSupport instance for getting locator from Lob. + pushRawSupport(a, instanceVar); + } + + boolean fromInstance = loadPropertyValue + (a, info, i, useReadMethods, instanceVar, adapterInstanceClass, partialStartVar); + + TypeDesc propType = info.getStorageType(); + if (!property.isNullable() && propType.toPrimitiveType() != null) { + // Since property type is a required primitive wrapper, convert + // to a primitive rather than encoding using the form that + // distinguishes null. + + // Property value that was passed in may be null, which is not + // allowed. + if (!fromInstance && !propType.isPrimitive()) { + a.dup(); + Label notNull = a.createLabel(); + a.ifNullBranch(notNull, false); + + TypeDesc errorType = TypeDesc.forClass(IllegalArgumentException.class); + a.newObject(errorType); + a.dup(); + a.loadConstant("Value for property \"" + property.getName() + + "\" cannot be null"); + a.invokeConstructor(errorType, new TypeDesc[] {TypeDesc.STRING}); + a.throwObject(); + + notNull.setLocation(); + } + + a.convert(propType, propType.toPrimitiveType()); + propType = propType.toPrimitiveType(); + } + + if (info.isLob()) { + // Extract locator from RawSupport. + getLobLocator(a, info); + + // Locator is a long, so switch the type to be encoded properly. + propType = TypeDesc.LONG; + } + + // Fill out remaining parameters before calling specific method + // to encode property value. + a.loadLocal(encodedVar); + if (offset == null) { + a.loadConstant(constantOffset); + } else { + a.loadLocal(offset); + } + + boolean descending = + forKey && directions != null && directions[i] == Direction.DESCENDING; + + int amt = encodeProperty(a, propType, forKey, descending); + + if (amt > 0) { + if (i + 1 < properties.length) { + // Only adjust offset if there are more properties. + + if (offset == null) { + constantOffset += amt; + } else { + a.loadConstant(amt); + a.loadLocal(offset); + a.math(Opcode.IADD); + a.storeLocal(offset); + } + } + } else { + if (i + 1 >= properties.length) { + // Don't need to keep track of offset anymore. + a.pop(); + } else { + // Only adjust offset if there are more properties. + if (offset == null) { + if (constantOffset > 0) { + a.loadConstant(constantOffset); + a.math(Opcode.IADD); + } + offset = a.createLocalVariable(null, TypeDesc.INT); + } else { + a.loadLocal(offset); + a.math(Opcode.IADD); + } + a.storeLocal(offset); + } + } + } + + exitPoint.setLocation(); + + return encodedVar; + } + + /** + * Generates code to load a property value onto the operand stack. + * + * @param info info for property to load + * @param ordinal zero-based property ordinal, used only if instanceVar + * refers to an object array. + * @param useReadMethod when true, access property by public read method + * instead of protected field + * @param instanceVar local variable referencing Storable instance, + * defaults to "this" if null. If variable type is an Object array, then + * property values are read from the runtime value of this array instead + * of a Storable instance. + * @param adapterInstanceClass class containing static references to + * adapter instances - defaults to instanceVar + * @param partialStartVar optional variable for supporting partial key + * generation. It must be an int, whose runtime value must be less than the + * properties array length. It marks the range start of the partial + * property range. + * @return true if property was loaded from instance, false if loaded from + * value array + */ + protected boolean loadPropertyValue(CodeAssembler a, + StorablePropertyInfo info, int ordinal, + boolean useReadMethod, + LocalVariable instanceVar, + Class adapterInstanceClass, + LocalVariable partialStartVar) + { + TypeDesc type = info.getPropertyType(); + TypeDesc storageType = info.getStorageType(); + + boolean isObjectArrayInstanceVar = instanceVar != null + && instanceVar.getType() == TypeDesc.forClass(Object[].class); + + boolean useAdapterInstance = adapterInstanceClass != null + && info.getToStorageAdapter() != null + && (useReadMethod || isObjectArrayInstanceVar); + + if (useAdapterInstance) { + // Push adapter instance to stack to be used later. + String fieldName = + info.getPropertyName() + StorableGenerator.ADAPTER_FIELD_ELEMENT + 0; + TypeDesc adapterType = TypeDesc.forClass + (info.getToStorageAdapter().getDeclaringClass()); + a.loadStaticField + (TypeDesc.forClass(adapterInstanceClass), fieldName, adapterType); + } + + if (instanceVar == null) { + a.loadThis(); + if (useReadMethod) { + info.addInvokeReadMethod(a); + } else { + // Access property value directly from protected field of "this". + if (info.getToStorageAdapter() == null) { + a.loadField(info.getPropertyName(), type); + } else { + // Invoke adapter method. + a.invokeVirtual(info.getReadMethodName() + '$', storageType, null); + } + } + } else if (!isObjectArrayInstanceVar) { + a.loadLocal(instanceVar); + if (useReadMethod) { + info.addInvokeReadMethod(a, instanceVar.getType()); + } else { + // Access property value directly from protected field of + // referenced instance. Assumes code is being defined in the + // same package or a subclass. + if (info.getToStorageAdapter() == null) { + a.loadField(instanceVar.getType(), info.getPropertyName(), type); + } else { + // Invoke adapter method. + a.invokeVirtual(instanceVar.getType(), + info.getReadMethodName() + '$', storageType, null); + } + } + } else { + // Access property value from object array. + + a.loadLocal(instanceVar); + a.loadConstant(ordinal); + if (ordinal > 0 && partialStartVar != null) { + a.loadLocal(partialStartVar); + a.math(Opcode.ISUB); + } + + a.loadFromArray(TypeDesc.OBJECT); + a.checkCast(type.toObjectType()); + if (type.isPrimitive()) { + a.convert(type.toObjectType(), type); + } + } + + if (useAdapterInstance) { + // Invoke adapter method on instance pushed earlier. + a.invoke(info.getToStorageAdapter()); + } + + return !isObjectArrayInstanceVar; + } + + /** + * Returns a negative value if encoding is variable. The minimum static + * amount is computed from the one's compliment. Of the types with variable + * encoding lengths, only for primitives is the minimum static amount + * returned more than zero. + */ + private int staticEncodingLength(GenericPropertyInfo info) { + TypeDesc type = info.getStorageType(); + TypeDesc primType = type.toPrimitiveType(); + + if (primType == null) { + if (info.isLob()) { + // Lob locator is stored as a long. + return 8; + } + } else { + if (info.isNullable()) { + // Type is a primitive wrapper. + switch (primType.getTypeCode()) { + case TypeDesc.BYTE_CODE: + return ~1; + case TypeDesc.BOOLEAN_CODE: + return 1; + case TypeDesc.SHORT_CODE: + case TypeDesc.CHAR_CODE: + return ~1; + case TypeDesc.INT_CODE: + return ~1; + case TypeDesc.FLOAT_CODE: + return 4; + case TypeDesc.LONG_CODE: + return ~1; + case TypeDesc.DOUBLE_CODE: + return 8; + } + } else { + // Type is primitive or a required primitive wrapper. + switch (type.getTypeCode()) { + case TypeDesc.BYTE_CODE: + case TypeDesc.BOOLEAN_CODE: + return 1; + case TypeDesc.SHORT_CODE: + case TypeDesc.CHAR_CODE: + return 2; + case TypeDesc.INT_CODE: + case TypeDesc.FLOAT_CODE: + return 4; + case TypeDesc.LONG_CODE: + case TypeDesc.DOUBLE_CODE: + return 8; + } + } + } + + return ~0; + } + + /** + * @param partialStartVar must not be null + */ + private Label[] jumpToPartialEntryPoints(CodeAssembler a, LocalVariable partialStartVar, + int propertyCount) { + // Create all the entry points for offset var, whose locations will be + // set later. + int[] cases = new int[propertyCount]; + Label[] entryPoints = new Label[propertyCount]; + for (int i=0; i> (8 * (3 - i)))); + a.storeToArray(TypeDesc.BYTE); + } + } + } + + /** + * Generates code to push RawSupport instance to the stack. RawSupport is + * available only in Storable instances. If instanceVar is an Object[], a + * SupportException is thrown. + */ + private void pushRawSupport(CodeAssembler a, LocalVariable instanceVar) + throws SupportException + { + boolean isObjectArrayInstanceVar = instanceVar != null + && instanceVar.getType() == TypeDesc.forClass(Object[].class); + + if (isObjectArrayInstanceVar) { + throw new SupportException("Lob properties not supported"); + } + + if (instanceVar == null) { + a.loadThis(); + } else { + a.loadLocal(instanceVar); + } + + a.loadField(StorableGenerator.SUPPORT_FIELD_NAME, + TypeDesc.forClass(TriggerSupport.class)); + a.checkCast(TypeDesc.forClass(RawSupport.class)); + } + + /** + * Generates code to get a Lob locator value from RawSupport. RawSupport + * instance and Lob instance must be on the stack. Result is a long locator + * value on the stack. + */ + private void getLobLocator(CodeAssembler a, StorablePropertyInfo info) { + if (!info.isLob()) { + throw new IllegalArgumentException(); + } + a.invokeInterface(TypeDesc.forClass(RawSupport.class), "getLocator", + TypeDesc.LONG, new TypeDesc[] {info.getStorageType()}); + } + + /** + * Generates code to get a Lob from a locator from RawSupport. RawSupport + * instance and long locator must be on the stack. Result is a Lob on the + * stack, which may be null. + */ + private void getLobFromLocator(CodeAssembler a, StorablePropertyInfo info) { + if (!info.isLob()) { + throw new IllegalArgumentException(); + } + + TypeDesc type = info.getStorageType(); + String name; + if (Blob.class.isAssignableFrom(type.toClass())) { + name = "getBlob"; + } else if (Clob.class.isAssignableFrom(type.toClass())) { + name = "getClob"; + } else { + throw new IllegalArgumentException(); + } + + a.invokeInterface(TypeDesc.forClass(RawSupport.class), name, + type, new TypeDesc[] {TypeDesc.LONG}); + } + + ///////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////// + + private void buildDecoding(boolean forKey, + CodeAssembler a, + StorableProperty[] properties, + Direction[] directions, + LocalVariable instanceVar, + Class adapterInstanceClass, + boolean useWriteMethods, + int generation, + Label altGenerationHandler, + LocalVariable encodedVar) + throws SupportException + { + if (a == null) { + throw new IllegalArgumentException(); + } + if (encodedVar == null || encodedVar.getType() != TypeDesc.forClass(byte[].class)) { + throw new IllegalArgumentException(); + } + + // Decoding order is: + // + // 1. Prefix + // 2. Generation prefix + // 3. Properties + // 4. Suffix + + final int prefix = forKey ? mKeyPrefixPadding : mDataPrefixPadding; + + final int generationPrefix; + if (generation < 0) { + generationPrefix = 0; + } else if (generation < 128) { + generationPrefix = 1; + } else { + generationPrefix = 4; + } + + final int suffix = forKey ? mKeySuffixPadding : mDataSuffixPadding; + + final TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + + StorablePropertyInfo[] infos = checkSupport(properties); + + decodeGeneration(a, encodedVar, prefix, generation, altGenerationHandler); + + if (properties.length == 1) { + StorableProperty property = properties[0]; + StorablePropertyInfo info = infos[0]; + + if (info.getStorageType().toClass() == byte[].class) { + // Since there is only one property, and it is just a byte + // array, it doesn't have any fancy encoding. + + // Push to stack in preparation for storing a property. + pushDecodingInstanceVar(a, 0, instanceVar); + + a.loadLocal(encodedVar); + + boolean descending = + forKey && directions != null && directions[0] == Direction.DESCENDING; + + TypeDesc[] params; + if (prefix > 0 || generationPrefix > 0 || suffix > 0) { + a.loadConstant(prefix + generationPrefix); + a.loadConstant(suffix); + params = new TypeDesc[] {byteArrayType, TypeDesc.INT, TypeDesc.INT}; + } else { + params = new TypeDesc[] {byteArrayType}; + } + + if (property.isNullable()) { + if (descending) { + a.invokeStatic(KeyDecoder.class.getName(), "decodeSingleNullableDesc", + byteArrayType, params); + } else { + a.invokeStatic(DataDecoder.class.getName(), "decodeSingleNullable", + byteArrayType, params); + } + } else if (descending) { + a.invokeStatic(KeyDecoder.class.getName(), "decodeSingleDesc", + byteArrayType, params); + } else if (prefix > 0 || generationPrefix > 0 || suffix > 0) { + a.invokeStatic(DataDecoder.class.getName(), "decodeSingle", + byteArrayType, params); + } else { + // Just store raw property value. + } + + storePropertyValue(a, info, useWriteMethods, instanceVar, adapterInstanceClass); + return; + } + } + + // Now decode from the byte array. + + int constantOffset = prefix + generationPrefix; + LocalVariable offset = null; + // References to local variables which will hold references. + LocalVariable[] stringRef = new LocalVariable[1]; + LocalVariable[] byteArrayRef = new LocalVariable[1]; + LocalVariable[] valueRefRef = new LocalVariable[1]; + + for (int i=0; i 0) { + if (offset == null) { + constantOffset += amt; + } else { + a.loadConstant(amt); + a.loadLocal(offset); + a.math(Opcode.IADD); + a.storeLocal(offset); + } + } else { + // Offset adjust is one if returned object is null. + a.dup(); + Label notNull = a.createLabel(); + a.ifNullBranch(notNull, false); + a.loadConstant(1 + (offset == null ? constantOffset : 0)); + Label cont = a.createLabel(); + a.branch(cont); + notNull.setLocation(); + a.loadConstant(~amt + (offset == null ? constantOffset : 0)); + cont.setLocation(); + + if (offset == null) { + offset = a.createLocalVariable(null, TypeDesc.INT); + } else { + a.loadLocal(offset); + a.math(Opcode.IADD); + } + a.storeLocal(offset); + } + } + } else { + if (i + 1 >= properties.length) { + // Don't need to keep track of offset anymore. + a.pop(); + } else { + // Only adjust offset if there are more properties. + if (offset == null) { + if (constantOffset > 0) { + a.loadConstant(constantOffset); + a.math(Opcode.IADD); + } + offset = a.createLocalVariable(null, TypeDesc.INT); + } else { + a.loadLocal(offset); + a.math(Opcode.IADD); + } + a.storeLocal(offset); + } + + // Get the value out of the ref array so that it can be stored. + a.loadLocal(valueRefRef[0]); + a.loadConstant(0); + a.loadFromArray(valueRefRef[0].getType()); + } + + storePropertyValue(a, info, useWriteMethods, instanceVar, adapterInstanceClass); + } + } + + /** + * Generates code that calls a decoding method in DataDecoder or + * KeyDecoder. Parameters must already be on the stack. + * + * @return 0 if an int amount is pushed onto the stack, or a positive value + * if offset adjust amount is constant, or a negative value if offset + * adjust is constant or one more + */ + private int decodeProperty(CodeAssembler a, + GenericPropertyInfo info, TypeDesc storageType, + boolean forKey, boolean descending, + LocalVariable[] stringRefRef, LocalVariable[] byteArrayRefRef, + LocalVariable[] valueRefRef) + throws SupportException + { + TypeDesc primType = storageType.toPrimitiveType(); + + if (primType != null) { + String methodName; + TypeDesc returnType; + int adjust; + + if (primType != storageType && info.isNullable()) { + // Property type is a nullable boxed primitive. + returnType = storageType; + + switch (primType.getTypeCode()) { + case TypeDesc.BYTE_CODE: + methodName = "decodeByteObj"; + adjust = ~2; + break; + case TypeDesc.BOOLEAN_CODE: + methodName = "decodeBooleanObj"; + adjust = 1; + break; + case TypeDesc.SHORT_CODE: + methodName = "decodeShortObj"; + adjust = ~3; + break; + case TypeDesc.CHAR_CODE: + methodName = "decodeCharacterObj"; + adjust = ~3; + break; + default: + case TypeDesc.INT_CODE: + methodName = "decodeIntegerObj"; + adjust = ~5; + break; + case TypeDesc.FLOAT_CODE: + methodName = "decodeFloatObj"; + adjust = 4; + break; + case TypeDesc.LONG_CODE: + methodName = "decodeLongObj"; + adjust = ~9; + break; + case TypeDesc.DOUBLE_CODE: + methodName = "decodeDoubleObj"; + adjust = 8; + break; + } + } else { + // Property type is a primitive or a boxed primitive. + returnType = primType; + + switch (primType.getTypeCode()) { + case TypeDesc.BYTE_CODE: + methodName = "decodeByte"; + adjust = 1; + break; + case TypeDesc.BOOLEAN_CODE: + methodName = "decodeBoolean"; + adjust = 1; + break; + case TypeDesc.SHORT_CODE: + methodName = "decodeShort"; + adjust = 2; + break; + case TypeDesc.CHAR_CODE: + methodName = "decodeChar"; + adjust = 2; + break; + default: + case TypeDesc.INT_CODE: + methodName = "decodeInt"; + adjust = 4; + break; + case TypeDesc.FLOAT_CODE: + methodName = "decodeFloat"; + adjust = 4; + break; + case TypeDesc.LONG_CODE: + methodName = "decodeLong"; + adjust = 8; + break; + case TypeDesc.DOUBLE_CODE: + methodName = "decodeDouble"; + adjust = 8; + break; + } + } + + TypeDesc[] params = {TypeDesc.forClass(byte[].class), TypeDesc.INT}; + if (forKey && descending) { + a.invokeStatic + (KeyDecoder.class.getName(), methodName + "Desc", returnType, params); + } else { + a.invokeStatic + (DataDecoder.class.getName(), methodName, returnType, params); + } + + if (returnType.isPrimitive()) { + if (!storageType.isPrimitive()) { + // Wrap it. + a.convert(returnType, storageType); + } + } + + return adjust; + } else { + String className = (forKey ? KeyDecoder.class : DataDecoder.class).getName(); + String methodName; + TypeDesc refType; + + if (storageType == TypeDesc.STRING) { + methodName = (forKey && descending) ? "decodeStringDesc" : "decodeString"; + refType = TypeDesc.forClass(String[].class); + if (stringRefRef[0] == null) { + stringRefRef[0] = a.createLocalVariable(null, refType); + a.loadConstant(1); + a.newObject(refType); + a.storeLocal(stringRefRef[0]); + } + a.loadLocal(stringRefRef[0]); + valueRefRef[0] = stringRefRef[0]; + } else if (storageType.toClass() == byte[].class) { + methodName = (forKey && descending) ? "decodeDesc" : "decode"; + refType = TypeDesc.forClass(byte[][].class); + if (byteArrayRefRef[0] == null) { + byteArrayRefRef[0] = a.createLocalVariable(null, refType); + a.loadConstant(1); + a.newObject(refType); + a.storeLocal(byteArrayRefRef[0]); + } + a.loadLocal(byteArrayRefRef[0]); + valueRefRef[0] = byteArrayRefRef[0]; + } else { + throw notSupported(info.getPropertyName(), storageType.getFullName()); + } + + TypeDesc[] params = {TypeDesc.forClass(byte[].class), TypeDesc.INT, refType}; + a.invokeStatic(className, methodName, TypeDesc.INT, params); + + return 0; + } + } + + /** + * Push decoding instanceVar to stack in preparation to calling + * storePropertyValue. + * + * @param ordinal zero-based property ordinal, used only if instanceVar + * refers to an object array. + * @param instanceVar local variable referencing Storable instance, + * defaults to "this" if null. If variable type is an Object array, then + * property values are written to the runtime value of this array instead + * of a Storable instance. + * @see #storePropertyValue storePropertyValue + */ + protected void pushDecodingInstanceVar(CodeAssembler a, int ordinal, + LocalVariable instanceVar) { + if (instanceVar == null) { + // Push this to stack in preparation for storing a property. + a.loadThis(); + } else if (instanceVar.getType() != TypeDesc.forClass(Object[].class)) { + // Push reference to stack in preparation for storing a property. + a.loadLocal(instanceVar); + } else { + // Push array and index to stack in preparation for storing a property. + a.loadLocal(instanceVar); + a.loadConstant(ordinal); + } + } + + /** + * Generates code to store a property value into an instance which is + * already on the operand stack. If instance is an Object array, index into + * array must also be on the operand stack. + * + * @param info info for property to store to + * @param useWriteMethod when true, set property by public write method + * instead of protected field + * @param instanceVar local variable referencing Storable instance, + * defaults to "this" if null. If variable type is an Object array, then + * property values are written to the runtime value of this array instead + * of a Storable instance. + * @param adapterInstanceClass class containing static references to + * adapter instances - defaults to instanceVar + * @see #pushDecodingInstanceVar pushDecodingInstanceVar + */ + protected void storePropertyValue(CodeAssembler a, StorablePropertyInfo info, + boolean useWriteMethod, + LocalVariable instanceVar, + Class adapterInstanceClass) { + TypeDesc type = info.getPropertyType(); + TypeDesc storageType = info.getStorageType(); + + boolean isObjectArrayInstanceVar = instanceVar != null + && instanceVar.getType() == TypeDesc.forClass(Object[].class); + + boolean useAdapterInstance = adapterInstanceClass != null + && info.getToStorageAdapter() != null + && (useWriteMethod || isObjectArrayInstanceVar); + + if (useAdapterInstance) { + // Push adapter instance to adapt property value. It must be on the + // stack before the property value, so swap. + + // Store unadapted property to temp var in order to be swapped. + LocalVariable temp = a.createLocalVariable(null, storageType); + a.storeLocal(temp); + + String fieldName = + info.getPropertyName() + StorableGenerator.ADAPTER_FIELD_ELEMENT + 0; + TypeDesc adapterType = TypeDesc.forClass + (info.getToStorageAdapter().getDeclaringClass()); + a.loadStaticField + (TypeDesc.forClass(adapterInstanceClass), fieldName, adapterType); + + a.loadLocal(temp); + a.invoke(info.getFromStorageAdapter()); + + // Stack now contains property adapted to its publicly declared type. + } + + if (instanceVar == null) { + if (useWriteMethod) { + info.addInvokeWriteMethod(a); + } else { + // Set property value directly to protected field of instance. + if (info.getToStorageAdapter() == null) { + a.storeField(info.getPropertyName(), type); + } else { + // Invoke adapter method. + a.invokeVirtual(info.getWriteMethodName() + '$', + null, new TypeDesc[] {storageType}); + } + } + } else if (!isObjectArrayInstanceVar) { + TypeDesc instanceVarType = instanceVar.getType(); + + // Drop properties that are missing or whose types are incompatible. + doDrop: { + Class instanceVarClass = instanceVarType.toClass(); + if (instanceVarClass != null) { + Map props = + BeanIntrospector.getAllProperties(instanceVarClass); + BeanProperty prop = props.get(info.getPropertyName()); + if (prop != null) { + if (prop.getType() == type.toClass()) { + break doDrop; + } + // Types differ, but if primitive types, perform conversion. + TypeDesc primType = type.toPrimitiveType(); + if (primType != null) { + TypeDesc propType = TypeDesc.forClass(prop.getType()); + TypeDesc primPropType = propType.toPrimitiveType(); + if (primPropType != null) { + // Apply conversion and store property. + a.convert(type, propType); + type = propType; + break doDrop; + } + } + } + } + + // Drop missing or incompatible property. + if (storageType.isDoubleWord()) { + a.pop2(); + } else { + a.pop(); + } + return; + } + + if (useWriteMethod) { + info.addInvokeWriteMethod(a, instanceVarType); + } else { + // Set property value directly to protected field of referenced + // instance. Assumes code is being defined in the same package + // or a subclass. + if (info.getToStorageAdapter() == null) { + a.storeField(instanceVarType, info.getPropertyName(), type); + } else { + // Invoke adapter method. + a.invokeVirtual(instanceVarType, info.getWriteMethodName() + '$', + null, new TypeDesc[] {storageType}); + } + } + } else { + // Set property value to object array. No need to check if we + // should call a write method because arrays don't have write + // methods. + if (type.isPrimitive()) { + a.convert(type, type.toObjectType()); + } + a.storeToArray(TypeDesc.OBJECT); + } + } + + /** + * Generates code that ensures a matching generation value exists in the + * byte array referenced by the local variable, throwing a + * CorruptEncodingException otherwise. + * + * @param generation if less than zero, no code is generated + */ + private void decodeGeneration(CodeAssembler a, LocalVariable encodedVar, + int offset, int generation, Label altGenerationHandler) + { + if (offset < 0) { + throw new IllegalArgumentException(); + } + if (generation < 0) { + return; + } + + LocalVariable actualGeneration = a.createLocalVariable(null, TypeDesc.INT); + a.loadLocal(encodedVar); + a.loadConstant(offset); + a.loadFromArray(TypeDesc.BYTE); + a.storeLocal(actualGeneration); + a.loadLocal(actualGeneration); + Label compareGeneration = a.createLabel(); + a.ifZeroComparisonBranch(compareGeneration, ">="); + + // Decode four byte generation format. + a.loadLocal(actualGeneration); + a.loadConstant(24); + a.math(Opcode.ISHL); + a.loadConstant(0x7fffffff); + a.math(Opcode.IAND); + for (int i=1; i<4; i++) { + a.loadLocal(encodedVar); + a.loadConstant(offset + i); + a.loadFromArray(TypeDesc.BYTE); + a.loadConstant(0xff); + a.math(Opcode.IAND); + int shift = 8 * (3 - i); + if (shift > 0) { + a.loadConstant(shift); + a.math(Opcode.ISHL); + } + a.math(Opcode.IOR); + } + a.storeLocal(actualGeneration); + + compareGeneration.setLocation(); + + a.loadConstant(generation); + a.loadLocal(actualGeneration); + Label generationMatches = a.createLabel(); + a.ifComparisonBranch(generationMatches, "=="); + + if (altGenerationHandler != null) { + a.loadLocal(actualGeneration); + a.branch(altGenerationHandler); + } else { + // Throw CorruptEncodingException. + + TypeDesc corruptEncodingEx = TypeDesc.forClass(CorruptEncodingException.class); + a.newObject(corruptEncodingEx); + a.dup(); + a.loadConstant(generation); // expected generation + a.loadLocal(actualGeneration); // actual generation + a.invokeConstructor(corruptEncodingEx, new TypeDesc[] {TypeDesc.INT, TypeDesc.INT}); + a.throwObject(); + } + + generationMatches.setLocation(); + } +} diff --git a/src/main/java/com/amazon/carbonado/raw/GenericInstanceFactory.java b/src/main/java/com/amazon/carbonado/raw/GenericInstanceFactory.java new file mode 100644 index 0000000..a9c535d --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/GenericInstanceFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; + +/** + * Can be used with {@link com.amazon.carbonado.util.QuickConstructorGenerator} + * for instantiating generic storable instances. + * + * @author Brian S O'Neill + */ +public interface GenericInstanceFactory { + Storable instantiate(RawSupport support); + + Storable instantiate(RawSupport support, byte[] key, byte[] value) + throws FetchException; +} diff --git a/src/main/java/com/amazon/carbonado/raw/GenericPropertyInfo.java b/src/main/java/com/amazon/carbonado/raw/GenericPropertyInfo.java new file mode 100644 index 0000000..8aa92ee --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/GenericPropertyInfo.java @@ -0,0 +1,60 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import java.lang.reflect.Method; + +import org.cojen.classfile.TypeDesc; + +/** + * Minimal information required by {@link GenericEncodingStrategy} to encode + * and decode a storable property or layout property. + * + * @author Brian S O'Neill + */ +public interface GenericPropertyInfo { + String getPropertyName(); + + /** + * Returns the user specified property type. + */ + TypeDesc getPropertyType(); + + /** + * Returns the storage supported type. If it differs from the property + * type, then adapter methods must also exist. + */ + TypeDesc getStorageType(); + + boolean isNullable(); + + boolean isLob(); + + /** + * Returns the optional method used to adapt the property from the + * storage supported type to the user visible type. + */ + Method getFromStorageAdapter(); + + /** + * Returns the optional method used to adapt the property from the user + * visible type to the storage supported type. + */ + Method getToStorageAdapter(); +} diff --git a/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java b/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java new file mode 100644 index 0000000..f572dd8 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java @@ -0,0 +1,813 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.Map; + +import org.cojen.classfile.ClassFile; +import org.cojen.classfile.CodeBuilder; +import org.cojen.classfile.Label; +import org.cojen.classfile.LocalVariable; +import org.cojen.classfile.MethodInfo; +import org.cojen.classfile.Modifiers; +import org.cojen.classfile.TypeDesc; +import org.cojen.util.ClassInjector; +import org.cojen.util.IntHashMap; +import org.cojen.util.KeyFactory; +import org.cojen.util.SoftValuedHashMap; + +import com.amazon.carbonado.CorruptEncodingException; +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.FetchNoneException; +import com.amazon.carbonado.RepositoryException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.SupportException; +import com.amazon.carbonado.info.Direction; +import com.amazon.carbonado.info.OrderedProperty; +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.layout.Layout; +import com.amazon.carbonado.spi.CodeBuilderUtil; +import com.amazon.carbonado.util.ThrowUnchecked; +import com.amazon.carbonado.util.QuickConstructorGenerator; + +/** + * Generic codec that supports any kind of storable by auto-generating and + * caching storable implementations. + * + * @author Brian S O'Neill + * @see GenericStorableCodecFactory + */ +public class GenericStorableCodec implements StorableCodec { + private static final String BLANK_KEY_FIELD_NAME = "blankKey$"; + private static final String CODEC_FIELD_NAME = "codec$"; + private static final String ASSIGN_CODEC_METHOD_NAME = "assignCodec$"; + + // Maps GenericEncodingStrategy instances to GenericStorableCodec instances. + private static final Map cCache = new SoftValuedHashMap(); + + /** + * Returns an instance of the codec. The Storable type itself may be an + * interface or a class. If it is a class, then it must not be final, and + * it must have a public, no-arg constructor. + * + * @param isMaster when true, version properties and sequences are managed + * @param layout when non-null, encode a storable layout generation + * value in one or four bytes. Generation 0..127 is encoded in one byte, and + * 128..max is encoded in four bytes, with the most significant bit set. + * @throws SupportException if Storable is not supported + * @throws amazon.carbonado.MalformedTypeException if Storable type is not well-formed + * @throws IllegalArgumentException if type is null + */ + @SuppressWarnings("unchecked") + static synchronized GenericStorableCodec getInstance + (GenericStorableCodecFactory factory, + GenericEncodingStrategy encodingStrategy, boolean isMaster, Layout layout) + throws SupportException + { + Object key; + if (layout == null) { + key = KeyFactory.createKey(new Object[] {encodingStrategy, isMaster}); + } else { + key = KeyFactory.createKey + (new Object[] {encodingStrategy, isMaster, factory, layout.getGeneration()}); + } + + GenericStorableCodec codec = (GenericStorableCodec) cCache.get(key); + if (codec == null) { + codec = new GenericStorableCodec + (factory, + encodingStrategy.getType(), + generateStorable(encodingStrategy, isMaster, layout), + encodingStrategy, + layout); + cCache.put(key, codec); + } + + return codec; + } + + @SuppressWarnings("unchecked") + private static Class generateStorable + (GenericEncodingStrategy encodingStrategy, boolean isMaster, Layout layout) + throws SupportException + { + final Class storableClass = encodingStrategy.getType(); + final Class abstractClass = + RawStorableGenerator.getAbstractClass(storableClass, isMaster); + final int generation = layout == null ? -1 : layout.getGeneration(); + + ClassInjector ci = ClassInjector.create + (storableClass.getName(), abstractClass.getClassLoader()); + + ClassFile cf = new ClassFile(ci.getClassName(), abstractClass); + cf.markSynthetic(); + cf.setSourceFile(GenericStorableCodec.class.getName()); + cf.setTarget("1.5"); + + // Declare some types. + final TypeDesc storageType = TypeDesc.forClass(Storage.class); + final TypeDesc rawSupportType = TypeDesc.forClass(RawSupport.class); + final TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + final TypeDesc[] byteArrayParam = {byteArrayType}; + final TypeDesc codecType = TypeDesc.forClass(GenericStorableCodec.class); + final TypeDesc decoderType = TypeDesc.forClass(Decoder.class); + final TypeDesc weakRefType = TypeDesc.forClass(WeakReference.class); + + // If Layout provided, then keep a (weak) static reference to this + // GenericStorableCodec in order to get decoders for different + // generations. It is assigned a value after the class is loaded via a + // public static method. It can only be assigned once. + if (layout != null) { + cf.addField(Modifiers.PRIVATE.toStatic(true), CODEC_FIELD_NAME, weakRefType); + MethodInfo mi = cf.addMethod(Modifiers.PUBLIC.toStatic(true), ASSIGN_CODEC_METHOD_NAME, + null, new TypeDesc[] {weakRefType}); + CodeBuilder b = new CodeBuilder(mi); + b.loadStaticField(CODEC_FIELD_NAME, weakRefType); + Label done = b.createLabel(); + b.ifNullBranch(done, false); + b.loadLocal(b.getParameter(0)); + b.storeStaticField(CODEC_FIELD_NAME, weakRefType); + done.setLocation(); + b.returnVoid(); + } + + // Add constructor that accepts a RawSupport. + { + TypeDesc[] params = {rawSupportType}; + MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.invokeSuperConstructor(params); + b.returnVoid(); + } + + // Add constructor that accepts a RawSupport, an encoded key, and an + // encoded data. + { + TypeDesc[] params = {rawSupportType, byteArrayType, byteArrayType}; + MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.loadLocal(b.getParameter(1)); + b.loadLocal(b.getParameter(2)); + b.invokeSuperConstructor(params); + b.returnVoid(); + } + + // Implement protected abstract methods inherited from parent class. + + // byte[] encodeKey() + { + // Encode the primary key into a byte array that supports correct + // ordering. No special key comparator is needed. + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.ENCODE_KEY_METHOD_NAME, + byteArrayType, null); + CodeBuilder b = new CodeBuilder(mi); + + // TODO: Consider caching generated key. Rebuild if null or if pk is dirty. + + // assembler = b + // properties = null (defaults to all key properties) + // instanceVar = null (null means "this") + // adapterInstanceClass = null (null means use instanceVar, in this case is "this") + // useReadMethods = false (will read fields directly) + // partialStartVar = null (only support encoding all properties) + // partialEndVar = null (only support encoding all properties) + LocalVariable encodedVar = + encodingStrategy.buildKeyEncoding(b, null, null, null, false, null, null); + + b.loadLocal(encodedVar); + b.returnValue(byteArrayType); + } + + // byte[] encodeData() + { + // Encoding non-primary key data properties. + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.ENCODE_DATA_METHOD_NAME, + byteArrayType, null); + CodeBuilder b = new CodeBuilder(mi); + + // assembler = b + // properties = null (defaults to all non-key properties) + // instanceVar = null (null means "this") + // adapterInstanceClass = null (null means use instanceVar, in this case is "this") + // useReadMethods = false (will read fields directly) + // generation = generation + LocalVariable encodedVar = + encodingStrategy.buildDataEncoding(b, null, null, null, false, generation); + + b.loadLocal(encodedVar); + b.returnValue(byteArrayType); + } + + // void decodeKey(byte[]) + { + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.DECODE_KEY_METHOD_NAME, + null, byteArrayParam); + CodeBuilder b = new CodeBuilder(mi); + + // assembler = b + // properties = null (defaults to all key properties) + // instanceVar = null (null means "this") + // adapterInstanceClass = null (null means use instanceVar, in this case is "this") + // useWriteMethods = false (will set fields directly) + // encodedVar = references byte array with encoded key + encodingStrategy.buildKeyDecoding(b, null, null, null, false, b.getParameter(0)); + + b.returnVoid(); + } + + // void decodeData(byte[]) + { + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.DECODE_DATA_METHOD_NAME, + null, byteArrayParam); + CodeBuilder b = new CodeBuilder(mi); + Label altGenerationHandler = b.createLabel(); + + // assembler = b + // properties = null (defaults to all non-key properties) + // instanceVar = null (null means "this") + // adapterInstanceClass = null (null means use instanceVar, in this case is "this") + // useWriteMethods = false (will set fields directly) + // generation = generation + // altGenerationHandler = altGenerationHandler + // encodedVar = references byte array with encoded data + encodingStrategy.buildDataDecoding + (b, null, null, null, false, generation, altGenerationHandler, b.getParameter(0)); + + b.returnVoid(); + + // Support decoding alternate generation. + + altGenerationHandler.setLocation(); + LocalVariable actualGeneration = b.createLocalVariable(null, TypeDesc.INT); + b.storeLocal(actualGeneration); + + b.loadStaticField(CODEC_FIELD_NAME, weakRefType); + b.invokeVirtual(weakRefType, "get", TypeDesc.OBJECT, null); + b.dup(); + Label haveCodec = b.createLabel(); + b.ifNullBranch(haveCodec, false); + + // Codec got reclaimed, which is unlikely to happen during normal + // use since it must be referenced by the storage object. + b.pop(); // Don't need the duped codec instance. + CodeBuilderUtil.throwException(b, IllegalStateException.class, "Codec missing"); + + haveCodec.setLocation(); + b.checkCast(codecType); + b.loadLocal(actualGeneration); + b.invokeVirtual(codecType, "getDecoder", decoderType, new TypeDesc[] {TypeDesc.INT}); + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.invokeInterface(decoderType, "decode", null, + new TypeDesc[] {TypeDesc.forClass(Storable.class), byteArrayType}); + + b.returnVoid(); + } + + return ci.defineClass(cf); + } + + private final GenericStorableCodecFactory mFactory; + + private final Class mType; + + private final Class mStorableClass; + + // Weakly reference the encoding strategy because of the way + // GenericStorableCodec instances are cached in a SoftValuedHashMap. + // GenericStorableCodec can still be reclaimed by the garbage collector. + private final WeakReference> mEncodingStrategy; + + private final GenericInstanceFactory mInstanceFactory; + + private final SearchKeyFactory mPrimaryKeyFactory; + + // Maps OrderedProperty[] keys to SearchKeyFactory instances. + private final Map mSearchKeyFactories = new SoftValuedHashMap(); + + private final Layout mLayout; + + // Maps layout generations to Decoders. + private IntHashMap mDecoders; + + private GenericStorableCodec(GenericStorableCodecFactory factory, + Class type, Class storableClass, + GenericEncodingStrategy encodingStrategy, + Layout layout) { + mFactory = factory; + mType = type; + mStorableClass = storableClass; + mEncodingStrategy = new WeakReference>(encodingStrategy); + mInstanceFactory = QuickConstructorGenerator + .getInstance(storableClass, GenericInstanceFactory.class); + mPrimaryKeyFactory = getSearchKeyFactory(encodingStrategy.gatherAllKeyProperties()); + mLayout = layout; + + if (layout != null) { + try { + // Assign static reference back to this codec. + Method m = storableClass.getMethod + (ASSIGN_CODEC_METHOD_NAME, WeakReference.class); + m.invoke(null, new WeakReference(this)); + } catch (Exception e) { + ThrowUnchecked.fireFirstDeclaredCause(e); + } + } + } + + /** + * Returns the type of Storable that code is generated for. + */ + public final Class getStorableType() { + return mType; + } + + /** + * Instantiate a Storable with no key or value defined yet. + * + * @param support binds generated storable with a storage layer + */ + @SuppressWarnings("unchecked") + public S instantiate(RawSupport support) { + return (S) mInstanceFactory.instantiate(support); + } + + /** + * Instantiate a Storable with a specific key and value. + * + * @param support binds generated storable with a storage layer + */ + @SuppressWarnings("unchecked") + public S instantiate(RawSupport support, byte[] key, byte[] value) + throws FetchException + { + return (S) mInstanceFactory.instantiate(support, key, value); + } + + public StorableIndex getPrimaryKeyIndex() { + return getEncodingStrategy().getPrimaryKeyIndex(); + } + + public int getPrimaryKeyPrefixLength() { + return getEncodingStrategy().getConstantKeyPrefixLength(); + } + + public byte[] encodePrimaryKey(S storable) { + return mPrimaryKeyFactory.encodeSearchKey(storable); + } + + public byte[] encodePrimaryKey(S storable, int rangeStart, int rangeEnd) { + return mPrimaryKeyFactory.encodeSearchKey(storable, rangeStart, rangeEnd); + } + + public byte[] encodePrimaryKey(Object[] values) { + return mPrimaryKeyFactory.encodeSearchKey(values); + } + + public byte[] encodePrimaryKey(Object[] values, int rangeStart, int rangeEnd) { + return mPrimaryKeyFactory.encodeSearchKey(values, rangeStart, rangeEnd); + } + + public byte[] encodePrimaryKeyPrefix() { + return mPrimaryKeyFactory.encodeSearchKeyPrefix(); + } + + /** + * Returns a concrete Storable implementation, which is fully + * thread-safe. It has two constructors defined: + * + *

+     * public <init>(Storage, RawSupport);
+     *
+     * public <init>(Storage, RawSupport, byte[] key, byte[] value);
+     * 
+ * + * Convenience methods are provided in this class to instantiate the + * generated Storable. + */ + public Class getStorableClass() { + return mStorableClass; + } + + /** + * Returns a search key factory, which is useful for implementing indexes + * and queries. + * + * @param properties properties to build the search key from + */ + @SuppressWarnings("unchecked") + public SearchKeyFactory getSearchKeyFactory(OrderedProperty[] properties) { + // This KeyFactory makes arrays work as hashtable keys. + Object key = org.cojen.util.KeyFactory.createKey(properties); + + synchronized (mSearchKeyFactories) { + SearchKeyFactory factory = (SearchKeyFactory) mSearchKeyFactories.get(key); + if (factory == null) { + factory = generateSearchKeyFactory(properties); + mSearchKeyFactories.put(key, factory); + } + return factory; + } + } + + /** + * Returns a data decoder for the given generation. + * + * @throws FetchNoneException if generation is unknown + */ + public Decoder getDecoder(int generation) throws FetchNoneException, FetchException { + try { + synchronized (mLayout) { + IntHashMap decoders = mDecoders; + if (decoders == null) { + mDecoders = decoders = new IntHashMap(); + } + Decoder decoder = (Decoder) decoders.get(generation); + if (decoder == null) { + decoder = generateDecoder(generation); + mDecoders.put(generation, decoder); + } + return decoder; + } + } catch (NullPointerException e) { + if (mLayout == null) { + throw new FetchNoneException("Layout evolution not supported"); + } + throw e; + } + } + + private GenericEncodingStrategy getEncodingStrategy() { + // Should never be null, even though it is weakly referenced. As long + // as this class can be reached by the cache, the encoding strategy + // object exists since it is the cache key. + return mEncodingStrategy.get(); + } + + @SuppressWarnings("unchecked") + private SearchKeyFactory generateSearchKeyFactory(OrderedProperty[] properties) { + GenericEncodingStrategy encodingStrategy = getEncodingStrategy(); + + ClassInjector ci; + { + StringBuilder b = new StringBuilder(); + b.append(mType.getName()); + b.append('$'); + for (OrderedProperty property : properties) { + if (property.getDirection() == Direction.UNSPECIFIED) { + property = property.direction(Direction.ASCENDING); + } + try { + property.appendTo(b); + } catch (java.io.IOException e) { + // Not gonna happen + } + } + String prefix = b.toString(); + ci = ClassInjector.create(prefix, mStorableClass.getClassLoader()); + } + + ClassFile cf = new ClassFile(ci.getClassName()); + cf.addInterface(SearchKeyFactory.class); + cf.markSynthetic(); + cf.setSourceFile(GenericStorableCodec.class.getName()); + cf.setTarget("1.5"); + + // Add public no-arg constructor. + cf.addDefaultConstructor(); + + // Declare some types. + final TypeDesc storableType = TypeDesc.forClass(Storable.class); + final TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + final TypeDesc objectArrayType = TypeDesc.forClass(Object[].class); + final TypeDesc instanceType = TypeDesc.forClass(mStorableClass); + + // Define encodeSearchKey(Storable). + try { + MethodInfo mi = cf.addMethod + (Modifiers.PUBLIC, "encodeSearchKey", byteArrayType, + new TypeDesc[] {storableType}); + CodeBuilder b = new CodeBuilder(mi); + b.loadLocal(b.getParameter(0)); + b.checkCast(instanceType); + LocalVariable instanceVar = b.createLocalVariable(null, instanceType); + b.storeLocal(instanceVar); + + // assembler = b + // properties = properties to encode + // instanceVar = instanceVar which references storable instance + // adapterInstanceClass = null (null means use instanceVar) + // useReadMethods = false (will read fields directly) + // partialStartVar = null (only support encoding all properties) + // partialEndVar = null (only support encoding all properties) + LocalVariable encodedVar = encodingStrategy.buildKeyEncoding + (b, properties, instanceVar, null, false, null, null); + + b.loadLocal(encodedVar); + b.returnValue(byteArrayType); + } catch (SupportException e) { + // Shouldn't happen since all properties were checked in order + // to create this StorableCodec. + throw new UndeclaredThrowableException(e); + } + + // Define encodeSearchKey(Storable, int, int). + try { + MethodInfo mi = cf.addMethod + (Modifiers.PUBLIC, "encodeSearchKey", byteArrayType, + new TypeDesc[] {storableType, TypeDesc.INT, TypeDesc.INT}); + CodeBuilder b = new CodeBuilder(mi); + b.loadLocal(b.getParameter(0)); + b.checkCast(instanceType); + LocalVariable instanceVar = b.createLocalVariable(null, instanceType); + b.storeLocal(instanceVar); + + // assembler = b + // properties = properties to encode + // instanceVar = instanceVar which references storable instance + // adapterInstanceClass = null (null means use instanceVar) + // useReadMethods = false (will read fields directly) + // partialStartVar = int parameter 1, references start property index + // partialEndVar = int parameter 2, references end property index + LocalVariable encodedVar = encodingStrategy.buildKeyEncoding + (b, properties, instanceVar, null, false, b.getParameter(1), b.getParameter(2)); + + b.loadLocal(encodedVar); + b.returnValue(byteArrayType); + } catch (SupportException e) { + // Shouldn't happen since all properties were checked in order + // to create this StorableCodec. + throw new UndeclaredThrowableException(e); + } + + // The Storable class that we generated earlier is a subclass of the + // abstract class defined by StorableGenerator. StorableGenerator + // creates static final adapter instances, with protected + // access. Calling getSuperclass results in the exact class that + // StorableGenerator made, which is where the fields are. + final Class adapterInstanceClass = getStorableClass().getSuperclass(); + + // Define encodeSearchKey(Object[] values). + try { + MethodInfo mi = cf.addMethod + (Modifiers.PUBLIC, "encodeSearchKey", byteArrayType, + new TypeDesc[] {objectArrayType}); + CodeBuilder b = new CodeBuilder(mi); + + // assembler = b + // properties = properties to encode + // instanceVar = parameter 0, an object array + // adapterInstanceClass = adapterInstanceClass - see comment above + // useReadMethods = false (will read fields directly) + // partialStartVar = null (only support encoding all properties) + // partialEndVar = null (only support encoding all properties) + LocalVariable encodedVar = encodingStrategy.buildKeyEncoding + (b, properties, b.getParameter(0), adapterInstanceClass, false, null, null); + + b.loadLocal(encodedVar); + b.returnValue(byteArrayType); + } catch (SupportException e) { + // Shouldn't happen since all properties were checked in order + // to create this StorableCodec. + throw new UndeclaredThrowableException(e); + } + + // Define encodeSearchKey(Object[] values, int, int). + try { + MethodInfo mi = cf.addMethod + (Modifiers.PUBLIC, "encodeSearchKey", byteArrayType, + new TypeDesc[] {objectArrayType, TypeDesc.INT, TypeDesc.INT}); + CodeBuilder b = new CodeBuilder(mi); + + // assembler = b + // properties = properties to encode + // instanceVar = parameter 0, an object array + // adapterInstanceClass = adapterInstanceClass - see comment above + // useReadMethods = false (will read fields directly) + // partialStartVar = int parameter 1, references start property index + // partialEndVar = int parameter 2, references end property index + LocalVariable encodedVar = encodingStrategy.buildKeyEncoding + (b, properties, b.getParameter(0), adapterInstanceClass, + false, b.getParameter(1), b.getParameter(2)); + + b.loadLocal(encodedVar); + b.returnValue(byteArrayType); + } catch (SupportException e) { + // Shouldn't happen since all properties were checked in order + // to create this StorableCodec. + throw new UndeclaredThrowableException(e); + } + + // Define encodeSearchKeyPrefix(). + try { + MethodInfo mi = cf.addMethod + (Modifiers.PUBLIC, "encodeSearchKeyPrefix", byteArrayType, null); + CodeBuilder b = new CodeBuilder(mi); + + if (encodingStrategy.getKeyPrefixPadding() == 0 && + encodingStrategy.getKeySuffixPadding() == 0) { + // Return null instead of a zero-length array. + b.loadNull(); + b.returnValue(byteArrayType); + } else { + // Build array once and re-use. Trust that no one modifies it. + cf.addField(Modifiers.PRIVATE.toStatic(true).toFinal(true), + BLANK_KEY_FIELD_NAME, byteArrayType); + b.loadStaticField(BLANK_KEY_FIELD_NAME, byteArrayType); + b.returnValue(byteArrayType); + + // Create static initializer to set field. + mi = cf.addInitializer(); + b = new CodeBuilder(mi); + + // assembler = b + // properties = no parameters - we just want the key prefix + // instanceVar = null (no parameters means we don't need this) + // adapterInstanceClass = null (no parameters means we don't need this) + // useReadMethods = false (no parameters means we don't need this) + // partialStartVar = null (no parameters means we don't need this) + // partialEndVar = null (no parameters means we don't need this) + LocalVariable encodedVar = encodingStrategy.buildKeyEncoding + (b, new OrderedProperty[0], null, null, false, null, null); + + b.loadLocal(encodedVar); + b.storeStaticField(BLANK_KEY_FIELD_NAME, byteArrayType); + b.returnVoid(); + } + } catch (SupportException e) { + // Shouldn't happen since all properties were checked in order + // to create this StorableCodec. + throw new UndeclaredThrowableException(e); + } + + Class clazz = ci.defineClass(cf); + try { + return clazz.newInstance(); + } catch (InstantiationException e) { + throw new UndeclaredThrowableException(e); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } + } + + private Decoder generateDecoder(int generation) throws FetchException { + // Create an encoding strategy against the reconstructed storable. + GenericEncodingStrategy altStrategy; + try { + Class altStorable = mLayout.getGeneration(generation) + .reconstruct(mStorableClass.getClassLoader()); + altStrategy = mFactory.createStrategy(altStorable, null); + } catch (RepositoryException e) { + throw new CorruptEncodingException(e); + } + + ClassInjector ci = ClassInjector.create(mType.getName(), mStorableClass.getClassLoader()); + ClassFile cf = new ClassFile(ci.getClassName()); + cf.addInterface(Decoder.class); + cf.markSynthetic(); + cf.setSourceFile(GenericStorableCodec.class.getName()); + cf.setTarget("1.5"); + + // Add public no-arg constructor. + cf.addDefaultConstructor(); + + // Declare some types. + final TypeDesc storableType = TypeDesc.forClass(Storable.class); + final TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + + // Define the required decode method. + MethodInfo mi = cf.addMethod + (Modifiers.PUBLIC, "decode", null, new TypeDesc[] {storableType, byteArrayType}); + CodeBuilder b = new CodeBuilder(mi); + + LocalVariable uncastDestVar = b.getParameter(0); + b.loadLocal(uncastDestVar); + LocalVariable destVar = b.createLocalVariable(null, TypeDesc.forClass(mStorableClass)); + b.checkCast(destVar.getType()); + b.storeLocal(destVar); + LocalVariable dataVar = b.getParameter(1); + + // assembler = b + // properties = null (defaults to all non-key properties) + // instanceVar = "dest" storable + // adapterInstanceClass = null (null means use instanceVar, in this case is "dest") + // useWriteMethods = false (will set fields directly) + // generation = generation + // altGenerationHandler = null (generation should match) + // encodedVar = "data" byte array + try { + altStrategy.buildDataDecoding + (b, null, destVar, null, false, generation, null, dataVar); + } catch (SupportException e) { + throw new CorruptEncodingException(e); + } + + b.returnVoid(); + + Class clazz = ci.defineClass(cf); + try { + return clazz.newInstance(); + } catch (InstantiationException e) { + throw new UndeclaredThrowableException(e); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } + } + + /** + * Creates custom raw search keys for {@link Storable} types. It is + * intended for supporting queries and indexes. + */ + public interface SearchKeyFactory { + /** + * Build a search key by extracting all the desired properties from the + * given storable. + * + * @param storable extract a subset of properties from this instance + * @return raw search key + */ + byte[] encodeSearchKey(S storable); + + /** + * Build a search key by extracting all the desired properties from the + * given storable. + * + * @param storable extract a subset of properties from this instance + * @param rangeStart index of first property to use. Its value must be less + * than the count of properties used by this factory. + * @param rangeEnd index of last property to use, exlusive. Its value must + * be less than or equal to the count of properties used by this factory. + * @return raw search key + */ + byte[] encodeSearchKey(S storable, int rangeStart, int rangeEnd); + + /** + * Build a search key by supplying property values without a storable. + * + * @param values values to build into a key. It must be long enough to + * accommodate all of properties used by this factory. + * @return raw search key + */ + byte[] encodeSearchKey(Object[] values); + + /** + * Build a search key by supplying property values without a storable. + * + * @param values values to build into a key. The length may be less than + * the amount of properties used by this factory. It must not be less than the + * difference between rangeStart and rangeEnd. + * @param rangeStart index of first property to use. Its value must be less + * than the count of properties used by this factory. + * @param rangeEnd index of last property to use, exlusive. Its value must + * be less than or equal to the count of properties used by this factory. + * @return raw search key + */ + byte[] encodeSearchKey(Object[] values, int rangeStart, int rangeEnd); + + /** + * Returns the search key for when there are no values. Returned value + * may be null. + */ + byte[] encodeSearchKeyPrefix(); + } + + /** + * Used for decoding different generations of Storable. + */ + public interface Decoder { + /** + * @param dest storable to receive decoded properties + * @param data decoded into properties, some of which may be dropped if + * destination storable doesn't have it + */ + void decode(S dest, byte[] data); + } +} diff --git a/src/main/java/com/amazon/carbonado/raw/GenericStorableCodecFactory.java b/src/main/java/com/amazon/carbonado/raw/GenericStorableCodecFactory.java new file mode 100644 index 0000000..ab640e6 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/GenericStorableCodecFactory.java @@ -0,0 +1,76 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.SupportException; + +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.layout.Layout; + +/** + * Factory for generic codec that supports any kind of storable by + * auto-generating and caching storable implementations. + * + * @author Brian S O'Neill + */ +public class GenericStorableCodecFactory implements StorableCodecFactory { + public GenericStorableCodecFactory() { + } + + /** + * Returns null to let repository decide what the name should be. + */ + public String getStorageName(Class type) throws SupportException { + return null; + } + + /** + * @param type type of storable to create codec for + * @param pkIndex suggested index for primary key (optional) + * @param isMaster when true, version properties and sequences are managed + * @param layout when non-null, encode a storable layout generation + * value in one or four bytes. Generation 0..127 is encoded in one byte, and + * 128..max is encoded in four bytes, with the most significant bit set. + * @throws SupportException if type is not supported + */ + @SuppressWarnings("unchecked") + public GenericStorableCodec createCodec(Class type, + StorableIndex pkIndex, + boolean isMaster, + Layout layout) + throws SupportException + { + return GenericStorableCodec.getInstance + (this, createStrategy(type, pkIndex), isMaster, layout); + } + + /** + * Override to return a different EncodingStrategy. + * + * @param type type of Storable to generate code for + * @param pkIndex specifies sequence and ordering of key properties (optional) + */ + protected GenericEncodingStrategy createStrategy + (Class type, StorableIndex pkIndex) + throws SupportException + { + return new GenericEncodingStrategy(type, pkIndex); + } +} diff --git a/src/main/java/com/amazon/carbonado/raw/KeyDecoder.java b/src/main/java/com/amazon/carbonado/raw/KeyDecoder.java new file mode 100644 index 0000000..a3254b1 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/KeyDecoder.java @@ -0,0 +1,646 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import com.amazon.carbonado.CorruptEncodingException; + +import static com.amazon.carbonado.raw.KeyEncoder.*; + +/** + * A very low-level class that decodes key components encoded by methods of + * {@link KeyEncoder}. + * + * @author Brian S O'Neill + */ +public class KeyDecoder extends DataDecoder { + + /** + * Decodes a signed integer from exactly 4 bytes, as encoded for descending + * order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed integer value + */ + public static int decodeIntDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + return ~decodeInt(src, srcOffset); + } + + /** + * Decodes a signed Integer object from exactly 1 or 5 bytes, as encoded + * for descending order. If null is returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Integer object or null + */ + public static Integer decodeIntegerObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeIntDesc(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed long from exactly 8 bytes, as encoded for descending + * order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed long value + */ + public static long decodeLongDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + return ~decodeLong(src, srcOffset); + } + + /** + * Decodes a signed Long object from exactly 1 or 9 bytes, as encoded for + * descending order. If null is returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Long object or null + */ + public static Long decodeLongObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeLongDesc(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed byte from exactly 1 byte, as encoded for descending + * order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed byte value + */ + public static byte decodeByteDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return (byte)(src[srcOffset] ^ 0x7f); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed Byte object from exactly 1 or 2 bytes, as encoded for + * descending order. If null is returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Byte object or null + */ + public static Byte decodeByteObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeByteDesc(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed short from exactly 2 bytes, as encoded for descending + * order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed short value + */ + public static short decodeShortDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return (short)(((src[srcOffset] << 8) | (src[srcOffset + 1] & 0xff)) ^ 0x7fff); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed Short object from exactly 1 or 3 bytes, as encoded for + * descending order. If null is returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Short object or null + */ + public static Short decodeShortObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeShortDesc(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a char from exactly 2 bytes, as encoded for descending order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return char value + */ + public static char decodeCharDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return (char)~((src[srcOffset] << 8) | (src[srcOffset + 1] & 0xff)); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a Character object from exactly 1 or 3 bytes, as encoded for + * descending order. If null is returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Character object or null + */ + public static Character decodeCharacterObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeCharDesc(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a boolean from exactly 1 byte, as encoded for descending order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return boolean value + */ + public static boolean decodeBooleanDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return src[srcOffset] == 127; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a Boolean object from exactly 1 byte, as encoded for descending + * order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Boolean object or null + */ + public static Boolean decodeBooleanObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + switch (src[srcOffset]) { + case NULL_BYTE_LOW: case NULL_BYTE_HIGH: + return null; + case (byte)127: + return Boolean.TRUE; + default: + return Boolean.FALSE; + } + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a float from exactly 4 bytes, as encoded for descending order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return float value + */ + public static float decodeFloatDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + int bits = decodeFloatBits(src, srcOffset); + if (bits >= 0) { + bits ^= 0x7fffffff; + } + return Float.intBitsToFloat(bits); + } + + /** + * Decodes a Float object from exactly 4 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Float object or null + */ + public static Float decodeFloatObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + int bits = decodeFloatBits(src, srcOffset); + if (bits >= 0) { + bits ^= 0x7fffffff; + } + return bits == 0x7fffffff ? null : Float.intBitsToFloat(bits); + } + + /** + * Decodes a double from exactly 8 bytes, as encoded for descending order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return double value + */ + public static double decodeDoubleDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + long bits = decodeDoubleBits(src, srcOffset); + if (bits >= 0) { + bits ^= 0x7fffffffffffffffL; + } + return Double.longBitsToDouble(bits); + } + + /** + * Decodes a Double object from exactly 8 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Double object or null + */ + public static Double decodeDoubleObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + long bits = decodeDoubleBits(src, srcOffset); + if (bits >= 0) { + bits ^= 0x7fffffffffffffffL; + } + return bits == 0x7fffffffffffffffL ? null : Double.longBitsToDouble(bits); + } + + /** + * Decodes the given byte array as originally encoded for ascending order. + * The decoding stops when any kind of terminator or illegal byte has been + * read. The decoded bytes are stored in valueRef. + * + * @param src source of encoded data + * @param srcOffset offset into encoded data + * @param valueRef decoded byte array is stored in element 0, which may be null + * @return amount of bytes read from source + * @throws CorruptEncodingException if source data is corrupt + */ + public static int decode(byte[] src, int srcOffset, byte[][] valueRef) + throws CorruptEncodingException + { + try { + return decode(src, srcOffset, valueRef, 0); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes the given byte array as originally encoded for descending order. + * The decoding stops when any kind of terminator or illegal byte has been + * read. The decoded bytes are stored in valueRef. + * + * @param src source of encoded data + * @param srcOffset offset into encoded data + * @param valueRef decoded byte array is stored in element 0, which may be null + * @return amount of bytes read from source + * @throws CorruptEncodingException if source data is corrupt + */ + public static int decodeDesc(byte[] src, int srcOffset, byte[][] valueRef) + throws CorruptEncodingException + { + try { + return decode(src, srcOffset, valueRef, -1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * @param xorMask 0 for normal decoding, -1 for descending decoding + */ + private static int decode(byte[] src, int srcOffset, byte[][] valueRef, int xorMask) { + // Scan ahead, looking for terminator. + int srcEnd = srcOffset; + while (true) { + byte b = src[srcEnd++]; + if (-32 <= b && b < 32) { + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + if ((srcEnd - 1) <= srcOffset) { + valueRef[0] = null; + return 1; + } + } + break; + } + } + + if (srcEnd - srcOffset == 1) { + valueRef[0] = EMPTY_BYTE_ARRAY; + return 1; + } + + // Value is decoded from base-32768. + + int valueLength = ((srcEnd - srcOffset - 1) * 120) >> 7; + byte[] value = new byte[valueLength]; + int valueOffset = 0; + + final int originalOffset = srcOffset; + + int accumBits = 0; + int accum = 0; + + while (true) { + int d = (src[srcOffset++] ^ xorMask) & 0xff; + int b; + if (srcOffset == srcEnd || + (b = (src[srcOffset++] ^ xorMask) & 0xff) < 32 || b > 223) { + // Handle special case where one byte was emitted for digit. + d -= 32; + // To produce digit, multiply d by 192 and add 191 to adjust + // for missing remainder. The lower bits are discarded anyhow. + d = (d << 7) + (d << 6) + 191; + + // Shift decoded digit into accumulator. + accumBits += 15; + accum = (accum << 15) | d; + + break; + } + + d -= 32; + // To produce digit, multiply d by 192 and add in remainder. + d = ((d << 7) + (d << 6)) + b - 32; + + // Shift decoded digit into accumulator. + accumBits += 15; + accum = (accum << 15) | d; + + if (accumBits == 15) { + value[valueOffset++] = (byte)(accum >> 7); + } else { + value[valueOffset++] = (byte)(accum >> (accumBits - 8)); + accumBits -= 8; + value[valueOffset++] = (byte)(accum >> (accumBits - 8)); + } + accumBits -= 8; + } + + if (accumBits >= 8 && valueOffset < valueLength) { + value[valueOffset] = (byte)(accum >> (accumBits - 8)); + } + + valueRef[0] = value; + + return srcOffset - originalOffset; + } + + /** + * Decodes an encoded string from the given byte array. + * + * @param src source of encoded data + * @param srcOffset offset into encoded data + * @param valueRef decoded string is stored in element 0, which may be null + * @return amount of bytes read from source + * @throws CorruptEncodingException if source data is corrupt + */ + public static int decodeString(byte[] src, int srcOffset, String[] valueRef) + throws CorruptEncodingException + { + try { + return decodeString(src, srcOffset, valueRef, 0); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes an encoded string from the given byte array as originally + * encoded for descending order. + * + * @param src source of encoded data + * @param srcOffset offset into encoded data + * @param valueRef decoded string is stored in element 0, which may be null + * @return amount of bytes read from source + * @throws CorruptEncodingException if source data is corrupt + */ + public static int decodeStringDesc(byte[] src, int srcOffset, String[] valueRef) + throws CorruptEncodingException + { + try { + return decodeString(src, srcOffset, valueRef, -1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * @param xorMask 0 for normal decoding, -1 for descending decoding + */ + private static int decodeString(byte[] src, int srcOffset, String[] valueRef, int xorMask) + throws CorruptEncodingException + { + // Scan ahead, looking for terminator. + int srcEnd = srcOffset; + while (true) { + byte b = src[srcEnd++]; + if (-2 <= b && b < 2) { + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + if ((srcEnd - 1) <= srcOffset) { + valueRef[0] = null; + return 1; + } + } + break; + } + } + + if (srcEnd - srcOffset == 1) { + valueRef[0] = ""; + return 1; + } + + // Allocate a character array which may be longer than needed once + // bytes are decoded into characters. + char[] value = new char[srcEnd - srcOffset]; + int valueOffset = 0; + + final int originalOffset = srcOffset; + + while (srcOffset < srcEnd) { + int c = (src[srcOffset++] ^ xorMask) & 0xff; + switch (c >> 5) { + case 0: case 1: case 2: case 3: + // 0xxxxxxx + value[valueOffset++] = (char)(c - 2); + break; + case 4: case 5: + // 10xxxxxx xxxxxxxx + + c = c & 0x3f; + // Multiply by 192, add in remainder, remove offset of 2, and de-normalize. + value[valueOffset++] = + (char)((c << 7) + (c << 6) + ((src[srcOffset++] ^ xorMask) & 0xff) + 94); + + break; + case 6: + // 110xxxxx xxxxxxxx xxxxxxxx + + c = c & 0x1f; + // Multiply by 192, add in remainder... + c = (c << 7) + (c << 6) + ((src[srcOffset++] ^ xorMask) & 0xff) - 32; + // ...multiply by 192, add in remainder, remove offset of 2, and de-normalize. + c = (c << 7) + (c << 6) + ((src[srcOffset++] ^ xorMask) & 0xff) + 12382; + + if (c >= 0x10000) { + // Split into surrogate pair. + c -= 0x10000; + value[valueOffset++] = (char)(0xd800 | ((c >> 10) & 0x3ff)); + value[valueOffset++] = (char)(0xdc00 | (c & 0x3ff)); + } else { + value[valueOffset++] = (char)c; + } + + break; + default: + // 111xxxxx + // Illegal. + throw new CorruptEncodingException + ("Corrupt encoded string data (source offset = " + + (srcOffset - 1) + ')'); + } + } + + valueRef[0] = new String(value, 0, valueOffset - 1); + + return srcEnd - originalOffset; + } + + /** + * Decodes the given byte array which was encoded by {@link + * KeyEncoder#encodeSingleDesc}. + */ + public static byte[] decodeSingleDesc(byte[] src) throws CorruptEncodingException { + return decodeSingleDesc(src, 0, 0); + } + + /** + * Decodes the given byte array which was encoded by {@link + * KeyEncoder#encodeSingleDesc}. + * + * @param prefixPadding amount of extra bytes to skip from start of encoded byte array + * @param suffixPadding amount of extra bytes to skip at end of encoded byte array + */ + public static byte[] decodeSingleDesc(byte[] src, int prefixPadding, int suffixPadding) + throws CorruptEncodingException + { + try { + int length = src.length - suffixPadding - prefixPadding; + if (length == 0) { + return EMPTY_BYTE_ARRAY; + } + byte[] dst = new byte[length]; + while (--length >= 0) { + dst[length] = (byte) (~src[prefixPadding + length]); + } + return dst; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes the given byte array which was encoded by {@link + * KeyEncoder#encodeSingleNullableDesc}. + */ + public static byte[] decodeSingleNullableDesc(byte[] src) throws CorruptEncodingException { + return decodeSingleNullableDesc(src, 0, 0); + } + + /** + * Decodes the given byte array which was encoded by {@link + * KeyEncoder#encodeSingleNullableDesc}. + * + * @param prefixPadding amount of extra bytes to skip from start of encoded byte array + * @param suffixPadding amount of extra bytes to skip at end of encoded byte array + */ + public static byte[] decodeSingleNullableDesc(byte[] src, int prefixPadding, int suffixPadding) + throws CorruptEncodingException + { + try { + byte b = src[prefixPadding]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + int length = src.length - suffixPadding - 1 - prefixPadding; + if (length == 0) { + return EMPTY_BYTE_ARRAY; + } + byte[] dst = new byte[length]; + while (--length >= 0) { + dst[length] = (byte) (~src[1 + prefixPadding + length]); + } + return dst; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } +} diff --git a/src/main/java/com/amazon/carbonado/raw/KeyEncoder.java b/src/main/java/com/amazon/carbonado/raw/KeyEncoder.java new file mode 100644 index 0000000..44c5af5 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/KeyEncoder.java @@ -0,0 +1,741 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +/** + * A very low-level class that supports encoding of primitive data into unique, + * sortable byte array keys. If the data to encode is of a variable size, then + * it is written in base-32768, using only byte values 32..223. This allows + * special values such as nulls and terminators to be unambiguously + * encoded. Terminators for variable data can be encoded using 1 for ascending + * order and 254 for descending order. Nulls can be encoded as 255 for high + * ordering and 0 for low ordering. + * + * @author Brian S O'Neill + * @see KeyDecoder + */ +public class KeyEncoder extends DataEncoder { + + /** Byte to terminate variable data encoded for ascending order */ + static final byte TERMINATOR = (byte)1; + + /** + * Encodes the given signed integer into exactly 4 bytes for descending + * order. + * + * @param value signed integer value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(int value, byte[] dst, int dstOffset) { + encode(~value, dst, dstOffset); + } + + /** + * Encodes the given signed Integer object into exactly 1 or 5 bytes for + * descending order. If the Integer object is never expected to be null, + * consider encoding as an int primitive. + * + * @param value optional signed Integer value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(Integer value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_LOW; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_LOW; + encode(~value.intValue(), dst, dstOffset + 1); + return 5; + } + } + + /** + * Encodes the given signed long into exactly 8 bytes for descending order. + * + * @param value signed long value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(long value, byte[] dst, int dstOffset) { + encode(~value, dst, dstOffset); + } + + /** + * Encodes the given signed Long object into exactly 1 or 9 bytes for + * descending order. If the Long object is never expected to be null, + * consider encoding as a long primitive. + * + * @param value optional signed Long value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(Long value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_LOW; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_LOW; + encode(~value.longValue(), dst, dstOffset + 1); + return 9; + } + } + + /** + * Encodes the given signed byte into exactly 1 byte for descending order. + * + * @param value signed byte value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(byte value, byte[] dst, int dstOffset) { + dst[dstOffset] = (byte)(value ^ 0x7f); + } + + /** + * Encodes the given signed Byte object into exactly 1 or 2 bytes for + * descending order. If the Byte object is never expected to be null, + * consider encoding as a byte primitive. + * + * @param value optional signed Byte value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(Byte value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_LOW; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_LOW; + dst[dstOffset + 1] = (byte)(value ^ 0x7f); + return 2; + } + } + + /** + * Encodes the given signed short into exactly 2 bytes for descending + * order. + * + * @param value signed short value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(short value, byte[] dst, int dstOffset) { + encode((short) ~value, dst, dstOffset); + } + + /** + * Encodes the given signed Short object into exactly 1 or 3 bytes for + * descending order. If the Short object is never expected to be null, + * consider encoding as a short primitive. + * + * @param value optional signed Short value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(Short value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_LOW; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_LOW; + encode((short) ~value.shortValue(), dst, dstOffset + 1); + return 3; + } + } + + /** + * Encodes the given character into exactly 2 bytes for descending order. + * + * @param value character value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(char value, byte[] dst, int dstOffset) { + encode((char) ~value, dst, dstOffset); + } + + /** + * Encodes the given Character object into exactly 1 or 3 bytes for + * descending order. If the Character object is never expected to be null, + * consider encoding as a char primitive. + * + * @param value optional Character value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(Character value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_LOW; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_LOW; + encode((char) ~value.charValue(), dst, dstOffset + 1); + return 3; + } + } + + /** + * Encodes the given boolean into exactly 1 byte for descending order. + * + * @param value boolean value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(boolean value, byte[] dst, int dstOffset) { + dst[dstOffset] = value ? (byte)127 : (byte)128; + } + + /** + * Encodes the given Boolean object into exactly 1 byte for descending + * order. + * + * @param value optional Boolean value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(Boolean value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_LOW; + } else { + dst[dstOffset] = value.booleanValue() ? (byte)127 : (byte)128; + } + } + + /** + * Encodes the given float into exactly 4 bytes for descending order. + * + * @param value float value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(float value, byte[] dst, int dstOffset) { + int bits = Float.floatToIntBits(value); + if (bits >= 0) { + bits ^= 0x7fffffff; + } + dst[dstOffset ] = (byte)(bits >> 24); + dst[dstOffset + 1] = (byte)(bits >> 16); + dst[dstOffset + 2] = (byte)(bits >> 8); + dst[dstOffset + 3] = (byte)bits; + } + + /** + * Encodes the given Float object into exactly 4 bytes for descending + * order. A non-canonical NaN value is used to represent null. + * + * @param value optional Float value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(Float value, byte[] dst, int dstOffset) { + if (value == null) { + encode(~0x7fffffff, dst, dstOffset); + } else { + encodeDesc(value.floatValue(), dst, dstOffset); + } + } + + /** + * Encodes the given double into exactly 8 bytes for descending order. + * + * @param value double value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(double value, byte[] dst, int dstOffset) { + long bits = Double.doubleToLongBits(value); + if (bits >= 0) { + bits ^= 0x7fffffffffffffffL; + } + int w = (int)(bits >> 32); + dst[dstOffset ] = (byte)(w >> 24); + dst[dstOffset + 1] = (byte)(w >> 16); + dst[dstOffset + 2] = (byte)(w >> 8); + dst[dstOffset + 3] = (byte)w; + w = (int)bits; + dst[dstOffset + 4] = (byte)(w >> 24); + dst[dstOffset + 5] = (byte)(w >> 16); + dst[dstOffset + 6] = (byte)(w >> 8); + dst[dstOffset + 7] = (byte)w; + } + + /** + * Encodes the given Double object into exactly 8 bytes for descending + * order. A non-canonical NaN value is used to represent null. + * + * @param value optional Double value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(Double value, byte[] dst, int dstOffset) { + if (value == null) { + encode(~0x7fffffffffffffffL, dst, dstOffset); + } else { + encodeDesc(value.doubleValue(), dst, dstOffset); + } + } + + /** + * Encodes the given optional unsigned byte array into a variable amount of + * bytes. If the byte array is null, exactly 1 byte is written. Otherwise, + * the amount written can be determined by calling calculateEncodedLength. + * + * @param value byte array value to encode, may be null + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(byte[] value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } + return encode(value, 0, value.length, dst, dstOffset, 0); + } + + /** + * Encodes the given optional unsigned byte array into a variable amount of + * bytes. If the byte array is null, exactly 1 byte is written. Otherwise, + * the amount written can be determined by calling calculateEncodedLength. + * + * @param value byte array value to encode, may be null + * @param valueOffset offset into byte array + * @param valueLength length of data in byte array + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(byte[] value, int valueOffset, int valueLength, + byte[] dst, int dstOffset) { + return encode(value, valueOffset, valueLength, dst, dstOffset, 0); + } + + /** + * Encodes the given optional unsigned byte array into a variable amount of + * bytes for descending order. If the byte array is null, exactly 1 byte is + * written. Otherwise, the amount written is determined by calling + * calculateEncodedLength. + * + * @param value byte array value to encode, may be null + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(byte[] value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_LOW; + return 1; + } + return encode(value, 0, value.length, dst, dstOffset, -1); + } + + /** + * Encodes the given optional unsigned byte array into a variable amount of + * bytes for descending order. If the byte array is null, exactly 1 byte is + * written. Otherwise, the amount written is determined by calling + * calculateEncodedLength. + * + * @param value byte array value to encode, may be null + * @param valueOffset offset into byte array + * @param valueLength length of data in byte array + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(byte[] value, int valueOffset, int valueLength, + byte[] dst, int dstOffset) { + return encode(value, valueOffset, valueLength, dst, dstOffset, -1); + } + + /** + * @param xorMask 0 for normal encoding, -1 for descending encoding + */ + private static int encode(byte[] value, int valueOffset, int valueLength, + byte[] dst, int dstOffset, int xorMask) { + if (value == null) { + dst[dstOffset] = (byte)(NULL_BYTE_HIGH ^ xorMask); + return 1; + } + + final int originalOffset = dstOffset; + + // Value is encoded in base-32768. + + int accumBits = 0; + int accum = 0; + + final int end = valueOffset + valueLength; + for (int i=valueOffset; i> (8 - supply)); + emitDigit(accum, dst, dstOffset, xorMask); + dstOffset += 2; + accumBits = 8 - supply; + accum = value[i] & ((1 << accumBits) - 1); + } + } + + if (accumBits > 0) { + // Pad with zeros. + accum <<= (15 - accumBits); + if (accumBits <= 7) { + // Since amount of significant bits is small, emit only the + // upper half of the digit. The following code is modified from + // emitDigit. + + int a = (accum * 21845) >> 22; + if (accum - ((a << 7) + (a << 6)) == 192) { + a++; + } + dst[dstOffset++] = (byte)((a + 32) ^ xorMask); + } else { + emitDigit(accum, dst, dstOffset, xorMask); + dstOffset += 2; + } + } + + // Append terminator. + dst[dstOffset++] = (byte)(TERMINATOR ^ xorMask); + + return dstOffset - originalOffset; + } + + /** + * Emits a base-32768 digit using exactly two bytes. The first byte is in the range + * 32..202 and the second byte is in the range 32..223. + * + * @param value digit value in the range 0..32767 + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @param xorMask 0 for normal encoding, -1 for descending encoding + */ + private static void emitDigit(int value, byte[] dst, int dstOffset, int xorMask) { + // The first byte is computed as ((value / 192) + 32) and the second + // byte is computed as ((value % 192) + 32). To speed things up a bit, + // the integer division and remainder operations are replaced with a + // scaled multiplication. + + // approximate value / 192 + int a = (value * 21845) >> 22; + + // approximate value % 192 + // Note: the value 192 was chosen as a divisor because a multiply by + // 192 can be replaced with two summed shifts. + int b = value - ((a << 7) + (a << 6)); + if (b == 192) { + // Fix error. + a++; + b = 0; + } + + dst[dstOffset++] = (byte)((a + 32) ^ xorMask); + dst[dstOffset] = (byte)((b + 32) ^ xorMask); + } + + /** + * Returns the amount of bytes required to encode a byte array of the given + * length. + * + * @param value byte array value to encode, may be null + * @return amount of bytes needed to encode + */ + public static int calculateEncodedLength(byte[] value) { + return value == null ? 1 : calculateEncodedLength(value, 0, value.length); + } + + /** + * Returns the amount of bytes required to encode the given byte array. + * + * @param value byte array value to encode, may be null + * @param valueOffset offset into byte array + * @param valueLength length of data in byte array + * @return amount of bytes needed to encode + */ + public static int calculateEncodedLength(byte[] value, int valueOffset, int valueLength) { + // The add of 119 is used to force ceiling rounding. + return value == null ? 1 : (((valueLength << 7) + 119) / 120 + 1); + } + + /** + * Encodes the given optional String into a variable amount of bytes. The + * amount written can be determined by calling + * calculateEncodedStringLength. + *

+ * Strings are encoded in a fashion similar to UTF-8, in that ASCII + * characters are usually written in one byte. This encoding is more + * efficient than UTF-8, but it isn't compatible with UTF-8. + * + * @param value String value to encode, may be null + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(String value, byte[] dst, int dstOffset) { + return encode(value, dst, dstOffset, 0); + } + + /** + * Encodes the given optional String into a variable amount of bytes for + * descending order. The amount written can be determined by calling + * calculateEncodedStringLength. + *

+ * Strings are encoded in a fashion similar to UTF-8, in that ASCII + * characters are usually written in one byte. This encoding is more + * efficient than UTF-8, but it isn't compatible with UTF-8. + * + * @param value String value to encode, may be null + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(String value, byte[] dst, int dstOffset) { + return encode(value, dst, dstOffset, -1); + } + + /** + * @param xorMask 0 for normal encoding, -1 for descending encoding + */ + private static int encode(String value, byte[] dst, int dstOffset, int xorMask) { + if (value == null) { + dst[dstOffset] = (byte)(NULL_BYTE_HIGH ^ xorMask); + return 1; + } + + final int originalOffset = dstOffset; + + // All characters have an offset of 2 added, in order to reserve bytes + // 0 and 1 for encoding nulls and terminators. This means the ASCII + // string "HelloWorld" is actually encoded as "JgnnqYqtnf". This also + // means that the ASCII '~' and del characters are encoded in two bytes. + + int length = value.length(); + for (int i = 0; i < length; i++) { + int c = value.charAt(i) + 2; + if (c <= 0x7f) { + // 0xxxxxxx + dst[dstOffset++] = (byte)(c ^ xorMask); + } else if (c <= 12415) { + // 10xxxxxx xxxxxxxx + + // Second byte cannot have the values 0, 1, 254, or 255 because + // they clash with null and terminator bytes. Divide by 192 and + // store in first 6 bits. The remainder, with 32 added, goes + // into the second byte. Note that (192 * 63 + 191) + 128 == 12415. + // 63 is the maximum value that can be represented in 6 bits. + + c -= 128; // c will always be at least 128, so normalize. + + // approximate value / 192 + int a = (c * 21845) >> 22; + + // approximate value % 192 + // Note: the value 192 was chosen as a divisor because a multiply by + // 192 can be replaced with two summed shifts. + c = c - ((a << 7) + (a << 6)); + if (c == 192) { + // Fix error. + a++; + c = 0; + } + + dst[dstOffset++] = (byte)((0x80 | a) ^ xorMask); + dst[dstOffset++] = (byte)((c + 32) ^ xorMask); + } else { + // 110xxxxx xxxxxxxx xxxxxxxx + + if ((c - 2) >= 0xd800 && (c - 2) <= 0xdbff) { + // Found a high surrogate. Verify that surrogate pair is + // well-formed. Low surrogate must follow high surrogate. + if (i + 1 < length) { + int c2 = value.charAt(i + 1); + if (c2 >= 0xdc00 && c2 <= 0xdfff) { + c = ((((c - 2) & 0x3ff) << 10) | (c2 & 0x3ff)) + 0x10002; + i++; + } + } + } + + // Second and third bytes cannot have the values 0, 1, 254, or + // 255 because they clash with null and terminator + // bytes. Divide by 192 twice, storing the first and second + // remainders in the third and second bytes, respectively. + // Note that largest unicode value supported is 2^20 + 65535 == + // 1114111. When divided by 192 twice, the value is 30, which + // just barely fits in the 5 available bits of the first byte. + + c -= 12416; // c will always be at least 12416, so normalize. + + int a = (int)((c * 21845L) >> 22); + c = c - ((a << 7) + (a << 6)); + if (c == 192) { + a++; + c = 0; + } + + dst[dstOffset + 2] = (byte)((c + 32) ^ xorMask); + + c = (a * 21845) >> 22; + a = a - ((c << 7) + (c << 6)); + if (a == 192) { + c++; + a = 0; + } + + dst[dstOffset++] = (byte)((0xc0 | c) ^ xorMask); + dst[dstOffset++] = (byte)((a + 32) ^ xorMask); + dstOffset++; + } + } + + // Append terminator. + dst[dstOffset++] = (byte)(TERMINATOR ^ xorMask); + + return dstOffset - originalOffset; + } + + /** + * Returns the amount of bytes required to encode the given String. + * + * @param value String to encode, may be null + */ + public static int calculateEncodedStringLength(String value) { + int encodedLen = 1; + if (value != null) { + int valueLength = value.length(); + for (int i = 0; i < valueLength; i++) { + int c = value.charAt(i); + if (c <= (0x7f - 2)) { + encodedLen++; + } else if (c <= (12415 - 2)) { + encodedLen += 2; + } else { + if (c >= 0xd800 && c <= 0xdbff) { + // Found a high surrogate. Verify that surrogate pair is + // well-formed. Low surrogate must follow high surrogate. + if (i + 1 < valueLength) { + int c2 = value.charAt(i + 1); + if (c2 >= 0xdc00 && c2 <= 0xdfff) { + i++; + } + } + } + encodedLen += 3; + } + } + } + return encodedLen; + } + + /** + * Encodes the given byte array for use when there is only a single + * required property, descending order, whose type is a byte array. The + * original byte array is returned if the length is zero. + */ + public static byte[] encodeSingleDesc(byte[] value) { + return encodeSingleDesc(value, 0, 0); + } + + /** + * Encodes the given byte array for use when there is only a single + * required property, descending order, whose type is a byte array. The + * original byte array is returned if the length and padding lengths are + * zero. + * + * @param prefixPadding amount of extra bytes to allocate at start of encoded byte array + * @param suffixPadding amount of extra bytes to allocate at end of encoded byte array + */ + public static byte[] encodeSingleDesc(byte[] value, int prefixPadding, int suffixPadding) { + int length = value.length; + if (prefixPadding <= 0 && suffixPadding <= 0 && length == 0) { + return value; + } + byte[] dst = new byte[prefixPadding + length + suffixPadding]; + while (--length >= 0) { + dst[prefixPadding + length] = (byte) (~value[length]); + } + return dst; + } + + /** + * Encodes the given byte array for use when there is only a single + * nullable property, descending order, whose type is a byte array. + */ + public static byte[] encodeSingleNullableDesc(byte[] value) { + return encodeSingleNullableDesc(value, 0, 0); + } + + /** + * Encodes the given byte array for use when there is only a single + * nullable property, descending order, whose type is a byte array. + * + * @param prefixPadding amount of extra bytes to allocate at start of encoded byte array + * @param suffixPadding amount of extra bytes to allocate at end of encoded byte array + */ + public static byte[] encodeSingleNullableDesc(byte[] value, + int prefixPadding, int suffixPadding) { + if (prefixPadding <= 0 && suffixPadding <= 0) { + if (value == null) { + return NULL_BYTE_ARRAY_LOW; + } + + int length = value.length; + if (length == 0) { + return NOT_NULL_BYTE_ARRAY_LOW; + } + + byte[] dst = new byte[1 + length]; + dst[0] = NOT_NULL_BYTE_LOW; + while (--length >= 0) { + dst[1 + length] = (byte) (~value[length]); + } + return dst; + } + + if (value == null) { + byte[] dst = new byte[prefixPadding + 1 + suffixPadding]; + dst[prefixPadding] = NULL_BYTE_LOW; + return dst; + } + + int length = value.length; + byte[] dst = new byte[prefixPadding + 1 + length + suffixPadding]; + dst[prefixPadding] = NOT_NULL_BYTE_LOW; + while (--length >= 0) { + dst[prefixPadding + 1 + length] = (byte) (~value[length]); + } + return dst; + } +} diff --git a/src/main/java/com/amazon/carbonado/raw/LayoutPropertyInfo.java b/src/main/java/com/amazon/carbonado/raw/LayoutPropertyInfo.java new file mode 100644 index 0000000..4049dc2 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/LayoutPropertyInfo.java @@ -0,0 +1,86 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import java.lang.reflect.Method; + +import org.cojen.classfile.TypeDesc; + +import com.amazon.carbonado.layout.LayoutProperty; +import com.amazon.carbonado.lob.Lob; + +/** + * + * + * @author Brian S O'Neill + */ +public class LayoutPropertyInfo implements GenericPropertyInfo { + private final LayoutProperty mProp; + private final TypeDesc mPropertyType; + private final TypeDesc mStorageType; + private final Method mFromStorage; + private final Method mToStorage; + + LayoutPropertyInfo(LayoutProperty property) { + this(property, null, null, null); + } + + LayoutPropertyInfo(LayoutProperty property, + Class storageType, Method fromStorage, Method toStorage) + { + mProp = property; + mPropertyType = TypeDesc.forDescriptor(property.getPropertyTypeDescriptor()); + if (storageType == null) { + mStorageType = mPropertyType; + } else { + mStorageType = TypeDesc.forClass(storageType); + } + mFromStorage = fromStorage; + mToStorage = toStorage; + } + + public String getPropertyName() { + return mProp.getPropertyName(); + } + + public TypeDesc getPropertyType() { + return mPropertyType; + } + + public TypeDesc getStorageType() { + return mStorageType; + } + + public boolean isNullable() { + return mProp.isNullable(); + } + + public boolean isLob() { + Class clazz = mPropertyType.toClass(); + return clazz != null && Lob.class.isAssignableFrom(clazz); + } + + public Method getFromStorageAdapter() { + return mFromStorage; + } + + public Method getToStorageAdapter() { + return mToStorage; + } +} diff --git a/src/main/java/com/amazon/carbonado/raw/RawCursor.java b/src/main/java/com/amazon/carbonado/raw/RawCursor.java new file mode 100644 index 0000000..865cdb2 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/RawCursor.java @@ -0,0 +1,743 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import java.util.NoSuchElementException; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.cursor.AbstractCursor; + +/** + * Abstract Cursor implementation for a repository that manipulates raw bytes. + * + * @author Brian S O'Neill + */ +public abstract class RawCursor extends AbstractCursor { + // States for mState. + private static final byte + UNINITIALIZED = 0, + CLOSED = 1, + TRY_NEXT = 2, + HAS_NEXT = 3; + + /** Lock object, as passed into the constructor */ + protected final Lock mLock; + + private final byte[] mStartBound; + private final boolean mInclusiveStart; + private final byte[] mEndBound; + private final boolean mInclusiveEnd; + private final int mPrefixLength; + private final boolean mReverse; + + private byte mState; + + /** + * @param lock operations lock on this object + * @param startBound specify the starting key for the cursor, or null if first + * @param inclusiveStart true if start bound is inclusive + * @param endBound specify the ending key for the cursor, or null if last + * @param inclusiveEnd true if end bound is inclusive + * @param maxPrefix maximum expected common initial bytes in start and end bound + * @param reverse when true, iteration is reversed + * @throws IllegalArgumentException if any bound is null but is not inclusive + */ + protected RawCursor(Lock lock, + byte[] startBound, boolean inclusiveStart, + byte[] endBound, boolean inclusiveEnd, + int maxPrefix, + boolean reverse) { + mLock = lock == null ? new ReentrantLock() : lock; + + if ((startBound == null && !inclusiveStart) || (endBound == null && !inclusiveEnd)) { + throw new IllegalArgumentException(); + } + + mStartBound = startBound; + mInclusiveStart = inclusiveStart; + mEndBound = endBound; + mInclusiveEnd = inclusiveEnd; + mReverse = reverse; + + // Determine common prefix for start and end bound. + if (maxPrefix <= 0 || startBound == null && endBound == null) { + mPrefixLength = 0; + } else { + int len = Math.min(maxPrefix, Math.min(startBound.length, endBound.length)); + int i; + for (i=0; i= amount) { + return actual; + } + mState = TRY_NEXT; + // Since state was HAS_NEXT and is forced into TRY_NEXT, actual + // amount skipped is effectively one more. + actual++; + } + + // Reached the end naturally, so close. + close(); + + return actual; + } finally { + mLock.unlock(); + } + } + + /** + * Release any internal resources, called when closed. + */ + protected abstract void release() throws FetchException; + + /** + * Returns the contents of the current key being referenced, or null + * otherwise. Caller is responsible for making a copy of the key. The array + * must not be modified concurrently. + * + *

If cursor is not opened, null must be returned. + * + * @return currently referenced key bytes or null if no current + * @throws IllegalStateException if key is disabled + */ + protected abstract byte[] getCurrentKey() throws FetchException; + + /** + * Returns the contents of the current value being referenced, or null + * otherwise. Caller is responsible for making a copy of the value. The + * array must not be modified concurrently. + * + *

If cursor is not opened, null must be returned. + * + * @return currently referenced value bytes or null if no current + * @throws IllegalStateException if value is disabled + */ + protected abstract byte[] getCurrentValue() throws FetchException; + + /** + * An optimization hint which disables key and value acquisition. The + * default implementation of this method does nothing. + */ + protected void disableKeyAndValue() { + } + + /** + * An optimization hint which disables just value acquisition. The default + * implementation of this method does nothing. + */ + protected void disableValue() { + } + + /** + * Enable key and value acquisition again, after they have been + * disabled. Calling this method forces the key and value to be + * re-acquired, if they had been disabled. Key and value acquisition must + * be enabled by default. The default implementation of this method does + * nothing. + */ + protected void enableKeyAndValue() throws FetchException { + } + + /** + * Returns a new Storable instance for the currently referenced entry. + * + * @return new Storable instance, never null + * @throws IllegalStateException if no current entry to instantiate + */ + protected abstract S instantiateCurrent() throws FetchException; + + /** + * Move the cursor to the first available entry. If false is returned, the + * cursor must be positioned before the first available entry. + * + * @return true if first entry exists and is now current + * @throws IllegalStateException if cursor is not opened + */ + protected abstract boolean toFirst() throws FetchException; + + /** + * Move the cursor to the first available entry at or after the given + * key. If false is returned, the cursor must be positioned before the + * first available entry. Caller is responsible for preserving contents of + * array. + * + * @param key key to search for + * @return true if first entry exists and is now current + * @throws IllegalStateException if cursor is not opened + */ + protected abstract boolean toFirst(byte[] key) throws FetchException; + + /** + * Move the cursor to the last available entry. If false is returned, the + * cursor must be positioned after the last available entry. + * + * @return true if last entry exists and is now current + * @throws IllegalStateException if cursor is not opened + */ + protected abstract boolean toLast() throws FetchException; + + /** + * Move the cursor to the last available entry at or before the given + * key. If false is returned, the cursor must be positioned after the last + * available entry. Caller is responsible for preserving contents of array. + * + * @param key key to search for + * @return true if last entry exists and is now current + * @throws IllegalStateException if cursor is not opened + */ + protected abstract boolean toLast(byte[] key) throws FetchException; + + /** + * Move the cursor to the next available entry, returning false if none. If + * false is returned, the cursor must be positioned after the last + * available entry. + * + * @return true if moved to next entry + * @throws IllegalStateException if cursor is not opened + */ + protected abstract boolean toNext() throws FetchException; + + /** + * Move the cursor to the next available entry, incrementing by the amount + * given. The actual amount incremented is returned. If the amount is less + * then requested, the cursor must be positioned after the last available + * entry. Subclasses may wish to override this method with a faster + * implementation. + * + *

Calling to toNext(1) is equivalent to calling toNext(). + * + * @param amount positive amount to advance + * @return actual amount advanced + * @throws IllegalStateException if cursor is not opened + */ + protected int toNext(int amount) throws FetchException { + if (amount <= 1) { + return (amount <= 0) ? 0 : (toNext() ? 1 : 0); + } + + int count = 0; + + disableKeyAndValue(); + try { + while (amount > 0) { + if (toNext()) { + count++; + amount--; + } else { + break; + } + } + } finally { + enableKeyAndValue(); + } + + return count; + } + + /** + * Move the cursor to the next unique key, returning false if none. If + * false is returned, the cursor must be positioned after the last + * available entry. Subclasses may wish to override this method with a + * faster implementation. + * + * @return true if moved to next unique key + * @throws IllegalStateException if cursor is not opened + */ + protected boolean toNextKey() throws FetchException { + byte[] initialKey = getCurrentKey(); + if (initialKey == null) { + return false; + } + + disableValue(); + try { + while (true) { + if (toNext()) { + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + return false; + } + if (compareKeysPartially(currentKey, initialKey) > 0) { + break; + } + } else { + return false; + } + } + } finally { + enableKeyAndValue(); + } + + return true; + } + + /** + * Move the cursor to the previous available entry, returning false if + * none. If false is returned, the cursor must be positioned before the + * first available entry. + * + * @return true if moved to previous entry + * @throws IllegalStateException if cursor is not opened + */ + protected abstract boolean toPrevious() throws FetchException; + + /** + * Move the cursor to the previous available entry, decrementing by the + * amount given. The actual amount decremented is returned. If the amount + * is less then requested, the cursor must be positioned before the first + * available entry. Subclasses may wish to override this method with a + * faster implementation. + * + *

Calling to toPrevious(1) is equivalent to calling toPrevious(). + * + * @param amount positive amount to retreat + * @return actual amount retreated + * @throws IllegalStateException if cursor is not opened + */ + protected int toPrevious(int amount) throws FetchException { + if (amount <= 1) { + return (amount <= 0) ? 0 : (toPrevious() ? 1 : 0); + } + + int count = 0; + + disableKeyAndValue(); + try { + while (amount > 0) { + if (toPrevious()) { + count++; + amount--; + } else { + break; + } + } + } finally { + enableKeyAndValue(); + } + + return count; + } + + /** + * Move the cursor to the previous unique key, returning false if none. If + * false is returned, the cursor must be positioned before the first + * available entry. Subclasses may wish to override this method with a + * faster implementation. + * + * @return true if moved to previous unique key + * @throws IllegalStateException if cursor is not opened + */ + protected boolean toPreviousKey() throws FetchException { + byte[] initialKey = getCurrentKey(); + if (initialKey == null) { + return false; + } + + disableValue(); + try { + while (true) { + if (toPrevious()) { + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + return false; + } + if (compareKeysPartially(getCurrentKey(), initialKey) < 0) { + break; + } + } else { + return false; + } + } + } finally { + enableKeyAndValue(); + } + + return true; + } + + /** + * Returns <0 if key1 is less, 0 if equal (at least partially), >0 + * if key1 is greater. + */ + protected int compareKeysPartially(byte[] key1, byte[] key2) { + int length = Math.min(key1.length, key2.length); + for (int i=0; i 0) { + byte[] prefix = mStartBound; + byte[] key = getCurrentKey(); + if (key == null) { + return false; + } + for (int i=0; i= 0) { + if (result > 0 || !mInclusiveEnd) { + return false; + } + } + } + + return prefixMatches(); + } + + // Calls toLast, but considers start and end bounds. Caller is responsible + // for preserving key. + private boolean toBoundedLast() throws FetchException { + if (mEndBound == null) { + if (!toLast()) { + return false; + } + } else { + if (!toLast(mEndBound.clone())) { + return false; + } + if (!mInclusiveEnd) { + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + return false; + } + if (compareKeysPartially(mEndBound, currentKey) == 0) { + if (!toPreviousKey()) { + return false; + } + } + } + } + + if (mStartBound != null) { + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + return false; + } + int result = compareKeysPartially(currentKey, mStartBound); + if (result <= 0) { + if (result < 0 || !mInclusiveStart) { + return false; + } + } + } + + return prefixMatches(); + } + + // Calls toNext, but considers end bound. + private boolean toBoundedNext() throws FetchException { + if (!toNext()) { + return false; + } + + if (mEndBound != null) { + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + return false; + } + int result = compareKeysPartially(currentKey, mEndBound); + if (result >= 0) { + if (result > 0 || !mInclusiveEnd) { + return false; + } + } + } + + return prefixMatches(); + } + + // Calls toNext, but considers end bound. + private int toBoundedNext(int amount) throws FetchException { + if (mEndBound == null) { + return toNext(amount); + } + + int count = 0; + + disableValue(); + try { + while (amount > 0) { + if (!toNext()) { + break; + } + + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + break; + } + + int result = compareKeysPartially(currentKey, mEndBound); + if (result >= 0) { + if (result > 0 || !mInclusiveEnd) { + break; + } + } + + if (!prefixMatches()) { + break; + } + + count++; + amount--; + } + } finally { + enableKeyAndValue(); + } + + return count; + } + + // Calls toPrevious, but considers start bound. + private boolean toBoundedPrevious() throws FetchException { + if (!toPrevious()) { + return false; + } + + if (mStartBound != null) { + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + return false; + } + int result = compareKeysPartially(currentKey, mStartBound); + if (result <= 0) { + if (result < 0 || !mInclusiveStart) { + // Too far now, reset to first. + toBoundedFirst(); + return false; + } + } + } + + return prefixMatches(); + } + + // Calls toPrevious, but considers start bound. + private int toBoundedPrevious(int amount) throws FetchException { + if (mStartBound == null) { + return toPrevious(amount); + } + + int count = 0; + + disableValue(); + try { + while (amount > 0) { + if (!toPrevious()) { + break; + } + + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + break; + } + + int result = compareKeysPartially(currentKey, mStartBound); + if (result <= 0) { + if (result < 0 || !mInclusiveStart) { + // Too far now, reset to first. + toBoundedFirst(); + break; + } + } + + if (!prefixMatches()) { + break; + } + + count++; + amount--; + } + } finally { + enableKeyAndValue(); + } + + return count; + } +} diff --git a/src/main/java/com/amazon/carbonado/raw/RawStorableGenerator.java b/src/main/java/com/amazon/carbonado/raw/RawStorableGenerator.java new file mode 100644 index 0000000..a8c7f6b --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/RawStorableGenerator.java @@ -0,0 +1,355 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.util.EnumSet; +import java.util.Map; + +import org.cojen.classfile.ClassFile; +import org.cojen.classfile.CodeBuilder; +import org.cojen.classfile.Label; +import org.cojen.classfile.LocalVariable; +import org.cojen.classfile.MethodInfo; +import org.cojen.classfile.Modifiers; +import org.cojen.classfile.TypeDesc; +import org.cojen.util.ClassInjector; +import org.cojen.util.WeakIdentityMap; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.SupportException; + +import com.amazon.carbonado.spi.MasterFeature; +import com.amazon.carbonado.spi.MasterStorableGenerator; +import com.amazon.carbonado.spi.MasterSupport; +import com.amazon.carbonado.spi.StorableGenerator; +import com.amazon.carbonado.spi.TriggerSupport; + +import static com.amazon.carbonado.spi.CommonMethodNames.*; + +/** + * Generates and caches abstract implementations of {@link Storable} types + * which are encoded and decoded in a raw format. The generated abstract + * classes extend those created by {@link MasterStorableGenerator}. + * + * @author Brian S O'Neill + * @see GenericStorableCodec + * @see RawSupport + */ +public class RawStorableGenerator { + // Note: All generated fields/methods have a "$" character in them to + // prevent name collisions with any inherited fields/methods. User storable + // properties are defined as fields which exactly match the property + // name. We don't want collisions with those either. Legal bean properties + // cannot have "$" in them, so there's nothing to worry about. + + /** Name of protected abstract method in generated storable */ + public static final String + ENCODE_KEY_METHOD_NAME = "encodeKey$", + DECODE_KEY_METHOD_NAME = "decodeKey$", + ENCODE_DATA_METHOD_NAME = "encodeData$", + DECODE_DATA_METHOD_NAME = "decodeData$"; + + @SuppressWarnings("unchecked") + private static Map> cCache = new WeakIdentityMap(); + + /** + * Collection of different abstract class flavors. + */ + static class Flavors { + private Reference> mMasterFlavor; + + private Reference> mNonMasterFlavor; + + /** + * May return null. + */ + Class getClass(boolean isMaster) { + Reference> ref; + if (isMaster) { + ref = mMasterFlavor; + } else { + ref = mNonMasterFlavor; + } + return (ref != null) ? ref.get() : null; + } + + @SuppressWarnings("unchecked") + void setClass(Class clazz, boolean isMaster) { + Reference> ref = new SoftReference(clazz); + if (isMaster) { + mMasterFlavor = ref; + } else { + mNonMasterFlavor = ref; + } + } + } + + // Can't be instantiated or extended + private RawStorableGenerator() { + } + + /** + * Returns an abstract implementation of the given Storable type, which is + * fully thread-safe. The Storable type itself may be an interface or a + * class. If it is a class, then it must not be final, and it must have a + * public, no-arg constructor. Two constructors are defined for the + * abstract implementation: + * + *

+     * public <init>(RawSupport);
+
+     * public <init>(RawSupport, byte[] key, byte[] value);
+     * 
+ * + *

Subclasses must implement the following abstract protected methods, + * whose exact names are defined by constants in this class: + * + *

+     * // Encode the primary key of this storable.
+     * protected abstract byte[] encodeKey();
+     *
+     * // Encode all properties of this storable excluding the primary key.
+     * protected abstract byte[] encodeData();
+     *
+     * // Decode the primary key into properties of this storable.
+     * // Note: this method is also invoked by the four argument constructor.
+     * protected abstract void decodeKey(byte[]);
+     *
+     * // Decode the data into properties of this storable.
+     * // Note: this method is also invoked by the four argument constructor.
+     * protected abstract void decodeData(byte[]);
+     * 
+ * + * @param isMaster when true, version properties, sequences, and triggers are managed + * @throws IllegalArgumentException if type is null + */ + @SuppressWarnings("unchecked") + public static Class + getAbstractClass(Class type, boolean isMaster) + throws SupportException, IllegalArgumentException + { + synchronized (cCache) { + Class abstractClass; + + Flavors flavors = (Flavors) cCache.get(type); + + if (flavors == null) { + flavors = new Flavors(); + cCache.put(type, flavors); + } else if ((abstractClass = flavors.getClass(isMaster)) != null) { + return abstractClass; + } + + abstractClass = generateAbstractClass(type, isMaster); + flavors.setClass(abstractClass, isMaster); + + return abstractClass; + } + } + + @SuppressWarnings("unchecked") + private static Class + generateAbstractClass(Class storableClass, boolean isMaster) + throws SupportException + { + EnumSet features; + if (isMaster) { + features = EnumSet.of(MasterFeature.VERSIONING, + MasterFeature.UPDATE_FULL, + MasterFeature.INSERT_SEQUENCES, + MasterFeature.INSERT_CHECK_REQUIRED); + } else { + features = EnumSet.of(MasterFeature.UPDATE_FULL); + } + + final Class abstractClass = + MasterStorableGenerator.getAbstractClass(storableClass, features); + + ClassInjector ci = ClassInjector.create + (storableClass.getName(), abstractClass.getClassLoader()); + + ClassFile cf = new ClassFile(ci.getClassName(), abstractClass); + cf.setModifiers(cf.getModifiers().toAbstract(true)); + cf.markSynthetic(); + cf.setSourceFile(RawStorableGenerator.class.getName()); + cf.setTarget("1.5"); + + // Declare some types. + final TypeDesc storableType = TypeDesc.forClass(Storable.class); + final TypeDesc storageType = TypeDesc.forClass(Storage.class); + final TypeDesc triggerSupportType = TypeDesc.forClass(TriggerSupport.class); + final TypeDesc masterSupportType = TypeDesc.forClass(MasterSupport.class); + final TypeDesc rawSupportType = TypeDesc.forClass(RawSupport.class); + final TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + + // Add constructor that accepts a RawSupport. + { + TypeDesc[] params = {rawSupportType}; + MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.invokeSuperConstructor(new TypeDesc[] {masterSupportType}); + b.returnVoid(); + } + + // Add constructor that accepts a RawSupport, an encoded key, and an + // encoded data. + { + TypeDesc[] params = {rawSupportType, byteArrayType, byteArrayType}; + MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.invokeSuperConstructor(new TypeDesc[] {masterSupportType}); + + params = new TypeDesc[] {byteArrayType}; + + b.loadThis(); + b.loadLocal(b.getParameter(1)); + b.invokeVirtual(DECODE_KEY_METHOD_NAME, null, params); + + b.loadThis(); + b.loadLocal(b.getParameter(2)); + b.invokeVirtual(DECODE_DATA_METHOD_NAME, null, params); + + // Indicate that object is clean by calling markAllPropertiesClean. + b.loadThis(); + b.invokeVirtual(MARK_ALL_PROPERTIES_CLEAN, null, null); + + b.returnVoid(); + } + + // Declare protected abstract methods. + { + cf.addMethod(Modifiers.PROTECTED.toAbstract(true), + ENCODE_KEY_METHOD_NAME, byteArrayType, null); + cf.addMethod(Modifiers.PROTECTED.toAbstract(true), + DECODE_KEY_METHOD_NAME, null, new TypeDesc[]{byteArrayType}); + cf.addMethod(Modifiers.PROTECTED.toAbstract(true), + ENCODE_DATA_METHOD_NAME, byteArrayType, null); + cf.addMethod(Modifiers.PROTECTED.toAbstract(true), + DECODE_DATA_METHOD_NAME, null, new TypeDesc[]{byteArrayType}); + } + + // Add required protected doTryLoad_master method, which delegates to RawSupport. + { + MethodInfo mi = cf.addMethod + (Modifiers.PROTECTED.toFinal(true), + MasterStorableGenerator.DO_TRY_LOAD_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(TypeDesc.forClass(FetchException.class)); + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadField(StorableGenerator.SUPPORT_FIELD_NAME, triggerSupportType); + b.checkCast(rawSupportType); + b.loadThis(); + b.invokeVirtual(ENCODE_KEY_METHOD_NAME, byteArrayType, null); + TypeDesc[] params = {byteArrayType}; + b.invokeInterface(rawSupportType, "tryLoad", byteArrayType, params); + LocalVariable encodedDataVar = b.createLocalVariable(null, byteArrayType); + b.storeLocal(encodedDataVar); + b.loadLocal(encodedDataVar); + Label notNull = b.createLabel(); + b.ifNullBranch(notNull, false); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + notNull.setLocation(); + b.loadThis(); + b.loadLocal(encodedDataVar); + params = new TypeDesc[] {byteArrayType}; + b.invokeVirtual(DECODE_DATA_METHOD_NAME, null, params); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + } + + // Add required protected doTryInsert_master method, which delegates to RawSupport. + { + MethodInfo mi = cf.addMethod + (Modifiers.PROTECTED.toFinal(true), + MasterStorableGenerator.DO_TRY_INSERT_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(TypeDesc.forClass(PersistException.class)); + CodeBuilder b = new CodeBuilder(mi); + + // return rawSupport.tryInsert(this, this.encodeKey$(), this.encodeData$()); + b.loadThis(); + b.loadField(StorableGenerator.SUPPORT_FIELD_NAME, triggerSupportType); + b.checkCast(rawSupportType); + b.loadThis(); // pass this to tryInsert method + b.loadThis(); + b.invokeVirtual(ENCODE_KEY_METHOD_NAME, byteArrayType, null); + b.loadThis(); + b.invokeVirtual(ENCODE_DATA_METHOD_NAME, byteArrayType, null); + TypeDesc[] params = {storableType, byteArrayType, byteArrayType}; + b.invokeInterface(rawSupportType, "tryInsert", TypeDesc.BOOLEAN, params); + b.returnValue(TypeDesc.BOOLEAN); + } + + // Add required protected doTryUpdate_master method, which delegates to RawSupport. + { + MethodInfo mi = cf.addMethod + (Modifiers.PROTECTED.toFinal(true), + MasterStorableGenerator.DO_TRY_UPDATE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(TypeDesc.forClass(PersistException.class)); + CodeBuilder b = new CodeBuilder(mi); + + // rawSupport.store(this, this.encodeKey$(), this.encodeData$()); + // return true; + b.loadThis(); + b.loadField(StorableGenerator.SUPPORT_FIELD_NAME, triggerSupportType); + b.checkCast(rawSupportType); + b.loadThis(); // pass this to store method + b.loadThis(); + b.invokeVirtual(ENCODE_KEY_METHOD_NAME, byteArrayType, null); + b.loadThis(); + b.invokeVirtual(ENCODE_DATA_METHOD_NAME, byteArrayType, null); + TypeDesc[] params = {storableType, byteArrayType, byteArrayType}; + b.invokeInterface(rawSupportType, "store", null, params); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + } + + // Add required protected doTryDelete_master method, which delegates to RawSupport. + { + MethodInfo mi = cf.addMethod + (Modifiers.PROTECTED.toFinal(true), + MasterStorableGenerator.DO_TRY_DELETE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(TypeDesc.forClass(PersistException.class)); + CodeBuilder b = new CodeBuilder(mi); + + // return rawSupport.tryDelete(this.encodeKey$()); + b.loadThis(); + b.loadField(StorableGenerator.SUPPORT_FIELD_NAME, triggerSupportType); + b.checkCast(rawSupportType); + b.loadThis(); + b.invokeVirtual(ENCODE_KEY_METHOD_NAME, byteArrayType, null); + + TypeDesc[] params = {byteArrayType}; + b.invokeInterface(rawSupportType, "tryDelete", TypeDesc.BOOLEAN, params); + b.returnValue(TypeDesc.BOOLEAN); + } + + return ci.defineClass(cf); + } +} diff --git a/src/main/java/com/amazon/carbonado/raw/RawSupport.java b/src/main/java/com/amazon/carbonado/raw/RawSupport.java new file mode 100644 index 0000000..f6253c9 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/RawSupport.java @@ -0,0 +1,97 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Storable; + +import com.amazon.carbonado.lob.Blob; +import com.amazon.carbonado.lob.Clob; + +import com.amazon.carbonado.spi.MasterSupport; + +/** + * Provides runtime support for Storable classes generated by {@link RawStorableGenerator}. + * + * @author Brian S O'Neill + */ +public interface RawSupport extends MasterSupport { + /** + * Try to load the entry referenced by the given key, but return null + * if not found. + * + * @param key non-null key to search for + * @return non-null value that was found, or null if not found + */ + byte[] tryLoad(byte[] key) throws FetchException; + + /** + * Try to insert the entry referenced by the given key with the given + * value. + * + * @param storable storable object that key and value were derived from + * @param key non-null key to insert + * @param value non-null value to insert + * @return false if unique constraint prevents insert + */ + boolean tryInsert(S storable, byte[] key, byte[] value) throws PersistException; + + /** + * Try to store the entry referenced by the given key with the given + * value. If the entry does not exist, insert it. Otherwise, update it. + * + * @param storable storable object that key and value were derived from + * @param key non-null key to store + * @param value non-null value to store + */ + void store(S storable, byte[] key, byte[] value) throws PersistException; + + /** + * Try to delete the entry referenced by the given key. + * + * @param key non-null key to delete + * @return true if entry existed and is now deleted + */ + boolean tryDelete(byte[] key) throws PersistException; + + /** + * Returns the Blob for the given locator, returning null if not found. + */ + Blob getBlob(long locator) throws FetchException; + + /** + * Returns the locator for the given Blob, returning zero if null. + * + * @throws PersistException if blob is unrecognized + */ + long getLocator(Blob blob) throws PersistException; + + /** + * Returns the Clob for the given locator, returning null if not found. + */ + Clob getClob(long locator) throws FetchException; + + /** + * Returns the locator for the given Clob, returning zero if null. + * + * @throws PersistException if blob is unrecognized + */ + long getLocator(Clob clob) throws PersistException; +} diff --git a/src/main/java/com/amazon/carbonado/raw/RawUtil.java b/src/main/java/com/amazon/carbonado/raw/RawUtil.java new file mode 100644 index 0000000..f93316a --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/RawUtil.java @@ -0,0 +1,66 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +/** + * Utilities for manipulating binary data. + * + * @author Brian S O'Neill + */ +public class RawUtil { + /** + * Adds one to an unsigned integer, represented as a byte array. If + * overflowed, value in byte array is 0x00, 0x00, 0x00... + * + * @param value unsigned integer to increment + * @return false if overflowed + */ + public static boolean increment(byte[] value) { + for (int i=value.length; --i>=0; ) { + byte newValue = (byte) ((value[i] & 0xff) + 1); + value[i] = newValue; + if (newValue != 0) { + // No carry bit, so done adding. + return true; + } + } + // This point is reached upon overflow. + return false; + } + + /** + * Subtracts one from an unsigned integer, represented as a byte array. If + * overflowed, value in byte array is 0xff, 0xff, 0xff... + * + * @param value unsigned integer to decrement + * @return false if overflowed + */ + public static boolean decrement(byte[] value) { + for (int i=value.length; --i>=0; ) { + byte newValue = (byte) ((value[i] & 0xff) + -1); + value[i] = newValue; + if (newValue != -1) { + // No borrow bit, so done subtracting. + return true; + } + } + // This point is reached upon overflow. + return false; + } +} diff --git a/src/main/java/com/amazon/carbonado/raw/StorableCodec.java b/src/main/java/com/amazon/carbonado/raw/StorableCodec.java new file mode 100644 index 0000000..b781691 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/StorableCodec.java @@ -0,0 +1,118 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; + +import com.amazon.carbonado.info.StorableIndex; + +/** + * Supports encoding and decoding of storables. + * + * @author Brian S O'Neill + * @see StorableCodecFactory + */ +public interface StorableCodec { + /** + * Returns the type of Storable produced by this codec. + */ + Class getStorableType(); + + /** + * Instantiate a Storable with no key or value defined yet. + * + * @param support binds generated storable with a storage layer + */ + S instantiate(RawSupport support); + + /** + * Instantiate a Storable with a specific key and value. + * + * @param support binds generated storable with a storage layer + */ + S instantiate(RawSupport support, byte[] key, byte[] value) + throws FetchException; + + /** + * Returns the sequence and directions of properties that make up the + * primary key. + */ + StorableIndex getPrimaryKeyIndex(); + + /** + * Returns the number of prefix bytes in the primary key, which may be + * zero. + */ + int getPrimaryKeyPrefixLength(); + + /** + * Encode a key by extracting all the primary key properties from the given + * storable. + * + * @param storable extract primary key properties from this instance + * @return raw search key + */ + byte[] encodePrimaryKey(S storable); + + /** + * Encode a key by extracting all the primary key properties from the given + * storable. + * + * @param storable extract primary key properties from this instance + * @param rangeStart index of first property to use. Its value must be less + * than the count of primary key properties. + * @param rangeEnd index of last property to use, exlusive. Its value must + * be less than or equal to the count of primary key properties. + * @return raw search key + */ + byte[] encodePrimaryKey(S storable, int rangeStart, int rangeEnd); + + /** + * Encode a key by extracting all the primary key properties from the given + * storable. + * + * @param values values to build into a key. It must be long enough to + * accommodate all primary key properties. + * @return raw search key + */ + byte[] encodePrimaryKey(Object[] values); + + /** + * Encode a key by extracting all the primary key properties from the given + * storable. + * + * @param values values to build into a key. The length may be less than + * the amount of primary key properties used by this factory. It must not + * be less than the difference between rangeStart and rangeEnd. + * @param rangeStart index of first property to use. Its value must be less + * than the count of primary key properties. + * @param rangeEnd index of last property to use, exlusive. Its value must + * be less than or equal to the count of primary key properties. + * @return raw search key + */ + byte[] encodePrimaryKey(Object[] values, int rangeStart, int rangeEnd); + + /** + * Encode the primary key for when there are no values, but there may be a + * prefix. Returned value may be null if no prefix is defined. + */ + byte[] encodePrimaryKeyPrefix(); +} diff --git a/src/main/java/com/amazon/carbonado/raw/StorableCodecFactory.java b/src/main/java/com/amazon/carbonado/raw/StorableCodecFactory.java new file mode 100644 index 0000000..1b19490 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/StorableCodecFactory.java @@ -0,0 +1,54 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.SupportException; + +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.layout.Layout; + +/** + * Factory for creating instances of {@link StorableCodec}. + * + * @author Brian S O'Neill + */ +public interface StorableCodecFactory { + /** + * Returns the preferred storage/database name for the given type. Return + * null to let repository decide. + * + * @throws SupportException if type is not supported + */ + String getStorageName(Class type) throws SupportException; + + /** + * @param type type of storable to create codec for + * @param pkIndex suggested index for primary key (optional) + * @param isMaster when true, version properties and sequences are managed + * @param layout when non-null, attempt to encode a storable layout + * generation value in each storable + * @throws SupportException if type is not supported + */ + StorableCodec createCodec(Class type, + StorableIndex pkIndex, + boolean isMaster, + Layout layout) + throws SupportException; +} diff --git a/src/main/java/com/amazon/carbonado/raw/StorablePropertyInfo.java b/src/main/java/com/amazon/carbonado/raw/StorablePropertyInfo.java new file mode 100644 index 0000000..e3a18b1 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/StorablePropertyInfo.java @@ -0,0 +1,132 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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.amazon.carbonado.raw; + +import java.lang.reflect.Method; + +import org.cojen.classfile.CodeAssembler; +import org.cojen.classfile.TypeDesc; + +import com.amazon.carbonado.info.StorableProperty; +import com.amazon.carbonado.lob.Lob; + +/** + * + * + * @author Brian S O'Neill + */ +public class StorablePropertyInfo implements GenericPropertyInfo { + private final StorableProperty mProp; + private final TypeDesc mPropertyType; + private final TypeDesc mStorageType; + private final Method mFromStorage; + private final Method mToStorage; + + StorablePropertyInfo(StorableProperty property) { + this(property, null, null, null); + } + + StorablePropertyInfo(StorableProperty property, + Class storageType, Method fromStorage, Method toStorage) { + mProp = property; + mPropertyType = TypeDesc.forClass(property.getType()); + if (storageType == null) { + mStorageType = mPropertyType; + } else { + mStorageType = TypeDesc.forClass(storageType); + } + mFromStorage = fromStorage; + mToStorage = toStorage; + } + + public String getPropertyName() { + return mProp.getName(); + } + + public TypeDesc getPropertyType() { + return mPropertyType; + } + + public TypeDesc getStorageType() { + return mStorageType; + } + + public boolean isNullable() { + return mProp.isNullable(); + } + + public boolean isLob() { + Class clazz = mPropertyType.toClass(); + return clazz != null && Lob.class.isAssignableFrom(clazz); + } + + public Method getFromStorageAdapter() { + return mFromStorage; + } + + public Method getToStorageAdapter() { + return mToStorage; + } + + public String getReadMethodName() { + return mProp.getReadMethodName(); + } + + public void addInvokeReadMethod(CodeAssembler a) { + a.invoke(mProp.getReadMethod()); + } + + public void addInvokeReadMethod(CodeAssembler a, TypeDesc instanceType) { + Class clazz = instanceType.toClass(); + if (clazz == null) { + // Can't know if instance should be invoked as an interface or as a + // virtual method. + throw new IllegalArgumentException("Instance type has no known class"); + } + if (clazz.isInterface()) { + a.invokeInterface(instanceType, getReadMethodName(), getPropertyType(), null); + } else { + a.invokeVirtual(instanceType, getReadMethodName(), getPropertyType(), null); + } + } + + public String getWriteMethodName() { + return mProp.getWriteMethodName(); + } + + public void addInvokeWriteMethod(CodeAssembler a) { + a.invoke(mProp.getWriteMethod()); + } + + public void addInvokeWriteMethod(CodeAssembler a, TypeDesc instanceType) { + Class clazz = instanceType.toClass(); + if (clazz == null) { + // Can't know if instance should be invoked as an interface or as a + // virtual method. + throw new IllegalArgumentException("Instance type has no known class"); + } + if (clazz.isInterface()) { + a.invokeInterface(instanceType, + getWriteMethodName(), null, new TypeDesc[] {getPropertyType()}); + } else { + a.invokeVirtual(instanceType, + getWriteMethodName(), null, new TypeDesc[] {getPropertyType()}); + } + } +} diff --git a/src/main/java/com/amazon/carbonado/raw/package-info.java b/src/main/java/com/amazon/carbonado/raw/package-info.java new file mode 100644 index 0000000..c92afd8 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/raw/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Provides support for repositories that encode/decode storables in a raw + * binary format. + */ +package com.amazon.carbonado.raw; -- cgit v1.2.3