/*
* Copyright 2006-2012 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.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import com.amazon.carbonado.FetchException;
import com.amazon.carbonado.Storable;
import com.amazon.carbonado.SupportException;
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 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.BeanComparator;
/**
* A SyntheticStorableReference defines new kinds of Storables from an existing
* master storable. This is used in situations when additional information about
* a storable needs to be tracked -- eg, for an index, or for caching. The
* storable may optionally have completely new, synthetic properties added.
*
* <P>
* All primary key properties of the master storable will also be provided by the
* derived storable. Three special methods will be provided:
* <ul>
* <li>getMaster - retrieves the original storable</li>
* <li>setAllProperties - sets the properties the syntheticReference has in
* common with the master to the values of the master instance</li>
* <li>isConsistent - verifies that the properties the syntheticReference has
* in common with the master are consistent with an instance of the master,
* meaning that they are in the same state and, if set, equal.</li>
* </ul>
*
* @author Brian S O'Neill
* @author Don Schneider
* @author David Rosenstrauch
*/
public class SyntheticStorableReferenceBuilder<S extends Storable>
implements SyntheticBuilder {
// The property setter will be called something like "copyFromMaster_0"
private static final String COPY_FROM_MASTER_PREFIX = "copyFromMaster_";
private static final String COPY_TO_MASTER_PK_PREFIX = "copyToMasterPk_";
private static final String IS_CONSISTENT_PREFIX = "getIsConsistent_";
// Information about the storable from which this one is derived
// private StorableInfo<S> mBaseStorableInfo;
private Class<S> mMasterStorableClass;
// Stashed copy of the results of calling StorableIntrospector.examine(...)
// on the master storable class.
private StorableInfo<S> mMasterStorableInfo;
// This guy will actually do the work
private SyntheticStorableBuilder mBuilder;
// Primary key of generated storable.
SyntheticKey mPrimaryKey;
// Elements added to primary key to ensure uniqueness
private Set<String> mExtraPkProps;
// True if the specified properties for this derived reference class are sufficient to
// uniquely identify a unique instance of the referent.
private boolean mIsUnique = true;
// The list of properties explicitly added to this reference builder
private List<SyntheticProperty> mUserProps;
// This list of properties the master and this one have in common
// The StorableProperties that get added to this list
// are retrieved from the master.
private List<StorableProperty> mCommonProps;
String mCopyFromMasterMethodName;
String mIsConsistentMethodName;
String mCopyToMasterPkMethodName;
// The result of building.
private SyntheticStorableReferenceAccess<S> mReferenceAccess;
/**
* @param storableClass
* class of the storable that will be referenced by this
* synthetic. The name for the synthetic storable will be based
* on this class's name, decorated with the properties which
* participate in the primary key for the synthetic storable.
*/
public SyntheticStorableReferenceBuilder(Class<S> storableClass,
boolean isUnique) {
this(storableClass, null, isUnique);
}
/**
* @param storableClass
* class of the storable that will be referenced by this
* synthetic
* @param baseName
* of the generated synthetic. Note that for some repositories
* this name will be visible across the entire repository, so it
* is good practice to include namespace information to guarantee
* uniqueness.
* @param isUnique
* true if the properties that are explicitly identified as primary
* key properites are sufficient to uniquely identify the index object.
*/
public SyntheticStorableReferenceBuilder(Class<S> storableClass,
final String baseName,
boolean isUnique) {
mMasterStorableClass = storableClass;
// Stash this away for later reference
mMasterStorableInfo = StorableIntrospector.examine(storableClass);
mIsUnique = isUnique;
mBuilder = new SyntheticStorableBuilder(storableClass.getCanonicalName()
+ (baseName != null? "_" + baseName : ""),
storableClass.getClassLoader());
mBuilder.setClassNameProvider(new ReferenceClassNameProvider(isUnique));
mPrimaryKey = mBuilder.addPrimaryKey();
mExtraPkProps = new LinkedHashSet<String>();
mUserProps = new ArrayList<SyntheticProperty>();
mCommonProps = new ArrayList<StorableProperty>();
}
/*
* (non-Javadoc)
*
* @see com.amazon.carbonado.synthetic.SyntheticBuilder#prepare()
*/
public ClassFileBuilder prepare() throws SupportException {
// Add a property to the reference for each primary key in the master
List<StorableProperty> masterPkProps = new ArrayList<StorableProperty>
(mMasterStorableInfo.getPrimaryKeyProperties().values());
// Sort master primary keys, to ensure consistent behavior.
Collections.sort(masterPkProps,
BeanComparator.forClass(StorableProperty.class).orderBy("name"));
for (StorableProperty masterPkProp : masterPkProps) {
// Some of them may have already been added as explicit
// primary keys, to articulate sort direction.
if (!mBuilder.hasProperty(masterPkProp.getName())) {
addProperty(masterPkProp);
// For non-unique indexes, *all* master pk properties are members of the
// generated primary key, in order to support duplicates.
if (!mIsUnique) {
mPrimaryKey.addProperty(masterPkProp.getName());
mExtraPkProps.add(masterPkProp.getName());
}
}
}
// Ensure that a version property exists in the index entry, if the
// user storable had one.
if (!mBuilder.isVersioned()) {
StorableProperty versionProperty = mMasterStorableInfo.getVersionProperty();
if (versionProperty != null) {
addProperty(versionProperty);
}
}
ClassFileBuilder cfg = mBuilder.prepare();
addSpecialMethods(cfg.getClassFile());
return cfg;
}
/**
* Build and return access to the generated storable reference class.
*
* @since 1.2.1
*/
public SyntheticStorableReferenceAccess<S> getReferenceAccess() {
if (mReferenceAccess == null) {
Class<? extends Storable> referenceClass = mBuilder.getStorableClass();
mReferenceAccess = new SyntheticStorableReferenceAccess<S>
(mMasterStorableClass, referenceClass, this);
}
return mReferenceAccess;
}
/*
* (non-Javadoc)
*
* @see com.amazon.carbonado.synthetic.SyntheticBuilder#getStorableClass()
*/
public Class<? extends Storable> getStorableClass() throws IllegalStateException {
return getReferenceAccess().getReferenceClass();
}
/*
* (non-Javadoc)
*
* @see com.amazon.carbonado.synthetic.SyntheticBuilder#build()
*/
public Class<? extends Storable> build() throws SupportException {
prepare();
return getStorableClass();
}
/**
* Add a property to the primary key which is a member of the Storable type
* being referenced by this one.
*
* @param name
*/
public SyntheticProperty addKeyProperty(String name, Direction direction) {
StorableProperty<S> prop = mMasterStorableInfo.getAllProperties().get(name);
if (prop == null) {
throw new IllegalArgumentException(name + " is not a property of "
+ mMasterStorableInfo.getName());
}
mPrimaryKey.addProperty(name, direction);
SyntheticProperty result = addProperty(prop);
mUserProps.add(result);
return result;
}
/**
* @see com.amazon.carbonado.synthetic.SyntheticStorableBuilder#addProperty(java.lang.String,
* java.lang.Class)
*/
public SyntheticProperty addProperty(String name, Class type) {
SyntheticProperty result = mBuilder.addProperty(name, type);
mUserProps.add(result);
return result;
}
/**
* @see com.amazon.carbonado.synthetic.SyntheticStorableBuilder#addProperty(com.amazon.carbonado.synthetic.SyntheticProperty)
*/
public SyntheticProperty addProperty(SyntheticProperty prop) {
SyntheticProperty result = mBuilder.addProperty(prop);
mUserProps.add(result);
return result;
}
/*
* (non-Javadoc)
*
* @see com.amazon.carbonado.synthetic.SyntheticBuilder#hasProperty(java.lang.String)
*/
public boolean hasProperty(String name) {
return mBuilder.hasProperty(name);
}
/**
* @return Returns the indexProps.
*/
public List<SyntheticProperty> getUserProps() {
return mUserProps;
}
public SyntheticKey addPrimaryKey() {
return mBuilder.addPrimaryKey();
}
/*
* (non-Javadoc)
*
* @see com.amazon.carbonado.synthetic.SyntheticBuilder#addAlternateKey() Note that
* using this method for a SyntheticReference being used as an alternate key is
* not well defined.
*/
public SyntheticKey addAlternateKey() {
return mBuilder.addAlternateKey();
}
/*
* (non-Javadoc)
*
* @see com.amazon.carbonado.synthetic.SyntheticBuilder#addIndex() Note that
* using this method for a SyntheticReference being used as an index is
* not well defined.
*/
public SyntheticIndex addIndex() {
return mBuilder.addIndex();
}
/*
* (non-Javadoc)
* @see com.amazon.carbonado.synthetic.SyntheticBuilder#getName()
*/
public Object getName() {
return mBuilder.getName();
}
/**
* True if the generated derived class should be considered unique. If
* non-unique, all properties are added to the primary key so there will be
* no conflicts between various derived classes derived from the same base
* storable.
*/
public boolean isUnique() {
return mIsUnique;
}
/*
* (non-Javadoc)
*
* @see com.amazon.carbonado.synthetic.SyntheticBuilder#isVersioned()
*/
public boolean isVersioned() {
return mBuilder.isVersioned();
}
/**
* Sets all the primary key properties of the given master, using the
* applicable properties of the given index entry.
*
* @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) throws FetchException {
getReferenceAccess().copyToMasterPrimaryKey(indexEntry, master);
}
/**
* Sets all the properties of the given index entry, using the applicable
* properties of the given master.
*
* @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) throws FetchException {
getReferenceAccess().copyFromMaster(indexEntry, master);
}
/**
* Returns true if the properties of the given index entry match those
* contained in the master, excluding any version property. This will
* always return true after a call to copyFromMaster.
*
* @param indexEntry
* index entry whose properties will be tested
* @param master
* source of property values
* @deprecated call getReferenceAccess
*/
@Deprecated
public boolean isConsistent(Storable indexEntry, S master) throws FetchException {
return getReferenceAccess().isConsistent(indexEntry, master);
}
/**
* Returns a comparator for ordering index entries.
* @deprecated call getReferenceAccess
*/
@Deprecated
public Comparator<? extends Storable> getComparator() {
return getReferenceAccess().getComparator();
}
/**
* Create methods for copying properties and testing properties.
*
* @throws amazon.carbonado.SupportException
*/
private void addSpecialMethods(ClassFile cf) throws SupportException {
// Generate safe names for special methods.
{
mCopyToMasterPkMethodName =
generateSafeMethodName(mMasterStorableInfo, COPY_TO_MASTER_PK_PREFIX);
mCopyFromMasterMethodName =
generateSafeMethodName(mMasterStorableInfo, COPY_FROM_MASTER_PREFIX);
mIsConsistentMethodName =
generateSafeMethodName(mMasterStorableInfo, IS_CONSISTENT_PREFIX);
}
// Add methods which copies properties between master and index entry.
addCopyMethod(cf, mCopyFromMasterMethodName);
addCopyMethod(cf, mCopyToMasterPkMethodName);
TypeDesc masterStorableType = TypeDesc.forClass(mMasterStorableClass);
// Add a method which tests all properties of index entry object
// against master object, excluding version and derived properties.
{
TypeDesc[] params = new TypeDesc[] {masterStorableType};
MethodInfo mi = cf.addMethod
(Modifiers.PUBLIC, mIsConsistentMethodName, TypeDesc.BOOLEAN, params);
CodeBuilder b = new CodeBuilder(mi);
for (StorableProperty prop : mCommonProps) {
if (prop.isVersion() || prop.isDerived()) {
continue;
}
Label propsAreEqual = b.createLabel();
addPropertyTest(b, prop, b.getParameter(0), propsAreEqual);
propsAreEqual.setLocation();
}
b.loadConstant(true);
b.returnValue(TypeDesc.BOOLEAN);
}
}
private void addCopyMethod(ClassFile cf, String methodName) throws SupportException {
TypeDesc masterStorableType = TypeDesc.forClass(mMasterStorableClass);
// void copyXxMaster(Storable master)
TypeDesc[] params = new TypeDesc[] { masterStorableType };
MethodInfo mi = cf.addMethod(Modifiers.PUBLIC, methodName, null, params);
CodeBuilder b = new CodeBuilder(mi);
boolean toMasterPk;
if (methodName.equals(mCopyToMasterPkMethodName)) {
toMasterPk = true;
} else if (methodName.equals(mCopyFromMasterMethodName)) {
toMasterPk = false;
} else {
throw new IllegalArgumentException();
}
for (StorableProperty prop : mCommonProps) {
if (toMasterPk && !prop.isPrimaryKeyMember()) {
continue;
}
TypeDesc propType = TypeDesc.forClass(prop.getType());
if (toMasterPk) {
if (prop.getWriteMethod() == null) {
throw new SupportException
("Property does not have a public mutator method: " + prop);
}
b.loadLocal(b.getParameter(0));
b.loadThis();
b.invokeVirtual(prop.getReadMethodName(), propType, null);
b.invoke(prop.getWriteMethod());
} else if (methodName.equals(mCopyFromMasterMethodName)) {
if (prop.getReadMethod() == null) {
throw new SupportException
("Property does not have a public accessor method: " + prop);
}
b.loadThis();
b.loadLocal(b.getParameter(0));
b.invoke(prop.getReadMethod());
b.invokeVirtual(prop.getWriteMethodName(), null, new TypeDesc[] {propType});
}
}
b.returnVoid();
}
/**
* Generate code to test equality of two properties.
* @param b
* @param property
* @param otherInstance
* @param propsAreEqual
*/
private static void addPropertyTest(CodeBuilder b,
StorableProperty<?> property,
LocalVariable otherInstance,
Label propsAreEqual)
{
TypeDesc propertyType = TypeDesc.forClass(property.getType());
b.loadThis();
b.invokeVirtual(property.getReadMethodName(), propertyType, null);
b.loadLocal(otherInstance);
b.invoke(property.getReadMethod());
CodeBuilderUtil.addValuesEqualCall(b,
propertyType,
true, // test for null
propsAreEqual,
true); // branch to propsAreEqual when equal
// Property values differ, so return false.
b.loadConstant(false);
b.returnValue(TypeDesc.BOOLEAN);
}
/**
* Generates a property name which doesn't clash with any already defined.
*/
private String generateSafeMethodName(StorableInfo info, String prefix) {
Class type = info.getStorableType();
// Try a few times to generate a unique name. There's nothing special
// about choosing 100 as the limit.
int value = 0;
for (int i = 0; i < 100; i++) {
String name = prefix + value;
if (!methodExists(type, name)) {
return name;
}
value = name.hashCode();
}
throw new InternalError("Unable to create unique method name starting with: "
+ prefix);
}
/**
* Look for conflicting method names
*/
private boolean methodExists(Class clazz, String name) {
Method[] methods = clazz.getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals(name)) {
return true;
}
}
if (clazz.getSuperclass() != null
&& methodExists(clazz.getSuperclass(), name)) {
return true;
}
Class[] interfaces = clazz.getInterfaces();
for (int i = 0; i < interfaces.length; i++) {
if (methodExists(interfaces[i], name)) {
return true;
}
}
return false;
}
/**
* @param prop
* StorableProperty to add to the list of properties to generate.
* This method will set the nullable and version annotations, but
* not the primary key or direction annotations. Warning: this
* should only be called with properties that are also in the
* master
* @return resulting SyntheticProperty
*/
private SyntheticProperty addProperty(StorableProperty prop) {
SyntheticProperty refProp = mBuilder.addProperty(prop.getName(),
prop.getType());
refProp.setReadMethodName(prop.getReadMethodName());
refProp.setWriteMethodName(prop.getWriteMethodName());
refProp.setIsNullable(prop.isNullable());
refProp.setIsVersion(prop.isVersion());
// If property has an adapter, make sure it is still applied here.
if (prop.getAdapter() != null) {
refProp.setAdapter(prop.getAdapter());
}
mCommonProps.add(prop);
return refProp;
}
/**
* Mechanism for making the name correspond to the semantics of a reference
* (an index, really)
*/
class ReferenceClassNameProvider implements ClassNameProvider {
boolean mUniquely;
public ReferenceClassNameProvider(boolean unique) {
super();
mUniquely = unique;
}
public String getName() {
StringBuilder b = new StringBuilder();
b.append(SyntheticStorableReferenceBuilder.this.getName());
b.append('~');
b.append(mUniquely ? 'U' : 'N');
Iterator<String> props =
SyntheticStorableReferenceBuilder.this.mPrimaryKey.getProperties();
while (props.hasNext()) {
String prop = props.next();
if (mExtraPkProps.contains(prop)) {
continue;
}
if (prop.charAt(0) != '+' && prop.charAt(0) != '-') {
b.append('~');
}
b.append(prop);
}
return b.toString();
}
public boolean isExplicit() {
return true;
}
}
}