Migrate User Guide Events chapter extras to test folder

This commit is contained in:
Vlad Mihalcea 2016-02-01 16:40:44 +02:00
parent ff3b54a962
commit 3642bb63cd
10 changed files with 472 additions and 197 deletions

View File

@ -1,6 +1,6 @@
[[events]] [[events]]
== Interceptors and events == Interceptors and events
:sourcedir: extras :sourcedir: ../../../../../test/java/org/hibernate/userguide/events
It is useful for the application to react to certain events that occur inside Hibernate. It is useful for the application to react to certain events that occur inside Hibernate.
This allows for the implementation of generic functionality and the extension of Hibernate functionality. This allows for the implementation of generic functionality and the extension of Hibernate functionality.
@ -12,8 +12,15 @@ The `org.hibernate.Interceptor` interface provides callbacks from the session to
allowing the application to inspect and/or manipulate properties of a persistent object before it is saved, updated, deleted or loaded. allowing the application to inspect and/or manipulate properties of a persistent object before it is saved, updated, deleted or loaded.
One possible use for this is to track auditing information. One possible use for this is to track auditing information.
The following example shows an `Interceptor` implementation that automatically sets the `createTimestamp` property when an `Auditable` entity is created, The following example shows an `Interceptor` implementation that automatically logs when an entity is updated.
and it updates the `lastUpdateTimestamp` property when an Auditable entity is updated.
[[events-interceptors-example]]
====
[source, JAVA, indent=0]
----
include::{sourcedir}/InterceptorTest.java[tags=events-interceptors-example]
----
====
[NOTE] [NOTE]
==== ====
@ -24,10 +31,11 @@ An Interceptor can be either `Session`-scoped or `SessionFactory`-scoped.
A Session-scoped interceptor is specified when a session is opened. A Session-scoped interceptor is specified when a session is opened.
[[events-interceptors-session-scope-example]]
==== ====
[source,java] [source, JAVA, indent=0]
---- ----
include::{sourcedir}/SessionScopedExample.java[] include::{sourcedir}/InterceptorTest.java[tags=events-interceptors-session-scope-example]
---- ----
==== ====
@ -36,10 +44,11 @@ Unless a session is opened explicitly specifying the interceptor to use, the `Se
`SessionFactory`-scoped interceptors must be thread safe. `SessionFactory`-scoped interceptors must be thread safe.
Ensure that you do not store session-specific states, since multiple sessions will use this interceptor potentially concurrently. Ensure that you do not store session-specific states, since multiple sessions will use this interceptor potentially concurrently.
[[events-interceptors-session-factory-scope-example]]
==== ====
[source,java] [source, JAVA, indent=0]
---- ----
include::{sourcedir}/SessionFactoryScopedExample.java[] include::{sourcedir}/InterceptorTest.java[tags=events-interceptors-session-factory-scope-example]
---- ----
==== ====
@ -65,14 +74,16 @@ A custom listener implements the appropriate interface for the event it wants to
(or even the default event listeners used by Hibernate out-of-the-box as these are declared non-final for this purpose). (or even the default event listeners used by Hibernate out-of-the-box as these are declared non-final for this purpose).
Here is an example of a custom load event listener: Here is an example of a custom load event listener:
[[events-interceptors-load-listener-example]]
.Custom LoadListener example .Custom LoadListener example
==== ====
[source,java] [source, JAVA, indent=0]
---- ----
include::{sourcedir}/LoadListenerExample.java[] include::{sourcedir}/ListenerTest.java[tags=events-interceptors-load-listener-example]
---- ----
==== ====
[[events-declarative-security]]
=== Hibernate declarative security === Hibernate declarative security
Usually, declarative security in Hibernate applications is managed in a session facade layer. Usually, declarative security in Hibernate applications is managed in a session facade layer.
@ -84,16 +95,18 @@ Again, see <<chapters/bootstrap/Bootstrap.adoc#bootstrap-event-listener-registra
Below is an example of an appropriate `org.hibernate.integrator.spi.Integrator` implementation for this purpose. Below is an example of an appropriate `org.hibernate.integrator.spi.Integrator` implementation for this purpose.
[[events-declarative-security-jacc-example]]
.JACC listener registration example .JACC listener registration example
==== ====
[source,java] [source, JAVA, indent=0]
---- ----
include::{sourcedir}/jacc-event-reg-example.java[] include::{sourcedir}/ListenerTest.java[tags=events-declarative-security-jacc-example]
---- ----
==== ====
You must also decide how to configure your JACC provider. Consult your JACC provider documentation. You must also decide how to configure your JACC provider. Consult your JACC provider documentation.
[[events-jpa-callbacks]]
=== JPA Callbacks === JPA Callbacks
JPA also defines a more limited set of callbacks through annotations. JPA also defines a more limited set of callbacks through annotations.
@ -111,7 +124,7 @@ JPA also defines a more limited set of callbacks through annotations.
|@PostLoad |Executed after an entity has been loaded into the current persistence context or an entity has been refreshed. |@PostLoad |Executed after an entity has been loaded into the current persistence context or an entity has been refreshed.
|======================================================================= |=======================================================================
There are 2 available approaches defined for specifying callback handling: There are two available approaches defined for specifying callback handling:
* The first approach is to annotate methods on the entity itself to receive notification of particular entity life cycle event(s). * The first approach is to annotate methods on the entity itself to receive notification of particular entity life cycle event(s).
* The second is to use a separate entity listener class. * The second is to use a separate entity listener class.
@ -119,11 +132,12 @@ An entity listener is a stateless class with a no-arg constructor.
The callback annotations are placed on a method of this class instead of the entity class. The callback annotations are placed on a method of this class instead of the entity class.
The entity listener class is then associated with the entity using the `javax.persistence.EntityListeners` annotation The entity listener class is then associated with the entity using the `javax.persistence.EntityListeners` annotation
[[events-jpa-callbacks-example]]
.Example of specifying JPA callbacks .Example of specifying JPA callbacks
==== ====
[source,java] [source, JAVA, indent=0]
---- ----
include::{sourcedir}/JpaCallbacksExample.java[] include::{sourcedir}/ListenerTest.java[tags=events-jpa-callbacks-example]
---- ----
==== ====

View File

@ -1,81 +0,0 @@
import org.hibernate.EmptyInterceptor;
import org.hibernate.Transaction;
import org.hibernate.type.Type;
import java.io.Serializable;
import java.util.Date;
public class AuditInterceptor extends EmptyInterceptor {
private int updates;
private int creates;
private int loads;
public void onDelete(Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
// do nothing
}
public boolean onFlushDirty(Object entity,
Serializable id,
Object[] currentState,
Object[] previousState,
String[] propertyNames,
Type[] types) {
if ( entity instanceof Auditable ) {
updates++;
for ( int i = 0; i < propertyNames.length; i++ ) {
if ( "lastUpdateTimestamp".equals( propertyNames[i] ) ) {
currentState[i] = new Date();
return true;
}
}
}
return false;
}
public boolean onLoad(Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
if ( entity instanceof Auditable ) {
loads++;
}
return false;
}
public boolean onSave(Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
if ( entity instanceof Auditable ) {
creates++;
for ( int i = 0; i < propertyNames.length; i++ ) {
if ( "createTimestamp".equals( propertyNames[i] ) ) {
state[i] = new Date();
return true;
}
}
}
return false;
}
public void afterTransactionCompletion( Transaction tx ) {
if ( tx.wasCommitted() ) {
System.out.println( "Creations: " + creates + ", Updates: " + updates + "Loads: " + loads );
}
updates = 0;
creates = 0;
loads = 0;
}
}

View File

@ -1,46 +0,0 @@
@Entity
@EntityListeners( LastUpdateListener.class )
public class Cat {
@Id
private Integer id;
private String name;
private Calendar dateOfBirth;
@Transient
private int age;
private Date lastUpdate;
//getters and setters
/**
* Set my transient property at load time based on a calculation,
* note that a native Hibernate formula mapping is better for this purpose.
*/
@PostLoad
public void calculateAge() {
Calendar birth = new GregorianCalendar();
birth.setTime( dateOfBirth );
Calendar now = new GregorianCalendar();
now.setTime( new Date() );
int adjust = 0;
if ( now.get( Calendar.DAY_OF_YEAR ) - birth.get( Calendar.DAY_OF_YEAR ) & lt;
0){
adjust = -1;
}
age = now.get( Calendar.YEAR ) - birth.get( Calendar.YEAR ) + adjust;
}
}
public class LastUpdateListener {
/**
* automatic property set before any database persistence
*/
@PreUpdate
@PrePersist
public void setLastUpdate( Cat o ) {
o.setLastUpdate( new Date() );
}
}

View File

@ -1,9 +0,0 @@
public class LoadListenerExample implements LoadEventListener {
// this is the single method defined by the LoadEventListener interface
public void onLoad( LoadEvent event, LoadEventListener.LoadType loadType )
throws HibernateException {
if (!MySecurity.isAuthorized( event.getEntityClassName(), event.getEntityId() )) {
throw MySecurityException( "Unauthorized access" );
}
}
}

View File

@ -1,4 +0,0 @@
SessionFactory sessionFactory = new Configuration()
.setInterceptor( new AuditInterceptor() )
...
.buildSessionFactory();

View File

@ -1 +0,0 @@
Session session = sf.withOptions().interceptor( new AuditInterceptor() ).openSession();

View File

@ -1,42 +0,0 @@
import org.hibernate.event.service.spi.DuplicationStrategy;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.integrator.spi.Integrator;
import org.hibernate.secure.internal.*;
public class JaccEventListenerIntegrator implements Integrator {
private static final DuplicationStrategy JACC_DUPLICATION_STRATEGY = new DuplicationStrategy() {
@Override
public boolean areMatch( Object listener, Object original ) {
return listener.getClass().equals( original.getClass() ) &&
JACCSecurityListener.class.isInstance( original );
}
@Override
public Action getAction() {
return Action.KEEP_ORIGINAL;
}
};
@Override
@SuppressWarnings({"unchecked"})
public void integrate(
Configuration configuration,
SessionFactoryImplementor sessionFactory,
SessionFactoryServiceRegistry serviceRegistry) {
boolean isSecurityEnabled = configuration.getProperties().containsKey( AvailableSettings.JACC_ENABLED );
if (!isSecurityEnabled) {
return;
}
final EventListenerRegistry eventListenerRegistry = serviceRegistry.getService( EventListenerRegistry.class );
eventListenerRegistry.addDuplicationStrategy( JACC_DUPLICATION_STRATEGY );
final String jaccContextId = configuration.getProperty( Environment.JACC_CONTEXTID );
eventListenerRegistry.prependListeners( EventType.PRE_DELETE, new JACCPreDeleteEventListener( jaccContextId ) );
eventListenerRegistry.prependListeners( EventType.PRE_INSERT, new JACCPreInsertEventListener( jaccContextId ) );
eventListenerRegistry.prependListeners( EventType.PRE_UPDATE, new JACCPreUpdateEventListener( jaccContextId ) );
eventListenerRegistry.prependListeners( EventType.PRE_LOAD, new JACCPreLoadEventListener( jaccContextId ) );
}
}

View File

@ -0,0 +1,147 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.userguide.events;
import java.io.Serializable;
import java.util.Arrays;
import javax.persistence.Entity;
import javax.persistence.EntityManagerFactory;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import org.hibernate.EmptyInterceptor;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;
import org.hibernate.type.Type;
import org.junit.Before;
import org.junit.Test;
import org.jboss.logging.Logger;
import static org.hibernate.userguide.util.TransactionUtil.doInJPA;
/**
* @author Vlad Mihalcea
*/
public class InterceptorTest extends BaseEntityManagerFunctionalTestCase {
private static final Logger LOGGER = Logger.getLogger( InterceptorTest.class );
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] {
Customer.class
};
}
@Before
public void init() {
doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.persist( new Customer( "John Doe" ) );
Customer customer = new Customer();
entityManager.persist( customer );
} );
}
@Test
public void testSessionInterceptor() {
EntityManagerFactory entityManagerFactory = entityManagerFactory();
Serializable customerId = 1L;
//tag::events-interceptors-session-scope-example[]
SessionFactory sessionFactory = entityManagerFactory.unwrap( SessionFactory.class );
Session session = sessionFactory
.withOptions()
.interceptor(new LoggingInterceptor() )
.openSession();
Customer customer = session.get( Customer.class, customerId );
customer.setName( "Mr. John Doe" );
//Entity Customer#1 changed from [John Doe, 0] to [Mr. John Doe, 0]
session.flush();
//end::events-interceptors-session-scope-example[]
session.close();
}
@Test
public void testSessionFactoryInterceptor() {
Serializable customerId = 1L;
//tag::events-interceptors-session-factory-scope-example[]
SessionFactory sessionFactory = new MetadataSources( new StandardServiceRegistryBuilder().build() )
.addAnnotatedClass( Customer.class )
.getMetadataBuilder()
.build()
.getSessionFactoryBuilder()
.applyInterceptor( new LoggingInterceptor() )
.build();
//end::events-interceptors-session-factory-scope-example[]
Session session = sessionFactory.openSession();
Customer customer = session.get( Customer.class, customerId );
customer.setName( "Mr. John Doe" );
//Entity Customer#1 changed from [John Doe, 0] to [Mr. John Doe, 0]
session.flush();
session.close();
sessionFactory.close();
}
@Entity(name = "Customer")
public static class Customer {
@Id
@GeneratedValue
private Long id;
private String name;
public Customer() {
}
public Customer(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
//tag::events-interceptors-example[]
public static class LoggingInterceptor extends EmptyInterceptor {
@Override
public boolean onFlushDirty(
Object entity,
Serializable id,
Object[] currentState,
Object[] previousState,
String[] propertyNames,
Type[] types) {
LOGGER.debugv( "Entity {0}#{1} changed from {2} to {3}",
entity.getClass().getSimpleName(),
id,
Arrays.toString( previousState ),
Arrays.toString( currentState )
);
return super.onFlushDirty( entity, id, currentState,
previousState, propertyNames, types
);
}
}
//end::events-interceptors-example[]
}

View File

@ -0,0 +1,296 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.userguide.events;
import java.io.Serializable;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Map;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.EntityManagerFactory;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.PostLoad;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Transient;
import org.hibernate.HibernateException;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.service.spi.DuplicationStrategy;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.event.spi.LoadEvent;
import org.hibernate.event.spi.LoadEventListener;
import org.hibernate.integrator.spi.ServiceContributingIntegrator;
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;
import org.hibernate.secure.internal.DisabledJaccServiceImpl;
import org.hibernate.secure.internal.JaccPreDeleteEventListener;
import org.hibernate.secure.internal.JaccPreInsertEventListener;
import org.hibernate.secure.internal.JaccPreLoadEventListener;
import org.hibernate.secure.internal.JaccPreUpdateEventListener;
import org.hibernate.secure.internal.JaccSecurityListener;
import org.hibernate.secure.internal.StandardJaccServiceImpl;
import org.hibernate.secure.spi.GrantedPermission;
import org.hibernate.secure.spi.IntegrationException;
import org.hibernate.secure.spi.JaccPermissionDeclarations;
import org.hibernate.secure.spi.JaccService;
import org.hibernate.service.spi.SessionFactoryServiceRegistry;
import org.junit.Test;
import org.jboss.logging.Logger;
import static org.hibernate.userguide.util.TransactionUtil.doInJPA;
import static org.junit.Assert.assertTrue;
/**
* @author Vlad Mihalcea
*/
public class ListenerTest extends BaseEntityManagerFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] {
Person.class,
Customer.class
};
}
@Test(expected = SecurityException.class)
public void testLoadListener() {
Serializable customerId = 1L;
doInJPA( this::entityManagerFactory, entityManager -> {
//tag::events-interceptors-load-listener-example[]
EntityManagerFactory entityManagerFactory = entityManagerFactory();
SessionFactoryImplementor sessionFactory = entityManagerFactory.unwrap( SessionFactoryImplementor.class );
sessionFactory
.getServiceRegistry()
.getService( EventListenerRegistry.class )
.prependListeners( EventType.LOAD, new SecuredLoadEntityListener() );
Customer customer = entityManager.find( Customer.class, customerId );
//end::events-interceptors-load-listener-example[]
} );
}
@Test
public void testJPACallback() {
Long personId = 1L;
doInJPA( this::entityManagerFactory, entityManager -> {
Person person = new Person();
person.id = personId;
person.name = "John Doe";
person.dateOfBirth = Timestamp.valueOf(LocalDateTime.of( 2000, 1, 1, 0, 0, 0 ));
entityManager.persist( person );
} );
doInJPA( this::entityManagerFactory, entityManager -> {
Person person = entityManager.find( Person.class, personId );
assertTrue(person.age > 0);
} );
}
@Entity(name = "Customer")
public static class Customer {
@Id
@GeneratedValue
private Long id;
private String name;
public Customer() {
}
public Customer(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
//tag::events-jpa-callbacks-example[]
@Entity
@EntityListeners( LastUpdateListener.class )
public static class Person {
@Id
private Long id;
private String name;
private Date dateOfBirth;
@Transient
private long age;
private Date lastUpdate;
public void setLastUpdate(Date lastUpdate) {
this.lastUpdate = lastUpdate;
}
/**
* Set the transient property at load time based on a calculation.
* Note that a native Hibernate formula mapping is better for this purpose.
*/
@PostLoad
public void calculateAge() {
age = ChronoUnit.YEARS.between( LocalDateTime.ofInstant(
Instant.ofEpochMilli( dateOfBirth.getTime()), ZoneOffset.UTC),
LocalDateTime.now()
);
}
}
public static class LastUpdateListener {
@PreUpdate
@PrePersist
public void setLastUpdate( Person p ) {
p.setLastUpdate( new Date() );
}
}
//end::events-jpa-callbacks-example[]
//tag::events-interceptors-example[]
public static class SecuredLoadEntityListener implements LoadEventListener {
// this is the single method defined by the LoadEventListener interface
public void onLoad(LoadEvent event, LoadEventListener.LoadType loadType)
throws HibernateException {
if ( !Principal.isAuthorized( event.getEntityClassName(), event.getEntityId() ) ) {
throw new SecurityException( "Unauthorized access" );
}
}
}
//end::events-Principal-example[]
public static class Principal {
public static boolean isAuthorized(String clazz, Serializable id) {
return false;
}
}
//tag::events-declarative-security-jacc-example[]
public static class JaccIntegrator implements ServiceContributingIntegrator {
private static final Logger log = Logger.getLogger( JaccIntegrator.class );
private static final DuplicationStrategy DUPLICATION_STRATEGY =
new DuplicationStrategy() {
@Override
public boolean areMatch(Object listener, Object original) {
return listener.getClass().equals( original.getClass() ) &&
JaccSecurityListener.class.isInstance( original );
}
@Override
public Action getAction() {
return Action.KEEP_ORIGINAL;
}
};
@Override
public void prepareServices(
StandardServiceRegistryBuilder serviceRegistryBuilder) {
boolean isSecurityEnabled = serviceRegistryBuilder
.getSettings().containsKey( AvailableSettings.JACC_ENABLED );
final JaccService jaccService = isSecurityEnabled ?
new StandardJaccServiceImpl() : new DisabledJaccServiceImpl();
serviceRegistryBuilder.addService( JaccService.class, jaccService );
}
@Override
public void integrate(
Metadata metadata,
SessionFactoryImplementor sessionFactory,
SessionFactoryServiceRegistry serviceRegistry) {
doIntegration(
serviceRegistry
.getService( ConfigurationService.class ).getSettings(),
// pass no permissions here, because atm actually injecting the
// permissions into the JaccService is handled on SessionFactoryImpl via
// the org.hibernate.boot.cfgxml.spi.CfgXmlAccessService
null,
serviceRegistry
);
}
private void doIntegration(
Map properties,
JaccPermissionDeclarations permissionDeclarations,
SessionFactoryServiceRegistry serviceRegistry) {
boolean isSecurityEnabled = properties
.containsKey( AvailableSettings.JACC_ENABLED );
if ( ! isSecurityEnabled ) {
log.debug( "Skipping JACC integration as it was not enabled" );
return;
}
final String contextId = (String) properties
.get( AvailableSettings.JACC_CONTEXT_ID );
if ( contextId == null ) {
throw new IntegrationException( "JACC context id must be specified" );
}
final JaccService jaccService = serviceRegistry
.getService( JaccService.class );
if ( jaccService == null ) {
throw new IntegrationException( "JaccService was not set up" );
}
if ( permissionDeclarations != null ) {
for ( GrantedPermission declaration : permissionDeclarations
.getPermissionDeclarations() ) {
jaccService.addPermission( declaration );
}
}
final EventListenerRegistry eventListenerRegistry =
serviceRegistry.getService( EventListenerRegistry.class );
eventListenerRegistry.addDuplicationStrategy( DUPLICATION_STRATEGY );
eventListenerRegistry.prependListeners(
EventType.PRE_DELETE, new JaccPreDeleteEventListener() );
eventListenerRegistry.prependListeners(
EventType.PRE_INSERT, new JaccPreInsertEventListener() );
eventListenerRegistry.prependListeners(
EventType.PRE_UPDATE, new JaccPreUpdateEventListener() );
eventListenerRegistry.prependListeners(
EventType.PRE_LOAD, new JaccPreLoadEventListener() );
}
@Override
public void disintegrate(SessionFactoryImplementor sessionFactory,
SessionFactoryServiceRegistry serviceRegistry) {
// nothing to do
}
}
//end::events-declarative-security-jacc-example[]
}

View File

@ -49,3 +49,4 @@ log4j.logger.org.hibernate.tool.hbm2ddl=info
### provide information about merged entity copies. ### provide information about merged entity copies.
#log4j.logger.org.hibernate.event.internal.EntityCopyAllowedLoggedObserver=warn #log4j.logger.org.hibernate.event.internal.EntityCopyAllowedLoggedObserver=warn
log4j.logger.org.hibernate.userguide=debug