From 938a251ae9aa1849bd676dfc0199445d16648702 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Mon, 19 Oct 2015 20:19:40 -0400 Subject: [PATCH] Cleanup tests for java config in JPA --- .../jpa/dao/FhirResourceDaoValueSetDstu2.java | 114 +- .../jpa/dao/IFhirResourceDaoValueSet.java | 70 +- .../BaseJpaResourceProviderValueSetDstu2.java | 40 + .../jpa/config/DispatcherServletConfig.java | 8 + .../BaseResourceProviderDstu2Test.java | 5 +- .../provider/ResourceProviderDstu1Test.java | 15 +- .../ResourceProviderDstu2ValueSetTest.java | 143 +- .../ResourceProviderMultiVersionTest.java | 205 --- .../jpa/provider/SystemProviderDstu1Test.java | 6 +- .../jpa/provider/SystemProviderDstu2Test.java | 7 +- .../ca/uhn/fhir/jpa/demo/ExampleServerIT.java | 6 +- .../java/ca/uhn/fhirtest/CORSFilter_.java | 1157 ++++++++++++++++ .../src/main/webapp/WEB-INF/web.xml | 2 +- .../ca/uhn/fhir/rest/server/CORSFilter_.java | 1176 +++++++++++++++++ .../ca/uhn/fhir/rest/server/CorsTest.java | 151 ++- .../fhir/instance/model/PrimitiveType.java | 2 +- .../java/ca/uhn/fhir/model/PrimititeTest.java | 22 + ...otationMethodHandlerAdapterConfigurer.java | 3 + pom.xml | 6 +- .../uhn/example/config/FhirTesterConfig.java | 7 + src/changes/changes.xml | 5 + src/site/xdoc/doc_server_tester.xml.vm | 72 +- 22 files changed, 2881 insertions(+), 341 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/DispatcherServletConfig.java delete mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderMultiVersionTest.java create mode 100755 hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/CORSFilter_.java create mode 100755 hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CORSFilter_.java create mode 100644 hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/model/PrimititeTest.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java index f4d5eed65da..84101af93e0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java @@ -81,14 +81,24 @@ public class FhirResourceDaoValueSetDstu2 extends FhirResourceDaoDstu2 @Override public ValueSet expand(IIdType theId, String theFilter) { + ValueSet source = loadValueSetForExpansion(theId); + return expand(source, theFilter); + + } + + private ValueSet loadValueSetForExpansion(IIdType theId) { + if (theId.getValue().startsWith("http://hl7.org/fhir/")) { + org.hl7.fhir.instance.model.ValueSet valueSet = myValidationSupport.fetchResource(myRiCtx, org.hl7.fhir.instance.model.ValueSet.class, theId.getValue()); + if (valueSet != null) { + return getContext().newJsonParser().parseResource(ValueSet.class, myRiCtx.newJsonParser().encodeResourceToString(valueSet)); + } + } BaseHasResource sourceEntity = readEntity(theId); if (sourceEntity == null) { throw new ResourceNotFoundException(theId); } ValueSet source = (ValueSet) toResource(sourceEntity, false); - - return expand(source, theFilter); - + return source; } @Override @@ -182,7 +192,7 @@ public class FhirResourceDaoValueSetDstu2 extends FhirResourceDaoDstu2 if (!haveCodeableConcept && !haveCoding && !haveCode) { throw new InvalidRequestException("No code, coding, or codeableConcept provided to validate"); } - if (!(haveCodeableConcept ^ haveCoding ^ haveCode)) { + if (!multiXor(haveCodeableConcept, haveCoding, haveCode)) { throw new InvalidRequestException("$validate-code can only validate (system AND code) OR (coding) OR (codeableConcept)"); } @@ -199,11 +209,9 @@ public class FhirResourceDaoValueSetDstu2 extends FhirResourceDaoDstu2 if (theCode == null || theCode.isEmpty()) { throw new InvalidRequestException("Either ValueSet ID or ValueSet identifier or system and code must be provided. Unable to validate."); } - Set ids = searchForIds(ValueSet.SP_CODE, new TokenParam(toStringOrNull(theSystem), theCode.getValue())); - valueSetIds = new ArrayList(); - for (Long next : ids) { - valueSetIds.add(new IdDt("ValueSet", next)); - } + String code = theCode.getValue(); + String system = toStringOrNull(theSystem); + valueSetIds = findValueSetIdsContainingSystemAndCode(code, system); } for (IIdType nextId : valueSetIds) { @@ -223,6 +231,30 @@ public class FhirResourceDaoValueSetDstu2 extends FhirResourceDaoDstu2 return new ValidateCodeResult(false, "Code not found", null); } + private List findValueSetIdsContainingSystemAndCode(String theCode, String theSystem) { + if (theSystem != null && theSystem.startsWith("http://hl7.org/fhir/")) { + return Collections.singletonList((IIdType)new IdDt(theSystem)); + } + + List valueSetIds; + Set ids = searchForIds(ValueSet.SP_CODE, new TokenParam(theSystem, theCode)); + valueSetIds = new ArrayList(); + for (Long next : ids) { + valueSetIds.add(new IdDt("ValueSet", next)); + } + return valueSetIds; + } + + private static boolean multiXor(boolean... theValues) { + int count = 0; + for (int i = 0; i < theValues.length; i++) { + if (theValues[i]) { + count++; + } + } + return count == 1; + } + private String toStringOrNull(IPrimitiveType thePrimitive) { return thePrimitive != null ? thePrimitive.getValue() : null; } @@ -259,4 +291,68 @@ public class FhirResourceDaoValueSetDstu2 extends FhirResourceDaoDstu2 return null; } + @Override + public ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.LookupCodeResult lookupCode(CodeDt theCode, UriDt theSystem, CodingDt theCoding) { + boolean haveCoding = theCoding != null && isNotBlank(theCoding.getSystem()) && isNotBlank(theCoding.getCode()); + boolean haveCode = theCode != null && theCode.isEmpty() == false; + boolean haveSystem = theSystem != null && theSystem.isEmpty() == false; + + if (!haveCoding && !(haveSystem && haveCode)) { + throw new InvalidRequestException("No code, coding, or codeableConcept provided to validate"); + } + if (!multiXor(haveCoding, (haveSystem && haveCode)) || (haveSystem != haveCode)) { + throw new InvalidRequestException("$lookup can only validate (system AND code) OR (coding.system AND coding.code)"); + } + + String code; + String system; + if (haveCoding) { + code = theCoding.getCode(); + system = theCoding.getSystem(); + } else { + code = theCode.getValue(); + system = theSystem.getValue(); + } + + List valueSetIds = findValueSetIdsContainingSystemAndCode(code, system); + for (IIdType nextId : valueSetIds) { + ValueSet expansion = expand(nextId, null); + List contains = expansion.getExpansion().getContains(); + ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.LookupCodeResult result = lookup(contains, system, code); + if (result != null) { + return result; + } + } + + LookupCodeResult retVal = new LookupCodeResult(); + retVal.setFound(false); + retVal.setSearchedForCode(code); + retVal.setSearchedForSystem(system); + return retVal; + } + + private ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.LookupCodeResult lookup(List theContains, String theSystem, String theCode) { + for (ExpansionContains nextCode : theContains) { + + String system = nextCode.getSystem(); + String code = nextCode.getCode(); + if (theSystem.equals(system) && theCode.equals(code)) { + ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.LookupCodeResult retVal = new LookupCodeResult(); + retVal.setSearchedForCode(code); + retVal.setSearchedForSystem(system); + retVal.setFound(true); + if (nextCode.getAbstract() != null) { + retVal.setCodeIsAbstract(nextCode.getAbstract().booleanValue()); + } + retVal.setCodeDisplay(nextCode.getDisplay()); + retVal.setCodeSystemVersion(nextCode.getVersion()); + retVal.setCodeSystemDisplayName("Unknown"); // TODO: implement + return retVal; + } + + } + + return null; + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoValueSet.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoValueSet.java index 488dc99fc0f..7f5b8da255f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoValueSet.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoValueSet.java @@ -33,13 +33,81 @@ import ca.uhn.fhir.model.primitive.UriDt; public interface IFhirResourceDaoValueSet extends IFhirResourceDao { ValueSet expand(IIdType theId, String theFilter); - + ValueSet expand(ValueSet theSource, String theFilter); ValueSet expandByIdentifier(String theUri, String theFilter); + LookupCodeResult lookupCode(CodeDt theCode, UriDt theSystem, CodingDt theCoding); + ValidateCodeResult validateCode(UriDt theValueSetIdentifier, IIdType theId, CodeDt theCode, UriDt theSystem, StringDt theDisplay, CodingDt theCoding, CodeableConceptDt theCodeableConcept); + public class LookupCodeResult { + private String myCodeDisplay; + private boolean myCodeIsAbstract; + private String myCodeSystemDisplayName; + private String myCodeSystemVersion; + private boolean myFound; + private String mySearchedForCode; + private String mySearchedForSystem; + + public String getCodeDisplay() { + return myCodeDisplay; + } + + public String getCodeSystemDisplayName() { + return myCodeSystemDisplayName; + } + + public String getCodeSystemVersion() { + return myCodeSystemVersion; + } + + public String getSearchedForCode() { + return mySearchedForCode; + } + + public String getSearchedForSystem() { + return mySearchedForSystem; + } + + public boolean isCodeIsAbstract() { + return myCodeIsAbstract; + } + + public boolean isFound() { + return myFound; + } + + public void setCodeDisplay(String theCodeDisplay) { + myCodeDisplay = theCodeDisplay; + } + + public void setCodeIsAbstract(boolean theCodeIsAbstract) { + myCodeIsAbstract = theCodeIsAbstract; + } + + public void setCodeSystemDisplayName(String theCodeSystemDisplayName) { + myCodeSystemDisplayName = theCodeSystemDisplayName; + } + + public void setCodeSystemVersion(String theCodeSystemVersion) { + myCodeSystemVersion = theCodeSystemVersion; + } + + public void setFound(boolean theFound) { + myFound = theFound; + } + + public void setSearchedForCode(String theSearchedForCode) { + mySearchedForCode = theSearchedForCode; + } + + public void setSearchedForSystem(String theSearchedForSystem) { + mySearchedForSystem = theSearchedForSystem; + } + } + public class ValidateCodeResult { private String myDisplay; private String myMessage; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java index f97e3054695..4db6b9f3ab4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java @@ -27,6 +27,7 @@ import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang3.BooleanUtils; import ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet; +import ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.LookupCodeResult; import ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.ValidateCodeResult; import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; import ca.uhn.fhir.model.dstu2.composite.CodingDt; @@ -41,6 +42,7 @@ import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; public class BaseJpaResourceProviderValueSetDstu2 extends JpaResourceProviderDstu2 { @@ -102,6 +104,42 @@ public class BaseJpaResourceProviderValueSetDstu2 extends JpaResourceProviderDst return theFilter != null ? theFilter.getValue() : null; } + //@formatter:off + @Operation(name = "$lookup", idempotent = true, returnParameters= { + @OperationParam(name="name", type=StringDt.class, min=1), + @OperationParam(name="version", type=StringDt.class, min=0), + @OperationParam(name="display", type=StringDt.class, min=1), + @OperationParam(name="abstract", type=BooleanDt.class, min=1), + }) + public Parameters lookup( + HttpServletRequest theServletRequest, + @OperationParam(name="code", min=0, max=1) CodeDt theCode, + @OperationParam(name="system", min=0, max=1) UriDt theSystem, + @OperationParam(name="coding", min=0, max=1) CodingDt theCoding + ) { + //@formatter:on + + startRequest(theServletRequest); + try { + IFhirResourceDaoValueSet dao = (IFhirResourceDaoValueSet) getDao(); + LookupCodeResult result = dao.lookupCode(theCode, theSystem, theCoding); + if (result.isFound()==false) { + throw new ResourceNotFoundException("Unable to find code[" + result.getSearchedForCode() + "] in system[" + result.getSearchedForSystem() + "]"); + } + Parameters retVal = new Parameters(); + retVal.addParameter().setName("name").setValue(new StringDt(result.getCodeSystemDisplayName())); + if (isNotBlank(result.getCodeSystemVersion())) { + retVal.addParameter().setName("version").setValue(new StringDt(result.getCodeSystemVersion())); + } + retVal.addParameter().setName("display").setValue(new StringDt(result.getCodeDisplay())); + retVal.addParameter().setName("abstract").setValue(new BooleanDt(result.isCodeIsAbstract())); + return retVal; + } finally { + endRequest(theServletRequest); + } + } + + //@formatter:off @Operation(name = "$validate-code", idempotent = true, returnParameters= { @OperationParam(name="result", type=BooleanDt.class, min=1), @@ -137,4 +175,6 @@ public class BaseJpaResourceProviderValueSetDstu2 extends JpaResourceProviderDst endRequest(theServletRequest); } } + + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/DispatcherServletConfig.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/DispatcherServletConfig.java new file mode 100644 index 00000000000..ab3e2abc8da --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/DispatcherServletConfig.java @@ -0,0 +1,8 @@ +package ca.uhn.fhir.jpa.config; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class DispatcherServletConfig { + //nothing +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java index 10f421d97a2..199fddc8cc2 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java @@ -20,10 +20,13 @@ import org.junit.AfterClass; import org.junit.Before; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.servlet.DispatcherServlet; +import ca.uhn.fhir.jpa.config.DispatcherServletConfig; +import ca.uhn.fhir.jpa.config.TestDstu2Config; import ca.uhn.fhir.jpa.dao.BaseJpaDstu2Test; import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider; import ca.uhn.fhir.model.api.Bundle; @@ -126,7 +129,7 @@ public abstract class BaseResourceProviderDstu2Test extends BaseJpaDstu2Test { DispatcherServlet dispatcherServlet = new DispatcherServlet(); // dispatcherServlet.setApplicationContext(webApplicationContext); - dispatcherServlet.setContextConfigLocation("classpath:/fhir-spring-subscription-config-dstu2.xml"); + dispatcherServlet.setContextClass(AnnotationConfigWebApplicationContext.class); ServletHolder subsServletHolder = new ServletHolder(); subsServletHolder.setServlet(dispatcherServlet); proxyHandler.addServlet(subsServletHolder, "/*"); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu1Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu1Test.java index b388e727ebe..b35d654e6f0 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu1Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu1Test.java @@ -1,7 +1,11 @@ package ca.uhn.fhir.jpa.provider; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.stringContainsInOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; import java.util.ArrayList; import java.util.Date; @@ -21,9 +25,10 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.config.TestDstu1Config; import ca.uhn.fhir.jpa.dao.BaseJpaTest; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; @@ -60,7 +65,7 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; public class ResourceProviderDstu1Test extends BaseJpaTest { - private static ClassPathXmlApplicationContext ourAppCtx; + private static AnnotationConfigApplicationContext ourAppCtx; private static IGenericClient ourClient; private static DaoConfig ourDaoConfig; private static FhirContext ourCtx = FhirContext.forDstu1(); @@ -523,7 +528,7 @@ public class ResourceProviderDstu1Test extends BaseJpaTest { ourServerBase = "http://localhost:" + port + "/fhir/context"; - ourAppCtx = new ClassPathXmlApplicationContext("hapi-fhir-server-resourceproviders-dstu1.xml", "fhir-jpabase-spring-test-config.xml"); + ourAppCtx = new AnnotationConfigApplicationContext(TestDstu1Config.class); ourDaoConfig = (DaoConfig) ourAppCtx.getBean(DaoConfig.class); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2ValueSetTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2ValueSetTest.java index 2ceff26bd2a..c1fbf97c419 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2ValueSetTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2ValueSetTest.java @@ -11,9 +11,11 @@ import java.io.IOException; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.springframework.transaction.annotation.Transactional; +import ca.uhn.fhir.model.dstu2.composite.CodingDt; import ca.uhn.fhir.model.dstu2.resource.Parameters; import ca.uhn.fhir.model.dstu2.resource.ValueSet; import ca.uhn.fhir.model.primitive.BooleanDt; @@ -36,7 +38,7 @@ public class ResourceProviderDstu2ValueSetTest extends BaseResourceProviderDstu2 } @Test - public void testValidateCodeOperationByCodeAndSystemBad() { + public void testValidateCodeOperationByCodeAndSystemInstance() { //@formatter:off Parameters respParam = ourClient .operation() @@ -53,8 +55,147 @@ public class ResourceProviderDstu2ValueSetTest extends BaseResourceProviderDstu2 assertEquals(new BooleanDt(true), respParam.getParameter().get(0).getValue()); } + @Test + public void testValidateCodeOperationByCodeAndSystemType() { + //@formatter:off + Parameters respParam = ourClient + .operation() + .onType(ValueSet.class) + .named("validate-code") + .withParameter(Parameters.class, "code", new CodeDt("8450-9")) + .andParameter("system", new UriDt("http://loinc.org")) + .execute(); + //@formatter:on + String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam); + ourLog.info(resp); + + assertEquals(new BooleanDt(true), respParam.getParameter().get(0).getValue()); + } + + @Test + public void testLookupOperationByCodeAndSystem() { + //@formatter:off + Parameters respParam = ourClient + .operation() + .onType(ValueSet.class) + .named("lookup") + .withParameter(Parameters.class, "code", new CodeDt("8450-9")) + .andParameter("system", new UriDt("http://loinc.org")) + .execute(); + //@formatter:on + + String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam); + ourLog.info(resp); + + assertEquals("name", respParam.getParameter().get(0).getName()); + assertEquals(new StringDt("Unknown"), respParam.getParameter().get(0).getValue()); + assertEquals("display", respParam.getParameter().get(1).getName()); + assertEquals(new StringDt("Systolic blood pressure--expiration"), respParam.getParameter().get(1).getValue()); + assertEquals("abstract", respParam.getParameter().get(2).getName()); + assertEquals(new BooleanDt(false), respParam.getParameter().get(2).getValue()); + } + @Test + @Ignore + public void testLookupOperationForBuiltInCode() { + //@formatter:off + Parameters respParam = ourClient + .operation() + .onType(ValueSet.class) + .named("lookup") + .withParameter(Parameters.class, "code", new CodeDt("M")) + .andParameter("system", new UriDt("http://hl7.org/fhir/v3/MaritalStatus")) + .execute(); + //@formatter:on + + String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam); + ourLog.info(resp); + + assertEquals("name", respParam.getParameter().get(0).getName()); + assertEquals(new StringDt("Unknown"), respParam.getParameter().get(0).getValue()); + assertEquals("display", respParam.getParameter().get(1).getName()); + assertEquals(new StringDt("Married"), respParam.getParameter().get(1).getValue()); + assertEquals("abstract", respParam.getParameter().get(2).getName()); + assertEquals(new BooleanDt(false), respParam.getParameter().get(2).getValue()); + } + + @Test + public void testLookupOperationByCoding() { + //@formatter:off + Parameters respParam = ourClient + .operation() + .onType(ValueSet.class) + .named("lookup") + .withParameter(Parameters.class, "coding", new CodingDt("http://loinc.org", "8450-9")) + .execute(); + //@formatter:on + + String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam); + ourLog.info(resp); + + assertEquals("name", respParam.getParameter().get(0).getName()); + assertEquals(new StringDt("Unknown"), respParam.getParameter().get(0).getValue()); + assertEquals("display", respParam.getParameter().get(1).getName()); + assertEquals(new StringDt("Systolic blood pressure--expiration"), respParam.getParameter().get(1).getValue()); + assertEquals("abstract", respParam.getParameter().get(2).getName()); + assertEquals(new BooleanDt(false), respParam.getParameter().get(2).getValue()); + } + + @Test + public void testLookupOperationByInvalidCombination() { + //@formatter:off + try { + ourClient + .operation() + .onType(ValueSet.class) + .named("lookup") + .withParameter(Parameters.class, "coding", new CodingDt("http://loinc.org", "8450-9")) + .andParameter("code", new CodeDt("8450-9")) + .andParameter("system", new UriDt("http://loinc.org")) + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertEquals("HTTP 400 Bad Request: $lookup can only validate (system AND code) OR (coding.system AND coding.code)", e.getMessage()); + } + //@formatter:on + } + + @Test + public void testLookupOperationByInvalidCombination2() { + //@formatter:off + try { + ourClient + .operation() + .onType(ValueSet.class) + .named("lookup") + .withParameter(Parameters.class, "coding", new CodingDt("http://loinc.org", "8450-9")) + .andParameter("system", new UriDt("http://loinc.org")) + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertEquals("HTTP 400 Bad Request: $lookup can only validate (system AND code) OR (coding.system AND coding.code)", e.getMessage()); + } + //@formatter:on + } + + @Test + public void testLookupOperationByInvalidCombination3() { + //@formatter:off + try { + ourClient + .operation() + .onType(ValueSet.class) + .named("lookup") + .withParameter(Parameters.class, "coding", new CodingDt("http://loinc.org", null)) + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertEquals("HTTP 400 Bad Request: No code, coding, or codeableConcept provided to validate", e.getMessage()); + } + //@formatter:on + } + @Test public void testExpandById() throws IOException { //@formatter:off diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderMultiVersionTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderMultiVersionTest.java deleted file mode 100644 index eedbf544c3c..00000000000 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderMultiVersionTest.java +++ /dev/null @@ -1,205 +0,0 @@ -package ca.uhn.fhir.jpa.provider; - -import static org.junit.Assert.assertEquals; - -import java.util.List; - -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; -import org.springframework.context.support.ClassPathXmlApplicationContext; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.dao.BaseJpaTest; -import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider; -import ca.uhn.fhir.model.api.Bundle; -import ca.uhn.fhir.model.dstu.resource.Patient; -import ca.uhn.fhir.model.dstu.valueset.AdministrativeGenderCodesEnum; -import ca.uhn.fhir.model.dstu2.resource.PaymentNotice; -import ca.uhn.fhir.model.dstu2.valueset.AdministrativeGenderEnum; -import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.model.primitive.StringDt; -import ca.uhn.fhir.rest.client.IGenericClient; -import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; -import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; - -public class ResourceProviderMultiVersionTest extends BaseJpaTest { - - private static ClassPathXmlApplicationContext ourAppCtx; - private static IGenericClient ourClientDstu2; - private static Server ourServer; - private static IGenericClient ourClientDstu1; - - @AfterClass - public static void afterClass() throws Exception { - ourServer.stop(); - ourAppCtx.stop(); - } - - @Test - public void testSubmitPatient() { - Patient p = new Patient(); - p.addIdentifier("urn:MultiFhirVersionTest", "testSubmitPatient01"); - p.addUndeclaredExtension(false, "http://foo#ext1", new StringDt("The value")); - p.getGender().setValueAsEnum(AdministrativeGenderCodesEnum.M); - IdDt id = (IdDt) ourClientDstu1.create().resource(p).execute().getId(); - - // Read back as DSTU1 - Patient patDstu1 = ourClientDstu1.read(Patient.class, id); - assertEquals("testSubmitPatient01", p.getIdentifierFirstRep().getValue().getValue()); - assertEquals(1, patDstu1.getUndeclaredExtensionsByUrl("http://foo#ext1").size()); - assertEquals("M", patDstu1.getGender().getCodingFirstRep().getCode().getValue()); - - // Read back as DEV - ca.uhn.fhir.model.dstu2.resource.Patient patDstu2; - patDstu2 = ourClientDstu2.read(ca.uhn.fhir.model.dstu2.resource.Patient.class, id); - assertEquals("testSubmitPatient01", p.getIdentifierFirstRep().getValue().getValue()); - assertEquals(1, patDstu2.getUndeclaredExtensionsByUrl("http://foo#ext1").size()); - assertEquals(null, patDstu2.getGender()); - - // Search using new bundle format - Bundle bundle = ourClientDstu2.search().forResource(ca.uhn.fhir.model.dstu2.resource.Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode("urn:MultiFhirVersionTest", "testSubmitPatient01")).encodedJson().execute(); - patDstu2 = (ca.uhn.fhir.model.dstu2.resource.Patient) bundle.getEntries().get(0).getResource(); - assertEquals("testSubmitPatient01", p.getIdentifierFirstRep().getValue().getValue()); - assertEquals(1, patDstu2.getUndeclaredExtensionsByUrl("http://foo#ext1").size()); - assertEquals(null, patDstu2.getGender()); - - } - - @Test - public void testSubmitPatientDstu2() { - ca.uhn.fhir.model.dstu2.resource.Patient p = new ca.uhn.fhir.model.dstu2.resource.Patient(); - p.addIdentifier().setSystem("urn:MultiFhirVersionTest").setValue("testSubmitPatientDstu201"); - p.addUndeclaredExtension(false, "http://foo#ext1", new StringDt("The value")); - p.setGender(AdministrativeGenderEnum.MALE); - IdDt id = (IdDt) ourClientDstu2.create().resource(p).execute().getId(); - - // Read back as DSTU1 - Patient patDstu1 = ourClientDstu1.read(Patient.class, id); - assertEquals("testSubmitPatientDstu201", p.getIdentifierFirstRep().getValue()); - assertEquals(1, patDstu1.getUndeclaredExtensionsByUrl("http://foo#ext1").size()); - assertEquals(null, patDstu1.getGender().getCodingFirstRep().getCode().getValue()); - - // Read back as DEV - ca.uhn.fhir.model.dstu2.resource.Patient patDstu2; - patDstu2 = ourClientDstu2.read(ca.uhn.fhir.model.dstu2.resource.Patient.class, id); - assertEquals("testSubmitPatientDstu201", p.getIdentifierFirstRep().getValue()); - assertEquals(1, patDstu2.getUndeclaredExtensionsByUrl("http://foo#ext1").size()); - assertEquals("male", patDstu2.getGender()); - - // Search using new bundle format - Bundle bundle = ourClientDstu2.search().forResource(ca.uhn.fhir.model.dstu2.resource.Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode("urn:MultiFhirVersionTest", "testSubmitPatientDstu201")).encodedJson().execute(); - patDstu2 = (ca.uhn.fhir.model.dstu2.resource.Patient) bundle.getEntries().get(0).getResource(); - assertEquals("testSubmitPatientDstu201", p.getIdentifierFirstRep().getValue()); - assertEquals(1, patDstu2.getUndeclaredExtensionsByUrl("http://foo#ext1").size()); - assertEquals("male", patDstu2.getGender()); - - } - - @SuppressWarnings("deprecation") - @Test - public void testUnknownResourceType() { - ca.uhn.fhir.model.dstu2.resource.Patient p = new ca.uhn.fhir.model.dstu2.resource.Patient(); - p.addIdentifier().setSystem("urn:MultiFhirVersionTest").setValue("testUnknownResourceType01"); - IdDt id = (IdDt) ourClientDstu2.create().resource(p).execute().getId(); - - PaymentNotice s = new PaymentNotice(); - s.addIdentifier().setSystem("urn:MultiFhirVersionTest").setValue("testUnknownResourceType02"); - ourClientDstu2.create().resource(s).execute().getId(); - - Bundle history = ourClientDstu2.history(null, id, null, null); - assertEquals(PaymentNotice.class, history.getEntries().get(0).getResource().getClass()); - assertEquals(ca.uhn.fhir.model.dstu2.resource.Patient.class, history.getEntries().get(1).getResource().getClass()); - - history = ourClientDstu1.history(null, id, null, null); - assertEquals(ca.uhn.fhir.model.dstu.resource.Patient.class, history.getEntries().get(0).getResource().getClass()); - - history = ourClientDstu2.history().onServer().andReturnDstu1Bundle().execute(); - assertEquals(PaymentNotice.class, history.getEntries().get(0).getResource().getClass()); - assertEquals(ca.uhn.fhir.model.dstu2.resource.Patient.class, history.getEntries().get(1).getResource().getClass()); - - history = ourClientDstu1.history().onServer().andReturnDstu1Bundle().execute(); - assertEquals(ca.uhn.fhir.model.dstu.resource.Patient.class, history.getEntries().get(0).getResource().getClass()); - - history = ourClientDstu1.history().onInstance(id).andReturnDstu1Bundle().execute(); - assertEquals(ca.uhn.fhir.model.dstu.resource.Patient.class, history.getEntries().get(0).getResource().getClass()); - - } - - @SuppressWarnings("unchecked") - @BeforeClass - public static void beforeClass() throws Exception { - //@formatter:off - ourAppCtx = new ClassPathXmlApplicationContext( - "hapi-fhir-server-resourceproviders-dstu1.xml", - "hapi-fhir-server-resourceproviders-dstu2.xml", - "fhir-jpabase-spring-test-config.xml" - ); - //@formatter:on - - int port = RandomServerPortProvider.findFreePort(); - ServletContextHandler proxyHandler = new ServletContextHandler(); - proxyHandler.setContextPath("/"); - - ourServer = new Server(port); - - /* - * DEV resources - */ - - RestfulServer restServerDstu2 = new RestfulServer(ourAppCtx.getBean("myFhirContextDstu2", FhirContext.class)); - List rpsDstu2 = (List) ourAppCtx.getBean("myResourceProvidersDstu2", List.class); - restServerDstu2.setResourceProviders(rpsDstu2); - - JpaSystemProviderDstu2 systemProvDstu2 = (JpaSystemProviderDstu2) ourAppCtx.getBean("mySystemProviderDstu2", JpaSystemProviderDstu2.class); - restServerDstu2.setPlainProviders(systemProvDstu2); - - ServletHolder servletHolder = new ServletHolder(); - servletHolder.setServlet(restServerDstu2); - proxyHandler.addServlet(servletHolder, "/fhir/contextDstu2/*"); - - /* - * DSTU resources - */ - - RestfulServer restServerDstu1 = new RestfulServer(ourAppCtx.getBean("myFhirContextDstu1", FhirContext.class)); - List rpsDstu1 = (List) ourAppCtx.getBean("myResourceProvidersDstu1", List.class); - restServerDstu1.setResourceProviders(rpsDstu1); - - JpaSystemProviderDstu1 systemProvDstu1 = (JpaSystemProviderDstu1) ourAppCtx.getBean("mySystemProviderDstu1", JpaSystemProviderDstu1.class); - restServerDstu1.setPlainProviders(systemProvDstu1); - - servletHolder = new ServletHolder(); - servletHolder.setServlet(restServerDstu1); - proxyHandler.addServlet(servletHolder, "/fhir/contextDstu1/*"); - - /* - * Start server - */ - ourServer.setHandler(proxyHandler); - ourServer.start(); - - /* - * DEV Client - */ - String serverBaseDstu2 = "http://localhost:" + port + "/fhir/contextDstu2"; - FhirContext ctxDstu2 = ourAppCtx.getBean("myFhirContextDstu2", FhirContext.class); - ctxDstu2.getRestfulClientFactory().setSocketTimeout(600 * 1000); - ourClientDstu2 = ctxDstu2.newRestfulGenericClient(serverBaseDstu2); - ourClientDstu2.registerInterceptor(new LoggingInterceptor(true)); - - /* - * DSTU1 Client - */ - String serverBaseDstu1 = "http://localhost:" + port + "/fhir/contextDstu1"; - FhirContext ctxDstu1 = ourAppCtx.getBean("myFhirContextDstu1", FhirContext.class); - ctxDstu1.getRestfulClientFactory().setSocketTimeout(600 * 1000); - ourClientDstu1 = ctxDstu1.newRestfulGenericClient(serverBaseDstu1); - ourClientDstu1.registerInterceptor(new LoggingInterceptor(true)); - } - -} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu1Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu1Test.java index 380079407bd..08ed63475f8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu1Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu1Test.java @@ -16,9 +16,11 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.config.TestDstu1Config; import ca.uhn.fhir.jpa.dao.BaseJpaTest; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.rp.dstu.ObservationResourceProvider; @@ -39,7 +41,7 @@ public class SystemProviderDstu1Test extends BaseJpaTest { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SystemProviderDstu1Test.class); private static Server ourServer; - private static ClassPathXmlApplicationContext ourAppCtx; + private static AnnotationConfigApplicationContext ourAppCtx; private static FhirContext ourCtx; private static IGenericClient ourClient; @@ -72,7 +74,7 @@ public class SystemProviderDstu1Test extends BaseJpaTest { @SuppressWarnings("unchecked") @BeforeClass public static void beforeClass() throws Exception { - ourAppCtx = new ClassPathXmlApplicationContext("fhir-jpabase-spring-test-config.xml", "hapi-fhir-server-resourceproviders-dstu1.xml"); + ourAppCtx = new AnnotationConfigApplicationContext(TestDstu1Config.class); IFhirResourceDao patientDao = (IFhirResourceDao) ourAppCtx.getBean("myPatientDaoDstu1", IFhirResourceDao.class); PatientResourceProvider patientRp = new PatientResourceProvider(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java index db0badd78fc..cd6dba0bd76 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java @@ -20,9 +20,12 @@ import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.config.TestDstu1Config; +import ca.uhn.fhir.jpa.config.TestDstu2Config; import ca.uhn.fhir.jpa.dao.BaseJpaTest; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.rp.dstu2.ObservationResourceProvider; @@ -49,7 +52,7 @@ public class SystemProviderDstu2Test extends BaseJpaTest { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SystemProviderDstu2Test.class); private static Server ourServer; - private static ClassPathXmlApplicationContext ourAppCtx; + private static AnnotationConfigApplicationContext ourAppCtx; private static FhirContext ourCtx; private static IGenericClient ourClient; private static String ourServerBase; @@ -210,7 +213,7 @@ public class SystemProviderDstu2Test extends BaseJpaTest { @SuppressWarnings("unchecked") @BeforeClass public static void beforeClass() throws Exception { - ourAppCtx = new ClassPathXmlApplicationContext("fhir-jpabase-spring-test-config.xml", "hapi-fhir-server-resourceproviders-dstu2.xml"); + ourAppCtx = new AnnotationConfigApplicationContext(TestDstu2Config.class); IFhirResourceDao patientDao = (IFhirResourceDao) ourAppCtx.getBean("myPatientDaoDstu2", IFhirResourceDao.class); PatientResourceProvider patientRp = new PatientResourceProvider(); diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/ExampleServerIT.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/ExampleServerIT.java index afa2c580087..f4efde6ddd9 100644 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/ExampleServerIT.java +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/ExampleServerIT.java @@ -1,6 +1,6 @@ package ca.uhn.fhir.jpa.demo; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; import java.io.File; import java.io.IOException; @@ -27,6 +27,7 @@ public class ExampleServerIT { private static int ourPort; private static Server ourServer; + private static String ourServerBase; @Test public void testCreateAndRead() throws IOException { @@ -71,7 +72,8 @@ public class ExampleServerIT { ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); ourCtx.getRestfulClientFactory().setSocketTimeout(1200 * 1000); - ourClient = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort + "/baseDstu2"); + ourServerBase = "http://localhost:" + ourPort + "/baseDstu2"; + ourClient = ourCtx.newRestfulGenericClient(ourServerBase); ourClient.registerInterceptor(new LoggingInterceptor(true)); } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/CORSFilter_.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/CORSFilter_.java new file mode 100755 index 00000000000..418ca86c90b --- /dev/null +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/CORSFilter_.java @@ -0,0 +1,1157 @@ +/** + * Copyright 2012-2013 eBay Software Foundation, All Rights Reserved. + * + * 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. + */ +package ca.uhn.fhirtest; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * This is a clone of the + * https://github.com/eBay/cors-filter + * CORSFilter, but with a single change applied to allow null origins to work. + * + * This change has been submitted as a pull request against the origin + * project: + * https://github.com/eBay/cors-filter/pull/7 + */ +public final class CORSFilter_ implements Filter { + // ----------------------------------------------------- Instance variables + /** + * Holds filter configuration. + */ + private FilterConfig filterConfig; + + /** + * A {@link Collection} of origins consisting of zero or more origins that + * are allowed access to the resource. + */ + private final Collection allowedOrigins; + + /** + * Determines if any origin is allowed to make request. + */ + private boolean anyOriginAllowed; + + /** + * A {@link Collection} of methods consisting of zero or more methods that + * are supported by the resource. + */ + private final Collection allowedHttpMethods; + + /** + * A {@link Collection} of headers consisting of zero or more header field + * names that are supported by the resource. + */ + private final Collection allowedHttpHeaders; + + /** + * A {@link Collection} of exposed headers consisting of zero or more header + * field names of headers other than the simple response headers that the + * resource might use and can be exposed. + */ + private final Collection exposedHeaders; + + /** + * A supports credentials flag that indicates whether the resource supports + * user credentials in the request. It is true when the resource does and + * false otherwise. + */ + private boolean supportsCredentials; + + /** + * Indicates (in seconds) how long the results of a pre-flight request can + * be cached in a pre-flight result cache. + */ + private long preflightMaxAge; + + /** + * Controls access log logging. + */ + private boolean loggingEnabled; + + /** + * Determines if the request should be decorated or not. + */ + private boolean decorateRequest; + + // --------------------------------------------------------- Constructor(s) + public CORSFilter_() { + this.allowedOrigins = new HashSet(); + this.allowedHttpMethods = new HashSet(); + this.allowedHttpHeaders = new HashSet(); + this.exposedHeaders = new HashSet(); + } + + // --------------------------------------------------------- Public methods + @Override + public void doFilter(final ServletRequest servletRequest, + final ServletResponse servletResponse, + final FilterChain filterChain) throws IOException, + ServletException { + if (!(servletRequest instanceof HttpServletRequest) + || !(servletResponse instanceof HttpServletResponse)) { + String message = + "CORS doesn't support non-HTTP request or response."; + throw new ServletException(message); + } + + // Safe to downcast at this point. + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + // Determines the CORS request type. + CORSFilter_.CORSRequestType requestType = checkRequestType(request); + + // Adds CORS specific attributes to request. + if (decorateRequest) { + CORSFilter_.decorateCORSProperties(request, requestType); + } + switch (requestType) { + case SIMPLE: + // Handles a Simple CORS request. + this.handleSimpleCORS(request, response, filterChain); + break; + case ACTUAL: + // Handles an Actual CORS request. + this.handleSimpleCORS(request, response, filterChain); + break; + case PRE_FLIGHT: + // Handles a Pre-flight CORS request. + this.handlePreflightCORS(request, response, filterChain); + break; + case NOT_CORS: + // Handles a Normal request that is not a cross-origin request. + this.handleNonCORS(request, response, filterChain); + break; + default: + // Handles a CORS request that violates specification. + this.handleInvalidCORS(request, response, filterChain); + break; + } + } + + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + // Initialize defaults + parseAndStore(DEFAULT_ALLOWED_ORIGINS, DEFAULT_ALLOWED_HTTP_METHODS, + DEFAULT_ALLOWED_HTTP_HEADERS, DEFAULT_EXPOSED_HEADERS, + DEFAULT_SUPPORTS_CREDENTIALS, DEFAULT_PREFLIGHT_MAXAGE, + DEFAULT_LOGGING_ENABLED, DEFAULT_DECORATE_REQUEST); + + this.filterConfig = filterConfig; + this.loggingEnabled = false; + + if (filterConfig != null) { + String configAllowedOrigins = + filterConfig.getInitParameter(PARAM_CORS_ALLOWED_ORIGINS); + String configAllowedHttpMethods = + filterConfig.getInitParameter(PARAM_CORS_ALLOWED_METHODS); + String configAllowedHttpHeaders = + filterConfig.getInitParameter(PARAM_CORS_ALLOWED_HEADERS); + String configExposedHeaders = + filterConfig.getInitParameter(PARAM_CORS_EXPOSED_HEADERS); + String configSupportsCredentials = + filterConfig + .getInitParameter(PARAM_CORS_SUPPORT_CREDENTIALS); + String configPreflightMaxAge = + filterConfig.getInitParameter(PARAM_CORS_PREFLIGHT_MAXAGE); + String configLoggingEnabled = + filterConfig.getInitParameter(PARAM_CORS_LOGGING_ENABLED); + String configDecorateRequest = + filterConfig.getInitParameter(PARAM_CORS_REQUEST_DECORATE); + + parseAndStore(configAllowedOrigins, configAllowedHttpMethods, + configAllowedHttpHeaders, + configExposedHeaders, configSupportsCredentials, + configPreflightMaxAge, + configLoggingEnabled, configDecorateRequest); + } + } + + // --------------------------------------------------------------- Handlers + /** + * Handles a CORS request of type {@link CORSRequestType}.SIMPLE. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + * @throws IOException + * @throws ServletException + * @see Simple + * Cross-Origin Request, Actual Request, and Redirects + */ + public void handleSimpleCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) + throws IOException, ServletException { + CORSFilter_.CORSRequestType requestType = + checkRequestType(request); + if (!(requestType == CORSFilter_.CORSRequestType.SIMPLE + || requestType == CORSFilter_.CORSRequestType.ACTUAL)) { + String message = + "Expects a HttpServletRequest object of type " + + CORSFilter_.CORSRequestType.SIMPLE + + " or " + + CORSFilter_.CORSRequestType.ACTUAL; + throw new IllegalArgumentException(message); + } + + final String origin = + request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN); + final String method = request.getMethod(); + + // Section 6.1.2 + if (!isOriginAllowed(origin)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + if (!allowedHttpMethods.contains(method)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + // Section 6.1.3 + // Add a single Access-Control-Allow-Origin header. + if (anyOriginAllowed && !supportsCredentials) { + // If resource doesn't support credentials and if any origin is + // allowed + // to make CORS request, return header with '*'. + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + } else { + // If the resource supports credentials add a single + // Access-Control-Allow-Origin header, with the value of the Origin + // header as value. + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + origin); + } + // Section 6.1.3 + // If the resource supports credentials, add a single + // Access-Control-Allow-Credentials header with the case-sensitive + // string "true" as value. + if (supportsCredentials) { + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, + "true"); + } + + // Section 6.1.4 + // If the list of exposed headers is not empty add one or more + // Access-Control-Expose-Headers headers, with as values the header + // field names given in the list of exposed headers. + if ((exposedHeaders != null) && (exposedHeaders.size() > 0)) { + String exposedHeadersString = join(exposedHeaders, ","); + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, + exposedHeadersString); + } + + // Forward the request down the filter chain. + filterChain.doFilter(request, response); + } + + /** + * Handles CORS pre-flight request. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + * @throws IOException + * @throws ServletException + */ + public void handlePreflightCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) + throws IOException, ServletException { + CORSRequestType requestType = checkRequestType(request); + if (requestType != CORSRequestType.PRE_FLIGHT) { + throw new IllegalArgumentException( + "Expects a HttpServletRequest object of type " + + CORSRequestType.PRE_FLIGHT.name().toLowerCase()); + } + + final String origin = + request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN); + + // Section 6.2.2 + if (!isOriginAllowed(origin)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + // Section 6.2.3 + String accessControlRequestMethod = + request.getHeader(CORSFilter_.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD); + if (accessControlRequestMethod == null + || (!HTTP_METHODS + .contains(accessControlRequestMethod.trim()))) { + handleInvalidCORS(request, response, filterChain); + return; + } else { + accessControlRequestMethod = accessControlRequestMethod.trim(); + } + + // Section 6.2.4 + String accessControlRequestHeadersHeader = + request.getHeader(CORSFilter_.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + List accessControlRequestHeaders = new LinkedList(); + if (accessControlRequestHeadersHeader != null + && !accessControlRequestHeadersHeader.trim().isEmpty()) { + String[] headers = + accessControlRequestHeadersHeader.trim().split(","); + for (String header : headers) { + accessControlRequestHeaders.add(header.trim().toLowerCase()); + } + } + + // Section 6.2.5 + if (!allowedHttpMethods.contains(accessControlRequestMethod)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + // Section 6.2.6 + if (!accessControlRequestHeaders.isEmpty()) { + for (String header : accessControlRequestHeaders) { + if (!allowedHttpHeaders.contains(header)) { + handleInvalidCORS(request, response, filterChain); + return; + } + } + } + + // Section 6.2.7 + if (supportsCredentials) { + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + origin); + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, + "true"); + } else { + if (anyOriginAllowed) { + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + "*"); + } else { + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + origin); + } + } + + // Section 6.2.8 + if (preflightMaxAge > 0) { + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE, + String.valueOf(preflightMaxAge)); + } + + // Section 6.2.9 + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS, + accessControlRequestMethod); + + // Section 6.2.10 + if ((allowedHttpHeaders != null) && (!allowedHttpHeaders.isEmpty())) { + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, + join(allowedHttpHeaders, ",")); + } + + // Do not forward the request down the filter chain. + } + + /** + * Handles a request, that's not a CORS request, but is a valid request i.e. + * it is not a cross-origin request. This implementation, just forwards the + * request down the filter chain. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + * @throws IOException + * @throws ServletException + */ + public void handleNonCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) + throws IOException, ServletException { + // Let request pass. + filterChain.doFilter(request, response); + } + + /** + * Handles a CORS request that violates specification. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + * @throws IOException + * @throws ServletException + */ + public void handleInvalidCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) { + String origin = request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN); + String method = request.getMethod(); + String accessControlRequestHeaders = + request.getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + + String message = + "Invalid CORS request; Origin=" + origin + ";Method=" + method; + if (accessControlRequestHeaders != null) { + message = + message + ";Access-Control-Request-Headers=" + + accessControlRequestHeaders; + } + response.setContentType("text/plain"); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.resetBuffer(); + + log(message); + } + + @Override + public void destroy() { + // NOOP + } + + // -------------------------------------------------------- Utility methods + /** + * Decorates the {@link HttpServletRequest}, with CORS attributes. + *
    + *
  • cors.isCorsRequest: Flag to determine if request is a CORS + * request. Set to true if CORS request; false + * otherwise.
  • + *
  • cors.request.origin: The Origin URL.
  • + *
  • cors.request.type: Type of request. Values: + * simple or preflight or not_cors or + * invalid_cors
  • + *
  • cors.request.headers: Request headers sent as + * 'Access-Control-Request-Headers' header, for pre-flight request.
  • + *
+ * + * @param request + * The {@link HttpServletRequest} object. + * @param corsRequestType + * The {@link CORSRequestType} object. + */ + public static void decorateCORSProperties(final HttpServletRequest request, + final CORSRequestType corsRequestType) { + if (request == null) { + throw new IllegalArgumentException( + "HttpServletRequest object is null"); + } + + if (corsRequestType == null) { + throw new IllegalArgumentException("CORSRequestType object is null"); + } + + switch (corsRequestType) { + case SIMPLE: + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, true); + request.setAttribute(CORSFilter_.HTTP_REQUEST_ATTRIBUTE_ORIGIN, + request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN)); + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, + corsRequestType.name().toLowerCase()); + break; + case ACTUAL: + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, true); + request.setAttribute(CORSFilter_.HTTP_REQUEST_ATTRIBUTE_ORIGIN, + request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN)); + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, + corsRequestType.name().toLowerCase()); + break; + case PRE_FLIGHT: + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, true); + request.setAttribute(CORSFilter_.HTTP_REQUEST_ATTRIBUTE_ORIGIN, + request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN)); + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, + corsRequestType.name().toLowerCase()); + String headers = + request.getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + if (headers == null) { + headers = ""; + } + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS, + headers + ); + break; + case NOT_CORS: + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, false); + break; + default: + // Don't set any attributes + break; + } + } + + /** + * Joins elements of {@link Set} into a string, where each element is + * separated by the provided separator. + * + * @param elements + * The {@link Set} containing elements to join together. + * @param joinSeparator + * The character to be used for separating elements. + * @return The joined {@link String}; null if elements + * {@link Set} is null. + */ + public static String join(final Collection elements, + final String joinSeparator) { + String separator = ","; + if (elements == null) { + return null; + } + if (joinSeparator != null) { + separator = joinSeparator; + } + StringBuilder buffer = new StringBuilder(); + boolean isFirst = true; + for (String element : elements) { + if (!isFirst) { + buffer.append(separator); + } else { + isFirst = false; + } + + if (element != null) { + buffer.append(element); + } + } + + return buffer.toString(); + } + + /** + * Determines the request type. + * + * @param request + * @return + */ + public CORSRequestType checkRequestType(final HttpServletRequest request) { + CORSRequestType requestType = CORSRequestType.INVALID_CORS; + if (request == null) { + throw new IllegalArgumentException( + "HttpServletRequest object is null"); + } + String originHeader = request.getHeader(REQUEST_HEADER_ORIGIN); + // Section 6.1.1 and Section 6.2.1 + if (originHeader != null) { + if (originHeader.isEmpty()) { + requestType = CORSRequestType.INVALID_CORS; + } else if ("null".equals(originHeader) == false && !isValidOrigin(originHeader)) { + requestType = CORSRequestType.INVALID_CORS; + } else { + String method = request.getMethod(); + if (method != null && HTTP_METHODS.contains(method)) { + if ("OPTIONS".equals(method)) { + String accessControlRequestMethodHeader = + request.getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD); + if (accessControlRequestMethodHeader != null + && !accessControlRequestMethodHeader.isEmpty()) { + requestType = CORSRequestType.PRE_FLIGHT; + } else if (accessControlRequestMethodHeader != null + && accessControlRequestMethodHeader.isEmpty()) { + requestType = CORSRequestType.INVALID_CORS; + } else { + requestType = CORSRequestType.ACTUAL; + } + } else if ("GET".equals(method) || "HEAD".equals(method)) { + requestType = CORSRequestType.SIMPLE; + } else if ("POST".equals(method)) { + String contentType = request.getContentType(); + if (contentType != null) { + contentType = contentType.toLowerCase().trim(); + if (SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES + .contains(contentType)) { + requestType = CORSRequestType.SIMPLE; + } else { + requestType = CORSRequestType.ACTUAL; + } + } + } else if (COMPLEX_HTTP_METHODS.contains(method)) { + requestType = CORSRequestType.ACTUAL; + } + } + } + } else { + requestType = CORSRequestType.NOT_CORS; + } + + return requestType; + } + + /** + * Checks if the Origin is allowed to make a CORS request. + * + * @param origin + * The Origin. + * @return true if origin is allowed; false + * otherwise. + */ + private boolean isOriginAllowed(final String origin) { + if (anyOriginAllowed) { + return true; + } + + // If 'Origin' header is a case-sensitive match of any of allowed + // origins, then return true, else return false. + return allowedOrigins.contains(origin); + } + + private void log(String message) { + if (loggingEnabled) { + filterConfig.getServletContext().log(message); + } + } + + /** + * Parses each param-value and populates configuration variables. If a param + * is provided, it overrides the default. + * + * @param allowedOrigins + * A {@link String} of comma separated origins. + * @param allowedHttpMethods + * A {@link String} of comma separated HTTP methods. + * @param allowedHttpHeaders + * A {@link String} of comma separated HTTP headers. + * @param exposedHeaders + * A {@link String} of comma separated headers that needs to be + * exposed. + * @param supportsCredentials + * "true" if support credentials needs to be enabled. + * @param preflightMaxAge + * The amount of seconds the user agent is allowed to cache the + * result of the pre-flight request. + * @param loggingEnabled + * Flag to control logging to access log. + * @throws ServletException + */ + private void parseAndStore(final String allowedOrigins, + final String allowedHttpMethods, final String allowedHttpHeaders, + final String exposedHeaders, final String supportsCredentials, + final String preflightMaxAge, final String loggingEnabled, + final String decorateRequest) + throws ServletException { + if (allowedOrigins != null) { + if (allowedOrigins.trim().equals("*")) { + this.anyOriginAllowed = true; + } else { + this.anyOriginAllowed = false; + Set setAllowedOrigins = + parseStringToSet(allowedOrigins); + this.allowedOrigins.clear(); + this.allowedOrigins.addAll(setAllowedOrigins); + } + } + + if (allowedHttpMethods != null) { + Set setAllowedHttpMethods = + parseStringToSet(allowedHttpMethods); + this.allowedHttpMethods.clear(); + this.allowedHttpMethods.addAll(setAllowedHttpMethods); + } + + if (allowedHttpHeaders != null) { + Set setAllowedHttpHeaders = + parseStringToSet(allowedHttpHeaders); + Set lowerCaseHeaders = new HashSet(); + for (String header : setAllowedHttpHeaders) { + String lowerCase = header.toLowerCase(); + lowerCaseHeaders.add(lowerCase); + } + this.allowedHttpHeaders.clear(); + this.allowedHttpHeaders.addAll(lowerCaseHeaders); + } + + if (exposedHeaders != null) { + Set setExposedHeaders = parseStringToSet(exposedHeaders); + this.exposedHeaders.clear(); + this.exposedHeaders.addAll(setExposedHeaders); + } + + if (supportsCredentials != null) { + // For any value other then 'true' this will be false. + this.supportsCredentials = + Boolean.parseBoolean(supportsCredentials); + } + + if (preflightMaxAge != null) { + try { + if (!preflightMaxAge.isEmpty()) { + this.preflightMaxAge = Long.parseLong(preflightMaxAge); + } else { + this.preflightMaxAge = 0L; + } + } catch (NumberFormatException e) { + throw new ServletException("Unable to parse preflightMaxAge", e); + } + } + + if (loggingEnabled != null) { + // For any value other then 'true' this will be false. + this.loggingEnabled = Boolean.parseBoolean(loggingEnabled); + } + + if (decorateRequest != null) { + // For any value other then 'true' this will be false. + this.decorateRequest = Boolean.parseBoolean(decorateRequest); + } + } + + /** + * Takes a comma separated list and returns a Set. + * + * @param data + * A comma separated list of strings. + * @return Set + */ + private Set parseStringToSet(final String data) { + String[] splits; + + if (data != null && data.length() > 0) { + splits = data.split(","); + } else { + splits = new String[] {}; + } + + Set set = new HashSet(); + if (splits.length > 0) { + for (String split : splits) { + set.add(split.trim()); + } + } + + return set; + } + + /** + * Checks if a given origin is valid or not. Criteria: + *
    + *
  • If an encoded character is present in origin, it's not valid.
  • + *
  • Origin should be a valid {@link URI}
  • + *
+ * + * @param origin + * @see RFC952 + * @return + */ + public static boolean isValidOrigin(String origin) { + // Checks for encoded characters. Helps prevent CRLF injection. + if (origin.contains("%")) { + return false; + } + + URI originURI; + + try { + originURI = new URI(origin); + } catch (URISyntaxException e) { + return false; + } + // If scheme for URI is null, return false. Return true otherwise. + return originURI.getScheme() != null; + + } + + // -------------------------------------------------------------- Accessors + /** + * Determines if logging is enabled or not. + * + * @return true if it's enabled; false otherwise. + */ + public boolean isLoggingEnabled() { + return loggingEnabled; + } + + /** + * Determines if any origin is allowed to make CORS request. + * + * @return true if it's enabled; false otherwise. + */ + public boolean isAnyOriginAllowed() { + return anyOriginAllowed; + } + + /** + * Returns a {@link Set} of headers that should be exposed by browser. + * + * @return + */ + public Collection getExposedHeaders() { + return exposedHeaders; + } + + /** + * Determines is supports credentials is enabled + * + * @return + */ + public boolean isSupportsCredentials() { + return supportsCredentials; + } + + /** + * Returns the preflight response cache time in seconds. + * + * @return Time to cache in seconds. + */ + public long getPreflightMaxAge() { + return preflightMaxAge; + } + + /** + * Returns the {@link Set} of allowed origins that are allowed to make + * requests. + * + * @return {@link Set} + */ + public Collection getAllowedOrigins() { + return allowedOrigins; + } + + /** + * Returns a {@link Set} of HTTP methods that are allowed to make requests. + * + * @return {@link Set} + */ + public Collection getAllowedHttpMethods() { + return allowedHttpMethods; + } + + /** + * Returns a {@link Set} of headers support by resource. + * + * @return {@link Set} + */ + public Collection getAllowedHttpHeaders() { + return allowedHttpHeaders; + } + + // -------------------------------------------------- CORS Response Headers + /** + * The Access-Control-Allow-Origin header indicates whether a resource can + * be shared based by returning the value of the Origin request header in + * the response. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = + "Access-Control-Allow-Origin"; + + /** + * The Access-Control-Allow-Credentials header indicates whether the + * response to request can be exposed when the omit credentials flag is + * unset. When part of the response to a preflight request it indicates that + * the actual request can include user credentials. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS = + "Access-Control-Allow-Credentials"; + + /** + * The Access-Control-Expose-Headers header indicates which headers are safe + * to expose to the API of a CORS API specification + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = + "Access-Control-Expose-Headers"; + + /** + * The Access-Control-Max-Age header indicates how long the results of a + * preflight request can be cached in a preflight result cache. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE = + "Access-Control-Max-Age"; + + /** + * The Access-Control-Allow-Methods header indicates, as part of the + * response to a preflight request, which methods can be used during the + * actual request. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS = + "Access-Control-Allow-Methods"; + + /** + * The Access-Control-Allow-Headers header indicates, as part of the + * response to a preflight request, which header field names can be used + * during the actual request. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS = + "Access-Control-Allow-Headers"; + + // -------------------------------------------------- CORS Request Headers + /** + * The Origin header indicates where the cross-origin request or preflight + * request originates from. + */ + public static final String REQUEST_HEADER_ORIGIN = "Origin"; + + /** + * The Access-Control-Request-Method header indicates which method will be + * used in the actual request as part of the preflight request. + */ + public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD = + "Access-Control-Request-Method"; + + /** + * The Access-Control-Request-Headers header indicates which headers will be + * used in the actual request as part of the preflight request. + */ + public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS = + "Access-Control-Request-Headers"; + + // ----------------------------------------------------- Request attributes + /** + * The prefix to a CORS request attribute. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_PREFIX = "cors."; + + /** + * Attribute that contains the origin of the request. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_ORIGIN = + HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.origin"; + + /** + * Boolean value, suggesting if the request is a CORS request or not. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST = + HTTP_REQUEST_ATTRIBUTE_PREFIX + "isCorsRequest"; + + /** + * Type of CORS request, of type {@link CORSRequestType}. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE = + HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.type"; + + /** + * Request headers sent as 'Access-Control-Request-Headers' header, for + * pre-flight request. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS = + HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.headers"; + + // -------------------------------------------------------------- Constants + /** + * Enumerates varies types of CORS requests. Also, provides utility methods + * to determine the request type. + */ + public static enum CORSRequestType { + /** + * A simple HTTP request, i.e. it shouldn't be pre-flighted. + */ + SIMPLE, + /** + * A HTTP request that needs to be pre-flighted. + */ + ACTUAL, + /** + * A pre-flight CORS request, to get meta information, before a + * non-simple HTTP request is sent. + */ + PRE_FLIGHT, + /** + * Not a CORS request, but a normal request. + */ + NOT_CORS, + /** + * An invalid CORS request, i.e. it qualifies to be a CORS request, but + * fails to be a valid one. + */ + INVALID_CORS + } + + /** + * {@link Collection} of HTTP methods. Case sensitive. + * + * @see http://tools.ietf.org/html/rfc2616#section-5.1.1 + */ + public static final Collection HTTP_METHODS = new HashSet( + Arrays.asList("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", + "TRACE", "CONNECT")); + /** + * {@link Collection} of non-simple HTTP methods. Case sensitive. + */ + public static final Collection COMPLEX_HTTP_METHODS = + new HashSet( + Arrays.asList("PUT", "DELETE", "TRACE", "CONNECT")); + /** + * {@link Collection} of Simple HTTP methods. Case sensitive. + * + * @see http://www.w3.org/TR/cors/#terminology + */ + public static final Collection SIMPLE_HTTP_METHODS = + new HashSet( + Arrays.asList("GET", "POST", "HEAD")); + + /** + * {@link Collection} of Simple HTTP request headers. Case in-sensitive. + * + * @see http://www.w3.org/TR/cors/#terminology + */ + public static final Collection SIMPLE_HTTP_REQUEST_HEADERS = + new HashSet(Arrays.asList("Accept", "Accept-Language", + "Content-Language")); + + /** + * {@link Collection} of Simple HTTP request headers. Case in-sensitive. + * + * @see http://www.w3.org/TR/cors/#terminology + */ + public static final Collection SIMPLE_HTTP_RESPONSE_HEADERS = + new HashSet(Arrays.asList("Cache-Control", + "Content-Language", "Content-Type", "Expires", + "Last-Modified", "Pragma")); + + /** + * {@link Collection} of Simple HTTP request headers. Case in-sensitive. + * + * @see http://www.w3.org/TR/cors/#terminology + */ + public static final Collection SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES = + new HashSet(Arrays.asList( + "application/x-www-form-urlencoded", "multipart/form-data", + "text/plain")); + + // ------------------------------------------------ Configuration Defaults + /** + * By default, all origins are allowed to make requests. + */ + public static final String DEFAULT_ALLOWED_ORIGINS = "*"; + + /** + * By default, following methods are supported: GET, POST, HEAD and OPTIONS. + */ + public static final String DEFAULT_ALLOWED_HTTP_METHODS = + "GET,POST,HEAD,OPTIONS"; + + /** + * By default, time duration to cache pre-flight response is 30 mins. + */ + public static final String DEFAULT_PREFLIGHT_MAXAGE = "1800"; + + /** + * By default, support credentials is turned on. + */ + public static final String DEFAULT_SUPPORTS_CREDENTIALS = "true"; + + /** + * By default, following headers are supported: + * Origin,Accept,X-Requested-With, Content-Type, + * Access-Control-Request-Method, and Access-Control-Request-Headers. + */ + public static final String DEFAULT_ALLOWED_HTTP_HEADERS = + "Origin,Accept,X-Requested-With,Content-Type," + + + "Access-Control-Request-Method,Access-Control-Request-Headers"; + + /** + * By default, none of the headers are exposed in response. + */ + public static final String DEFAULT_EXPOSED_HEADERS = ""; + + /** + * By default, access log logging is turned off + */ + public static final String DEFAULT_LOGGING_ENABLED = "false"; + + /** + * By default, request is decorated with CORS attributes. + */ + public static final String DEFAULT_DECORATE_REQUEST = "true"; + + // ----------------------------------------Filter Config Init param-name(s) + /** + * Key to retrieve allowed origins from {@link FilterConfig}. + */ + public static final String PARAM_CORS_ALLOWED_ORIGINS = + "cors.allowed.origins"; + + /** + * Key to retrieve support credentials from {@link FilterConfig}. + */ + public static final String PARAM_CORS_SUPPORT_CREDENTIALS = + "cors.support.credentials"; + + /** + * Key to retrieve exposed headers from {@link FilterConfig}. + */ + public static final String PARAM_CORS_EXPOSED_HEADERS = + "cors.exposed.headers"; + + /** + * Key to retrieve allowed headers from {@link FilterConfig}. + */ + public static final String PARAM_CORS_ALLOWED_HEADERS = + "cors.allowed.headers"; + + /** + * Key to retrieve allowed methods from {@link FilterConfig}. + */ + public static final String PARAM_CORS_ALLOWED_METHODS = + "cors.allowed.methods"; + + /** + * Key to retrieve preflight max age from {@link FilterConfig}. + */ + public static final String PARAM_CORS_PREFLIGHT_MAXAGE = + "cors.preflight.maxage"; + + /** + * Key to retrieve access log logging flag. + */ + public static final String PARAM_CORS_LOGGING_ENABLED = + "cors.logging.enabled"; + + /** + * Key to determine if request should be decorated. + */ + public static final String PARAM_CORS_REQUEST_DECORATE = + "cors.request.decorate"; +} diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml index bb94f7332b8..4a7c36748c2 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml @@ -129,7 +129,7 @@ CORS Filter - org.ebaysf.web.cors.CORSFilter + ca.uhn.fhirtest.CORSFilter_ A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials. cors.allowed.origins diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CORSFilter_.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CORSFilter_.java new file mode 100755 index 00000000000..ddbae4371d7 --- /dev/null +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CORSFilter_.java @@ -0,0 +1,1176 @@ +/** + * Copyright 2012-2013 eBay Software Foundation, All Rights Reserved. + * + * 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. + */ +package ca.uhn.fhir.rest.server; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + *

+ * A {@link Filter} that enable client-side cross-origin requests by + * implementing W3C's CORS (Cross-Origin Resource + * Sharing) specification for resources. Each {@link HttpServletRequest} + * request is inspected as per specification, and appropriate response headers + * are added to {@link HttpServletResponse}. + *

+ * + *

+ * By default, it also sets following request attributes, that helps to + * determine nature of request downstream. + *

    + *
  • cors.isCorsRequest: Flag to determine if request is a CORS + * request. Set to true if CORS request; false + * otherwise.
  • + *
  • cors.request.origin: The Origin URL.
  • + *
  • cors.request.type: Type of request. Values: simple or + * preflight or not_cors or invalid_cors
  • + *
  • cors.request.headers: Request headers sent as + * 'Access-Control-Request-Headers' header, for pre-flight request.
  • + *
+ *

+ * + * @author Mohit Soni + * @see CORS specification + * + */ +public final class CORSFilter_ implements Filter { + // ----------------------------------------------------- Instance variables + /** + * Holds filter configuration. + */ + private FilterConfig filterConfig; + + /** + * A {@link Collection} of origins consisting of zero or more origins that + * are allowed access to the resource. + */ + private final Collection allowedOrigins; + + /** + * Determines if any origin is allowed to make request. + */ + private boolean anyOriginAllowed; + + /** + * A {@link Collection} of methods consisting of zero or more methods that + * are supported by the resource. + */ + private final Collection allowedHttpMethods; + + /** + * A {@link Collection} of headers consisting of zero or more header field + * names that are supported by the resource. + */ + private final Collection allowedHttpHeaders; + + /** + * A {@link Collection} of exposed headers consisting of zero or more header + * field names of headers other than the simple response headers that the + * resource might use and can be exposed. + */ + private final Collection exposedHeaders; + + /** + * A supports credentials flag that indicates whether the resource supports + * user credentials in the request. It is true when the resource does and + * false otherwise. + */ + private boolean supportsCredentials; + + /** + * Indicates (in seconds) how long the results of a pre-flight request can + * be cached in a pre-flight result cache. + */ + private long preflightMaxAge; + + /** + * Controls access log logging. + */ + private boolean loggingEnabled; + + /** + * Determines if the request should be decorated or not. + */ + private boolean decorateRequest; + + // --------------------------------------------------------- Constructor(s) + public CORSFilter_() { + this.allowedOrigins = new HashSet(); + this.allowedHttpMethods = new HashSet(); + this.allowedHttpHeaders = new HashSet(); + this.exposedHeaders = new HashSet(); + } + + // --------------------------------------------------------- Public methods + @Override + public void doFilter(final ServletRequest servletRequest, + final ServletResponse servletResponse, + final FilterChain filterChain) throws IOException, + ServletException { + if (!(servletRequest instanceof HttpServletRequest) + || !(servletResponse instanceof HttpServletResponse)) { + String message = + "CORS doesn't support non-HTTP request or response."; + throw new ServletException(message); + } + + // Safe to downcast at this point. + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + // Determines the CORS request type. + CORSFilter_.CORSRequestType requestType = checkRequestType(request); + + // Adds CORS specific attributes to request. + if (decorateRequest) { + CORSFilter_.decorateCORSProperties(request, requestType); + } + switch (requestType) { + case SIMPLE: + // Handles a Simple CORS request. + this.handleSimpleCORS(request, response, filterChain); + break; + case ACTUAL: + // Handles an Actual CORS request. + this.handleSimpleCORS(request, response, filterChain); + break; + case PRE_FLIGHT: + // Handles a Pre-flight CORS request. + this.handlePreflightCORS(request, response, filterChain); + break; + case NOT_CORS: + // Handles a Normal request that is not a cross-origin request. + this.handleNonCORS(request, response, filterChain); + break; + default: + // Handles a CORS request that violates specification. + this.handleInvalidCORS(request, response, filterChain); + break; + } + } + + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + // Initialize defaults + parseAndStore(DEFAULT_ALLOWED_ORIGINS, DEFAULT_ALLOWED_HTTP_METHODS, + DEFAULT_ALLOWED_HTTP_HEADERS, DEFAULT_EXPOSED_HEADERS, + DEFAULT_SUPPORTS_CREDENTIALS, DEFAULT_PREFLIGHT_MAXAGE, + DEFAULT_LOGGING_ENABLED, DEFAULT_DECORATE_REQUEST); + + this.filterConfig = filterConfig; + this.loggingEnabled = false; + + if (filterConfig != null) { + String configAllowedOrigins = + filterConfig.getInitParameter(PARAM_CORS_ALLOWED_ORIGINS); + String configAllowedHttpMethods = + filterConfig.getInitParameter(PARAM_CORS_ALLOWED_METHODS); + String configAllowedHttpHeaders = + filterConfig.getInitParameter(PARAM_CORS_ALLOWED_HEADERS); + String configExposedHeaders = + filterConfig.getInitParameter(PARAM_CORS_EXPOSED_HEADERS); + String configSupportsCredentials = + filterConfig + .getInitParameter(PARAM_CORS_SUPPORT_CREDENTIALS); + String configPreflightMaxAge = + filterConfig.getInitParameter(PARAM_CORS_PREFLIGHT_MAXAGE); + String configLoggingEnabled = + filterConfig.getInitParameter(PARAM_CORS_LOGGING_ENABLED); + String configDecorateRequest = + filterConfig.getInitParameter(PARAM_CORS_REQUEST_DECORATE); + + parseAndStore(configAllowedOrigins, configAllowedHttpMethods, + configAllowedHttpHeaders, + configExposedHeaders, configSupportsCredentials, + configPreflightMaxAge, + configLoggingEnabled, configDecorateRequest); + } + } + + // --------------------------------------------------------------- Handlers + /** + * Handles a CORS request of type {@link CORSRequestType}.SIMPLE. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + * @throws IOException + * @throws ServletException + * @see Simple + * Cross-Origin Request, Actual Request, and Redirects + */ + public void handleSimpleCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) + throws IOException, ServletException { + CORSFilter_.CORSRequestType requestType = + checkRequestType(request); + if (!(requestType == CORSFilter_.CORSRequestType.SIMPLE + || requestType == CORSFilter_.CORSRequestType.ACTUAL)) { + String message = + "Expects a HttpServletRequest object of type " + + CORSFilter_.CORSRequestType.SIMPLE + + " or " + + CORSFilter_.CORSRequestType.ACTUAL; + throw new IllegalArgumentException(message); + } + + final String origin = + request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN); + final String method = request.getMethod(); + + // Section 6.1.2 + if (!isOriginAllowed(origin)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + if (!allowedHttpMethods.contains(method)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + // Section 6.1.3 + // Add a single Access-Control-Allow-Origin header. + if (anyOriginAllowed && !supportsCredentials) { + // If resource doesn't support credentials and if any origin is + // allowed + // to make CORS request, return header with '*'. + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + } else { + // If the resource supports credentials add a single + // Access-Control-Allow-Origin header, with the value of the Origin + // header as value. + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + origin); + } + // Section 6.1.3 + // If the resource supports credentials, add a single + // Access-Control-Allow-Credentials header with the case-sensitive + // string "true" as value. + if (supportsCredentials) { + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, + "true"); + } + + // Section 6.1.4 + // If the list of exposed headers is not empty add one or more + // Access-Control-Expose-Headers headers, with as values the header + // field names given in the list of exposed headers. + if ((exposedHeaders != null) && (exposedHeaders.size() > 0)) { + String exposedHeadersString = join(exposedHeaders, ","); + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, + exposedHeadersString); + } + + // Forward the request down the filter chain. + filterChain.doFilter(request, response); + } + + /** + * Handles CORS pre-flight request. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + * @throws IOException + * @throws ServletException + */ + public void handlePreflightCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) + throws IOException, ServletException { + CORSRequestType requestType = checkRequestType(request); + if (requestType != CORSRequestType.PRE_FLIGHT) { + throw new IllegalArgumentException( + "Expects a HttpServletRequest object of type " + + CORSRequestType.PRE_FLIGHT.name().toLowerCase()); + } + + final String origin = + request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN); + + // Section 6.2.2 + if (!isOriginAllowed(origin)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + // Section 6.2.3 + String accessControlRequestMethod = + request.getHeader(CORSFilter_.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD); + if (accessControlRequestMethod == null + || (!HTTP_METHODS + .contains(accessControlRequestMethod.trim()))) { + handleInvalidCORS(request, response, filterChain); + return; + } else { + accessControlRequestMethod = accessControlRequestMethod.trim(); + } + + // Section 6.2.4 + String accessControlRequestHeadersHeader = + request.getHeader(CORSFilter_.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + List accessControlRequestHeaders = new LinkedList(); + if (accessControlRequestHeadersHeader != null + && !accessControlRequestHeadersHeader.trim().isEmpty()) { + String[] headers = + accessControlRequestHeadersHeader.trim().split(","); + for (String header : headers) { + accessControlRequestHeaders.add(header.trim().toLowerCase()); + } + } + + // Section 6.2.5 + if (!allowedHttpMethods.contains(accessControlRequestMethod)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + // Section 6.2.6 + if (!accessControlRequestHeaders.isEmpty()) { + for (String header : accessControlRequestHeaders) { + if (!allowedHttpHeaders.contains(header)) { + handleInvalidCORS(request, response, filterChain); + return; + } + } + } + + // Section 6.2.7 + if (supportsCredentials) { + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + origin); + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, + "true"); + } else { + if (anyOriginAllowed) { + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + "*"); + } else { + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + origin); + } + } + + // Section 6.2.8 + if (preflightMaxAge > 0) { + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE, + String.valueOf(preflightMaxAge)); + } + + // Section 6.2.9 + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS, + accessControlRequestMethod); + + // Section 6.2.10 + if ((allowedHttpHeaders != null) && (!allowedHttpHeaders.isEmpty())) { + response.addHeader( + CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, + join(allowedHttpHeaders, ",")); + } + + // Do not forward the request down the filter chain. + } + + /** + * Handles a request, that's not a CORS request, but is a valid request i.e. + * it is not a cross-origin request. This implementation, just forwards the + * request down the filter chain. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + * @throws IOException + * @throws ServletException + */ + public void handleNonCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) + throws IOException, ServletException { + // Let request pass. + filterChain.doFilter(request, response); + } + + /** + * Handles a CORS request that violates specification. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + * @throws IOException + * @throws ServletException + */ + public void handleInvalidCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) { + String origin = request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN); + String method = request.getMethod(); + String accessControlRequestHeaders = + request.getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + + String message = + "Invalid CORS request; Origin=" + origin + ";Method=" + method; + if (accessControlRequestHeaders != null) { + message = + message + ";Access-Control-Request-Headers=" + + accessControlRequestHeaders; + } + response.setContentType("text/plain"); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.resetBuffer(); + + log(message); + } + + @Override + public void destroy() { + // NOOP + } + + // -------------------------------------------------------- Utility methods + /** + * Decorates the {@link HttpServletRequest}, with CORS attributes. + *
    + *
  • cors.isCorsRequest: Flag to determine if request is a CORS + * request. Set to true if CORS request; false + * otherwise.
  • + *
  • cors.request.origin: The Origin URL.
  • + *
  • cors.request.type: Type of request. Values: + * simple or preflight or not_cors or + * invalid_cors
  • + *
  • cors.request.headers: Request headers sent as + * 'Access-Control-Request-Headers' header, for pre-flight request.
  • + *
+ * + * @param request + * The {@link HttpServletRequest} object. + * @param corsRequestType + * The {@link CORSRequestType} object. + */ + public static void decorateCORSProperties(final HttpServletRequest request, + final CORSRequestType corsRequestType) { + if (request == null) { + throw new IllegalArgumentException( + "HttpServletRequest object is null"); + } + + if (corsRequestType == null) { + throw new IllegalArgumentException("CORSRequestType object is null"); + } + + switch (corsRequestType) { + case SIMPLE: + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, true); + request.setAttribute(CORSFilter_.HTTP_REQUEST_ATTRIBUTE_ORIGIN, + request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN)); + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, + corsRequestType.name().toLowerCase()); + break; + case ACTUAL: + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, true); + request.setAttribute(CORSFilter_.HTTP_REQUEST_ATTRIBUTE_ORIGIN, + request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN)); + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, + corsRequestType.name().toLowerCase()); + break; + case PRE_FLIGHT: + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, true); + request.setAttribute(CORSFilter_.HTTP_REQUEST_ATTRIBUTE_ORIGIN, + request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN)); + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, + corsRequestType.name().toLowerCase()); + String headers = + request.getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + if (headers == null) { + headers = ""; + } + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS, + headers + ); + break; + case NOT_CORS: + request.setAttribute( + CORSFilter_.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, false); + break; + default: + // Don't set any attributes + break; + } + } + + /** + * Joins elements of {@link Set} into a string, where each element is + * separated by the provided separator. + * + * @param elements + * The {@link Set} containing elements to join together. + * @param joinSeparator + * The character to be used for separating elements. + * @return The joined {@link String}; null if elements + * {@link Set} is null. + */ + public static String join(final Collection elements, + final String joinSeparator) { + String separator = ","; + if (elements == null) { + return null; + } + if (joinSeparator != null) { + separator = joinSeparator; + } + StringBuilder buffer = new StringBuilder(); + boolean isFirst = true; + for (String element : elements) { + if (!isFirst) { + buffer.append(separator); + } else { + isFirst = false; + } + + if (element != null) { + buffer.append(element); + } + } + + return buffer.toString(); + } + + /** + * Determines the request type. + * + * @param request + * @return + */ + public CORSRequestType checkRequestType(final HttpServletRequest request) { + CORSRequestType requestType = CORSRequestType.INVALID_CORS; + if (request == null) { + throw new IllegalArgumentException( + "HttpServletRequest object is null"); + } + String originHeader = request.getHeader(REQUEST_HEADER_ORIGIN); + // Section 6.1.1 and Section 6.2.1 + if (originHeader != null) { + if (originHeader.isEmpty()) { + requestType = CORSRequestType.INVALID_CORS; + } else if ("null".equals(originHeader) == false && !isValidOrigin(originHeader)) { + requestType = CORSRequestType.INVALID_CORS; + } else { + String method = request.getMethod(); + if (method != null && HTTP_METHODS.contains(method)) { + if ("OPTIONS".equals(method)) { + String accessControlRequestMethodHeader = + request.getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD); + if (accessControlRequestMethodHeader != null + && !accessControlRequestMethodHeader.isEmpty()) { + requestType = CORSRequestType.PRE_FLIGHT; + } else if (accessControlRequestMethodHeader != null + && accessControlRequestMethodHeader.isEmpty()) { + requestType = CORSRequestType.INVALID_CORS; + } else { + requestType = CORSRequestType.ACTUAL; + } + } else if ("GET".equals(method) || "HEAD".equals(method)) { + requestType = CORSRequestType.SIMPLE; + } else if ("POST".equals(method)) { + String contentType = request.getContentType(); + if (contentType != null) { + contentType = contentType.toLowerCase().trim(); + if (SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES + .contains(contentType)) { + requestType = CORSRequestType.SIMPLE; + } else { + requestType = CORSRequestType.ACTUAL; + } + } + } else if (COMPLEX_HTTP_METHODS.contains(method)) { + requestType = CORSRequestType.ACTUAL; + } + } + } + } else { + requestType = CORSRequestType.NOT_CORS; + } + + return requestType; + } + + /** + * Checks if the Origin is allowed to make a CORS request. + * + * @param origin + * The Origin. + * @return true if origin is allowed; false + * otherwise. + */ + private boolean isOriginAllowed(final String origin) { + if (anyOriginAllowed) { + return true; + } + + // If 'Origin' header is a case-sensitive match of any of allowed + // origins, then return true, else return false. + return allowedOrigins.contains(origin); + } + + private void log(String message) { + if (loggingEnabled) { + filterConfig.getServletContext().log(message); + } + } + + /** + * Parses each param-value and populates configuration variables. If a param + * is provided, it overrides the default. + * + * @param allowedOrigins + * A {@link String} of comma separated origins. + * @param allowedHttpMethods + * A {@link String} of comma separated HTTP methods. + * @param allowedHttpHeaders + * A {@link String} of comma separated HTTP headers. + * @param exposedHeaders + * A {@link String} of comma separated headers that needs to be + * exposed. + * @param supportsCredentials + * "true" if support credentials needs to be enabled. + * @param preflightMaxAge + * The amount of seconds the user agent is allowed to cache the + * result of the pre-flight request. + * @param loggingEnabled + * Flag to control logging to access log. + * @throws ServletException + */ + private void parseAndStore(final String allowedOrigins, + final String allowedHttpMethods, final String allowedHttpHeaders, + final String exposedHeaders, final String supportsCredentials, + final String preflightMaxAge, final String loggingEnabled, + final String decorateRequest) + throws ServletException { + if (allowedOrigins != null) { + if (allowedOrigins.trim().equals("*")) { + this.anyOriginAllowed = true; + } else { + this.anyOriginAllowed = false; + Set setAllowedOrigins = + parseStringToSet(allowedOrigins); + this.allowedOrigins.clear(); + this.allowedOrigins.addAll(setAllowedOrigins); + } + } + + if (allowedHttpMethods != null) { + Set setAllowedHttpMethods = + parseStringToSet(allowedHttpMethods); + this.allowedHttpMethods.clear(); + this.allowedHttpMethods.addAll(setAllowedHttpMethods); + } + + if (allowedHttpHeaders != null) { + Set setAllowedHttpHeaders = + parseStringToSet(allowedHttpHeaders); + Set lowerCaseHeaders = new HashSet(); + for (String header : setAllowedHttpHeaders) { + String lowerCase = header.toLowerCase(); + lowerCaseHeaders.add(lowerCase); + } + this.allowedHttpHeaders.clear(); + this.allowedHttpHeaders.addAll(lowerCaseHeaders); + } + + if (exposedHeaders != null) { + Set setExposedHeaders = parseStringToSet(exposedHeaders); + this.exposedHeaders.clear(); + this.exposedHeaders.addAll(setExposedHeaders); + } + + if (supportsCredentials != null) { + // For any value other then 'true' this will be false. + this.supportsCredentials = + Boolean.parseBoolean(supportsCredentials); + } + + if (preflightMaxAge != null) { + try { + if (!preflightMaxAge.isEmpty()) { + this.preflightMaxAge = Long.parseLong(preflightMaxAge); + } else { + this.preflightMaxAge = 0L; + } + } catch (NumberFormatException e) { + throw new ServletException("Unable to parse preflightMaxAge", e); + } + } + + if (loggingEnabled != null) { + // For any value other then 'true' this will be false. + this.loggingEnabled = Boolean.parseBoolean(loggingEnabled); + } + + if (decorateRequest != null) { + // For any value other then 'true' this will be false. + this.decorateRequest = Boolean.parseBoolean(decorateRequest); + } + } + + /** + * Takes a comma separated list and returns a Set. + * + * @param data + * A comma separated list of strings. + * @return Set + */ + private Set parseStringToSet(final String data) { + String[] splits; + + if (data != null && data.length() > 0) { + splits = data.split(","); + } else { + splits = new String[] {}; + } + + Set set = new HashSet(); + if (splits.length > 0) { + for (String split : splits) { + set.add(split.trim()); + } + } + + return set; + } + + /** + * Checks if a given origin is valid or not. Criteria: + *
    + *
  • If an encoded character is present in origin, it's not valid.
  • + *
  • Origin should be a valid {@link URI}
  • + *
+ * + * @param origin + * @see RFC952 + * @return + */ + public static boolean isValidOrigin(String origin) { + // Checks for encoded characters. Helps prevent CRLF injection. + if (origin.contains("%")) { + return false; + } + + URI originURI; + + try { + originURI = new URI(origin); + } catch (URISyntaxException e) { + return false; + } + // If scheme for URI is null, return false. Return true otherwise. + return originURI.getScheme() != null; + + } + + // -------------------------------------------------------------- Accessors + /** + * Determines if logging is enabled or not. + * + * @return true if it's enabled; false otherwise. + */ + public boolean isLoggingEnabled() { + return loggingEnabled; + } + + /** + * Determines if any origin is allowed to make CORS request. + * + * @return true if it's enabled; false otherwise. + */ + public boolean isAnyOriginAllowed() { + return anyOriginAllowed; + } + + /** + * Returns a {@link Set} of headers that should be exposed by browser. + * + * @return + */ + public Collection getExposedHeaders() { + return exposedHeaders; + } + + /** + * Determines is supports credentials is enabled + * + * @return + */ + public boolean isSupportsCredentials() { + return supportsCredentials; + } + + /** + * Returns the preflight response cache time in seconds. + * + * @return Time to cache in seconds. + */ + public long getPreflightMaxAge() { + return preflightMaxAge; + } + + /** + * Returns the {@link Set} of allowed origins that are allowed to make + * requests. + * + * @return {@link Set} + */ + public Collection getAllowedOrigins() { + return allowedOrigins; + } + + /** + * Returns a {@link Set} of HTTP methods that are allowed to make requests. + * + * @return {@link Set} + */ + public Collection getAllowedHttpMethods() { + return allowedHttpMethods; + } + + /** + * Returns a {@link Set} of headers support by resource. + * + * @return {@link Set} + */ + public Collection getAllowedHttpHeaders() { + return allowedHttpHeaders; + } + + // -------------------------------------------------- CORS Response Headers + /** + * The Access-Control-Allow-Origin header indicates whether a resource can + * be shared based by returning the value of the Origin request header in + * the response. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = + "Access-Control-Allow-Origin"; + + /** + * The Access-Control-Allow-Credentials header indicates whether the + * response to request can be exposed when the omit credentials flag is + * unset. When part of the response to a preflight request it indicates that + * the actual request can include user credentials. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS = + "Access-Control-Allow-Credentials"; + + /** + * The Access-Control-Expose-Headers header indicates which headers are safe + * to expose to the API of a CORS API specification + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = + "Access-Control-Expose-Headers"; + + /** + * The Access-Control-Max-Age header indicates how long the results of a + * preflight request can be cached in a preflight result cache. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE = + "Access-Control-Max-Age"; + + /** + * The Access-Control-Allow-Methods header indicates, as part of the + * response to a preflight request, which methods can be used during the + * actual request. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS = + "Access-Control-Allow-Methods"; + + /** + * The Access-Control-Allow-Headers header indicates, as part of the + * response to a preflight request, which header field names can be used + * during the actual request. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS = + "Access-Control-Allow-Headers"; + + // -------------------------------------------------- CORS Request Headers + /** + * The Origin header indicates where the cross-origin request or preflight + * request originates from. + */ + public static final String REQUEST_HEADER_ORIGIN = "Origin"; + + /** + * The Access-Control-Request-Method header indicates which method will be + * used in the actual request as part of the preflight request. + */ + public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD = + "Access-Control-Request-Method"; + + /** + * The Access-Control-Request-Headers header indicates which headers will be + * used in the actual request as part of the preflight request. + */ + public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS = + "Access-Control-Request-Headers"; + + // ----------------------------------------------------- Request attributes + /** + * The prefix to a CORS request attribute. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_PREFIX = "cors."; + + /** + * Attribute that contains the origin of the request. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_ORIGIN = + HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.origin"; + + /** + * Boolean value, suggesting if the request is a CORS request or not. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST = + HTTP_REQUEST_ATTRIBUTE_PREFIX + "isCorsRequest"; + + /** + * Type of CORS request, of type {@link CORSRequestType}. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE = + HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.type"; + + /** + * Request headers sent as 'Access-Control-Request-Headers' header, for + * pre-flight request. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS = + HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.headers"; + + // -------------------------------------------------------------- Constants + /** + * Enumerates varies types of CORS requests. Also, provides utility methods + * to determine the request type. + */ + public static enum CORSRequestType { + /** + * A simple HTTP request, i.e. it shouldn't be pre-flighted. + */ + SIMPLE, + /** + * A HTTP request that needs to be pre-flighted. + */ + ACTUAL, + /** + * A pre-flight CORS request, to get meta information, before a + * non-simple HTTP request is sent. + */ + PRE_FLIGHT, + /** + * Not a CORS request, but a normal request. + */ + NOT_CORS, + /** + * An invalid CORS request, i.e. it qualifies to be a CORS request, but + * fails to be a valid one. + */ + INVALID_CORS + } + + /** + * {@link Collection} of HTTP methods. Case sensitive. + * + * @see http://tools.ietf.org/html/rfc2616#section-5.1.1 + */ + public static final Collection HTTP_METHODS = new HashSet( + Arrays.asList("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", + "TRACE", "CONNECT")); + /** + * {@link Collection} of non-simple HTTP methods. Case sensitive. + */ + public static final Collection COMPLEX_HTTP_METHODS = + new HashSet( + Arrays.asList("PUT", "DELETE", "TRACE", "CONNECT")); + /** + * {@link Collection} of Simple HTTP methods. Case sensitive. + * + * @see http://www.w3.org/TR/cors/#terminology + */ + public static final Collection SIMPLE_HTTP_METHODS = + new HashSet( + Arrays.asList("GET", "POST", "HEAD")); + + /** + * {@link Collection} of Simple HTTP request headers. Case in-sensitive. + * + * @see http://www.w3.org/TR/cors/#terminology + */ + public static final Collection SIMPLE_HTTP_REQUEST_HEADERS = + new HashSet(Arrays.asList("Accept", "Accept-Language", + "Content-Language")); + + /** + * {@link Collection} of Simple HTTP request headers. Case in-sensitive. + * + * @see http://www.w3.org/TR/cors/#terminology + */ + public static final Collection SIMPLE_HTTP_RESPONSE_HEADERS = + new HashSet(Arrays.asList("Cache-Control", + "Content-Language", "Content-Type", "Expires", + "Last-Modified", "Pragma")); + + /** + * {@link Collection} of Simple HTTP request headers. Case in-sensitive. + * + * @see http://www.w3.org/TR/cors/#terminology + */ + public static final Collection SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES = + new HashSet(Arrays.asList( + "application/x-www-form-urlencoded", "multipart/form-data", + "text/plain")); + + // ------------------------------------------------ Configuration Defaults + /** + * By default, all origins are allowed to make requests. + */ + public static final String DEFAULT_ALLOWED_ORIGINS = "*"; + + /** + * By default, following methods are supported: GET, POST, HEAD and OPTIONS. + */ + public static final String DEFAULT_ALLOWED_HTTP_METHODS = + "GET,POST,HEAD,OPTIONS"; + + /** + * By default, time duration to cache pre-flight response is 30 mins. + */ + public static final String DEFAULT_PREFLIGHT_MAXAGE = "1800"; + + /** + * By default, support credentials is turned on. + */ + public static final String DEFAULT_SUPPORTS_CREDENTIALS = "true"; + + /** + * By default, following headers are supported: + * Origin,Accept,X-Requested-With, Content-Type, + * Access-Control-Request-Method, and Access-Control-Request-Headers. + */ + public static final String DEFAULT_ALLOWED_HTTP_HEADERS = + "Origin,Accept,X-Requested-With,Content-Type," + + + "Access-Control-Request-Method,Access-Control-Request-Headers"; + + /** + * By default, none of the headers are exposed in response. + */ + public static final String DEFAULT_EXPOSED_HEADERS = ""; + + /** + * By default, access log logging is turned off + */ + public static final String DEFAULT_LOGGING_ENABLED = "false"; + + /** + * By default, request is decorated with CORS attributes. + */ + public static final String DEFAULT_DECORATE_REQUEST = "true"; + + // ----------------------------------------Filter Config Init param-name(s) + /** + * Key to retrieve allowed origins from {@link FilterConfig}. + */ + public static final String PARAM_CORS_ALLOWED_ORIGINS = + "cors.allowed.origins"; + + /** + * Key to retrieve support credentials from {@link FilterConfig}. + */ + public static final String PARAM_CORS_SUPPORT_CREDENTIALS = + "cors.support.credentials"; + + /** + * Key to retrieve exposed headers from {@link FilterConfig}. + */ + public static final String PARAM_CORS_EXPOSED_HEADERS = + "cors.exposed.headers"; + + /** + * Key to retrieve allowed headers from {@link FilterConfig}. + */ + public static final String PARAM_CORS_ALLOWED_HEADERS = + "cors.allowed.headers"; + + /** + * Key to retrieve allowed methods from {@link FilterConfig}. + */ + public static final String PARAM_CORS_ALLOWED_METHODS = + "cors.allowed.methods"; + + /** + * Key to retrieve preflight max age from {@link FilterConfig}. + */ + public static final String PARAM_CORS_PREFLIGHT_MAXAGE = + "cors.preflight.maxage"; + + /** + * Key to retrieve access log logging flag. + */ + public static final String PARAM_CORS_LOGGING_ENABLED = + "cors.logging.enabled"; + + /** + * Key to determine if request should be decorated. + */ + public static final String PARAM_CORS_REQUEST_DECORATE = + "cors.request.decorate"; +} diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CorsTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CorsTest.java index 9a16f2c42ac..ea961066326 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CorsTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CorsTest.java @@ -2,6 +2,7 @@ package ca.uhn.fhir.rest.server; import static org.junit.Assert.*; +import java.io.IOException; import java.util.EnumSet; import java.util.concurrent.TimeUnit; @@ -10,6 +11,7 @@ import javax.servlet.DispatcherType; import org.apache.commons.io.IOUtils; import org.apache.http.Header; import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpOptions; import org.apache.http.client.methods.HttpPost; @@ -32,18 +34,91 @@ import ca.uhn.fhir.model.dstu.resource.Patient; import ca.uhn.fhir.rest.server.RestfulServerSelfReferenceTest.DummyPatientResourceProvider; import ca.uhn.fhir.util.PortUtil; -/** - * Created by dsotnikov on 2/25/2014. - */ public class CorsTest { private static CloseableHttpClient ourClient; + private static Server ourServer; + private static String ourBaseUri; private static final FhirContext ourCtx = FhirContext.forDstu1(); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CorsTest.class); + @Test + public void testRequestWithNullOrigin() throws ClientProtocolException, IOException { + { + HttpOptions httpOpt = new HttpOptions(ourBaseUri + "/Organization/b27ed191-f62d-4128-d99d-40b5e84f2bf2"); + httpOpt.addHeader("Access-Control-Request-Method", "GET"); + httpOpt.addHeader("Origin", "null"); + httpOpt.addHeader("Access-Control-Request-Headers", "accept, x-fhir-starter, content-type"); + HttpResponse status = ourClient.execute(httpOpt); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info("Response was:\n{}", responseContent); + assertEquals("GET", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_METHODS).getValue()); + assertEquals("null", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN).getValue()); + } + } + @Test public void testContextWithSpace() throws Exception { + { + HttpOptions httpOpt = new HttpOptions(ourBaseUri + "/Organization/b27ed191-f62d-4128-d99d-40b5e84f2bf2"); + httpOpt.addHeader("Access-Control-Request-Method", "POST"); + httpOpt.addHeader("Origin", "http://www.fhir-starter.com"); + httpOpt.addHeader("Access-Control-Request-Headers", "accept, x-fhir-starter, content-type"); + HttpResponse status = ourClient.execute(httpOpt); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info("Response was:\n{}", responseContent); + assertEquals("POST", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_METHODS).getValue()); + assertEquals("http://www.fhir-starter.com", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN).getValue()); + } + { + String uri = ourBaseUri + "/Patient?identifier=urn:hapitest:mrns%7C00001"; + HttpGet httpGet = new HttpGet(uri); + httpGet.addHeader("X-FHIR-Starter", "urn:fhir.starter"); + httpGet.addHeader("Origin", "http://www.fhir-starter.com"); + HttpResponse status = ourClient.execute(httpGet); + + Header origin = status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN); + assertEquals("http://www.fhir-starter.com", origin.getValue()); + + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info("Response was:\n{}", responseContent); + + assertEquals(200, status.getStatusLine().getStatusCode()); + Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent); + + assertEquals(1, bundle.getEntries().size()); + } + { + HttpPost httpOpt = new HttpPost(ourBaseUri + "/Patient"); + httpOpt.addHeader("Access-Control-Request-Method", "POST"); + httpOpt.addHeader("Origin", "http://www.fhir-starter.com"); + httpOpt.addHeader("Access-Control-Request-Headers", "accept, x-fhir-starter, content-type"); + httpOpt.setEntity(new StringEntity(ourCtx.newXmlParser().encodeResourceToString(new Patient()))); + HttpResponse status = ourClient.execute(httpOpt); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info("Response: {}", status); + ourLog.info("Response was:\n{}", responseContent); + assertEquals("http://www.fhir-starter.com", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN).getValue()); + } + } + + public static void afterClass() throws Exception { + ourServer.stop(); + ourClient.close(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + int port = PortUtil.findFreePort(); - Server server = new Server(port); + ourServer = new Server(port); RestfulServer restServer = new RestfulServer(ourCtx); restServer.setResourceProviders(new DummyPatientResourceProvider()); @@ -52,7 +127,7 @@ public class CorsTest { ServletHolder servletHolder = new ServletHolder(restServer); FilterHolder fh = new FilterHolder(); - fh.setHeldClass(CORSFilter.class); + fh.setHeldClass(CORSFilter_.class); fh.setInitParameter("cors.logging.enabled", "true"); fh.setInitParameter("cors.allowed.origins", "*"); fh.setInitParameter("cors.allowed.headers", "x-fhir-starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers"); @@ -65,69 +140,11 @@ public class CorsTest { ch.addFilter(fh, "/*", EnumSet.of(DispatcherType.INCLUDE, DispatcherType.REQUEST)); ContextHandlerCollection contexts = new ContextHandlerCollection(); - server.setHandler(contexts); + ourServer.setHandler(contexts); - server.setHandler(ch); - server.start(); - try { - String baseUri = "http://localhost:" + port + "/rootctx/rcp2/fhirctx/fcp2"; - - { - HttpOptions httpOpt = new HttpOptions(baseUri + "/Organization/b27ed191-f62d-4128-d99d-40b5e84f2bf2"); - httpOpt.addHeader("Access-Control-Request-Method", "POST"); - httpOpt.addHeader("Origin", "http://www.fhir-starter.com"); - httpOpt.addHeader("Access-Control-Request-Headers", "accept, x-fhir-starter, content-type"); - HttpResponse status = ourClient.execute(httpOpt); - String responseContent = IOUtils.toString(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); - ourLog.info("Response was:\n{}", responseContent); - assertEquals("POST", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_METHODS).getValue()); - assertEquals("http://www.fhir-starter.com", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN).getValue()); - } - { - String uri = baseUri + "/Patient?identifier=urn:hapitest:mrns%7C00001"; - HttpGet httpGet = new HttpGet(uri); - httpGet.addHeader("X-FHIR-Starter", "urn:fhir.starter"); - httpGet.addHeader("Origin", "http://www.fhir-starter.com"); - HttpResponse status = ourClient.execute(httpGet); - - Header origin = status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN); - assertEquals("http://www.fhir-starter.com", origin.getValue()); - - String responseContent = IOUtils.toString(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); - ourLog.info("Response was:\n{}", responseContent); - - assertEquals(200, status.getStatusLine().getStatusCode()); - Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent); - - assertEquals(1, bundle.getEntries().size()); - } - { - HttpPost httpOpt = new HttpPost(baseUri + "/Patient"); - httpOpt.addHeader("Access-Control-Request-Method", "POST"); - httpOpt.addHeader("Origin", "http://www.fhir-starter.com"); - httpOpt.addHeader("Access-Control-Request-Headers", "accept, x-fhir-starter, content-type"); - httpOpt.setEntity(new StringEntity(ourCtx.newXmlParser().encodeResourceToString(new Patient()))); - HttpResponse status = ourClient.execute(httpOpt); - String responseContent = IOUtils.toString(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); - ourLog.info("Response: {}", status); - ourLog.info("Response was:\n{}", responseContent); - assertEquals("http://www.fhir-starter.com", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN).getValue()); - } - } finally { - server.stop(); - } - - } - - @BeforeClass - public static void beforeClass() throws Exception { - PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); - HttpClientBuilder builder = HttpClientBuilder.create(); - builder.setConnectionManager(connectionManager); - ourClient = builder.build(); + ourServer.setHandler(ch); + ourServer.start(); + ourBaseUri = "http://localhost:" + port + "/rootctx/rcp2/fhirctx/fcp2"; } diff --git a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/model/PrimitiveType.java b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/model/PrimitiveType.java index 74069cd03b5..099234803f4 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/model/PrimitiveType.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/model/PrimitiveType.java @@ -80,7 +80,7 @@ public abstract class PrimitiveType extends Type implements IPrimitiveType } public boolean hasValue() { - return !isEmpty(); + return !StringUtils.isBlank(getValueAsString()); } public String getValueAsString() { diff --git a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/model/PrimititeTest.java b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/model/PrimititeTest.java new file mode 100644 index 00000000000..8ea63a31166 --- /dev/null +++ b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/model/PrimititeTest.java @@ -0,0 +1,22 @@ +package ca.uhn.fhir.model; + +import static org.junit.Assert.*; + +import org.hl7.fhir.instance.model.DecimalType; +import org.hl7.fhir.instance.model.StringType; +import org.junit.Test; +import org.thymeleaf.standard.expression.NumberTokenExpression; + +public class PrimititeTest { + + @Test + public void testHasValue() { + StringType type = new StringType(); + assertFalse(type.hasValue()); + type.addExtension().setUrl("http://foo").setValue(new DecimalType(123)); + assertFalse(type.hasValue()); + type.setValue("Hello"); + assertTrue(type.hasValue()); + } + +} diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/mvc/AnnotationMethodHandlerAdapterConfigurer.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/mvc/AnnotationMethodHandlerAdapterConfigurer.java index 63fd0e56718..43a126767d2 100644 --- a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/mvc/AnnotationMethodHandlerAdapterConfigurer.java +++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/mvc/AnnotationMethodHandlerAdapterConfigurer.java @@ -3,10 +3,13 @@ package ca.uhn.fhir.to.mvc; import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; public class AnnotationMethodHandlerAdapterConfigurer { + @Autowired + @Qualifier("requestMappingHandlerAdapter") private RequestMappingHandlerAdapter adapter; @PostConstruct diff --git a/pom.xml b/pom.xml index 1f35f6c4c54..30b26196539 100644 --- a/pom.xml +++ b/pom.xml @@ -258,7 +258,7 @@ ch.qos.logback logback-classic - 1.1.2 + 1.1.3 com.google.guava @@ -653,7 +653,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 2.10.1 + 2.10.3 org.apache.maven.plugins @@ -866,7 +866,7 @@ com.puppycrawl.tools checkstyle - 6.7 + 6.11.2 diff --git a/restful-server-example/src/main/java/ca/uhn/example/config/FhirTesterConfig.java b/restful-server-example/src/main/java/ca/uhn/example/config/FhirTesterConfig.java index 61b13d577ad..847703413ca 100644 --- a/restful-server-example/src/main/java/ca/uhn/example/config/FhirTesterConfig.java +++ b/restful-server-example/src/main/java/ca/uhn/example/config/FhirTesterConfig.java @@ -49,6 +49,13 @@ public class FhirTesterConfig { .withFhirVersion(FhirVersionEnum.DSTU2) .withBaseUrl("http://fhirtest.uhn.ca/baseDstu2") .withName("Public HAPI Test Server"); + + /* + * Use the method below to supply a client "factory" which can be used + * if your server requires authentication + */ + // retVal.setClientFactory(clientFactory); + return retVal; } diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 17f868d19e7..314905c9d16 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -13,6 +13,7 @@
  • Commons-lang3 (Core): 3.3.2 -> 3.4
  • +
  • Logback (Core): 1.1.2 -> 1.1.3
  • Springframework (JPA, Web Tester): 4.1.5 -> 4.2.2
  • Hibernate (JPA, Web Tester): 4.2.17 -> 5.0.2
  • Hibernate Validator (JPA, Web Tester): 5.2.1 -> 5.2.2
  • @@ -21,6 +22,10 @@ ]]> + + JPA and Tester Overlay now use Spring Java config files instead + of the older XML config files. All example projects have been updated. + JPA server removes duplicate resource index entries before storing them (e.g. if a patient has the same name twice, only one index entry is created diff --git a/src/site/xdoc/doc_server_tester.xml.vm b/src/site/xdoc/doc_server_tester.xml.vm index d5b20b2a9be..090fedc7fbc 100644 --- a/src/site/xdoc/doc_server_tester.xml.vm +++ b/src/site/xdoc/doc_server_tester.xml.vm @@ -48,6 +48,13 @@ war provided
    + + ca.uhn.hapi.fhir + hapi-fhir-testpage-overlay + ${project.version} + classes + provided + ]]>

    @@ -75,18 +82,12 @@

    - Next, create the following directory in your project - if it doesn't already exist:
    - src/main/webapp/WEB-INF -

    - -

    - Then, create a file in that directory - called hapi-fhir-tester-config.xml + Then, create a Java source file + called FhirTesterConfig.java and copy in the following contents:

    - +

    @@ -98,37 +99,39 @@

    - Finally, in the same directory you should open you web.xml file. This file is - required in order to deploy to a servlet container and you should create it if - it does not already exist. Place the following contents in that file. + Next, create the following directory in your project + if it doesn't already exist:
    + src/main/webapp/WEB-INF

    - - org.springframework.web.context.ContextLoaderListener - - - contextConfigLocation - - /WEB-INF/hapi-fhir-tester-application-context.xml - /WEB-INF/hapi-fhir-tester-config.xml - - - +

    + In this directory you should open your web.xml file, or create + it if it doesn't exist. + This file is + required in order to deploy to a servlet container and you should create it if + it does not already exist. Place the following contents in that file, adjusting + the package on the FhirTesterConfig to match the + actual package in which you placed this file. +

    + + spring org.springframework.web.servlet.DispatcherServlet + + contextClass + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + contextConfigLocation - - /WEB-INF/hapi-fhir-tester-application-context.xml - /WEB-INF/hapi-fhir-tester-config.xml - + ca.uhn.example.config.FhirTesterConfig 2
    spring /tester/* - ]]> + +]]> @@ -193,19 +196,6 @@ a client.

    -

    - The following example shows an implementation of the client factory which registers - an authorization interceptor with hardcoded credentials. -

    - - - - -

    - This client factory is then registered with the TesterConfig in the hapi-fhir-tester-config.xml - file, as shown above. -

    -