From f70189f41678c9107d8e8268ff40f20d9ebd847c Mon Sep 17 00:00:00 2001 From: zuchos Date: Fri, 27 Sep 2013 13:20:33 +0200 Subject: [PATCH] HHH-8505: Auditing of dynamic components --- .../metadata/ComponentMetadataGenerator.java | 85 ++++--- .../reader/AuditedPropertiesReader.java | 165 ++++++++----- .../metadata/reader/DynamicProperty.java | 101 ++++++++ .../mapper/ComponentPropertyMapper.java | 19 +- .../mapper/MultiDynamicComponentMapper.java | 102 ++++++++ .../entities/mapper/MultiPropertyMapper.java | 9 +- .../envers/internal/tools/MapProxyTool.java | 140 +++++++++++ .../envers/internal/tools/StringTools.java | 92 +++---- .../envers/internal/tools/MapProxyTest.java | 23 ++ .../AuditedDynamicComponentEntity.java | 93 +++++++ .../dynamic/AuditedDynamicComponentTest.java | 232 ++++++++++++++---- .../components/dynamic/SimpleEntity.java | 63 +++++ .../dynamicComponents/mapAudited.hbm.xml | 15 +- 13 files changed, 937 insertions(+), 202 deletions(-) create mode 100644 hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/DynamicProperty.java create mode 100644 hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiDynamicComponentMapper.java create mode 100644 hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/MapProxyTool.java create mode 100644 hibernate-envers/src/test/java/org/hibernate/envers/internal/tools/MapProxyTest.java create mode 100644 hibernate-envers/src/test/java/org/hibernate/envers/test/integration/components/dynamic/AuditedDynamicComponentEntity.java create mode 100644 hibernate-envers/src/test/java/org/hibernate/envers/test/integration/components/dynamic/SimpleEntity.java diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ComponentMetadataGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ComponentMetadataGenerator.java index 103a17e9f5..16b783b05a 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ComponentMetadataGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ComponentMetadataGenerator.java @@ -23,10 +23,7 @@ */ package org.hibernate.envers.configuration.internal.metadata; -import java.util.Iterator; - import org.dom4j.Element; - import org.hibernate.envers.configuration.internal.metadata.reader.ComponentAuditingData; import org.hibernate.envers.configuration.internal.metadata.reader.PropertyAuditingData; import org.hibernate.envers.internal.entities.mapper.CompositeMapperBuilder; @@ -35,52 +32,64 @@ import org.hibernate.mapping.Component; import org.hibernate.mapping.Property; import org.hibernate.mapping.Value; +import java.util.Iterator; +import java.util.Map; + /** * Generates metadata for components. * * @author Adam Warski (adam at warski dot org) + * @author Lukasz Zuchowski (author at zuchos dot com) */ public final class ComponentMetadataGenerator { - private final AuditMetadataGenerator mainGenerator; + private final AuditMetadataGenerator mainGenerator; - ComponentMetadataGenerator(AuditMetadataGenerator auditMetadataGenerator) { - mainGenerator = auditMetadataGenerator; - } + ComponentMetadataGenerator(AuditMetadataGenerator auditMetadataGenerator) { + mainGenerator = auditMetadataGenerator; + } - @SuppressWarnings({"unchecked"}) - public void addComponent( - Element parent, PropertyAuditingData propertyAuditingData, - Value value, CompositeMapperBuilder mapper, String entityName, - EntityXmlMappingData xmlMappingData, boolean firstPass) { - final Component propComponent = (Component) value; + @SuppressWarnings({"unchecked"}) + public void addComponent( + Element parent, PropertyAuditingData propertyAuditingData, + Value value, CompositeMapperBuilder mapper, String entityName, + EntityXmlMappingData xmlMappingData, boolean firstPass) { + final Component propComponent = (Component) value; - final Class componentClass = ReflectionTools.loadClass( - propComponent.getComponentClassName(), - mainGenerator.getClassLoaderService() - ); - final CompositeMapperBuilder componentMapper = mapper.addComponent( - propertyAuditingData.getPropertyData(), - componentClass - ); + final Class componentClass; + if (propComponent.isDynamic()) { + componentClass = ReflectionTools.loadClass( + Map.class.getCanonicalName(), + mainGenerator.getClassLoaderService()); - // The property auditing data must be for a component. - final ComponentAuditingData componentAuditingData = (ComponentAuditingData) propertyAuditingData; + } else { + componentClass = ReflectionTools.loadClass( + propComponent.getComponentClassName(), + mainGenerator.getClassLoaderService() + ); + } + final CompositeMapperBuilder componentMapper = mapper.addComponent( + propertyAuditingData.getPropertyData(), + componentClass + ); - // Adding all properties of the component - final Iterator properties = (Iterator) propComponent.getPropertyIterator(); - while ( properties.hasNext() ) { - final Property property = properties.next(); + // The property auditing data must be for a component. + final ComponentAuditingData componentAuditingData = (ComponentAuditingData) propertyAuditingData; - final PropertyAuditingData componentPropertyAuditingData = - componentAuditingData.getPropertyAuditingData( property.getName() ); + // Adding all properties of the component + final Iterator properties = (Iterator) propComponent.getPropertyIterator(); + while (properties.hasNext()) { + final Property property = properties.next(); - // Checking if that property is audited - if ( componentPropertyAuditingData != null ) { - mainGenerator.addValue( - parent, property.getValue(), componentMapper, entityName, xmlMappingData, - componentPropertyAuditingData, property.isInsertable(), firstPass, false - ); - } - } - } + final PropertyAuditingData componentPropertyAuditingData = + componentAuditingData.getPropertyAuditingData(property.getName()); + + // Checking if that property is audited + if (componentPropertyAuditingData != null) { + mainGenerator.addValue( + parent, property.getValue(), componentMapper, entityName, xmlMappingData, + componentPropertyAuditingData, property.isInsertable(), firstPass, false + ); + } + } + } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/AuditedPropertiesReader.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/AuditedPropertiesReader.java index a76050504a..0770c6de7c 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/AuditedPropertiesReader.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/AuditedPropertiesReader.java @@ -23,30 +23,12 @@ */ package org.hibernate.envers.configuration.internal.metadata.reader; -import javax.persistence.JoinColumn; -import javax.persistence.MapKey; -import javax.persistence.OneToMany; -import javax.persistence.Version; -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - import org.hibernate.MappingException; import org.hibernate.annotations.common.reflection.ReflectionManager; import org.hibernate.annotations.common.reflection.XClass; import org.hibernate.annotations.common.reflection.XProperty; -import org.hibernate.envers.AuditJoinTable; -import org.hibernate.envers.AuditMappedBy; -import org.hibernate.envers.AuditOverride; -import org.hibernate.envers.AuditOverrides; -import org.hibernate.envers.Audited; -import org.hibernate.envers.ModificationStore; -import org.hibernate.envers.NotAudited; -import org.hibernate.envers.RelationTargetAuditMode; +import org.hibernate.cfg.AccessType; +import org.hibernate.envers.*; import org.hibernate.envers.configuration.internal.GlobalConfiguration; import org.hibernate.envers.configuration.internal.metadata.MetadataTools; import org.hibernate.envers.internal.tools.MappingTools; @@ -56,6 +38,13 @@ import org.hibernate.mapping.Component; import org.hibernate.mapping.Property; import org.hibernate.mapping.Value; +import javax.persistence.JoinColumn; +import javax.persistence.MapKey; +import javax.persistence.OneToMany; +import javax.persistence.Version; +import java.lang.annotation.Annotation; +import java.util.*; + import static org.hibernate.envers.internal.tools.Tools.newHashMap; import static org.hibernate.envers.internal.tools.Tools.newHashSet; @@ -68,6 +57,7 @@ import static org.hibernate.envers.internal.tools.Tools.newHashSet; * @author Hern&aacut;n Chanfreau * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) * @author Michal Skowronek (mskowr at o2 dot pl) + * @author Lukasz Zuchowski (author at zuchos dot com) */ public class AuditedPropertiesReader { protected final ModificationStore defaultStore; @@ -117,14 +107,18 @@ public class AuditedPropertiesReader { // First reading the access types for the persistent properties. readPersistentPropertiesAccess(); - // Retrieve classes and properties that are explicitly marked for auditing process by any superclass - // of currently mapped entity or itself. - final XClass clazz = persistentPropertiesSource.getXClass(); - readAuditOverrides( clazz ); + if (persistentPropertiesSource instanceof DynamicComponentSource) { + addPropertiesFromDynamicComponent((DynamicComponentSource) persistentPropertiesSource); + } else { + // Retrieve classes and properties that are explicitly marked for auditing process by any superclass + // of currently mapped entity or itself. + final XClass clazz = persistentPropertiesSource.getXClass(); + readAuditOverrides( clazz ); - // Adding all properties from the given class. - addPropertiesFromClass( clazz ); - } + // Adding all properties from the given class. + addPropertiesFromClass( clazz ); + } + } /** * Recursively constructs sets of audited and not audited properties and classes which behavior has been overridden @@ -309,6 +303,42 @@ public class AuditedPropertiesReader { return allClassAudited; } + private void addPropertiesFromDynamicComponent(DynamicComponentSource dynamicComponentSource) { + Audited audited = computeAuditConfiguration(dynamicComponentSource.getXClass()); + if(!fieldAccessedPersistentProperties.isEmpty()) { + //TODO ŁŻ hmm... for sure ? + throw new MappingException("Dynamic component cannot have field accessed persistent properties"); + } + for (String property : propertyAccessedPersistentProperties) { + // If this is not a persistent property, with the same access type as currently checked, + // it's not audited as well. + // If the property was already defined by the subclass, is ignored by superclasses + String accessType = AccessType.PROPERTY.getType(); + if (!auditedPropertiesHolder.contains(property)) { + final Value propertyValue = persistentPropertiesSource.getProperty(property).getValue(); + if (propertyValue instanceof Component) { + this.addFromComponentProperty(new DynamicProperty(dynamicComponentSource, property), accessType, (Component) propertyValue, audited); + } else { + this.addFromNotComponentProperty(new DynamicProperty(dynamicComponentSource, property), accessType, audited); + } + } else if (propertiesGroupMapping.containsKey(property)) { + //todo ŁŻ - I'm not sure is that the case that we should handle for dynamic component. + // Retrieve embedded component name based on class field. + final String embeddedName = propertiesGroupMapping.get(property); + if (!auditedPropertiesHolder.contains(embeddedName)) { + // Manage properties mapped within tag. + final Value propertyValue = persistentPropertiesSource.getProperty(embeddedName).getValue(); + this.addFromPropertiesGroup( + embeddedName, + new DynamicProperty(dynamicComponentSource, property), accessType, + (Component) propertyValue, + audited + ); + } + } + } + } + /** * Recursively adds all audited properties of entity class and its superclasses. * @@ -317,7 +347,7 @@ public class AuditedPropertiesReader { private void addPropertiesFromClass(XClass clazz) { final Audited allClassAudited = computeAuditConfiguration( clazz ); - //look in the class + //look in the class addFromProperties( clazz.getDeclaredProperties( "field" ), "field", @@ -346,7 +376,7 @@ public class AuditedPropertiesReader { Audited allClassAudited) { for ( XProperty property : properties ) { // If this is not a persistent property, with the same access type as currently checked, - // it's not audited as well. + // it's not audited as well. // If the property was already defined by the subclass, is ignored by superclasses if ( persistentProperties.contains( property.getName() ) && !auditedPropertiesHolder.contains( property.getName() ) ) { @@ -404,43 +434,36 @@ public class AuditedPropertiesReader { } } - private void addFromComponentProperty( - XProperty property, - String accessType, - Component propertyValue, - Audited allClassAudited) { - final ComponentAuditingData componentData = new ComponentAuditingData(); - final boolean isAudited = fillPropertyData( property, componentData, accessType, allClassAudited ); + private void addFromComponentProperty( + XProperty property, + String accessType, + Component propertyValue, + Audited allClassAudited) { + final ComponentAuditingData componentData = new ComponentAuditingData(); + final boolean isAudited = fillPropertyData(property, componentData, accessType, allClassAudited); - if ( propertyValue.isDynamic() ) { - if ( isAudited ) { - throw new MappingException( - "Audited dynamic-component properties are not supported. Consider applying @NotAudited annotation to " - + propertyValue.getOwner().getEntityName() + "#" + property + "." - ); - } - return; - } + final PersistentPropertiesSource componentPropertiesSource; + if (propertyValue.isDynamic() && isAudited) { + componentPropertiesSource = new DynamicComponentSource(reflectionManager, propertyValue, property); + } else { + componentPropertiesSource = new ComponentPropertiesSource(reflectionManager, propertyValue); + } - final PersistentPropertiesSource componentPropertiesSource = new ComponentPropertiesSource( - reflectionManager, propertyValue - ); + final ComponentAuditedPropertiesReader audPropReader = new ComponentAuditedPropertiesReader( + ModificationStore.FULL, + componentPropertiesSource, + componentData, + globalCfg, + reflectionManager, + propertyNamePrefix + MappingTools.createComponentPrefix(property.getName()) + ); + audPropReader.read(); - final ComponentAuditedPropertiesReader audPropReader = new ComponentAuditedPropertiesReader( - ModificationStore.FULL, - componentPropertiesSource, - componentData, - globalCfg, - reflectionManager, - propertyNamePrefix + MappingTools.createComponentPrefix( property.getName() ) - ); - audPropReader.read(); - - if ( isAudited ) { - // Now we know that the property is audited - auditedPropertiesHolder.addPropertyAuditingData( property.getName(), componentData ); - } - } + if (isAudited) { + // Now we know that the property is audited + auditedPropertiesHolder.addPropertyAuditingData(property.getName(), componentData); + } + } private void addFromNotComponentProperty(XProperty property, String accessType, Audited allClassAudited) { final PropertyAuditingData propertyData = new PropertyAuditingData(); @@ -688,6 +711,11 @@ public class AuditedPropertiesReader { private final XClass xclass; private final Component component; + protected ComponentPropertiesSource(XClass xClazz, Component component) { + this.xclass = xClazz; + this.component = component; + } + public ComponentPropertiesSource(ReflectionManager reflectionManager, Component component) { try { this.xclass = reflectionManager.classForName( component.getComponentClassName(), this.getClass() ); @@ -715,4 +743,15 @@ public class AuditedPropertiesReader { return xclass; } } + + public static class DynamicComponentSource extends ComponentPropertiesSource { + + private XProperty baseProperty; + + public DynamicComponentSource(ReflectionManager reflectionManager, Component component, XProperty baseProperty) { + super(reflectionManager.toXClass(Map.class), component); + this.baseProperty = baseProperty; + } + } + } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/DynamicProperty.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/DynamicProperty.java new file mode 100644 index 0000000000..efae0812ba --- /dev/null +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/DynamicProperty.java @@ -0,0 +1,101 @@ +package org.hibernate.envers.configuration.internal.metadata.reader; + +import org.hibernate.annotations.common.reflection.XClass; +import org.hibernate.annotations.common.reflection.XProperty; + +import java.lang.annotation.Annotation; +import java.util.Collection; + +/** + * This class prenteds to be property but in fact it represents entry in the map (for dynamic component) + * @author Lukasz Zuchowski (author at zuchos dot com) + */ +public class DynamicProperty implements XProperty { + + private AuditedPropertiesReader.DynamicComponentSource source; + private String propertyName; + + public DynamicProperty(AuditedPropertiesReader.DynamicComponentSource source, String propertyName) { + this.source = source; + this.propertyName = propertyName; + } + + @Override + public XClass getDeclaringClass() { + return source.getXClass(); + } + + @Override + public String getName() { + return propertyName; + } + + @Override + public boolean isCollection() { + return false; + } + + @Override + public boolean isArray() { + return false; + } + + @Override + public Class getCollectionClass() { + return null; + } + + @Override + public XClass getType() { + return source.getXClass(); + } + + @Override + public XClass getElementClass() { + return null; + } + + @Override + public XClass getClassOrElementClass() { + return null; + } + + @Override + public XClass getMapKey() { + return null; + } + + @Override + public int getModifiers() { + return 0; + } + + @Override + public void setAccessible(boolean accessible) { + } + + @Override + public Object invoke(Object target, Object... parameters) { + return null; + } + + @Override + public boolean isTypeResolved() { + return false; + } + + @Override + public T getAnnotation(Class annotationType) { + return null; + } + + @Override + public boolean isAnnotationPresent(Class annotationType) { + return false; + } + + @Override + public Annotation[] getAnnotations() { + return new Annotation[0]; + } +} diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/ComponentPropertyMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/ComponentPropertyMapper.java index 3cdb1c2d89..6b7f7a7d4a 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/ComponentPropertyMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/ComponentPropertyMapper.java @@ -41,17 +41,24 @@ import org.hibernate.property.Setter; /** * @author Adam Warski (adam at warski dot org) * @author Michal Skowronek (mskowr at o2 dot pl) + * @author Lukasz Zuchowski (author at zuchos dot com) */ public class ComponentPropertyMapper implements PropertyMapper, CompositeMapperBuilder { private final PropertyData propertyData; private final MultiPropertyMapper delegate; private final Class componentClass; - public ComponentPropertyMapper(PropertyData propertyData, Class componentClass) { - this.propertyData = propertyData; - this.delegate = new MultiPropertyMapper(); - this.componentClass = componentClass; - } + public ComponentPropertyMapper(PropertyData propertyData, Class componentClass) { + this.propertyData = propertyData; + //ŁŻ this could be done better + if (Map.class.equals(componentClass)) { + this.delegate = new MultiDynamicComponentMapper(propertyData); + this.componentClass = HashMap.class; + } else { + this.delegate = new MultiPropertyMapper(); + this.componentClass = componentClass; + } + } @Override public void add(PropertyData propertyData) { @@ -139,7 +146,7 @@ public class ComponentPropertyMapper implements PropertyMapper, CompositeMapperB } if ( allNullAndSingle ) { - // single property, but default value need not be null, so we'll set it to null anyway + // single property, but default value need not be null, so we'll set it to null anyway setter.set( obj, null, null ); } else { diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiDynamicComponentMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiDynamicComponentMapper.java new file mode 100644 index 0000000000..cf95487704 --- /dev/null +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiDynamicComponentMapper.java @@ -0,0 +1,102 @@ +package org.hibernate.envers.internal.entities.mapper; + +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.envers.configuration.spi.AuditConfiguration; +import org.hibernate.envers.internal.entities.PropertyData; +import org.hibernate.envers.internal.reader.AuditReaderImplementor; +import org.hibernate.envers.internal.tools.MapProxyTool; +import org.hibernate.envers.internal.tools.StringTools; + +import java.util.Map; + +/** + * Multi mapper for dynamic components (it knows that component is a map, not a class) + * @author Lukasz Zuchowski (author at zuchos dot com) + */ +public class MultiDynamicComponentMapper extends MultiPropertyMapper { + + private PropertyData dynamicComponentData; + + public MultiDynamicComponentMapper(PropertyData dynamicComponentData) { + this.dynamicComponentData = dynamicComponentData; + } + + @Override + public boolean mapToMapFromEntity( + SessionImplementor session, + Map data, + Object newObj, + Object oldObj) { + boolean ret = false; + for (PropertyData propertyData : properties.keySet()) { + if (newObj == null && oldObj == null) { + return false; + } + Object newValue = newObj == null ? null : getValue(newObj, propertyData); + Object oldValue = oldObj == null ? null : getValue(oldObj, propertyData); + + ret |= properties.get(propertyData).mapToMapFromEntity(session, data, newValue, oldValue); + } + + return ret; + } + + private Object getValue(Object newObj, PropertyData propertyData) { + return ((Map) newObj).get(propertyData.getBeanName()); + } + + @Override + public boolean map( + SessionImplementor session, + Map data, + String[] propertyNames, + Object[] newState, + Object[] oldState) { + boolean ret = false; + for (int i = 0; i < propertyNames.length; i++) { + final String propertyName = propertyNames[i]; + Map propertyDatas = getPropertyDatas(); + if (propertyDatas.containsKey(propertyName)) { + final PropertyMapper propertyMapper = properties.get(propertyDatas.get(propertyName)); + final Object newObj = getAtIndexOrNull(newState, i); + final Object oldObj = getAtIndexOrNull(oldState, i); + ret |= propertyMapper.mapToMapFromEntity(session, data, newObj, oldObj); + propertyMapper.mapModifiedFlagsToMapFromEntity(session, data, newObj, oldObj); + } + } + + return ret; + } + + @Override + public void mapModifiedFlagsToMapFromEntity( + SessionImplementor session, + Map data, + Object newObj, + Object oldObj) { + for (PropertyData propertyData : properties.keySet()) { + if (newObj == null && oldObj == null) { + return; + } + Object newValue = newObj == null ? null : getValue(newObj, propertyData); + Object oldValue = oldObj == null ? null : getValue(oldObj, propertyData); + properties.get(propertyData).mapModifiedFlagsToMapFromEntity(session, data, newValue, oldValue); + } + } + + @Override + public void mapToEntityFromMap( + AuditConfiguration verCfg, Object obj, Map data, Object primaryKey, + AuditReaderImplementor versionsReader, Number revision) { + Object mapProxy = MapProxyTool.newInstanceOfBeanProxyForMap(generateClassName(data, dynamicComponentData.getBeanName()), (Map) obj, properties.keySet(), verCfg.getClassLoaderService()); + for (PropertyData propertyData : properties.keySet()) { + PropertyMapper mapper = properties.get(propertyData); + mapper.mapToEntityFromMap(verCfg, mapProxy, data, primaryKey, versionsReader, revision); + } + } + + private String generateClassName(Map data, String dynamicComponentPropertyName) { + return (data.get("$type$") + StringTools.capitalizeFirst(dynamicComponentPropertyName)).replaceAll("_", ""); + } + +} diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiPropertyMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiPropertyMapper.java index fe99f02166..e6519452a3 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiPropertyMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiPropertyMapper.java @@ -41,6 +41,7 @@ import org.hibernate.property.Getter; /** * @author Adam Warski (adam at warski dot org) * @author Michal Skowronek (mskowr at o2 dot pl) + * @author Lukasz Zuchowski (author at zuchos dot com) */ public class MultiPropertyMapper implements ExtendedPropertyMapper { protected final Map properties; @@ -66,7 +67,7 @@ public class MultiPropertyMapper implements ExtendedPropertyMapper { return (CompositeMapperBuilder) properties.get( propertyData ); } - final ComponentPropertyMapper componentMapperBuilder = new ComponentPropertyMapper( propertyData, componentClass ); + final ComponentPropertyMapper componentMapperBuilder = new ComponentPropertyMapper(propertyData, componentClass); addComposite( propertyData, componentMapperBuilder ); return componentMapperBuilder; @@ -78,7 +79,7 @@ public class MultiPropertyMapper implements ExtendedPropertyMapper { propertyDatas.put( propertyData.getName(), propertyData ); } - private Object getAtIndexOrNull(Object[] array, int index) { + protected Object getAtIndexOrNull(Object[] array, int index) { return array == null ? null : array[index]; } @@ -223,4 +224,8 @@ public class MultiPropertyMapper implements ExtendedPropertyMapper { public Map getProperties() { return properties; } + + public Map getPropertyDatas() { + return propertyDatas; + } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/MapProxyTool.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/MapProxyTool.java new file mode 100644 index 0000000000..dd5250b7ce --- /dev/null +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/MapProxyTool.java @@ -0,0 +1,140 @@ +package org.hibernate.envers.internal.tools; + +import javassist.*; +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; +import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; +import org.hibernate.envers.internal.entities.PropertyData; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import static org.hibernate.envers.internal.tools.StringTools.capitalizeFirst; +import static org.hibernate.envers.internal.tools.StringTools.getLastComponent; + +/** + * @author Lukasz Zuchowski (author at zuchos dot com) + */ +public class MapProxyTool { + + /** + * @author Lukasz Zuchowski (author at zuchos dot com) + * Creates instance of map proxy class. This proxy class will be a java bean with properties from propertyDatas. + * Instance will proxy calls to instance of the map passed as parameter. + * @param name Name of the class to construct (should be unique within class loader) + * @param map instance that will be proxied by java bean + * @param propertyDatas properties that should java bean declare + * @param classLoaderService + * @return new instance of proxy + */ + public static Object newInstanceOfBeanProxyForMap(String name, Map map, Set propertyDatas, ClassLoaderService classLoaderService) { + Class aClass = loadClass(name, classLoaderService); + if (aClass == null) { + Map> properties = prepareProperties(propertyDatas); + aClass = generate(name, properties); + } + return createNewInstance(map, aClass); + } + + private static Object createNewInstance(Map map, Class aClass) { + try { + return aClass.getConstructor(Map.class).newInstance(map); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static Map> prepareProperties(Set propertyDatas) { + Map> properties = new HashMap>(); + for (PropertyData propertyData : propertyDatas) { + properties.put(propertyData.getBeanName(), Object.class); + } + return properties; + } + + private static Class loadClass(String className, ClassLoaderService classLoaderService) { + try { + return ReflectionTools.loadClass(className, classLoaderService); + } catch (ClassLoadingException e) { + return null; + } + + } + + /** + * Protected for test only + */ + protected static Class generate(String className, Map> properties) { + try { + ClassPool pool = ClassPool.getDefault(); + CtClass cc = pool.makeClass(className); + + cc.addInterface(resolveCtClass(Serializable.class)); + cc.addField(new CtField(resolveCtClass(Map.class), "theMap", cc)); + cc.addConstructor(generateConstructor(className, cc)); + + for (Entry> entry : properties.entrySet()) { + cc.addField(new CtField(resolveCtClass(entry.getValue()), entry.getKey(), cc)); + + // add getter + cc.addMethod(generateGetter(cc, entry.getKey(), entry.getValue())); + + // add setter + cc.addMethod(generateSetter(cc, entry.getKey(), entry.getValue())); + } + return cc.toClass(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static CtConstructor generateConstructor(String className, CtClass cc) throws NotFoundException, CannotCompileException { + StringBuffer sb = new StringBuffer(); + sb.append("public ").append(getLastComponent(className)).append("(").append(Map.class.getName()).append(" map)").append("{") + .append("this.theMap = map;").append("}"); + System.out.println(sb); + return CtNewConstructor.make(sb.toString(), cc); + } + + private static CtMethod generateGetter(CtClass declaringClass, String fieldName, Class fieldClass) + throws CannotCompileException { + + String getterName = "get" + capitalizeFirst(fieldName); + + StringBuffer sb = new StringBuffer(); + sb.append("public ").append(fieldClass.getName()).append(" ") + .append(getterName).append("(){").append("return (" + fieldClass.getName() + ")this.theMap.get(\"") + .append(fieldName).append("\")").append(";").append("}"); + return CtMethod.make(sb.toString(), declaringClass); + } + + private static CtMethod generateSetter(CtClass declaringClass, String fieldName, Class fieldClass) + throws CannotCompileException { + + String setterName = "set" + capitalizeFirst(fieldName); + + StringBuffer sb = new StringBuffer(); + sb.append("public void ").append(setterName).append("(") + .append(fieldClass.getName()).append(" ").append(fieldName) + .append(")").append("{").append("this.theMap.put(\"").append(fieldName) + .append("\",").append(fieldName).append(")").append(";").append("}"); + return CtMethod.make(sb.toString(), declaringClass); + } + + private static CtClass resolveCtClass(Class clazz) throws NotFoundException { + return resolveCtClass(clazz.getName()); + } + + + private static CtClass resolveCtClass(String clazz) throws NotFoundException { + try { + ClassPool pool = ClassPool.getDefault(); + return pool.get(clazz); + } catch (NotFoundException e) { + return null; + } + } + +} diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/StringTools.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/StringTools.java index a583f3ff54..99b09ed020 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/StringTools.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/StringTools.java @@ -27,51 +27,59 @@ import java.util.Iterator; /** * @author Adam Warski (adam at warski dot org) + * @author Lukasz Zuchowski (author at zuchos dot com) */ public abstract class StringTools { - public static boolean isEmpty(String s) { - return s == null || "".equals( s ); - } + public static boolean isEmpty(String s) { + return s == null || "".equals(s); + } - public static boolean isEmpty(Object o) { - return o == null || "".equals( o ); - } + public static boolean isEmpty(Object o) { + return o == null || "".equals(o); + } - /** - * @param s String, from which to get the last component. - * - * @return The last component of the dot-separated string s. For example, for a string - * "a.b.c", the result is "c". - */ - public static String getLastComponent(String s) { - if ( s == null ) { - return null; - } - final int lastDot = s.lastIndexOf( "." ); - if ( lastDot == -1 ) { - return s; - } - else { - return s.substring( lastDot + 1 ); - } - } + /** + * @param s String, from which to get the last component. + * @return The last component of the dot-separated string s. For example, for a string + * "a.b.c", the result is "c". + */ + public static String getLastComponent(String s) { + if (s == null) { + return null; + } + final int lastDot = s.lastIndexOf("."); + if (lastDot == -1) { + return s; + } else { + return s.substring(lastDot + 1); + } + } - /** - * To the given string builder, appends all strings in the given iterator, separating them with the given - * separator. For example, for an interator "a" "b" "c" and separator ":" the output is "a:b:c". - * - * @param sb String builder, to which to append. - * @param contents Strings to be appended. - * @param separator Separator between subsequent content. - */ - public static void append(StringBuilder sb, Iterator contents, String separator) { - boolean isFirst = true; - while ( contents.hasNext() ) { - if ( !isFirst ) { - sb.append( separator ); - } - sb.append( contents.next() ); - isFirst = false; - } - } + /** + * To the given string builder, appends all strings in the given iterator, separating them with the given + * separator. For example, for an interator "a" "b" "c" and separator ":" the output is "a:b:c". + * + * @param sb String builder, to which to append. + * @param contents Strings to be appended. + * @param separator Separator between subsequent content. + */ + public static void append(StringBuilder sb, Iterator contents, String separator) { + boolean isFirst = true; + while (contents.hasNext()) { + if (!isFirst) { + sb.append(separator); + } + sb.append(contents.next()); + isFirst = false; + } + } + + /** + * Capitalizes first letter of the string + * @param fieldName + * @return capitalized string + */ + public static String capitalizeFirst(String fieldName) { + return fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); + } } diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/internal/tools/MapProxyTest.java b/hibernate-envers/src/test/java/org/hibernate/envers/internal/tools/MapProxyTest.java new file mode 100644 index 0000000000..2dc3552022 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/internal/tools/MapProxyTest.java @@ -0,0 +1,23 @@ +package org.hibernate.envers.internal.tools; + +import org.hibernate.property.Getter; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +public class MapProxyTest { + + @Test + public void testGenerate() throws Exception { + Map map = new HashMap(); + map.put("age",14); + Map> properties = new HashMap>(); + properties.put("age",Integer.class); + Class testClass = MapProxyTool.generate("TestClass", properties); + Object testClassInstance = testClass.getConstructor(Map.class).newInstance(map); + Getter getter = ReflectionTools.getGetter(testClass, "age", "property"); + Object o = getter.get(testClassInstance); + System.out.println(o); + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/components/dynamic/AuditedDynamicComponentEntity.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/components/dynamic/AuditedDynamicComponentEntity.java new file mode 100644 index 0000000000..726b214c8e --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/components/dynamic/AuditedDynamicComponentEntity.java @@ -0,0 +1,93 @@ +package org.hibernate.envers.test.integration.components.dynamic; + +import org.hibernate.envers.Audited; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +@Audited +public class AuditedDynamicComponentEntity implements Serializable { + private long id; + private String note; + private Map customFields = new HashMap(); + private SimpleEntity simpleEntity; + + public AuditedDynamicComponentEntity() { + } + + public AuditedDynamicComponentEntity(long id, String note) { + this.id = id; + this.note = note; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( !(o instanceof AuditedDynamicComponentEntity) ) { + return false; + } + + AuditedDynamicComponentEntity that = (AuditedDynamicComponentEntity) o; + + if ( id != that.id ) { + return false; + } + if ( customFields != null ? !customFields.equals( that.customFields ) : that.customFields != null ) { + return false; + } + if ( note != null ? !note.equals( that.note ) : that.note != null ) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = (int) (id ^ (id >>> 32)); + result = 31 * result + (note != null ? note.hashCode() : 0); + result = 31 * result + (customFields != null ? customFields.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "AuditedDynamicMapComponent(id = " + id + ", note = " + note + ", customFields = " + customFields + ")"; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getNote() { + return note; + } + + public void setNote(String note) { + this.note = note; + } + + public Map getCustomFields() { + return customFields; + } + + public void setCustomFields(Map customFields) { + this.customFields = customFields; + } + + + public SimpleEntity getSimpleEntity() { + return simpleEntity; + } + + public void setSimpleEntity(SimpleEntity simpleEntity) { + this.simpleEntity = simpleEntity; + } +} \ No newline at end of file diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/components/dynamic/AuditedDynamicComponentTest.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/components/dynamic/AuditedDynamicComponentTest.java index 418be712ff..33b9b93a89 100644 --- a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/components/dynamic/AuditedDynamicComponentTest.java +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/components/dynamic/AuditedDynamicComponentTest.java @@ -1,65 +1,203 @@ package org.hibernate.envers.test.integration.components.dynamic; -import java.io.File; -import java.io.Serializable; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; - +import junit.framework.Assert; import org.hibernate.MappingException; +import org.hibernate.Session; import org.hibernate.cfg.Configuration; -import org.hibernate.envers.Audited; import org.hibernate.envers.configuration.EnversSettings; import org.hibernate.envers.internal.tools.StringTools; -import org.hibernate.envers.test.AbstractEnversTest; +import org.hibernate.envers.query.AuditEntity; +import org.hibernate.envers.test.BaseEnversFunctionalTestCase; +import org.hibernate.envers.test.Priority; import org.hibernate.service.ServiceRegistry; - -import org.junit.Test; -import junit.framework.Assert; - import org.hibernate.testing.ServiceRegistryBuilder; import org.hibernate.testing.TestForIssue; +import org.junit.Test; + +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Arrays; +import java.util.List; /** * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) + * @author Lukasz Zuchowski (author at zuchos dot com) */ @TestForIssue(jiraKey = "HHH-8049") -public class AuditedDynamicComponentTest extends AbstractEnversTest { - @Test - public void testAuditedDynamicComponentFailure() throws URISyntaxException { - final Configuration config = new Configuration(); - final URL hbm = Thread.currentThread().getContextClassLoader().getResource( - "mappings/dynamicComponents/mapAudited.hbm.xml" - ); - config.addFile( new File( hbm.toURI() ) ); +public class AuditedDynamicComponentTest extends BaseEnversFunctionalTestCase { - final String auditStrategy = getAuditStrategy(); - if ( !StringTools.isEmpty( auditStrategy ) ) { - config.setProperty( EnversSettings.AUDIT_STRATEGY, auditStrategy ); - } + @Override + protected String[] getMappings() { + return new String[]{"mappings/dynamicComponents/mapAudited.hbm.xml"}; + } - final ServiceRegistry serviceRegistry = ServiceRegistryBuilder.buildServiceRegistry( config.getProperties() ); - try { - config.buildSessionFactory( serviceRegistry ); - Assert.fail( "MappingException expected" ); - } - catch (MappingException e) { - Assert.assertEquals( - "Audited dynamic-component properties are not supported. Consider applying @NotAudited annotation to " - + AuditedDynamicMapComponent.class.getName() + "#customFields.", - e.getMessage() - ); - } - finally { - ServiceRegistryBuilder.destroy( serviceRegistry ); - } - } + //@Test + public void testAuditedDynamicComponentFailure() throws URISyntaxException { + final Configuration config = new Configuration(); + final URL hbm = Thread.currentThread().getContextClassLoader().getResource( + "mappings/dynamicComponents/mapAudited.hbm.xml" + ); + config.addFile(new File(hbm.toURI())); + + final String auditStrategy = getAuditStrategy(); + if (!StringTools.isEmpty(auditStrategy)) { + config.setProperty(EnversSettings.AUDIT_STRATEGY, auditStrategy); + } + + final ServiceRegistry serviceRegistry = ServiceRegistryBuilder.buildServiceRegistry(config.getProperties()); + try { + config.buildSessionFactory(serviceRegistry); + Assert.fail("MappingException expected"); + } catch (MappingException e) { + Assert.assertEquals( + "Audited dynamic-component properties are not supported. Consider applying @NotAudited annotation to " + + AuditedDynamicComponentEntity.class.getName() + "#customFields.", + e.getMessage() + ); + } finally { + ServiceRegistryBuilder.destroy(serviceRegistry); + } + } + + @Test + @Priority(10) + public void initData() { + Session session = openSession(); + + SimpleEntity simpleEntity = new SimpleEntity(1L, "Very simple entity"); + + // Revision 1 + session.getTransaction().begin(); + session.save(simpleEntity); + session.getTransaction().commit(); + + // Revision 2 + session.getTransaction().begin(); + AuditedDynamicComponentEntity entity = new AuditedDynamicComponentEntity(1L, "static field value"); + entity.getCustomFields().put("prop1", 13); + entity.getCustomFields().put("prop2", 0.1f); + entity.getCustomFields().put("prop3", simpleEntity); + session.save(entity); + session.getTransaction().commit(); + + // revision 3 + session.getTransaction().begin(); + SimpleEntity simpleEntity2 = new SimpleEntity(2L, "Not so simple entity"); + session.save(simpleEntity2); + entity = (AuditedDynamicComponentEntity) session.get(AuditedDynamicComponentEntity.class, entity.getId()); + entity.getCustomFields().put("prop3", simpleEntity2); + session.update(entity); + session.getTransaction().commit(); + + // Revision 4 + session.getTransaction().begin(); + entity = (AuditedDynamicComponentEntity) session.get(AuditedDynamicComponentEntity.class, entity.getId()); + entity.getCustomFields().put("prop1", 2); + session.update(entity); + session.getTransaction().commit(); + + // Revision 5 + session.getTransaction().begin(); + entity = (AuditedDynamicComponentEntity) session.load(AuditedDynamicComponentEntity.class, entity.getId()); + entity.getCustomFields().remove("prop2"); + session.update(entity); + session.getTransaction().commit(); + + // Revision 6 + session.getTransaction().begin(); + entity = (AuditedDynamicComponentEntity) session.load(AuditedDynamicComponentEntity.class, entity.getId()); + entity.getCustomFields().clear(); + session.update(entity); + session.getTransaction().commit(); + + // Revision 7 + session.getTransaction().begin(); + entity = (AuditedDynamicComponentEntity) session.load(AuditedDynamicComponentEntity.class, entity.getId()); + session.delete(entity); + session.getTransaction().commit(); + + session.close(); + } + + @Test + public void testRevisionsCounts() { + Assert.assertEquals( + Arrays.asList(2, 3, 4, 5,6,7), + getAuditReader().getRevisions(AuditedDynamicComponentEntity.class, 1L) + ); + } + + @Test + public void testHistoryOfId1() { + // Revision 2 + AuditedDynamicComponentEntity entity = new AuditedDynamicComponentEntity(1L, "static field value"); + entity.getCustomFields().put("prop1", 13); + entity.getCustomFields().put("prop2", 0.1f); + entity.getCustomFields().put("prop3", new SimpleEntity(1L, "Very simple entity")); + AuditedDynamicComponentEntity ver2 = getAuditReader().find( + AuditedDynamicComponentEntity.class, + entity.getId(), + 2 + ); + Assert.assertEquals(entity, ver2); + + // Revision 3 + SimpleEntity simpleEntity2 = new SimpleEntity(2L, "Not so simple entity"); + entity.getCustomFields().put("prop3", simpleEntity2); + AuditedDynamicComponentEntity ver3 = getAuditReader().find( + AuditedDynamicComponentEntity.class, + entity.getId(), + 3 + ); + Assert.assertEquals(entity, ver3); + + // Revision 4 + entity.getCustomFields().put("prop1", 2); + AuditedDynamicComponentEntity ver4 = getAuditReader().find( + AuditedDynamicComponentEntity.class, + entity.getId(), + 4 + ); + Assert.assertEquals(entity, ver4); + + // Revision 5 + entity.getCustomFields().put("prop2",null); + AuditedDynamicComponentEntity ver5 = getAuditReader().find( + AuditedDynamicComponentEntity.class, + entity.getId(), + 5 + ); + Assert.assertEquals(entity, ver5); + + // Revision 5 + entity.getCustomFields().put("prop1",null); + entity.getCustomFields().put("prop2",null); + entity.getCustomFields().put("prop3",null); + AuditedDynamicComponentEntity ver6 = getAuditReader().find( + AuditedDynamicComponentEntity.class, + entity.getId(), + 6 + ); + Assert.assertEquals(entity, ver6); + } + + + //@Test + public void testOfQueryOnDynamicComponent() { + //given (and result of initData() + AuditedDynamicComponentEntity entity = new AuditedDynamicComponentEntity(1L, "static field value"); + entity.getCustomFields().put("prop1", 13); + entity.getCustomFields().put("prop2", 0.1f); + + //when + List ver1 = getAuditReader().createQuery() + .forEntitiesAtRevision(AuditedDynamicComponentEntity.class, 2) + .add(AuditEntity.property("customFields_prop1").le(20)) + .getResultList(); + + //then + Assert.assertEquals(entity, ver1.get(0)); + } - @Audited - public static class AuditedDynamicMapComponent implements Serializable { - public long id; - public String note; - public Map customFields = new HashMap(); // Invalid audited dynamic-component. - } } diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/components/dynamic/SimpleEntity.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/components/dynamic/SimpleEntity.java new file mode 100644 index 0000000000..4b598921e2 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/components/dynamic/SimpleEntity.java @@ -0,0 +1,63 @@ +package org.hibernate.envers.test.integration.components.dynamic; + +import org.hibernate.envers.Audited; + +@Audited +public class SimpleEntity { + + private Long id; + private String simpleProperty; + + public SimpleEntity() { + } + + public SimpleEntity(Long id, String simpleProperty) { + this.id = id; + this.simpleProperty = simpleProperty; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getSimpleProperty() { + return simpleProperty; + } + + public void setSimpleProperty(String simpleProperty) { + this.simpleProperty = simpleProperty; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SimpleEntity)) return false; + + SimpleEntity that = (SimpleEntity) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (simpleProperty != null ? !simpleProperty.equals(that.simpleProperty) : that.simpleProperty != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (simpleProperty != null ? simpleProperty.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "SimpleEntity{" + + "id=" + id + + ", simpleProperty='" + simpleProperty + '\'' + + '}'; + } +} diff --git a/hibernate-envers/src/test/resources/mappings/dynamicComponents/mapAudited.hbm.xml b/hibernate-envers/src/test/resources/mappings/dynamicComponents/mapAudited.hbm.xml index 240d35395f..67d75a8d6b 100644 --- a/hibernate-envers/src/test/resources/mappings/dynamicComponents/mapAudited.hbm.xml +++ b/hibernate-envers/src/test/resources/mappings/dynamicComponents/mapAudited.hbm.xml @@ -3,12 +3,19 @@ "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> - - - - + + + + + + + + + + +