diff --git a/hapi-fhir-base-example-embedded-ws/pom.xml b/hapi-fhir-base-example-embedded-ws/pom.xml new file mode 100644 index 00000000000..98dac37f470 --- /dev/null +++ b/hapi-fhir-base-example-embedded-ws/pom.xml @@ -0,0 +1,47 @@ + + 4.0.0 + + ca.uhn.hapi.fhir + hapi-fhir + 1.2 + + + jar + + hapi-fhir-base-example-embedded-ws + + + org.eclipse.jetty + jetty-servlet + + + org.eclipse.jetty + jetty-webapp + + + com.google.guava + guava + + + com.google.inject + guice + 3.0 + + + com.google.inject.extensions + guice-servlet + 3.0 + + + com.sun.jersey.contribs + jersey-guice + 1.18.1 + + + ca.uhn.hapi.fhir + hapi-fhir-structures-dstu2 + 1.2 + + + \ No newline at end of file diff --git a/hapi-fhir-base-example-embedded-ws/src/main/java/embedded/ContextListener.java b/hapi-fhir-base-example-embedded-ws/src/main/java/embedded/ContextListener.java new file mode 100644 index 00000000000..02019b886cb --- /dev/null +++ b/hapi-fhir-base-example-embedded-ws/src/main/java/embedded/ContextListener.java @@ -0,0 +1,37 @@ +package embedded; +import java.util.Map; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.servlet.GuiceServletContextListener; +import com.sun.jersey.api.container.filter.GZIPContentEncodingFilter; +import com.sun.jersey.api.core.ResourceConfig; +import com.sun.jersey.guice.JerseyServletModule; + +import filters.CharsetResponseFilter; +import filters.CorsResponseFilter; + +public class ContextListener extends GuiceServletContextListener { + + @Override + protected Injector getInjector() { + + return Guice.createInjector(new JerseyServletModule() { + + @Override + protected void configureServlets() { + final Map params = ImmutableMap + . builder() + .put(ResourceConfig.PROPERTY_CONTAINER_RESPONSE_FILTERS, + Joiner.on(";").join( + CharsetResponseFilter.class.getName(), + CorsResponseFilter.class.getName(), + GZIPContentEncodingFilter.class + .getName())).build(); + serve("/model/*").with(FhirRestfulServlet.class, params); + } + }); + } +} diff --git a/hapi-fhir-base-example-embedded-ws/src/main/java/embedded/FhirRestfulServlet.java b/hapi-fhir-base-example-embedded-ws/src/main/java/embedded/FhirRestfulServlet.java new file mode 100644 index 00000000000..a7e082015b9 --- /dev/null +++ b/hapi-fhir-base-example-embedded-ws/src/main/java/embedded/FhirRestfulServlet.java @@ -0,0 +1,59 @@ +package embedded; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Singleton; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; +import ca.uhn.fhir.narrative.INarrativeGenerator; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; + +@Singleton +public class FhirRestfulServlet extends RestfulServer { + + /** + * + */ + private static final long serialVersionUID = -3931111342737918913L; + + public FhirRestfulServlet() { + super(FhirContext.forDstu2()); // Support DSTU2 + } + + /** + * This method is called automatically when the servlet is initializing. + */ + @Override + public void initialize() { + /* + * Two resource providers are defined. Each one handles a specific type + * of resource. + */ + final List providers = new ArrayList(); + providers.add(new SomeResourceProvider()); + setResourceProviders(providers); + + /* + * Use a narrative generator. This is a completely optional step, but + * can be useful as it causes HAPI to generate narratives for resources + * which don't otherwise have one. + */ + final INarrativeGenerator narrativeGen = new DefaultThymeleafNarrativeGenerator(); + getFhirContext().setNarrativeGenerator(narrativeGen); + + /* + * Tells HAPI to use content types which are not technically FHIR + * compliant when a browser is detected as the requesting client. This + * prevents browsers from trying to download resource responses instead + * of displaying them inline which can be handy for troubleshooting. + */ + setUseBrowserFriendlyContentTypes(true); + + registerInterceptor(new ResponseHighlighterInterceptor()); + + } +} diff --git a/hapi-fhir-base-example-embedded-ws/src/main/java/embedded/ServerStartup.java b/hapi-fhir-base-example-embedded-ws/src/main/java/embedded/ServerStartup.java new file mode 100644 index 00000000000..fb8dde91365 --- /dev/null +++ b/hapi-fhir-base-example-embedded-ws/src/main/java/embedded/ServerStartup.java @@ -0,0 +1,25 @@ +package embedded; +import java.util.EnumSet; + +import javax.servlet.DispatcherType; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletContextHandler; + +import com.google.inject.servlet.GuiceFilter; + +public class ServerStartup { + + public static void main(final String[] args) throws Exception { + + final Server server = new Server(9090); + final ServletContextHandler sch = new ServletContextHandler(server, "/"); + sch.addEventListener(new ContextListener()); + sch.addFilter(GuiceFilter.class, "/*", + EnumSet.of(DispatcherType.REQUEST)); + sch.addServlet(DefaultServlet.class, "/"); + server.start(); + } + +} diff --git a/hapi-fhir-base-example-embedded-ws/src/main/java/embedded/SomeResourceProvider.java b/hapi-fhir-base-example-embedded-ws/src/main/java/embedded/SomeResourceProvider.java new file mode 100644 index 00000000000..aac9da8adb9 --- /dev/null +++ b/hapi-fhir-base-example-embedded-ws/src/main/java/embedded/SomeResourceProvider.java @@ -0,0 +1,28 @@ +package embedded; + +import java.util.List; + +import org.hl7.fhir.instance.model.api.IBaseResource; + +import ca.uhn.fhir.model.dstu2.resource.Practitioner; +import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.rest.annotation.RequiredParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; + +public class SomeResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Practitioner.class; + } + + @Search() + public List findPractitionersByName( + @RequiredParam(name = Practitioner.SP_NAME) final StringDt theName) { + throw new UnprocessableEntityException( + "Please provide more than 4 characters for the name"); + } + +} diff --git a/hapi-fhir-base-example-embedded-ws/src/main/java/filters/CharsetResponseFilter.java b/hapi-fhir-base-example-embedded-ws/src/main/java/filters/CharsetResponseFilter.java new file mode 100644 index 00000000000..656f7535578 --- /dev/null +++ b/hapi-fhir-base-example-embedded-ws/src/main/java/filters/CharsetResponseFilter.java @@ -0,0 +1,24 @@ +package filters; + + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; + +import com.sun.jersey.spi.container.ContainerRequest; +import com.sun.jersey.spi.container.ContainerResponse; +import com.sun.jersey.spi.container.ContainerResponseFilter; + +public class CharsetResponseFilter implements ContainerResponseFilter { + + @Override + public ContainerResponse filter(final ContainerRequest request, + final ContainerResponse response) { + + final MediaType contentType = response.getMediaType(); + if (contentType != null) { + response.getHttpHeaders().putSingle(HttpHeaders.CONTENT_TYPE, + contentType.toString() + ";charset=UTF-8"); + } + return response; + } +} \ No newline at end of file diff --git a/hapi-fhir-base-example-embedded-ws/src/main/java/filters/CorsResponseFilter.java b/hapi-fhir-base-example-embedded-ws/src/main/java/filters/CorsResponseFilter.java new file mode 100644 index 00000000000..cab06c28419 --- /dev/null +++ b/hapi-fhir-base-example-embedded-ws/src/main/java/filters/CorsResponseFilter.java @@ -0,0 +1,33 @@ +package filters; + + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; + +import com.sun.jersey.spi.container.ContainerRequest; +import com.sun.jersey.spi.container.ContainerResponse; +import com.sun.jersey.spi.container.ContainerResponseFilter; + +public class CorsResponseFilter implements ContainerResponseFilter { + + @Override + public ContainerResponse filter(final ContainerRequest req, + final ContainerResponse contResp) { + + final ResponseBuilder resp = Response.fromResponse(contResp + .getResponse()); + resp.header("Access-Control-Allow-Origin", "*").header( + "Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + + final String reqHead = req + .getHeaderValue("Access-Control-Request-Headers"); + + if (null != reqHead && !reqHead.equals("")) { + resp.header("Access-Control-Allow-Headers", reqHead); + } + + contResp.setResponse(resp.build()); + return contResp; + } + +} \ No newline at end of file diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java index 300fcd3f4ce..bb0ae9a495b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.rest.method; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + /* * #%L * HAPI FHIR - Core Library @@ -138,6 +140,9 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding implem return false; } } + if (isNotBlank(theRequest.getCompartmentName())) { + return false; + } if (theRequest.getRequestType() != RequestTypeEnum.GET) { ourLog.trace("Method {} doesn't match because request type is not GET: {}", theRequest.getId(), theRequest.getRequestType()); return false; @@ -198,6 +203,9 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding implem return resource; case BUNDLE_PROVIDER: return new SimpleBundleProvider(resource); + case BUNDLE_RESOURCE: + case METHOD_OUTCOME: + break; } throw new IllegalStateException("" + getMethodReturnType()); // should not happen diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java index 11bf98d28dd..2dfdafc63a7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java @@ -43,6 +43,9 @@ import org.hibernate.search.annotations.Field; }) public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchParam { + /* + * Note that MYSQL chokes on unique indexes for lengths > 255 so be careful here + */ public static final int MAX_LENGTH = 200; private static final long serialVersionUID = 1L; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java index 5e3adeca952..6fa9351f7ef 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java @@ -43,7 +43,10 @@ import org.hibernate.search.annotations.Indexed; //@formatter:on public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchParam { - public static final int MAX_LENGTH = 256; + /* + * Note that MYSQL chokes on unique indexes for lengths > 255 so be careful here + */ + public static final int MAX_LENGTH = 255; private static final long serialVersionUID = 1L; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/IndexNonDeletedInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/IndexNonDeletedInterceptor.java index e440cf65917..3209b63c377 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/IndexNonDeletedInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/IndexNonDeletedInterceptor.java @@ -55,4 +55,4 @@ public class IndexNonDeletedInterceptor implements EntityIndexingInterceptor ../hapi-fhir-structures-dstu/src/main/resources - true + false ../hapi-fhir-structures-dstu/target/generated-resources/tinder - true + false ../hapi-fhir-structures-dstu2/src/main/resources - true + false ../hapi-fhir-structures-dstu2/target/generated-resources/tinder - true + false ../hapi-fhir-structures-hl7org-dstu2/src/resources/java - true + false diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/CompartmentDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/CompartmentDstu2Test.java new file mode 100644 index 00000000000..bcd1312565d --- /dev/null +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/CompartmentDstu2Test.java @@ -0,0 +1,187 @@ +package ca.uhn.fhir.rest.server; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.dstu2.composite.ResourceReferenceDt; +import ca.uhn.fhir.model.dstu2.resource.Encounter; +import ca.uhn.fhir.model.dstu2.resource.Observation; +import ca.uhn.fhir.model.dstu2.resource.Patient; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; +import ca.uhn.fhir.rest.annotation.Create; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.OptionalParam; +import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.util.PortUtil; + +/** + * Created by dsotnikov on 2/25/2014. + */ +public class CompartmentDstu2Test { + private static CloseableHttpClient ourClient; + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CompartmentDstu2Test.class); + private static int ourPort; + private static Server ourServer; + private static String ourLastMethod; + private static FhirContext ourCtx = FhirContext.forDstu2(); + private static IdDt ourLastId; + + @Before + public void before() { + ourLastMethod = null; + ourLastId = null; + } + + + @Test + public void testReadFirst() throws Exception { + init(new TempPatientResourceProvider()); + + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123"); + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info("Response was:\n{}", responseContent); + assertEquals("read", ourLastMethod); + assertEquals("Patient", ourLastId.getResourceType()); + assertEquals("123", ourLastId.getIdPart()); + assertThat(responseContent, startsWith(" getResourceType() { + return Patient.class; + } + + @Read() + public Patient method1Read(final @IdParam IdDt theId) { + ourLastMethod = "read"; + ourLastId = theId; + Patient patient = new Patient(); + patient.setId(theId); + return patient; + } + + @Search(compartmentName = "Encounter") + public List method2SearchCompartment(final @IdParam IdDt theId) { + ourLastId = theId; + ourLastMethod = "searchEncounterCompartment"; + System.out.println("Encounter compartment search"); + List encounters = new ArrayList(); + Encounter encounter = new Encounter(); + encounter.setId("1"); + encounter.setPatient(new ResourceReferenceDt(theId)); + encounters.add(encounter); + return encounters; + } + + @Search(compartmentName = "Observation") + public List method2SearchCompartment2(final @IdParam IdDt theId) { + ourLastId = theId; + ourLastMethod = "searchObservationCompartment"; + System.out.println("Encounter compartment search"); + List encounters = new ArrayList(); + Observation obs = new Observation(); + obs.setId("1"); + obs.setSubject(new ResourceReferenceDt(theId)); + encounters.add(obs); + return encounters; + } + + } + +} diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/TesterConfig.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/TesterConfig.java index df6d7146786..159f5fb8f60 100644 --- a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/TesterConfig.java +++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/TesterConfig.java @@ -27,6 +27,7 @@ public class TesterConfig { public IServerBuilderStep1 addServer() { ServerBuilder retVal = new ServerBuilder(); + myServerBuilders.add(retVal); return retVal; } diff --git a/pom.xml b/pom.xml index c207d763b2f..5bfdc74a027 100644 --- a/pom.xml +++ b/pom.xml @@ -190,6 +190,10 @@ botunge Thomas Andersen + + samlanfranchi + Sam Lanfranchi + @@ -1374,4 +1378,7 @@ + + hapi-fhir-base-example-embedded-ws + diff --git a/src/changes/changes.xml b/src/changes/changes.xml index db3b07ab36c..4fa882f66e3 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -194,6 +194,19 @@ handle JSON responses or use interceptors. Thanks to JT for reporting! + + JPA server maximumn length for a URI search parameter has been reduced from + 256 to 255 in order to accomodate MySQL's indexing requirements + + + Server failed to respond correctly to compartment search operations + if the same provider also contained a read operation. Thanks to GitHub user + @am202 for reporting! + + + FIx issue in testpage-overlay's new Java configuration where only the first + configured server actually gets used. + diff --git a/vagrant/Vagrantfile b/vagrant/Vagrantfile index 7624d3e7640..f635f95e279 100644 --- a/vagrant/Vagrantfile +++ b/vagrant/Vagrantfile @@ -86,7 +86,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| proxy_port: 80, # ssl_port: 443, authbind: 'yes', - java_options: '-Dfhir.logdir=/var/log/fhir -Dfhir.db.location=/var/fhirdb' + java_options: '-Dfhir.logdir=/var/log/fhir -Dfhir.db.location=/var/fhirdb -Dfhir.db.location.dstu2=/var/fhirdb2 -Dfhir.lucene.location.dstu2=/var/fhirlucene2' }, mysql: { version: '5.6',