diff --git a/driver-core/src/main/java/com/datastax/driver/core/AbstractData.java b/driver-core/src/main/java/com/datastax/driver/core/AbstractData.java index d4144162c26..f2e4be68906 100644 --- a/driver-core/src/main/java/com/datastax/driver/core/AbstractData.java +++ b/driver-core/src/main/java/com/datastax/driver/core/AbstractData.java @@ -15,9 +15,12 @@ */ package com.datastax.driver.core; +import com.datastax.driver.core.utils.ReflectionUtils; import com.google.common.base.Objects; import com.google.common.reflect.TypeToken; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; import java.math.BigDecimal; import java.math.BigInteger; import java.net.InetAddress; @@ -527,6 +530,10 @@ public T set(String name, V v, TypeToken targetType) { @Override public T set(int i, V v, TypeCodec codec) { checkType(i, codec.getCqlType().getName()); + if (isCodecUDT(codec) && areDifferentClassLoader(v, codec)) { + v = mapValuesToOSGIClass(v, codec); + return setValue(i, codec.serialize(v, protocolVersion)); + } return setValue(i, codec.serialize(v, protocolVersion)); } @@ -589,4 +596,69 @@ public int hashCode() { hash += values[i] == null ? 1 : codecFor(i).deserialize(values[i], protocolVersion).hashCode(); return hash; } + + /** + * When an Object come from other classloader, we need to change the Object to persist to an Object in the + * same classloader of the Codec. + * + * @param v Object to persist + * @param codec Codec to parse the object + * @return Object to persist in the same classloader on the Codec + */ + private V mapValuesToOSGIClass(V v, TypeCodec codec) { + try { + Class clazz = codec.getClass().getClassLoader().loadClass(v.getClass().getName()); + V newObject = (V) clazz.newInstance(); + Map fromProperties = ReflectionUtils.scanProperties(v.getClass()); + Map toProperties = ReflectionUtils.scanProperties(clazz); + for (String key : fromProperties.keySet()) { + try { + Method read = fromProperties.get(key).getReadMethod(); + Object readValue = read.invoke(v); + if (readValue != null) { + Method write = toProperties.get(key).getWriteMethod(); + write.invoke(newObject, readValue); + } + } catch (Exception e) { + // Discard if there is an error + e.printStackTrace(); + } + } + return newObject; + } catch (Exception e) { + //Do not do anything + } + return v; + } + + /** + * Check if classloader between object to persist and codec are different, within OSGI objects + * come from different places + * + * @param v Object to persist + * @param codec Codec to parse the object + * @return true if classloaders are different + */ + private boolean areDifferentClassLoader(V v, TypeCodec codec) { + ClassLoader clObject = v.getClass().getClassLoader(); + ClassLoader clCodec = codec.getClass().getClassLoader(); + if (clObject.equals(clCodec)) { + return false; + } + return true; + } + + /** + * Check if it is an UDT codec + * + * @param codec Codec to check + * @return true if codec is UDT type + */ + private boolean isCodecUDT(TypeCodec codec) { + String UDT = "udt"; + if (codec != null && codec.getCqlType() != null && codec.getCqlType().getName() != null && codec.getCqlType().getName().toString() != null) { + return codec.getCqlType().getName().toString().equals(UDT); + } + return false; + } } diff --git a/driver-core/src/main/java/com/datastax/driver/core/CodecRegistry.java b/driver-core/src/main/java/com/datastax/driver/core/CodecRegistry.java index fcf1d90c414..815c923d8e2 100644 --- a/driver-core/src/main/java/com/datastax/driver/core/CodecRegistry.java +++ b/driver-core/src/main/java/com/datastax/driver/core/CodecRegistry.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; @@ -189,17 +190,41 @@ public CacheKey(DataType cqlType, TypeToken javaType) { public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) + Class clazzInput = getClassFromClassLoader(o.getClass()); + if (o == null || getClass() != clazzInput) return false; CacheKey cacheKey = (CacheKey) o; return Objects.equal(cqlType, cacheKey.cqlType) && Objects.equal(javaType, cacheKey.javaType); } + private Class getClassFromClassLoader(Class aClass) { + Class clazzInput; + try { + clazzInput = CodecRegistry.class.getClassLoader().loadClass(aClass.getName()); + } catch (ClassNotFoundException e) { + return aClass; + } + return clazzInput; + } + @Override public int hashCode() { return Objects.hashCode(cqlType, javaType); } + @Override + public String toString() { + if (cqlType != null && javaType != null && javaType.getRawType() != null) { + return "CacheKey{" + + "cqlType=" + cqlType.getName() + + ", javaType=" + javaType.getRawType().getName() + + '}'; + } + return "CacheKey{" + + "cqlType=" + cqlType + + ", javaType=" + javaType + + '}'; + } } /** @@ -289,12 +314,19 @@ public void onRemoval(RemovalNotification> notification) */ private final LoadingCache> cache; + /** + * Saving cached keys, we need to do it, because within an OSGI environment + * the classloader is different, it depends where the class is located + */ + private Map saveUDTKeysOSGI; + /** * Creates a new instance initialized with built-in codecs for all the base CQL types. */ public CodecRegistry() { this.codecs = new CopyOnWriteArrayList>(PRIMITIVE_CODECS); this.cache = defaultCacheBuilder().build(new TypeCodecCacheLoader()); + this.saveUDTKeysOSGI = new ConcurrentHashMap(); } private CacheBuilder> defaultCacheBuilder() { @@ -337,6 +369,7 @@ public CodecRegistry register(TypeCodec newCodec) { } this.codecs.add(newCodec); + this.saveUDTKeysOSGI.put(key.toString(), key); return this; } @@ -477,7 +510,7 @@ private TypeCodec lookupCodec(DataType cqlType, TypeToken javaType) { logger.trace("Querying cache for codec [{} <-> {}]", toString(cqlType), toString(javaType)); CacheKey cacheKey = new CacheKey(cqlType, javaType); try { - TypeCodec codec = cache.get(cacheKey); + TypeCodec codec = getCodec(cacheKey); logger.trace("Returning cached codec {}", codec); return (TypeCodec) codec; } catch (UncheckedExecutionException e) { @@ -683,4 +716,21 @@ private static String toString(Object value) { return value == null ? "ANY" : value.toString(); } + /** + * Get codec from cache, we need to verify is the key is an OSGI from different classloader + * @param cacheKey Object which represent to KEY + * @return Codec instance registered from cache + * @throws ExecutionException + */ + private TypeCodec getCodec(CacheKey cacheKey) throws ExecutionException { + CacheKey key = (CacheKey) saveUDTKeysOSGI.get(cacheKey.toString()); + TypeCodec codec = null; + + if (key == null) { + key = cacheKey; + } + codec = cache.get(key); + logger.trace("Returning cached codec {}", codec); + return (TypeCodec) codec; + } } diff --git a/driver-core/src/main/java/com/datastax/driver/core/utils/ReflectionUtils.java b/driver-core/src/main/java/com/datastax/driver/core/utils/ReflectionUtils.java new file mode 100644 index 00000000000..d6200639c9d --- /dev/null +++ b/driver-core/src/main/java/com/datastax/driver/core/utils/ReflectionUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2012-2015 DataStax 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 com.datastax.driver.core.utils; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.util.HashMap; +import java.util.Map; + +/** + * Handling the reflection within core driver, it is recycled from mapping driver jar + * + * Created by @christmo(christmo99@gmail.com) on 14/10/16. + */ +public class ReflectionUtils { + + @SuppressWarnings("all") + public static Map scanProperties(Class baseClass) { + BeanInfo beanInfo = null; + try { + beanInfo = Introspector.getBeanInfo(baseClass); + } catch (IntrospectionException e) { + } + Map properties = new HashMap(); + if (beanInfo != null) { + for (PropertyDescriptor property : beanInfo.getPropertyDescriptors()) { + if (property.getName().equals("class")) + continue; + properties.put(property.getName(), property); + } + } + return properties; + } +} diff --git a/driver-mapping/src/main/java/com/datastax/driver/mapping/AnnotationChecks.java b/driver-mapping/src/main/java/com/datastax/driver/mapping/AnnotationChecks.java index 71286d78020..30c94a1ed28 100644 --- a/driver-mapping/src/main/java/com/datastax/driver/mapping/AnnotationChecks.java +++ b/driver-mapping/src/main/java/com/datastax/driver/mapping/AnnotationChecks.java @@ -20,10 +20,7 @@ import com.datastax.driver.mapping.annotations.Table; import java.lang.annotation.Annotation; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; +import java.util.*; /** * Various checks on mapping annotations. @@ -39,9 +36,12 @@ class AnnotationChecks { */ static T getTypeAnnotation(Class annotation, Class annotatedClass) { T instance = annotatedClass.getAnnotation(annotation); - if (instance == null) - throw new IllegalArgumentException(String.format("@%s annotation was not found on %s", - annotation.getSimpleName(), annotatedClass)); + if (instance == null) { + instance = ProxyAnnotationUtils.rebuildAnnotationFromProxy(annotatedClass, annotation); + if (instance == null) + throw new IllegalArgumentException(String.format("@%s annotation was not found on %s", + annotation.getSimpleName(), annotatedClass)); + } // Check that no other mapping annotations are present validateAnnotations(annotatedClass, annotation); diff --git a/driver-mapping/src/main/java/com/datastax/driver/mapping/PropertyMapper.java b/driver-mapping/src/main/java/com/datastax/driver/mapping/PropertyMapper.java index b66b749363c..469ee64c22e 100644 --- a/driver-mapping/src/main/java/com/datastax/driver/mapping/PropertyMapper.java +++ b/driver-mapping/src/main/java/com/datastax/driver/mapping/PropertyMapper.java @@ -110,7 +110,9 @@ Collection getAnnotations() { @SuppressWarnings("unchecked") A annotation(Class annotationClass) { - return (A) annotations.get(annotationClass); + A annotation = (A) annotations.get(annotationClass); + return ProxyAnnotationUtils.convertProxyAnnotation(annotation); + } boolean isComputed() { @@ -156,7 +158,7 @@ private TypeToken inferJavaType() { type = getter.getGenericReturnType(); else type = field.getGenericType(); - return (TypeToken) TypeToken.of(type); + return (TypeToken) TypeToken.of((Type) type); } private int inferPosition() { diff --git a/driver-mapping/src/main/java/com/datastax/driver/mapping/ProxyAnnotationUtils.java b/driver-mapping/src/main/java/com/datastax/driver/mapping/ProxyAnnotationUtils.java new file mode 100644 index 00000000000..224d7192130 --- /dev/null +++ b/driver-mapping/src/main/java/com/datastax/driver/mapping/ProxyAnnotationUtils.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2012-2015 DataStax 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 com.datastax.driver.mapping; + +import com.datastax.driver.mapping.annotations.UDT; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; + +/** + * Handle Proxy annotations within an OSGI environment + * + * Created by @christmo (christmo99@gmail.com) on 10/10/16. + */ +public class ProxyAnnotationUtils { + + /** + * Package where a Proxy come from + */ + private static final String PACKAGE_PROXY = "com.sun.proxy"; + + /** + * In an OSGI environment, we need to get the correct class from the actual classloader + * + * @param klass class to get from the actual classloader + * @return New class within actual classloader + */ + public static Class getClassFromOSGIClassLoader(Class klass) { + Class newClass = klass; + if (klass != null) { + try { + newClass = ProxyAnnotationUtils.class.getClassLoader().loadClass(klass.getName()); + } catch (ClassNotFoundException e) { + return klass; + } + } + return newClass; + } + + /** + * Check if there is an UDT annotation in the class, when klass is a Proxy from an OSGI environment + * + * @param klass Class to check + * @param udtClass UDT Annotation + * @return true if klass is annotated like UDT + */ + public static boolean checkUDTAnnotationProxy(Class klass, Class udtClass) { + Annotation[] annotations = klass.getAnnotations(); + for (Annotation annotation : annotations) { + annotation = ProxyAnnotationUtils.convertProxyAnnotation(annotation); + return annotation.annotationType().equals(udtClass); + } + return false; + } + + /** + * Within an OSGI environment, we cannot get annotations directly, it comes like a Proxy, we need to handle it + * with an extra class Handler, this method return a new Annotation with a Handler to interact with the annotation. + * @param annotatedClass Class which have some annotation. + * @param annotation Class annotation to handle from the clas annotated + * @return T new Annotation handled by a class + */ + public static T rebuildAnnotationFromProxy(Class annotatedClass, Class annotation) { + boolean hasAnnotation = false; + Map properties = new HashMap(); + + for (Annotation classAnnotation : annotatedClass.getAnnotations()) { + if (isProxy(classAnnotation)) { + if (classAnnotation.annotationType().getCanonicalName().equals(annotation.getCanonicalName())) { + for (Method method : classAnnotation.annotationType().getDeclaredMethods()) { + try { + Object value = method.invoke(classAnnotation); + properties.put(method.getName(), value); + } catch (Exception e) { + //Do not do anything + } + } + hasAnnotation = true; + break; + } + } + } + + if (hasAnnotation) { + T newAnnotation = (T) Proxy.newProxyInstance(annotation.getClassLoader(), new Class[]{annotation}, + new Handler(annotation, properties)); + return newAnnotation; + } + + return null; + } + + /** + * Convert a Proxy Annotation to a Handled Annotation + * + * @param annotation Proxy annotation + * @return T new annotation Handled by a class + */ + public static T convertProxyAnnotation(Annotation annotation) { + Map properties = new HashMap(); + T newAnnotation = null; + Class classAnnotation = null; + + if (isProxy(annotation)) { + classAnnotation = annotation.annotationType(); + for (Method method : classAnnotation.getDeclaredMethods()) { + try { + Object value = method.invoke(annotation); + properties.put(method.getName(), value); + } catch (Exception e) { + //Do not do anything + } + } + for (Method method : annotation.getClass().getMethods()) { + try { + Object value = method.invoke(annotation); + properties.put(method.getName(), value); + } catch (Exception e) { + //Do not do anything + } + } + + newAnnotation = (T) Proxy.newProxyInstance(classAnnotation.getClassLoader(), + new Class[]{classAnnotation}, new Handler(classAnnotation, properties)); + } + return newAnnotation; + } + + /** + * Check if the annotation come from a Proxy, it could proceed from an OSGI environment + * + * @param annotation Annotation to check if it is a Proxy + * @return true if the annotation is a Proxy + */ + private static boolean isProxy(Annotation annotation) { + if (annotation != null) { + return annotation.getClass().getName().contains(PACKAGE_PROXY); + } + return false; + } +/* + private static boolean isProxy(Class annotation) { + if (annotation != null) { + return annotation.getName().contains(PACKAGE_PROXY); + } + return false; + } +*/ +} + +/** + * Class to handle annotation properties come from a Proxy within an OSGI environment + */ +class Handler implements InvocationHandler { + + private final String TO_STRING_METHOD = "toString"; + private final String CODEC_METHOD = "codec"; + private final String HASHCODE_METHOD = "hashcode"; + private final String CLASS_METHOD = "getClass"; + private final String ANNOTATION_TYPE_METHOD = "annotationType"; + + private Map properties; + private Class annotation; + + public Handler(Class annotation, Map props) { + this.annotation = annotation; + this.properties = props; + } + + /** + * This method is called when an annotation property is invoked. + * + * @param proxy Proxy Object which have data + * @param method method executed + * @param args optional args + * @return Value from the proxy annotation + * @throws Throwable + */ + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Object result; + if (TO_STRING_METHOD.equals(method.getName()) && annotation != null) { + return annotation.toString(); + } + if (properties != null && method != null) { + result = properties.get(method.getName()); + if (result != null && ANNOTATION_TYPE_METHOD.equals(method.getName()) || CODEC_METHOD.equals(method.getName())) { + return this.getClass().getClassLoader().loadClass(((Class) result).getName()); + } + if (result != null) { + return result; + } + } + return null; + } +} diff --git a/driver-mapping/src/main/java/com/datastax/driver/mapping/ReflectionUtils.java b/driver-mapping/src/main/java/com/datastax/driver/mapping/ReflectionUtils.java index 4bef1f47f99..961b21a93a0 100644 --- a/driver-mapping/src/main/java/com/datastax/driver/mapping/ReflectionUtils.java +++ b/driver-mapping/src/main/java/com/datastax/driver/mapping/ReflectionUtils.java @@ -114,6 +114,7 @@ static Map, Annotation> scanPropertyAnnotations(Fiel private static Map, Annotation> scanFieldAnnotations(Field field, Map, Annotation> annotations) { for (Annotation annotation : field.getAnnotations()) { + annotation = ProxyAnnotationUtils.convertProxyAnnotation(annotation); annotations.put(annotation.annotationType(), annotation); } return annotations; diff --git a/driver-mapping/src/main/java/com/datastax/driver/mapping/TypeMappings.java b/driver-mapping/src/main/java/com/datastax/driver/mapping/TypeMappings.java index e8def3dd0b7..3117554fdf2 100644 --- a/driver-mapping/src/main/java/com/datastax/driver/mapping/TypeMappings.java +++ b/driver-mapping/src/main/java/com/datastax/driver/mapping/TypeMappings.java @@ -47,7 +47,11 @@ private static boolean mapsToMap(Class klass) { } static boolean isMappedUDT(Class klass) { - return klass.isAnnotationPresent(UDT.class); + boolean result = klass.isAnnotationPresent(UDT.class); + if (result == false) { + result = ProxyAnnotationUtils.checkUDTAnnotationProxy(klass, UDT.class); + } + return result; } /** @@ -80,6 +84,7 @@ private static Set> findUDTs(Type type, Set> udts) { if (isMappedUDT(klass)) { if (udts == null) udts = Sets.newHashSet(); + klass = ProxyAnnotationUtils.getClassFromOSGIClassLoader(klass); udts.add(klass); } }