JavaFieldWriterUtil.java
/*
* Copyright (C) 2017 Red Hat, Inc.
*
* 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 io.atlasmap.java.core;
import java.beans.beancontext.BeanContext;
import java.beans.beancontext.BeanContextServices;
import java.beans.beancontext.BeanContextServicesSupport;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.Queue;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;
import org.slf4j.LoggerFactory;
import io.atlasmap.api.AtlasException;
import io.atlasmap.core.AtlasPath.SegmentContext;
import io.atlasmap.spi.AtlasConversionService;
import io.atlasmap.v2.CollectionType;
public class JavaFieldWriterUtil {
private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(JavaFieldWriterUtil.class);
private AtlasConversionService conversionService = null;
private ClassLoader classLoader;
private Map<Class<?>, Class<?>> defaultCollectionImplClasses = new HashMap<>();
public JavaFieldWriterUtil(AtlasConversionService conversionService) {
this(Thread.currentThread().getContextClassLoader(), conversionService);
}
public JavaFieldWriterUtil(ClassLoader classLoader, AtlasConversionService conversionService) {
this.conversionService = conversionService;
this.classLoader = classLoader;
defaultCollectionImplClasses.put(BeanContext.class, BeanContextServicesSupport.class);
defaultCollectionImplClasses.put(BeanContextServices.class, BeanContextServicesSupport.class);
defaultCollectionImplClasses.put(BlockingDeque.class, LinkedBlockingDeque.class);
defaultCollectionImplClasses.put(BlockingQueue.class, LinkedBlockingQueue.class);
defaultCollectionImplClasses.put(Collection.class, LinkedList.class);
defaultCollectionImplClasses.put(ConcurrentMap.class, ConcurrentHashMap.class);
defaultCollectionImplClasses.put(ConcurrentNavigableMap.class, ConcurrentSkipListMap.class);
defaultCollectionImplClasses.put(Deque.class, ArrayDeque.class);
defaultCollectionImplClasses.put(List.class, LinkedList.class);
defaultCollectionImplClasses.put(Map.class, HashMap.class);
defaultCollectionImplClasses.put(NavigableSet.class, TreeSet.class);
defaultCollectionImplClasses.put(NavigableMap.class, TreeMap.class);
defaultCollectionImplClasses.put(Queue.class, LinkedList.class);
defaultCollectionImplClasses.put(Set.class, HashSet.class);
defaultCollectionImplClasses.put(SortedSet.class, TreeSet.class);
defaultCollectionImplClasses.put(SortedMap.class, TreeMap.class);
defaultCollectionImplClasses.put(TransferQueue.class, LinkedTransferQueue.class);
}
public Object instantiateObject(Class<?> clz) throws AtlasException {
if (clz == null) {
throw new AtlasException("Cannot instantiate null class");
}
// TODO https://github.com/atlasmap/atlasmap/issues/48
// - Allow default implementation for abstract target field
// TODO support hierarchical class loader
Class<?> clazz = clz;
if (clazz.isArray()) {
return Array.newInstance(clazz.getComponentType(), 0);
}
if (this.defaultCollectionImplClasses.get(clazz) != null) {
clazz = this.defaultCollectionImplClasses.get(clazz);
}
try {
Constructor<?> constructor = null;
if (clazz.getEnclosingClass() != null && !Modifier.isStatic(clazz.getModifiers())) {
// Nested class requires an instance of enclosing class to instantiate
Object enclosing = clazz.getEnclosingClass()
.getDeclaredConstructor(new Class[0])
.newInstance(new Object[0]);
constructor = clazz.getDeclaredConstructor(new Class[] { enclosing.getClass() });
constructor.setAccessible(true);
return constructor.newInstance(new Object[] { enclosing });
} else {
constructor = clazz.getDeclaredConstructor(new Class[0]);
constructor.setAccessible(true);
return constructor.newInstance(new Object[0]);
}
} catch (Exception e) {
throw new AtlasException("Could not instantiate class: " + clazz.getName(), e);
}
}
public Class<?> loadClass(String name) throws AtlasException {
try {
return this.classLoader.loadClass(name);
} catch (Exception e) {
throw new AtlasException(e);
}
}
public Class<?> getDefaultCollectionImplClass(CollectionType type) {
if (type == CollectionType.LIST) {
return this.defaultCollectionImplClasses.get(List.class);
} else if (type == CollectionType.MAP) {
return this.defaultCollectionImplClasses.get(Map.class);
}
return null;
}
public Object getChildObject(Object parentObject, SegmentContext segment) throws AtlasException {
String fieldName = segment.getName();
if (LOG.isDebugEnabled()) {
LOG.debug(
"Retrieving child '" + fieldName + "'.\n\tparentObject: " + parentObject);
}
if (parentObject == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Cannot find child '" + fieldName + "', parent is null.");
}
return null;
}
Method getterMethod = resolveGetterMethod(parentObject.getClass(), fieldName);
if (getterMethod == null) {
if (LOG.isDebugEnabled()) {
LOG.debug(String.format(
"Unable to detect getter method for: %s on parent: %s",
fieldName, parentObject));
}
return null;
}
getterMethod.setAccessible(true);
Object childObject;
try {
childObject = getterMethod.invoke(parentObject);
} catch (Exception e) {
throw new AtlasException(e);
}
if (LOG.isDebugEnabled()) {
if (childObject == null) {
LOG.debug("Could not find child object for path: " + fieldName);
} else {
LOG.debug("Found child object for path '" + fieldName + "': " + childObject);
}
}
return childObject;
}
public Object createComplexChildObject(Object parentObject, SegmentContext segmentContext, Class<?> clazz) throws AtlasException {
if (LOG.isDebugEnabled()) {
LOG.debug("Creating object for segment:'{} \n\tparentObject: {} \n\tclass: {}",
segmentContext, parentObject, clazz.getName());
}
try {
Method setterMethod = resolveSetterMethod(parentObject, segmentContext, null);
Object targetObject = instantiateObject(clazz);
setterMethod.setAccessible(true);
setterMethod.invoke(parentObject, targetObject);
return targetObject;
} catch (Exception e) {
try {
java.lang.reflect.Field field = resolveField(parentObject.getClass(), segmentContext.getName());
field.setAccessible(true);
Object targetObject = instantiateObject(clazz);
field.set(parentObject, targetObject);
return targetObject;
} catch (Exception e2) {
String parentClassName = parentObject == null ? null : parentObject.getClass().getName();
throw new AtlasException("Unable to create value for segment: " + segmentContext.getExpression() + " parentObject: "
+ parentClassName, e2);
}
}
}
public Object createComplexChildObject(Object parentObject, SegmentContext segmentContext) throws AtlasException {
if (LOG.isDebugEnabled()) {
LOG.debug("Creating object for segment:'{} \n\tparentObject: {}", segmentContext, parentObject);
}
Class<?> clazz;
try {
Method setterMethod = resolveSetterMethod(parentObject, segmentContext, null);
clazz = setterMethod.getParameterTypes()[0];
Object targetObject = instantiateObject(clazz);
setterMethod.setAccessible(true);
setterMethod.invoke(parentObject, targetObject);
return targetObject;
} catch (Exception e) {
try {
java.lang.reflect.Field field = resolveField(parentObject.getClass(), segmentContext.getName());
field.setAccessible(true);
clazz = field.getType();
Object targetObject = instantiateObject(clazz);
field.set(parentObject, targetObject);
return targetObject;
} catch (Exception e2) {
String parentClassName = parentObject == null ? null : parentObject.getClass().getName();
throw new AtlasException("Unable to create value for segment: " + segmentContext.getExpression() + " parentObject: "
+ parentClassName, e2);
}
}
}
public void setChildObject(Object parentObject, Object childObject, SegmentContext segmentContext) throws AtlasException {
if (LOG.isDebugEnabled()) {
LOG.debug("Setting object for segment:'" + segmentContext.getExpression() + "'.\n\tchildObject: " + childObject
+ "\n\tparentObject: " + parentObject);
}
try {
Class<?> childClass = childObject == null ? null : childObject.getClass();
Object targetObject = parentObject;
try {
Method setterMethod = resolveSetterMethod(parentObject, segmentContext, childClass);
Class<?> targetClass = setterMethod.getParameterTypes()[0];
if (childObject != null) {
childObject = conversionService.convertType(childObject, null, targetClass, null);
}
// We already know we have a 1 paramter setter here
if (childObject == null && conversionService.isPrimitive(targetClass)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Not setting null value for primitive method paramter for segment:'" + segmentContext.getExpression()
+ "'.\n\tchildObject: " + childObject + "\n\tparentObject: " + parentObject);
}
return;
}
setterMethod.setAccessible(true);
setterMethod.invoke(targetObject, childObject);
} catch (Exception e) {
java.lang.reflect.Field field = resolveField(targetObject.getClass(), segmentContext.getName());
if (field == null) {
String parentClassName = parentObject == null ? null : parentObject.getClass().getName();
String childClassName = childObject == null ? null : childObject.getClass().getName();
throw new AtlasException(String.format(
"Unable to set value for segment: %s parentObject: %s childObject: %s",
segmentContext.getExpression(), parentClassName, childClassName), e);
}
if (childObject != null) {
childObject = conversionService.convertType(childObject, null, field.getType(), null);
}
if (childObject == null && conversionService.isPrimitive(field.getType())) {
if (LOG.isDebugEnabled()) {
LOG.debug("Not setting null value for primitive method paramter for segment:'" + segmentContext.getExpression()
+ "'.\n\tchildObject: " + childObject + "\n\tparentObject: " + parentObject);
}
return;
}
field.setAccessible(true);
field.set(targetObject, childObject);
}
} catch (Exception e) {
String parentClassName = parentObject == null ? null : parentObject.getClass().getName();
String childClassName = childObject == null ? null : childObject.getClass().getName();
throw new AtlasException(String.format(
"Unable to set value for segment: %s parentObject: %s childObject: %s",
segmentContext, parentClassName, childClassName), e);
}
}
public Class<?> resolveChildClass(Object parentObject, SegmentContext segment) throws AtlasException {
try {
Method setterMethod = resolveSetterMethod(parentObject, segment, null);
return setterMethod.getParameterTypes()[0];
} catch (NoSuchMethodException e) {
try {
java.lang.reflect.Field field = resolveField(parentObject.getClass(), segment.getName());
field.setAccessible(true);
return field.getType();
} catch (Exception e2) {
String parentClassName = parentObject == null ? null : parentObject.getClass().getName();
throw new AtlasException("Unable to create value for segment: " + segment + " parentObject: "
+ parentClassName, e2);
}
}
}
public Object getCollectionItem(Object collectionObject, SegmentContext segmentContext) throws AtlasException {
Integer index = segmentContext.getCollectionIndex();
if (index == null) {
return new AtlasException("Collection item is requested without an index");
}
if (segmentContext.getCollectionType() == CollectionType.ARRAY) {
return Array.getLength(collectionObject) > index ? Array.get(collectionObject, index) : null;
} else if (segmentContext.getCollectionType() == CollectionType.LIST) {
if (collectionObject instanceof List) {
List<?> list = (List<?>)collectionObject;
return list.size() > index ? list.get(index) : null;
} else {
LOG.warn("Converting non-List Collection into array - order might not be preserved: segment={}",
segmentContext.getExpression());
Object[] array = ((Collection<?>)collectionObject).toArray();
return array.length > index ? array[index] : null;
}
} else if (segmentContext.getCollectionType() == CollectionType.MAP) {
throw new AtlasException("TODO: java.util.Map is not yet supported, segment: " + segmentContext.getExpression());
}
throw new AtlasException("Cannot determine collection type from segment: " + segmentContext.getExpression());
}
public Object adjustCollectionSize(Object collectionObject, SegmentContext segmentContext) throws AtlasException {
Object answer = collectionObject;
Integer index = segmentContext.getCollectionIndex();
if (segmentContext.getCollectionType() == CollectionType.MAP) {
LOG.warn("It doesn't make sense to adjust the size of {}, Ignoring... {}", CollectionType.MAP, segmentContext);
return answer;
}
if (index == null) {
throw new AtlasException(String.format(
"No index was specified for adjusting collection size, segment=%s", segmentContext.getExpression()));
}
if (answer.getClass().isArray()) {
if (Array.getLength(answer) < (index + 1)) {
Object newArray = Array.newInstance(answer.getClass().getComponentType(), segmentContext.getCollectionIndex() + 1);
// copy pre-existing items over to new array
for (int i = 0; i < Array.getLength(answer); i++) {
Array.set(newArray, i, Array.get(answer, i));
}
answer = newArray;
}
} else if (answer instanceof List) {
List<?> list = (List<?>) answer;
while (list.size() < index + 1) {
int size = list.size();
list.add(null);
}
} else if (answer instanceof Collection) {
LOG.warn("Collection object other than List doesn't support indexed operation. Ignoring... "
+ "segment: {} \n\tparentObject: {}", segmentContext, collectionObject);
}
return answer;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public Object createComplexCollectionItem(Object collectionObject, Class<?> itemType, SegmentContext segmentContext) throws AtlasException {
Integer index = segmentContext.getCollectionIndex();
if (segmentContext.getCollectionType() != CollectionType.MAP && index == null) {
throw new AtlasException(String.format(
"No index was specified for setting collection item, segment=%s", segmentContext.getExpression()));
}
if (collectionObject.getClass().isArray()) {
if (index >= Array.getLength(collectionObject)) {
throw new AtlasException("Cannot fit item in array, array size: " + Array.getLength(collectionObject)
+ ", item index: " + index + ", segment: " + segmentContext);
}
Class<?> clazz = collectionObject.getClass().getComponentType();
if (clazz.isPrimitive()) {
return Array.get(collectionObject, index);
}
Object answer = instantiateObject(itemType != null ? itemType : clazz);
Array.set(collectionObject, index, answer);
return answer;
} else if (collectionObject instanceof Collection) {
Object newItem = null;
newItem = instantiateObject(itemType);
Collection collection = (Collection) collectionObject;
if (collection instanceof List) {
if (index > collection.size()) {
throw new AtlasException("Cannot fit item in list, list size: " + collection.size()
+ ", item index: " + index + ", segment: " + segmentContext);
}
List list = (List) collection;
list.set(index, newItem);
} else if (index == collection.size()) {
collection.add(newItem);
} else {
LOG.warn(String.format(
"Writing into non-List collection - it will be added as a last element anyway. "
+ "segment: %s \n\tparentObject: %s\n\tchild: %s",
segmentContext, collectionObject, newItem));
collection.add(newItem);
}
return newItem;
} else if (collectionObject instanceof Map) {
throw new AtlasException("TODO: Cannot yet handle adding children to maps");
}
throw new AtlasException("Cannot determine collection type for: " + collectionObject);
}
public Object createComplexCollectionItem(Object parentObject, Object collectionObject, SegmentContext segmentContext) throws AtlasException {
Class<?> itemClazz = resolveCollectionItemClass(parentObject, segmentContext);
return createComplexCollectionItem(collectionObject, itemClazz, segmentContext);
}
public Class<?> resolveCollectionItemClass(Object parentObject, SegmentContext segmentContext) throws AtlasException {
Class<?> itemType = null;
Method getterMethod = resolveGetterMethod(parentObject.getClass(), segmentContext.getName());
try {
Type genericType = null;
if (getterMethod != null) {
genericType = getterMethod.getGenericReturnType();
} else {
java.lang.reflect.Field field = resolveField(parentObject.getClass(), segmentContext.getName());
if (field == null) {
throw new AtlasException(String.format(
"Failed to create a collection item, parent class={}, field name={}",
parentObject.getClass(), segmentContext.getName()));
}
genericType = field.getGenericType();
}
if (genericType instanceof Class) {
if (((Class<?>)genericType).isArray()) {
itemType = ((Class<?>)genericType).getComponentType();
} else {
itemType = Object.class;
}
} else if (genericType instanceof ParameterizedType
&& ((ParameterizedType) genericType).getActualTypeArguments().length > 0) {
String typeArg = ((ParameterizedType) genericType).getActualTypeArguments()[0].getTypeName();
itemType = classLoader.loadClass(typeArg);
} else {
itemType = Object.class;
}
} catch (Throwable t) {
throw new AtlasException(String.format(
"Failed to resolve collection item class, parent class={}, field name={}",
parentObject.getClass(), segmentContext.getName()), t);
}
return itemType;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public void setCollectionItem(Object collectionObject, Object item, SegmentContext segmentContext)
throws AtlasException {
Integer index = segmentContext.getCollectionIndex();
if (segmentContext.getCollectionType() != CollectionType.MAP && index == null) {
throw new AtlasException(String.format(
"No index was specified for setting collection item, segment=%s", segmentContext.getExpression()));
}
if (collectionObject.getClass().isArray()) {
if (index >= Array.getLength(collectionObject)) {
throw new AtlasException("Cannot fit item in array, array size: " + Array.getLength(collectionObject)
+ ", item index: " + index + ", segment: " + segmentContext);
}
try {
Array.set(collectionObject, index, item);
} catch (Exception e) {
String parentClass = collectionObject == null ? null : collectionObject.getClass().getName();
String childClass = item == null ? null : item.getClass().getName();
throw new AtlasException("Could not set child class '" + childClass + "' on parent '" + parentClass
+ "' for: " + segmentContext, e);
}
return;
} else if (collectionObject instanceof Collection) {
Collection collection = (Collection) collectionObject;
if (collection instanceof List) {
if (index > collection.size()) {
throw new AtlasException("Cannot fit item in list, list size: " + collection.size()
+ ", item index: " + index + ", segment: " + segmentContext);
}
List list = (List) collection;
list.set(index, item);
} else if (index == collection.size()) {
collection.add(item);
} else {
LOG.warn(String.format(
"Writing into non-List collection - it will be added as a last element anyway. "
+ "segment: %s \n\tparentObject: %s\n\tchild: %s",
segmentContext, collectionObject, item));
collection.add(item);
}
return;
} else if (collectionObject instanceof Map) {
throw new AtlasException("TODO: Cannot yet handle adding children to maps");
}
throw new AtlasException("Cannot determine collection type for: " + collectionObject);
}
public Map<Class<?>, Class<?>> getDefaultCollectionImplClasses() {
return this.defaultCollectionImplClasses;
}
private Method resolveGetterMethod(Class<?> clz, String fieldName) {
List<String> getters = ClassHelper.getterMethodNames(fieldName);
List<Class<?>> classTree = resolveMappableClasses(clz);
Method getterMethod = null;
for (Class<?> clazz : classTree) {
for (String getter : getters) {
try {
return ClassHelper.detectGetterMethod(clazz, getter);
} catch (NoSuchMethodException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Looking for getter for '{}' on this class: {}",
fieldName, clazz.getName(), e);
}
}
}
}
return getterMethod;
}
private Method resolveSetterMethod(Object sourceObject, SegmentContext segmentContext, Class<?> targetType)
throws NoSuchMethodException {
String setterMethodName = "set" + capitalizeFirstLetter(segmentContext.getName());
List<Class<?>> classTree = resolveMappableClasses(sourceObject.getClass());
Method m = null;
for (Class<?> clazz : classTree) {
if (LOG.isDebugEnabled()) {
LOG.debug("Looking for setter '" + setterMethodName + "' on this class: " + clazz.getName());
}
try {
m = ClassHelper.detectSetterMethod(clazz, setterMethodName, targetType);
if (m != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Found setter '" + setterMethodName + "' on this class: " + clazz.getName());
}
return m;
}
} catch (NoSuchMethodException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Did not find setter '" + setterMethodName + "' on this class: " + clazz.getName(), e);
}
}
// Try the boxUnboxed version
if (conversionService.isPrimitive(targetType) || conversionService.isBoxedPrimitive(targetType)) {
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Looking for boxed setter '" + setterMethodName + "' on this class: "
+ clazz.getName());
}
m = ClassHelper.detectSetterMethod(clazz, setterMethodName,
conversionService.boxOrUnboxPrimitive(targetType));
if (m != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Found setter '" + setterMethodName + "' on this class: " + clazz.getName());
}
return m;
}
} catch (NoSuchMethodException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Did not find setter '" + setterMethodName + "' on this class: " + clazz.getName(),
e);
}
}
}
}
throw new NoSuchMethodException("Unable to resolve expected setter '" + setterMethodName + "' for segment: "
+ segmentContext.getExpression() + ", on object: " + sourceObject);
}
private java.lang.reflect.Field resolveField(Class<?> clz, String name) {
List<Class<?>> classTree = resolveMappableClasses(clz);
for (Class<?> clazz : classTree) {
try {
return clazz.getDeclaredField(name);
} catch (Exception e) {
continue;
}
}
return null;
}
private static String capitalizeFirstLetter(String string) {
if (StringUtil.isEmpty(string)) {
return string;
}
if (string.length() == 1) {
return String.valueOf(string.charAt(0)).toUpperCase();
}
return String.valueOf(string.charAt(0)).toUpperCase() + string.substring(1);
}
private List<Class<?>> resolveMappableClasses(Class<?> clazz) {
List<Class<?>> classTree = new ArrayList<>();
classTree.add(clazz);
Class<?> superClazz = clazz.getSuperclass();
while (superClazz != null) {
if (JdkPackages.contains(superClazz.getPackage().getName())) {
superClazz = null;
} else {
classTree.add(superClazz);
superClazz = superClazz.getSuperclass();
}
}
// DON'T reverse.. prefer child -> parent -> grandparent
// List<Class<?>> reverseTree = classTree.subList(0, classTree.size());
// Collections.reverse(reverseTree);
// return reverseTree;
if (LOG.isDebugEnabled()) {
LOG.debug("Found " + classTree.size() + " mappable classes for class '"
+ clazz.getName() + "': " + classTree);
}
return classTree;
}
}