/* * 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.info; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.cojen.util.BeanProperty; import org.cojen.util.ThrowUnchecked; import com.amazon.carbonado.adapter.AdapterDefinition; /** * Information about an {@link com.amazon.carbonado.adapter.AdapterDefinition * adapter} annotation applied to a property. * * @author Brian S O'Neill */ public class StorablePropertyAdapter { static Class getEnclosingType(BeanProperty property) { Method m = property.getReadMethod(); if (m == null) { m = property.getWriteMethod(); } return m.getDeclaringClass(); } /** * @return null if not found */ static Class findAdapterClass(Class<? extends Annotation> annotationType) { AdapterDefinition ad = annotationType.getAnnotation(AdapterDefinition.class); if (ad == null) { return null; } Class adapterClass = ad.implementation(); if (adapterClass == void.class) { // Magic value meaning "use default", which is an inner class of // the annotation. adapterClass = null; // Search for inner class named "Adapter". Class[] innerClasses = annotationType.getClasses(); for (Class c : innerClasses) { if ("Adapter".equals(c.getSimpleName())) { adapterClass = c; break; } } } return adapterClass; } /** * @return empty array if none found */ @SuppressWarnings("unchecked") static Method[] findAdaptMethods(Class<?> propertyType, Class<?> adapterClass) { List<Method> adaptMethods = new ArrayList<Method>(); for (Method adaptMethod : adapterClass.getMethods()) { if (!adaptMethod.getName().startsWith("adapt")) { continue; } Class<?> toType = adaptMethod.getReturnType(); if (toType == void.class) { continue; } Class<?>[] paramTypes = adaptMethod.getParameterTypes(); Class<?> fromType; if (paramTypes.length != 1) { continue; } else { fromType = paramTypes[0]; } if (!fromType.isAssignableFrom(propertyType) && !propertyType.isAssignableFrom(toType)) { continue; } adaptMethods.add(adaptMethod); } return (Method[]) adaptMethods.toArray(new Method[adaptMethods.size()]); } private final Class mEnclosingType; private final String mPropertyName; private final StorablePropertyAnnotation mAnnotation; private final Class[] mStorageTypePreferences; private final Constructor mConstructor; private final Method[] mAdaptMethods; private transient Object mAdapterInstance; /** * Construct a generic StorablePropertyAdapter instance not attached to a * storable definition. Call {@link StorableProperty#getAdapter} to gain * access to adapter information on actual storable definitions. * * @param propertyName name of property with adapter * @param propertyType declated type of adapted property * @param adapterType adapter type * @throws IllegalArgumentException if adapterType is not an adapter * definition. */ public StorablePropertyAdapter(String propertyName, Class<?> propertyType, Class<? extends Annotation> adapterType) { this(null, propertyName, propertyType, null, adapterType); } /** * Used by StorableIntrospector. * * @see StorableIntrospector */ StorablePropertyAdapter(BeanProperty property, StorablePropertyAnnotation annotation, AdapterDefinition ad, Constructor ctor, Method[] adaptMethods) { mEnclosingType = getEnclosingType(property); mPropertyName = property.getName(); mAnnotation = annotation; mConstructor = ctor; mAdaptMethods = adaptMethods; Class[] storageTypePreferences = ad.storageTypePreferences(); if (storageTypePreferences != null && storageTypePreferences.length == 0) { storageTypePreferences = null; } mStorageTypePreferences = storageTypePreferences; } /** * Used with automatic adapter selection. * * @see AutomaticAdapterSeletor */ StorablePropertyAdapter(BeanProperty property, StorablePropertyAnnotation annotation) { this(getEnclosingType(property), property.getName(), property.getType(), annotation, annotation.getAnnotationType()); } private StorablePropertyAdapter(Class enclosingType, String propertyName, Class<?> propertyType, StorablePropertyAnnotation annotation, Class<? extends Annotation> adapterType) { mEnclosingType = enclosingType; mPropertyName = propertyName; mAnnotation = annotation; AdapterDefinition ad = adapterType.getAnnotation(AdapterDefinition.class); if (ad == null) { throw new IllegalArgumentException(); } Class[] storageTypePreferences = ad.storageTypePreferences(); if (storageTypePreferences != null && storageTypePreferences.length == 0) { storageTypePreferences = null; } mStorageTypePreferences = storageTypePreferences; Class adapterClass = findAdapterClass(adapterType); if (adapterClass == null) { throw new IllegalArgumentException(); } try { mConstructor = adapterClass.getConstructor(Class.class, String.class, adapterType); } catch (NoSuchMethodException e) { throw new IllegalArgumentException(e); } mAdaptMethods = findAdaptMethods(propertyType, adapterClass); if (mAdaptMethods.length == 0) { throw new IllegalArgumentException(); } } /** * Returns the annotation that applied this adapter, or null if none. */ public StorablePropertyAnnotation getAnnotation() { return mAnnotation; } /** * Returns the constructor for the adapter class. It has the signature * <code>(Class type, String propertyName, <i>Annotation</i>)</code>, where * <i>Annotation</i> is the fully resolved annotation. */ public Constructor getAdapterConstructor() { return mConstructor; } /** * Returns an instance of the adapter, for which an adapt method is applied to. */ public Object getAdapterInstance() { if (mAdapterInstance == null) { try { mAdapterInstance = mConstructor.newInstance (mEnclosingType, mPropertyName, mAnnotation.getAnnotation()); } catch (Exception e) { ThrowUnchecked.fireFirstDeclaredCause(e); } } return mAdapterInstance; } /** * Returns the adapter's storage type preferences. * * @see com.amazon.carbonado.adapter.AdapterDefinition#storageTypePreferences */ public Class[] getStorageTypePreferences() { if (mStorageTypePreferences == null) { return new Class[0]; } return mStorageTypePreferences.clone(); } /** * Returns an adapt method that supports the given conversion, or null if * none. */ @SuppressWarnings("unchecked") public Method findAdaptMethod(Class from, Class to) { Method[] methods = mAdaptMethods; List<Method> candidates = new ArrayList<Method>(methods.length); for (int i=methods.length; --i>=0; ) { Method method = methods[i]; if (to.isAssignableFrom(method.getReturnType()) && method.getParameterTypes()[0].isAssignableFrom(from)) { candidates.add(method); } } reduceCandidates(candidates, to); if (candidates.size() == 0) { return null; } return candidates.get(0); } /** * Returns all the adapt methods that convert from the given type. */ public Method[] findAdaptMethodsFrom(Class from) { Method[] methods = mAdaptMethods; List<Method> candidates = new ArrayList<Method>(methods.length); for (int i=methods.length; --i>=0; ) { Method method = methods[i]; if (method.getParameterTypes()[0].isAssignableFrom(from)) { candidates.add(method); } } return (Method[]) candidates.toArray(new Method[candidates.size()]); } /** * Returns all the adapt methods that convert to the given type. */ @SuppressWarnings("unchecked") public Method[] findAdaptMethodsTo(Class to) { Method[] methods = mAdaptMethods; List<Method> candidates = new ArrayList<Method>(methods.length); for (int i=methods.length; --i>=0; ) { Method method = methods[i]; if (to.isAssignableFrom(method.getReturnType())) { candidates.add(method); } } reduceCandidates(candidates, to); return (Method[]) candidates.toArray(new Method[candidates.size()]); } /** * Returns the count of all defined adapt methods. */ public int getAdaptMethodCount() { return mAdaptMethods.length; } /** * Returns a specific adapt method. */ public Method getAdaptMethod(int index) throws IndexOutOfBoundsException { return mAdaptMethods[index]; } /** * Returns a new array with all the adapt methods in it. */ public Method[] getAdaptMethods() { return mAdaptMethods.clone(); } private void reduceCandidates(List<Method> candidates, Class to) { if (candidates.size() <= 1) { // Shortcut. return; } // Map "from" type to all methods that convert from it. When reduced, // the list lengths are one. Map<Class, List<Method>> fromMap = new LinkedHashMap<Class, List<Method>>(); for (Method method : candidates) { Class from = method.getParameterTypes()[0]; List<Method> matches = fromMap.get(from); if (matches == null) { matches = new ArrayList<Method>(); fromMap.put(from, matches); } matches.add(method); } candidates.clear(); for (List<Method> matches : fromMap.values()) { Method best = null; int bestDistance = Integer.MAX_VALUE; for (Method method : matches) { int distance = distance(method.getReturnType(), to); if (best == null || distance < bestDistance) { best = method; bestDistance = distance; } } candidates.add(best); } } private static int distance(Class from, Class to) { int distance = 0; while (from != to) { from = from.getSuperclass(); if (from == null) { return Integer.MAX_VALUE; } distance++; } return distance; } }