From 64d29c675c718ac618466b607d95717d00bf1cef Mon Sep 17 00:00:00 2001 From: Jessie James Cosare Date: Wed, 20 Feb 2019 09:32:40 +0800 Subject: [PATCH 1/7] - fixed issue on local conformance statement not loading properly --- README.md | 6 +++--- pom.xml | 2 +- src/main/resources/hapi.properties | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ad0eb2b..45c8eab 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ mvn jetty:run Then, browse to the following link to use the server: -[http://localhost:8080/hapi-fhir-jpaserver/](http://localhost:8080/hapi-fhir-jpaserver/) +[http://localhost:8080/](http://localhost:8080/) # Deploying to a Container @@ -40,7 +40,7 @@ This will create a file called `hapi-fhir-jpaserver.war` in your `target` direct Again, browse to the following link to use the server (note that the port 8080 may not be correct depending on how your server is configured). -[http://localhost:8080/hapi-fhir-jpaserver/](http://localhost:8080/hapi-fhir-jpaserver/) +[http://localhost:8080/](http://localhost:8080/) # Customizing The Web Testpage UI @@ -62,4 +62,4 @@ To configure the starter app to use MySQL, instead of the default Derby, update * datasource.url=jdbc:mysql://localhost:3306/hapi_dstu3 * hibernate.dialect=org.hibernate.dialect.MySQL5Dialect -It is important to use MySQL5Dialect when using MySQL version 5+. \ No newline at end of file +It is important to use MySQL5Dialect when using MySQL version 5+. diff --git a/pom.xml b/pom.xml index 3bf5549..8a42a4e 100644 --- a/pom.xml +++ b/pom.xml @@ -216,7 +216,7 @@ 9.4.8.v20180619 - /hapi-fhir-jpaserver + / true diff --git a/src/main/resources/hapi.properties b/src/main/resources/hapi.properties index 332825e..277618d 100644 --- a/src/main/resources/hapi.properties +++ b/src/main/resources/hapi.properties @@ -6,7 +6,7 @@ fhir_version=DSTU3 # This is the address that the FHIR server will report as its own address. # If this server will be deployed (for example) to an internet accessible # server, put the DNS name of that server here. -server_address=http://localhost/fhir/ +server_address=http://localhost:8080/fhir/ # This is the context path for the FHIR endpoint. If this is changed, the # setting above should also be changed. @@ -46,4 +46,4 @@ hibernate.cache.use_structured_entries=false hibernate.cache.use_minimal_puts=false hibernate.search.default.directory_provider=filesystem hibernate.search.default.indexBase=target/lucenefiles -hibernate.search.lucene_version=LUCENE_CURRENT \ No newline at end of file +hibernate.search.lucene_version=LUCENE_CURRENT From 6907ae34ba3ca8d01f27948257228dc5f835483a Mon Sep 17 00:00:00 2001 From: Jessie James Cosare Date: Wed, 20 Feb 2019 17:02:53 +0800 Subject: [PATCH 2/7] - updated project base path resolution - removed .keep_hapi-fhir-jpaserver-starter file as its not needed anymore --- .../ca/uhn/fhir/jpa/starter/ExampleServerDstu2IT.java | 9 ++------- .../ca/uhn/fhir/jpa/starter/ExampleServerDstu3IT.java | 9 ++------- .../java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java | 9 ++------- src/test/resources/.keep_hapi-fhir-jpaserver-starter | 0 4 files changed, 6 insertions(+), 21 deletions(-) delete mode 100644 src/test/resources/.keep_hapi-fhir-jpaserver-starter diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu2IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu2IT.java index e1aeb5f..28acdfc 100644 --- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu2IT.java +++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu2IT.java @@ -15,6 +15,7 @@ import org.junit.Test; import java.io.File; import java.io.IOException; +import java.nio.file.Paths; import static org.junit.Assert.assertEquals; @@ -56,13 +57,7 @@ public class ExampleServerDstu2IT { @BeforeClass public static void beforeClass() throws Exception { - /* - * This runs under maven, and I'm not sure how else to figure out the target directory from code.. - */ - String path = ExampleServerDstu2IT.class.getClassLoader().getResource(".keep_hapi-fhir-jpaserver-starter").getPath(); - path = new File(path).getParent(); - path = new File(path).getParent(); - path = new File(path).getParent(); + String path = Paths.get("").toAbsolutePath().toString(); ourLog.info("Project base path is: {}", path); diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu3IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu3IT.java index 43a5b89..577f30e 100644 --- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu3IT.java +++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu3IT.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertEquals; import java.io.File; import java.io.IOException; +import java.nio.file.Paths; import ca.uhn.fhir.util.PortUtil; import org.eclipse.jetty.server.Server; @@ -55,13 +56,7 @@ public class ExampleServerDstu3IT { @BeforeClass public static void beforeClass() throws Exception { - /* - * This runs under maven, and I'm not sure how else to figure out the target directory from code.. - */ - String path = ExampleServerDstu3IT.class.getClassLoader().getResource(".keep_hapi-fhir-jpaserver-starter").getPath(); - path = new File(path).getParent(); - path = new File(path).getParent(); - path = new File(path).getParent(); + String path = Paths.get("").toAbsolutePath().toString(); ourLog.info("Project base path is: {}", path); diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java index bccdd20..4f66d28 100644 --- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java +++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java @@ -15,6 +15,7 @@ import org.junit.Test; import java.io.File; import java.io.IOException; +import java.nio.file.Paths; import static org.junit.Assert.assertEquals; @@ -56,13 +57,7 @@ public class ExampleServerR4IT { @BeforeClass public static void beforeClass() throws Exception { - /* - * This runs under maven, and I'm not sure how else to figure out the target directory from code.. - */ - String path = ExampleServerR4IT.class.getClassLoader().getResource(".keep_hapi-fhir-jpaserver-starter").getPath(); - path = new File(path).getParent(); - path = new File(path).getParent(); - path = new File(path).getParent(); + String path = Paths.get("").toAbsolutePath().toString(); ourLog.info("Project base path is: {}", path); diff --git a/src/test/resources/.keep_hapi-fhir-jpaserver-starter b/src/test/resources/.keep_hapi-fhir-jpaserver-starter deleted file mode 100644 index e69de29..0000000 From c5bfade9ce03e64172993dca47be0363b38b99e5 Mon Sep 17 00:00:00 2001 From: Jessie James Cosare Date: Fri, 8 Mar 2019 13:22:25 +0800 Subject: [PATCH 3/7] - fixed issue on ui regarding pagination of results by configuring refuseToFetchThirdPartyUrls - updated fhir version to R4 and used mysql as datasource with docs updated - transferred jetty plugin to be viewable on intellij - disabled oss snapshots - replaced corsFilter based on new hapi docs --- README.md | 11 ++- pom.xml | 47 ++++----- .../fhir/jpa/starter/FhirTesterConfig.java | 1 + .../uhn/fhir/jpa/starter/HapiProperties.java | 5 + .../fhir/jpa/starter/JpaRestfulServer.java | 33 +++++++ src/main/resources/hapi.properties | 13 +-- src/main/webapp/WEB-INF/web.xml | 98 +++++++++---------- 7 files changed, 119 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 45c8eab..ab908be 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,13 @@ Much of this HAPI starter project can be configured using the properties file in ## MySql -To configure the starter app to use MySQL, instead of the default Derby, update the hapi.properties file to have the following: +To configure the starter app to with MySQL, use the commands below to add user and database: -* datasource.driver=com.mysql.jdbc.Driver -* datasource.url=jdbc:mysql://localhost:3306/hapi_dstu3 -* hibernate.dialect=org.hibernate.dialect.MySQL5Dialect +``` +CREATE USER 'fhirUser'@'localhost' IDENTIFIED BY 'fhirPass'; +CREATE DATABASE fhir_r4; +GRANT ALL PRIVILEGES ON fhir_r4.* to 'fhirUser'@'localhost'; +FLUSH PRIVILEGES; +``` It is important to use MySQL5Dialect when using MySQL version 5+. diff --git a/pom.xml b/pom.xml index 8a42a4e..fb188fc 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ oss-snapshots - true + false https://oss.sonatype.org/content/repositories/snapshots/ @@ -111,16 +111,11 @@ - - org.ebaysf.web - cors-filter - - - servlet-api - javax.servlet - - - + + + + + @@ -207,24 +202,20 @@ hapi-fhir-jpaserver - - - - - org.eclipse.jetty - jetty-maven-plugin - 9.4.8.v20180619 - - - / - true - - - - - - + + + org.eclipse.jetty + jetty-maven-plugin + 9.4.8.v20180619 + + + / + true + + + + org.apache.maven.plugins diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/FhirTesterConfig.java b/src/main/java/ca/uhn/fhir/jpa/starter/FhirTesterConfig.java index f7d8a77..11ccbcb 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/FhirTesterConfig.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/FhirTesterConfig.java @@ -43,6 +43,7 @@ public class FhirTesterConfig { .withFhirVersion(HapiProperties.getFhirVersion()) .withBaseUrl(HapiProperties.getServerAddress()) .withName(HapiProperties.getServerName()); + retVal.setRefuseToFetchThirdPartyUrls(HapiProperties.getTesterConfigRefustToFetchThirdPartyUrls()); return retVal; } diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java b/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java index 0a5d74c..88becfb 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java @@ -39,6 +39,7 @@ public class HapiProperties { static final String SUBSCRIPTION_EMAIL_ENABLED = "subscription.email.enabled"; static final String SUBSCRIPTION_RESTHOOK_ENABLED = "subscription.resthook.enabled"; static final String TEST_PORT = "test.port"; + static final String TESTER_CONFIG_REFUSE_TO_FETCH_THIRD_PARTY_URLS = "tester.config.refuse_to_fetch_third_party_urls"; private static Properties properties; @@ -249,6 +250,10 @@ public class HapiProperties { return HapiProperties.getIntegerProperty(TEST_PORT, 0); } + public static Boolean getTesterConfigRefustToFetchThirdPartyUrls() { + return HapiProperties.getBooleanProperty(TESTER_CONFIG_REFUSE_TO_FETCH_THIRD_PARTY_URLS, false); + } + public static String getServerBase() { return HapiProperties.getProperty(SERVER_BASE, "/fhir"); } diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java b/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java index f4fbece..74aceb0 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.model.interceptor.executor.InterceptorService; import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2; import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2; import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; @@ -14,17 +15,22 @@ import ca.uhn.fhir.jpa.provider.r4.JpaConformanceProviderR4; import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4; import ca.uhn.fhir.jpa.provider.r4.TerminologyUploaderProviderR4; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; +import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader; +import ca.uhn.fhir.jpa.subscription.module.interceptor.SubscriptionDebugLogInterceptor; import ca.uhn.fhir.model.dstu2.composite.MetaDt; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.rest.server.HardcodedServerAddressStrategy; import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Meta; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.web.cors.CorsConfiguration; import javax.servlet.ServletException; +import java.util.Arrays; import java.util.List; public class JpaRestfulServer extends RestfulServer { @@ -173,6 +179,33 @@ public class JpaRestfulServer extends RestfulServer { SubscriptionTriggeringProvider retriggeringProvider = appCtx.getBean(SubscriptionTriggeringProvider.class); registerProvider(retriggeringProvider); } + + // Define your CORS configuration. This is an example + // showing a typical setup. You should customize this + // to your specific needs + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedHeader("x-fhir-starter"); + config.addAllowedHeader("Origin"); + config.addAllowedHeader("Accept"); + config.addAllowedHeader("X-Requested-With"); + config.addAllowedHeader("Content-Type"); + + config.addAllowedOrigin("*"); + + config.addExposedHeader("Location"); + config.addExposedHeader("Content-Location"); + config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); + + // Create the interceptor and register it + CorsInterceptor interceptor = new CorsInterceptor(config); + registerInterceptor(interceptor); + + // Enable the use of subscriptions + SubscriptionInterceptorLoader subscriptionInterceptorLoader = appCtx.getBean(SubscriptionInterceptorLoader.class); + subscriptionInterceptorLoader.registerInterceptors(); + // Subscription debug logging + InterceptorService interceptorService = (InterceptorService) appCtx.getBean("interceptorService"); + interceptorService.registerInterceptor(new SubscriptionDebugLogInterceptor()); } } diff --git a/src/main/resources/hapi.properties b/src/main/resources/hapi.properties index 277618d..3749603 100644 --- a/src/main/resources/hapi.properties +++ b/src/main/resources/hapi.properties @@ -1,7 +1,7 @@ # Adjust this to set the version of FHIR supported by this server. See # FhirVersionEnum for a list of available constants. -fhir_version=DSTU3 +fhir_version=R4 # This is the address that the FHIR server will report as its own address. # If this server will be deployed (for example) to an internet accessible @@ -25,16 +25,16 @@ logger.name=fhirtest.access logger.format=Path[${servletPath}] Source[${requestHeader.x-forwarded-for}] Operation[${operationType} ${operationName} ${idOrResourceName}] UA[${requestHeader.user-agent}] Params[${requestParameters}] ResponseEncoding[${responseEncodingNoDefault}] logger.error_format=ERROR - ${requestVerb} ${requestUrl} logger.log_exceptions=true -datasource.driver=org.apache.derby.jdbc.EmbeddedDriver -datasource.url=jdbc:derby:directory:target/jpaserver_derby_files;create=true -datasource.username= -datasource.password= +datasource.driver=com.mysql.cj.jdbc.Driver +datasource.url=jdbc:mysql://localhost:3306/fhir_r4 +datasource.username=fhirUser +datasource.password=fhirPass server.name=Local Tester server.id=home test.port= subscription.email.enabled=true subscription.resthook.enabled=true -hibernate.dialect=ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect +hibernate.dialect=org.hibernate.dialect.MySQL5Dialect hibernate.search.model_mapping=ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory hibernate.format_sql=false hibernate.show_sql=false @@ -47,3 +47,4 @@ hibernate.cache.use_minimal_puts=false hibernate.search.default.directory_provider=filesystem hibernate.search.default.indexBase=target/lucenefiles hibernate.search.lucene_version=LUCENE_CURRENT +tester.config.refuse_to_fetch_third_party_urls=false diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 54164b1..60250e0 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -37,69 +37,65 @@ 2 + + spring + / + fhirServlet ca.uhn.fhir.jpa.starter.JpaRestfulServer 1 - fhirServlet /fhir/* - - spring - / - - - - - - CORS Filter - org.ebaysf.web.cors.CORSFilter - - A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials. - cors.allowed.origins - * - - - A comma separated list of HTTP verbs, using which a CORS request can be made. - cors.allowed.methods - GET,POST,PUT,DELETE,OPTIONS - - - A comma separated list of allowed headers when making a non simple CORS request. - cors.allowed.headers - Accept,Access-Control-Request-Headers,Access-Control-Request-Method,Cache-Control,Content-Type,Origin,Prefer,X-FHIR-Starter,X-Requested-With - - - A comma separated list non-standard response headers that will be exposed to XHR2 object. - cors.exposed.headers - Location,Content-Location - - - A flag that suggests if CORS is supported with cookies - cors.support.credentials - true - - - A flag to control logging - cors.logging.enabled - true - - - Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache. - cors.preflight.maxage - 300 - - - - CORS Filter - /* - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 6858c0d799ade820ab3d26b303a907cb0d6caec8 Mon Sep 17 00:00:00 2001 From: Jessie James Cosare Date: Fri, 15 Mar 2019 23:09:54 +0800 Subject: [PATCH 4/7] Merge remote-tracking branch 'upstream/master' # Conflicts: # src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java # src/main/resources/hapi.properties # src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java --- pom.xml | 6 --- .../fhir/jpa/starter/FhirServerConfigR4.java | 1 - src/main/webapp/WEB-INF/web.xml | 48 +------------------ 3 files changed, 1 insertion(+), 54 deletions(-) diff --git a/pom.xml b/pom.xml index fb188fc..ca54c6b 100644 --- a/pom.xml +++ b/pom.xml @@ -111,12 +111,6 @@ - - - - - - org.springframework diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigR4.java b/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigR4.java index e658930..a9957e9 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigR4.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigR4.java @@ -18,7 +18,6 @@ public class FhirServerConfigR4 extends BaseJavaConfigR4 { @Autowired private DataSource myDataSource; - /** * We override the paging provider definition so that we can customize * the default/max page sizes for search results. You can set these however diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 6ebba85..6ce88b0 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -31,7 +31,7 @@ contextConfigLocation - ca.uhn.fhir.jpa.starter.FhirTesterConfig, + ca.uhn.fhir.jpa.starter.FhirTesterConfig 2 @@ -51,50 +51,4 @@ /fhir/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 1cfcaddda856463d9020cfaba4ba0b2208beafa9 Mon Sep 17 00:00:00 2001 From: Jessie James Cosare Date: Sat, 16 Mar 2019 16:20:47 +0800 Subject: [PATCH 5/7] - cors configuration via hapi properties based on change request https://github.com/hapifhir/hapi-fhir-jpaserver-starter/pull/12/files/6858c0d799ade820ab3d26b303a907cb0d6caec8#r266027967 - reverted mysql as default and used derby based on change request https://github.com/hapifhir/hapi-fhir-jpaserver-starter/pull/12/files/6858c0d799ade820ab3d26b303a907cb0d6caec8#r266028296 and updated readme to include specifics on configuring mysql - defaulted to jetty configs on hapi.properties so that this starter app will outright work without changing any configs with mvn jetty:run - updated subscription configs as it doesn't match already active subscription on subsequent runs using mysql as db - updated project based path resolution and removed unused file .keep_hapi-fhir-jpaserver-starter --- README.md | 22 +++++++++---- pom.xml | 2 +- .../uhn/fhir/jpa/starter/HapiProperties.java | 10 ++++++ .../fhir/jpa/starter/JpaRestfulServer.java | 33 +++++++++---------- src/main/resources/hapi.properties | 25 +++++++------- .../fhir/jpa/starter/ExampleServerR4IT.java | 10 ++---- 6 files changed, 57 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index ab908be..e834006 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ In order to use this sample, you should have: # Running Locally -The easiest way to run this server is to run it directly in Maven using a built-in Jetty server. To do this, execute the following command: +The easiest way to run this server is to run it directly in Maven using a built-in Jetty server. To do this, change `src/main/resources/hapi.properties` `server_address` and `server.base` with the values commented out as *For Jetty, use this* and then execute the following command: ``` mvn jetty:run @@ -20,7 +20,7 @@ mvn jetty:run Then, browse to the following link to use the server: -[http://localhost:8080/](http://localhost:8080/) +[http://localhost:8080/hapi-fhir-jpaserver/](http://localhost:8080/hapi-fhir-jpaserver/) # Deploying to a Container @@ -40,7 +40,7 @@ This will create a file called `hapi-fhir-jpaserver.war` in your `target` direct Again, browse to the following link to use the server (note that the port 8080 may not be correct depending on how your server is configured). -[http://localhost:8080/](http://localhost:8080/) +[http://localhost:8080/hapi-fhir-jpaserver/](http://localhost:8080/hapi-fhir-jpaserver/) # Customizing The Web Testpage UI @@ -56,13 +56,21 @@ Much of this HAPI starter project can be configured using the properties file in ## MySql -To configure the starter app to with MySQL, use the commands below to add user and database: +To configure the starter app to use MySQL, instead of the default Derby: +> Add user and database on your mysql server via mysql cli ``` -CREATE USER 'fhirUser'@'localhost' IDENTIFIED BY 'fhirPass'; -CREATE DATABASE fhir_r4; -GRANT ALL PRIVILEGES ON fhir_r4.* to 'fhirUser'@'localhost'; +CREATE USER 'hapiDbUser'@'localhost' IDENTIFIED BY 'hapiDbPass'; +CREATE DATABASE hapi_dstu3; +GRANT ALL PRIVILEGES ON hapi_dstu3.* to 'hapiDbUser'@'localhost'; FLUSH PRIVILEGES; ``` +> Update hapi.properties file to have the following +* @line34 datasource.driver=com.mysql.cj.jdbc.Driver +* @line35 datasource.url=jdbc:mysql://localhost:3306/hapi_dstu3 +* @line36 datasource.username=hapiDbUser +* @line37 datasource.password=hapiDbPass +* @line41 hibernate.dialect=org.hibernate.dialect.MySQL5Dialect + It is important to use MySQL5Dialect when using MySQL version 5+. diff --git a/pom.xml b/pom.xml index ca54c6b..350b5ed 100644 --- a/pom.xml +++ b/pom.xml @@ -204,7 +204,7 @@ 9.4.8.v20180619 - / + /hapi-fhir-jpaserver true diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java b/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java index 58e08da..44f1b0c 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java @@ -41,6 +41,8 @@ public class HapiProperties { static final String SUBSCRIPTION_WEBSOCKET_ENABLED = "subscription.websocket.enabled"; static final String TEST_PORT = "test.port"; static final String TESTER_CONFIG_REFUSE_TO_FETCH_THIRD_PARTY_URLS = "tester.config.refuse_to_fetch_third_party_urls"; + static final String CORS_ENABLED = "cors.enabled"; + static final String CORS_ALLOWED_ORIGIN = "cors.allowed_origin"; private static Properties properties; @@ -255,6 +257,14 @@ public class HapiProperties { return HapiProperties.getBooleanProperty(TESTER_CONFIG_REFUSE_TO_FETCH_THIRD_PARTY_URLS, false); } + public static Boolean getCorsEnabled() { + return HapiProperties.getBooleanProperty(CORS_ENABLED, true); + } + + public static String getCorsAllowedOrigin() { + return HapiProperties.getProperty(CORS_ALLOWED_ORIGIN, "*"); + } + public static String getServerBase() { return HapiProperties.getProperty(SERVER_BASE, "/fhir"); } diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java b/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java index ef08924..c0a3403 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java @@ -5,7 +5,6 @@ import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.model.interceptor.executor.InterceptorService; -import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2; import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2; import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; @@ -18,8 +17,6 @@ import ca.uhn.fhir.jpa.provider.r4.TerminologyUploaderProviderR4; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader; import ca.uhn.fhir.jpa.subscription.module.interceptor.SubscriptionDebugLogInterceptor; -import ca.uhn.fhir.jpa.subscription.SubscriptionActivatingInterceptor; -import ca.uhn.fhir.jpa.subscription.SubscriptionMatcherInterceptor; import ca.uhn.fhir.model.dstu2.composite.MetaDt; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.rest.server.HardcodedServerAddressStrategy; @@ -175,29 +172,30 @@ public class JpaRestfulServer extends RestfulServer { // Define your CORS configuration. This is an example // showing a typical setup. You should customize this // to your specific needs - CorsConfiguration config = new CorsConfiguration(); - config.addAllowedHeader("x-fhir-starter"); - config.addAllowedHeader("Origin"); - config.addAllowedHeader("Accept"); - config.addAllowedHeader("X-Requested-With"); - config.addAllowedHeader("Content-Type"); + if(HapiProperties.getCorsEnabled()) { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedHeader("x-fhir-starter"); + config.addAllowedHeader("Origin"); + config.addAllowedHeader("Accept"); + config.addAllowedHeader("X-Requested-With"); + config.addAllowedHeader("Content-Type"); - config.addAllowedOrigin("*"); + config.addAllowedOrigin(HapiProperties.getCorsAllowedOrigin()); - config.addExposedHeader("Location"); - config.addExposedHeader("Content-Location"); - config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); + config.addExposedHeader("Location"); + config.addExposedHeader("Content-Location"); + config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); - // Create the interceptor and register it - CorsInterceptor interceptor = new CorsInterceptor(config); - registerInterceptor(interceptor); + // Create the interceptor and register it + CorsInterceptor interceptor = new CorsInterceptor(config); + registerInterceptor(interceptor); + } // If subscriptions are enabled, we want to register the interceptor that // will activate them and match results against them if (HapiProperties.getSubscriptionWebsocketEnabled() || HapiProperties.getSubscriptionEmailEnabled() || HapiProperties.getSubscriptionRestHookEnabled()) { - // Loads subscription interceptors (SubscriptionActivatingInterceptor, SubscriptionMatcherInterceptor) // with activation of scheduled subscription SubscriptionInterceptorLoader subscriptionInterceptorLoader = appCtx.getBean(SubscriptionInterceptorLoader.class); @@ -206,7 +204,6 @@ public class JpaRestfulServer extends RestfulServer { // Subscription debug logging InterceptorService interceptorService = (InterceptorService) appCtx.getBean("interceptorService"); interceptorService.registerInterceptor(new SubscriptionDebugLogInterceptor()); - } } diff --git a/src/main/resources/hapi.properties b/src/main/resources/hapi.properties index 7c049de..6e53091 100644 --- a/src/main/resources/hapi.properties +++ b/src/main/resources/hapi.properties @@ -1,27 +1,29 @@ # Adjust this to set the version of FHIR supported by this server. See # FhirVersionEnum for a list of available constants. -fhir_version=R4 +fhir_version=DSTU3 # This is the address that the FHIR server will report as its own address. # If this server will be deployed (for example) to an internet accessible # server, put the DNS name of that server here. -server_address=http://localhost:8080/fhir/ +#server_address=http://localhost/fhir/ # For Jetty, use this: -# server_address=http://localhost:8080/hapi-fhir-jpaserver/fhir/ +server_address=http://localhost:8080/hapi-fhir-jpaserver/fhir/ # This is the context path for the FHIR endpoint. If this is changed, the # setting above should also be changed. -server.base=/fhir +#server.base=/fhir # For Jetty, use this: -# server.base=/hapi-fhir-jpaserver/fhir +server.base=/hapi-fhir-jpaserver/fhir default_encoding=JSON etag_support=ENABLED default_page_size=20 max_page_size=200 +allow_override_default_search_params=true +allow_contains_searches=true allow_multiple_delete=true allow_external_references=true allow_placeholder_references=true @@ -31,14 +33,14 @@ logger.name=fhirtest.access logger.format=Path[${servletPath}] Source[${requestHeader.x-forwarded-for}] Operation[${operationType} ${operationName} ${idOrResourceName}] UA[${requestHeader.user-agent}] Params[${requestParameters}] ResponseEncoding[${responseEncodingNoDefault}] logger.error_format=ERROR - ${requestVerb} ${requestUrl} logger.log_exceptions=true -datasource.driver=com.mysql.cj.jdbc.Driver -datasource.url=jdbc:mysql://localhost:3306/fhir_r4 -datasource.username=fhirUser -datasource.password=fhirPass +datasource.driver=org.apache.derby.jdbc.EmbeddedDriver +datasource.url=jdbc:derby:directory:target/jpaserver_derby_files;create=true +datasource.username= +datasource.password= server.name=Local Tester server.id=home test.port= -hibernate.dialect=org.hibernate.dialect.MySQL5Dialect +hibernate.dialect=ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect hibernate.search.model_mapping=ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory hibernate.format_sql=false hibernate.show_sql=false @@ -52,7 +54,8 @@ hibernate.search.default.directory_provider=filesystem hibernate.search.default.indexBase=target/lucenefiles hibernate.search.lucene_version=LUCENE_CURRENT tester.config.refuse_to_fetch_third_party_urls=false - +cors.enabled=true +cors.allowed_origin=* ################################################## # Subscriptions diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java index 6858b41..3364371 100644 --- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java +++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java @@ -22,9 +22,9 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import java.io.File; import java.io.IOException; import java.net.URI; +import java.nio.file.Paths; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -131,13 +131,7 @@ public class ExampleServerR4IT { @BeforeClass public static void beforeClass() throws Exception { - /* - * This runs under maven, and I'm not sure how else to figure out the target directory from code.. - */ - String path = ExampleServerR4IT.class.getClassLoader().getResource(".keep_hapi-fhir-jpaserver-starter").getPath(); - path = new File(path).getParent(); - path = new File(path).getParent(); - path = new File(path).getParent(); + String path = Paths.get("").toAbsolutePath().toString(); ourLog.info("Project base path is: {}", path); From d5c35723d38e35309dc162ed0824cffb466775ee Mon Sep 17 00:00:00 2001 From: Jessie James Cosare Date: Sat, 16 Mar 2019 18:06:46 +0800 Subject: [PATCH 6/7] - fixed issue on diff contextPath during running and testing, updated from "/hapi-fhir-jpaserver" to "/" and hapi.properties configs on server_address and server.base works out of the box with jetty - replicated testWebsocketSubscription test from ExampleServerR4IT into ExampleServerDstu3IT --- README.md | 14 ++-- pom.xml | 2 +- src/main/resources/hapi.properties | 10 +-- .../jpa/starter/ExampleServerDstu3IT.java | 73 ++++++++++++++++++- .../fhir/jpa/starter/ExampleServerR4IT.java | 2 - 5 files changed, 82 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index e834006..7def907 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ mvn jetty:run Then, browse to the following link to use the server: -[http://localhost:8080/hapi-fhir-jpaserver/](http://localhost:8080/hapi-fhir-jpaserver/) +[http://localhost:8080/](http://localhost:8080/) # Deploying to a Container @@ -40,7 +40,7 @@ This will create a file called `hapi-fhir-jpaserver.war` in your `target` direct Again, browse to the following link to use the server (note that the port 8080 may not be correct depending on how your server is configured). -[http://localhost:8080/hapi-fhir-jpaserver/](http://localhost:8080/hapi-fhir-jpaserver/) +[http://localhost:8080/](http://localhost:8080/) # Customizing The Web Testpage UI @@ -67,10 +67,10 @@ FLUSH PRIVILEGES; ``` > Update hapi.properties file to have the following -* @line34 datasource.driver=com.mysql.cj.jdbc.Driver -* @line35 datasource.url=jdbc:mysql://localhost:3306/hapi_dstu3 -* @line36 datasource.username=hapiDbUser -* @line37 datasource.password=hapiDbPass -* @line41 hibernate.dialect=org.hibernate.dialect.MySQL5Dialect +* datasource.driver=com.mysql.cj.jdbc.Driver +* datasource.url=jdbc:mysql://localhost:3306/hapi_dstu3 +* datasource.username=hapiDbUser +* datasource.password=hapiDbPass +* hibernate.dialect=org.hibernate.dialect.MySQL5Dialect It is important to use MySQL5Dialect when using MySQL version 5+. diff --git a/pom.xml b/pom.xml index 350b5ed..ca54c6b 100644 --- a/pom.xml +++ b/pom.xml @@ -204,7 +204,7 @@ 9.4.8.v20180619 - /hapi-fhir-jpaserver + / true diff --git a/src/main/resources/hapi.properties b/src/main/resources/hapi.properties index 449412b..0d7e11e 100644 --- a/src/main/resources/hapi.properties +++ b/src/main/resources/hapi.properties @@ -6,17 +6,11 @@ fhir_version=DSTU3 # This is the address that the FHIR server will report as its own address. # If this server will be deployed (for example) to an internet accessible # server, put the DNS name of that server here. -#server_address=http://localhost/fhir/ - -# For Jetty, use this: -server_address=http://localhost:8080/hapi-fhir-jpaserver/fhir/ +server_address=http://localhost:8080/fhir/ # This is the context path for the FHIR endpoint. If this is changed, the # setting above should also be changed. -#server.base=/fhir - -# For Jetty, use this: -server.base=/hapi-fhir-jpaserver/fhir +server.base=/fhir default_encoding=JSON etag_support=ENABLED diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu3IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu3IT.java index 7841833..c481402 100644 --- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu3IT.java +++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerDstu3IT.java @@ -1,15 +1,27 @@ package ca.uhn.fhir.jpa.starter; +import static ca.uhn.fhir.util.TestUtil.waitForSize; import static org.junit.Assert.assertEquals; -import java.io.File; import java.io.IOException; +import java.net.URI; import java.nio.file.Paths; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import ca.uhn.fhir.rest.api.CacheControlDirective; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.util.PortUtil; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.dstu3.model.Observation; import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.Subscription; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.*; @@ -33,6 +45,7 @@ public class ExampleServerDstu3IT { HapiProperties.setProperty(HapiProperties.FHIR_VERSION, "DSTU3"); HapiProperties.setProperty(HapiProperties.DATASOURCE_URL, "jdbc:derby:memory:dbr3;create=true"); HapiProperties.setProperty(HapiProperties.TEST_PORT, Integer.toString(PortUtil.findFreePort())); + HapiProperties.setProperty(HapiProperties.SUBSCRIPTION_WEBSOCKET_ENABLED, "true"); ourCtx = FhirContext.forDstu3(); ourPort = HapiProperties.getTestPort(); } @@ -50,6 +63,64 @@ public class ExampleServerDstu3IT { assertEquals(methodName, pt2.getName().get(0).getFamily()); } + @Test + public void testWebsocketSubscription() throws Exception { + /* + * Create subscription + */ + Subscription subscription = new Subscription(); + subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); + subscription.setStatus(Subscription.SubscriptionStatus.REQUESTED); + subscription.setCriteria("Observation?status=final"); + + Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent(); + channel.setType(Subscription.SubscriptionChannelType.WEBSOCKET); + channel.setPayload("application/json"); + subscription.setChannel(channel); + + MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute(); + IIdType mySubscriptionId = methodOutcome.getId(); + + // Wait for the subscription to be activated + waitForSize(1, () -> ourClient.search().forResource(Subscription.class).where(Subscription.STATUS.exactly().code("active")).cacheControl(new CacheControlDirective().setNoCache(true)).returnBundle(Bundle.class).execute().getEntry().size()); + + /* + * Attach websocket + */ + + WebSocketClient myWebSocketClient = new WebSocketClient(); + SocketImplementation mySocketImplementation = new SocketImplementation(mySubscriptionId.getIdPart(), EncodingEnum.JSON); + + myWebSocketClient.start(); + URI echoUri = new URI("ws://localhost:" + ourPort + "/websocket"); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + ourLog.info("Connecting to : {}", echoUri); + Future connection = myWebSocketClient.connect(mySocketImplementation, echoUri, request); + Session session = connection.get(2, TimeUnit.SECONDS); + + ourLog.info("Connected to WS: {}", session.isOpen()); + + /* + * Create a matching resource + */ + Observation obs = new Observation(); + obs.setStatus(Observation.ObservationStatus.FINAL); + ourClient.create().resource(obs).execute(); + + // Give some time for the subscription to deliver + Thread.sleep(2000); + + /* + * Ensure that we receive a ping on the websocket + */ + waitForSize(1, () -> mySocketImplementation.myPingCount); + + /* + * Clean up + */ + ourClient.delete().resourceById(mySubscriptionId).execute(); + } + @AfterClass public static void afterClass() throws Exception { ourServer.stop(); diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java index 3364371..ce77acd 100644 --- a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java +++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4IT.java @@ -64,7 +64,6 @@ public class ExampleServerR4IT { assertEquals(methodName, pt2.getName().get(0).getFamily()); } - @Test public void testWebsocketSubscription() throws Exception { /* @@ -123,7 +122,6 @@ public class ExampleServerR4IT { ourClient.delete().resourceById(mySubscriptionId).execute(); } - @AfterClass public static void afterClass() throws Exception { ourServer.stop(); From 95ac3dcb4ae7a737e545a3eacd2f68e8a5a546c9 Mon Sep 17 00:00:00 2001 From: Jessie James Cosare Date: Sat, 16 Mar 2019 18:46:19 +0800 Subject: [PATCH 7/7] - fixed issue https://github.com/hapifhir/hapi-fhir-jpaserver-starter/issues/16 on moment.locale - added favicon based on smiley on hapi logo --- .../webapp/WEB-INF/templates/tmpl-head.html | 30 ++++++++++++++++++ src/main/webapp/img/favicon.ico | Bin 0 -> 1406 bytes src/main/webapp/js/moment-with-locales.min.js | 1 + 3 files changed, 31 insertions(+) create mode 100644 src/main/webapp/WEB-INF/templates/tmpl-head.html create mode 100644 src/main/webapp/img/favicon.ico create mode 100644 src/main/webapp/js/moment-with-locales.min.js diff --git a/src/main/webapp/WEB-INF/templates/tmpl-head.html b/src/main/webapp/WEB-INF/templates/tmpl-head.html new file mode 100644 index 0000000..3d7bb4e --- /dev/null +++ b/src/main/webapp/WEB-INF/templates/tmpl-head.html @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/webapp/img/favicon.ico b/src/main/webapp/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4c2419d1a4fe5f736824fffac536fbc40656cf6c GIT binary patch literal 1406 zcmd^8YgANK6#iyFXFvoTk;*I)D8@%gp!f*GED1}?Cq4-cGBp)U3n}FkWNI2pL@F3% z+B+Y;w31q@C29{`7Ar*!hJjHKMPOhackaFC9Jl&MfBUn0*LU{*_St9Oea`tVV4ycN z6f~N#s2dOtz)GMHA_&*w*B`?TbF*(fT!k#BL`;J~nMcD;uRuk;1F6j%jN*AN36<;6 z2z5>uG$CQJN$6w+A-Befe#8qYFU6suh-@(-`x3{bW`Tir9ZsJL7a=4v2D`ixX)PMk zX-?-HQmXJj>V{=#A{Ji~@T*d=yj6qWNd*%eI_8}8z;Ieaw4Ec;!4Y@Pg`6t_<~o5s zloNHl6KTy7zd0`A2*FBRN)6i_of!U?1MyUQc9oZEag?|<7|!a*?x6kJ1$KA@rZsVx z>LmOFf>2V|g&N zV$g9HGFt>PS_KThD=4?SFt%O7SgpY_G6~W3oW3s*SL4O}Y99h>1)a%Y%%eJrE)YsQ z{i2S7PJtDtyTGM@Agm`Id$b*fQ|EmHuDze)+{-HR|&^`!nJYBNx2(x;-f3u5oHrM{S5zK zm*qV<<*m5=%eHij-`wM_?-|QuL47~BKKs{e+3VLYjB|Ts%-bEo*ph*3AK5m4(zwWH2CaE}Dq}CM z8jP2Fk6s@$;`KYz2QU`>>8v?L6Tg|*z3A>;OAj-4f6g6GKU6+_`iy%Qe7R#bV|zZy zj517qV$y_Ol1;Ccz4!d$H@J{S7<2EU|i|F3>t7(kS)JduJc=l+r9a#3nl{p literal 0 HcmV?d00001 diff --git a/src/main/webapp/js/moment-with-locales.min.js b/src/main/webapp/js/moment-with-locales.min.js new file mode 100644 index 0000000..d81e02c --- /dev/null +++ b/src/main/webapp/js/moment-with-locales.min.js @@ -0,0 +1 @@ +!function(e,a){"object"==typeof exports&&"undefined"!=typeof module?module.exports=a():"function"==typeof define&&define.amd?define(a):e.moment=a()}(this,function(){"use strict";var e,n;function l(){return e.apply(null,arguments)}function _(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function i(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function o(e){return void 0===e}function m(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function u(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function M(e,a){var t,s=[];for(t=0;t>>0,s=0;sTe(e)?(d=e+1,_-Te(e)):(d=e,_),{year:d,dayOfYear:r}}function Ce(e,a,t){var s,n,d=Ne(e.year(),a,t),r=Math.floor((e.dayOfYear()-d-1)/7)+1;return r<1?s=r+Ie(n=e.year()-1,a,t):r>Ie(e.year(),a,t)?(s=r-Ie(e.year(),a,t),n=e.year()+1):(n=e.year(),s=r),{week:s,year:n}}function Ie(e,a,t){var s=Ne(e,a,t),n=Ne(e+1,a,t);return(Te(e)-s+n)/7}C("w",["ww",2],"wo","week"),C("W",["WW",2],"Wo","isoWeek"),O("week","w"),O("isoWeek","W"),E("week",5),E("isoWeek",5),ie("w",B),ie("ww",B,V),ie("W",B),ie("WW",B,V),Me(["w","ww","W","WW"],function(e,a,t,s){a[s.substr(0,1)]=g(e)});function Ue(e,a){return e.slice(a,7).concat(e.slice(0,a))}C("d",0,"do","day"),C("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),C("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),C("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),C("e",0,0,"weekday"),C("E",0,0,"isoWeekday"),O("day","d"),O("weekday","e"),O("isoWeekday","E"),E("day",11),E("weekday",11),E("isoWeekday",11),ie("d",B),ie("e",B),ie("E",B),ie("dd",function(e,a){return a.weekdaysMinRegex(e)}),ie("ddd",function(e,a){return a.weekdaysShortRegex(e)}),ie("dddd",function(e,a){return a.weekdaysRegex(e)}),Me(["dd","ddd","dddd"],function(e,a,t,s){var n=t._locale.weekdaysParse(e,s,t._strict);null!=n?a.d=n:Y(t).invalidWeekday=e}),Me(["d","e","E"],function(e,a,t,s){a[s]=g(e)});var Ge="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_");var Ve="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_");var Ke="Su_Mo_Tu_We_Th_Fr_Sa".split("_");var Ze=re;var $e=re;var Be=re;function qe(){function e(e,a){return a.length-e.length}var a,t,s,n,d,r=[],_=[],i=[],o=[];for(a=0;a<7;a++)t=c([2e3,1]).day(a),s=this.weekdaysMin(t,""),n=this.weekdaysShort(t,""),d=this.weekdays(t,""),r.push(s),_.push(n),i.push(d),o.push(s),o.push(n),o.push(d);for(r.sort(e),_.sort(e),i.sort(e),o.sort(e),a=0;a<7;a++)_[a]=me(_[a]),i[a]=me(i[a]),o[a]=me(o[a]);this._weekdaysRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+_.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+r.join("|")+")","i")}function Qe(){return this.hours()%12||12}function Xe(e,a){C(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),a)})}function ea(e,a){return a._meridiemParse}C("H",["HH",2],0,"hour"),C("h",["hh",2],0,Qe),C("k",["kk",2],0,function(){return this.hours()||24}),C("hmm",0,0,function(){return""+Qe.apply(this)+F(this.minutes(),2)}),C("hmmss",0,0,function(){return""+Qe.apply(this)+F(this.minutes(),2)+F(this.seconds(),2)}),C("Hmm",0,0,function(){return""+this.hours()+F(this.minutes(),2)}),C("Hmmss",0,0,function(){return""+this.hours()+F(this.minutes(),2)+F(this.seconds(),2)}),Xe("a",!0),Xe("A",!1),O("hour","h"),E("hour",13),ie("a",ea),ie("A",ea),ie("H",B),ie("h",B),ie("k",B),ie("HH",B,V),ie("hh",B,V),ie("kk",B,V),ie("hmm",q),ie("hmmss",Q),ie("Hmm",q),ie("Hmmss",Q),le(["H","HH"],Ye),le(["k","kk"],function(e,a,t){var s=g(e);a[Ye]=24===s?0:s}),le(["a","A"],function(e,a,t){t._isPm=t._locale.isPM(e),t._meridiem=e}),le(["h","hh"],function(e,a,t){a[Ye]=g(e),Y(t).bigHour=!0}),le("hmm",function(e,a,t){var s=e.length-2;a[Ye]=g(e.substr(0,s)),a[ye]=g(e.substr(s)),Y(t).bigHour=!0}),le("hmmss",function(e,a,t){var s=e.length-4,n=e.length-2;a[Ye]=g(e.substr(0,s)),a[ye]=g(e.substr(s,2)),a[fe]=g(e.substr(n)),Y(t).bigHour=!0}),le("Hmm",function(e,a,t){var s=e.length-2;a[Ye]=g(e.substr(0,s)),a[ye]=g(e.substr(s))}),le("Hmmss",function(e,a,t){var s=e.length-4,n=e.length-2;a[Ye]=g(e.substr(0,s)),a[ye]=g(e.substr(s,2)),a[fe]=g(e.substr(n))});var aa,ta=Se("Hours",!0),sa={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Oe,monthsShort:Pe,week:{dow:0,doy:6},weekdays:Ge,weekdaysMin:Ke,weekdaysShort:Ve,meridiemParse:/[ap]\.?m?\.?/i},na={},da={};function ra(e){return e?e.toLowerCase().replace("_","-"):e}function _a(e){var a=null;if(!na[e]&&"undefined"!=typeof module&&module&&module.exports)try{a=aa._abbr,require("./locale/"+e),ia(a)}catch(e){}return na[e]}function ia(e,a){var t;return e&&((t=o(a)?ma(e):oa(e,a))?aa=t:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),aa._abbr}function oa(e,a){if(null===a)return delete na[e],null;var t,s=sa;if(a.abbr=e,null!=na[e])S("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),s=na[e]._config;else if(null!=a.parentLocale)if(null!=na[a.parentLocale])s=na[a.parentLocale]._config;else{if(null==(t=_a(a.parentLocale)))return da[a.parentLocale]||(da[a.parentLocale]=[]),da[a.parentLocale].push({name:e,config:a}),null;s=t._config}return na[e]=new j(b(s,a)),da[e]&&da[e].forEach(function(e){oa(e.name,e.config)}),ia(e),na[e]}function ma(e){var a;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return aa;if(!_(e)){if(a=_a(e))return a;e=[e]}return function(e){for(var a,t,s,n,d=0;d=a&&r(n,t,!0)>=a-1)break;a--}d++}return aa}(e)}function ua(e){var a,t=e._a;return t&&-2===Y(e).overflow&&(a=t[Le]<0||11je(t[he],t[Le])?ce:t[Ye]<0||24Ie(t,d,r)?Y(e)._overflowWeeks=!0:null!=i?Y(e)._overflowWeekday=!0:(_=Re(t,s,n,d,r),e._a[he]=_.year,e._dayOfYear=_.dayOfYear)}(e),null!=e._dayOfYear&&(d=la(e._a[he],s[he]),(e._dayOfYear>Te(d)||0===e._dayOfYear)&&(Y(e)._overflowDayOfYear=!0),t=Je(d,0,e._dayOfYear),e._a[Le]=t.getUTCMonth(),e._a[ce]=t.getUTCDate()),a=0;a<3&&null==e._a[a];++a)e._a[a]=r[a]=s[a];for(;a<7;a++)e._a[a]=r[a]=null==e._a[a]?2===a?1:0:e._a[a];24===e._a[Ye]&&0===e._a[ye]&&0===e._a[fe]&&0===e._a[ke]&&(e._nextDay=!0,e._a[Ye]=0),e._d=(e._useUTC?Je:function(e,a,t,s,n,d,r){var _;return e<100&&0<=e?(_=new Date(e+400,a,t,s,n,d,r),isFinite(_.getFullYear())&&_.setFullYear(e)):_=new Date(e,a,t,s,n,d,r),_}).apply(null,r),n=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[Ye]=24),e._w&&void 0!==e._w.d&&e._w.d!==n&&(Y(e).weekdayMismatch=!0)}}var ha=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,La=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,ca=/Z|[+-]\d\d(?::?\d\d)?/,Ya=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],ya=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],fa=/^\/?Date\((\-?\d+)/i;function ka(e){var a,t,s,n,d,r,_=e._i,i=ha.exec(_)||La.exec(_);if(i){for(Y(e).iso=!0,a=0,t=Ya.length;at.valueOf():t.valueOf()this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},Mt.isLocal=function(){return!!this.isValid()&&!this._isUTC},Mt.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},Mt.isUtc=Ra,Mt.isUTC=Ra,Mt.zoneAbbr=function(){return this._isUTC?"UTC":""},Mt.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},Mt.dates=t("dates accessor is deprecated. Use date instead.",_t),Mt.months=t("months accessor is deprecated. Use month instead",Ae),Mt.years=t("years accessor is deprecated. Use year instead",ve),Mt.zone=t("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,a){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,a),this):-this.utcOffset()}),Mt.isDSTShifted=t("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!o(this._isDSTShifted))return this._isDSTShifted;var e={};if(k(e,this),(e=va(e))._a){var a=e._isUTC?c(e._a):Ha(e._a);this._isDSTShifted=this.isValid()&&0