Clean up custom seach param code
This commit is contained in:
parent
f1828d1ca8
commit
53f6effd56
|
@ -27,7 +27,6 @@ import java.util.Map;
|
|||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
|
@ -37,8 +36,6 @@ import ca.uhn.fhir.context.RuntimeSearchParam;
|
|||
|
||||
public abstract class BaseSearchParamRegistry implements ISearchParamRegistry {
|
||||
|
||||
private static final Map<String, RuntimeSearchParam> EMPTY_SP_MAP = Collections.emptyMap();
|
||||
|
||||
private Map<String, Map<String, RuntimeSearchParam>> myBuiltInSearchParams;
|
||||
|
||||
@Autowired
|
||||
|
@ -68,15 +65,6 @@ public abstract class BaseSearchParamRegistry implements ISearchParamRegistry {
|
|||
return myBuiltInSearchParams.get(theResourceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<RuntimeSearchParam> getAllSearchParams(String theResourceName) {
|
||||
Validate.notBlank(theResourceName, "theResourceName must not be null or blank");
|
||||
|
||||
Map<String, RuntimeSearchParam> map = myBuiltInSearchParams.get(theResourceName);
|
||||
map = ObjectUtils.defaultIfNull(map, EMPTY_SP_MAP);
|
||||
return Collections.unmodifiableCollection(map.values());
|
||||
}
|
||||
|
||||
public Map<String, Map<String, RuntimeSearchParam>> getBuiltInSearchParams() {
|
||||
return myBuiltInSearchParams;
|
||||
}
|
||||
|
|
|
@ -1,26 +1,5 @@
|
|||
package ca.uhn.fhir.jpa.dao;
|
||||
|
||||
/*
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2017 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%
|
||||
*/
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
|
@ -33,6 +12,4 @@ public interface ISearchParamRegistry {
|
|||
|
||||
Map<String,RuntimeSearchParam> getActiveSearchParams(String theResourceName);
|
||||
|
||||
Collection<RuntimeSearchParam> getAllSearchParams(String theResourceName);
|
||||
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ package ca.uhn.fhir.jpa.dao.dstu3;
|
|||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
|
@ -36,22 +34,21 @@ import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSearchParameter;
|
|||
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
|
||||
import ca.uhn.fhir.jpa.dao.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.jpa.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.util.DeleteConflict;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
import ca.uhn.fhir.util.ElementUtil;
|
||||
|
||||
public class FhirResourceDaoSearchParameterDstu3 extends FhirResourceDaoDstu3<SearchParameter> implements IFhirResourceDaoSearchParameter<SearchParameter> {
|
||||
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoSearchParameterDstu3.class);
|
||||
|
||||
@Autowired
|
||||
private IFhirSystemDao<Bundle, Meta> mySystemDao;
|
||||
|
||||
@Autowired
|
||||
private ISearchParamRegistry mySearchParamRegistry;
|
||||
|
||||
private void markAffectedResources(SearchParameter theResource) {
|
||||
@Autowired
|
||||
private IFhirSystemDao<Bundle, Meta> mySystemDao;
|
||||
|
||||
protected void markAffectedResources(SearchParameter theResource) {
|
||||
if (theResource != null) {
|
||||
String expression = theResource.getExpression();
|
||||
String resourceType = expression.substring(0, expression.indexOf('.'));
|
||||
|
@ -63,7 +60,6 @@ public class FhirResourceDaoSearchParameterDstu3 extends FhirResourceDaoDstu3<Se
|
|||
mySearchParamRegistry.forceRefresh();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method is called once per minute to perform any required re-indexing. During most passes this will
|
||||
* just check and find that there are no resources requiring re-indexing. In that case the method just returns
|
||||
|
@ -89,13 +85,6 @@ public class FhirResourceDaoSearchParameterDstu3 extends FhirResourceDaoDstu3<Se
|
|||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void preDelete(SearchParameter theResourceToDelete) {
|
||||
super.preDelete(theResourceToDelete);
|
||||
markAffectedResources(theResourceToDelete);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void postPersist(ResourceTable theEntity, SearchParameter theResource) {
|
||||
super.postPersist(theEntity, theResource);
|
||||
|
@ -108,6 +97,12 @@ public class FhirResourceDaoSearchParameterDstu3 extends FhirResourceDaoDstu3<Se
|
|||
markAffectedResources(theResource);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void preDelete(SearchParameter theResourceToDelete) {
|
||||
super.preDelete(theResourceToDelete);
|
||||
markAffectedResources(theResourceToDelete);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void validateResourceForStorage(SearchParameter theResource, ResourceTable theEntityToSave) {
|
||||
super.validateResourceForStorage(theResource, theEntityToSave);
|
||||
|
@ -121,9 +116,18 @@ public class FhirResourceDaoSearchParameterDstu3 extends FhirResourceDaoDstu3<Se
|
|||
throw new UnprocessableEntityException("SearchParameter.expression is missing");
|
||||
}
|
||||
|
||||
if (ElementUtil.isEmpty(theResource.getBase())) {
|
||||
throw new UnprocessableEntityException("SearchParameter.base is missing");
|
||||
}
|
||||
|
||||
expression = expression.trim();
|
||||
theResource.setExpression(expression);
|
||||
|
||||
String[] expressionSplit = BaseSearchParamExtractor.SPLIT.split(expression);
|
||||
String allResourceName = null;
|
||||
for (String nextPath : expressionSplit) {
|
||||
nextPath = nextPath.trim();
|
||||
|
||||
int dotIdx = nextPath.indexOf('.');
|
||||
if (dotIdx == -1) {
|
||||
throw new UnprocessableEntityException("Invalid SearchParameter.expression value \"" + nextPath + "\". Must start with a resource name");
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.apache.commons.lang3.time.DateUtils;
|
|||
import org.hl7.fhir.dstu3.model.CodeType;
|
||||
import org.hl7.fhir.dstu3.model.SearchParameter;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
|
@ -50,19 +51,43 @@ import ca.uhn.fhir.rest.server.IBundleProvider;
|
|||
public class SearchParamRegistryDstu3 extends BaseSearchParamRegistry {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamRegistryDstu3.class);
|
||||
|
||||
@Autowired
|
||||
private IFhirResourceDao<SearchParameter> mySpDao;
|
||||
|
||||
private long myLastRefresh;
|
||||
|
||||
private volatile Map<String, Map<String, RuntimeSearchParam>> myActiveSearchParams;
|
||||
|
||||
@Autowired
|
||||
private DaoConfig myDaoConfig;
|
||||
|
||||
private long myLastRefresh;
|
||||
|
||||
@Autowired
|
||||
private IFhirResourceDao<SearchParameter> mySpDao;
|
||||
|
||||
@Override
|
||||
public void forceRefresh() {
|
||||
myLastRefresh = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() {
|
||||
refreshCacheIfNeccesary();
|
||||
return myActiveSearchParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName) {
|
||||
refreshCacheIfNeccesary();
|
||||
return myActiveSearchParams.get(theResourceName);
|
||||
}
|
||||
|
||||
private Map<String, RuntimeSearchParam> getSearchParamMap(Map<String, Map<String, RuntimeSearchParam>> searchParams, String theResourceName) {
|
||||
Map<String, RuntimeSearchParam> retVal = searchParams.get(theResourceName);
|
||||
if (retVal == null) {
|
||||
retVal = new HashMap<String, RuntimeSearchParam>();
|
||||
searchParams.put(theResourceName, retVal);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
private void refreshCacheIfNeccesary() {
|
||||
long refreshInterval = 60 * DateUtils.MILLIS_PER_MINUTE;
|
||||
if (System.currentTimeMillis() - refreshInterval > myLastRefresh) {
|
||||
StopWatch sw = new StopWatch();
|
||||
|
@ -134,22 +159,6 @@ public class SearchParamRegistryDstu3 extends BaseSearchParamRegistry {
|
|||
myLastRefresh = System.currentTimeMillis();
|
||||
ourLog.info("Refreshed search parameter cache in {}ms", sw.getMillis());
|
||||
}
|
||||
|
||||
return myActiveSearchParams.get(theResourceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forceRefresh() {
|
||||
myLastRefresh = 0;
|
||||
}
|
||||
|
||||
private Map<String, RuntimeSearchParam> getSearchParamMap(Map<String, Map<String, RuntimeSearchParam>> searchParams, String theResourceName) {
|
||||
Map<String, RuntimeSearchParam> retVal = searchParams.get(theResourceName);
|
||||
if (retVal == null) {
|
||||
retVal = new HashMap<String, RuntimeSearchParam>();
|
||||
searchParams.put(theResourceName, retVal);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
private RuntimeSearchParam toRuntimeSp(SearchParameter theNextSp) {
|
||||
|
@ -208,7 +217,9 @@ public class SearchParamRegistryDstu3 extends BaseSearchParamRegistry {
|
|||
return null;
|
||||
}
|
||||
|
||||
RuntimeSearchParam retVal = new RuntimeSearchParam(name, description, path, paramType, providesMembershipInCompartments, targets, status);
|
||||
IIdType id = theNextSp.getIdElement();
|
||||
String uri = "";
|
||||
RuntimeSearchParam retVal = new RuntimeSearchParam(id, uri, name, description, path, paramType, null, providesMembershipInCompartments, targets, status);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
|
|
|
@ -70,6 +70,10 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable {
|
|||
return myResourcePid;
|
||||
}
|
||||
|
||||
public String getResourceType() {
|
||||
return myResourceType;
|
||||
}
|
||||
|
||||
public void setParamName(String theName) {
|
||||
myParamName = theName;
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
|
|||
@Test
|
||||
public void testCreateInvalidParamInvalidResourceName() {
|
||||
SearchParameter fooSp = new SearchParameter();
|
||||
fooSp.addBase("Patient");
|
||||
fooSp.setCode("foo");
|
||||
fooSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
fooSp.setTitle("FOO SP");
|
||||
|
@ -43,9 +44,27 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateInvalidNoBase() {
|
||||
SearchParameter fooSp = new SearchParameter();
|
||||
fooSp.setCode("foo");
|
||||
fooSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
fooSp.setTitle("FOO SP");
|
||||
fooSp.setExpression("Patient.gender");
|
||||
fooSp.setXpathUsage(org.hl7.fhir.dstu3.model.SearchParameter.XPathUsageType.NORMAL);
|
||||
fooSp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE);
|
||||
try {
|
||||
mySearchParameterDao.create(fooSp, mySrd);
|
||||
fail();
|
||||
} catch (UnprocessableEntityException e) {
|
||||
assertEquals("SearchParameter.base is missing", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateInvalidParamMismatchedResourceName() {
|
||||
SearchParameter fooSp = new SearchParameter();
|
||||
fooSp.addBase("Patient");
|
||||
fooSp.setCode("foo");
|
||||
fooSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
fooSp.setTitle("FOO SP");
|
||||
|
@ -63,6 +82,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
|
|||
@Test
|
||||
public void testCreateInvalidParamNoPath() {
|
||||
SearchParameter fooSp = new SearchParameter();
|
||||
fooSp.addBase("Patient");
|
||||
fooSp.setCode("foo");
|
||||
fooSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
fooSp.setTitle("FOO SP");
|
||||
|
@ -79,6 +99,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
|
|||
@Test
|
||||
public void testCreateInvalidParamNoResourceName() {
|
||||
SearchParameter fooSp = new SearchParameter();
|
||||
fooSp.addBase("Patient");
|
||||
fooSp.setCode("foo");
|
||||
fooSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
fooSp.setTitle("FOO SP");
|
||||
|
@ -97,6 +118,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
|
|||
public void testCreateInvalidParamParamNullStatus() {
|
||||
|
||||
SearchParameter fooSp = new SearchParameter();
|
||||
fooSp.addBase("Patient");
|
||||
fooSp.setCode("foo");
|
||||
fooSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
fooSp.setTitle("FOO SP");
|
||||
|
@ -115,6 +137,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
|
|||
@Test
|
||||
public void testExtensionWithNoValueIndexesWithoutFailure() {
|
||||
SearchParameter eyeColourSp = new SearchParameter();
|
||||
eyeColourSp.addBase("Patient");
|
||||
eyeColourSp.setCode("eyecolour");
|
||||
eyeColourSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
eyeColourSp.setTitle("Eye Colour");
|
||||
|
@ -135,6 +158,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
|
|||
@Test
|
||||
public void testSearchForExtension() {
|
||||
SearchParameter eyeColourSp = new SearchParameter();
|
||||
eyeColourSp.addBase("Patient");
|
||||
eyeColourSp.setCode("eyecolour");
|
||||
eyeColourSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
eyeColourSp.setTitle("Eye Colour");
|
||||
|
@ -168,6 +192,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
|
|||
public void testSearchWithCustomParam() {
|
||||
|
||||
SearchParameter fooSp = new SearchParameter();
|
||||
fooSp.addBase("Patient");
|
||||
fooSp.setCode("foo");
|
||||
fooSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
fooSp.setTitle("FOO SP");
|
||||
|
@ -221,6 +246,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
|
|||
public void testSearchWithCustomParamDraft() {
|
||||
|
||||
SearchParameter fooSp = new SearchParameter();
|
||||
fooSp.addBase("Patient");
|
||||
fooSp.setCode("foo");
|
||||
fooSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
fooSp.setTitle("FOO SP");
|
||||
|
|
|
@ -59,11 +59,6 @@ public class SearchParamExtractorDstu3Test {
|
|||
// nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<RuntimeSearchParam> getAllSearchParams(String theResourceName) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() {
|
||||
throw new UnsupportedOperationException();
|
||||
|
|
|
@ -76,6 +76,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv
|
|||
@Test
|
||||
public void testSearchForExtension() {
|
||||
SearchParameter eyeColourSp = new SearchParameter();
|
||||
eyeColourSp.addBase("Patient");
|
||||
eyeColourSp.setCode("eyecolour");
|
||||
eyeColourSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
eyeColourSp.setTitle("Eye Colour");
|
||||
|
@ -147,6 +148,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv
|
|||
|
||||
// Add a custom search parameter
|
||||
SearchParameter fooSp = new SearchParameter();
|
||||
fooSp.addBase("Patient");
|
||||
fooSp.setCode("foo");
|
||||
fooSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
fooSp.setTitle("FOO SP");
|
||||
|
@ -157,6 +159,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv
|
|||
|
||||
// Disable an existing parameter
|
||||
fooSp = new SearchParameter();
|
||||
fooSp.addBase("Patient");
|
||||
fooSp.setCode("gender");
|
||||
fooSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
fooSp.setTitle("Gender");
|
||||
|
@ -199,6 +202,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv
|
|||
|
||||
// Add a custom search parameter
|
||||
SearchParameter fooSp = new SearchParameter();
|
||||
fooSp.addBase("Patient");
|
||||
fooSp.setCode("foo");
|
||||
fooSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
fooSp.setTitle("FOO SP");
|
||||
|
@ -209,6 +213,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv
|
|||
|
||||
// Disable an existing parameter
|
||||
fooSp = new SearchParameter();
|
||||
fooSp.addBase("Patient");
|
||||
fooSp.setCode("gender");
|
||||
fooSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
fooSp.setTitle("Gender");
|
||||
|
@ -253,6 +258,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv
|
|||
public void testSearchWithCustomParam() {
|
||||
|
||||
SearchParameter fooSp = new SearchParameter();
|
||||
fooSp.addBase("Patient");
|
||||
fooSp.setCode("foo");
|
||||
fooSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
fooSp.setTitle("FOO SP");
|
||||
|
@ -304,6 +310,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv
|
|||
assertEquals(BaseHapiFhirDao.INDEX_STATUS_INDEXED, res.getIndexStatus().longValue());
|
||||
|
||||
SearchParameter fooSp = new SearchParameter();
|
||||
fooSp.addBase("Patient");
|
||||
fooSp.setCode("foo");
|
||||
fooSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
|
||||
fooSp.setTitle("FOO SP");
|
||||
|
@ -324,6 +331,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv
|
|||
public void testSearchQualifiedWithCustomReferenceParam() {
|
||||
|
||||
SearchParameter fooSp = new SearchParameter();
|
||||
fooSp.addBase("Patient");
|
||||
fooSp.setCode("foo");
|
||||
fooSp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.REFERENCE);
|
||||
fooSp.setTitle("FOO SP");
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
mode. This allows users to create search parameters which contain
|
||||
custom paths, or even override and disable existing search
|
||||
parameters.
|
||||
</action>
|
||||
<action type="fix">
|
||||
CLI example uploader couldn't find STU3 examples after CI server
|
||||
was moved to build.fhir.org
|
||||
|
@ -151,6 +152,11 @@
|
|||
being expanded had codes included explicitly (i.e. not by
|
||||
is-a relationship). Thanks to David Hay for reporting!
|
||||
</action>
|
||||
<action type="fix">
|
||||
JPA validator incorrectly returned an HTTP 400 instead of an HTTP 422 when
|
||||
the resource ID was not present and required, or vice versa. Thanks to
|
||||
Brian Postlethwaite for reporting!
|
||||
</action>
|
||||
</release>
|
||||
<release version="2.2" date="2016-12-20">
|
||||
<action type="add">
|
||||
|
|
Loading…
Reference in New Issue