From b66fb3db6951b2b6d3ace72ca3e197c7c2048e86 Mon Sep 17 00:00:00 2001
From: "Brian S. O'Neill" <bronee@gmail.com>
Date: Thu, 18 Dec 2008 20:58:00 +0000
Subject: Fixes for excessive class generation and memory usage when opening
 multiple repositories.

---
 .../java/com/amazon/carbonado/layout/Layout.java   |  43 ++++++
 .../amazon/carbonado/raw/GenericStorableCodec.java | 122 ++++++++---------
 .../java/com/amazon/carbonado/raw/RawSupport.java  |  14 ++
 .../com/amazon/carbonado/raw/StorableCodec.java    |  14 ++
 .../repo/indexed/IndexEntryGenerator.java          |  23 ++--
 .../carbonado/repo/sleepycat/BDBStorage.java       |  11 +-
 .../SyntheticStorableReferenceAccess.java          | 152 +++++++++++++++++++++
 .../SyntheticStorableReferenceBuilder.java         | 110 +++++----------
 8 files changed, 338 insertions(+), 151 deletions(-)
 create mode 100644 src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceAccess.java

(limited to 'src/main/java/com/amazon/carbonado')

diff --git a/src/main/java/com/amazon/carbonado/layout/Layout.java b/src/main/java/com/amazon/carbonado/layout/Layout.java
index ca50172..b6c6c0d 100644
--- a/src/main/java/com/amazon/carbonado/layout/Layout.java
+++ b/src/main/java/com/amazon/carbonado/layout/Layout.java
@@ -394,6 +394,49 @@ public class Layout {
         return reconstructed;
     }
 
+    @Override
+    public int hashCode() {
+        long id = getLayoutID();
+        return ((int) id) ^ (int) (id >> 32);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof Layout) {
+            Layout other = (Layout) obj;
+            try {
+                return mStoredLayout.equals(other.mStoredLayout) && equalLayouts(other);
+            } catch (FetchException e) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder b = new StringBuilder();
+        b.append("Layout {type=").append(getStorableTypeName());
+        b.append(", generation=").append(getGeneration());
+        b.append(", properties={");
+        try {
+            List<LayoutProperty> props = getAllProperties();
+            for (int i=0; i<props.size(); i++) {
+                if (i > 0) {
+                    b.append(", ");
+                }
+                b.append(props.get(i));
+            }
+        } catch (FetchException e) {
+            b.append(e.toString());
+        }
+        b.append("}}");
+        return b.toString();
+    }
+
     /**
      * Returns true if the given layout matches this one. Layout ID,
      * generation, and creation info is not considered in the comparison.
diff --git a/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java b/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java
index dd43515..4506bc1 100644
--- a/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java
+++ b/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java
@@ -18,7 +18,6 @@
 
 package com.amazon.carbonado.raw;
 
-import java.lang.ref.WeakReference;
 import java.lang.reflect.Method;
 import java.lang.reflect.UndeclaredThrowableException;
 import java.util.Map;
@@ -51,6 +50,8 @@ import com.amazon.carbonado.info.StorableProperty;
 import com.amazon.carbonado.layout.Layout;
 
 import com.amazon.carbonado.gen.CodeBuilderUtil;
+import com.amazon.carbonado.gen.StorableGenerator;
+import com.amazon.carbonado.gen.TriggerSupport;
 
 import com.amazon.carbonado.util.ThrowUnchecked;
 import com.amazon.carbonado.util.QuickConstructorGenerator;
@@ -64,10 +65,8 @@ import com.amazon.carbonado.util.QuickConstructorGenerator;
  */
 public class GenericStorableCodec<S extends Storable> implements StorableCodec<S> {
     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.
+    // Maps GenericEncodingStrategy instances to Storable classes.
     private static final Map cCache = new SoftValuedHashMap();
 
     /**
@@ -100,7 +99,8 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
         }
 
         return new GenericStorableCodec<S>
-            (factory,
+            (key,
+             factory,
              encodingStrategy.getType(),
              storableImpl,
              encodingStrategy,
@@ -128,30 +128,10 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
 
         // Declare some types.
         final TypeDesc storableType = TypeDesc.forClass(Storable.class);
+        final TypeDesc triggerSupportType = TypeDesc.forClass(TriggerSupport.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 constructors.
         // 1: Accepts a RawSupport.
@@ -277,26 +257,16 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
             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");
+            b.loadThis();
+            b.loadField(StorableGenerator.SUPPORT_FIELD_NAME, triggerSupportType);
+            b.checkCast(rawSupportType);
 
-            haveCodec.setLocation();
-            b.checkCast(codecType);
-            b.loadLocal(actualGeneration);
             Label tryStartDecode = b.createLabel().setLocation();
-            b.invokeVirtual(codecType, "getDecoder", decoderType, new TypeDesc[] {TypeDesc.INT});
             b.loadThis();
+            b.loadLocal(actualGeneration);
             b.loadLocal(b.getParameter(0));
-            b.invokeInterface(decoderType, "decode", null,
-                              new TypeDesc[] {storableType, byteArrayType});
+            b.invokeInterface(rawSupportType, "decode", null,
+                              new TypeDesc[] {storableType, TypeDesc.INT, byteArrayType});
             Label tryEndDecode = b.createLabel().setLocation();
 
             b.returnVoid();
@@ -318,8 +288,14 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
         return ci.defineClass(cf);
     }
 
-    private final GenericStorableCodecFactory mFactory;
+    // Maps codec key and OrderedProperty[] keys to SearchKeyFactory instances.
+    private static final Map cCodecSearchKeyFactories = new SoftValuedHashMap();
+
+    // Maps codec key and layout generations to Decoders.
+    private static final Map cCodecDecoders = new SoftValuedHashMap();
 
+    private final Object mCodecKey;
+    private final GenericStorableCodecFactory mFactory;
     private final Class<S> mType;
 
     private final Class<? extends S> mStorableClass;
@@ -330,9 +306,6 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
 
     private final SearchKeyFactory<S> mPrimaryKeyFactory;
 
-    // Maps OrderedProperty[] keys to SearchKeyFactory instances.
-    private final Map mSearchKeyFactories = new SoftValuedHashMap();
-
     private final Layout mLayout;
 
     private final RawSupport<S> mSupport;
@@ -340,11 +313,16 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
     // Maps layout generations to Decoders.
     private IntHashMap mDecoders;
 
-    private GenericStorableCodec(GenericStorableCodecFactory factory,
+    /**
+     * @param codecKey cache key for this GenericStorableCodec instance
+     */
+    private GenericStorableCodec(Object codecKey,
+                                 GenericStorableCodecFactory factory,
                                  Class<S> type, Class<? extends S> storableClass,
                                  GenericEncodingStrategy<S> encodingStrategy,
                                  Layout layout, RawSupport<S> support)
     {
+        mCodecKey = codecKey;
         mFactory = factory;
         mType = type;
         mStorableClass = storableClass;
@@ -354,16 +332,6 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
         mPrimaryKeyFactory = getSearchKeyFactory(encodingStrategy.gatherAllKeyProperties());
         mLayout = layout;
         mSupport = support;
-
-        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);
-            }
-        }
     }
 
     /**
@@ -498,23 +466,36 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
     @SuppressWarnings("unchecked")
     public SearchKeyFactory<S> getSearchKeyFactory(OrderedProperty<S>[] properties) {
         // This KeyFactory makes arrays work as hashtable keys.
-        Object key = org.cojen.util.KeyFactory.createKey(properties);
+        Object key = KeyFactory.createKey(new Object[] {mCodecKey, properties});
 
-        synchronized (mSearchKeyFactories) {
-            SearchKeyFactory<S> factory = (SearchKeyFactory<S>) mSearchKeyFactories.get(key);
+        synchronized (cCodecSearchKeyFactories) {
+            SearchKeyFactory<S> factory = (SearchKeyFactory<S>) cCodecSearchKeyFactories.get(key);
             if (factory == null) {
                 factory = generateSearchKeyFactory(properties);
-                mSearchKeyFactories.put(key, factory);
+                cCodecSearchKeyFactories.put(key, factory);
             }
             return factory;
         }
     }
 
+    @Override
+    public void decode(S dest, int generation, byte[] data) throws CorruptEncodingException {
+        try {
+            getDecoder(generation).decode(dest, data);
+        } catch (CorruptEncodingException e) {
+            throw e;
+        } catch (RepositoryException e) {
+            throw new CorruptEncodingException(e);
+        }
+    }
+
     /**
      * Returns a data decoder for the given generation.
      *
      * @throws FetchNoneException if generation is unknown
+     * @deprecated use direct decode method
      */
+    @Deprecated
     public Decoder<S> getDecoder(int generation) throws FetchNoneException, FetchException {
         try {
             synchronized (mLayout) {
@@ -524,7 +505,22 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
                 }
                 Decoder<S> decoder = (Decoder<S>) decoders.get(generation);
                 if (decoder == null) {
-                    decoder = generateDecoder(generation);
+                    synchronized (cCodecDecoders) {
+                        Object key = KeyFactory.createKey(new Object[] {mCodecKey, generation});
+                        decoder = (Decoder<S>) cCodecDecoders.get(key);
+                        if (decoder == null) {
+                            decoder = generateDecoder(generation);
+                            cCodecDecoders.put(key, decoder);
+                        } else {
+                            // Confirm that layout still exists.
+                            try {
+                                mLayout.getGeneration(generation);
+                            } catch (FetchNoneException e) {
+                                cCodecDecoders.remove(key);
+                                throw e;
+                            }
+                        }
+                    }
                     mDecoders.put(generation, decoder);
                 }
                 return decoder;
@@ -915,6 +911,6 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
          * @param data decoded into properties, some of which may be dropped if
          * destination storable doesn't have it
          */
-        void decode(S dest, byte[] data);
+        void decode(S dest, byte[] data) throws CorruptEncodingException;
     }
 }
diff --git a/src/main/java/com/amazon/carbonado/raw/RawSupport.java b/src/main/java/com/amazon/carbonado/raw/RawSupport.java
index b83e548..e2545b0 100644
--- a/src/main/java/com/amazon/carbonado/raw/RawSupport.java
+++ b/src/main/java/com/amazon/carbonado/raw/RawSupport.java
@@ -18,6 +18,7 @@
 
 package com.amazon.carbonado.raw;
 
+import com.amazon.carbonado.CorruptEncodingException;
 import com.amazon.carbonado.FetchException;
 import com.amazon.carbonado.PersistException;
 import com.amazon.carbonado.Storable;
@@ -102,4 +103,17 @@ public interface RawSupport<S extends Storable> extends MasterSupport<S> {
      * @throws PersistException if blob is unrecognized
      */
     long getLocator(Clob clob) throws PersistException;
+
+    /**
+     * Used for decoding different generations of Storable. If layout
+     * generations are not supported, simply throw a CorruptEncodingException.
+     *
+     * @param dest storable to receive decoded properties
+     * @param int storable layout generation number
+     * @param data decoded into properties, some of which may be dropped if
+     * destination storable doesn't have it
+     * @throws CorruptEncodingException if generation is unknown or if data cannot be decoded
+     * @since 1.2.1
+     */
+    void decode(S dest, int generation, byte[] data) throws CorruptEncodingException;
 }
diff --git a/src/main/java/com/amazon/carbonado/raw/StorableCodec.java b/src/main/java/com/amazon/carbonado/raw/StorableCodec.java
index fefb0fe..6874305 100644
--- a/src/main/java/com/amazon/carbonado/raw/StorableCodec.java
+++ b/src/main/java/com/amazon/carbonado/raw/StorableCodec.java
@@ -18,6 +18,7 @@
 
 package com.amazon.carbonado.raw;
 
+import com.amazon.carbonado.CorruptEncodingException;
 import com.amazon.carbonado.FetchException;
 import com.amazon.carbonado.Storable;
 import com.amazon.carbonado.info.StorableIndex;
@@ -133,6 +134,19 @@ public interface StorableCodec<S extends Storable> {
      */
     byte[] encodePrimaryKeyPrefix();
 
+    /**
+     * Used for decoding different generations of Storable. If layout
+     * generations are not supported, simply throw a CorruptEncodingException.
+     *
+     * @param dest storable to receive decoded properties
+     * @param int storable layout generation number
+     * @param data decoded into properties, some of which may be dropped if
+     * destination storable doesn't have it
+     * @throws CorruptEncodingException if generation is unknown or if data cannot be decoded
+     * @since 1.2.1
+     */
+    void decode(S dest, int generation, byte[] data) throws CorruptEncodingException;
+
     /**
      * Returns the default {@link RawSupport} object that is supplied to
      * Storable instances produced by this codec.
diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryGenerator.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryGenerator.java
index a126224..7fb80dc 100644
--- a/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryGenerator.java
+++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryGenerator.java
@@ -28,6 +28,7 @@ import com.amazon.carbonado.Storable;
 import com.amazon.carbonado.SupportException;
 import com.amazon.carbonado.info.StorableIndex;
 import com.amazon.carbonado.info.StorableProperty;
+import com.amazon.carbonado.synthetic.SyntheticStorableReferenceAccess;
 import com.amazon.carbonado.synthetic.SyntheticStorableReferenceBuilder;
 
 /**
@@ -100,7 +101,7 @@ class IndexEntryGenerator <S extends Storable> {
         }
     }
 
-    private SyntheticStorableReferenceBuilder<S> mBuilder;
+    private SyntheticStorableReferenceAccess<S> mIndexAccess;
 
     /**
      * Convenience class for gluing new "builder" style synthetics to the traditional
@@ -112,13 +113,17 @@ class IndexEntryGenerator <S extends Storable> {
         // but we have nothing better available to us
         Class<S> type = index.getProperty(0).getEnclosingType();
 
-        mBuilder = new SyntheticStorableReferenceBuilder<S>(type, index.isUnique());
+        SyntheticStorableReferenceBuilder<S> builder =
+            new SyntheticStorableReferenceBuilder<S>(type, index.isUnique());
 
         for (int i=0; i<index.getPropertyCount();  i++) {
             StorableProperty source = index.getProperty(i);
-            mBuilder.addKeyProperty(source.getName(), index.getPropertyDirection(i));
+            builder.addKeyProperty(source.getName(), index.getPropertyDirection(i));
         }
-        mBuilder.build();
+
+        builder.build();
+
+        mIndexAccess = builder.getReferenceAccess();
     }
 
     /**
@@ -127,7 +132,7 @@ class IndexEntryGenerator <S extends Storable> {
      * @return class of index entry, which is a custom Storable
      */
     public Class<? extends Storable> getIndexEntryClass() {
-        return mBuilder.getStorableClass();
+        return mIndexAccess.getReferenceClass();
     }
 
     /**
@@ -138,7 +143,7 @@ class IndexEntryGenerator <S extends Storable> {
      * @param master master whose primary key properties will be set
      */
     public void copyToMasterPrimaryKey(Storable indexEntry, S master) {
-        mBuilder.copyToMasterPrimaryKey(indexEntry, master);
+        mIndexAccess.copyToMasterPrimaryKey(indexEntry, master);
     }
 
     /**
@@ -149,7 +154,7 @@ class IndexEntryGenerator <S extends Storable> {
      * @param master source of property values
      */
     public void copyFromMaster(Storable indexEntry, S master) {
-        mBuilder.copyFromMaster(indexEntry, master);
+        mIndexAccess.copyFromMaster(indexEntry, master);
     }
 
     /**
@@ -161,13 +166,13 @@ class IndexEntryGenerator <S extends Storable> {
      * @param master source of property values
      */
     public boolean isConsistent(Storable indexEntry, S master) {
-        return mBuilder.isConsistent(indexEntry, master);
+        return mIndexAccess.isConsistent(indexEntry, master);
     }
 
     /**
      * Returns a comparator for ordering index entries.
      */
     public Comparator<? extends Storable> getComparator() {
-        return mBuilder.getComparator();
+        return mIndexAccess.getComparator();
     }
 }
diff --git a/src/main/java/com/amazon/carbonado/repo/sleepycat/BDBStorage.java b/src/main/java/com/amazon/carbonado/repo/sleepycat/BDBStorage.java
index f4a010b..1733167 100644
--- a/src/main/java/com/amazon/carbonado/repo/sleepycat/BDBStorage.java
+++ b/src/main/java/com/amazon/carbonado/repo/sleepycat/BDBStorage.java
@@ -24,6 +24,7 @@ import java.util.Map;
 
 import org.cojen.classfile.TypeDesc;
 
+import com.amazon.carbonado.CorruptEncodingException;
 import com.amazon.carbonado.Cursor;
 import com.amazon.carbonado.FetchDeadlockException;
 import com.amazon.carbonado.FetchException;
@@ -104,7 +105,7 @@ abstract class BDBStorage<Txn, S extends Storable> implements Storage<S>, Storag
     private final Class<S> mType;
 
     /** Does most of the work in generating storables, used for preparing and querying  */
-    private StorableCodec<S> mStorableCodec;
+    StorableCodec<S> mStorableCodec;
 
     /**
      * Reference to an instance of Proxy, defined in this class, which binds
@@ -135,7 +136,7 @@ abstract class BDBStorage<Txn, S extends Storable> implements Storage<S>, Storag
     {
         mRepository = repository;
         mType = type;
-        mRawSupport = new Support<Txn, S>(repository, this);
+        mRawSupport = new Support(repository, this);
         mTriggerManager = new TriggerManager<S>();
         try {
             // Ask if any lobs via static method first, to prevent stack
@@ -1009,7 +1010,7 @@ abstract class BDBStorage<Txn, S extends Storable> implements Storage<S>, Storag
     // Note: BDBStorage could just implement the RawSupport interface, but
     // then these hidden methods would be public. A simple cast of Storage to
     // RawSupport would expose them.
-    private static class Support<Txn, S extends Storable> implements RawSupport<S> {
+    private class Support implements RawSupport<S> {
         private final BDBRepository<Txn> mRepository;
         private final BDBStorage<Txn, S> mStorage;
         private Map<String, ? extends StorableProperty<S>> mProperties;
@@ -1128,6 +1129,10 @@ abstract class BDBStorage<Txn, S extends Storable> implements Storage<S>, Storag
             return mStorage.getLocator(clob);
         }
 
+        public void decode(S dest, int generation, byte[] data) throws CorruptEncodingException {
+            mStorableCodec.decode(dest, generation, data);
+        }
+
         public SequenceValueProducer getSequenceValueProducer(String name)
             throws PersistException
         {
diff --git a/src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceAccess.java b/src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceAccess.java
new file mode 100644
index 0000000..778b4ac
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceAccess.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2008 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.synthetic;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.UndeclaredThrowableException;
+
+import java.util.Comparator;
+import java.util.Iterator;
+
+import com.amazon.carbonado.Storable;
+
+import com.amazon.carbonado.cursor.SortedCursor;
+
+import com.amazon.carbonado.util.ThrowUnchecked;
+
+/**
+ * Provides access to the generated storable reference class and utility
+ * methods.
+ *
+ * @author Brian S O'Neill
+ * @see SyntheticStorableReferenceBuilder
+ * @since 1.2.1
+ */
+public class SyntheticStorableReferenceAccess<S extends Storable> {
+    private final Class<S> mMasterClass;
+    private final Class<? extends Storable> mReferenceClass;
+
+    private final Comparator<? extends Storable> mComparator;
+
+    private final Method mCopyFromMasterMethod;
+    private final Method mIsConsistentMethod;
+    private final Method mCopyToMasterPkMethod;
+
+    SyntheticStorableReferenceAccess(Class<S> masterClass,
+                                     Class<? extends Storable> referenceClass,
+                                     SyntheticStorableReferenceBuilder builder)
+    {
+        mMasterClass = masterClass;
+        mReferenceClass = referenceClass;
+
+        // We need a comparator which follows the same order as the generated
+        // storable.
+        SyntheticKey pk = builder.mPrimaryKey;
+        String[] orderBy = new String[pk.getPropertyCount()];
+        int i=0;
+        Iterator<String> it = pk.getProperties();
+        while (it.hasNext()) {
+            orderBy[i++] = it.next();
+        }
+        mComparator = SortedCursor.createComparator(referenceClass, orderBy);
+
+        try {
+            mCopyFromMasterMethod =
+                referenceClass.getMethod(builder.mCopyFromMasterMethodName, masterClass);
+
+            mIsConsistentMethod =
+                referenceClass.getMethod(builder.mIsConsistentMethodName, masterClass);
+
+            mCopyToMasterPkMethod =
+                referenceClass.getMethod(builder.mCopyToMasterPkMethodName, masterClass);
+        } catch (NoSuchMethodException e) {
+            throw new UndeclaredThrowableException(e);
+        }
+    }
+
+    /**
+     * Returns the storable class which is referenced.
+     */
+    public Class<S> getMasterClass() {
+        return mMasterClass;
+    }
+
+    /**
+     * Returns the generated storable reference class.
+     */
+    public Class<? extends Storable> getReferenceClass() {
+        return mReferenceClass;
+    }
+
+    /**
+     * Returns a comparator for ordering storable reference instances. This
+     * order matches the primary key of the master storable.
+     */
+    public Comparator<? extends Storable> getComparator() {
+        return mComparator;
+    }
+
+    /**
+     * Sets all the primary key properties of the given master, using the
+     * applicable properties of the given reference.
+     *
+     * @param reference source of property values
+     * @param master master whose primary key properties will be set
+     */
+    public void copyToMasterPrimaryKey(Storable reference, S master) {
+        try {
+            mCopyToMasterPkMethod.invoke(reference, master);
+        } catch (Exception e) {
+            ThrowUnchecked.fireFirstDeclaredCause(e);
+        }
+    }
+
+    /**
+     * Sets all the properties of the given reference, using the applicable
+     * properties of the given master.
+     *
+     * @param reference reference whose properties will be set
+     * @param master source of property values
+     */
+    public void copyFromMaster(Storable reference, S master) {
+        try {
+            mCopyFromMasterMethod.invoke(reference, master);
+        } catch (Exception e) {
+            ThrowUnchecked.fireFirstDeclaredCause(e);
+        }
+    }
+
+    /**
+     * Returns true if the properties of the given reference match those
+     * contained in the master, excluding any version property. This will
+     * always return true after a call to copyFromMaster.
+     *
+     * @param reference reference whose properties will be tested
+     * @param master source of property values
+     */
+    public boolean isConsistent(Storable reference, S master) {
+        try {
+            return (Boolean) mIsConsistentMethod.invoke(reference, master);
+        } catch (Exception e) {
+            ThrowUnchecked.fireFirstDeclaredCause(e);
+            // Not reached.
+            return false;
+        }
+    }
+}
diff --git a/src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceBuilder.java b/src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceBuilder.java
index 4fddcd5..a06c557 100644
--- a/src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceBuilder.java
+++ b/src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceBuilder.java
@@ -18,7 +18,6 @@
 package com.amazon.carbonado.synthetic;
 
 import java.lang.reflect.Method;
-import java.lang.reflect.UndeclaredThrowableException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -29,13 +28,11 @@ import java.util.Set;
 
 import com.amazon.carbonado.Storable;
 import com.amazon.carbonado.SupportException;
-import com.amazon.carbonado.cursor.SortedCursor;
 import com.amazon.carbonado.info.Direction;
 import com.amazon.carbonado.info.StorableInfo;
 import com.amazon.carbonado.info.StorableIntrospector;
 import com.amazon.carbonado.info.StorableProperty;
 import com.amazon.carbonado.gen.CodeBuilderUtil;
-import com.amazon.carbonado.util.ThrowUnchecked;
 
 import org.cojen.classfile.ClassFile;
 import org.cojen.classfile.CodeBuilder;
@@ -80,7 +77,7 @@ public class SyntheticStorableReferenceBuilder<S extends Storable>
 
     // Information about the storable from which this one is derived
     // private StorableInfo<S> mBaseStorableInfo;
-    private Class mMasterStorableClass;
+    private Class<S> mMasterStorableClass;
 
     // Stashed copy of the results of calling StorableIntrospector.examine(...)
     // on the master storable class.
@@ -90,7 +87,7 @@ public class SyntheticStorableReferenceBuilder<S extends Storable>
     private SyntheticStorableBuilder mBuilder;
 
     // Primary key of generated storable.
-    private SyntheticKey mPrimaryKey;
+    SyntheticKey mPrimaryKey;
 
     // Elements added to primary key to ensure uniqueness
     private Set<String> mExtraPkProps;
@@ -99,9 +96,6 @@ public class SyntheticStorableReferenceBuilder<S extends Storable>
     // uniquely identify a unique instance of the referent.
     private boolean mIsUnique = true;
 
-    // The result of building
-    private Class<? extends Storable> mSyntheticClass;
-
     // The list of properties explicitly added to this reference builder
     private List<SyntheticProperty> mUserProps;
 
@@ -110,16 +104,12 @@ public class SyntheticStorableReferenceBuilder<S extends Storable>
     // are retrieved from the master.
     private List<StorableProperty> mCommonProps;
 
-    private String mCopyFromMasterMethodName;
-    private Method mCopyFromMasterMethod;
-
-    private String mIsConsistentMethodName;
-    private Method mIsConsistentMethod;
-
-    private String mCopyToMasterPkMethodName;
-    private Method mCopyToMasterPkMethod;
+    String mCopyFromMasterMethodName;
+    String mIsConsistentMethodName;
+    String mCopyToMasterPkMethodName;
 
-    private Comparator<? extends Storable> mComparator;
+    // The result of building.
+    private SyntheticStorableReferenceAccess mReferenceAccess;
 
     /**
      * @param storableClass
@@ -210,26 +200,27 @@ public class SyntheticStorableReferenceBuilder<S extends Storable>
         return cfg;
     }
 
+    /**
+     * Build and return access to the generated storable reference class.
+     *
+     * @since 1.2.1
+     */
+    public SyntheticStorableReferenceAccess getReferenceAccess() {
+        if (mReferenceAccess == null) {
+            Class<? extends Storable> referenceClass = mBuilder.getStorableClass();
+            mReferenceAccess = new SyntheticStorableReferenceAccess
+                (mMasterStorableClass, referenceClass, this);
+        }
+        return mReferenceAccess;
+    }
+
     /*
      * (non-Javadoc)
      *
      * @see com.amazon.carbonado.synthetic.SyntheticBuilder#getStorableClass()
      */
     public Class<? extends Storable> getStorableClass() throws IllegalStateException {
-        if (mSyntheticClass == null) {
-            mSyntheticClass = mBuilder.getStorableClass();
-
-            // We need a comparator which follows the same order as the generated
-            // storable. We can't construct it until we get here.
-            String[] orderBy = new String[mPrimaryKey.getPropertyCount()];
-            int i=0;
-            Iterator<String> it = mPrimaryKey.getProperties();
-            while (it.hasNext()) {
-                orderBy[i++] = it.next();
-            }
-            mComparator = SortedCursor.createComparator(mSyntheticClass, orderBy);
-        }
-        return mSyntheticClass;
+        return getReferenceAccess().getReferenceClass();
     }
 
     /*
@@ -356,22 +347,11 @@ public class SyntheticStorableReferenceBuilder<S extends Storable>
      *
      * @param indexEntry source of property values
      * @param master master whose primary key properties will be set
+     * @deprecated call getReferenceAccess
      */
+    @Deprecated
     public void copyToMasterPrimaryKey(Storable indexEntry, S master) {
-        if (mCopyToMasterPkMethod == null) {
-            try {
-                mCopyToMasterPkMethod =
-                    mSyntheticClass.getMethod(mCopyToMasterPkMethodName, mMasterStorableClass);
-            } catch (NoSuchMethodException e) {
-                throw new UndeclaredThrowableException(e);
-            }
-        }
-
-        try {
-            mCopyToMasterPkMethod.invoke(indexEntry, master);
-        } catch (Exception e) {
-            ThrowUnchecked.fireFirstDeclaredCause(e);
-        }
+        getReferenceAccess().copyToMasterPrimaryKey(indexEntry, master);
     }
 
     /**
@@ -380,22 +360,11 @@ public class SyntheticStorableReferenceBuilder<S extends Storable>
      *
      * @param indexEntry index entry whose properties will be set
      * @param master source of property values
+     * @deprecated call getReferenceAccess
      */
+    @Deprecated
     public void copyFromMaster(Storable indexEntry, S master) {
-        if (mCopyFromMasterMethod == null) {
-            try {
-                mCopyFromMasterMethod =
-                    mSyntheticClass.getMethod(mCopyFromMasterMethodName, mMasterStorableClass);
-            } catch (NoSuchMethodException e) {
-                throw new UndeclaredThrowableException(e);
-            }
-        }
-
-        try {
-            mCopyFromMasterMethod.invoke(indexEntry, master);
-        } catch (Exception e) {
-            ThrowUnchecked.fireFirstDeclaredCause(e);
-        }
+        getReferenceAccess().copyFromMaster(indexEntry, master);
     }
 
     /**
@@ -407,31 +376,20 @@ public class SyntheticStorableReferenceBuilder<S extends Storable>
      *            index entry whose properties will be tested
      * @param master
      *            source of property values
+     * @deprecated call getReferenceAccess
      */
+    @Deprecated
     public boolean isConsistent(Storable indexEntry, S master) {
-        if (mIsConsistentMethod == null) {
-            try {
-                mIsConsistentMethod =
-                    mSyntheticClass.getMethod(mIsConsistentMethodName, mMasterStorableClass);
-            } catch (NoSuchMethodException e) {
-                throw new UndeclaredThrowableException(e);
-            }
-        }
-
-        try {
-            return (Boolean) mIsConsistentMethod.invoke(indexEntry, master);
-        } catch (Exception e) {
-            ThrowUnchecked.fireFirstDeclaredCause(e);
-            // Not reached.
-            return false;
-        }
+        return getReferenceAccess().isConsistent(indexEntry, master);
     }
 
     /**
      * Returns a comparator for ordering index entries.
+     * @deprecated call getReferenceAccess
      */
+    @Deprecated
     public Comparator<? extends Storable> getComparator() {
-        return mComparator;
+        return getReferenceAccess().getComparator();
     }
 
     /**
-- 
cgit v1.2.3