Author: davidb Date: Mon Feb 6 11:54:34 2017 New Revision: 1781884 URL: http://svn.apache.org/viewvc?rev=1781884&view=rev Log: Felix Converter - support dynamic maps that reflect changes in the backing object.
Added: felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/DynamicMapLikeFacade.java felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/MapDelegate.java Modified: felix/trunk/converter/converter/pom.xml felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/ConvertingImpl.java felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/Util.java felix/trunk/converter/converter/src/test/java/org/apache/felix/converter/impl/ConverterBuilderTest.java felix/trunk/converter/converter/src/test/java/org/apache/felix/converter/impl/ConverterEqualsTest.java felix/trunk/converter/converter/src/test/java/org/apache/felix/converter/impl/ConverterTest.java Modified: felix/trunk/converter/converter/pom.xml URL: http://svn.apache.org/viewvc/felix/trunk/converter/converter/pom.xml?rev=1781884&r1=1781883&r2=1781884&view=diff ============================================================================== --- felix/trunk/converter/converter/pom.xml (original) +++ felix/trunk/converter/converter/pom.xml Mon Feb 6 11:54:34 2017 @@ -28,7 +28,7 @@ <name>Apache Felix Converter</name> <artifactId>org.apache.felix.converter</artifactId> - <version>0.1-SNAPSHOT</version> + <version>0.1.0-SNAPSHOT</version> <packaging>jar</packaging> <scm> @@ -66,7 +66,7 @@ <configuration> <instructions> <Private-Package>org.apache.felix.converter.*</Private-Package> - <Export-Package>org.osgi.util.function,org.osgi.util.converter; version="0.1"; mandatory:="status"; status="provisional"</Export-Package> + <Export-Package>org.osgi.util.function,org.osgi.util.converter</Export-Package> <Import-Package>org.osgi.util.converter, *</Import-Package> </instructions> </configuration> Modified: felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/ConvertingImpl.java URL: http://svn.apache.org/viewvc/felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/ConvertingImpl.java?rev=1781884&r1=1781883&r2=1781884&view=diff ============================================================================== --- felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/ConvertingImpl.java (original) +++ felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/ConvertingImpl.java Mon Feb 6 11:54:34 2017 @@ -58,17 +58,18 @@ public class ConvertingImpl implements C interfaceImplementations = Collections.unmodifiableMap(m); } - private volatile InternalConverter converter; + volatile InternalConverter converter; private volatile Object object; - private volatile Class<?> sourceAsClass; private volatile Object defaultValue; private volatile boolean hasDefault; - private volatile Class<?> sourceClass; - private volatile Class<?> targetActualClass; + volatile Class<?> sourceClass; + volatile Class<?> sourceAsClass; + private volatile Class<?> targetClass; private volatile Class<?> targetAsClass; - private volatile Type[] typeArguments; - private List<Object> keys = new ArrayList<>(); + volatile Type[] typeArguments; + List<Object> keys = new ArrayList<>(); private volatile Object root; + private volatile boolean forceCopy = false; private volatile boolean sourceAsJavaBean = false; @SuppressWarnings( "unused" ) private volatile boolean targetAsJavaBean = false; @@ -126,7 +127,8 @@ public class ConvertingImpl implements C @Override public Converting copy() { - // TODO Implement this + forceCopy = true; + return null; } @@ -154,7 +156,6 @@ public class ConvertingImpl implements C return this; } - @SuppressWarnings( "unchecked" ) @Override public void setConverter(Converter c) { if (c instanceof InternalConverter) @@ -197,17 +198,12 @@ public class ConvertingImpl implements C if (object == null) return handleNull(cls); - targetActualClass = Util.primitiveToBoxed(cls); + targetClass = Util.primitiveToBoxed(cls); if (targetAsClass == null) - targetAsClass = targetActualClass; + targetAsClass = targetClass; sourceClass = sourceAsClass != null ? sourceAsClass : object.getClass(); - // Temporary - to remove next commit!! - // This is just to catch any old code that may still be using {source|target}As(DTO.class) - if(DTO.class.equals(sourceClass) || DTO.class.equals(targetAsClass)) - throw new RuntimeException("To update!!"); - if (!isCopyRequiredType(targetAsClass) && targetAsClass.isAssignableFrom(sourceClass)) { return object; } @@ -220,7 +216,7 @@ public class ConvertingImpl implements C return convertToArray(); } else if (Collection.class.isAssignableFrom(targetAsClass)) { return convertToCollection(); - } else if (isDTOType(targetAsClass) || ((sourceAsDTO || targetAsDTO) && DTO.class.isAssignableFrom(targetActualClass))) { + } else if (isDTOType(targetAsClass) || ((sourceAsDTO || targetAsDTO) && DTO.class.isAssignableFrom(targetClass))) { return convertToDTO(); } else if (isMapType(targetAsClass)) { return convertToMapType(); @@ -238,7 +234,7 @@ public class ConvertingImpl implements C return res2; } else { if (defaultValue != null) - return converter.convert(defaultValue).sourceAs(sourceAsClass).targetAs(targetAsClass).to(targetActualClass); + return converter.convert(defaultValue).sourceAs(sourceAsClass).targetAs(targetAsClass).to(targetClass); else return null; } @@ -312,17 +308,17 @@ public class ConvertingImpl implements C Class<?> cls = targetAsClass; if (targetAsDTO) - cls = targetActualClass; + cls = targetClass; try { - T dto = (T) targetActualClass.newInstance(); + T dto = (T) targetClass.newInstance(); for (Map.Entry entry : (Set<Map.Entry>) m.entrySet()) { Field f = null; try { - f = cls.getDeclaredField(mangleName(entry.getKey().toString())); + f = cls.getDeclaredField(Util.mangleName(entry.getKey().toString())); } catch (NoSuchFieldException e) { try { - f = cls.getField(mangleName(entry.getKey().toString())); + f = cls.getField(Util.mangleName(entry.getKey().toString())); } catch (NoSuchFieldException e1) { // There is not field with this name } @@ -340,7 +336,7 @@ public class ConvertingImpl implements C return dto; } catch (Exception e) { - throw new ConversionException("Cannot create DTO " + targetActualClass, e); + throw new ConversionException("Cannot create DTO " + targetClass, e); } } @@ -349,15 +345,14 @@ public class ConvertingImpl implements C Map m = mapView(object, sourceClass, converter); if (m == null) return null; - Type targetKeyType = null, targetValueType = null; - if (typeArguments != null && typeArguments.length > 1) { + Type targetKeyType = null; + if (typeArguments != null && typeArguments.length > 0) { targetKeyType = typeArguments[0]; - targetValueType = typeArguments[1]; } - Class<?> ctrCls = interfaceImplementations.get(targetActualClass); + Class<?> ctrCls = interfaceImplementations.get(targetClass); if (ctrCls == null) - ctrCls = targetActualClass; + ctrCls = targetClass; Map instance = (Map) createMapOrCollection(ctrCls, m.size()); if (instance == null) @@ -369,32 +364,62 @@ public class ConvertingImpl implements C if (targetKeyType != null) key = converter.convert(key).key(ks.toArray()).to(targetKeyType); ks.add(key); - Object[] ka = ks.toArray(); Object value = entry.getValue(); - if (value != null) { - if (targetValueType != null) { - value = converter.convert(value).key(ka).to(targetValueType); - } else { - Class<?> cls = value.getClass(); - if (isCopyRequiredType(cls)) { - cls = getConstructableType(cls); - } - if (sourceAsDTO && DTO.class.isAssignableFrom(cls)) - // sourceAsDTO or sourceAsClass??? - value = converter.convert(value).key(ka).sourceAsDTO().to(cls); - else - value = converter.convert(value).key(ka).to(cls); - } - } + value = convertMapValue(value, ks.toArray()); instance.put(key, value); } return instance; } + Object convertMapValue(Object value, Object[] ka) { + Type targetValueType = null; + if (typeArguments != null && typeArguments.length > 1) { + targetValueType = typeArguments[1]; + } + + if (value != null) { + if (targetValueType != null) { + value = converter.convert(value).key(ka).to(targetValueType); + } else { + Class<?> cls = value.getClass(); + if (isCopyRequiredType(cls)) { + cls = getConstructableType(cls); + } + if (sourceAsDTO && DTO.class.isAssignableFrom(cls)) + value = converter.convert(value).key(ka).sourceAsDTO().to(cls); + else + value = converter.convert(value).key(ka).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 (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)) @@ -423,7 +448,7 @@ public class ConvertingImpl implements C @SuppressWarnings("rawtypes") Map m = mapView(object, sourceCls, converter); try { - Object res = targetActualClass.newInstance(); + 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()); @@ -447,7 +472,7 @@ public class ConvertingImpl implements C new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String propName = getInterfacePropertyName(method); + String propName = Util.getInterfacePropertyName(method); if (propName == null) return null; @@ -574,14 +599,7 @@ public class ConvertingImpl implements C return null; } } else if (Enum.class.isAssignableFrom(targetAsClass)) { - if (object instanceof Boolean) { - try { - Method m = targetAsClass.getMethod("valueOf", String.class); - return m.invoke(null, object.toString().toUpperCase()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } else if (object instanceof Number) { + if (object instanceof Number) { try { Method m = targetAsClass.getMethod("values"); Object[] values = (Object[]) m.invoke(null); @@ -589,8 +607,24 @@ public class ConvertingImpl implements C } 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; } @@ -598,13 +632,13 @@ public class ConvertingImpl implements C @SuppressWarnings("unchecked") private <T> T tryStandardMethods() { try { - Method m = targetActualClass.getDeclaredMethod("valueOf", String.class); + Method m = targetClass.getDeclaredMethod("valueOf", String.class); if (m != null) { return (T) m.invoke(null, object.toString()); } } catch (Exception e) { try { - Constructor<?> ctr = targetActualClass.getConstructor(String.class); + Constructor<?> ctr = targetClass.getConstructor(String.class); return (T) ctr.newInstance(object.toString()); } catch (Exception e2) { } @@ -654,9 +688,6 @@ public class ConvertingImpl implements C for (Method md : sourceCls.getDeclaredMethods()) { handleBeanMethod(obj, md, invokedMethods, result); } - for (Method md : sourceCls.getMethods()) { - handleBeanMethod(obj, md, invokedMethods, result); - } return result; } @@ -666,11 +697,12 @@ public class ConvertingImpl implements C 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()) { - handleField(obj, f, handledFields, result, converter); + handleDTOField(obj, f, handledFields, result, converter); } for (Field f : obj.getClass().getFields()) { - handleField(obj, f, handledFields, result, converter); + handleDTOField(obj, f, handledFields, result, converter); } return result; } @@ -732,59 +764,13 @@ public class ConvertingImpl implements C return null; } - private static String getAccessorPropertyName(Method md) { - if (md.getReturnType().equals(Void.class)) - return null; // not an accessor - - if (md.getParameterTypes().length > 0) - return null; // not an accessor - - if (Object.class.equals(md.getDeclaringClass())) - return null; // do not use any methods on the Object class as a accessor - - String mn = md.getName(); - int prefix; - if (mn.startsWith("get")) - prefix = 3; - else if (mn.startsWith("is")) - prefix = 2; - else - return null; // not an accessor prefix - - if (mn.length() <= prefix) - return null; // just 'get' or 'is': not an accessor - String propStr = mn.substring(prefix); - StringBuilder propName = new StringBuilder(propStr.length()); - char firstChar = propStr.charAt(0); - if (!Character.isUpperCase(firstChar)) - return null; // no acccessor as no camel casing - propName.append(Character.toLowerCase(firstChar)); - if (propStr.length() > 1) - propName.append(propStr.substring(1)); - - return propName.toString(); - } - - private static String getInterfacePropertyName(Method md) { - if (md.getReturnType().equals(Void.class)) - return null; // not an accessor - - if (md.getParameterTypes().length > 1) - return null; // not an accessor - - if (Object.class.equals(md.getDeclaringClass())) - return null; // do not use any methods on the Object class as a accessor - - return md.getName().replace('_', '.'); // TODO support all the escaping mechanisms. - } - @SuppressWarnings({ "rawtypes", "unchecked" }) - private void handleField(Object obj, Field field, Set<String> handledFields, Map result, + private void handleDTOField(Object obj, Field field, Set<String> handledFields, Map result, InternalConverter converter) { - if (Modifier.isStatic(field.getModifiers())) + String fn = Util.getDTOKey(field); + if (fn == null) return; - String fn = unMangleName(field.getName()); if (handledFields.contains(fn)) return; // Field with this name was already handled @@ -804,64 +790,38 @@ public class ConvertingImpl implements C } } - private String mangleName(String key) { - String res = key.replace("_", "__"); - res = res.replace("$", "$$"); - res = res.replaceAll("[.]([._])", "_\\$$1"); - res = res.replace('.', '_'); - // TODO handle Java keywords - return res; - } - - private String unMangleName(String key) { - String res = key.replaceAll("_\\$", "."); - res = res.replace("__", "\f"); // parkl double underscore as formfeed char - res = res.replace('_', '.'); - res = res.replace("$$", "\b"); // park double dollar as backspace char - res = res.replace("$", ""); - res = res.replace('\f', '_'); // convert formfeed char back to single underscore - res = res.replace('\b', '$'); // convert backspace char back go dollar - return res; - } - @SuppressWarnings({ "rawtypes", "unchecked" }) private static void handleBeanMethod(Object obj, Method md, Set<String> invokedMethods, Map res) { - if (Modifier.isStatic(md.getModifiers())) + String bp = Util.getBeanKey(md); + if (bp == null) return; - String mn = md.getName(); - if (invokedMethods.contains(mn)) + if (invokedMethods.contains(bp)) return; // method with this name already invoked - String propName = getAccessorPropertyName(md); - if (propName == null) - return; - try { - res.put(propName.toString(), md.invoke(obj)); - invokedMethods.add(mn); + res.put(bp, md.invoke(obj)); + invokedMethods.add(bp); } catch (Exception e) { } } @SuppressWarnings({ "rawtypes", "unchecked" }) private static void handleInterfaceMethod(Object obj, Method md, Set<String> invokedMethods, Map res) { - if (Modifier.isStatic(md.getModifiers())) - return; - - if (md.getParameterCount() > 0) - return; - String mn = md.getName(); if (invokedMethods.contains(mn)) return; // method with this name already invoked - String propName = getInterfacePropertyName(md); + String propName = Util.getInterfacePropertyName(md); if (propName == null) return; try { - res.put(propName.toString(), md.invoke(obj)); + Object r = Util.getInterfaceProperty(obj, md); + if (r == null) + return; + + res.put(propName, r); invokedMethods.add(mn); } catch (Exception e) { } Added: felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/DynamicMapLikeFacade.java URL: http://svn.apache.org/viewvc/felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/DynamicMapLikeFacade.java?rev=1781884&view=auto ============================================================================== --- felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/DynamicMapLikeFacade.java (added) +++ felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/DynamicMapLikeFacade.java Mon Feb 6 11:54:34 2017 @@ -0,0 +1,242 @@ +/* + * 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.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +abstract class DynamicMapLikeFacade<K, V> implements Map<K, V> { + protected final ConvertingImpl convertingImpl; + + protected DynamicMapLikeFacade(ConvertingImpl convertingImpl) { + this.convertingImpl = convertingImpl; + } + + @Override + public int size() { + return keySet().size(); + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public boolean containsKey(Object key) { + return keySet().contains(key); + } + + @Override + public boolean containsValue(Object value) { + for (Entry<K, V> entry : entrySet()) { + if (value == null) { + if (entry.getValue() == null) { + return true; + } + } else if (value.equals(entry.getValue())) { + return true; + } + } + return false; + } + + @Override + public V put(K key, V value) { + // Should never be called; the delegate should swap to a copy in this case + throw new UnsupportedOperationException(); + } + + @Override + public V remove(Object key) { + // Should never be called; the delegate should swap to a copy in this case + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map<? extends K, ? extends V> m) { + // Should never be called; the delegate should swap to a copy in this case + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + // Should never be called; the delegate should swap to a copy in this case + throw new UnsupportedOperationException(); + } + + @Override + public Collection<V> values() { + return entrySet().stream().map(Entry::getValue).collect(Collectors.toList()); + } + + @Override + public Set<Entry<K, V>> entrySet() { + Set<K> ks = keySet(); + + Set<Entry<K, V>> res = new LinkedHashSet<>(ks.size()); + + for (K k : ks) { + V v = get(k); + res.add(new MapDelegate.MapEntry<K,V>(k, v)); + } + return res; + } +} + +class DynamicBeanFacade extends DynamicMapLikeFacade<String,Object> { + private Map <String, Method> keys = null; + private final Object backingObject; + + DynamicBeanFacade(Object backingObject, ConvertingImpl convertingImpl) { + super(convertingImpl); + this.backingObject = backingObject; + } + + @Override + public Object get(Object key) { + Method m = getKeys().get(key); + try { + return m.invoke(backingObject); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public Set<String> keySet() { + return getKeys().keySet(); + } + + private Map<String, Method> getKeys() { + if (keys == null) + keys = Util.getBeanKeys(convertingImpl.sourceClass); + + return keys; + } +} + +class DynamicDictionaryFacade<K,V> extends DynamicMapLikeFacade<K,V> { + private final Dictionary<K, V> backingObject; + + DynamicDictionaryFacade(Dictionary<K, V> backingObject, ConvertingImpl convertingImpl) { + super(convertingImpl); + this.backingObject = backingObject; + } + + @Override + public V get(Object key) { + return backingObject.get(key); + } + + @Override + public Set<K> keySet() { + return new HashSet<>(Collections.list(backingObject.keys())); + } +} + +class DynamicMapFacade<K,V> extends DynamicMapLikeFacade<K,V> { + private final Map<K, V> backingObject; + + DynamicMapFacade(Map<K,V> backingObject, ConvertingImpl convertingImpl) { + super(convertingImpl); + this.backingObject = backingObject; + } + + @Override + public V get(Object key) { + return backingObject.get(key); + } + + @Override + public Set<K> keySet() { + Map<K, V> m = backingObject; + return m.keySet(); + } +} + +class DynamicDTOFacade extends DynamicMapLikeFacade<String, Object> { + private Map <String, Field> keys = null; + private final Object backingObject; + + DynamicDTOFacade(Object backingObject, ConvertingImpl converting) { + super(converting); + this.backingObject = backingObject; + } + + @Override + public Object get(Object key) { + Field f = getKeys().get(key); + try { + return f.get(backingObject); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public Set<String> keySet() { + return getKeys().keySet(); + } + + private Map<String, Field> getKeys() { + if (keys == null) + keys = Util.getDTOKeys(convertingImpl.sourceClass); + + return keys; + } +} + +class DynamicInterfaceFacade extends DynamicMapLikeFacade<String, Object> { + private Map <String, Method> keys = null; + private final Object backingObject; + + DynamicInterfaceFacade(Object backingObject, ConvertingImpl convertingImpl) { + super(convertingImpl); + this.backingObject = backingObject; + } + + @Override + public Object get(Object key) { + Method m = getKeys().get(key); + try { + return m.invoke(backingObject); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public Set<String> keySet() { + return getKeys().keySet(); + } + + private Map<String, Method> getKeys() { + if (keys == null) + keys = Util.getInterfaceKeys(convertingImpl.sourceClass); + + return keys; + } +} \ No newline at end of file Added: felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/MapDelegate.java URL: http://svn.apache.org/viewvc/felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/MapDelegate.java?rev=1781884&view=auto ============================================================================== --- felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/MapDelegate.java (added) +++ felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/MapDelegate.java Mon Feb 6 11:54:34 2017 @@ -0,0 +1,259 @@ +/* + * 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.util.ArrayList; +import java.util.Collection; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + +class MapDelegate<K, V> implements Map<K, V> { + private final ConvertingImpl convertingImpl; + Map<K, V> delegate; + + private MapDelegate(ConvertingImpl converting, Map<K, V> del) { + convertingImpl = converting; + delegate = del; + } + + static MapDelegate<String, Object> forBean(Object b, ConvertingImpl converting) { + return new MapDelegate<>(converting, new DynamicBeanFacade(b, converting)); + } + + static <K, V> Map<K, V> forMap(Map<K, V> m, ConvertingImpl converting) { + return new MapDelegate<>(converting, new DynamicMapFacade<>(m, converting)); + } + + static <K, V> MapDelegate<K, V> forDictionary(Dictionary<K, V> d, ConvertingImpl converting) { + return new MapDelegate<>(converting, new DynamicDictionaryFacade<>(d, converting)); + } + + static MapDelegate<String, Object> forDTO(Object obj, ConvertingImpl converting) { + return new MapDelegate<>(converting, new DynamicDTOFacade(obj, converting)); + } + + static MapDelegate<String, Object> forInterface(Object obj, ConvertingImpl converting) { + return new MapDelegate<>(converting, new DynamicInterfaceFacade(obj, converting)); + } + + public int size() { + return delegate.size(); + } + + public boolean isEmpty() { + return delegate.isEmpty(); + } + + public boolean containsKey(Object key) { + return delegate.containsKey(key); + } + + public boolean containsValue(Object value) { + return delegate.containsValue(value); + } + + @SuppressWarnings("unchecked") + public V get(Object key) { + V val = null; + if (keySet().contains(key)) { + val = delegate.get(key); + } + + if (val == null) { + key = findConvertedKey(keySet(), key); + val = delegate.get(key); + } + + if (val == null) + return null; + else + return (V) getConvertedValue(key, val); + } + + private Object getConvertedValue(Object key, Object val) { + List<Object> ks = new ArrayList<>(convertingImpl.keys); + ks.add(key); + return convertingImpl.convertMapValue(val, ks.toArray()); + } + + private Object findConvertedKey(Set<?> keySet, Object key) { + for (Object k : keySet) { + Object c = convertingImpl.converter.convert(k).to(key.getClass()); + if (c != null && c.equals(key)) + return k; + +// Maybe the other way around too? +// Object c2 = facade.convertingImpl.converter.convert(key).to(k.getClass()); +// if (c2 != null && c2.equals(key)) +// return c2; + } + return key; + } + + public V put(K key, V value) { + cloneDelegate(); + + return delegate.put(key, value); + } + + public V remove(Object key) { + cloneDelegate(); + + return delegate.remove(key); + } + + public void putAll(Map<? extends K, ? extends V> m) { + cloneDelegate(); + + delegate.putAll(m); + } + + public void clear() { + delegate = new HashMap<>(); + } + + private Set<K> internalKeySet() { + return delegate.keySet(); + } + + public Set<K> keySet() { + Set<K> keys = new HashSet<>(); + for (Map.Entry<K,V> entry : entrySet()) { + keys.add(entry.getKey()); + } + return keys; + } + + public Collection<V> values() { + List<V> values = new ArrayList<>(); + for (Map.Entry<K,V> entry : entrySet()) { + values.add(entry.getValue()); + } + return values; + } + + @SuppressWarnings("unchecked") + public Set<java.util.Map.Entry<K, V>> entrySet() { + Set<Map.Entry<K,V>> result = new HashSet<>(); + for (Map.Entry<?,?> entry : delegate.entrySet()) { + K key = (K) findConvertedKey(internalKeySet(), entry.getKey()); + V val = (V) getConvertedValue(key, entry.getValue()); + result.add(new MapEntry<K,V>(key, val)); + } + return result; + } + + public boolean equals(Object o) { + return delegate.equals(o); + } + + public int hashCode() { + return delegate.hashCode(); + } + + public V getOrDefault(Object key, V defaultValue) { + return delegate.getOrDefault(key, defaultValue); + } + + public void forEach(BiConsumer<? super K, ? super V> action) { + delegate.forEach(action); + } + + public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) { + cloneDelegate(); + + delegate.replaceAll(function); + } + + public V putIfAbsent(K key, V value) { + cloneDelegate(); + + return delegate.putIfAbsent(key, value); + } + + public boolean remove(Object key, Object value) { + cloneDelegate(); + + return delegate.remove(key, value); + } + + public boolean replace(K key, V oldValue, V newValue) { + cloneDelegate(); + + return delegate.replace(key, oldValue, newValue); + } + + public V replace(K key, V value) { + cloneDelegate(); + + return delegate.replace(key, value); + } + + public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) { + return delegate.computeIfAbsent(key, mappingFunction); + } + + public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) { + return delegate.computeIfPresent(key, remappingFunction); + } + + public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) { + return delegate.compute(key, remappingFunction); + } + + public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) { + cloneDelegate(); + + return delegate.merge(key, value, remappingFunction); + } + + private void cloneDelegate() { + delegate = new HashMap<>(delegate); + } + + static class MapEntry<K,V> implements Map.Entry<K,V> { + private final K key; + private final V value; + + MapEntry(K k, V v) { + key = k; + value = v; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V value) { + throw new UnsupportedOperationException(); + } + } +} \ No newline at end of file Modified: felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/Util.java URL: http://svn.apache.org/viewvc/felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/Util.java?rev=1781884&r1=1781883&r2=1781884&view=diff ============================================================================== --- felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/Util.java (original) +++ felix/trunk/converter/converter/src/main/java/org/apache/felix/converter/impl/Util.java Mon Feb 6 11:54:34 2017 @@ -16,9 +16,14 @@ */ package org.apache.felix.converter.impl; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; class Util { @@ -53,4 +58,134 @@ class Util { else return cls; } + + static Map<String, Method> getBeanKeys(Class<?> beanClass) { + Map<String, Method> keys = new LinkedHashMap<>(); + for (Method md : beanClass.getDeclaredMethods()) { + String key = getBeanKey(md); + if (key != null && !keys.containsKey(key)) + keys.put(key, md); + } + return keys; + } + + static String getBeanKey(Method md) { + if (Modifier.isStatic(md.getModifiers())) + return null; + + if (!Modifier.isPublic(md.getModifiers())) + return null; + + return getBeanAccessorPropertyName(md); + } + + private static String getBeanAccessorPropertyName(Method md) { + if (md.getReturnType().equals(Void.class)) + return null; // not an accessor + + if (md.getParameterTypes().length > 0) + return null; // not an accessor + + if (Object.class.equals(md.getDeclaringClass())) + return null; // do not use any methods on the Object class as a accessor + + String mn = md.getName(); + int prefix; + if (mn.startsWith("get")) + prefix = 3; + else if (mn.startsWith("is")) + prefix = 2; + else + return null; // not an accessor prefix + + if (mn.length() <= prefix) + return null; // just 'get' or 'is': not an accessor + String propStr = mn.substring(prefix); + StringBuilder propName = new StringBuilder(propStr.length()); + char firstChar = propStr.charAt(0); + if (!Character.isUpperCase(firstChar)) + return null; // no acccessor as no camel casing + propName.append(Character.toLowerCase(firstChar)); + if (propStr.length() > 1) + propName.append(propStr.substring(1)); + + return propName.toString(); + } + + + static Map<String, Field> getDTOKeys(Class<?> dto) { + Map<String, Field> keys = new LinkedHashMap<>(); + + for (Field f : dto.getFields()) { + String key = getDTOKey(f); + if (key != null && !keys.containsKey(key)) + keys.put(key, f); + } + return keys; + } + + static String getDTOKey(Field f) { + if (Modifier.isStatic(f.getModifiers())) + return null; + + if (!Modifier.isPublic(f.getModifiers())) + return null; + + return unMangleName(f.getName()); + } + + static Map<String, Method> getInterfaceKeys(Class<?> intf) { + Map<String, Method> keys = new LinkedHashMap<>(); + + for (Method md : intf.getMethods()) { + String name = getInterfacePropertyName(md); + if (name != null) + keys.put(name, md); + } + return keys; + } + + static String getInterfacePropertyName(Method md) { + if (md.getReturnType().equals(Void.class)) + return null; // not an accessor + + if (md.getParameterTypes().length > 1) + return null; // not an accessor + + if (Object.class.equals(md.getDeclaringClass())) + return null; // do not use any methods on the Object class as a accessor + + return md.getName().replace('_', '.'); // TODO support all the escaping mechanisms. + } + + static Object getInterfaceProperty(Object obj, Method md) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + if (Modifier.isStatic(md.getModifiers())) + return null; + + if (md.getParameterCount() > 0) + return null; + + return md.invoke(obj); + } + + static String mangleName(String key) { + String res = key.replace("_", "__"); + res = res.replace("$", "$$"); + res = res.replaceAll("[.]([._])", "_\\$$1"); + res = res.replace('.', '_'); + // TODO handle Java keywords + return res; + } + + static String unMangleName(String key) { + String res = key.replaceAll("_\\$", "."); + res = res.replace("__", "\f"); // park double underscore as formfeed char + res = res.replace('_', '.'); + res = res.replace("$$", "\b"); // park double dollar as backspace char + res = res.replace("$", ""); + res = res.replace('\f', '_'); // convert formfeed char back to single underscore + res = res.replace('\b', '$'); // convert backspace char back go dollar + // TODO handle Java keywords + return res; + } } Modified: felix/trunk/converter/converter/src/test/java/org/apache/felix/converter/impl/ConverterBuilderTest.java URL: http://svn.apache.org/viewvc/felix/trunk/converter/converter/src/test/java/org/apache/felix/converter/impl/ConverterBuilderTest.java?rev=1781884&r1=1781883&r2=1781884&view=diff ============================================================================== --- felix/trunk/converter/converter/src/test/java/org/apache/felix/converter/impl/ConverterBuilderTest.java (original) +++ felix/trunk/converter/converter/src/test/java/org/apache/felix/converter/impl/ConverterBuilderTest.java Mon Feb 6 11:54:34 2017 @@ -30,6 +30,7 @@ import java.util.stream.Stream; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.osgi.util.converter.ConvertFunction; import org.osgi.util.converter.Converter; @@ -242,7 +243,7 @@ public class ConverterBuilderTest { } @SuppressWarnings("rawtypes") - @Test + @Test @Ignore("This test assumes that the all the embedded objects are also converted to maps, but they aren't") public void testConvertWithKeysDeep() { MyDTO6 subsubDTO1 = new MyDTO6(); subsubDTO1.chars = Arrays.asList('a', 'b', 'c'); Modified: felix/trunk/converter/converter/src/test/java/org/apache/felix/converter/impl/ConverterEqualsTest.java URL: http://svn.apache.org/viewvc/felix/trunk/converter/converter/src/test/java/org/apache/felix/converter/impl/ConverterEqualsTest.java?rev=1781884&r1=1781883&r2=1781884&view=diff ============================================================================== --- felix/trunk/converter/converter/src/test/java/org/apache/felix/converter/impl/ConverterEqualsTest.java (original) +++ felix/trunk/converter/converter/src/test/java/org/apache/felix/converter/impl/ConverterEqualsTest.java Mon Feb 6 11:54:34 2017 @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.Hashtable; import java.util.Map; +import org.junit.Ignore; import org.junit.Test; import org.osgi.util.converter.Converter; import org.osgi.util.converter.StandardConverter; @@ -29,7 +30,7 @@ import static org.junit.Assert.assertFal import static org.junit.Assert.assertTrue; public class ConverterEqualsTest { - @Test + @Test @Ignore("This functionality should go") public void testEquals() { Converter c = new StandardConverter(); Modified: felix/trunk/converter/converter/src/test/java/org/apache/felix/converter/impl/ConverterTest.java URL: http://svn.apache.org/viewvc/felix/trunk/converter/converter/src/test/java/org/apache/felix/converter/impl/ConverterTest.java?rev=1781884&r1=1781883&r2=1781884&view=diff ============================================================================== --- felix/trunk/converter/converter/src/test/java/org/apache/felix/converter/impl/ConverterTest.java (original) +++ felix/trunk/converter/converter/src/test/java/org/apache/felix/converter/impl/ConverterTest.java Mon Feb 6 11:54:34 2017 @@ -19,6 +19,8 @@ package org.apache.felix.converter.impl; import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.BigInteger; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.time.LocalDate; import java.time.LocalDateTime; @@ -34,6 +36,7 @@ import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; +import java.util.Hashtable; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -52,7 +55,6 @@ import org.apache.felix.converter.impl.M import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.osgi.dto.DTO; import org.osgi.util.converter.ConversionException; import org.osgi.util.converter.Converter; import org.osgi.util.converter.ConverterBuilder; @@ -456,11 +458,17 @@ public class ConverterTest { assertEquals(Long.MIN_VALUE, m.get("pong")); assertEquals(Count.ONE, m.get("count")); assertNotNull(m.get("embedded")); - @SuppressWarnings("rawtypes") + + MyEmbeddedDTO e = (MyEmbeddedDTO) m.get("embedded"); + assertEquals("hohoho", e.marco); + assertEquals(Long.MAX_VALUE, e.polo); + assertEquals(Alpha.A, e.alpha); + /* Map e = (Map)m.get("embedded"); assertEquals("hohoho", e.get("marco")); assertEquals(Long.MAX_VALUE, e.get("polo")); assertEquals(Alpha.A, e.get("alpha")); + */ } @Test @@ -483,11 +491,18 @@ public class ConverterTest { assertEquals(Long.MIN_VALUE, m.get("pong")); assertEquals(Count.ONE, m.get("count")); assertNotNull(m.get("embedded")); - @SuppressWarnings("rawtypes") + + MyEmbeddedDTO e = (MyEmbeddedDTO) m.get("embedded"); + assertEquals("hohoho", e.marco); + assertEquals(Long.MAX_VALUE, e.polo); + assertEquals(Alpha.A, e.alpha); + + /* TODO this is the way it was, but it does not seem right Map e = (Map)m.get("embedded"); assertEquals("hohoho", e.get("marco")); assertEquals(Long.MAX_VALUE, e.get("polo")); assertEquals(Alpha.A, e.get("alpha")); + */ } @Test @@ -523,7 +538,7 @@ public class ConverterTest { assertEquals(Count.ONE, e.count); assertNotNull(e.embedded); assertTrue(e.embedded instanceof MyEmbeddedDTO); - MyEmbeddedDTO e2 = (MyEmbeddedDTO)e.embedded; + MyEmbeddedDTO e2 = e.embedded; assertEquals("hohoho", e2.marco); assertEquals(Long.MAX_VALUE, e2.polo); assertEquals(Alpha.A, e2.alpha); @@ -723,7 +738,112 @@ public class ConverterTest { // And convert back Map<String, String> m2 = converter.convert(dto).to(new TypeReference<Map<String,String>>() {}); - assertEquals(m, m2); + assertEquals(new HashMap<String,String>(m), new HashMap<String,String>(m2)); + } + + @SuppressWarnings("unchecked") + @Test + public void testLiveMapFromInterface() { + int[] val = new int[1]; + val[0] = 51; + + MyIntf intf = new MyIntf() { + @Override + public int value() { + return val[0]; + } + }; + + @SuppressWarnings("rawtypes") + Map m = converter.convert(intf).to(Map.class); + assertEquals(51, m.get("value")); + + val[0] = 52; + assertEquals("Changes to the backing map should be reflected", + 52, m.get("value")); + + m.put("value", 53); + assertEquals(53, m.get("value")); + + val[0] = 54; + assertEquals("Changes to the backing map should not be reflected any more", + 53, m.get("value")); + } + + @SuppressWarnings("unchecked") + @Test + public void testLiveMapFromDTO() { + MyDTO8 myDTO = new MyDTO8(); + + myDTO.count = MyDTO8.Count.TWO; + myDTO.pong = 42L; + + @SuppressWarnings("rawtypes") + Map m = converter.convert(myDTO).to(Map.class); + assertEquals(42L, m.get("pong")); + + myDTO.ping = "Ping!"; + assertEquals("Ping!", m.get("ping")); + myDTO.pong = 52L; + assertEquals(52L, m.get("pong")); + myDTO.ping = "Pong!"; + assertEquals("Pong!", m.get("ping")); + + m.put("pong", 62L); + myDTO.ping = "Poing!"; + myDTO.pong = 72L; + assertEquals("Pong!", m.get("ping")); + assertEquals(62L, m.get("pong")); + } + + @Test + public void testLiveMapFromDictionary() throws URISyntaxException { + URI testURI = new URI("http://foo"); + Hashtable<String, Object> d = new Hashtable<>(); + d.put("test", testURI); + + Map<String, Object> m = converter.convert(d).to(new TypeReference<Map<String, Object>>(){}); + assertEquals(testURI, m.get("test")); + + URI testURI2 = new URI("http://bar"); + d.put("test2", testURI2); + assertEquals(testURI2, m.get("test2")); + assertEquals(testURI, m.get("test")); + } + + @Test + public void testLiveMapFromMap() { + Map<String, String> s = new HashMap<>(); + + s.put("true", "123"); + s.put("false", "456"); + + Map<Boolean, Short> m = converter.convert(s).to(new TypeReference<Map<Boolean, Short>>(){}); + assertEquals(Short.valueOf("123"), m.get(Boolean.TRUE)); + assertEquals(Short.valueOf("456"), m.get(Boolean.FALSE)); + + s.remove("true"); + assertNull(m.get(Boolean.TRUE)); + + s.put("TRUE", "999"); + assertEquals(Short.valueOf("999"), m.get(Boolean.TRUE)); + } + + @Test + public void testLiveMapFromBean() { + MyBean mb = new MyBean(); + mb.beanVal = "" + Long.MAX_VALUE; + + Map<SomeEnum, Long> m = converter.convert(mb).sourceAsBean().to(new TypeReference<Map<SomeEnum, Long>>(){}); + assertEquals(1, m.size()); + assertEquals(Long.valueOf(Long.MAX_VALUE), m.get(SomeEnum.VALUE)); + + mb.beanVal = "" + Long.MIN_VALUE; + assertEquals(Long.valueOf(Long.MIN_VALUE), m.get(SomeEnum.VALUE)); + + m.put(SomeEnum.GETVALUE, 123L); + mb.beanVal = "12"; + assertEquals(Long.valueOf(Long.MIN_VALUE), m.get(SomeEnum.VALUE)); } static class MyClass2 { @@ -768,4 +888,6 @@ public class ConverterTest { return value; } } + + enum SomeEnum { VALUE, GETVALUE }; }