diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/GeneratorType.java b/hibernate-core/src/main/java/org/hibernate/annotations/GeneratorType.java new file mode 100644 index 0000000000..fbd2a16217 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/GeneratorType.java @@ -0,0 +1,59 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2013, Red Hat Inc. or third-party contributors as + * indicated by the @author tags or express copyright attribution + * statements applied by the authors. All third-party contributions are + * distributed under license by Red Hat Inc. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ +package org.hibernate.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.hibernate.tuple.ValueGenerator; +import org.hibernate.tuple.VmValueGeneration; + +/** + * Marks a property as generated, specifying the {@link ValueGenerator} type to be used for generating the value. It is + * the responsibility of the client to ensure that a generator type is specified which matches the data type of the + * annotated property. + * + * @author Gunnar Morling + */ +@ValueGenerationType( generatedBy = VmValueGeneration.class ) +@Retention( RetentionPolicy.RUNTIME ) +@Target( value = { ElementType.FIELD, ElementType.METHOD } ) +public @interface GeneratorType { + + /** + * The value generator type + * + * @return the value generator type + */ + Class> type(); + + /** + * When to generate the value, either only during insert or during insert and update of the hosting entity. + * + * @return the time of generation + */ + GenerationTime when() default GenerationTime.ALWAYS; +} diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/ValueGenerationType.java b/hibernate-core/src/main/java/org/hibernate/annotations/ValueGenerationType.java new file mode 100644 index 0000000000..f2139419f8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/ValueGenerationType.java @@ -0,0 +1,60 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2013, Red Hat Inc. or third-party contributors as + * indicated by the @author tags or express copyright attribution + * statements applied by the authors. All third-party contributions are + * distributed under license by Red Hat Inc. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ +package org.hibernate.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.hibernate.tuple.AnnotationValueGeneration; + +/** + * Marks an annotation type as a generator annotation type. + *

+ * Adding a generator annotation to an entity property causes the value of the property to be generated upon insert + * or update of the owning entity. Not more than one generator annotation may be placed on a given property. + *

+ * Each generator annotation type is associated with a {@link AnnotationValueGeneration} which implements the strategy + * for generating the value. Generator annotation types may define arbitrary custom attributes, e.g. allowing the + * client to configure the generation timing (if applicable) or other settings taking an effect on the value generation. + * The corresponding implementation can retrieve these settings from the annotation instance passed to + * {@link AnnotationValueGeneration#initialize(java.lang.annotation.Annotation, Class)}. + *

+ * Custom generator annotation types must have retention policy {@link RetentionPolicy#RUNTIME}. + + * @author Gunnar Morling + */ +@Target( value = ElementType.ANNOTATION_TYPE ) +@Retention( RetentionPolicy.RUNTIME ) +public @interface ValueGenerationType { + + /** + * The type of value generation associated with the annotated value generator annotation type. The referenced + * generation type must be parameterized with the type of the given generator annotation. + * + * @return the value generation type + */ + Class> generatedBy(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/PropertyBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/PropertyBinder.java index 8ab07549c6..f1734d340c 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/PropertyBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/PropertyBinder.java @@ -23,6 +23,7 @@ */ package org.hibernate.cfg.annotations; +import java.lang.annotation.Annotation; import java.util.Map; import javax.persistence.EmbeddedId; @@ -30,11 +31,13 @@ import javax.persistence.Id; import javax.persistence.Lob; import org.hibernate.AnnotationException; +import org.hibernate.HibernateException; import org.hibernate.annotations.Generated; import org.hibernate.annotations.GenerationTime; import org.hibernate.annotations.Immutable; import org.hibernate.annotations.NaturalId; import org.hibernate.annotations.OptimisticLock; +import org.hibernate.annotations.ValueGenerationType; import org.hibernate.annotations.common.AssertionFailure; import org.hibernate.annotations.common.reflection.XClass; import org.hibernate.annotations.common.reflection.XProperty; @@ -56,10 +59,10 @@ import org.hibernate.mapping.RootClass; import org.hibernate.mapping.SimpleValue; import org.hibernate.mapping.ToOne; import org.hibernate.mapping.Value; +import org.hibernate.tuple.AnnotationValueGeneration; import org.hibernate.tuple.GenerationTiming; import org.hibernate.tuple.ValueGeneration; import org.hibernate.tuple.ValueGenerator; - import org.jboss.logging.Logger; /** @@ -278,7 +281,7 @@ public class PropertyBinder { if ( property != null ) { prop.setValueGenerationStrategy( determineValueGenerationStrategy( property ) ); } - + NaturalId naturalId = property != null ? property.getAnnotation( NaturalId.class ) : null; if ( naturalId != null ) { if ( ! entityBinder.isRootEntity() ) { @@ -289,11 +292,11 @@ public class PropertyBinder { } prop.setNaturalIdentifier( true ); } - + // HHH-4635 -- needed for dialect-specific property ordering Lob lob = property != null ? property.getAnnotation( Lob.class ) : null; prop.setLob( lob != null ); - + prop.setInsertable( insertable ); prop.setUpdateable( updatable ); @@ -330,15 +333,24 @@ public class PropertyBinder { } private ValueGeneration determineValueGenerationStrategy(XProperty property) { - // for now, we just handle the legacy '@Generated' annotation - Generated generatedAnnotation = property.getAnnotation( Generated.class ); - if ( generatedAnnotation == null - || generatedAnnotation.value() == null - || generatedAnnotation.value() == GenerationTime.NEVER ) { + ValueGeneration annotationValueGeneration = getValueGenerationFromAnnotations( property ); + ValueGeneration legacyValueGeneration = getLegacyValueGeneration( property ); + + if ( annotationValueGeneration == null && legacyValueGeneration == null ) { return NoValueGeneration.INSTANCE; } + else if ( annotationValueGeneration != null && legacyValueGeneration != null ) { + throw new AnnotationException( + "@Generated and a generator annotation must not be specified at the same time:" + StringHelper.qualify( + holder.getPath(), + name + ) + ); + } - final GenerationTiming when = generatedAnnotation.value().getEquivalent(); + final GenerationTiming when = annotationValueGeneration != null ? + annotationValueGeneration.getGenerationTiming() : + legacyValueGeneration.getGenerationTiming(); if ( property.isAnnotationPresent( javax.persistence.Version.class ) && when == GenerationTiming.INSERT ) { throw new AnnotationException( @@ -347,12 +359,103 @@ public class PropertyBinder { ); } - insertable = false; - if ( when == GenerationTiming.ALWAYS ) { - updatable = false; + if ( legacyValueGeneration != null ) { + insertable = false; + if ( when == GenerationTiming.ALWAYS ) { + updatable = false; + } } - return new LegacyValueGeneration( when ); + return annotationValueGeneration != null ? annotationValueGeneration : legacyValueGeneration; + } + + private ValueGeneration getLegacyValueGeneration(XProperty property) { + Generated generatedAnnotation = property.getAnnotation( Generated.class ); + + if ( generatedAnnotation != null && generatedAnnotation.value() != null && generatedAnnotation.value() != GenerationTime.NEVER ) { + return new LegacyValueGeneration( generatedAnnotation.value().getEquivalent() ); + } + + return null; + } + + /** + * Returns the value generation strategy for the given property, if any. + */ + private ValueGeneration getValueGenerationFromAnnotations(XProperty property) { + AnnotationValueGeneration valueGeneration = null; + + for ( Annotation annotation : property.getAnnotations() ) { + AnnotationValueGeneration candidate = getValueGenerationFromAnnotation( property, annotation ); + + if ( candidate != null ) { + if ( valueGeneration != null ) { + throw new AnnotationException( + "Only one generator annotation is allowed:" + StringHelper.qualify( + holder.getPath(), + name + ) + ); + } + else { + valueGeneration = candidate; + } + } + } + + return valueGeneration; + } + + /** + * In case the given annotation is a value generator annotation, the corresponding value generation strategy to be + * applied to the given property is returned, {@code null} otherwise. + */ + private AnnotationValueGeneration getValueGenerationFromAnnotation( + XProperty property, + A annotation) { + ValueGenerationType generatorAnnotation = annotation.annotationType() + .getAnnotation( ValueGenerationType.class ); + + if ( generatorAnnotation == null ) { + return null; + } + + Class> generationType = generatorAnnotation.generatedBy(); + return instantiateAndInitializeValueGeneration( + annotation, generationType, property + ); + } + + /** + * Instantiates the given generator annotation type, initializing it with the given instance of the corresponding + * generator annotation and the property's type. + */ + private AnnotationValueGeneration instantiateAndInitializeValueGeneration( + A annotation, + Class> generationType, + XProperty property) { + + try { + // This will cause a CCE in case the generation type doesn't match the annotation type; As this would be a + // programming error of the generation type developer and thus should show up during testing, we don't + // check this explicitly; If required, this could be done e.g. using ClassMate + @SuppressWarnings( "unchecked" ) + AnnotationValueGeneration valueGeneration = (AnnotationValueGeneration) generationType.newInstance(); + valueGeneration.initialize( annotation, mappings.getReflectionManager().toClass(property.getType() ) ); + + return valueGeneration; + } + catch (HibernateException e) { + throw e; + } + catch (Exception e) { + throw new AnnotationException( + "Exception occurred during processing of generator annotation:" + StringHelper.qualify( + holder.getPath(), + name + ), e + ); + } } private static class NoValueGeneration implements ValueGeneration { diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/ReflectHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/ReflectHelper.java index 9a7f6811cc..a1b289b540 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/ReflectHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/ReflectHelper.java @@ -296,13 +296,13 @@ public final class ReflectHelper { * @return The default constructor. * @throws PropertyNotFoundException Indicates there was not publicly accessible, no-arg constructor (todo : why PropertyNotFoundException???) */ - public static Constructor getDefaultConstructor(Class clazz) throws PropertyNotFoundException { + public static Constructor getDefaultConstructor(Class clazz) throws PropertyNotFoundException { if ( isAbstractClass( clazz ) ) { return null; } try { - Constructor constructor = clazz.getDeclaredConstructor( NO_PARAM_SIGNATURE ); + Constructor constructor = clazz.getDeclaredConstructor( NO_PARAM_SIGNATURE ); if ( !isPublic( clazz, constructor ) ) { constructor.setAccessible( true ); } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index ef13465478..b2361890cf 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -81,6 +81,7 @@ import org.hibernate.engine.spi.PersistenceContext.NaturalIdHelper; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.ValueInclusion; +import org.hibernate.event.spi.EventSource; import org.hibernate.id.IdentifierGenerator; import org.hibernate.id.PostInsertIdentifierGenerator; import org.hibernate.id.PostInsertIdentityPersister; @@ -4853,7 +4854,7 @@ public abstract class AbstractEntityPersister for ( NonIdentifierAttribute attribute : entityMetamodel.getProperties() ) { propertyIndex++; final ValueGeneration valueGeneration = attribute.getValueGenerationStrategy(); - if ( valueGeneration != null && valueGeneration.getGenerationTiming() == matchTiming ) { + if ( isReadRequired( valueGeneration, matchTiming ) ) { final Object hydratedState = attribute.getType().hydrate( rs, getPropertyAliases( "", @@ -4892,6 +4893,22 @@ public abstract class AbstractEntityPersister } + /** + * Whether the given value generation strategy requires to read the value from the database or not. + */ + private boolean isReadRequired(ValueGeneration valueGeneration, GenerationTiming matchTiming) { + return valueGeneration != null && + valueGeneration.getValueGenerator() == null && + timingsMatch( valueGeneration, matchTiming ); + } + + private boolean timingsMatch(ValueGeneration valueGeneration, GenerationTiming matchTiming) { + return + (matchTiming == GenerationTiming.INSERT && valueGeneration.getGenerationTiming().includesInsert()) || + (matchTiming == GenerationTiming.ALWAYS && valueGeneration.getGenerationTiming() + .includesUpdate()); + } + public String getIdentifierPropertyName() { return entityMetamodel.getIdentifierProperty().getName(); } diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/AnnotationValueGeneration.java b/hibernate-core/src/main/java/org/hibernate/tuple/AnnotationValueGeneration.java new file mode 100644 index 0000000000..aac9fd4f9f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/tuple/AnnotationValueGeneration.java @@ -0,0 +1,51 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2013, Red Hat Inc. or third-party contributors as + * indicated by the @author tags or express copyright attribution + * statements applied by the authors. All third-party contributions are + * distributed under license by Red Hat Inc. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ +package org.hibernate.tuple; + +import java.lang.annotation.Annotation; + +import org.hibernate.HibernateException; + +/** + * A {@link ValueGeneration} based on a custom Java generator annotation type. + * + * @param The generator annotation type supported by an implementation + * + * @author Gunnar Morling + */ +public interface AnnotationValueGeneration extends ValueGeneration { + + /** + * Initializes this generation strategy for the given annotation instance. + * + * @param annotation an instance of the strategy's annotation type. Typically implementations will retrieve the + * annotation's attribute values and store them in fields. + * @param propertyType the type of the property annotated with the generator annotation. Implementations may use + * the type to determine the right {@link ValueGenerator} to be applied. + * + * @throws HibernateException in case an error occurred during initialization, e.g. if an implementation can't + * create a value for the given property type. + */ + void initialize(A annotation, Class propertyType); +} diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/ValueGeneration.java b/hibernate-core/src/main/java/org/hibernate/tuple/ValueGeneration.java index b95d6f9d19..b73dc935ee 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/ValueGeneration.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/ValueGeneration.java @@ -44,7 +44,7 @@ public interface ValueGeneration { * * @return The strategy for performing in-VM value generation */ - public ValueGenerator getValueGenerator(); + public ValueGenerator getValueGenerator(); /** * For values which are generated in the database ({@link #getValueGenerator()} == {@code null}), should the diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/VmValueGeneration.java b/hibernate-core/src/main/java/org/hibernate/tuple/VmValueGeneration.java new file mode 100644 index 0000000000..dd4aecf1b5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/tuple/VmValueGeneration.java @@ -0,0 +1,74 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2013, Red Hat Inc. or third-party contributors as + * indicated by the @author tags or express copyright attribution + * statements applied by the authors. All third-party contributions are + * distributed under license by Red Hat Inc. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ +package org.hibernate.tuple; + +import java.lang.reflect.Constructor; + +import org.hibernate.HibernateException; +import org.hibernate.annotations.GeneratorType; +import org.hibernate.internal.util.ReflectHelper; + +/** + * A {@link AnnotationValueGeneration} which allows to specify the {@link ValueGenerator} to be used to determine the + * value of the annotated property. + * + * @author Gunnar Morling + */ +public class VmValueGeneration implements AnnotationValueGeneration { + + private GenerationTiming generationTiming; + private Constructor> constructor; + + @Override + public void initialize(GeneratorType annotation, Class propertyType) { + Class> generatorType = annotation.type(); + constructor = ReflectHelper.getDefaultConstructor( generatorType ); + this.generationTiming = annotation.when().getEquivalent(); + } + + @Override + public GenerationTiming getGenerationTiming() { + return generationTiming; + } + + @Override + public ValueGenerator getValueGenerator() { + try { + return constructor.newInstance(); + } + catch (Exception e) { + throw new HibernateException( "Couldn't instantiate value generator", e ); + } + } + + @Override + public boolean referenceColumnInSql() { + return false; + } + + @Override + public String getDatabaseGeneratedReferencedColumnValue() { + return null; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/generated/DefaultGeneratedValueTest.java b/hibernate-core/src/test/java/org/hibernate/test/generated/DefaultGeneratedValueTest.java index 03c1ca27b9..89881a7da7 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/generated/DefaultGeneratedValueTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/generated/DefaultGeneratedValueTest.java @@ -27,28 +27,33 @@ import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; - import java.util.Date; import org.hibernate.Session; import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.Generated; import org.hibernate.annotations.GenerationTime; +import org.hibernate.annotations.GeneratorType; +import org.hibernate.tuple.ValueGenerator; import org.junit.Test; import org.hibernate.testing.TestForIssue; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; /** + * Test for the generation of column values using different + * {@link org.hibernate.tuple.ValueGeneration} implementations. + * * @author Steve Ebersole + * @author Gunnar Morling */ public class DefaultGeneratedValueTest extends BaseCoreFunctionalTestCase { + @Test @TestForIssue( jiraKey = "HHH-2907" ) public void testGeneration() { @@ -56,17 +61,21 @@ public class DefaultGeneratedValueTest extends BaseCoreFunctionalTestCase { s.beginTransaction(); TheEntity theEntity = new TheEntity( 1 ); assertNull( theEntity.createdDate ); + assertNull( theEntity.name ); s.save( theEntity ); assertNull( theEntity.createdDate ); + assertNull( theEntity.name ); s.getTransaction().commit(); s.close(); assertNotNull( theEntity.createdDate ); + assertEquals( "Bob", theEntity.name ); s = openSession(); s.beginTransaction(); theEntity = (TheEntity) session.get( TheEntity.class, 1 ); assertNotNull( theEntity.createdDate ); + assertEquals( "Bob", theEntity.name ); s.delete( theEntity ); s.getTransaction().commit(); s.close(); @@ -82,11 +91,15 @@ public class DefaultGeneratedValueTest extends BaseCoreFunctionalTestCase { private static class TheEntity { @Id private Integer id; + @Generated( GenerationTime.INSERT ) @ColumnDefault( "CURRENT_TIMESTAMP" ) @Column( nullable = false ) private Date createdDate; + @GeneratorType( type = MyVmValueGenerator.class, when = GenerationTime.INSERT ) + private String name; + private TheEntity() { } @@ -95,4 +108,11 @@ public class DefaultGeneratedValueTest extends BaseCoreFunctionalTestCase { } } + public static class MyVmValueGenerator implements ValueGenerator { + + @Override + public String generateValue(Session session, Object owner) { + return "Bob"; + } + } }