/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.felix.converter.impl; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Dictionary; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.apache.felix.converter.dto.DTOUtil; import org.osgi.util.converter.ConversionException; import org.osgi.util.converter.Converter; import org.osgi.util.converter.Converting; import org.osgi.util.converter.TypeReference; public class ConvertingImpl implements Converting, InternalConverting { private static final Map<Class<?>, Class<?>> INTERFACE_IMPLS; static { Map<Class<?>, Class<?>> m = new HashMap<>(); m.put(Collection.class, ArrayList.class); m.put(List.class, ArrayList.class); m.put(Set.class, LinkedHashSet.class); // preserves insertion order m.put(Map.class, LinkedHashMap.class); // preserves insertion order INTERFACE_IMPLS = Collections.unmodifiableMap(m); } private static final Collection<Class<?>> NO_MAP_VIEW_TYPES; static { NO_MAP_VIEW_TYPES = Arrays.asList(String.class); } volatile InternalConverter converter; private volatile Object object; private volatile Object defaultValue; private volatile boolean hasDefault = false; private volatile boolean keysIgnoreCase = false; volatile Class<?> sourceClass; volatile Class<?> sourceAsClass; private volatile Class<?> targetClass; private volatile Class<?> targetAsClass; volatile Type[] typeArguments; private volatile boolean forceCopy = false; private volatile boolean sourceAsJavaBean = false; private volatile boolean targetAsJavaBean = false; private volatile boolean sourceAsDTO = false; private volatile boolean targetAsDTO = false; ConvertingImpl(InternalConverter c, Object obj) { converter = c; object = obj; } @Override public Converting sourceAs(Class<?> cls) { sourceAsClass = cls; return this; } @Override public Converting sourceAsBean() { // To avoid ambiguity, reset any instruction to sourceAsDTO sourceAsDTO = false; sourceAsJavaBean = true; return this; } @Override public Converting sourceAsDTO() { // To avoid ambiguity, reset any instruction to sourceAsJavaBean sourceAsJavaBean = false; sourceAsDTO = true; return this; } @Override public Converting targetAs(Class<?> cls) { targetAsClass = cls; return this; } @Override public Converting targetAsBean() { // To avoid ambiguity, reset any instruction to targetAsDTO targetAsDTO = false; targetAsJavaBean = true; return this; } @Override public Converting targetAsDTO() { // To avoid ambiguity, reset any instruction to targetAsJavaBean targetAsJavaBean = false; targetAsDTO = true; return this; } @Override public Converting copy() { forceCopy = true; return this; } @Override public Converting defaultValue(Object defVal) { defaultValue = defVal; hasDefault = true; return this; } @Override public Converting keysIgnoreCase() { keysIgnoreCase = true; return this; } @Override public void setConverter(Converter c) { if (c instanceof InternalConverter) converter = (InternalConverter) c; else throw new IllegalStateException("Incorrect converter used. Should implement " + InternalConverter.class + " but was " + c); } @SuppressWarnings("unchecked") @Override public <T> T to(Class<T> cls) { Type type = cls; return (T) to(type); } @SuppressWarnings("unchecked") @Override public <T> T to(TypeReference<T> ref) { return (T) to(ref.getType()); } @SuppressWarnings("unchecked") @Override public Object to(Type type) { Class<?> cls = null; if (type instanceof Class) { cls = (Class<?>) type; } else if (type instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) type; Type rt = pt.getRawType(); typeArguments = pt.getActualTypeArguments(); if (rt instanceof Class) cls = (Class<?>) rt; } if (cls == null) return null; if (object == null) return handleNull(cls); targetClass = Util.primitiveToBoxed(cls); if (targetAsClass == null) targetAsClass = targetClass; sourceClass = sourceAsClass != null ? sourceAsClass : object.getClass(); if (!isCopyRequiredType(targetAsClass) && targetAsClass.isAssignableFrom(sourceClass)) { return object; } Object res = trySpecialCases(); if (res != null) return res; if (targetAsClass.isArray()) { return convertToArray(); } else if (Collection.class.isAssignableFrom(targetAsClass)) { return convertToCollection(); } else if (DTOUtil.isDTOType(targetAsClass)) { return convertToDTO(); } else if (isMapType(targetAsClass, targetAsJavaBean)) { return convertToMapType(); } // At this point we know that the target is a 'singular' type: not a map, collection or array if (Collection.class.isAssignableFrom(sourceClass)) { return convertCollectionToSingleValue(cls); } else if ((object = asBoxedArray(object)) instanceof Object[]) { return convertArrayToSingleValue(cls); } Object res2 = tryStandardMethods(); if (res2 != null) { return res2; } else { if (defaultValue != null) return converter.convert(defaultValue).sourceAs(sourceAsClass).targetAs(targetAsClass).to(targetClass); else throw new ConversionException("Cannot convert " + object + " to " + targetAsClass); } } private Object convertArrayToSingleValue(Class<?> cls) { Object[] arr = (Object[]) object; if (arr.length == 0) return null; else return converter.convert(arr[0]).to(cls); } private Object convertCollectionToSingleValue(Class<?> cls) { Collection<?> coll = (Collection<?>) object; if (coll.size() == 0) return null; else return converter.convert(coll.iterator().next()).to(cls); } @SuppressWarnings("unchecked") private <T> T convertToArray() { Collection<?> collectionView = collectionView(object); Iterator<?> itertor = collectionView.iterator(); try { Object array = Array.newInstance(targetAsClass.getComponentType(), collectionView.size()); for (int i=0; i<collectionView.size() && itertor.hasNext(); i++) { Object next = itertor.next(); Object converted = converter.convert(next).to(targetAsClass.getComponentType()); Array.set(array, i, converted); } return (T) array; } catch (Exception e) { return null; } } @SuppressWarnings({ "unchecked", "rawtypes" }) private <T> T convertToCollection() { Collection<?> cv = collectionView(object); Class<?> targetElementType = null; if (typeArguments != null && typeArguments.length > 0 && typeArguments[0] instanceof Class) { targetElementType = (Class<?>) typeArguments[0]; } Class<?> ctrCls = INTERFACE_IMPLS.get(targetAsClass); Class<?>targetCls; if (ctrCls != null) targetCls = ctrCls; else targetCls = targetAsClass; Collection instance = (Collection) createMapOrCollection(targetCls, cv.size()); if (instance == null) return null; for (Object o : cv) { if (targetElementType != null) o = converter.convert(o).to(targetElementType); instance.add(o); } return (T) instance; } @SuppressWarnings({ "rawtypes", "unchecked" }) private <T> T convertToDTO() { Map m = mapView(object, sourceClass, converter); Class<?> cls = targetAsClass; if (targetAsDTO) cls = targetClass; try { String prefix = Util.getPrefix(cls); T dto = (T) targetClass.newInstance(); for (Map.Entry entry : (Set<Map.Entry>) m.entrySet()) { Object key = entry.getKey(); if (key == null) continue; String fieldName = Util.mangleName(prefix, key.toString()); if (fieldName == null) continue; Field f = null; try { f = cls.getDeclaredField(fieldName); } catch (NoSuchFieldException e) { try { f = cls.getField(fieldName); } catch (NoSuchFieldException | NullPointerException e1) { // There is no field with this name if (keysIgnoreCase) { // If enabled, try again but now ignore case for (Field fs : cls.getDeclaredFields()) { if (fs.getName().equalsIgnoreCase(fieldName)) { f = fs; break; } } if (f == null) { for (Field fs : cls.getFields()) { if (fs.getName().equalsIgnoreCase(fieldName)) { f = fs; break; } } } } } } if (f != null) { Object val = entry.getValue(); if (sourceAsDTO && DTOUtil.isDTOType(f.getType())) val = converter.convert(val).sourceAsDTO().to(f.getType()); else val = converter.convert(val).to(f.getType()); f.set(dto, val); } } return dto; } catch (Exception e) { throw new ConversionException("Cannot create DTO " + targetClass, e); } } @SuppressWarnings({ "rawtypes", "unchecked" }) private Map convertToMap() { Map m = mapView(object, sourceClass, converter); if (m == null) return null; Class<?> ctrCls = INTERFACE_IMPLS.get(targetClass); if (ctrCls == null) ctrCls = targetClass; Map instance = (Map) createMapOrCollection(ctrCls, m.size()); if (instance == null) return null; for (Map.Entry entry : (Set<Entry>) m.entrySet()) { Object key = entry.getKey(); Object value = entry.getValue(); value = convertMapValue(value); instance.put(key, value); } return instance; } Object convertMapValue(Object value) { Type targetValueType = null; if (typeArguments != null && typeArguments.length > 1) { targetValueType = typeArguments[1]; } if (value != null) { if (targetValueType != null) { value = converter.convert(value).to(targetValueType); } else { Class<?> cls = value.getClass(); if (isCopyRequiredType(cls)) { cls = getConstructableType(cls); } if (sourceAsDTO && DTOUtil.isDTOType(cls)) value = converter.convert(value).sourceAsDTO().to(cls); else value = converter.convert(value).to(cls); } } return value; } @SuppressWarnings({ "unchecked", "rawtypes" }) private Map convertToMapDelegate() { if (Map.class.isAssignableFrom(sourceClass)) { return MapDelegate.forMap((Map) object, this); } else if (Dictionary.class.isAssignableFrom(sourceClass)) { return MapDelegate.forDictionary((Dictionary) object, this); } else if (DTOUtil.isDTOType(sourceClass) || sourceAsDTO) { return MapDelegate.forDTO(object, this); } else if (sourceAsJavaBean) { return MapDelegate.forBean(object, this); } // Assume it's an interface return MapDelegate.forInterface(object, this); } @SuppressWarnings({ "unchecked", "rawtypes" }) private Object convertToMapType() { if (Map.class.equals(targetClass) && !forceCopy) { Map res = convertToMapDelegate(); if (res != null) return res; } if (Map.class.isAssignableFrom(targetAsClass)) return convertToMap(); else if (Dictionary.class.isAssignableFrom(targetAsClass)) return new Hashtable((Map) converter.convert(object).to(new ParameterizedType() { @Override public Type getRawType() { return HashMap.class; } @Override public Type getOwnerType() { return null; } @Override public Type[] getActualTypeArguments() { return typeArguments; } })); else if (targetAsClass.isInterface()) return createInterface(sourceClass, targetAsClass); return createJavaBean(sourceClass, targetAsClass); } private Object createJavaBean(Class<?> sourceCls, Class<?> targetCls) { @SuppressWarnings("rawtypes") Map m = mapView(object, sourceCls, converter); try { Object res = targetClass.newInstance(); for (Method setter : getSetters(targetCls)) { String setterName = setter.getName(); StringBuilder propName = new StringBuilder(Character.valueOf(Character.toLowerCase(setterName.charAt(3))).toString()); if (setterName.length() > 4) propName.append(setterName.substring(4)); Class<?> setterType = setter.getParameterTypes()[0]; setter.invoke(res, converter.convert(m.get(propName.toString())).to(setterType)); } return res; } catch (Exception e) { throw new ConversionException("Cannot convert to class: " + targetCls.getName() + ". Not a JavaBean with a Zero-arg Constructor.", e); } } @SuppressWarnings("rawtypes") private Object createInterface(Class<?> sourceCls, Class<?> targetCls) { Map m = mapView(object, sourceCls, converter); return Proxy.newProxyInstance(targetCls.getClassLoader(), new Class[] {targetCls}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Class<?> mdDecl = method.getDeclaringClass(); if (mdDecl.equals(Object.class)) switch (method.getName()) { case "equals": return proxy == args[0]; case "hashCode": return System.identityHashCode(proxy); case "toString": return "Proxy for " + targetCls; default: throw new UnsupportedOperationException("Method " + method + " not supported on proxy for " + targetCls); } if (mdDecl.equals(Annotation.class)) { if ("annotationType".equals(method.getName()) && method.getParameterTypes().length == 0) { return targetCls; } } String propName = Util.getInterfacePropertyName(method, Util.getSingleElementAnnotationKey(targetCls, proxy), proxy); if (propName == null) return null; Class<?> targetType = method.getReturnType(); Object val = m.get(propName); if (val == null && keysIgnoreCase) { // try in a case-insensitive way for (Iterator it = m.keySet().iterator(); it.hasNext() && val == null; ) { String k = it.next().toString(); if (propName.equalsIgnoreCase(k)) { val = m.get(k); } } } // If no value is available take the default if specified if (val == null) { if (targetCls.isAnnotation()) { val = method.getDefaultValue(); } if (val == null && args != null && args.length == 1) { val = args[0]; } if (val == null) throw new ConversionException("No value for property: " + propName); } return converter.convert(val).to(targetType); } }); } private Object handleNull(Class<?> cls) { if (hasDefault) return converter.convert(defaultValue).to(cls); Class<?> boxed = Util.primitiveToBoxed(cls); if (boxed.equals(cls)) { if (cls.isArray()) { return new Object[] {}; } else if (Collection.class.isAssignableFrom(cls)) { return converter.convert(Collections.emptyList()).to(cls); } // This is not a primitive, just return null return null; } if (cls.equals(boolean.class)) { return false; } else if (cls.equals(long.class) ) { return 0L; } else if (cls.equals(double.class) ) { return 0.0; } return 0; } private static boolean isMapType(Class<?> cls, boolean asJavaBean) { // All interface types that are not Collections are treated as maps if (Map.class.isAssignableFrom(cls)) return true; else if (cls.isInterface() && !Collection.class.isAssignableFrom(cls)) return true; else if (DTOUtil.isDTOType(cls)) return true; else if (asJavaBean && isWriteableJavaBean(cls)) return true; else return Dictionary.class.isAssignableFrom(cls); } private Object trySpecialCases() { // TODO some of these can probably be implemented as an adapter if (Boolean.class.equals(targetAsClass)) { if (object instanceof Number) { return ((Number) object).longValue() != 0; } else if (object instanceof Collection && ((Collection<?>) object).size() == 0) { // TODO What about arrays? return Boolean.FALSE; } } else if (Character.class.equals(targetAsClass)) { if (object instanceof Number) { return Character.valueOf((char) ((Number) object).intValue()); } } else if (Number.class.isAssignableFrom(targetAsClass)) { if (object instanceof Boolean) { return ((Boolean) object).booleanValue() ? 1 : 0; } else if (object instanceof Number) { if (Byte.class.isAssignableFrom(targetAsClass)) { return ((Number) object).byteValue(); } else if (Short.class.isAssignableFrom(targetAsClass)) { return ((Number) object).shortValue(); } else if (Integer.class.isAssignableFrom(targetAsClass)) { return ((Number) object).intValue(); } else if (Long.class.isAssignableFrom(targetAsClass)) { return ((Number) object).longValue(); } else if (Float.class.isAssignableFrom(targetAsClass)) { return ((Number) object).floatValue(); } else if (Double.class.isAssignableFrom(targetAsClass)) { return ((Number) object).doubleValue(); } } } else if (Class.class.equals(targetAsClass)) { if (object instanceof Collection && ((Collection<?>) object).size() == 0) { return null; } } else if (Enum.class.isAssignableFrom(targetAsClass)) { if (object instanceof Number) { try { Method m = targetAsClass.getMethod("values"); Object[] values = (Object[]) m.invoke(null); return values[((Number) object).intValue()]; } catch (Exception e) { throw new RuntimeException(e); } } else { try { Method m = targetAsClass.getMethod("valueOf", String.class); return m.invoke(null, object.toString()); } catch (Exception e) { try { // Case insensitive fallback Method m = targetAsClass.getMethod("values"); for (Object v : (Object[]) m.invoke(null)) { if (v.toString().equalsIgnoreCase(object.toString())) { return v; } } } catch (Exception e1) { throw new RuntimeException(e1); } } } } return null; } @SuppressWarnings("unchecked") private <T> T tryStandardMethods() { try { Method m = targetAsClass.getDeclaredMethod("valueOf", String.class); if (m != null) { return (T) m.invoke(null, object.toString()); } } catch (Exception e) { try { Constructor<?> ctr = targetAsClass.getConstructor(String.class); return (T) ctr.newInstance(object.toString()); } catch (Exception e2) { } } return null; } private static Collection<?> collectionView(Object obj) { if (obj == null) return null; Collection<?> c = asCollection(obj); if (c == null) return Collections.singleton(obj); else return c; } private static Collection<?> asCollection(Object obj) { if (obj instanceof Collection) return (Collection<?>) obj; else if ((obj = asBoxedArray(obj)) instanceof Object[]) return Arrays.asList((Object[]) obj); else return null; } private static Object asBoxedArray(Object obj) { Class<?> objClass = obj.getClass(); if (!objClass.isArray()) return obj; int len = Array.getLength(obj); Object arr = Array.newInstance(Util.primitiveToBoxed(objClass.getComponentType()), len); for (int i=0; i<len; i++) { Object val = Array.get(obj, i); Array.set(arr, i, val); } return arr; } @SuppressWarnings("rawtypes") private static Map createMapFromBeanAccessors(Object obj, Class<?> sourceCls) { Set<String> invokedMethods = new HashSet<>(); Map result = new HashMap(); for (Method md : sourceCls.getDeclaredMethods()) { handleBeanMethod(obj, md, invokedMethods, result); } return result; } @SuppressWarnings("rawtypes") private Map createMapFromDTO(Object obj, InternalConverter converter) { Set<String> handledFields = new HashSet<>(); Map result = new HashMap(); // Do we need 'declaredfields'? We only need to look at the public ones... for (Field f : obj.getClass().getDeclaredFields()) { handleDTOField(obj, f, handledFields, result, converter); } for (Field f : obj.getClass().getFields()) { handleDTOField(obj, f, handledFields, result, converter); } return result; } @SuppressWarnings("rawtypes") private static Map createMapFromInterface(Object obj) { Map result = new HashMap(); for (Class i : obj.getClass().getInterfaces()) { for (Method md : i.getMethods()) { handleInterfaceMethod(obj, i, md, new HashSet<>(), result); } if (result.size() > 0) return result; } throw new ConversionException("Cannot be converted to map: " + obj); } private static Object createMapOrCollection(Class<?> cls, int initialSize) { try { Constructor<?> ctor = cls.getConstructor(int.class); return ctor.newInstance(initialSize); } catch (Exception e1) { try { Constructor<?> ctor2 = cls.getConstructor(); return ctor2.newInstance(); } catch (Exception e2) { // ignore } } return null; } private static Class<?> getConstructableType(Class<?> targetCls) { if (targetCls.isArray()) return targetCls; Class<?> cls = targetCls; do { try { cls.getConstructor(int.class); return cls; // If no exception the constructor is there } catch (NoSuchMethodException e) { try { cls.getConstructor(); return cls; // If no exception the constructor is there } catch (NoSuchMethodException e1) { // There is no constructor with this name } } for (Class<?> intf : cls.getInterfaces()) { Class<?> impl = INTERFACE_IMPLS.get(intf); if (impl != null) return impl; } cls = cls.getSuperclass(); } while (!Object.class.equals(cls)); return null; } @SuppressWarnings({ "rawtypes", "unchecked" }) private void handleDTOField(Object obj, Field field, Set<String> handledFields, Map result, InternalConverter converter) { String fn = Util.getDTOKey(field); if (fn == null) return; if (handledFields.contains(fn)) return; // Field with this name was already handled try { Object fVal = field.get(obj); result.put(fn, fVal); handledFields.add(fn); } catch (Exception e) { } } @SuppressWarnings({ "rawtypes", "unchecked" }) private static void handleBeanMethod(Object obj, Method md, Set<String> invokedMethods, Map res) { String bp = Util.getBeanKey(md); if (bp == null) return; if (invokedMethods.contains(bp)) return; // method with this name already invoked try { res.put(bp, md.invoke(obj)); invokedMethods.add(bp); } catch (Exception e) { } } @SuppressWarnings({ "rawtypes", "unchecked" }) private static void handleInterfaceMethod(Object obj, Class<?> intf, Method md, Set<String> invokedMethods, Map res) { String mn = md.getName(); if (invokedMethods.contains(mn)) return; // method with this name already invoked String propName = Util.getInterfacePropertyName(md, Util.getSingleElementAnnotationKey(intf, obj), obj); if (propName == null) return; try { Object r = Util.getInterfaceProperty(obj, md); if (r == null) return; res.put(propName, r); invokedMethods.add(mn); } catch (Exception e) { } } private Map<?,?> mapView(Object obj, Class<?> sourceCls, InternalConverter converter) { if (Map.class.isAssignableFrom(sourceCls) || (DTOUtil.isDTOType(sourceCls) && obj instanceof Map)) return (Map<?,?>) obj; else if (Dictionary.class.isAssignableFrom(sourceCls)) return null; // TODO else if (DTOUtil.isDTOType(sourceCls) || sourceAsDTO) return createMapFromDTO(obj, converter); else if (sourceAsJavaBean) { Map<?,?> m = createMapFromBeanAccessors(obj, sourceCls); if (m.size() > 0) return m; } else if (typeProhibitedFromMapView(sourceCls)) throw new ConversionException("No map view for " + obj); return createMapFromInterface(obj); } private boolean typeProhibitedFromMapView(Class<?> sourceCls) { return NO_MAP_VIEW_TYPES.contains(sourceCls); } private static boolean isCopyRequiredType(Class<?> cls) { if (cls.isEnum()) return false; return Map.class.isAssignableFrom(cls) || Collection.class.isAssignableFrom(cls) || DTOUtil.isDTOType(cls) || // isJavaBean cls.isArray(); } private static boolean isWriteableJavaBean(Class<?> cls) { boolean hasNoArgCtor = false; for (Constructor<?> ctor : cls.getConstructors()) { if (ctor.getParameterTypes().length == 0) hasNoArgCtor = true; } if (!hasNoArgCtor) return false; // A JavaBean must have a public no-arg constructor return getSetters(cls).size() > 0; } private static Set<Method> getSetters(Class<?> cls) { Set<Method> setters = new HashSet<>(); while (!Object.class.equals(cls)) { Set<Method> methods = new HashSet<>(); methods.addAll(Arrays.asList(cls.getDeclaredMethods())); methods.addAll(Arrays.asList(cls.getMethods())); for (Method md : methods) { if (md.getParameterTypes().length != 1) continue; // Only setters with a single argument String name = md.getName(); if (name.length() < 4) continue; if (name.startsWith("set") && Character.isUpperCase(name.charAt(3))) setters.add(md); } cls = cls.getSuperclass(); } return setters; } }