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.logbacklogback-classic
- 1.1.2
+ 1.1.3com.google.guava
@@ -653,7 +653,7 @@
org.apache.maven.pluginsmaven-javadoc-plugin
- 2.10.1
+ 2.10.3org.apache.maven.plugins
@@ -866,7 +866,7 @@
com.puppycrawl.toolscheckstyle
- 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 @@
warprovided
+
+ 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
+ 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.
+