Don't crash on startup if an invalid subscription is in the database

This commit is contained in:
James Agnew 2018-02-03 15:47:48 -05:00
parent efc812bf56
commit 3cbf669007
12 changed files with 533 additions and 96 deletions

View File

@ -0,0 +1,64 @@
package ca.uhn.fhir.util;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.thymeleaf.util.Validate;
import java.util.List;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2018 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%
*/
/**
* Utilities for working with the subscription resource
*/
public class SubscriptionUtil {
private static void populatePrimitiveValue(FhirContext theContext, IBaseResource theSubscription, String theChildName, String theValue) {
RuntimeResourceDefinition def = theContext.getResourceDefinition(theSubscription);
Validate.isTrue(def.getName().equals("Subscription"), "theResource is not a subscription");
BaseRuntimeChildDefinition statusChild = def.getChildByName(theChildName);
List<IBase> entries = statusChild.getAccessor().getValues(theSubscription);
IPrimitiveType<?> instance;
if (entries.size() == 0) {
BaseRuntimeElementDefinition<?> statusElement = statusChild.getChildByName(theChildName);
instance = (IPrimitiveType<?>) statusElement.newInstance(statusChild.getInstanceConstructorArguments());
statusChild.getMutator().addValue(theSubscription, instance);
} else {
instance = (IPrimitiveType<?>) entries.get(0);
}
instance.setValueAsString(theValue);
}
public static void setReason(FhirContext theContext, IBaseResource theSubscription, String theMessage) {
populatePrimitiveValue(theContext, theSubscription, "reason", theMessage);
}
public static void setStatus(FhirContext theContext, IBaseResource theSubscription, String theStatus) {
populatePrimitiveValue(theContext, theSubscription, "status", theStatus);
}
}

View File

@ -50,6 +50,7 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.*; import ca.uhn.fhir.util.*;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
@ -75,8 +76,6 @@ import javax.xml.stream.events.Characters;
import javax.xml.stream.events.XMLEvent; import javax.xml.stream.events.XMLEvent;
import java.io.CharArrayWriter; import java.io.CharArrayWriter;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.nio.CharBuffer;
import java.text.Normalizer; import java.text.Normalizer;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
@ -104,6 +103,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class);
private static final Map<FhirVersionEnum, FhirContext> ourRetrievalContexts = new HashMap<FhirVersionEnum, FhirContext>(); private static final Map<FhirVersionEnum, FhirContext> ourRetrievalContexts = new HashMap<FhirVersionEnum, FhirContext>();
private static final String PROCESSING_SUB_REQUEST = "BaseHapiFhirDao.processingSubRequest"; private static final String PROCESSING_SUB_REQUEST = "BaseHapiFhirDao.processingSubRequest";
private static boolean ourValidationDisabledForUnitTest;
static { static {
Map<String, Class<? extends IQueryParameterType>> resourceMetaParams = new HashMap<String, Class<? extends IQueryParameterType>>(); Map<String, Class<? extends IQueryParameterType>> resourceMetaParams = new HashMap<String, Class<? extends IQueryParameterType>>();
@ -1213,6 +1213,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
retVal.removeAll(theToRemove); retVal.removeAll(theToRemove);
return retVal; return retVal;
} }
private void setUpdatedTime(Collection<? extends BaseResourceIndexedSearchParam> theParams, Date theUpdateTime) { private void setUpdatedTime(Collection<? extends BaseResourceIndexedSearchParam> theParams, Date theUpdateTime) {
for (BaseResourceIndexedSearchParam nextSearchParam : theParams) { for (BaseResourceIndexedSearchParam nextSearchParam : theParams) {
nextSearchParam.setUpdated(theUpdateTime); nextSearchParam.setUpdated(theUpdateTime);
@ -1377,8 +1378,10 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
*/ */
if (theResource != null) { if (theResource != null) {
if (thePerformIndexing) { if (thePerformIndexing) {
if (!ourValidationDisabledForUnitTest) {
validateResourceForStorage((T) theResource, theEntity); validateResourceForStorage((T) theResource, theEntity);
} }
}
String resourceType = myContext.getResourceDefinition(theResource).getName(); String resourceType = myContext.getResourceDefinition(theResource).getName();
if (isNotBlank(theEntity.getResourceType()) && !theEntity.getResourceType().equals(resourceType)) { if (isNotBlank(theEntity.getResourceType()) && !theEntity.getResourceType().equals(resourceType)) {
throw new UnprocessableEntityException( throw new UnprocessableEntityException(
@ -2105,6 +2108,14 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
return b.toString(); return b.toString();
} }
/**
* Do not call this method outside of unit tests
*/
@VisibleForTesting
public static void setValidationDisabledForUnitTest(boolean theValidationDisabledForUnitTest) {
ourValidationDisabledForUnitTest = theValidationDisabledForUnitTest;
}
private static List<BaseCodingDt> toBaseCodingList(List<IBaseCoding> theSecurityLabels) { private static List<BaseCodingDt> toBaseCodingList(List<IBaseCoding> theSecurityLabels) {
ArrayList<BaseCodingDt> retVal = new ArrayList<BaseCodingDt>(theSecurityLabels.size()); ArrayList<BaseCodingDt> retVal = new ArrayList<BaseCodingDt>(theSecurityLabels.size());
for (IBaseCoding next : theSecurityLabels) { for (IBaseCoding next : theSecurityLabels) {

View File

@ -29,6 +29,7 @@ import ca.uhn.fhir.jpa.entity.SubscriptionTable;
import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import org.apache.commons.lang3.ObjectUtils;
import org.hl7.fhir.dstu3.model.Subscription; import org.hl7.fhir.dstu3.model.Subscription;
import org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType; import org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType;
import org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus; import org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus;
@ -108,6 +109,16 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3<Subsc
} }
public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(Subscription theResource) { public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(Subscription theResource) {
switch (ObjectUtils.defaultIfNull(theResource.getStatus(), SubscriptionStatus.OFF)) {
case REQUESTED:
case ACTIVE:
break;
case ERROR:
case OFF:
case NULL:
return null;
}
String query = theResource.getCriteria(); String query = theResource.getCriteria();
if (isBlank(query)) { if (isBlank(query)) {
throw new UnprocessableEntityException("Subscription.criteria must be populated"); throw new UnprocessableEntityException("Subscription.criteria must be populated");
@ -144,6 +155,9 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3<Subsc
super.validateResourceForStorage(theResource, theEntityToSave); super.validateResourceForStorage(theResource, theEntityToSave);
RuntimeResourceDefinition resDef = validateCriteriaAndReturnResourceDefinition(theResource); RuntimeResourceDefinition resDef = validateCriteriaAndReturnResourceDefinition(theResource);
if (resDef == null) {
return;
}
IFhirResourceDao<? extends IBaseResource> dao = getDao(resDef.getImplementingClass()); IFhirResourceDao<? extends IBaseResource> dao = getDao(resDef.getImplementingClass());
if (dao == null) { if (dao == null) {

View File

@ -29,6 +29,7 @@ import ca.uhn.fhir.jpa.entity.SubscriptionTable;
import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import org.apache.commons.lang3.ObjectUtils;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Subscription; import org.hl7.fhir.r4.model.Subscription;
@ -37,20 +38,16 @@ import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import javax.annotation.Nullable;
import java.util.Date; import java.util.Date;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
public class FhirResourceDaoSubscriptionR4 extends FhirResourceDaoR4<Subscription> implements IFhirResourceDaoSubscription<Subscription> { public class FhirResourceDaoSubscriptionR4 extends FhirResourceDaoR4<Subscription> implements IFhirResourceDaoSubscription<Subscription> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoSubscriptionR4.class);
@Autowired @Autowired
private ISubscriptionTableDao mySubscriptionTableDao; private ISubscriptionTableDao mySubscriptionTableDao;
@Autowired
private PlatformTransactionManager myTxManager;
private void createSubscriptionTable(ResourceTable theEntity, Subscription theSubscription) { private void createSubscriptionTable(ResourceTable theEntity, Subscription theSubscription) {
SubscriptionTable subscriptionEntity = new SubscriptionTable(); SubscriptionTable subscriptionEntity = new SubscriptionTable();
subscriptionEntity.setCreated(new Date()); subscriptionEntity.setCreated(new Date());
@ -108,7 +105,18 @@ public class FhirResourceDaoSubscriptionR4 extends FhirResourceDaoR4<Subscriptio
} }
} }
@Nullable
public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(Subscription theResource) { public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(Subscription theResource) {
switch (ObjectUtils.defaultIfNull(theResource.getStatus(), Subscription.SubscriptionStatus.OFF)) {
case REQUESTED:
case ACTIVE:
break;
case ERROR:
case OFF:
case NULL:
return null;
}
String query = theResource.getCriteria(); String query = theResource.getCriteria();
if (isBlank(query)) { if (isBlank(query)) {
throw new UnprocessableEntityException("Subscription.criteria must be populated"); throw new UnprocessableEntityException("Subscription.criteria must be populated");
@ -145,6 +153,9 @@ public class FhirResourceDaoSubscriptionR4 extends FhirResourceDaoR4<Subscriptio
super.validateResourceForStorage(theResource, theEntityToSave); super.validateResourceForStorage(theResource, theEntityToSave);
RuntimeResourceDefinition resDef = validateCriteriaAndReturnResourceDefinition(theResource); RuntimeResourceDefinition resDef = validateCriteriaAndReturnResourceDefinition(theResource);
if (resDef == null) {
return;
}
IFhirResourceDao<? extends IBaseResource> dao = getDao(resDef.getImplementingClass()); IFhirResourceDao<? extends IBaseResource> dao = getDao(resDef.getImplementingClass());
if (dao == null) { if (dao == null) {

View File

@ -26,7 +26,6 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Subscription; import org.hl7.fhir.r4.model.Subscription;
import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
public abstract class BaseSubscriptionSubscriber implements MessageHandler { public abstract class BaseSubscriptionSubscriber implements MessageHandler {
@ -60,14 +59,6 @@ public abstract class BaseSubscriptionSubscriber implements MessageHandler {
} }
/**
* Does this subscription type (e.g. rest hook, websocket, etc) apply to this interceptor?
*/
protected boolean subscriptionTypeApplies(IBaseResource theSubscription) {
FhirContext ctx = mySubscriptionDao.getContext();
return subscriptionTypeApplies(ctx, theSubscription);
}
/** /**
* Does this subscription type (e.g. rest hook, websocket, etc) apply to this interceptor? * Does this subscription type (e.g. rest hook, websocket, etc) apply to this interceptor?
*/ */
@ -80,11 +71,13 @@ public abstract class BaseSubscriptionSubscriber implements MessageHandler {
* Does this subscription type (e.g. rest hook, websocket, etc) apply to this interceptor? * Does this subscription type (e.g. rest hook, websocket, etc) apply to this interceptor?
*/ */
static boolean subscriptionTypeApplies(FhirContext theCtx, IBaseResource theSubscription, Subscription.SubscriptionChannelType theChannelType) { static boolean subscriptionTypeApplies(FhirContext theCtx, IBaseResource theSubscription, Subscription.SubscriptionChannelType theChannelType) {
IPrimitiveType<?> status = theCtx.newTerser().getSingleValueOrNull(theSubscription, BaseSubscriptionInterceptor.SUBSCRIPTION_TYPE, IPrimitiveType.class); IPrimitiveType<?> subscriptionType = theCtx.newTerser().getSingleValueOrNull(theSubscription, BaseSubscriptionInterceptor.SUBSCRIPTION_TYPE, IPrimitiveType.class);
boolean subscriptionTypeApplies = false; boolean subscriptionTypeApplies = false;
if (theChannelType.toCode().equals(status.getValueAsString())) { if (subscriptionType != null) {
if (theChannelType.toCode().equals(subscriptionType.getValueAsString())) {
subscriptionTypeApplies = true; subscriptionTypeApplies = true;
} }
}
return subscriptionTypeApplies; return subscriptionTypeApplies;
} }

View File

@ -23,6 +23,9 @@ package ca.uhn.fhir.jpa.subscription;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.SubscriptionUtil;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
@ -30,12 +33,14 @@ import org.hl7.fhir.r4.model.Subscription;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.messaging.MessagingException; import org.springframework.messaging.MessagingException;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.*;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.Date;
import org.springframework.transaction.support.TransactionTemplate; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public class SubscriptionActivatingSubscriber { public class SubscriptionActivatingSubscriber {
@ -92,11 +97,18 @@ public class SubscriptionActivatingSubscriber {
} }
} }
private void activateSubscription(IPrimitiveType<?> theStatus, String theActiveStatus, IBaseResource theSubscription, String theRequestedStatus) { private void activateSubscription(IPrimitiveType<?> theStatus, String theActiveStatus, final IBaseResource theSubscription, String theRequestedStatus) {
theStatus.setValueAsString(theActiveStatus); theStatus.setValueAsString(theActiveStatus);
ourLog.info("Activating and registering subscription {} from status {} to {}", theSubscription.getIdElement().toUnqualified().getValue(), theRequestedStatus, theActiveStatus); ourLog.info("Activating and registering subscription {} from status {} to {}", theSubscription.getIdElement().toUnqualified().getValue(), theRequestedStatus, theActiveStatus);
try {
mySubscriptionDao.update(theSubscription); mySubscriptionDao.update(theSubscription);
mySubscriptionInterceptor.registerSubscription(theSubscription.getIdElement(), theSubscription); } catch (final UnprocessableEntityException e) {
ourLog.info("Changing status of {} to ERROR", theSubscription.getIdElement());
IBaseResource subscription = mySubscriptionDao.read(theSubscription.getIdElement());
SubscriptionUtil.setStatus(myCtx, subscription, "error");
SubscriptionUtil.setReason(myCtx, subscription, e.getMessage());
mySubscriptionDao.update(subscription);
}
} }

View File

@ -0,0 +1,150 @@
package ca.uhn.fhir.jpa.dao.dstu3;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.dstu3.model.Subscription;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.Query;
import static org.junit.Assert.*;
public class FhirResourceDaoDstu3InvalidSubscriptionTest extends BaseJpaDstu3Test {
@Autowired
private SubscriptionRestHookInterceptor myInterceptor;
@After
public void afterResetDao() {
myDaoConfig.setResourceServerIdStrategy(new DaoConfig().getResourceServerIdStrategy());
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
}
@Test
public void testCreateInvalidSubscriptionOkButCanNotActivate() {
Subscription s = new Subscription();
s.setStatus(Subscription.SubscriptionStatus.OFF);
s.setCriteria("FOO");
IIdType id = mySubscriptionDao.create(s).getId().toUnqualified();
s = mySubscriptionDao.read(id);
assertEquals("FOO", s.getCriteria());
s.setStatus(Subscription.SubscriptionStatus.REQUESTED);
try {
mySubscriptionDao.update(s);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("Subscription.criteria must be in the form \"{Resource Type}?[params]", e.getMessage());
}
}
/**
* Make sure that bad data in the database doesn't prevent startup
*/
@Test
public void testSubscriptionMarkedDeleted() {
BaseHapiFhirDao.setValidationDisabledForUnitTest(true);
Subscription s = new Subscription();
s.setStatus(Subscription.SubscriptionStatus.REQUESTED);
s.getChannel().setEndpoint("http://foo");
s.getChannel().setPayload("application/fhir+json");
s.setCriteria("Patient?foo");
final IIdType id = mySubscriptionDao.create(s).getId().toUnqualifiedVersionless();
assertNotNull(id.getIdPart());
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
new TransactionTemplate(myTransactionMgr).execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
Query q = myEntityManager.createNativeQuery("UPDATE HFJ_RESOURCE SET RES_DELETED_AT = RES_UPDATED WHERE RES_ID = " + id.getIdPart());
q.executeUpdate();
}
});
myEntityManager.clear();
myInterceptor.start();
}
/**
* Make sure that bad data in the database doesn't prevent startup
*/
@Test
public void testSubscriptionWithInvalidCriteria() {
BaseHapiFhirDao.setValidationDisabledForUnitTest(true);
Subscription s = new Subscription();
s.setStatus(Subscription.SubscriptionStatus.REQUESTED);
s.getChannel().setType(Subscription.SubscriptionChannelType.RESTHOOK);
s.getChannel().setEndpoint("http://foo");
s.getChannel().setPayload("application/fhir+json");
s.setCriteria("BLAH");
IIdType id = mySubscriptionDao.create(s).getId().toUnqualifiedVersionless();
assertNotNull(id.getIdPart());
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
myInterceptor.start();
}
/**
* Make sure that bad data in the database doesn't prevent startup
*/
@Test
public void testSubscriptionWithNoStatus() {
BaseHapiFhirDao.setValidationDisabledForUnitTest(true);
Subscription s = new Subscription();
s.getChannel().setType(Subscription.SubscriptionChannelType.RESTHOOK);
s.getChannel().setEndpoint("http://foo");
s.getChannel().setPayload("application/fhir+json");
s.setCriteria("Patient?active=true");
IIdType id = mySubscriptionDao.create(s).getId().toUnqualifiedVersionless();
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
myInterceptor.start();
}
/**
* Make sure that bad data in the database doesn't prevent startup
*/
@Test
public void testSubscriptionWithNoType() {
BaseHapiFhirDao.setValidationDisabledForUnitTest(true);
Subscription s = new Subscription();
s.setStatus(Subscription.SubscriptionStatus.REQUESTED);
s.getChannel().setEndpoint("http://foo");
s.getChannel().setPayload("application/fhir+json");
s.setCriteria("Patient?foo");
IIdType id = mySubscriptionDao.create(s).getId().toUnqualifiedVersionless();
assertNotNull(id.getIdPart());
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
myInterceptor.start();
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

@ -0,0 +1,150 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Subscription;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.Query;
import static org.junit.Assert.*;
public class FhirResourceDaoR4InvalidSubscriptionTest extends BaseJpaR4Test {
@Autowired
private SubscriptionRestHookInterceptor myInterceptor;
@After
public void afterResetDao() {
myDaoConfig.setResourceServerIdStrategy(new DaoConfig().getResourceServerIdStrategy());
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
}
@Test
public void testCreateInvalidSubscriptionOkButCanNotActivate() {
Subscription s = new Subscription();
s.setStatus(Subscription.SubscriptionStatus.OFF);
s.setCriteria("FOO");
IIdType id = mySubscriptionDao.create(s).getId().toUnqualified();
s = mySubscriptionDao.read(id);
assertEquals("FOO", s.getCriteria());
s.setStatus(Subscription.SubscriptionStatus.REQUESTED);
try {
mySubscriptionDao.update(s);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("Subscription.criteria must be in the form \"{Resource Type}?[params]", e.getMessage());
}
}
/**
* Make sure that bad data in the database doesn't prevent startup
*/
@Test
public void testSubscriptionMarkedDeleted() {
BaseHapiFhirDao.setValidationDisabledForUnitTest(true);
Subscription s = new Subscription();
s.setStatus(Subscription.SubscriptionStatus.REQUESTED);
s.getChannel().setEndpoint("http://foo");
s.getChannel().setPayload("application/fhir+json");
s.setCriteria("Patient?foo");
final IIdType id = mySubscriptionDao.create(s).getId().toUnqualifiedVersionless();
assertNotNull(id.getIdPart());
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
new TransactionTemplate(myTransactionMgr).execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
Query q = myEntityManager.createNativeQuery("UPDATE HFJ_RESOURCE SET RES_DELETED_AT = RES_UPDATED WHERE RES_ID = " + id.getIdPart());
q.executeUpdate();
}
});
myEntityManager.clear();
myInterceptor.start();
}
/**
* Make sure that bad data in the database doesn't prevent startup
*/
@Test
public void testSubscriptionWithInvalidCriteria() throws InterruptedException {
BaseHapiFhirDao.setValidationDisabledForUnitTest(true);
Subscription s = new Subscription();
s.setStatus(Subscription.SubscriptionStatus.REQUESTED);
s.getChannel().setType(Subscription.SubscriptionChannelType.RESTHOOK);
s.getChannel().setEndpoint("http://foo");
s.getChannel().setPayload("application/fhir+json");
s.setCriteria("BLAH");
IIdType id = mySubscriptionDao.create(s).getId().toUnqualifiedVersionless();
assertNotNull(id.getIdPart());
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
myInterceptor.start();
}
/**
* Make sure that bad data in the database doesn't prevent startup
*/
@Test
public void testSubscriptionWithNoStatus() {
BaseHapiFhirDao.setValidationDisabledForUnitTest(true);
Subscription s = new Subscription();
s.getChannel().setType(Subscription.SubscriptionChannelType.RESTHOOK);
s.getChannel().setEndpoint("http://foo");
s.getChannel().setPayload("application/fhir+json");
s.setCriteria("Patient?active=true");
IIdType id = mySubscriptionDao.create(s).getId().toUnqualifiedVersionless();
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
myInterceptor.start();
}
/**
* Make sure that bad data in the database doesn't prevent startup
*/
@Test
public void testSubscriptionWithNoType() {
BaseHapiFhirDao.setValidationDisabledForUnitTest(true);
Subscription s = new Subscription();
s.setStatus(Subscription.SubscriptionStatus.REQUESTED);
s.getChannel().setEndpoint("http://foo");
s.getChannel().setPayload("application/fhir+json");
s.setCriteria("Patient?foo");
IIdType id = mySubscriptionDao.create(s).getId().toUnqualifiedVersionless();
assertNotNull(id.getIdPart());
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
myInterceptor.start();
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

@ -318,7 +318,13 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
boolean force = false; boolean force = false;
String[] formatParams = theRequestDetails.getParameters().get(Constants.PARAM_FORMAT); String[] formatParams = theRequestDetails.getParameters().get(Constants.PARAM_FORMAT);
if (formatParams != null && formatParams.length > 0) { if (formatParams != null && formatParams.length > 0) {
String formatParam = formatParams[0]; String formatParam = defaultString(formatParams[0]);
int semiColonIdx = formatParam.indexOf(';');
if (semiColonIdx != -1) {
formatParam = formatParam.substring(0, semiColonIdx);
}
formatParam = trim(formatParam);
if (Constants.FORMATS_HTML.contains(formatParam)) { // this is a set if (Constants.FORMATS_HTML.contains(formatParam)) { // this is a set
force = true; force = true;
} else if (Constants.FORMATS_HTML_XML.equals(formatParam)) { } else if (Constants.FORMATS_HTML_XML.equals(formatParam)) {

View File

@ -1,29 +1,34 @@
package ca.uhn.fhir.rest.server.interceptor; package ca.uhn.fhir.rest.server.interceptor;
import static org.hamcrest.Matchers.containsString; import ca.uhn.fhir.context.FhirContext;
import static org.hamcrest.Matchers.containsStringIgnoringCase; import ca.uhn.fhir.context.api.BundleInclusionRule;
import static org.hamcrest.Matchers.matchesPattern; import ca.uhn.fhir.model.api.IResource;
import static org.hamcrest.Matchers.not; import ca.uhn.fhir.model.dstu2.composite.HumanNameDt;
import static org.hamcrest.Matchers.stringContainsInOrder; import ca.uhn.fhir.model.dstu2.composite.IdentifierDt;
import static org.junit.Assert.assertArrayEquals; import ca.uhn.fhir.model.dstu2.resource.Binary;
import static org.junit.Assert.assertEquals; import ca.uhn.fhir.model.dstu2.resource.OperationOutcome;
import static org.junit.Assert.assertFalse; import ca.uhn.fhir.model.dstu2.resource.OperationOutcome.Issue;
import static org.junit.Assert.assertNull; import ca.uhn.fhir.model.dstu2.resource.Organization;
import static org.junit.Assert.assertThat; import ca.uhn.fhir.model.dstu2.resource.Patient;
import static org.junit.Assert.assertTrue; import ca.uhn.fhir.model.dstu2.valueset.IdentifierUseEnum;
import static org.mockito.Mockito.mock; import ca.uhn.fhir.model.primitive.IdDt;
import static org.mockito.Mockito.when; import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.annotation.IdParam;
import java.io.PrintWriter; import ca.uhn.fhir.rest.annotation.Read;
import java.io.StringWriter; import ca.uhn.fhir.rest.annotation.RequiredParam;
import java.nio.charset.StandardCharsets; import ca.uhn.fhir.rest.annotation.Search;
import java.util.*; import ca.uhn.fhir.rest.api.Constants;
import java.util.concurrent.TimeUnit; import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import ca.uhn.fhir.rest.api.server.ResponseDetails; import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.UrlUtil;
import com.phloc.commons.collections.iterate.ArrayEnumeration;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
@ -34,30 +39,26 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.*; import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import com.phloc.commons.collections.iterate.ArrayEnumeration; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
import ca.uhn.fhir.context.FhirContext; import static org.hamcrest.Matchers.*;
import ca.uhn.fhir.context.api.BundleInclusionRule; import static org.junit.Assert.*;
import ca.uhn.fhir.model.api.IResource; import static org.mockito.Mockito.mock;
import ca.uhn.fhir.model.dstu2.composite.HumanNameDt; import static org.mockito.Mockito.when;
import ca.uhn.fhir.model.dstu2.composite.IdentifierDt;
import ca.uhn.fhir.model.dstu2.resource.*;
import ca.uhn.fhir.model.dstu2.resource.OperationOutcome.Issue;
import ca.uhn.fhir.model.dstu2.valueset.IdentifierUseEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.server.*;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.*;
public class ResponseHighlightingInterceptorTest { public class ResponseHighlightingInterceptorTest {
@ -253,6 +254,23 @@ public class ResponseHighlightingInterceptorTest {
ourLog.info(responseContent); ourLog.info(responseContent);
} }
@Test
public void testForceHtmlJsonWithAdditionalParts() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=" + UrlUtil.escapeUrlParam("html/json; fhirVersion=1.0"));
httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase());
assertThat(responseContent, containsString("html"));
assertThat(responseContent, containsString(">{<"));
assertThat(responseContent, not(containsString("&lt;")));
ourLog.info(responseContent);
}
@Test @Test
public void testForceHtmlXml() throws Exception { public void testForceHtmlXml() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/xml"); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/xml");
@ -895,8 +913,7 @@ public class ResponseHighlightingInterceptorTest {
/** /**
* Retrieve the resource by its identifier * Retrieve the resource by its identifier
* *
* @param theId * @param theId The resource identity
* The resource identity
* @return The resource * @return The resource
*/ */
@Read() @Read()
@ -909,8 +926,7 @@ public class ResponseHighlightingInterceptorTest {
/** /**
* Retrieve the resource by its identifier * Retrieve the resource by its identifier
* *
* @param theId * @param theId The resource identity
* The resource identity
* @return The resource * @return The resource
*/ */
@Search() @Search()

View File

@ -104,6 +104,16 @@
caused an exception if the client made a request with caused an exception if the client made a request with
no count parameter included. Thanks to Viviana Sanz for reporting! no count parameter included. Thanks to Viviana Sanz for reporting!
</action> </action>
<action type="fix">
A bug in the JPA server was fixed where a Subscription incorrectly created
without a status or with invalid criteria would cause a crash during
startup.
</action>
<action type="add">
ResponseHighlightingInterceptor now properly parses _format
parameters that include additional content (e.g.
<![CDATA[<code>_format=html/json;fhirVersion=1.0</code>]]>)
</action>
</release> </release>
<release version="3.2.0" date="2018-01-13"> <release version="3.2.0" date="2018-01-13">
<action type="add"> <action type="add">