Revamp the interceptor framework for JPA

This commit is contained in:
James Agnew 2015-10-27 18:34:27 -04:00
parent 60f4c27f5b
commit 43c1212840
15 changed files with 326 additions and 59 deletions

View File

@ -49,7 +49,6 @@ import javax.xml.stream.events.Characters;
import javax.xml.stream.events.XMLEvent;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -175,7 +174,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
private EntityManager myEntityManager;
private List<IDaoListener> myListeners = new ArrayList<IDaoListener>();
@Autowired
private PlatformTransactionManager myPlatformTransactionManager;
@ -647,12 +645,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
return true;
}
protected void notifyWriteCompleted() {
for (IDaoListener next : myListeners) {
next.writeCompleted();
}
}
protected void populateResourceIntoEntity(IResource theResource, ResourceTable theEntity) {
theEntity.setResourceType(toResourceName(theResource));
@ -896,12 +888,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
return parameters;
}
@Override
public void registerDaoListener(IDaoListener theListener) {
Validate.notNull(theListener, "theListener");
myListeners.add(theListener);
}
private void searchHistoryCurrentVersion(List<HistoryTuple> theTuples, List<BaseHasResource> theRetVal) {
Collection<HistoryTuple> tuples = Collections2.filter(theTuples, new com.google.common.base.Predicate<HistoryTuple>() {
@Override

View File

@ -95,6 +95,7 @@ import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchResult;
import ca.uhn.fhir.jpa.entity.TagDefinition;
import ca.uhn.fhir.jpa.entity.TagTypeEnum;
import ca.uhn.fhir.jpa.interceptor.IJpaServerInterceptor;
import ca.uhn.fhir.jpa.util.StopWatch;
import ca.uhn.fhir.model.api.IPrimitiveDatatype;
import ca.uhn.fhir.model.api.IQueryParameterType;
@ -136,6 +137,7 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.ObjectUtil;
@ -1031,7 +1033,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
myEntityManager.persist(newEntity);
myEntityManager.merge(entity);
notifyWriteCompleted();
ourLog.info("Processed addTag {}/{} on {} in {}ms", new Object[] { theScheme, theTerm, theId, w.getMillisAndRestart() });
}
@ -1363,7 +1365,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
Date updateTime = new Date();
ResourceTable savedEntity = updateEntity(null, entity, true, updateTime, updateTime);
notifyWriteCompleted();
// Notify JPA interceptors
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IJpaServerInterceptor) {
((IJpaServerInterceptor) next).resourceDeleted(requestDetails, entity);
}
}
ourLog.info("Processed delete on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
return toMethodOutcome(savedEntity, null);
@ -1404,7 +1411,13 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
// Perform delete
Date updateTime = new Date();
updateEntity(null, entity, true, updateTime, updateTime);
notifyWriteCompleted();
// Notify JPA interceptors
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IJpaServerInterceptor) {
((IJpaServerInterceptor) next).resourceDeleted(requestDetails, entity);
}
}
}
@ -1455,12 +1468,18 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
ActionRequestDetails requestDetails = new ActionRequestDetails(theResource.getId(), toResourceName(theResource), theResource);
notifyInterceptors(RestOperationTypeEnum.CREATE, requestDetails);
// Perform actual DB update
updateEntity(theResource, entity, false, null, thePerformIndexing, true, theUpdateTime);
// Notify JPA interceptors
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IJpaServerInterceptor) {
((IJpaServerInterceptor) next).resourceCreated(requestDetails, entity);
}
}
DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(true);
notifyWriteCompleted();
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulCreate", outcome.getId(), w.getMillisAndRestart());
outcome.setOperationOutcome(createInfoOperationOutcome(msg));
@ -1909,7 +1928,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
//@formatter:on
myEntityManager.merge(entity);
notifyWriteCompleted();
ourLog.info("Processed metaAddOperation on {} in {}ms", new Object[] { theResourceId, w.getMillisAndRestart() });
return metaGetOperation(theResourceId);
@ -2729,7 +2747,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
// Perform update
ResourceTable savedEntity = updateEntity(theResource, entity, true, null, thePerformIndexing, true, new Date());
notifyWriteCompleted();
// Notify JPA interceptors
for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IJpaServerInterceptor) {
((IJpaServerInterceptor) next).resourceUpdated(requestDetails, entity);
}
}
DaoMethodOutcome outcome = toMethodOutcome(savedEntity, theResource).setCreated(false);

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.dao;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang3.Validate;
@ -65,6 +66,9 @@ public class DaoConfig {
* @see #setInterceptors(List)
*/
public List<IServerInterceptor> getInterceptors() {
if (myInterceptors == null) {
return Collections.emptyList();
}
return myInterceptors;
}

View File

@ -40,6 +40,7 @@ import ca.uhn.fhir.util.FhirTerser;
public class FhirResourceDaoDstu1<T extends IResource> extends BaseHapiFhirResourceDao<T> {
@Override
protected List<Object> getIncludeValues(FhirTerser t, Include next, IBaseResource nextResource, RuntimeResourceDefinition def) {
List<Object> values;
if ("*".equals(next.getValue())) {

View File

@ -278,8 +278,6 @@ public class FhirSystemDaoDstu1 extends BaseHapiFhirSystemDao<List<IResource>> {
oo.addIssue().setSeverity(IssueSeverityEnum.INFORMATION).setDetails("Transaction completed in " + delay + "ms with " + creations + " creations and " + updates + " updates");
notifyWriteCompleted();
return retVal;
}

View File

@ -446,8 +446,6 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
long delay = System.currentTimeMillis() - start;
ourLog.info(theActionName + " completed in {}ms", new Object[] { delay });
notifyWriteCompleted();
response.setType(BundleTypeEnum.TRANSACTION_RESPONSE);
return response;
}

View File

@ -1,7 +1,5 @@
package ca.uhn.fhir.jpa.dao;
import java.util.List;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
@ -42,8 +40,4 @@ public interface IDao {
}
};
void registerDaoListener(IDaoListener theListener);
// void setResourceDaos(List<IFhirResourceDao<?>> theResourceDaos);
}

View File

@ -1,27 +0,0 @@
package ca.uhn.fhir.jpa.dao;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2015 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public interface IDaoListener {
void writeCompleted();
}

View File

@ -0,0 +1,74 @@
package ca.uhn.fhir.jpa.interceptor;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2015 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
/**
* Server interceptor for JPA DAOs which adds methods that will be called at certain points
* in the operation lifecycle for JPA operations.
*/
public interface IJpaServerInterceptor extends IServerInterceptor {
/**
* This method is invoked by the JPA DAOs when a resource has been newly created in the database.
* It will be invoked within the current transaction scope.
* <p>
* This method is called after the
* entity has been persisted and flushed to the database, so it is probably not a good
* candidate for security decisions.
* </p>
*
* @param theDetails The request details
* @param theResourceTable The actual created entity
*/
void resourceCreated(ActionRequestDetails theDetails, ResourceTable theResourceTable);
/**
* This method is invoked by the JPA DAOs when a resource has been updated in the database.
* It will be invoked within the current transaction scope.
* <p>
* This method is called after the
* entity has been persisted and flushed to the database, so it is probably not a good
* candidate for security decisions.
* </p>
*
* @param theDetails The request details
* @param theResourceTable The actual updated entity
*/
void resourceUpdated(ActionRequestDetails theDetails, ResourceTable theResourceTable);
/**
* This method is invoked by the JPA DAOs when a resource has been updated in the database.
* It will be invoked within the current transaction scope.
* <p>
* This method is called after the
* entity has been persisted and flushed to the database, so it is probably not a good
* candidate for security decisions.
* </p>
*
* @param theDetails The request details
* @param theResourceTable The actual updated entity
*/
void resourceDeleted(ActionRequestDetails theDetails, ResourceTable theResourceTable);
}

View File

@ -0,0 +1,23 @@
package ca.uhn.fhir.jpa.interceptor;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter;
public class JpaServerInterceptorAdapter extends InterceptorAdapter implements IJpaServerInterceptor {
@Override
public void resourceCreated(ActionRequestDetails theDetails, ResourceTable theResourceTable) {
// nothing
}
@Override
public void resourceUpdated(ActionRequestDetails theDetails, ResourceTable theResourceTable) {
// nothing
}
@Override
public void resourceDeleted(ActionRequestDetails theDetails, ResourceTable theResourceTable) {
// nothing
}
}

View File

@ -0,0 +1,156 @@
package ca.uhn.fhir.jpa.dao;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.interceptor.IJpaServerInterceptor;
import ca.uhn.fhir.jpa.interceptor.JpaServerInterceptorAdapter;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
public class FhirResourceDaoDstu2InterceptorTest extends BaseJpaDstu2Test {
private IJpaServerInterceptor myJpaInterceptor;
private JpaServerInterceptorAdapter myJpaInterceptorAdapter = new JpaServerInterceptorAdapter();
@After
public void after() {
myDaoConfig.getInterceptors().remove(myJpaInterceptor);
myDaoConfig.getInterceptors().remove(myJpaInterceptorAdapter);
}
@Before
public void before() {
myJpaInterceptor = mock(IJpaServerInterceptor.class);
myDaoConfig.getInterceptors().add(myJpaInterceptor);
myDaoConfig.getInterceptors().add(myJpaInterceptorAdapter);
}
/*
* *****************************************************
* Note that non JPA specific operations get tested in individual
* operation test methods too
* *****************************************************
*/
@Test
public void testJpaCreate() {
Patient p = new Patient();
p.addName().addFamily("PATIENT");
Long id = myPatientDao.create(p).getId().getIdPartAsLong();
ArgumentCaptor<ActionRequestDetails> detailsCapt;
ArgumentCaptor<ResourceTable> tableCapt;
detailsCapt = ArgumentCaptor.forClass(ActionRequestDetails.class);
tableCapt = ArgumentCaptor.forClass(ResourceTable.class);
verify(myJpaInterceptor, times(1)).resourceCreated(detailsCapt.capture(), tableCapt.capture());
assertNotNull(tableCapt.getValue().getId());
assertEquals(id, tableCapt.getValue().getId());
detailsCapt = ArgumentCaptor.forClass(ActionRequestDetails.class);
tableCapt = ArgumentCaptor.forClass(ResourceTable.class);
verify(myJpaInterceptor, times(0)).resourceUpdated(detailsCapt.capture(), tableCapt.capture());
/*
* Not do a conditional create
*/
p = new Patient();
p.addName().addFamily("PATIENT1");
Long id2 = myPatientDao.create(p, "Patient?family=PATIENT").getId().getIdPartAsLong();
assertEquals(id, id2);
detailsCapt = ArgumentCaptor.forClass(ActionRequestDetails.class);
tableCapt = ArgumentCaptor.forClass(ResourceTable.class);
verify(myJpaInterceptor, times(1)).resourceCreated(detailsCapt.capture(), tableCapt.capture());
verify(myJpaInterceptor, times(0)).resourceUpdated(detailsCapt.capture(), tableCapt.capture());
}
@Test
public void testJpaDelete() {
Patient p = new Patient();
p.addName().addFamily("PATIENT");
Long id = myPatientDao.create(p).getId().getIdPartAsLong();
myPatientDao.delete(new IdDt("Patient", id));
ArgumentCaptor<ActionRequestDetails> detailsCapt;
ArgumentCaptor<ResourceTable> tableCapt;
detailsCapt = ArgumentCaptor.forClass(ActionRequestDetails.class);
tableCapt = ArgumentCaptor.forClass(ResourceTable.class);
verify(myJpaInterceptor, times(1)).resourceDeleted(detailsCapt.capture(), tableCapt.capture());
assertNotNull(tableCapt.getValue().getId());
assertEquals(id, tableCapt.getValue().getId());
}
@Test
public void testJpaUpdate() {
Patient p = new Patient();
p.addName().addFamily("PATIENT");
Long id = myPatientDao.create(p).getId().getIdPartAsLong();
p = new Patient();
p.setId(new IdDt(id));
p.addName().addFamily("PATIENT1");
Long id2 = myPatientDao.update(p).getId().getIdPartAsLong();
assertEquals(id, id2);
ArgumentCaptor<ActionRequestDetails> detailsCapt;
ArgumentCaptor<ResourceTable> tableCapt;
detailsCapt = ArgumentCaptor.forClass(ActionRequestDetails.class);
tableCapt = ArgumentCaptor.forClass(ResourceTable.class);
verify(myJpaInterceptor, times(1)).resourceUpdated(detailsCapt.capture(), tableCapt.capture());
assertNotNull(tableCapt.getValue().getId());
assertEquals(id, tableCapt.getValue().getId());
/*
* Now do a conditional update
*/
p = new Patient();
p.setId(new IdDt(id));
p.addName().addFamily("PATIENT2");
id2 = myPatientDao.update(p, "Patient?family=PATIENT1").getId().getIdPartAsLong();
assertEquals(id, id2);
detailsCapt = ArgumentCaptor.forClass(ActionRequestDetails.class);
tableCapt = ArgumentCaptor.forClass(ResourceTable.class);
verify(myJpaInterceptor, times(1)).resourceCreated(detailsCapt.capture(), tableCapt.capture());
verify(myJpaInterceptor, times(2)).resourceUpdated(detailsCapt.capture(), tableCapt.capture());
assertEquals(id, tableCapt.getAllValues().get(2).getId());
/*
* Now do a conditional update where none will match (so this is actually a create)
*/
p = new Patient();
p.addName().addFamily("PATIENT3");
id2 = myPatientDao.update(p, "Patient?family=ZZZ").getId().getIdPartAsLong();
assertNotEquals(id, id2);
detailsCapt = ArgumentCaptor.forClass(ActionRequestDetails.class);
tableCapt = ArgumentCaptor.forClass(ResourceTable.class);
verify(myJpaInterceptor, times(2)).resourceUpdated(detailsCapt.capture(), tableCapt.capture());
verify(myJpaInterceptor, times(2)).resourceCreated(detailsCapt.capture(), tableCapt.capture());
assertEquals(id2, tableCapt.getAllValues().get(3).getId());
}
}

View File

@ -68,6 +68,7 @@ public class FhirServerConfig extends BaseJavaConfigDstu2 {
private Properties jpaProperties() {
Properties extraProperties = new Properties();
extraProperties.put("hibernate.dialect", org.hibernate.dialect.DerbyTenSevenDialect.class.getName());
extraProperties.put("hibernate.format_sql", "true");
extraProperties.put("hibernate.show_sql", "false");
extraProperties.put("hibernate.hbm2ddl.auto", "update");

View File

@ -194,6 +194,10 @@
<id>samlanfranchi</id>
<name>Sam Lanfranchi</name>
</developer>
<developer>
<id>jkiddo</id>
<name>Jens Kristian Villadsen</name>
</developer>
</developers>
<licenses>

View File

@ -204,9 +204,14 @@
@am202 for reporting!
</action>
<action type="fix" issue="245">
FIx issue in testpage-overlay's new Java configuration where only the first
Fix issue in testpage-overlay's new Java configuration where only the first
configured server actually gets used.
</action>
<action type="add">
Introduce
<![CDATA[<a href="./apidocs-jpaserver/ca/uhn/fhir/jpa/dao/IJpaServerInterceptor.html">IJpaServerInterceptor</a>]]>
interceptors for JPA server which can be used for more fine grained operations.
</action>
</release>
<release version="1.2" date="2015-09-18">
<action type="add">

View File

@ -275,6 +275,33 @@
</macro>
</section>
<section name="JPA Server Interceptors">
<p>
The HAPI <a href="./doc_jpa.html">JPA Server</a> is an added layer on top of the HAPI
REST server framework. If you are using it, you may wish to also register interceptors
against the <a href="./apidocs-jpaserver/ca/uhn/fhir/jpa/dao/DaoConfig.html">DaoConfig</a>
bean that you create using Spring configuration.
</p>
<p>
By registering an interceptor against the DaoConfig, the server will invoke
interceptor methods for operations such as <b>create</b>, <b>update</b>, etc even
when these operations are found nested within a transaction. This is useful
if you are using interceptors to make access control decisions because
it avoids clients using transactions as a means of bypassing these controls.
</p>
<p>
You may also choose to create interceptors which implement the
more specialized
<a href="./apidocs-jpaserver/ca/uhn/fhir/jpa/dao/IJpaServerInterceptor.html">IJpaServerInterceptor</a>
interface, as this interceptor adds additional methods which are called during the JPA
lifecycle.
</p>
</section>
</body>