Split out delivery and processing channel for subscriptions

This commit is contained in:
James 2017-08-19 12:14:48 -04:00
parent 7764484f44
commit e3a28e2ff5
16 changed files with 32481 additions and 35599 deletions

View File

@ -59,6 +59,7 @@ public abstract class BaseSubscriptionInterceptor extends ServerOperationInterce
static final String SUBSCRIPTION_HEADER = "Subscription.channel.header";
private static final Integer MAX_SUBSCRIPTION_RESULTS = 1000;
private SubscribableChannel myProcessingChannel;
private SubscribableChannel myDeliveryChannel;
private ExecutorService myExecutor;
private boolean myAutoActivateSubscriptions = true;
private int myExecutorThreadCount = 1;
@ -70,6 +71,14 @@ public abstract class BaseSubscriptionInterceptor extends ServerOperationInterce
public abstract Subscription.SubscriptionChannelType getChannelType();
public SubscribableChannel getDeliveryChannel() {
return myDeliveryChannel;
}
public void setDeliveryChannel(SubscribableChannel theDeliveryChannel) {
myDeliveryChannel = theDeliveryChannel;
}
public BlockingQueue<Runnable> getExecutorQueueForUnitTests() {
return myExecutorQueue;
}
@ -144,19 +153,22 @@ public abstract class BaseSubscriptionInterceptor extends ServerOperationInterce
rejectedExecutionHandler);
if (myProcessingChannel == null) {
myProcessingChannel = new ExecutorSubscribableChannel(myExecutor);
if (getProcessingChannel() == null) {
setProcessingChannel(new ExecutorSubscribableChannel(myExecutor));
}
if (getDeliveryChannel() == null) {
setDeliveryChannel(new ExecutorSubscribableChannel(myExecutor));
}
if (myAutoActivateSubscriptions) {
if (mySubscriptionActivatingSubscriber == null) {
mySubscriptionActivatingSubscriber = new SubscriptionActivatingSubscriber(getSubscriptionDao(), myIdToSubscription, getChannelType(), myProcessingChannel);
mySubscriptionActivatingSubscriber = new SubscriptionActivatingSubscriber(getSubscriptionDao(), myIdToSubscription, getChannelType(), this);
}
getProcessingChannel().subscribe(mySubscriptionActivatingSubscriber);
}
if (mySubscriptionCheckingSubscriber == null) {
mySubscriptionCheckingSubscriber = new SubscriptionCheckingSubscriber(getSubscriptionDao(), myIdToSubscription, getChannelType(), myProcessingChannel);
mySubscriptionCheckingSubscriber = new SubscriptionCheckingSubscriber(getSubscriptionDao(), myIdToSubscription, getChannelType(), this);
}
getProcessingChannel().subscribe(mySubscriptionCheckingSubscriber);

View File

@ -26,14 +26,14 @@ public abstract class BaseSubscriptionRestHookInterceptor extends BaseSubscripti
@Override
protected void registerDeliverySubscriber() {
if (mySubscriptionDeliverySubscriber == null) {
mySubscriptionDeliverySubscriber = new SubscriptionDeliveringRestHookSubscriber(getSubscriptionDao(), getIdToSubscription(), getChannelType(), getProcessingChannel());
mySubscriptionDeliverySubscriber = new SubscriptionDeliveringRestHookSubscriber(getSubscriptionDao(), getIdToSubscription(), getChannelType(), this);
}
getProcessingChannel().subscribe(mySubscriptionDeliverySubscriber);
getDeliveryChannel().subscribe(mySubscriptionDeliverySubscriber);
}
@Override
protected void unregisterDeliverySubscriber() {
getProcessingChannel().unsubscribe(mySubscriptionDeliverySubscriber);
getDeliveryChannel().unsubscribe(mySubscriptionDeliverySubscriber);
}
}

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.r4.model.Subscription;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.SubscribableChannel;
import java.util.concurrent.ConcurrentHashMap;
@ -35,16 +34,16 @@ public abstract class BaseSubscriptionSubscriber implements MessageHandler {
private final IFhirResourceDao mySubscriptionDao;
private final ConcurrentHashMap<String, IBaseResource> myIdToSubscription;
private final Subscription.SubscriptionChannelType myChannelType;
private final SubscribableChannel myProcessingChannel;
private final BaseSubscriptionInterceptor mySubscriptionInterceptor;
/**
* Constructor
*/
public BaseSubscriptionSubscriber(IFhirResourceDao<? extends IBaseResource> theSubscriptionDao, ConcurrentHashMap<String, IBaseResource> theIdToSubscription, Subscription.SubscriptionChannelType theChannelType, SubscribableChannel theProcessingChannel) {
public BaseSubscriptionSubscriber(IFhirResourceDao<? extends IBaseResource> theSubscriptionDao, ConcurrentHashMap<String, IBaseResource> theIdToSubscription, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) {
mySubscriptionDao = theSubscriptionDao;
myIdToSubscription = theIdToSubscription;
myChannelType = theChannelType;
myProcessingChannel = theProcessingChannel;
mySubscriptionInterceptor = theSubscriptionInterceptor;
}
public Subscription.SubscriptionChannelType getChannelType() {
@ -59,14 +58,14 @@ public abstract class BaseSubscriptionSubscriber implements MessageHandler {
return myIdToSubscription;
}
public SubscribableChannel getProcessingChannel() {
return myProcessingChannel;
}
public IFhirResourceDao getSubscriptionDao() {
return mySubscriptionDao;
}
public BaseSubscriptionInterceptor getSubscriptionInterceptor() {
return mySubscriptionInterceptor;
}
/**
* Does this subscription type (e.g. rest hook, websocket, etc) apply to this interceptor?
*/

View File

@ -44,14 +44,14 @@ public abstract class BaseSubscriptionWebsocketInterceptor extends BaseSubscript
@Override
protected void registerDeliverySubscriber() {
if (mySubscriptionDeliverySubscriber == null) {
mySubscriptionDeliverySubscriber = new SubscriptionDeliveringWebsocketSubscriber(getSubscriptionDao(), getIdToSubscription(), getChannelType(), getProcessingChannel(), myTxManager, mySubscriptionFlaggedResourceDataDao, mySubscriptionTableDao, myResourceTableDao);
mySubscriptionDeliverySubscriber = new SubscriptionDeliveringWebsocketSubscriber(getSubscriptionDao(), getIdToSubscription(), getChannelType(), this, myTxManager, mySubscriptionFlaggedResourceDataDao, mySubscriptionTableDao, myResourceTableDao);
}
getProcessingChannel().subscribe(mySubscriptionDeliverySubscriber);
getDeliveryChannel().subscribe(mySubscriptionDeliverySubscriber);
}
@Override
protected void unregisterDeliverySubscriber() {
getProcessingChannel().unsubscribe(mySubscriptionDeliverySubscriber);
getDeliveryChannel().unsubscribe(mySubscriptionDeliverySubscriber);
}
}

View File

@ -41,8 +41,8 @@ public class SubscriptionActivatingSubscriber extends BaseSubscriptionSubscriber
/**
* Constructor
*/
public SubscriptionActivatingSubscriber(IFhirResourceDao<? extends IBaseResource> theSubscriptionDao, ConcurrentHashMap<String, IBaseResource> theIdToSubscription, Subscription.SubscriptionChannelType theChannelType, SubscribableChannel theProcessingChannel) {
super(theSubscriptionDao, theIdToSubscription, theChannelType, theProcessingChannel);
public SubscriptionActivatingSubscriber(IFhirResourceDao<? extends IBaseResource> theSubscriptionDao, ConcurrentHashMap<String, IBaseResource> theIdToSubscription, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) {
super(theSubscriptionDao, theIdToSubscription, theChannelType, theSubscriptionInterceptor);
}
private void activateAndRegisterSubscriptionIfRequired(ResourceModifiedMessage theMsg) {

View File

@ -45,8 +45,8 @@ import java.util.concurrent.ConcurrentHashMap;
public class SubscriptionCheckingSubscriber extends BaseSubscriptionSubscriber {
private Logger ourLog = LoggerFactory.getLogger(SubscriptionCheckingSubscriber.class);
public SubscriptionCheckingSubscriber(IFhirResourceDao theSubscriptionDao, ConcurrentHashMap<String, IBaseResource> theIdToSubscription, Subscription.SubscriptionChannelType theChannelType, SubscribableChannel theProcessingChannel) {
super(theSubscriptionDao, theIdToSubscription, theChannelType, theProcessingChannel);
public SubscriptionCheckingSubscriber(IFhirResourceDao theSubscriptionDao, ConcurrentHashMap<String, IBaseResource> theIdToSubscription, Subscription.SubscriptionChannelType theChannelType,BaseSubscriptionInterceptor theSubscriptionInterceptor) {
super(theSubscriptionDao, theIdToSubscription, theChannelType, theSubscriptionInterceptor);
}
@Override
@ -112,7 +112,7 @@ public class SubscriptionCheckingSubscriber extends BaseSubscriptionSubscriber {
deliveryMsg.setOperationType(msg.getOperationType());
deliveryMsg.setPayloadId(msg.getId());
getProcessingChannel().send(new GenericMessage<>(deliveryMsg));
getSubscriptionInterceptor().getDeliveryChannel().send(new GenericMessage<>(deliveryMsg));
}
}

View File

@ -44,8 +44,8 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionSubscriber {
private Logger ourLog = LoggerFactory.getLogger(SubscriptionDeliveringRestHookSubscriber.class);
public SubscriptionDeliveringRestHookSubscriber(IFhirResourceDao theSubscriptionDao, ConcurrentHashMap<String, IBaseResource> theIdToSubscription, Subscription.SubscriptionChannelType theChannelType, SubscribableChannel theProcessingChannel) {
super(theSubscriptionDao, theIdToSubscription, theChannelType, theProcessingChannel);
public SubscriptionDeliveringRestHookSubscriber(IFhirResourceDao theSubscriptionDao, ConcurrentHashMap<String, IBaseResource> theIdToSubscription, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) {
super(theSubscriptionDao, theIdToSubscription, theChannelType, theSubscriptionInterceptor);
}
@Override

View File

@ -58,8 +58,8 @@ public class SubscriptionDeliveringWebsocketSubscriber extends BaseSubscriptionS
private final IResourceTableDao myResourceTableDao;
private Logger ourLog = LoggerFactory.getLogger(SubscriptionDeliveringWebsocketSubscriber.class);
public SubscriptionDeliveringWebsocketSubscriber(IFhirResourceDao theSubscriptionDao, ConcurrentHashMap<String, IBaseResource> theIdToSubscription, Subscription.SubscriptionChannelType theChannelType, SubscribableChannel theProcessingChannel, PlatformTransactionManager theTxManager, ISubscriptionFlaggedResourceDataDao theSubscriptionFlaggedResourceDataDao, ISubscriptionTableDao theSubscriptionTableDao, IResourceTableDao theResourceTableDao) {
super(theSubscriptionDao, theIdToSubscription, theChannelType, theProcessingChannel);
public SubscriptionDeliveringWebsocketSubscriber(IFhirResourceDao theSubscriptionDao, ConcurrentHashMap<String, IBaseResource> theIdToSubscription, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor, PlatformTransactionManager theTxManager, ISubscriptionFlaggedResourceDataDao theSubscriptionFlaggedResourceDataDao, ISubscriptionTableDao theSubscriptionTableDao, IResourceTableDao theResourceTableDao) {
super(theSubscriptionDao, theIdToSubscription, theChannelType, theSubscriptionInterceptor);
myTxManager = theTxManager;
mySubscriptionFlaggedResourceDao = theSubscriptionFlaggedResourceDataDao;

View File

@ -1,7 +1,7 @@
<Bundle xmlns="http://hl7.org/fhir">
<id value="v2-valuesets"/>
<meta>
<lastUpdated value="2015-11-11T10:54:01.813-05:00"/>
<lastUpdated value="2015-10-24T07:41:03.495+11:00"/>
</meta>
<type value="collection"/>
<entry>
@ -34461,7 +34461,7 @@
<display value="MMR"/>
<designation>
<language value="nl"/>
<value value="BMR - bof, mazelen, rodehond"/>
<value value="BMR - mazelen, bof, rodehond"/>
</designation>
</concept>
<concept>
@ -34901,7 +34901,7 @@
<display value="DTaP"/>
<designation>
<language value="nl"/>
<value value="DaKT -difterie-acellulaire kinkhoest-tetanus"/>
<value value="DaKT"/>
</designation>
</concept>
<concept>
@ -35396,7 +35396,7 @@
<display value="leprosy"/>
<designation>
<language value="nl"/>
<value value="lepra"/>
<value value="leprosy"/>
</designation>
</concept>
<concept>
@ -35715,7 +35715,7 @@
<display value="MMRV"/>
<designation>
<language value="nl"/>
<value value="BMRV - bof, mazelen, rodehond en varicella"/>
<value value="BMRV - mazelen, bof, rodehond en varicella"/>
</designation>
</concept>
<concept>

View File

@ -1,15 +1,10 @@
package org.hl7.fhir.instance.hapi.validation;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.instance.model.Bundle;
import org.hl7.fhir.instance.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.instance.model.CodeType;
import org.hl7.fhir.instance.model.IdType;
import org.hl7.fhir.instance.model.ValueSet;
import org.hl7.fhir.instance.model.ValueSet.ConceptSetComponent;
@ -17,19 +12,19 @@ import org.hl7.fhir.instance.model.ValueSet.ValueSetExpansionComponent;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class DefaultProfileValidationSupport implements IValidationSupport {
private Map<String, ValueSet> myDefaultValueSets;
private Map<String, ValueSet> myCodeSystems;
@Override
public boolean isCodeSystemSupported(FhirContext theContext, String theSystem) {
return false;
}
/**
* Constructor
*/
@ -37,9 +32,27 @@ public class DefaultProfileValidationSupport implements IValidationSupport {
super();
}
public void flush() {
myDefaultValueSets = null;
myCodeSystems = null;
@Override
public ValueSetExpansionComponent expandValueSet(FhirContext theContext, ConceptSetComponent theInclude) {
return null;
}
@Override
public ValueSet fetchCodeSystem(FhirContext theContext, String theSystem) {
synchronized (this) {
Map<String, ValueSet> valueSets = myCodeSystems;
if (valueSets == null) {
valueSets = new HashMap<String, ValueSet>();
loadCodeSystems(theContext, valueSets, "/org/hl7/fhir/instance/model/valueset/valuesets.xml");
loadCodeSystems(theContext, valueSets, "/org/hl7/fhir/instance/model/valueset/v2-tables.xml");
loadCodeSystems(theContext, valueSets, "/org/hl7/fhir/instance/model/valueset/v3-codesystems.xml");
myCodeSystems = valueSets;
}
return valueSets.get(theSystem);
}
}
@SuppressWarnings("unchecked")
@ -85,27 +98,14 @@ public class DefaultProfileValidationSupport implements IValidationSupport {
return null;
}
@Override
public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) {
return new CodeValidationResult(IssueSeverity.INFORMATION, "Unknown code: " + theCodeSystem + " / " + theCode);
public void flush() {
myDefaultValueSets = null;
myCodeSystems = null;
}
@Override
public ValueSet fetchCodeSystem(FhirContext theContext, String theSystem) {
synchronized (this) {
Map<String, ValueSet> valueSets = myCodeSystems;
if (valueSets == null) {
valueSets = new HashMap<String, ValueSet>();
loadCodeSystems(theContext, valueSets, "/org/hl7/fhir/instance/model/valueset/valuesets.xml");
loadCodeSystems(theContext, valueSets, "/org/hl7/fhir/instance/model/valueset/v2-tables.xml");
loadCodeSystems(theContext, valueSets, "/org/hl7/fhir/instance/model/valueset/v3-codesystems.xml");
myCodeSystems = valueSets;
}
return valueSets.get(theSystem);
}
public boolean isCodeSystemSupported(FhirContext theContext, String theSystem) {
return false;
}
private void loadCodeSystems(FhirContext theContext, Map<String, ValueSet> codeSystems, String file) {
@ -132,8 +132,18 @@ public class DefaultProfileValidationSupport implements IValidationSupport {
}
@Override
public ValueSetExpansionComponent expandValueSet(FhirContext theContext, ConceptSetComponent theInclude) {
return null;
public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) {
ValueSet vs = fetchCodeSystem(theContext, theCodeSystem);
if (vs != null) {
for (ValueSet.ConceptDefinitionComponent nextConcept : vs.getCodeSystem().getConcept()) {
if (nextConcept.getCode().equals(theCode)){
ValueSet.ConceptDefinitionComponent component = new ValueSet.ConceptDefinitionComponent(new CodeType(theCode));
return new CodeValidationResult(component);
}
}
}
return new CodeValidationResult(IssueSeverity.WARNING, "Unknown code: " + theCodeSystem + " / " + theCode);
}
}

View File

@ -1,8 +1,6 @@
package org.hl7.fhir.instance.hapi.validation;
import java.util.List;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.context.FhirContext;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.formats.IParser;
import org.hl7.fhir.instance.formats.ParserType;
@ -15,10 +13,14 @@ import org.hl7.fhir.instance.model.ValueSet.ValueSetExpansionComponent;
import org.hl7.fhir.instance.terminologies.ValueSetExpander;
import org.hl7.fhir.instance.terminologies.ValueSetExpanderFactory;
import org.hl7.fhir.instance.terminologies.ValueSetExpanderSimple;
import org.hl7.fhir.instance.utils.*;
import org.hl7.fhir.instance.utils.INarrativeGenerator;
import org.hl7.fhir.instance.utils.IResourceValidator;
import org.hl7.fhir.instance.utils.IWorkerContext;
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
import ca.uhn.fhir.context.FhirContext;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public final class HapiWorkerContext implements IWorkerContext, ValueSetExpanderFactory, ValueSetExpander {
private final FhirContext myCtx;
@ -174,21 +176,6 @@ public final class HapiWorkerContext implements IWorkerContext, ValueSetExpander
@Override
public ValidationResult validateCode(String theSystem, String theCode, String theDisplay, ValueSet theVs) {
/*
* For some reason the built-in valueset is empty
*/
if (theVs.getIdElement().getValue().equals("http://hl7.org/fhir/ValueSet/defined-types")) {
// try {
// myCtx.getResourceDefinition(theCode);
// return new ValidationResult(new ConceptDefinitionComponent(new CodeType(theCode)));
// } catch (DataFormatException e){
// if (myCtx.getElementDefinition(theCode) != null) {
// return new ValidationResult(new ConceptDefinitionComponent(new CodeType(theCode)));
// }
// }
return new ValidationResult(new ConceptDefinitionComponent(new CodeType(theCode)));
}
if (theSystem == null || StringUtils.equals(theSystem, theVs.getCodeSystem().getSystem())) {
for (ConceptDefinitionComponent next : theVs.getCodeSystem().getConcept()) {
ValidationResult retVal = validateCodeSystem(theCode, next);
@ -199,7 +186,13 @@ public final class HapiWorkerContext implements IWorkerContext, ValueSetExpander
}
for (ConceptSetComponent nextComposeConceptSet : theVs.getCompose().getInclude()) {
if (StringUtils.equals(theSystem, nextComposeConceptSet.getSystem())) {
String nextSystem = theSystem;
if (nextSystem == null && isNotBlank(nextComposeConceptSet.getSystem())) {
nextSystem = nextComposeConceptSet.getSystem();
}
if (StringUtils.equals(nextSystem, nextComposeConceptSet.getSystem())) {
for (ConceptReferenceComponent nextComposeCode : nextComposeConceptSet.getConcept()) {
ConceptDefinitionComponent conceptDef = new ConceptDefinitionComponent();
conceptDef.setCode(nextComposeCode.getCode());
@ -209,8 +202,16 @@ public final class HapiWorkerContext implements IWorkerContext, ValueSetExpander
return retVal;
}
}
if (nextComposeConceptSet.getConcept().isEmpty()){
ValidationResult result = validateCode(nextSystem, theCode, null);
if (result.isOk()){
return result;
}
}
}
}
return new ValidationResult(IssueSeverity.ERROR, "Unknown code[" + theCode + "] in system[" + theSystem + "]");
}

View File

@ -13,17 +13,28 @@ public class HapiWorkerContextTest {
@Test
public void testIdTypes(){
HapiWorkerContext hwc = new HapiWorkerContext(FhirContext.forDstu2(), new DefaultProfileValidationSupport());
ValueSet vs = new ValueSet();
vs.setId("http://hl7.org/fhir/ValueSet/defined-types");
DefaultProfileValidationSupport validationSupport = new DefaultProfileValidationSupport();
FhirContext ctx = FhirContext.forDstu2();
HapiWorkerContext hwc = new HapiWorkerContext(ctx, validationSupport);
ValueSet vs = validationSupport.fetchResource(ctx, ValueSet.class, "http://hl7.org/fhir/ValueSet/defined-types");
IWorkerContext.ValidationResult outcome;
outcome = hwc.validateCode("http://hl7.org/fhir/resource-types", "Patient", null);
assertTrue(outcome.isOk());
outcome = hwc.validateCode("http://hl7.org/fhir/resource-types", "Patient", null, vs);
assertTrue(outcome.isOk());
outcome = hwc.validateCode(null, "Patient", null, vs);
assertTrue(outcome.isOk());
outcome = hwc.validateCode(null, "id", null, vs);
assertTrue(outcome.isOk());
outcome = hwc.validateCode(null, "foo", null, vs);
assertFalse(outcome.isOk());
}

View File

@ -151,10 +151,14 @@ public class ResourceMinimizerMojo extends AbstractMojo {
}
public static void main(String[] args) throws Exception {
FhirContext ctxDstu2 = FhirContext.forDstu2();
FhirContext ctxDstu2_1 = FhirContext.forDstu2_1();
FhirContext ctxDstu3 = FhirContext.forDstu3();
FhirContext ctxR4 = FhirContext.forR4();
FhirContext ctxDstu2;
FhirContext ctxDstu2_1;
FhirContext ctxDstu3;
FhirContext ctxR4;
ctxDstu2 = FhirContext.forDstu2();
ctxDstu2_1 = FhirContext.forDstu2_1();
ctxDstu3 = FhirContext.forDstu3();
ctxR4 = FhirContext.forR4();
LoggerContext loggerContext = ((ch.qos.logback.classic.Logger) ourLog).getLoggerContext();
URL mainURL = ConfigurationWatchListUtil.getMainWatchURL(loggerContext);

View File

@ -319,6 +319,10 @@
<action type="add">
REST Hook subscriptions now honour the Subscription.channel.header field
</action>
<action type="add">
DSTU2 validator has been enhanced to do a better job handling
ValueSets with expansions pointing to other ValueSets
</action>
</release>
<release version="2.5" date="2017-06-08">
<action type="fix">