Make sure we don't update unchanged resources in a transaction
This commit is contained in:
parent
d550392047
commit
20c14fe8a6
|
@ -1508,6 +1508,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
|
|||
if (theResource != null) {
|
||||
populateResourceIdFromEntity(theEntity, theResource);
|
||||
}
|
||||
theEntity.setUnchangedInCurrentOperation(true);
|
||||
return theEntity;
|
||||
}
|
||||
|
||||
|
|
|
@ -1127,7 +1127,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
* we'll manually increase the version. This is important because we want the updated version number
|
||||
* to be reflected in the resource shared with interceptors
|
||||
*/
|
||||
if (!thePerformIndexing) {
|
||||
if (!thePerformIndexing && !savedEntity.isUnchangedInCurrentOperation()) {
|
||||
if (resourceId.hasVersionIdPart() == false) {
|
||||
resourceId = resourceId.withVersion(Long.toString(savedEntity.getVersion()));
|
||||
}
|
||||
|
|
|
@ -242,6 +242,9 @@ public class ResourceTable extends BaseHasResource implements Serializable {
|
|||
@OneToMany(mappedBy = "myResource", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
|
||||
private Set<ResourceTag> myTags;
|
||||
|
||||
@Transient
|
||||
private transient boolean myUnchangedInCurrentOperation;
|
||||
|
||||
@Column(name = "RES_VER")
|
||||
private long myVersion;
|
||||
|
||||
|
@ -398,6 +401,14 @@ public class ResourceTable extends BaseHasResource implements Serializable {
|
|||
return myParamsUriPopulated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transient (not saved in DB) flag indicating that this resource was found to be unchanged by the current operation
|
||||
* and was not re-saved in the database
|
||||
*/
|
||||
public boolean isUnchangedInCurrentOperation() {
|
||||
return myUnchangedInCurrentOperation;
|
||||
}
|
||||
|
||||
public void setContentTextParsedIntoWords(String theContentText) {
|
||||
myContentText = theContentText;
|
||||
}
|
||||
|
@ -532,6 +543,14 @@ public class ResourceTable extends BaseHasResource implements Serializable {
|
|||
myResourceType = theResourceType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transient (not saved in DB) flag indicating that this resource was found to be unchanged by the current operation
|
||||
* and was not re-saved in the database
|
||||
*/
|
||||
public void setUnchangedInCurrentOperation(boolean theUnchangedInCurrentOperation) {
|
||||
myUnchangedInCurrentOperation = theUnchangedInCurrentOperation;
|
||||
}
|
||||
|
||||
public void setVersion(long theVersion) {
|
||||
myVersion = theVersion;
|
||||
}
|
||||
|
|
|
@ -23,23 +23,59 @@ import static org.mockito.Mockito.reset;
|
|||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.hamcrest.core.StringContains;
|
||||
import org.hl7.fhir.dstu3.model.*;
|
||||
import org.hl7.fhir.dstu3.model.Age;
|
||||
import org.hl7.fhir.dstu3.model.Bundle;
|
||||
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
|
||||
import org.hl7.fhir.dstu3.model.Bundle.BundleType;
|
||||
import org.hl7.fhir.dstu3.model.Bundle.HTTPVerb;
|
||||
import org.hl7.fhir.dstu3.model.CarePlan;
|
||||
import org.hl7.fhir.dstu3.model.CodeSystem;
|
||||
import org.hl7.fhir.dstu3.model.CodeType;
|
||||
import org.hl7.fhir.dstu3.model.CodeableConcept;
|
||||
import org.hl7.fhir.dstu3.model.Coding;
|
||||
import org.hl7.fhir.dstu3.model.CompartmentDefinition;
|
||||
import org.hl7.fhir.dstu3.model.ConceptMap;
|
||||
import org.hl7.fhir.dstu3.model.Condition;
|
||||
import org.hl7.fhir.dstu3.model.DateTimeType;
|
||||
import org.hl7.fhir.dstu3.model.DateType;
|
||||
import org.hl7.fhir.dstu3.model.Device;
|
||||
import org.hl7.fhir.dstu3.model.DiagnosticReport;
|
||||
import org.hl7.fhir.dstu3.model.Encounter;
|
||||
import org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender;
|
||||
import org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus;
|
||||
import org.hl7.fhir.dstu3.model.IdType;
|
||||
import org.hl7.fhir.dstu3.model.Meta;
|
||||
import org.hl7.fhir.dstu3.model.NamingSystem;
|
||||
import org.hl7.fhir.dstu3.model.Observation;
|
||||
import org.hl7.fhir.dstu3.model.Observation.ObservationStatus;
|
||||
import org.hl7.fhir.dstu3.model.OperationDefinition;
|
||||
import org.hl7.fhir.dstu3.model.OperationOutcome;
|
||||
import org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity;
|
||||
import org.hl7.fhir.dstu3.model.OperationOutcome.IssueType;
|
||||
import org.hl7.fhir.dstu3.model.Organization;
|
||||
import org.hl7.fhir.dstu3.model.Patient;
|
||||
import org.hl7.fhir.dstu3.model.Period;
|
||||
import org.hl7.fhir.dstu3.model.Quantity;
|
||||
import org.hl7.fhir.dstu3.model.Quantity.QuantityComparator;
|
||||
import org.hl7.fhir.dstu3.model.Questionnaire;
|
||||
import org.hl7.fhir.dstu3.model.Range;
|
||||
import org.hl7.fhir.dstu3.model.Reference;
|
||||
import org.hl7.fhir.dstu3.model.SimpleQuantity;
|
||||
import org.hl7.fhir.dstu3.model.StringType;
|
||||
import org.hl7.fhir.dstu3.model.StructureDefinition;
|
||||
import org.hl7.fhir.dstu3.model.Timing;
|
||||
import org.hl7.fhir.dstu3.model.UriType;
|
||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
@ -49,14 +85,22 @@ import org.junit.Before;
|
|||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
import org.springframework.transaction.support.*;
|
||||
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import ca.uhn.fhir.jpa.dao.*;
|
||||
import ca.uhn.fhir.jpa.entity.*;
|
||||
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
|
||||
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.entity.ResourceEncodingEnum;
|
||||
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString;
|
||||
import ca.uhn.fhir.jpa.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.entity.TagTypeEnum;
|
||||
import ca.uhn.fhir.model.api.Include;
|
||||
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
|
||||
import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum;
|
||||
|
@ -67,10 +111,21 @@ import ca.uhn.fhir.rest.api.MethodOutcome;
|
|||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||
import ca.uhn.fhir.rest.api.SortOrderEnum;
|
||||
import ca.uhn.fhir.rest.api.SortSpec;
|
||||
import ca.uhn.fhir.rest.param.*;
|
||||
import ca.uhn.fhir.rest.param.DateParam;
|
||||
import ca.uhn.fhir.rest.param.DateRangeParam;
|
||||
import ca.uhn.fhir.rest.param.QuantityParam;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import ca.uhn.fhir.rest.param.StringParam;
|
||||
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import ca.uhn.fhir.rest.server.Constants;
|
||||
import ca.uhn.fhir.rest.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.server.exceptions.*;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
|
||||
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.ActionRequestDetails;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
|
||||
|
@ -258,7 +313,7 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test {
|
|||
final IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless();
|
||||
|
||||
TransactionTemplate tx = new TransactionTemplate(myTxManager);
|
||||
tx.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW);
|
||||
tx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||
tx.execute(new TransactionCallbackWithoutResult() {
|
||||
@Override
|
||||
protected void doInTransactionWithoutResult(TransactionStatus theStatus) {
|
||||
|
@ -593,15 +648,15 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test {
|
|||
* If any of this ever fails, it means that one of the OperationOutcome issue severity codes has changed code value across versions. We store the string as a constant, so something will need to
|
||||
* be fixed.
|
||||
*/
|
||||
assertEquals(org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity.ERROR.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_ERROR);
|
||||
assertEquals(ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum.ERROR.getCode(), BaseHapiFhirResourceDao.OO_SEVERITY_ERROR);
|
||||
assertEquals(org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity.ERROR.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_ERROR);
|
||||
assertEquals(org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity.INFORMATION.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_INFO);
|
||||
assertEquals(ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum.INFORMATION.getCode(), BaseHapiFhirResourceDao.OO_SEVERITY_INFO);
|
||||
assertEquals(org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity.INFORMATION.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_INFO);
|
||||
assertEquals(org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity.WARNING.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_WARN);
|
||||
assertEquals(ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum.WARNING.getCode(), BaseHapiFhirResourceDao.OO_SEVERITY_WARN);
|
||||
assertEquals(org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity.WARNING.toCode(), BaseHapiFhirResourceDao.OO_SEVERITY_WARN);
|
||||
assertEquals(org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity.ERROR.toCode(), BaseHapiFhirDao.OO_SEVERITY_ERROR);
|
||||
assertEquals(ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum.ERROR.getCode(), BaseHapiFhirDao.OO_SEVERITY_ERROR);
|
||||
assertEquals(org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity.ERROR.toCode(), BaseHapiFhirDao.OO_SEVERITY_ERROR);
|
||||
assertEquals(org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity.INFORMATION.toCode(), BaseHapiFhirDao.OO_SEVERITY_INFO);
|
||||
assertEquals(ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum.INFORMATION.getCode(), BaseHapiFhirDao.OO_SEVERITY_INFO);
|
||||
assertEquals(org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity.INFORMATION.toCode(), BaseHapiFhirDao.OO_SEVERITY_INFO);
|
||||
assertEquals(org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity.WARNING.toCode(), BaseHapiFhirDao.OO_SEVERITY_WARN);
|
||||
assertEquals(ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum.WARNING.getCode(), BaseHapiFhirDao.OO_SEVERITY_WARN);
|
||||
assertEquals(org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity.WARNING.toCode(), BaseHapiFhirDao.OO_SEVERITY_WARN);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -2010,7 +2065,7 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test {
|
|||
* See #534
|
||||
*/
|
||||
@Test
|
||||
public void testLogicalReferencesAreSearchable() throws IOException {
|
||||
public void testLogicalReferencesAreSearchable() {
|
||||
myDaoConfig.setTreatReferencesAsLogical(null);
|
||||
myDaoConfig.addTreatReferencesAsLogical("http://foo.com/identifier*");
|
||||
|
||||
|
@ -2288,7 +2343,7 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testReadForcedIdVersionHistory() throws InterruptedException {
|
||||
public void testReadForcedIdVersionHistory() {
|
||||
Patient p1 = new Patient();
|
||||
p1.addIdentifier().setSystem("urn:system").setValue("testReadVorcedIdVersionHistory01");
|
||||
p1.setId("testReadVorcedIdVersionHistory");
|
||||
|
@ -2809,21 +2864,21 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test {
|
|||
|
||||
pm = new SearchParameterMap();
|
||||
pm.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", methodName));
|
||||
pm.setSort(new SortSpec(BaseResource.SP_RES_ID));
|
||||
pm.setSort(new SortSpec(IAnyResource.SP_RES_ID));
|
||||
actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm));
|
||||
assertEquals(5, actual.size());
|
||||
assertThat(actual, contains(idMethodName, id1, id2, id3, id4));
|
||||
|
||||
pm = new SearchParameterMap();
|
||||
pm.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", methodName));
|
||||
pm.setSort(new SortSpec(BaseResource.SP_RES_ID).setOrder(SortOrderEnum.ASC));
|
||||
pm.setSort(new SortSpec(IAnyResource.SP_RES_ID).setOrder(SortOrderEnum.ASC));
|
||||
actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm));
|
||||
assertEquals(5, actual.size());
|
||||
assertThat(actual, contains(idMethodName, id1, id2, id3, id4));
|
||||
|
||||
pm = new SearchParameterMap();
|
||||
pm.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", methodName));
|
||||
pm.setSort(new SortSpec(BaseResource.SP_RES_ID).setOrder(SortOrderEnum.DESC));
|
||||
pm.setSort(new SortSpec(IAnyResource.SP_RES_ID).setOrder(SortOrderEnum.DESC));
|
||||
actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm));
|
||||
assertEquals(5, actual.size());
|
||||
assertThat(actual, contains(id4, id3, id2, id1, idMethodName));
|
||||
|
@ -3218,13 +3273,13 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test {
|
|||
SearchParameterMap map;
|
||||
|
||||
map = new SearchParameterMap();
|
||||
map.add(BaseResource.SP_RES_ID, new StringParam(id1.getIdPart()));
|
||||
map.add(IAnyResource.SP_RES_ID, new StringParam(id1.getIdPart()));
|
||||
map.setLastUpdated(new DateRangeParam("2001", "2003"));
|
||||
map.setSort(new SortSpec(Constants.PARAM_LASTUPDATED));
|
||||
assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), empty());
|
||||
|
||||
map = new SearchParameterMap();
|
||||
map.add(BaseResource.SP_RES_ID, new StringParam(id1.getIdPart()));
|
||||
map.add(IAnyResource.SP_RES_ID, new StringParam(id1.getIdPart()));
|
||||
map.setLastUpdated(new DateRangeParam("2001", "2003"));
|
||||
map.setSort(new SortSpec(Patient.SP_NAME));
|
||||
assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), empty());
|
||||
|
@ -3361,7 +3416,7 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test {
|
|||
assertEquals("Kittens", published.get(1).getDisplay());
|
||||
assertEquals("http://foo", published.get(1).getSystem());
|
||||
|
||||
secLabels = (ArrayList<Coding>) retrieved.getMeta().getSecurity();
|
||||
secLabels = retrieved.getMeta().getSecurity();
|
||||
sortCodings(secLabels);
|
||||
assertEquals(2, secLabels.size());
|
||||
assertEquals("seclabel:sys:1", secLabels.get(0).getSystemElement().getValue());
|
||||
|
|
|
@ -46,19 +46,130 @@ import ca.uhn.fhir.rest.param.StringParam;
|
|||
import ca.uhn.fhir.rest.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
|
||||
public class FhirResourceDaoDstu3UpdateTest extends BaseJpaDstu3Test {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu3UpdateTest.class);
|
||||
|
||||
@AfterClass
|
||||
public static void afterClassClearContext() {
|
||||
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||
@Test
|
||||
public void testCreateAndUpdateWithoutRequest() throws Exception {
|
||||
String methodName = "testUpdateByUrl";
|
||||
|
||||
Patient p = new Patient();
|
||||
p.addIdentifier().setSystem("urn:system").setValue(methodName + "2");
|
||||
IIdType id = myPatientDao.create(p).getId().toUnqualified();
|
||||
|
||||
p = new Patient();
|
||||
p.addIdentifier().setSystem("urn:system").setValue(methodName + "2");
|
||||
p.setActive(true);
|
||||
IIdType id2 = myPatientDao.create(p, "Patient?identifier=urn:system|" + methodName + "2").getId().toUnqualified();
|
||||
assertEquals(id.getValue(), id2.getValue());
|
||||
|
||||
p = new Patient();
|
||||
p.setId(id);
|
||||
p.addIdentifier().setSystem("urn:system").setValue(methodName + "2");
|
||||
p.setActive(false);
|
||||
myPatientDao.update(p).getId();
|
||||
|
||||
p.setActive(true);
|
||||
id2 = myPatientDao.update(p, "Patient?identifier=urn:system|" + methodName + "2").getId().toUnqualified();
|
||||
assertEquals(id.getIdPart(), id2.getIdPart());
|
||||
assertEquals("3", id2.getVersionIdPart());
|
||||
|
||||
Patient newPatient = myPatientDao.read(id);
|
||||
assertEquals("1", newPatient.getIdElement().getVersionIdPart());
|
||||
|
||||
newPatient = myPatientDao.read(id.toVersionless());
|
||||
assertEquals("3", newPatient.getIdElement().getVersionIdPart());
|
||||
|
||||
myPatientDao.delete(id.toVersionless());
|
||||
|
||||
try {
|
||||
myPatientDao.read(id.toVersionless());
|
||||
fail();
|
||||
} catch (ResourceGoneException e) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDuplicateProfilesIgnored() {
|
||||
String name = "testDuplicateProfilesIgnored";
|
||||
IIdType id;
|
||||
{
|
||||
Patient patient = new Patient();
|
||||
patient.addName().setFamily(name);
|
||||
|
||||
List<IdType> tl = new ArrayList<IdType>();
|
||||
tl.add(new IdType("http://foo/bar"));
|
||||
tl.add(new IdType("http://foo/bar"));
|
||||
tl.add(new IdType("http://foo/bar"));
|
||||
patient.getMeta().getProfile().addAll(tl);
|
||||
|
||||
id = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
|
||||
// Do a read
|
||||
{
|
||||
Patient patient = myPatientDao.read(id, mySrd);
|
||||
List<UriType> tl = patient.getMeta().getProfile();
|
||||
assertEquals(1, tl.size());
|
||||
assertEquals("http://foo/bar", tl.get(0).getValue());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleUpdatesWithNoChangesDoesNotResultInAnUpdateForDiscreteUpdates() {
|
||||
|
||||
// First time
|
||||
Patient p = new Patient();
|
||||
p.setActive(true);
|
||||
p.setId("Patient/A");
|
||||
String id = myPatientDao.update(p).getId().getValue();
|
||||
assertThat(id, endsWith("Patient/A/_history/1"));
|
||||
|
||||
// Second time should not result in an update
|
||||
p = new Patient();
|
||||
p.setActive(true);
|
||||
p.setId("Patient/A");
|
||||
id = myPatientDao.update(p).getId().getValue();
|
||||
assertThat(id, endsWith("Patient/A/_history/1"));
|
||||
|
||||
// And third time should not result in an update
|
||||
p = new Patient();
|
||||
p.setActive(true);
|
||||
p.setId("Patient/A");
|
||||
id = myPatientDao.update(p).getId().getValue();
|
||||
assertThat(id, endsWith("Patient/A/_history/1"));
|
||||
|
||||
myPatientDao.read(new IdType("Patient/A"));
|
||||
myPatientDao.read(new IdType("Patient/A/_history/1"));
|
||||
try {
|
||||
myPatientDao.read(new IdType("Patient/A/_history/2"));
|
||||
fail();
|
||||
} catch (ResourceNotFoundException e) {
|
||||
// good
|
||||
}
|
||||
try {
|
||||
myPatientDao.read(new IdType("Patient/A/_history/3"));
|
||||
fail();
|
||||
} catch (ResourceNotFoundException e) {
|
||||
// good
|
||||
}
|
||||
|
||||
// Create one more
|
||||
p = new Patient();
|
||||
p.setActive(false);
|
||||
p.setId("Patient/A");
|
||||
id = myPatientDao.update(p).getId().getValue();
|
||||
assertThat(id, endsWith("Patient/A/_history/2"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateAndGetHistoryResource() throws InterruptedException {
|
||||
|
@ -154,49 +265,6 @@ public class FhirResourceDaoDstu3UpdateTest extends BaseJpaDstu3Test {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateAndUpdateWithoutRequest() throws Exception {
|
||||
String methodName = "testUpdateByUrl";
|
||||
|
||||
Patient p = new Patient();
|
||||
p.addIdentifier().setSystem("urn:system").setValue(methodName + "2");
|
||||
IIdType id = myPatientDao.create(p).getId().toUnqualified();
|
||||
|
||||
p = new Patient();
|
||||
p.addIdentifier().setSystem("urn:system").setValue(methodName + "2");
|
||||
p.setActive(true);
|
||||
IIdType id2 = myPatientDao.create(p, "Patient?identifier=urn:system|" + methodName + "2").getId().toUnqualified();
|
||||
assertEquals(id.getValue(), id2.getValue());
|
||||
|
||||
p = new Patient();
|
||||
p.setId(id);
|
||||
p.addIdentifier().setSystem("urn:system").setValue(methodName + "2");
|
||||
p.setActive(false);
|
||||
myPatientDao.update(p).getId();
|
||||
|
||||
p.setActive(true);
|
||||
id2 = myPatientDao.update(p, "Patient?identifier=urn:system|" + methodName + "2").getId().toUnqualified();
|
||||
assertEquals(id.getIdPart(), id2.getIdPart());
|
||||
assertEquals("3", id2.getVersionIdPart());
|
||||
|
||||
Patient newPatient = myPatientDao.read(id);
|
||||
assertEquals("1", newPatient.getIdElement().getVersionIdPart());
|
||||
|
||||
newPatient = myPatientDao.read(id.toVersionless());
|
||||
assertEquals("3", newPatient.getIdElement().getVersionIdPart());
|
||||
|
||||
myPatientDao.delete(id.toVersionless());
|
||||
|
||||
try {
|
||||
myPatientDao.read(id.toVersionless());
|
||||
fail();
|
||||
} catch (ResourceGoneException e) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUpdateConditionalByLastUpdated() throws Exception {
|
||||
String methodName = "testUpdateByUrl";
|
||||
|
@ -296,11 +364,66 @@ public class FhirResourceDaoDstu3UpdateTest extends BaseJpaDstu3Test {
|
|||
myPatientDao.update(p, mySrd);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testUpdateIgnoresIdenticalVersions() {
|
||||
String methodName = "testUpdateIgnoresIdenticalVersions";
|
||||
|
||||
Patient p1 = new Patient();
|
||||
p1.addIdentifier().setSystem("urn:system").setValue(methodName);
|
||||
p1.addName().setFamily("Tester").addGiven(methodName);
|
||||
IIdType p1id = myPatientDao.create(p1, mySrd).getId();
|
||||
|
||||
IIdType p1id2 = myPatientDao.update(p1, mySrd).getId();
|
||||
assertEquals(p1id.getValue(), p1id2.getValue());
|
||||
|
||||
p1.addName().addGiven("NewGiven");
|
||||
IIdType p1id3 = myPatientDao.update(p1, mySrd).getId();
|
||||
assertNotEquals(p1id.getValue(), p1id3.getValue());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateMaintainsSearchParams() {
|
||||
Patient p1 = new Patient();
|
||||
p1.addIdentifier().setSystem("urn:system").setValue("testUpdateMaintainsSearchParamsDstu2AAA");
|
||||
p1.addName().setFamily("Tester").addGiven("testUpdateMaintainsSearchParamsDstu2AAA");
|
||||
IIdType p1id = myPatientDao.create(p1, mySrd).getId();
|
||||
|
||||
Patient p2 = new Patient();
|
||||
p2.addIdentifier().setSystem("urn:system").setValue("testUpdateMaintainsSearchParamsDstu2BBB");
|
||||
p2.addName().setFamily("Tester").addGiven("testUpdateMaintainsSearchParamsDstu2BBB");
|
||||
myPatientDao.create(p2, mySrd).getId();
|
||||
|
||||
Set<Long> ids = myPatientDao.searchForIds(new SearchParameterMap(Patient.SP_GIVEN, new StringParam("testUpdateMaintainsSearchParamsDstu2AAA")));
|
||||
assertEquals(1, ids.size());
|
||||
assertThat(ids, contains(p1id.getIdPartAsLong()));
|
||||
|
||||
// Update the name
|
||||
p1.getName().get(0).getGiven().get(0).setValue("testUpdateMaintainsSearchParamsDstu2BBB");
|
||||
MethodOutcome update2 = myPatientDao.update(p1, mySrd);
|
||||
IIdType p1id2 = update2.getId();
|
||||
|
||||
ids = myPatientDao.searchForIds(new SearchParameterMap(Patient.SP_GIVEN, new StringParam("testUpdateMaintainsSearchParamsDstu2AAA")));
|
||||
assertEquals(0, ids.size());
|
||||
|
||||
ids = myPatientDao.searchForIds(new SearchParameterMap(Patient.SP_GIVEN, new StringParam("testUpdateMaintainsSearchParamsDstu2BBB")));
|
||||
assertEquals(2, ids.size());
|
||||
|
||||
// Make sure vreads work
|
||||
p1 = myPatientDao.read(p1id, mySrd);
|
||||
assertEquals("testUpdateMaintainsSearchParamsDstu2AAA", p1.getName().get(0).getGivenAsSingleString());
|
||||
|
||||
p1 = myPatientDao.read(p1id2, mySrd);
|
||||
assertEquals("testUpdateMaintainsSearchParamsDstu2BBB", p1.getName().get(0).getGivenAsSingleString());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Per the spec, update should preserve tags and security labels but not profiles
|
||||
*/
|
||||
@Test
|
||||
public void testUpdateMaintainsTagsAndSecurityLabels() throws InterruptedException {
|
||||
public void testUpdateMaintainsTagsAndSecurityLabels() {
|
||||
String methodName = "testUpdateMaintainsTagsAndSecurityLabels";
|
||||
|
||||
IIdType p1id;
|
||||
|
@ -345,115 +468,6 @@ public class FhirResourceDaoDstu3UpdateTest extends BaseJpaDstu3Test {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateMaintainsSearchParams() throws InterruptedException {
|
||||
Patient p1 = new Patient();
|
||||
p1.addIdentifier().setSystem("urn:system").setValue("testUpdateMaintainsSearchParamsDstu2AAA");
|
||||
p1.addName().setFamily("Tester").addGiven("testUpdateMaintainsSearchParamsDstu2AAA");
|
||||
IIdType p1id = myPatientDao.create(p1, mySrd).getId();
|
||||
|
||||
Patient p2 = new Patient();
|
||||
p2.addIdentifier().setSystem("urn:system").setValue("testUpdateMaintainsSearchParamsDstu2BBB");
|
||||
p2.addName().setFamily("Tester").addGiven("testUpdateMaintainsSearchParamsDstu2BBB");
|
||||
myPatientDao.create(p2, mySrd).getId();
|
||||
|
||||
Set<Long> ids = myPatientDao.searchForIds(new SearchParameterMap(Patient.SP_GIVEN, new StringParam("testUpdateMaintainsSearchParamsDstu2AAA")));
|
||||
assertEquals(1, ids.size());
|
||||
assertThat(ids, contains(p1id.getIdPartAsLong()));
|
||||
|
||||
// Update the name
|
||||
p1.getName().get(0).getGiven().get(0).setValue("testUpdateMaintainsSearchParamsDstu2BBB");
|
||||
MethodOutcome update2 = myPatientDao.update(p1, mySrd);
|
||||
IIdType p1id2 = update2.getId();
|
||||
|
||||
ids = myPatientDao.searchForIds(new SearchParameterMap(Patient.SP_GIVEN, new StringParam("testUpdateMaintainsSearchParamsDstu2AAA")));
|
||||
assertEquals(0, ids.size());
|
||||
|
||||
ids = myPatientDao.searchForIds(new SearchParameterMap(Patient.SP_GIVEN, new StringParam("testUpdateMaintainsSearchParamsDstu2BBB")));
|
||||
assertEquals(2, ids.size());
|
||||
|
||||
// Make sure vreads work
|
||||
p1 = myPatientDao.read(p1id, mySrd);
|
||||
assertEquals("testUpdateMaintainsSearchParamsDstu2AAA", p1.getName().get(0).getGivenAsSingleString());
|
||||
|
||||
p1 = myPatientDao.read(p1id2, mySrd);
|
||||
assertEquals("testUpdateMaintainsSearchParamsDstu2BBB", p1.getName().get(0).getGivenAsSingleString());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateRejectsInvalidTypes() throws InterruptedException {
|
||||
Patient p1 = new Patient();
|
||||
p1.addIdentifier().setSystem("urn:system").setValue("testUpdateRejectsInvalidTypes");
|
||||
p1.addName().setFamily("Tester").addGiven("testUpdateRejectsInvalidTypes");
|
||||
IIdType p1id = myPatientDao.create(p1, mySrd).getId();
|
||||
|
||||
Organization p2 = new Organization();
|
||||
p2.getNameElement().setValue("testUpdateRejectsInvalidTypes");
|
||||
try {
|
||||
p2.setId(new IdType("Organization/" + p1id.getIdPart()));
|
||||
myOrganizationDao.update(p2, mySrd);
|
||||
fail();
|
||||
} catch (UnprocessableEntityException e) {
|
||||
// good
|
||||
}
|
||||
|
||||
try {
|
||||
p2.setId(new IdType("Patient/" + p1id.getIdPart()));
|
||||
myOrganizationDao.update(p2, mySrd);
|
||||
fail();
|
||||
} catch (UnprocessableEntityException e) {
|
||||
ourLog.error("Good", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testUpdateIgnoresIdenticalVersions() throws InterruptedException {
|
||||
String methodName = "testUpdateIgnoresIdenticalVersions";
|
||||
|
||||
Patient p1 = new Patient();
|
||||
p1.addIdentifier().setSystem("urn:system").setValue(methodName);
|
||||
p1.addName().setFamily("Tester").addGiven(methodName);
|
||||
IIdType p1id = myPatientDao.create(p1, mySrd).getId();
|
||||
|
||||
IIdType p1id2 = myPatientDao.update(p1, mySrd).getId();
|
||||
assertEquals(p1id.getValue(), p1id2.getValue());
|
||||
|
||||
p1.addName().addGiven("NewGiven");
|
||||
IIdType p1id3 = myPatientDao.update(p1, mySrd).getId();
|
||||
assertNotEquals(p1id.getValue(), p1id3.getValue());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDuplicateProfilesIgnored() {
|
||||
String name = "testDuplicateProfilesIgnored";
|
||||
IIdType id;
|
||||
{
|
||||
Patient patient = new Patient();
|
||||
patient.addName().setFamily(name);
|
||||
|
||||
List<IdType> tl = new ArrayList<IdType>();
|
||||
tl.add(new IdType("http://foo/bar"));
|
||||
tl.add(new IdType("http://foo/bar"));
|
||||
tl.add(new IdType("http://foo/bar"));
|
||||
patient.getMeta().getProfile().addAll(tl);
|
||||
|
||||
id = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
|
||||
// Do a read
|
||||
{
|
||||
Patient patient = myPatientDao.read(id, mySrd);
|
||||
List<UriType> tl = patient.getMeta().getProfile();
|
||||
assertEquals(1, tl.size());
|
||||
assertEquals("http://foo/bar", tl.get(0).getValue());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateModifiesProfiles() {
|
||||
String name = "testUpdateModifiesProfiles";
|
||||
|
@ -501,6 +515,33 @@ public class FhirResourceDaoDstu3UpdateTest extends BaseJpaDstu3Test {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateRejectsInvalidTypes() {
|
||||
Patient p1 = new Patient();
|
||||
p1.addIdentifier().setSystem("urn:system").setValue("testUpdateRejectsInvalidTypes");
|
||||
p1.addName().setFamily("Tester").addGiven("testUpdateRejectsInvalidTypes");
|
||||
IIdType p1id = myPatientDao.create(p1, mySrd).getId();
|
||||
|
||||
Organization p2 = new Organization();
|
||||
p2.getNameElement().setValue("testUpdateRejectsInvalidTypes");
|
||||
try {
|
||||
p2.setId(new IdType("Organization/" + p1id.getIdPart()));
|
||||
myOrganizationDao.update(p2, mySrd);
|
||||
fail();
|
||||
} catch (UnprocessableEntityException e) {
|
||||
// good
|
||||
}
|
||||
|
||||
try {
|
||||
p2.setId(new IdType("Patient/" + p1id.getIdPart()));
|
||||
myOrganizationDao.update(p2, mySrd);
|
||||
fail();
|
||||
} catch (UnprocessableEntityException e) {
|
||||
ourLog.error("Good", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateUnknownNumericIdFails() {
|
||||
Patient p = new Patient();
|
||||
|
@ -529,59 +570,6 @@ public class FhirResourceDaoDstu3UpdateTest extends BaseJpaDstu3Test {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateWithNumericIdFails() {
|
||||
Patient p = new Patient();
|
||||
p.addIdentifier().setSystem("urn:system").setValue("testCreateNumericIdFails");
|
||||
p.addName().setFamily("Hello");
|
||||
p.setId("Patient/123");
|
||||
try {
|
||||
myPatientDao.update(p, mySrd);
|
||||
fail();
|
||||
} catch (InvalidRequestException e) {
|
||||
assertThat(e.getMessage(), containsString("clients may only assign IDs which contain at least one non-numeric"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateWithNumericThenTextIdSucceeds() {
|
||||
Patient p = new Patient();
|
||||
p.addIdentifier().setSystem("urn:system").setValue("testCreateNumericIdFails");
|
||||
p.addName().setFamily("Hello");
|
||||
p.setId("Patient/123abc");
|
||||
IIdType id = myPatientDao.update(p, mySrd).getId();
|
||||
assertEquals("123abc", id.getIdPart());
|
||||
assertEquals("1", id.getVersionIdPart());
|
||||
|
||||
p = myPatientDao.read(id.toUnqualifiedVersionless(), mySrd);
|
||||
assertEquals("Patient/123abc", p.getIdElement().toUnqualifiedVersionless().getValue());
|
||||
assertEquals("Hello", p.getName().get(0).getFamily());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUpdateWithNoChangeDetectionUpdateUnchanged() {
|
||||
String name = "testUpdateUnchanged";
|
||||
IIdType id1, id2;
|
||||
{
|
||||
Patient patient = new Patient();
|
||||
patient.addName().setFamily(name);
|
||||
id1 = myPatientDao.create(patient, mySrd).getId().toUnqualified();
|
||||
}
|
||||
|
||||
// Update
|
||||
{
|
||||
Patient patient = new Patient();
|
||||
patient.setId(id1);
|
||||
patient.addName().setFamily(name);
|
||||
id2 = myPatientDao.update(patient, mySrd).getId().toUnqualified();
|
||||
}
|
||||
|
||||
assertEquals(id1.getValue(), id2.getValue());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUpdateWithNoChangeDetectionDisabledUpdateUnchanged() {
|
||||
myDaoConfig.setSuppressUpdatesWithNoChange(false);
|
||||
|
@ -627,6 +615,38 @@ public class FhirResourceDaoDstu3UpdateTest extends BaseJpaDstu3Test {
|
|||
assertNotEquals(id1.getValue(), id2.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateWithNoChangeDetectionUpdateTagMetaRemoved() {
|
||||
String name = "testUpdateUnchanged";
|
||||
IIdType id1, id2;
|
||||
{
|
||||
Patient patient = new Patient();
|
||||
patient.getMeta().addTag().setCode("CODE");
|
||||
patient.addName().setFamily(name);
|
||||
id1 = myPatientDao.create(patient, mySrd).getId().toUnqualified();
|
||||
}
|
||||
|
||||
Meta meta = new Meta();
|
||||
meta.addTag().setCode("CODE");
|
||||
myPatientDao.metaDeleteOperation(id1, meta, null);
|
||||
|
||||
meta = myPatientDao.metaGetOperation(Meta.class, id1, null);
|
||||
assertEquals(0, meta.getTag().size());
|
||||
|
||||
// Update
|
||||
{
|
||||
Patient patient = new Patient();
|
||||
patient.setId(id1);
|
||||
patient.addName().setFamily(name);
|
||||
id2 = myPatientDao.update(patient, mySrd).getId().toUnqualified();
|
||||
}
|
||||
|
||||
assertEquals(id1.getValue(), id2.getValue());
|
||||
|
||||
meta = myPatientDao.metaGetOperation(Meta.class, id2, null);
|
||||
assertEquals(0, meta.getTag().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateWithNoChangeDetectionUpdateTagNoChange() {
|
||||
String name = "testUpdateUnchanged";
|
||||
|
@ -685,24 +705,15 @@ public class FhirResourceDaoDstu3UpdateTest extends BaseJpaDstu3Test {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateWithNoChangeDetectionUpdateTagMetaRemoved() {
|
||||
public void testUpdateWithNoChangeDetectionUpdateUnchanged() {
|
||||
String name = "testUpdateUnchanged";
|
||||
IIdType id1, id2;
|
||||
{
|
||||
Patient patient = new Patient();
|
||||
patient.getMeta().addTag().setCode("CODE");
|
||||
patient.addName().setFamily(name);
|
||||
id1 = myPatientDao.create(patient, mySrd).getId().toUnqualified();
|
||||
}
|
||||
|
||||
Meta meta = new Meta();
|
||||
meta.addTag().setCode("CODE");
|
||||
myPatientDao.metaDeleteOperation(id1, meta, null);
|
||||
|
||||
|
||||
meta = myPatientDao.metaGetOperation(Meta.class, id1, null);
|
||||
assertEquals(0, meta.getTag().size());
|
||||
|
||||
// Update
|
||||
{
|
||||
Patient patient = new Patient();
|
||||
|
@ -712,9 +723,41 @@ public class FhirResourceDaoDstu3UpdateTest extends BaseJpaDstu3Test {
|
|||
}
|
||||
|
||||
assertEquals(id1.getValue(), id2.getValue());
|
||||
}
|
||||
|
||||
meta = myPatientDao.metaGetOperation(Meta.class, id2, null);
|
||||
assertEquals(0, meta.getTag().size());
|
||||
@Test
|
||||
public void testUpdateWithNumericIdFails() {
|
||||
Patient p = new Patient();
|
||||
p.addIdentifier().setSystem("urn:system").setValue("testCreateNumericIdFails");
|
||||
p.addName().setFamily("Hello");
|
||||
p.setId("Patient/123");
|
||||
try {
|
||||
myPatientDao.update(p, mySrd);
|
||||
fail();
|
||||
} catch (InvalidRequestException e) {
|
||||
assertThat(e.getMessage(), containsString("clients may only assign IDs which contain at least one non-numeric"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateWithNumericThenTextIdSucceeds() {
|
||||
Patient p = new Patient();
|
||||
p.addIdentifier().setSystem("urn:system").setValue("testCreateNumericIdFails");
|
||||
p.addName().setFamily("Hello");
|
||||
p.setId("Patient/123abc");
|
||||
IIdType id = myPatientDao.update(p, mySrd).getId();
|
||||
assertEquals("123abc", id.getIdPart());
|
||||
assertEquals("1", id.getVersionIdPart());
|
||||
|
||||
p = myPatientDao.read(id.toUnqualifiedVersionless(), mySrd);
|
||||
assertEquals("Patient/123abc", p.getIdElement().toUnqualifiedVersionless().getValue());
|
||||
assertEquals("Hello", p.getName().get(0).getFamily());
|
||||
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void afterClassClearContext() {
|
||||
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -36,7 +36,15 @@
|
|||
Search results will be cached and reused (so that if a client
|
||||
does two searches for "get me all patients matching FOO"
|
||||
with the same FOO in short succession, we won't query the DB
|
||||
again but will instead reuse the cached results)
|
||||
again but will instead reuse the cached results). Note that
|
||||
this can improve performance, but does mean that searches can
|
||||
return slightly out of date results. Essentially what this means
|
||||
is that the latest version of individual resources will always
|
||||
be returned despite this cacheing, but newly created resources
|
||||
that should match may not be returned until the cache
|
||||
expires. By default this cache has been set to one minute,
|
||||
which should be acceptable for most real-world usage, but
|
||||
this can be changed or disabled entirely.
|
||||
</li>
|
||||
<li>
|
||||
Updates which do not actually change the contents of the resource
|
||||
|
@ -50,7 +58,8 @@
|
|||
<code>HFJ_SEARCH_INCLUDE</code>,
|
||||
and
|
||||
<code>HFJ_SEARCH_RESULT</code>
|
||||
tables from your database before upgrading.
|
||||
tables from your database before upgrading, as the structure of these tables
|
||||
has changed and old search results can not be reused.
|
||||
]]>
|
||||
</action>
|
||||
<action type="fix" issue="590">
|
||||
|
|
Loading…
Reference in New Issue