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]]
== 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.
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.
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,
and it updates the `lastUpdateTimestamp` property when an Auditable entity is updated.
The following example shows an `Interceptor` implementation that automatically logs when an entity is updated.
[[events-interceptors-example]]
====
[source, JAVA, indent=0]
----
include::{sourcedir}/InterceptorTest.java[tags=events-interceptors-example]
----
====
[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.
[[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.
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).
Here is an example of a custom load event listener:
[[events-interceptors-load-listener-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
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.
[[events-declarative-security-jacc-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.
[[events-jpa-callbacks]]
=== JPA Callbacks
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.
|=======================================================================
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 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 entity listener class is then associated with the entity using the `javax.persistence.EntityListeners` annotation
[[events-jpa-callbacks-example]]
.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.
#log4j.logger.org.hibernate.event.internal.EntityCopyAllowedLoggedObserver=warn
log4j.logger.org.hibernate.userguide=debug