From 899a98c83e035dfcb9d10c050fd6f00a04b30828 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 12 Apr 2016 17:22:02 +0200 Subject: [PATCH] DATACMNS-809 - Add Class-generating property accessor factory. We now support generated PersistentPropertyAccessors when using Java 7 and if property/association name hashCodes are unique within a PersistentEntity. Generated PersistentPropertyAccessors provide optimized access to properties. They use either MethodHandles or direct property/field access, depending on the visibility/final modifiers of the entity type and its members. A generated PersistentPropertyAccessor is injected into the originating class loader of the entity class to enable optimizations for package-default/protected member access. Original pull request: #159. --- .../mapping/model/BasicPersistentEntity.java | 60 +- ...lassGeneratingPropertyAccessorFactory.java | 1430 +++++++++++++++++ ...aultPersistentPropertyAccessorFactory.java | 45 + .../PersistentPropertyAccessorFactory.java | 38 + .../model/BasicPersistentEntityUnitTests.java | 25 +- ...gPropertyAccessorFactoryDatatypeTests.java | 247 +++ ...eneratingPropertyAccessorFactoryTests.java | 433 +++++ ...ingPropertyAccessorPackageDefaultType.java | 83 + ...sGeneratingPropertyAccessorPublicType.java | 94 ++ .../model/subpackage/TypeInOtherPackage.java | 82 + 10 files changed, 2509 insertions(+), 28 deletions(-) create mode 100644 src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java create mode 100644 src/main/java/org/springframework/data/mapping/model/DefaultPersistentPropertyAccessorFactory.java create mode 100644 src/main/java/org/springframework/data/mapping/model/PersistentPropertyAccessorFactory.java create mode 100644 src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryDatatypeTests.java create mode 100644 src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryTests.java create mode 100644 src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPackageDefaultType.java create mode 100644 src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPublicType.java create mode 100644 src/test/java/org/springframework/data/mapping/model/subpackage/TypeInOtherPackage.java diff --git a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java index c9e41dd0e..281b8cd9b 100644 --- a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java @@ -45,12 +45,13 @@ import org.springframework.util.StringUtils; /** * Simple value object to capture information of {@link PersistentEntity}s. - * + * * @author Oliver Gierke * @author Jon Brisbin * @author Patryk Wasik * @author Thomas Darimont * @author Christoph Strobl + * @author Mark Paluch */ public class BasicPersistentEntity> implements MutablePersistentEntity { @@ -66,9 +67,11 @@ public class BasicPersistentEntity> implement private P idProperty; private P versionProperty; + private final PersistentPropertyAccessorFactory propertyAccessorFactory; + /** * Creates a new {@link BasicPersistentEntity} from the given {@link TypeInformation}. - * + * * @param information must not be {@literal null}. */ public BasicPersistentEntity(TypeInformation information) { @@ -79,7 +82,7 @@ public class BasicPersistentEntity> implement * Creates a new {@link BasicPersistentEntity} for the given {@link TypeInformation} and {@link Comparator}. The given * {@link Comparator} will be used to define the order of the {@link PersistentProperty} instances added to the * entity. - * + * * @param information must not be {@literal null}. * @param comparator can be {@literal null}. */ @@ -91,14 +94,15 @@ public class BasicPersistentEntity> implement this.properties = new ArrayList

(); this.comparator = comparator; this.constructor = new PreferredConstructorDiscoverer(information, this).getConstructor(); - this.associations = comparator == null ? new HashSet>() : new TreeSet>( - new AssociationComparator

(comparator)); + this.associations = comparator == null ? new HashSet>() + : new TreeSet>(new AssociationComparator

(comparator)); this.propertyCache = new HashMap(); this.annotationCache = new HashMap, Annotation>(); + this.propertyAccessorFactory = new DefaultPersistentPropertyAccessorFactory(); } - /* + /* * (non-Javadoc) * @see org.springframework.data.mapping.PersistentEntity#getPersistenceConstructor() */ @@ -122,7 +126,7 @@ public class BasicPersistentEntity> implement return this.idProperty == null ? false : this.idProperty.equals(property); } - /* + /* * (non-Javadoc) * @see org.springframework.data.mapping.PersistentEntity#isVersionProperty(org.springframework.data.mapping.PersistentProperty) */ @@ -146,7 +150,7 @@ public class BasicPersistentEntity> implement return idProperty; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mapping.PersistentEntity#getVersionProperty() */ @@ -154,7 +158,7 @@ public class BasicPersistentEntity> implement return versionProperty; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mapping.PersistentEntity#hasIdProperty() */ @@ -162,7 +166,7 @@ public class BasicPersistentEntity> implement return idProperty != null; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mapping.PersistentEntity#hasVersionProperty() */ @@ -170,7 +174,7 @@ public class BasicPersistentEntity> implement return versionProperty != null; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mapping.MutablePersistentEntity#addPersistentProperty(P) */ @@ -197,9 +201,11 @@ public class BasicPersistentEntity> implement if (property.isVersionProperty()) { if (this.versionProperty != null) { - throw new MappingException(String.format( - "Attempt to add version property %s but already have property %s registered " - + "as version. Check your mapping configuration!", property.getField(), versionProperty.getField())); + throw new MappingException( + String.format( + "Attempt to add version property %s but already have property %s registered " + + "as version. Check your mapping configuration!", + property.getField(), versionProperty.getField())); } this.versionProperty = property; @@ -208,7 +214,7 @@ public class BasicPersistentEntity> implement /** * Returns the given property if it is a better candidate for the id property than the current id property. - * + * * @param property the new id property candidate, will never be {@literal null}. * @return the given id property or {@literal null} if the given property is not an id property. */ @@ -244,7 +250,7 @@ public class BasicPersistentEntity> implement return propertyCache.get(name); } - /* + /* * (non-Javadoc) * @see org.springframework.data.mapping.PersistentEntity#getPersistentProperty(java.lang.Class) */ @@ -279,7 +285,7 @@ public class BasicPersistentEntity> implement return information.getType(); } - /* + /* * (non-Javadoc) * @see org.springframework.data.mapping.PersistentEntity#getTypeAlias() */ @@ -312,7 +318,7 @@ public class BasicPersistentEntity> implement } } - /* + /* * (non-Javadoc) * @see org.springframework.data.mapping.PersistentEntity#doWithProperties(org.springframework.data.mapping.PropertyHandler.Simple) */ @@ -354,7 +360,7 @@ public class BasicPersistentEntity> implement } } - /* + /* * (non-Javadoc) * @see org.springframework.data.mapping.PersistentEntity#findAnnotation(java.lang.Class) */ @@ -372,7 +378,7 @@ public class BasicPersistentEntity> implement return annotation; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mapping.MutablePersistentEntity#verify() */ @@ -383,7 +389,7 @@ public class BasicPersistentEntity> implement } } - /* + /* * (non-Javadoc) * @see org.springframework.data.mapping.PersistentEntity#getPropertyAccessor(java.lang.Object) */ @@ -393,10 +399,10 @@ public class BasicPersistentEntity> implement Assert.notNull(bean, "Target bean must not be null!"); Assert.isTrue(getType().isInstance(bean), "Target bean is not of type of the persistent entity!"); - return new BeanWrapper(bean); + return propertyAccessorFactory.getPropertyAccessor(this, bean); } - /* + /* * (non-Javadoc) * @see org.springframework.data.mapping.PersistentEntity#getIdentifierAccessor(java.lang.Object) */ @@ -419,7 +425,7 @@ public class BasicPersistentEntity> implement INSTANCE; - /* + /* * (non-Javadoc) * @see org.springframework.data.mapping.IdentifierAccessor#getIdentifier() */ @@ -431,11 +437,11 @@ public class BasicPersistentEntity> implement /** * Simple {@link Comparator} adaptor to delegate ordering to the inverse properties of the association. - * + * * @author Oliver Gierke */ - private static final class AssociationComparator

> implements - Comparator>, Serializable { + private static final class AssociationComparator

> + implements Comparator>, Serializable { private static final long serialVersionUID = 4508054194886854513L; private final Comparator

delegate; diff --git a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java new file mode 100644 index 000000000..c4a7713a9 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java @@ -0,0 +1,1430 @@ +/* + * Copyright 2016 the original author or authors. + * + * 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 org.springframework.data.mapping.model; + +import static org.springframework.asm.Opcodes.*; + +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.asm.ClassWriter; +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.asm.Type; +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.SimpleAssociationHandler; +import org.springframework.data.mapping.SimplePropertyHandler; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.ReflectionUtils; + +/** + * A factory that can generate byte code to speed-up dynamic property access. Uses the {@link PersistentEntity}'s + * {@link PersistentProperty} to discover the access to properties. Properties are accessed either using method handles + * to overcome Java visibility issues or directly using field access/getter/setter calls. + * + * @author Mark Paluch + * @since 1.13 + */ +class ClassGeneratingPropertyAccessorFactory implements PersistentPropertyAccessorFactory { + + private static final boolean IS_JAVA_JAVA_7 = org.springframework.util.ClassUtils + .isPresent("java.lang.invoke.MethodHandle", ClassGeneratingPropertyAccessorFactory.class.getClassLoader()); + + private final PropertyAccessorClassGenerator generator; + private volatile Map, Class> propertyAccessorClasses = new HashMap, Class>( + 32); + + /** + * Creates a new {@link ClassGeneratingPropertyAccessorFactory}. + */ + public ClassGeneratingPropertyAccessorFactory() { + this.generator = new PropertyAccessorClassGenerator(); + } + + /* (non-Javadoc) + * @see org.springframework.data.mapping.model.PersistentPropertyAccessorFactory#getPropertyAccessor(org.springframework.data.mapping.PersistentEntity, java.lang.Object) + */ + @Override + public PersistentPropertyAccessor getPropertyAccessor(PersistentEntity entity, Object bean) { + + Class propertyAccessorClass = propertyAccessorClasses.get(entity.getTypeInformation()); + + if (propertyAccessorClass == null) { + propertyAccessorClass = potentiallyCreateAndRegisterPersistentPropertyAccessorClass(entity); + } + + try { + return (PersistentPropertyAccessor) propertyAccessorClass.getConstructors()[0].newInstance(bean); + } catch (Exception e) { + throw new IllegalArgumentException(String.format("Cannot create persistent property accessor for %s", entity), e); + } + } + + /** + * Checks whether an accessor class can be generated. + * + * @param entity + * @return true if the runtime is equal or greater to Java 1.7, property name hash codes are unique and the type has a + * class loader we can use to re-inject types. + */ + public static boolean canGenerateAccessorClass(PersistentEntity entity) { + + if (!IS_JAVA_JAVA_7) { + return false; + } + + if (entity.getType().getClassLoader() == null || entity.getType().getPackage().getName().startsWith("java")) { + return false; + } + + final Set hashCodes = new HashSet(); + final AtomicInteger propertyCount = new AtomicInteger(); + entity.doWithProperties(new SimplePropertyHandler() { + @Override + public void doWithPersistentProperty(PersistentProperty property) { + hashCodes.add(property.getName().hashCode()); + propertyCount.incrementAndGet(); + } + }); + + entity.doWithAssociations(new SimpleAssociationHandler() { + @Override + public void doWithAssociation(Association> association) { + if (association.getInverse() != null) { + hashCodes.add(association.getInverse().getName().hashCode()); + propertyCount.incrementAndGet(); + } + } + }); + + return hashCodes.size() == propertyCount.get(); + } + + /** + * @param entity + * @return + */ + private synchronized Class potentiallyCreateAndRegisterPersistentPropertyAccessorClass( + PersistentEntity entity) { + + Map, Class> map = this.propertyAccessorClasses; + Class propertyAccessorClass = map.get(entity.getTypeInformation()); + + if (propertyAccessorClass != null) { + return propertyAccessorClass; + } + + propertyAccessorClass = createAccessorClass(entity); + + map = new HashMap, Class>(map); + map.put(entity.getTypeInformation(), propertyAccessorClass); + + this.propertyAccessorClasses = map; + + return propertyAccessorClass; + } + + @SuppressWarnings("unchecked") + private Class createAccessorClass(PersistentEntity entity) { + + try { + return (Class) this.generator.generateCustomAccessorClass(entity); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Generates {@link PersistentPropertyAccessor} classes to access properties of a {@link PersistentEntity}. This code + * uses {@code private final static} held method handles which perform about the speed of native method invocations + * for property access which is restricted due to Java rules (such as private fields/methods) or private inner + * classes. All other scoped members (package default, protected and public) are accessed via field or property access + * to bypass reflection overhead. That's only possible if the type and the member access is possible from another + * class within the same package and class loader. Mixed access (MethodHandle/getter/setter calls) is possible as + * well. Accessing properties using generated accessors imposes some constraints: + *

    + *
  • Runtime must be Java 7 or higher
  • + *
  • The generated accessor decides upon generation whether to use field or property access for particular + * properties. It's not possible to change the access method once the accessor class is generated.
  • + *
  • Property names and their {@link String#hashCode()} must be unique within a {@link PersistentEntity}.
  • + *
+ * These constraints apply to retain the performance gains, otherwise the generated code has to decide which method + * (field/property) has to be used. The {@link String#hashCode()} rule originates in dispatching of to the appropriate + * {@link java.lang.invoke.MethodHandle}. This is done by {@code LookupSwitch} which is a O(1) operation but requires + * a constant input. {@link String#hashCode()} may change but since we run in the same VM, no evil should happen. + * + *
+	 * {
+	 * 	@code
+	 * 	public class PersonWithId_Accessor_zd4wnl implements PersistentPropertyAccessor {
+	 * 		private final Object bean;
+	 * 		private static final MethodHandle $id_fieldGetter;
+	 * 		private static final MethodHandle $id_fieldSetter;
+	 * 		// ...
+	 * 		public PersonWithId_Accessor_zd4wnl(Object bean) {
+	 * 			this.bean = bean;
+	 *        }
+	 * 		static {
+	 * 			Method getter;
+	 * 			Method setter;
+	 * 			MethodHandles.Lookup lookup = MethodHandles.lookup();
+	 * 			Class class_1 = Class.forName("org.springframework.data.mapping.Person");
+	 * 			Class class_2 = Class.forName("org.springframework.data.mapping.PersonWithId");
+	 * 			Field field = class_2.getDeclaredField("id");
+	 * 			field.setAccessible(true);
+	 * 			$id_fieldGetter = lookup.unreflectGetter(field);
+	 * 			$id_fieldSetter = lookup.unreflectSetter(field);
+	 * 			// ...
+	 *        }
+	 * 		public Object getBean() {
+	 * 			return this.bean;
+	 *        }
+	 * 		public void setProperty(PersistentProperty property, Object value) {
+	 * 			Object bean = this.bean;
+	 * 			switch (property.getName().hashCode()) {
+	 * 				case 3355:
+	 * 					$id_fieldSetter.invoke(bean, value);
+	 * 					return;
+	 * 				// ...
+	 *            }
+	 * 			throw new UnsupportedOperationException(
+	 * 					String.format("No MethodHandle to set property %s", new Object[] { property }));
+	 *        }
+	 * 		 public Object getProperty(PersistentProperty property){
+	 * 			Object bean = this.bean;
+	 * 			switch (property.getName().hashCode()) {
+	 * 				case 3355:
+	 * 					return id_fieldGetter..invoke(bean);
+	 * 				case 3356:
+	 * 					return bean.getField();
+	 * 					// ...
+	 * 				case 3357:
+	 * 					return bean.field;
+	 * 					// ...
+	 * 			throw new UnsupportedOperationException(
+	 * 					String.format("No MethodHandle to get property %s", new Object[] { property }));
+	 *        }
+	 * }
+	 * 
+ * + * @author Mark Paluch + */ + static class PropertyAccessorClassGenerator { + + private static final String INIT = ""; + private static final String CLINIT = ""; + private static final String TAG = "_Accessor_"; + private static final String JAVA_LANG_OBJECT = "java/lang/Object"; + private static final String JAVA_LANG_STRING = "java/lang/String"; + private static final String JAVA_LANG_REFLECT_METHOD = "java/lang/reflect/Method"; + private static final String JAVA_LANG_INVOKE_METHOD_HANDLE = "java/lang/invoke/MethodHandle"; + private static final String JAVA_LANG_CLASS = "java/lang/Class"; + private static final String BEAN_FIELD = "bean"; + private static final String THIS_REF = "this"; + private static final String PERSISTENT_PROPERTY = "org/springframework/data/mapping/PersistentProperty"; + private static final String SET_ACCESSIBLE = "setAccessible"; + private static final String JAVA_LANG_REFLECT_FIELD = "java/lang/reflect/Field"; + private static final String JAVA_LANG_INVOKE_METHOD_HANDLES = "java/lang/invoke/MethodHandles"; + private static final String JAVA_LANG_INVOKE_METHOD_HANDLES_LOOKUP = "java/lang/invoke/MethodHandles$Lookup"; + private static final String JAVA_LANG_UNSUPPORTED_OPERATION_EXCEPTION = "java/lang/UnsupportedOperationException"; + + private static final String[] IMPLEMENTED_INTERFACES = new String[] { + Type.getInternalName(PersistentPropertyAccessor.class) }; + + public PropertyAccessorClassGenerator() {} + + /** + * @param entity + * @return + */ + private String generateClassName(PersistentEntity entity) { + return entity.getType().getName() + TAG + Integer.toString(entity.hashCode(), 36); + } + + /** + * Generate a new class for the given {@link PersistentEntity}. + * + * @param entity + * @return + */ + public Class generateCustomAccessorClass(PersistentEntity entity) { + + String className = generateClassName(entity); + byte[] bytecode = generateBytecode(className.replace('.', '/'), entity); + + Class accessorClass = Evil.injector.defineClass(className, bytecode, 0, bytecode.length, entity); + + return accessorClass; + } + + /** + * Generate a new class for the given {@link PersistentEntity}. + * + * @param internalClassName + * @param entity + * @return + */ + public byte[] generateBytecode(String internalClassName, PersistentEntity entity) { + + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); + cw.visit(Opcodes.V1_6, ACC_PUBLIC + ACC_SUPER, internalClassName, null, JAVA_LANG_OBJECT, IMPLEMENTED_INTERFACES); + + List> persistentProperties = getPersistentProperties(entity); + + visitFields(entity, persistentProperties, cw); + visitDefaultConstructor(entity, internalClassName, cw); + visitStaticInitializer(entity, persistentProperties, internalClassName, cw); + visitBeanGetter(entity, internalClassName, cw); + visitSetProperty(entity, persistentProperties, internalClassName, cw); + visitGetProperty(entity, persistentProperties, internalClassName, cw); + + cw.visitEnd(); + + return cw.toByteArray(); + } + + private List> getPersistentProperties(PersistentEntity entity) { + + final List> persistentProperties = new ArrayList>(); + entity.doWithAssociations(new SimpleAssociationHandler() { + @Override + public void doWithAssociation(Association> association) { + if (association.getInverse() != null) { + persistentProperties.add(association.getInverse()); + } + } + }); + + entity.doWithProperties(new SimplePropertyHandler() { + @Override + public void doWithPersistentProperty(PersistentProperty property) { + persistentProperties.add(property); + } + }); + return persistentProperties; + } + + /** + * Generates field declarations for private-visibility properties. + * + *
+		 * {
+		 * 	@code
+		 * 	private final Object bean;
+		 * 	private static final MethodHandle $id_fieldGetter;
+		 * 	private static final MethodHandle $id_fieldSetter;
+		 * 	// ...
+		 * }
+		 * 
+ * + * @param entity + * @param persistentProperties + * @param cw + */ + private void visitFields(PersistentEntity entity, List> persistentProperties, + ClassWriter cw) { + + cw.visitInnerClass(JAVA_LANG_INVOKE_METHOD_HANDLES_LOOKUP, JAVA_LANG_INVOKE_METHOD_HANDLES, "Lookup", + ACC_PRIVATE + ACC_FINAL + ACC_STATIC); + + boolean accessibleType = isAccessible(entity); + if (accessibleType) { + cw.visitField(ACC_PRIVATE + ACC_FINAL, BEAN_FIELD, referenceName(Type.getInternalName(entity.getType())), null, + null).visitEnd(); + } else { + cw.visitField(ACC_PRIVATE + ACC_FINAL, BEAN_FIELD, referenceName(JAVA_LANG_OBJECT), null, null).visitEnd(); + } + + for (PersistentProperty property : persistentProperties) { + + Method setter = property.getSetter(); + if (setter != null && generateMethodHandle(entity, setter)) { + cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, setterName(property), + referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE), null, null).visitEnd(); + } + + Method getter = property.getGetter(); + if (getter != null && generateMethodHandle(entity, getter)) { + cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, getterName(property), + referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE), null, null).visitEnd(); + } + + Field field = property.getField(); + if (field != null && generateSetterMethodHandle(entity, field)) { + cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, fieldSetterName(property), + referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE), null, null).visitEnd(); + cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, fieldGetterName(property), + referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE), null, null).visitEnd(); + } + } + } + + /** + * Generates the default constructor. + * + *
+		 * {
+		 * 		@code
+		 * 		public PersonWithId_Accessor_zd4wnl(PersonWithId bean) {
+		 * 			this.bean = bean;
+		 *      }
+		 * }
+		 * 
+ * + * @param entity + * @param internalClassName + * @param cw + */ + private void visitDefaultConstructor(PersistentEntity entity, String internalClassName, ClassWriter cw) { + + // public EntityAccessor(Entity bean) or EntityAccessor(Object bean) + MethodVisitor mv; + boolean accessibleType = isAccessible(entity); + if (accessibleType) { + mv = cw.visitMethod(ACC_PUBLIC, INIT, String.format("(%s)V", referenceName(entity.getType())), null, null); + } else { + mv = cw.visitMethod(ACC_PUBLIC, INIT, String.format("(%s)V", referenceName(JAVA_LANG_OBJECT)), null, null); + } + + mv.visitCode(); + Label l0 = new Label(); + mv.visitLabel(l0); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKESPECIAL, JAVA_LANG_OBJECT, INIT, "()V", false); + + // Assert.notNull(bean) + mv.visitVarInsn(ALOAD, 1); + mv.visitLdcInsn("Bean must not be null!"); + mv.visitMethodInsn(INVOKESTATIC, "org/springframework/util/Assert", "notNull", + String.format("(%s%s)V", referenceName(JAVA_LANG_OBJECT), referenceName(JAVA_LANG_STRING)), false); + + // this.bean = bean + mv.visitVarInsn(ALOAD, 0); + mv.visitVarInsn(ALOAD, 1); + + if (accessibleType) { + mv.visitFieldInsn(PUTFIELD, internalClassName, BEAN_FIELD, referenceName(entity.getType())); + } else { + mv.visitFieldInsn(PUTFIELD, internalClassName, BEAN_FIELD, referenceName(JAVA_LANG_OBJECT)); + } + + mv.visitInsn(RETURN); + Label l3 = new Label(); + mv.visitLabel(l3); + mv.visitLocalVariable(THIS_REF, referenceName(internalClassName), null, l0, l3, 0); + + if (accessibleType) { + mv.visitLocalVariable(BEAN_FIELD, referenceName(Type.getInternalName(entity.getType())), null, l0, l3, 1); + } else { + mv.visitLocalVariable(BEAN_FIELD, referenceName(JAVA_LANG_OBJECT), null, l0, l3, 1); + } + + mv.visitMaxs(2, 2); + } + + /** + * Generates the static initializer block. + * + *
+		 * 		@code
+		 * 		static {
+		 * 			Method getter;
+		 * 			Method setter;
+		 * 			MethodHandles.Lookup lookup = MethodHandles.lookup();
+		 * 			Class class_1 = Class.forName("org.springframework.data.mapping.Person");
+		 * 			Class class_2 = Class.forName("org.springframework.data.mapping.PersonWithId");
+		 * 			Field field = class_2.getDeclaredField("id");
+		 * 			field.setAccessible(true);
+		 * 			$id_fieldGetter = lookup.unreflectGetter(field);
+		 * 			$id_fieldSetter = lookup.unreflectSetter(field);
+		 *  		// ...
+		 *        }
+		 * 
+ * + * @param entity + * @param persistentProperties + * @param internalClassName + * @param cw + */ + private void visitStaticInitializer(PersistentEntity entity, List> persistentProperties, + String internalClassName, ClassWriter cw) { + + MethodVisitor mv = cw.visitMethod(ACC_STATIC, CLINIT, "()V", null, null); + mv.visitCode(); + Label l0 = new Label(); + Label l1 = new Label(); + mv.visitLabel(l0); + + // lookup = MethodHandles.lookup() + mv.visitMethodInsn(INVOKESTATIC, JAVA_LANG_INVOKE_METHOD_HANDLES, "lookup", + String.format("()%s", referenceName(JAVA_LANG_INVOKE_METHOD_HANDLES_LOOKUP)), false); + mv.visitVarInsn(ASTORE, 0); + + List> entityClasses = getPropertyDeclaratingClasses(persistentProperties); + + for (Class entityClass : entityClasses) { + mv.visitLdcInsn(entityClass.getName()); + mv.visitMethodInsn(INVOKESTATIC, JAVA_LANG_CLASS, "forName", + String.format("(%s)%s", referenceName(JAVA_LANG_STRING), referenceName(JAVA_LANG_CLASS)), false); + mv.visitVarInsn(ASTORE, classVariableIndex4(entityClasses, entityClass)); + } + + for (PersistentProperty property : persistentProperties) { + + if (property.getGetter() != null && generateMethodHandle(entity, property.getGetter())) { + visitPropertyGetterInitializer(property, mv, entityClasses, internalClassName); + } + + if (property.getSetter() != null && generateMethodHandle(entity, property.getSetter())) { + visitPropertySetterInitializer(property, mv, entityClasses, internalClassName); + } + + if (property.getField() != null && generateSetterMethodHandle(entity, property.getField())) { + visitFieldGetterSetterInitializer(property, mv, entityClasses, internalClassName); + } + } + + mv.visitLabel(l1); + mv.visitInsn(RETURN); + + mv.visitLocalVariable("lookup", referenceName(JAVA_LANG_INVOKE_METHOD_HANDLES_LOOKUP), null, l0, l1, 0); + mv.visitLocalVariable("field", referenceName(JAVA_LANG_REFLECT_FIELD), null, l0, l1, 1); + mv.visitLocalVariable("setter", referenceName(JAVA_LANG_REFLECT_METHOD), null, l0, l1, 2); + mv.visitLocalVariable("getter", referenceName(JAVA_LANG_REFLECT_METHOD), null, l0, l1, 3); + + for (Class entityClass : entityClasses) { + int index = classVariableIndex4(entityClasses, entityClass); + + mv.visitLocalVariable(String.format("class_%d", index), referenceName(JAVA_LANG_CLASS), null, l0, l1, index); + } + + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + /** + * Retrieve all classes which are involved in property/getter/setter declarations as these elements may be + * distributed across the type hierarchy. + * + * @param persistentProperties + * @return + */ + private List> getPropertyDeclaratingClasses(List> persistentProperties) { + + Set> entityClassesSet = new HashSet>(); + for (PersistentProperty property : persistentProperties) { + + if (property.getField() != null) { + entityClassesSet.add(property.getField().getDeclaringClass()); + } + if (property.getGetter() != null) { + entityClassesSet.add(property.getGetter().getDeclaringClass()); + } + if (property.getSetter() != null) { + entityClassesSet.add(property.getSetter().getDeclaringClass()); + } + } + + return new ArrayList>(entityClassesSet); + } + + /** + * Generate property getter initializer. + * + * @param property + * @param mv + * @param entityClasses + * @param internalClassName + */ + private void visitPropertyGetterInitializer(PersistentProperty property, MethodVisitor mv, + List> entityClasses, String internalClassName) { + + // getter = .class.getDeclaredMethod() + Method getter = property.getGetter(); + if (getter != null) { + mv.visitVarInsn(ALOAD, classVariableIndex4(entityClasses, getter.getDeclaringClass())); + mv.visitLdcInsn(getter.getName()); + mv.visitInsn(ICONST_0); + mv.visitTypeInsn(ANEWARRAY, JAVA_LANG_CLASS); + + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_CLASS, "getDeclaredMethod", String.format("(%s[%s)%s", + referenceName(JAVA_LANG_STRING), referenceName(JAVA_LANG_CLASS), referenceName(JAVA_LANG_REFLECT_METHOD)), + false); + mv.visitVarInsn(ASTORE, 3); + + // getter.setAccessible(true) + mv.visitVarInsn(ALOAD, 3); + mv.visitInsn(ICONST_1); + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_REFLECT_METHOD, SET_ACCESSIBLE, "(Z)V", false); + + mv.visitVarInsn(ALOAD, 0); + mv.visitVarInsn(ALOAD, 3); + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_INVOKE_METHOD_HANDLES_LOOKUP, "unreflect", String.format("(%s)%s", + referenceName(JAVA_LANG_REFLECT_METHOD), referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)), false); + } else { + mv.visitInsn(ACONST_NULL); + } + mv.visitFieldInsn(PUTSTATIC, internalClassName, getterName(property), + referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)); + } + + /** + * Generate property setter initializer. + * + * @param property + * @param mv + * @param entityClasses + * @param internalClassName + */ + private void visitPropertySetterInitializer(PersistentProperty property, MethodVisitor mv, + List> entityClasses, String internalClassName) { + + // setter = .class.getDeclaredMethod() + Method setter = property.getSetter(); + if (setter != null) { + + mv.visitVarInsn(ALOAD, classVariableIndex4(entityClasses, setter.getDeclaringClass())); + mv.visitLdcInsn(setter.getName()); + + mv.visitInsn(ICONST_1); + mv.visitTypeInsn(ANEWARRAY, JAVA_LANG_CLASS); + mv.visitInsn(DUP); + mv.visitInsn(ICONST_0); + mv.visitLdcInsn(Type.getType(referenceName(Type.getInternalName(autoboxType(setter.getParameterTypes()[0]))))); + mv.visitInsn(AASTORE); + + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_CLASS, "getDeclaredMethod", String.format("(%s[%s)%s", + referenceName(JAVA_LANG_STRING), referenceName(JAVA_LANG_CLASS), referenceName(JAVA_LANG_REFLECT_METHOD)), + false); + mv.visitVarInsn(ASTORE, 2); + + // setter.setAccessible(true) + mv.visitVarInsn(ALOAD, 2); + mv.visitInsn(ICONST_1); + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_REFLECT_METHOD, SET_ACCESSIBLE, "(Z)V", false); + + mv.visitVarInsn(ALOAD, 0); + mv.visitVarInsn(ALOAD, 2); + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_INVOKE_METHOD_HANDLES_LOOKUP, "unreflect", String.format("(%s)%s", + referenceName(JAVA_LANG_REFLECT_METHOD), referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)), false); + } else { + mv.visitInsn(ACONST_NULL); + } + + mv.visitFieldInsn(PUTSTATIC, internalClassName, setterName(property), + referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)); + } + + /** + * Generate field getter and setter initializers. + * + * @param property + * @param mv + * @param entityClasses + * @param internalClassName + */ + private void visitFieldGetterSetterInitializer(PersistentProperty property, MethodVisitor mv, + List> entityClasses, String internalClassName) { + // field = .class.getDeclaredField() + Field field = property.getField(); + mv.visitVarInsn(ALOAD, classVariableIndex4(entityClasses, field.getDeclaringClass())); + mv.visitLdcInsn(field.getName()); + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_CLASS, "getDeclaredField", + String.format("(%s)%s", referenceName(JAVA_LANG_STRING), referenceName(JAVA_LANG_REFLECT_FIELD)), false); + mv.visitVarInsn(ASTORE, 1); + + // field.setAccessible(true) + mv.visitVarInsn(ALOAD, 1); + mv.visitInsn(ICONST_1); + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_REFLECT_FIELD, SET_ACCESSIBLE, "(Z)V", false); + + // $fieldGetter = lookup.unreflectGetter(field) + mv.visitVarInsn(ALOAD, 0); + mv.visitVarInsn(ALOAD, 1); + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_INVOKE_METHOD_HANDLES_LOOKUP, "unreflectGetter", String.format( + "(%s)%s", referenceName(JAVA_LANG_REFLECT_FIELD), referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)), false); + mv.visitFieldInsn(PUTSTATIC, internalClassName, fieldGetterName(property), + referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)); + + // $fieldSetter = lookup.unreflectSetter(field) + mv.visitVarInsn(ALOAD, 0); + mv.visitVarInsn(ALOAD, 1); + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_INVOKE_METHOD_HANDLES_LOOKUP, "unreflectSetter", String.format( + "(%s)%s", referenceName(JAVA_LANG_REFLECT_FIELD), referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)), false); + mv.visitFieldInsn(PUTSTATIC, internalClassName, fieldSetterName(property), + referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)); + } + + private void visitBeanGetter(PersistentEntity entity, String internalClassName, ClassWriter cw) { + + // public Object getBean() + MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "getBean", String.format("()%s", referenceName(JAVA_LANG_OBJECT)), + null, null); + mv.visitCode(); + Label l0 = new Label(); + + // return this.bean + mv.visitLabel(l0); + mv.visitVarInsn(ALOAD, 0); + if (isAccessible(entity)) { + mv.visitFieldInsn(GETFIELD, internalClassName, BEAN_FIELD, referenceName(entity.getType())); + } else { + mv.visitFieldInsn(GETFIELD, internalClassName, BEAN_FIELD, referenceName(JAVA_LANG_OBJECT)); + } + mv.visitInsn(ARETURN); + + Label l1 = new Label(); + mv.visitLabel(l1); + mv.visitLocalVariable(THIS_REF, referenceName(internalClassName), null, l0, l1, 0); + mv.visitMaxs(1, 1); + mv.visitEnd(); + } + + /** + * Generate {@link PersistentPropertyAccessor#getProperty(PersistentProperty)} . * + * + *
+		 * {
+		 * 	@code
+		 * 		 public Object getProperty(PersistentProperty property){
+		 * 			Object bean = this.bean;
+		 * 			switch (property.getName().hashCode()) {
+		 * 				case 3355:
+		 * 					return id_fieldGetter..invoke(bean);
+		 * 				case 3356:
+		 * 					return bean.getField();
+		 * 					// ...
+		 * 				case 3357:
+		 * 					return bean.field;
+		 * 					// ...
+		 *            }
+		 * 			throw new UnsupportedOperationException(
+		 * 					String.format("No MethodHandle to get property %s", new Object[] { property }));
+		 *        }
+		 * }
+		 * 
+ * + * @param entity + * @param persistentProperties + * @param internalClassName + * @param cw + */ + private void visitGetProperty(PersistentEntity entity, List> persistentProperties, + String internalClassName, ClassWriter cw) { + + MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "getProperty", + "(Lorg/springframework/data/mapping/PersistentProperty;)Ljava/lang/Object;", + "(Lorg/springframework/data/mapping/PersistentProperty<*>;)Ljava/lang/Object;", null); + mv.visitCode(); + + Label l0 = new Label(); + Label l1 = new Label(); + mv.visitLabel(l0); + + // Assert.notNull(property) + visitAssertNotNull(mv); + + mv.visitVarInsn(ALOAD, 0); + if (isAccessible(entity)) { + mv.visitFieldInsn(GETFIELD, internalClassName, BEAN_FIELD, referenceName(entity.getType())); + } else { + mv.visitFieldInsn(GETFIELD, internalClassName, BEAN_FIELD, referenceName(JAVA_LANG_OBJECT)); + } + mv.visitVarInsn(ASTORE, 2); + + visitGetPropertySwitch(entity, persistentProperties, internalClassName, mv); + + mv.visitLabel(l1); + visitThrowUnsupportedOperationException(mv, "No accessor to get property %s"); + + mv.visitLocalVariable(THIS_REF, referenceName(internalClassName), null, l0, l1, 0); + mv.visitLocalVariable("property", referenceName(PERSISTENT_PROPERTY), + "Lorg/springframework/data/mapping/PersistentProperty<*>;", l0, l1, 1); + + if (isAccessible(entity)) { + mv.visitLocalVariable(BEAN_FIELD, referenceName(entity.getType()), null, l0, l1, 2); + } else { + mv.visitLocalVariable(BEAN_FIELD, referenceName(JAVA_LANG_OBJECT), null, l0, l1, 2); + } + + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + /** + * Generate the {@code switch(hashcode) {label: }} block. + * + * @param entity + * @param persistentProperties + * @param internalClassName + * @param mv + */ + private void visitGetPropertySwitch(PersistentEntity entity, List> persistentProperties, + String internalClassName, MethodVisitor mv) { + + Map propertyStackMap = createPropertyStackMap(persistentProperties); + + int[] hashes = new int[propertyStackMap.size()]; + Label[] switchJumpLabels = new Label[propertyStackMap.size()]; + List stackmap = new ArrayList(propertyStackMap.values()); + Collections.sort(stackmap); + + for (int i = 0; i < stackmap.size(); i++) { + PropertyStackAddress propertyStackAddress = stackmap.get(i); + hashes[i] = propertyStackAddress.hash; + switchJumpLabels[i] = propertyStackAddress.label; + } + + Label dfltLabel = new Label(); + + mv.visitVarInsn(ALOAD, 1); + mv.visitMethodInsn(INVOKEINTERFACE, PERSISTENT_PROPERTY, "getName", + String.format("()%s", referenceName(JAVA_LANG_STRING)), true); + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_STRING, "hashCode", "()I", false); + mv.visitLookupSwitchInsn(dfltLabel, hashes, switchJumpLabels); + + for (PersistentProperty property : persistentProperties) { + + mv.visitLabel(propertyStackMap.get(property.getName()).label); + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + + if (property.getGetter() != null || property.getField() != null) { + visitGetProperty0(entity, property, mv, internalClassName); + } else { + mv.visitJumpInsn(GOTO, dfltLabel); + } + } + + mv.visitLabel(dfltLabel); + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + } + + /** + * Generate property read access using a {@link java.lang.invoke.MethodHandle}. + * {@link java.lang.invoke.MethodHandle#invoke(Object...)} have a {@code @PolymorphicSignature} so {@code invoke} is + * called as if the method had the expected signature and not array/varargs. + * + * @param entity + * @param property + * @param mv + * @param internalClassName + */ + private void visitGetProperty0(PersistentEntity entity, PersistentProperty property, MethodVisitor mv, + String internalClassName) { + + Method getter = property.getGetter(); + if (property.usePropertyAccess() && getter != null) { + + if (generateMethodHandle(entity, getter)) { + // $getter.invoke(bean) + mv.visitFieldInsn(GETSTATIC, internalClassName, getterName(property), + referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)); + mv.visitVarInsn(ALOAD, 2); + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_INVOKE_METHOD_HANDLE, "invoke", + String.format("(%s)%s", referenceName(JAVA_LANG_OBJECT), referenceName(JAVA_LANG_OBJECT)), false); + } else { + // bean.get... + mv.visitVarInsn(ALOAD, 2); + mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(getter.getDeclaringClass()), getter.getName(), + String.format("()%s", signatureTypeName(getter.getReturnType())), false); + autoboxIfNeeded(getter.getReturnType(), autoboxType(getter.getReturnType()), mv); + } + } else { + + Field field = property.getField(); + if (generateMethodHandle(entity, field)) { + // $fieldGetter.invoke(bean) + mv.visitFieldInsn(GETSTATIC, internalClassName, fieldGetterName(property), + referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)); + mv.visitVarInsn(ALOAD, 2); + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_INVOKE_METHOD_HANDLE, "invoke", + String.format("(%s)%s", referenceName(JAVA_LANG_OBJECT), referenceName(JAVA_LANG_OBJECT)), false); + } else { + // bean.field + mv.visitVarInsn(ALOAD, 2); + mv.visitFieldInsn(GETFIELD, Type.getInternalName(field.getDeclaringClass()), field.getName(), + signatureTypeName(field.getType())); + autoboxIfNeeded(field.getType(), autoboxType(field.getType()), mv); + } + } + + mv.visitInsn(ARETURN); + } + + /** + * Generate the {@link PersistentPropertyAccessor#setProperty(PersistentProperty, Object)} method. * + * + *
+		 * {
+		 * 	@code
+		 * 		public void setProperty(PersistentProperty property, Object value) {
+		 * 			Object bean = this.bean;
+		 * 			switch (property.getName().hashCode()) {
+		 * 				case 3355:
+		 * 					$id_fieldSetter.invoke(bean, value);
+		 * 					return;
+		 * 				// ...
+		 *            }
+		 * 			throw new UnsupportedOperationException(
+		 * 					String.format("No MethodHandle to set property %s", new Object[] { property }));
+		 *        }
+		 *    }
+		 * 
+ * + * @param entity + * @param persistentProperties + * @param internalClassName + * @param cw + */ + private void visitSetProperty(PersistentEntity entity, List> persistentProperties, + String internalClassName, ClassWriter cw) { + + MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "setProperty", + "(Lorg/springframework/data/mapping/PersistentProperty;Ljava/lang/Object;)V", + "(Lorg/springframework/data/mapping/PersistentProperty<*>;Ljava/lang/Object;)V", null); + mv.visitCode(); + + Label l0 = new Label(); + mv.visitLabel(l0); + + visitAssertNotNull(mv); + + mv.visitVarInsn(ALOAD, 0); + if (isAccessible(entity)) { + mv.visitFieldInsn(GETFIELD, internalClassName, BEAN_FIELD, referenceName(entity.getType())); + } else { + mv.visitFieldInsn(GETFIELD, internalClassName, BEAN_FIELD, referenceName(JAVA_LANG_OBJECT)); + } + + mv.visitVarInsn(ASTORE, 3); + + visitSetPropertySwitch(entity, persistentProperties, internalClassName, mv); + + Label l1 = new Label(); + mv.visitLabel(l1); + + visitThrowUnsupportedOperationException(mv, "No accessor to set property %s"); + + mv.visitLocalVariable(THIS_REF, referenceName(internalClassName), null, l0, l1, 0); + mv.visitLocalVariable("property", "Lorg/springframework/data/mapping/PersistentProperty;", + "Lorg/springframework/data/mapping/PersistentProperty<*>;", l0, l1, 1); + mv.visitLocalVariable("value", referenceName(JAVA_LANG_OBJECT), null, l0, l1, 2); + + if (isAccessible(entity)) { + mv.visitLocalVariable(BEAN_FIELD, referenceName(entity.getType()), null, l0, l1, 3); + } else { + mv.visitLocalVariable(BEAN_FIELD, referenceName(JAVA_LANG_OBJECT), null, l0, l1, 3); + } + + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + /** + * Generate the {@code switch(hashcode) {label: }} block. + * + * @param entity + * @param persistentProperties + * @param internalClassName + * @param mv + */ + private void visitSetPropertySwitch(PersistentEntity entity, List> persistentProperties, + String internalClassName, MethodVisitor mv) { + + Map propertyStackMap = createPropertyStackMap(persistentProperties); + + int[] hashes = new int[propertyStackMap.size()]; + Label[] switchJumpLabels = new Label[propertyStackMap.size()]; + List stackmap = new ArrayList(propertyStackMap.values()); + Collections.sort(stackmap); + + for (int i = 0; i < stackmap.size(); i++) { + PropertyStackAddress propertyStackAddress = stackmap.get(i); + hashes[i] = propertyStackAddress.hash; + switchJumpLabels[i] = propertyStackAddress.label; + } + + Label dfltLabel = new Label(); + + mv.visitVarInsn(ALOAD, 1); + mv.visitMethodInsn(INVOKEINTERFACE, PERSISTENT_PROPERTY, "getName", + String.format("()%s", referenceName(JAVA_LANG_STRING)), true); + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_STRING, "hashCode", "()I", false); + mv.visitLookupSwitchInsn(dfltLabel, hashes, switchJumpLabels); + + for (PersistentProperty property : persistentProperties) { + mv.visitLabel(propertyStackMap.get(property.getName()).label); + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + + if (property.getSetter() != null || property.getField() != null) { + visitSetProperty0(entity, property, mv, internalClassName); + } else { + mv.visitJumpInsn(GOTO, dfltLabel); + } + } + + mv.visitLabel(dfltLabel); + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + } + + /** + * Generate property write access using a {@link java.lang.invoke.MethodHandle}. NOTE: + * {@link java.lang.invoke.MethodHandle#invoke(Object...)} have a {@code @PolymorphicSignature} so {@code invoke} is + * called as if the method had the expected signature and not array/varargs. + * + * @param entity + * @param property + * @param mv + * @param internalClassName + */ + private void visitSetProperty0(PersistentEntity entity, PersistentProperty property, MethodVisitor mv, + String internalClassName) { + + Method setter = property.getSetter(); + if (property.usePropertyAccess() && setter != null) { + + if (generateMethodHandle(entity, setter)) { + // $setter.invoke(bean) + mv.visitFieldInsn(GETSTATIC, internalClassName, setterName(property), + referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)); + mv.visitVarInsn(ALOAD, 3); + mv.visitVarInsn(ALOAD, 2); + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_INVOKE_METHOD_HANDLE, "invoke", + String.format("(%s%s)V", referenceName(JAVA_LANG_OBJECT), referenceName(JAVA_LANG_OBJECT)), false); + } else { + // bean.set...(object) + mv.visitVarInsn(ALOAD, 3); + mv.visitVarInsn(ALOAD, 2); + + Class parameterType = setter.getParameterTypes()[0]; + mv.visitTypeInsn(CHECKCAST, Type.getInternalName(autoboxType(parameterType))); + autoboxIfNeeded(autoboxType(parameterType), parameterType, mv); + mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(setter.getDeclaringClass()), setter.getName(), + String.format("(%s)V", signatureTypeName(parameterType)), false); + } + } else { + + Field field = property.getField(); + if (field != null) { + if (generateSetterMethodHandle(entity, field)) { + // $fieldSetter.invoke(bean, object) + mv.visitFieldInsn(GETSTATIC, internalClassName, fieldSetterName(property), + referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)); + mv.visitVarInsn(ALOAD, 3); + mv.visitVarInsn(ALOAD, 2); + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_INVOKE_METHOD_HANDLE, "invoke", + String.format("(%s%s)V", referenceName(JAVA_LANG_OBJECT), referenceName(JAVA_LANG_OBJECT)), false); + } else { + // bean.field + mv.visitVarInsn(ALOAD, 3); + mv.visitVarInsn(ALOAD, 2); + + Class fieldType = field.getType(); + + mv.visitTypeInsn(CHECKCAST, Type.getInternalName(autoboxType(fieldType))); + autoboxIfNeeded(autoboxType(fieldType), fieldType, mv); + mv.visitFieldInsn(PUTFIELD, Type.getInternalName(field.getDeclaringClass()), field.getName(), + signatureTypeName(fieldType)); + } + } + } + + mv.visitInsn(RETURN); + } + + private void visitAssertNotNull(MethodVisitor mv) { + + // Assert.notNull(property) + mv.visitVarInsn(ALOAD, 1); + mv.visitLdcInsn("Property must not be null!"); + mv.visitMethodInsn(INVOKESTATIC, "org/springframework/util/Assert", "notNull", + String.format("(%s%s)V", referenceName(JAVA_LANG_OBJECT), referenceName(JAVA_LANG_STRING)), false); + } + + private void visitThrowUnsupportedOperationException(MethodVisitor mv, String message) { + + // throw new UnsupportedOperationException(msg) + mv.visitTypeInsn(NEW, JAVA_LANG_UNSUPPORTED_OPERATION_EXCEPTION); + mv.visitInsn(DUP); + mv.visitLdcInsn(message); + mv.visitInsn(ICONST_1); + mv.visitTypeInsn(ANEWARRAY, JAVA_LANG_OBJECT); + mv.visitInsn(DUP); + mv.visitInsn(ICONST_0); + mv.visitVarInsn(ALOAD, 1); + mv.visitInsn(AASTORE); + mv.visitMethodInsn(INVOKESTATIC, JAVA_LANG_STRING, "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", false); + mv.visitMethodInsn(INVOKESPECIAL, JAVA_LANG_UNSUPPORTED_OPERATION_EXCEPTION, "", "(Ljava/lang/String;)V", + false); + mv.visitInsn(ATHROW); + } + + private String fieldSetterName(PersistentProperty property) { + return String.format("$%s_fieldSetter", property.getName()); + } + + private String fieldGetterName(PersistentProperty property) { + return String.format("$%s_fieldGetter", property.getName()); + } + + private String setterName(PersistentProperty property) { + return String.format("$%s_setter", property.getName()); + } + + private String getterName(PersistentProperty property) { + return String.format("$%s_getter", property.getName()); + } + + private boolean isAccessible(PersistentEntity entity) { + return isAccessible(entity.getType()); + } + + private boolean isAccessible(Class theClass) { + return isAccessible(theClass.getModifiers()); + } + + private boolean isAccessible(int modifiers) { + + if (Modifier.isPrivate(modifiers)) { + return false; + } + + return true; + } + + private boolean isDefault(int modifiers) { + + if (Modifier.isPrivate(modifiers) || Modifier.isProtected(modifiers) || Modifier.isPublic(modifiers)) { + return false; + } + + return true; + } + + private boolean generateSetterMethodHandle(PersistentEntity entity, Field field) { + return generateMethodHandle(entity, field) || Modifier.isFinal(field.getModifiers()); + } + + /** + * Check whether to generate {@link java.lang.invoke.MethodHandle} access. Checks visibility rules of the member and + * its declaring class. Use also {@link java.lang.invoke.MethodHandle} if visibility is protected/package-default + * and packages of the declaring types are different. + * + * @param entity + * @param member + * @return + */ + private boolean generateMethodHandle(PersistentEntity entity, Member member) { + + if (isAccessible(entity)) { + + if (Modifier.isProtected(member.getModifiers()) || isDefault(member.getModifiers())) { + if (!member.getDeclaringClass().getPackage().equals(entity.getClass().getPackage())) { + return true; + } + } + + if (isAccessible(member.getDeclaringClass()) && isAccessible(member.getModifiers())) { + return false; + } + } + return true; + } + + /** + * Retrieves the class variable index with an offset of {@code 4}. + * + * @param list + * @param item + * @return + */ + private int classVariableIndex4(List> list, Class item) { + return 4 + list.indexOf(item); + } + + } + + private static String referenceName(Class type) { + if (type.isArray()) { + return Type.getInternalName(type); + } + return referenceName(Type.getInternalName(type)); + } + + private static String referenceName(String internalTypeName) { + return String.format("L%s;", internalTypeName); + } + + private static Map createPropertyStackMap( + List> persistentProperties) { + + Map stackmap = new HashMap(); + + for (PersistentProperty property : persistentProperties) { + stackmap.put(property.getName(), new PropertyStackAddress(new Label(), property.getName().hashCode())); + } + return stackmap; + } + + /** + * Returns the appropriate autoboxing type. + * + * @param unboxed + * @return + */ + private static Class autoboxType(Class unboxed) { + + if (unboxed.equals(Boolean.TYPE)) { + return Boolean.class; + } + + if (unboxed.equals(Byte.TYPE)) { + return Byte.class; + } + + if (unboxed.equals(Character.TYPE)) { + return Character.class; + } + + if (unboxed.equals(Double.TYPE)) { + return Double.class; + } + + if (unboxed.equals(Float.TYPE)) { + return Float.class; + } + + if (unboxed.equals(Integer.TYPE)) { + return Integer.class; + } + + if (unboxed.equals(Long.TYPE)) { + return Long.class; + } + + if (unboxed.equals(Short.TYPE)) { + return Short.class; + } + + if (unboxed.equals(Void.TYPE)) { + return Void.class; + } + + return unboxed; + } + + /** + * Autobox/Auto-unbox primitives to object and vice versa. + * + * @param in the input type + * @param out the expected output type + * @param mv + */ + private static void autoboxIfNeeded(Class in, Class out, MethodVisitor mv) { + + if (in.equals(Boolean.class) && out.equals(Boolean.TYPE)) { + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false); + } + + if (in.equals(Boolean.TYPE) && out.equals(Boolean.class)) { + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false); + } + + if (in.equals(Byte.class) && out.equals(Byte.TYPE)) { + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B", false); + } + + if (in.equals(Byte.TYPE) && out.equals(Byte.class)) { + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false); + } + + if (in.equals(Character.class) && out.equals(Character.TYPE)) { + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C", false); + } + + if (in.equals(Character.TYPE) && out.equals(Character.class)) { + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", false); + } + + if (in.equals(Double.class) && out.equals(Double.TYPE)) { + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D", false); + } + + if (in.equals(Double.TYPE) && out.equals(Double.class)) { + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false); + } + + if (in.equals(Float.class) && out.equals(Float.TYPE)) { + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F", false); + } + + if (in.equals(Float.TYPE) && out.equals(Float.class)) { + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false); + } + + if (in.equals(Integer.class) && out.equals(Integer.TYPE)) { + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false); + } + + if (in.equals(Integer.TYPE) && out.equals(Integer.class)) { + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); + } + + if (in.equals(Long.class) && out.equals(Long.TYPE)) { + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false); + } + + if (in.equals(Long.TYPE) && out.equals(Long.class)) { + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false); + } + + if (in.equals(Short.class) && out.equals(Short.TYPE)) { + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S", false); + } + + if (in.equals(Short.TYPE) && out.equals(Short.class)) { + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false); + } + } + + /** + * Returns the signature type for a {@link Class} including primitives. + * + * @param type + * @return + */ + private static String signatureTypeName(Class type) { + + if (type.equals(Boolean.TYPE)) { + return "Z"; + } + + if (type.equals(Byte.TYPE)) { + return "B"; + } + + if (type.equals(Character.TYPE)) { + return "C"; + } + + if (type.equals(Double.TYPE)) { + return "D"; + } + + if (type.equals(Float.TYPE)) { + return "F"; + } + + if (type.equals(Integer.TYPE)) { + return "I"; + } + + if (type.equals(Long.TYPE)) { + return "J"; + } + + if (type.equals(Short.TYPE)) { + return "S"; + } + + if (type.equals(Void.TYPE)) { + return "V"; + } + + return referenceName(type); + } + + /** + * Stack map address for a particular property. + * + * @author Mark Paluch + */ + static class PropertyStackAddress implements Comparable { + + final int hash; + final Label label; + + public PropertyStackAddress(Label label, int hash) { + this.label = label; + this.hash = hash; + } + + @Override + public int compareTo(PropertyStackAddress o) { + return (hash < o.hash) ? -1 : ((hash == o.hash) ? 0 : 1); + } + } + + /** + * Yep, the name tells the truth. This little guy registers a class in the class loader of the + * {@link PersistentEntity} to allow protected and package-default access as protected/package-default members must be + * accessed from a class in the same class loader. + * + * @author Mark Paluch + */ + private static class Evil { + + final static Evil injector = new Evil(); + + private Evil() {} + + /** + * Define a Class in the {@link ClassLoader} of the {@link PersistentEntity} type. + * + * @param name + * @param bytes + * @param offset + * @param len + * @param persistentEntity + * @return + */ + Class defineClass(String name, byte[] bytes, int offset, int len, PersistentEntity persistentEntity) { + + ClassLoader classLoader = persistentEntity.getType().getClassLoader(); + Class classLoaderClass = classLoader.getClass(); + try { + + Class persistentEntityClass = persistentEntity.getClass(); + Method defineClass = ReflectionUtils.findMethod(classLoaderClass, "defineClass", String.class, byte[].class, + Integer.TYPE, Integer.TYPE, ProtectionDomain.class); + defineClass.setAccessible(true); + + return (Class) defineClass.invoke(classLoader, name, bytes, offset, len, + persistentEntityClass.getProtectionDomain()); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + } + +} diff --git a/src/main/java/org/springframework/data/mapping/model/DefaultPersistentPropertyAccessorFactory.java b/src/main/java/org/springframework/data/mapping/model/DefaultPersistentPropertyAccessorFactory.java new file mode 100644 index 000000000..2c7e33eaa --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/DefaultPersistentPropertyAccessorFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016 the original author or authors. + * + * 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 org.springframework.data.mapping.model; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentPropertyAccessor; + +/** + * Default implementation of {@link PersistentPropertyAccessorFactory}. Accessors can access bean properties either via + * reflection or use generated classes with direct field/method access. + * + * @author Mark Paluch + * @since 1.13 + */ +public class DefaultPersistentPropertyAccessorFactory implements PersistentPropertyAccessorFactory { + + private final ClassGeneratingPropertyAccessorFactory classGeneratingPropertyAccessorFactory = new ClassGeneratingPropertyAccessorFactory(); + + /* (non-Javadoc) + * @see org.springframework.data.mapping.model.PersistentPropertyAccessorFactory#getPropertyAccessor(org.springframework.data.mapping.PersistentEntity, java.lang.Object) + */ + @Override + public PersistentPropertyAccessor getPropertyAccessor(PersistentEntity entity, Object bean) { + + if (ClassGeneratingPropertyAccessorFactory.canGenerateAccessorClass(entity)) { + return classGeneratingPropertyAccessorFactory.getPropertyAccessor(entity, bean); + } + return new BeanWrapper(bean); + } + +} diff --git a/src/main/java/org/springframework/data/mapping/model/PersistentPropertyAccessorFactory.java b/src/main/java/org/springframework/data/mapping/model/PersistentPropertyAccessorFactory.java new file mode 100644 index 000000000..ae5b5b292 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/PersistentPropertyAccessorFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016 the original author or authors. + * + * 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 org.springframework.data.mapping.model; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentPropertyAccessor; + +/** + * Factory to create {@link PersistentPropertyAccessor} for a given {@link PersistentEntity} and {@code bean}. + * + * @author Mark Paluch + * @since 1.13 + */ +public interface PersistentPropertyAccessorFactory { + + /** + * Returns a {@link PersistentPropertyAccessor} for a given {@link PersistentEntity} and {@code bean}. + * + * @param bean + * @return + */ + PersistentPropertyAccessor getPropertyAccessor(PersistentEntity entity, Object bean); + +} diff --git a/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java b/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java index 6e2031f7f..28c8a5de4 100644 --- a/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java @@ -17,6 +17,7 @@ package org.springframework.data.mapping.model; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.junit.Assume.assumeThat; import static org.mockito.Mockito.*; import java.lang.annotation.Retention; @@ -25,6 +26,7 @@ import java.util.Comparator; import java.util.Iterator; import java.util.List; +import org.hamcrest.CoreMatchers; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -49,7 +51,7 @@ import org.springframework.test.util.ReflectionTestUtils; /** * Unit test for {@link BasicPersistentEntity}. - * + * * @author Oliver Gierke * @author Christoph Strobl */ @@ -181,6 +183,8 @@ public class BasicPersistentEntityUnitTests> { @Test public void returnsBeanWrapperForPropertyAccessor() { + assumeThat(System.getProperty("java.version"), CoreMatchers.startsWith("1.6")); + SampleMappingContext context = new SampleMappingContext(); PersistentEntity entity = context.getPersistentEntity(Entity.class); @@ -191,6 +195,25 @@ public class BasicPersistentEntityUnitTests> { assertThat(accessor.getBean(), is((Object) value)); } + /** + * @see DATACMNS-809 + */ + @Test + public void returnsGeneratedPropertyAccessorForPropertyAccessor() { + + assumeThat(System.getProperty("java.version"), not(CoreMatchers.startsWith("1.6"))); + + SampleMappingContext context = new SampleMappingContext(); + PersistentEntity entity = context.getPersistentEntity(Entity.class); + + Entity value = new Entity(); + PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value); + + assertThat(accessor, is(not(instanceOf(BeanWrapper.class)))); + assertThat(accessor.getClass().getName(), containsString("_Accessor_")); + assertThat(accessor.getBean(), is((Object) value)); + } + /** * @see DATACMNS-596 */ diff --git a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryDatatypeTests.java b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryDatatypeTests.java new file mode 100644 index 000000000..5e5065aec --- /dev/null +++ b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryDatatypeTests.java @@ -0,0 +1,247 @@ +/* + * Copyright 2016 the original author or authors. + * + * 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 org.springframework.data.mapping.model; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.annotation.AccessType; +import org.springframework.data.annotation.AccessType.Type; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.SampleMappingContext; +import org.springframework.data.mapping.context.SamplePersistentProperty; + +import lombok.Data; + +/** + * Unit tests for {@link ClassGeneratingPropertyAccessorFactory} + * + * @author Mark Paluch + * @see DATACMNS-809 + */ +@RunWith(Parameterized.class) +public class ClassGeneratingPropertyAccessorFactoryDatatypeTests { + + private final ClassGeneratingPropertyAccessorFactory factory = new ClassGeneratingPropertyAccessorFactory(); + private final SampleMappingContext mappingContext = new SampleMappingContext(); + + private Object bean; + private String propertyName; + private Object value; + + public ClassGeneratingPropertyAccessorFactoryDatatypeTests(Object bean, String propertyName, Object value, + String displayName) { + + this.bean = bean; + this.propertyName = propertyName; + this.value = value; + } + + @Parameters(name = "{3}") + public static List parameters() throws Exception { + + List parameters = new ArrayList(); + + List> types = Arrays.asList(FieldAccess.class, PropertyAccess.class); + + parameters.addAll(parameters(types, "primitiveInteger", Integer.valueOf(1))); + parameters.addAll(parameters(types, "primitiveIntegerArray", new int[] { 1, 2, 3 })); + parameters.addAll(parameters(types, "boxedInteger", Integer.valueOf(1))); + parameters.addAll(parameters(types, "boxedIntegerArray", new Integer[] { Integer.valueOf(1) })); + parameters.addAll(parameters(types, "primitiveShort", Short.valueOf("1"))); + parameters.addAll(parameters(types, "primitiveShortArray", new short[] { 1, 2, 3 })); + parameters.addAll(parameters(types, "boxedShort", Short.valueOf("1"))); + parameters.addAll(parameters(types, "boxedShortArray", new Short[] { Short.valueOf("1") })); + parameters.addAll(parameters(types, "primitiveByte", Byte.valueOf("1"))); + parameters.addAll(parameters(types, "primitiveByteArray", new byte[] { 1, 2, 3 })); + parameters.addAll(parameters(types, "boxedByte", Byte.valueOf("1"))); + parameters.addAll(parameters(types, "boxedByteArray", new Byte[] { Byte.valueOf("1") })); + parameters.addAll(parameters(types, "primitiveChar", Character.valueOf('c'))); + parameters.addAll(parameters(types, "primitiveCharArray", new char[] { 'a', 'b', 'c' })); + parameters.addAll(parameters(types, "boxedChar", Character.valueOf('c'))); + parameters.addAll(parameters(types, "boxedCharArray", new Character[] { Character.valueOf('c') })); + parameters.addAll(parameters(types, "primitiveBoolean", Boolean.valueOf(true))); + parameters.addAll(parameters(types, "primitiveBooleanArray", new boolean[] { true, false })); + parameters.addAll(parameters(types, "boxedBoolean", Boolean.valueOf(true))); + parameters.addAll(parameters(types, "boxedBooleanArray", new Boolean[] { Boolean.valueOf(true) })); + parameters.addAll(parameters(types, "primitiveFloat", Float.valueOf(1f))); + parameters.addAll(parameters(types, "primitiveFloatArray", new float[] { 1f, 2f })); + parameters.addAll(parameters(types, "boxedFloat", Float.valueOf(1f))); + parameters.addAll(parameters(types, "boxedFloatArray", new Float[] { Float.valueOf(1f) })); + parameters.addAll(parameters(types, "primitiveDouble", Double.valueOf(1d))); + parameters.addAll(parameters(types, "primitiveDoubleArray", new double[] { 1d, 2d })); + parameters.addAll(parameters(types, "boxedDouble", Double.valueOf(1d))); + parameters.addAll(parameters(types, "boxedDoubleArray", new Double[] { Double.valueOf(1d) })); + parameters.addAll(parameters(types, "primitiveLong", Long.valueOf(1L))); + parameters.addAll(parameters(types, "primitiveLongArray", new long[] { 1L, 2L })); + parameters.addAll(parameters(types, "boxedLong", Long.valueOf(1L))); + parameters.addAll(parameters(types, "boxedLongArray", new Long[] { Long.valueOf(1L) })); + parameters.addAll(parameters(types, "string", "hello")); + parameters.addAll(parameters(types, "stringArray", new String[] { "hello", "world" })); + + return parameters; + } + + private static List parameters(List> types, String propertyName, Object value) throws Exception { + + List parameters = new ArrayList(); + + for (Class type : types) { + parameters + .add(new Object[] { type.newInstance(), propertyName, value, type.getSimpleName() + "/" + propertyName }); + } + + return parameters; + } + + /** + * @see DATACMNS-809 + * @throws Exception + */ + @Test + public void shouldSetAndGetProperty() throws Exception { + + PersistentProperty property = getProperty(bean, propertyName); + PersistentPropertyAccessor persistentPropertyAccessor = getPersistentPropertyAccessor(bean); + + persistentPropertyAccessor.setProperty(property, value); + assertThat(persistentPropertyAccessor.getProperty(property), is(equalTo((Object) value))); + } + + private PersistentPropertyAccessor getPersistentPropertyAccessor(Object bean) { + return factory.getPropertyAccessor(mappingContext.getPersistentEntity(bean.getClass()), bean); + } + + private PersistentProperty getProperty(Object bean, String name) { + + BasicPersistentEntity persistentEntity = mappingContext + .getPersistentEntity(bean.getClass()); + return persistentEntity.getPersistentProperty(name); + } + + /** + * @see DATACMNS-809 + */ + @AccessType(Type.FIELD) + public static class FieldAccess { + + int primitiveInteger; + int primitiveIntegerArray[]; + Integer boxedInteger; + Integer boxedIntegerArray[]; + + short primitiveShort; + short primitiveShortArray[]; + Short boxedShort; + Short boxedShortArray[]; + + byte primitiveByte; + byte primitiveByteArray[]; + Byte boxedByte; + Byte boxedByteArray[]; + + char primitiveChar; + char primitiveCharArray[]; + Character boxedChar; + Character boxedCharArray[]; + + boolean primitiveBoolean; + boolean primitiveBooleanArray[]; + Boolean boxedBoolean; + Boolean boxedBooleanArray[]; + + float primitiveFloat; + float primitiveFloatArray[]; + Float boxedFloat; + Float boxedFloatArray[]; + + double primitiveDouble; + double primitiveDoubleArray[]; + Double boxedDouble; + Double boxedDoubleArray[]; + + long primitiveLong; + long primitiveLongArray[]; + Long boxedLong; + Long boxedLongArray[]; + + String string; + String stringArray[]; + } + + /** + * @see DATACMNS-809 + */ + @AccessType(Type.PROPERTY) + @Data + public static class PropertyAccess { + + int primitiveInteger; + int primitiveIntegerArray[]; + Integer boxedInteger; + Integer boxedIntegerArray[]; + + short primitiveShort; + short primitiveShortArray[]; + Short boxedShort; + Short boxedShortArray[]; + + byte primitiveByte; + byte primitiveByteArray[]; + Byte boxedByte; + Byte boxedByteArray[]; + + char primitiveChar; + char primitiveCharArray[]; + Character boxedChar; + Character boxedCharArray[]; + + boolean primitiveBoolean; + boolean primitiveBooleanArray[]; + Boolean boxedBoolean; + Boolean boxedBooleanArray[]; + + float primitiveFloat; + float primitiveFloatArray[]; + Float boxedFloat; + Float boxedFloatArray[]; + + double primitiveDouble; + double primitiveDoubleArray[]; + Double boxedDouble; + Double boxedDoubleArray[]; + + long primitiveLong; + long primitiveLongArray[]; + Long boxedLong; + Long boxedLongArray[]; + + String string; + String stringArray[]; + } + +} diff --git a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryTests.java b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryTests.java new file mode 100644 index 000000000..a8f5d6586 --- /dev/null +++ b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryTests.java @@ -0,0 +1,433 @@ +/* + * Copyright 2016 the original author or authors. + * + * 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 org.springframework.data.mapping.model; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.*; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.annotation.AccessType; +import org.springframework.data.annotation.AccessType.Type; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.SampleMappingContext; +import org.springframework.data.mapping.context.SamplePersistentProperty; +import org.springframework.data.mapping.model.subpackage.TypeInOtherPackage; + +/** + * Unit tests for {@link ClassGeneratingPropertyAccessorFactory} + * + * @author Mark Paluch + * @see DATACMNS-809 + */ +@RunWith(Parameterized.class) +public class ClassGeneratingPropertyAccessorFactoryTests { + + private final ClassGeneratingPropertyAccessorFactory factory = new ClassGeneratingPropertyAccessorFactory(); + private final SampleMappingContext mappingContext = new SampleMappingContext(); + + private Object bean; + private String propertyName; + private Class expectedConstructorType; + + public ClassGeneratingPropertyAccessorFactoryTests(Object bean, String propertyName, Class expectedConstructorType, + String displayName) { + + this.bean = bean; + this.propertyName = propertyName; + this.expectedConstructorType = expectedConstructorType; + } + + @Parameters(name = "{3}") + public static List parameters() { + + List parameters = new ArrayList(); + + List propertyNames = Arrays.asList("privateField", "packageDefaultField", "protectedField", "publicField", + "privateProperty", "packageDefaultProperty", "protectedProperty", "publicProperty", "syntheticProperty"); + parameters.addAll(parameters(new InnerPrivateType(), propertyNames, Object.class)); + parameters.addAll(parameters(new InnerTypeWithPrivateAncesor(), propertyNames, InnerTypeWithPrivateAncesor.class)); + parameters.addAll(parameters(new InnerPackageDefaultType(), propertyNames, InnerPackageDefaultType.class)); + parameters.addAll(parameters(new InnerProtectedType(), propertyNames, InnerProtectedType.class)); + parameters.addAll(parameters(new InnerPublicType(), propertyNames, InnerPublicType.class)); + parameters.addAll(parameters(new ClassGeneratingPropertyAccessorPackageDefaultType(), propertyNames, + ClassGeneratingPropertyAccessorPackageDefaultType.class)); + parameters.addAll(parameters(new ClassGeneratingPropertyAccessorPublicType(), propertyNames, + ClassGeneratingPropertyAccessorPublicType.class)); + parameters.addAll(parameters(new SubtypeOfTypeInOtherPackage(), propertyNames, + SubtypeOfTypeInOtherPackage.class)); + + return parameters; + } + + private static List parameters(Object bean, List propertyNames, Class expectedConstructorType) { + + List parameters = new ArrayList(); + + for (String propertyName : propertyNames) { + parameters.add(new Object[] { bean, propertyName, expectedConstructorType, + bean.getClass().getSimpleName() + "/" + propertyName }); + } + + return parameters; + } + + /** + * @see DATACMNS-809 + * @throws Exception + */ + @Test + public void shouldSetAndGetProperty() throws Exception { + + PersistentProperty property = getProperty(bean, propertyName); + PersistentPropertyAccessor persistentPropertyAccessor = getPersistentPropertyAccessor(bean); + + persistentPropertyAccessor.setProperty(property, "value"); + assertThat(persistentPropertyAccessor.getProperty(property), is(equalTo((Object) "value"))); + } + + /** + * @see DATACMNS-809 + * @throws Exception + */ + @Test + public void accessorShouldDeclareConstructor() throws Exception { + + PersistentPropertyAccessor persistentPropertyAccessor = getPersistentPropertyAccessor(bean); + + Constructor[] declaredConstructors = persistentPropertyAccessor.getClass().getDeclaredConstructors(); + assertThat(declaredConstructors.length, is(1)); + assertThat(declaredConstructors[0].getParameterTypes().length, is(1)); + assertThat(declaredConstructors[0].getParameterTypes()[0], is(equalTo((Class) expectedConstructorType))); + } + + /** + * @see DATACMNS-809 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldFailOnNullBean() { + factory.getPropertyAccessor(mappingContext.getPersistentEntity(bean.getClass()), null); + } + + /** + * @see DATACMNS-809 + */ + @Test(expected = UnsupportedOperationException.class) + public void getPropertyShouldFailOnUnhandledProperty() { + + PersistentProperty property = getProperty(new Dummy(), "dummy"); + PersistentPropertyAccessor persistentPropertyAccessor = getPersistentPropertyAccessor(bean); + + persistentPropertyAccessor.getProperty(property); + } + + /** + * @see DATACMNS-809 + */ + @Test(expected = UnsupportedOperationException.class) + public void setPropertyShouldFailOnUnhandledProperty() { + + PersistentProperty property = getProperty(new Dummy(), "dummy"); + PersistentPropertyAccessor persistentPropertyAccessor = getPersistentPropertyAccessor(bean); + + persistentPropertyAccessor.setProperty(property, null); + } + + private PersistentPropertyAccessor getPersistentPropertyAccessor(Object bean) { + return factory.getPropertyAccessor(mappingContext.getPersistentEntity(bean.getClass()), bean); + } + + private PersistentProperty getProperty(Object bean, String name) { + + BasicPersistentEntity persistentEntity = mappingContext + .getPersistentEntity(bean.getClass()); + return persistentEntity.getPersistentProperty(name); + } + + /** + * @see DATACMNS-809 + */ + private static class InnerPrivateType { + + private String privateField; + String packageDefaultField; + protected String protectedField; + public String publicField; + private String backing; + + @AccessType(Type.PROPERTY) private String privateProperty; + + @AccessType(Type.PROPERTY) private String packageDefaultProperty; + + @AccessType(Type.PROPERTY) private String protectedProperty; + + @AccessType(Type.PROPERTY) private String publicProperty; + + private String getPrivateProperty() { + return privateProperty; + } + + private void setPrivateProperty(String privateProperty) { + this.privateProperty = privateProperty; + } + + String getPackageDefaultProperty() { + return packageDefaultProperty; + } + + void setPackageDefaultProperty(String packageDefaultProperty) { + this.packageDefaultProperty = packageDefaultProperty; + } + + protected String getProtectedProperty() { + return protectedProperty; + } + + protected void setProtectedProperty(String protectedProperty) { + this.protectedProperty = protectedProperty; + } + + public String getPublicProperty() { + return publicProperty; + } + + public void setPublicProperty(String publicProperty) { + this.publicProperty = publicProperty; + } + + @AccessType(Type.PROPERTY) + public String getSyntheticProperty() { + return backing; + } + + public void setSyntheticProperty(String syntheticProperty) { + backing = syntheticProperty; + } + } + + /** + * @see DATACMNS-809 + */ + public static class InnerTypeWithPrivateAncesor extends InnerPrivateType { + + } + + /** + * @see DATACMNS-809 + */ + static class InnerPackageDefaultType { + + private String privateField; + String packageDefaultField; + protected String protectedField; + public String publicField; + private String backing; + + @AccessType(Type.PROPERTY) private String privateProperty; + + @AccessType(Type.PROPERTY) private String packageDefaultProperty; + + @AccessType(Type.PROPERTY) private String protectedProperty; + + @AccessType(Type.PROPERTY) private String publicProperty; + + private String getPrivateProperty() { + return privateProperty; + } + + private void setPrivateProperty(String privateProperty) { + this.privateProperty = privateProperty; + } + + String getPackageDefaultProperty() { + return packageDefaultProperty; + } + + void setPackageDefaultProperty(String packageDefaultProperty) { + this.packageDefaultProperty = packageDefaultProperty; + } + + protected String getProtectedProperty() { + return protectedProperty; + } + + protected void setProtectedProperty(String protectedProperty) { + this.protectedProperty = protectedProperty; + } + + public String getPublicProperty() { + return publicProperty; + } + + public void setPublicProperty(String publicProperty) { + this.publicProperty = publicProperty; + } + + @AccessType(Type.PROPERTY) + public String getSyntheticProperty() { + return backing; + } + + public void setSyntheticProperty(String syntheticProperty) { + backing = syntheticProperty; + } + } + + /** + * @see DATACMNS-809 + */ + protected static class InnerProtectedType { + + private String privateField; + String packageDefaultField; + protected String protectedField; + public String publicField; + private String backing; + + @AccessType(Type.PROPERTY) private String privateProperty; + + @AccessType(Type.PROPERTY) private String packageDefaultProperty; + + @AccessType(Type.PROPERTY) private String protectedProperty; + + @AccessType(Type.PROPERTY) private String publicProperty; + + private String getPrivateProperty() { + return privateProperty; + } + + private void setPrivateProperty(String privateProperty) { + this.privateProperty = privateProperty; + } + + String getPackageDefaultProperty() { + return packageDefaultProperty; + } + + void setPackageDefaultProperty(String packageDefaultProperty) { + this.packageDefaultProperty = packageDefaultProperty; + } + + protected String getProtectedProperty() { + return protectedProperty; + } + + protected void setProtectedProperty(String protectedProperty) { + this.protectedProperty = protectedProperty; + } + + public String getPublicProperty() { + return publicProperty; + } + + public void setPublicProperty(String publicProperty) { + this.publicProperty = publicProperty; + } + + @AccessType(Type.PROPERTY) + public String getSyntheticProperty() { + return backing; + } + + public void setSyntheticProperty(String syntheticProperty) { + backing = syntheticProperty; + } + } + + /** + * @see DATACMNS-809 + */ + public static class InnerPublicType { + + private String privateField; + String packageDefaultField; + protected String protectedField; + public String publicField; + private String backing; + + @AccessType(Type.PROPERTY) private String privateProperty; + + @AccessType(Type.PROPERTY) private String packageDefaultProperty; + + @AccessType(Type.PROPERTY) private String protectedProperty; + + @AccessType(Type.PROPERTY) private String publicProperty; + + private String getPrivateProperty() { + return privateProperty; + } + + private void setPrivateProperty(String privateProperty) { + this.privateProperty = privateProperty; + } + + String getPackageDefaultProperty() { + return packageDefaultProperty; + } + + void setPackageDefaultProperty(String packageDefaultProperty) { + this.packageDefaultProperty = packageDefaultProperty; + } + + protected String getProtectedProperty() { + return protectedProperty; + } + + protected void setProtectedProperty(String protectedProperty) { + this.protectedProperty = protectedProperty; + } + + public String getPublicProperty() { + return publicProperty; + } + + public void setPublicProperty(String publicProperty) { + this.publicProperty = publicProperty; + } + + @AccessType(Type.PROPERTY) + public String getSyntheticProperty() { + return backing; + } + + public void setSyntheticProperty(String syntheticProperty) { + backing = syntheticProperty; + } + } + + public static class SubtypeOfTypeInOtherPackage extends TypeInOtherPackage{ + + } + + /** + * @see DATACMNS-809 + */ + private static class Dummy { + private String dummy; + public String publicField; + } + +} diff --git a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPackageDefaultType.java b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPackageDefaultType.java new file mode 100644 index 000000000..f10d540b9 --- /dev/null +++ b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPackageDefaultType.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016 the original author or authors. + * + * 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 org.springframework.data.mapping.model; + +import org.springframework.data.annotation.AccessType; +import org.springframework.data.annotation.AccessType.Type; + +/** + * @author Mark Paluch + * @see DATACMNS-809 + */ +class ClassGeneratingPropertyAccessorPackageDefaultType { + + private String privateField; + String packageDefaultField; + protected String protectedField; + public String publicField; + private String backing; + + @AccessType(Type.PROPERTY) private String privateProperty; + + @AccessType(Type.PROPERTY) private String packageDefaultProperty; + + @AccessType(Type.PROPERTY) private String protectedProperty; + + @AccessType(Type.PROPERTY) private String publicProperty; + + private String getPrivateProperty() { + return privateProperty; + } + + private void setPrivateProperty(String privateProperty) { + this.privateProperty = privateProperty; + } + + String getPackageDefaultProperty() { + return packageDefaultProperty; + } + + void setPackageDefaultProperty(String packageDefaultProperty) { + this.packageDefaultProperty = packageDefaultProperty; + } + + protected String getProtectedProperty() { + return protectedProperty; + } + + protected void setProtectedProperty(String protectedProperty) { + this.protectedProperty = protectedProperty; + } + + public String getPublicProperty() { + return publicProperty; + } + + public void setPublicProperty(String publicProperty) { + this.publicProperty = publicProperty; + } + + @AccessType(Type.PROPERTY) + public String getSyntheticProperty() { + return backing; + } + + public void setSyntheticProperty(String syntheticProperty) { + backing = syntheticProperty; + } + +} diff --git a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPublicType.java b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPublicType.java new file mode 100644 index 000000000..d098a684d --- /dev/null +++ b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPublicType.java @@ -0,0 +1,94 @@ +/* + * Copyright 2016 the original author or authors. + * + * 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 org.springframework.data.mapping.model; + +import org.springframework.data.annotation.AccessType; +import org.springframework.data.annotation.AccessType.Type; + +/** + * @author Mark Paluch + * @see DATACMNS-809 + */ +public class ClassGeneratingPropertyAccessorPublicType { + + private String privateField; + String packageDefaultField; + protected String protectedField; + public String publicField; + private String backing; + private Integer aa; + private int bb; + + @AccessType(Type.PROPERTY) private String privateProperty; + + @AccessType(Type.PROPERTY) private String packageDefaultProperty; + + @AccessType(Type.PROPERTY) private String protectedProperty; + + @AccessType(Type.PROPERTY) private String publicProperty; + + private String getPrivateProperty() { + return privateProperty; + } + + private void setPrivateProperty(String privateProperty) { + this.privateProperty = privateProperty; + } + + String getPackageDefaultProperty() { + return packageDefaultProperty; + } + + void setPackageDefaultProperty(String packageDefaultProperty) { + this.packageDefaultProperty = packageDefaultProperty; + } + + protected String getProtectedProperty() { + return protectedProperty; + } + + protected void setProtectedProperty(String protectedProperty) { + this.protectedProperty = protectedProperty; + } + + public String getPublicProperty() { + return publicProperty; + } + + public void setPublicProperty(String publicProperty) { + this.publicProperty = publicProperty; + } + + @AccessType(Type.PROPERTY) + public String getSyntheticProperty() { + return backing; + } + + public void setSyntheticProperty(String syntheticProperty) { + backing = syntheticProperty; + } + + public Object set(Object e) { + aa = (Integer) e; + bb = (Integer) e; + return bb; + } + + public static void main(String[] args) { + + } +} diff --git a/src/test/java/org/springframework/data/mapping/model/subpackage/TypeInOtherPackage.java b/src/test/java/org/springframework/data/mapping/model/subpackage/TypeInOtherPackage.java new file mode 100644 index 000000000..ae65b6f04 --- /dev/null +++ b/src/test/java/org/springframework/data/mapping/model/subpackage/TypeInOtherPackage.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016 the original author or authors. + * + * 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 org.springframework.data.mapping.model.subpackage; + +import org.springframework.data.annotation.AccessType; +import org.springframework.data.annotation.AccessType.Type; + +/** + * @author Mark Paluch + * @see DATACMNS-809 + */ +public class TypeInOtherPackage { + + private String privateField; + String packageDefaultField; + protected String protectedField; + public String publicField; + private String backing; + + @AccessType(Type.PROPERTY) private String privateProperty; + + @AccessType(Type.PROPERTY) private String packageDefaultProperty; + + @AccessType(Type.PROPERTY) private String protectedProperty; + + @AccessType(Type.PROPERTY) private String publicProperty; + + private String getPrivateProperty() { + return privateProperty; + } + + private void setPrivateProperty(String privateProperty) { + this.privateProperty = privateProperty; + } + + String getPackageDefaultProperty() { + return packageDefaultProperty; + } + + void setPackageDefaultProperty(String packageDefaultProperty) { + this.packageDefaultProperty = packageDefaultProperty; + } + + protected String getProtectedProperty() { + return protectedProperty; + } + + protected void setProtectedProperty(String protectedProperty) { + this.protectedProperty = protectedProperty; + } + + public String getPublicProperty() { + return publicProperty; + } + + public void setPublicProperty(String publicProperty) { + this.publicProperty = publicProperty; + } + + @AccessType(Type.PROPERTY) + public String getSyntheticProperty() { + return backing; + } + + public void setSyntheticProperty(String syntheticProperty) { + backing = syntheticProperty; + } +}