├── .gitignore ├── LICENSE ├── README.md ├── application.properties ├── grails-app ├── conf │ ├── ApplicationResources.groovy │ ├── BootStrap.groovy │ ├── BuildConfig.groovy │ ├── Config.groovy │ ├── DataSource.groovy │ ├── UrlMappings.groovy │ ├── examples │ │ ├── Patient-example.xml │ │ ├── bundle.xml │ │ ├── diagnosticorder.xml │ │ └── medicationprescription.xml │ └── spring │ │ └── resources.groovy ├── controllers │ └── fhir │ │ ├── ApiController.groovy │ │ ├── ApiControllerCommands.groovy │ │ ├── ErrorController.groovy │ │ └── LaunchContextController.groovy ├── domain │ └── fhir │ │ ├── LaunchContext.groovy │ │ ├── LaunchContextParam.groovy │ │ ├── ResourceCompartment.groovy │ │ ├── ResourceIndexComposite.groovy │ │ ├── ResourceIndexDate.groovy │ │ ├── ResourceIndexNumber.groovy │ │ ├── ResourceIndexReference.groovy │ │ ├── ResourceIndexString.groovy │ │ ├── ResourceIndexTerm.groovy │ │ ├── ResourceIndexToken.groovy │ │ └── ResourceVersion.groovy ├── i18n │ ├── messages.properties │ ├── messages_cs_CZ.properties │ ├── messages_da.properties │ ├── messages_de.properties │ ├── messages_es.properties │ ├── messages_fr.properties │ ├── messages_it.properties │ ├── messages_ja.properties │ ├── messages_nb.properties │ ├── messages_nl.properties │ ├── messages_pl.properties │ ├── messages_pt_BR.properties │ ├── messages_pt_PT.properties │ ├── messages_ru.properties │ ├── messages_sv.properties │ ├── messages_th.properties │ └── messages_zh_CN.properties ├── services │ └── fhir │ │ ├── AuthorizationService.groovy │ │ ├── BundleService.groovy │ │ ├── ConformanceService.groovy │ │ ├── SearchIndexService.groovy │ │ ├── SqlService.groovy │ │ ├── UrlService.groovy │ │ └── XmlService.groovy ├── utils │ └── fhir │ │ ├── DbObjectCodec.groovy │ │ ├── FhirJsonCodec.groovy │ │ ├── FhirXmlCodec.groovy │ │ ├── RequestFilters.groovy │ │ └── ResponseFilters.groovy └── views │ ├── error.gsp │ ├── index.gsp │ └── layouts │ └── main.gsp ├── grailsWrapper ├── grails-wrapper-runtime-2.4.4.jar ├── grails-wrapper.properties └── springloaded-1.2.1.RELEASE.jar ├── grailsw ├── grailsw.bat ├── load-emerge-patients ├── .gradle │ ├── 1.7 │ │ └── taskArtifacts │ │ │ ├── cache.properties │ │ │ ├── cache.properties.lock │ │ │ ├── fileHashes.bin │ │ │ ├── fileSnapshots.bin │ │ │ ├── outputFileStates.bin │ │ │ └── taskArtifacts.bin │ └── 1.9-rc-3 │ │ └── taskArtifacts │ │ ├── cache.properties │ │ ├── fileHashes.bin │ │ ├── fileSnapshots.bin │ │ ├── outputFileStates.bin │ │ └── taskArtifacts.bin ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src │ └── main │ └── groovy │ └── LoadCCDA.groovy ├── postgres-tables.sql ├── reset-db.sql ├── scripts └── CreateDatabase.groovy ├── src └── groovy │ └── fhir │ ├── auth │ └── TokenCache.groovy │ └── searchParam │ ├── CompositeSearchParamHandler.groovy │ ├── DateSearchParamHandler.groovy │ ├── IdSearchParamHandler.groovy │ ├── IndexedValue.groovy │ ├── NumberSearchParamHandler.groovy │ ├── QuantitySearchParamHandler.groovy │ ├── ReferenceSearchParamHandler.groovy │ ├── SearchParamHandler.groovy │ ├── SearchedValue.groovy │ ├── StringSearchParamHandler.groovy │ ├── TokenSearchParamHandler.groovy │ └── UriSearchParamHandler.groovy ├── test └── unit │ └── fhir │ ├── ApiControllerTests.groovy │ ├── LaunchContextControllerSpec.groovy │ ├── LaunchContextParamSpec.groovy │ └── LaunchContextSpec.groovy └── web-app ├── WEB-INF ├── applicationContext.xml ├── sitemesh.xml └── tld │ ├── c.tld │ ├── fmt.tld │ ├── grails.tld │ └── spring.tld ├── css ├── errors.css ├── main.css └── mobile.css ├── images ├── apple-touch-icon-retina.png ├── apple-touch-icon.png ├── favicon.ico ├── fire-shot.jpg ├── grails_logo.jpg ├── grails_logo.png ├── leftnav_btm.png ├── leftnav_midstretch.png ├── leftnav_top.png ├── skin │ ├── database_add.png │ ├── database_delete.png │ ├── database_edit.png │ ├── database_save.png │ ├── database_table.png │ ├── exclamation.png │ ├── house.png │ ├── information.png │ ├── shadow.jpg │ ├── sorted_asc.gif │ └── sorted_desc.gif ├── spinner.gif └── springsource.png └── js └── application.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.iws 2 | *Db.properties 3 | *Db.script 4 | .settings 5 | stacktrace.log 6 | /*.zip 7 | /plugin.xml 8 | /*.log 9 | /*DB.* 10 | /cobertura.ser 11 | .DS_Store 12 | /target/ 13 | /out/ 14 | /web-app/plugins 15 | /web-app/WEB-INF/classes 16 | /.link_to_grails_plugins/ 17 | /target-eclipse/ 18 | /lib/*.jar 19 | .classpath 20 | .project 21 | *.swo 22 | *.swp 23 | .groovy 24 | *.class 25 | *.properties.lock 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Boston Children's Hospital 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this software except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## This project is no longer maintained, but there is a list of other open source FHIR servers [here](http://wiki.hl7.org/index.php?title=Open_Source_FHIR_implementations). 2 | --- 3 | 4 | 5 | SMART on FHIR 6 | ============= 7 | 8 | 9 | Open-source [FHIR](http://hl7.org/implement/standards/fhir/) Server to support patient- and clinician-facing apps. 10 | 11 | Still highly experimental, but has limited support for: 12 | 13 | * GET, POST, and PUT resources 14 | * `transaction` (POST a bundle of resources) 15 | * Search resources based on FHIR's defined search params 16 | 17 | ## Live demo: [API](https://fhir-api.smarthealthit.org) | [Apps](https://fhir.smarthealthit.org) 18 | 19 | ## Installing 20 | 21 | ### Prerequisites 22 | * Install Postgres 9.1+ (locally or use a remote service) 23 | * Oracle Java 7 JDK (not JRE -- and **not Java 8**) 24 | 25 | ### Get it 26 | ``` 27 | $ git clone https://github.com/smart-on-fhir/api-server 28 | $ cd api-server 29 | ``` 30 | 31 | ### Initialize the DB (see config below as needed) 32 | Ensure that `/etc/postgresql/9.1/main/pg_hba.conf` contains a line like: 33 | 34 | ``` 35 | local all all md5 36 | ``` 37 | (If you have `local all all peer`, for example, `peer` with `md5`.) 38 | 39 | 40 | 41 | ``` 42 | $ sudo -u postgres -i 43 | postgres@$ createuser -R -P -S -D fhir 44 | [at password prompt: fhir] 45 | postgres@$ createdb -O fhir fhir 46 | postgres@$ logout 47 | ``` 48 | 49 | ### Run it 50 | ``` 51 | $ ./grailsw run-app 52 | ``` 53 | 54 | ## Configuring 55 | Key settings files are: 56 | 57 | #### grails-app/conf/Config.groovy 58 | * Turn authentication on or off with `fhir.oauth.enabled`: `true | false` 59 | * Configure authentication with `fhir.oauth` 60 | 61 | #### grails-app/conf/DataSource.groovy 62 | * Configure your Postgres `dataSource` 63 | 64 | ## Using 65 | Add new data to the server via HTTP PUT or POST. For example, with default 66 | authentication settings and a server running at http://localhost:8080, you can add a new Diagnostic Order via: 67 | 68 | ``` 69 | curl 'http://localhost:8080/DiagnosticOrder/example' \ 70 | -X PUT \ 71 | -H 'Authorization: Basic Y2xpZW50OnNlY3JldA=='\ 72 | -H 'Content-Type: text/xml' \ 73 | --data @grails-app/conf/examples/diagnosticorder.xml 74 | ``` 75 | 76 | And then you can retrieve a feed of diagnostic orders via: 77 | 78 | ``` 79 | curl 'http://localhost:8080/DiagnosticOrder' \ 80 | -H 'Authorization: Basic Y2xpZW50OnNlY3JldA==' 81 | ``` 82 | 83 | or fetch a single resource as JSON via: 84 | 85 | ``` 86 | curl 'http://localhost:8080/DiagnosticOrder/example' \ 87 | -H 'Authorization: Basic Y2xpZW50OnNlY3JldA==' \ 88 | -H 'Accept: application/json' 89 | ``` 90 | 91 | ## Getting more sample data 92 | You can load sample data from SMART's [Sample Patients](https://github.com/smart-on-fhir/sample-patients): 93 | 94 | ``` 95 | $ git clone --recursive https://github.com/smart-on-fhir/sample-patients 96 | $ cd sample-patients/bin 97 | $ pip install -r requirements.txt 98 | $ python generate.py --write-fhir ../generated-data 99 | $ ls ../generated-data # a bunch of XML files 100 | ``` 101 | 102 | ### Loading these files into your system 103 | 104 | ``` 105 | cd ../generated-data 106 | for i in *.xml; do 107 | curl 'http://localhost:8080/?' \ 108 | -H 'Content-Type: text/xml' \ 109 | --data-binary @$i; 110 | done 111 | ``` 112 | 113 | -------------------------------------------------------------------------------- /application.properties: -------------------------------------------------------------------------------- 1 | #Grails Metadata file 2 | #Tue Apr 29 17:05:01 PDT 2014 3 | app.grails.version=2.4.4 4 | app.name=fhir 5 | app.servlet.version=3.0 6 | app.version=0.1 7 | -------------------------------------------------------------------------------- /grails-app/conf/ApplicationResources.groovy: -------------------------------------------------------------------------------- 1 | modules = { 2 | application { 3 | resource url:'js/application.js' 4 | } 5 | } -------------------------------------------------------------------------------- /grails-app/conf/BootStrap.groovy: -------------------------------------------------------------------------------- 1 | class BootStrap { 2 | 3 | def init = { servletContext -> 4 | } 5 | def destroy = { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /grails-app/conf/BuildConfig.groovy: -------------------------------------------------------------------------------- 1 | grails.servlet.version = "3.0" // Change depending on target container compliance (2.5 or 3.0) 2 | grails.project.class.dir = "target/classes" 3 | grails.project.test.class.dir = "target/test-classes" 4 | grails.project.test.reports.dir = "target/test-reports" 5 | grails.project.target.level = 1.7 6 | grails.project.source.level = 1.7 7 | grails.project.dependency.resolver = "maven" // or ivy 8 | 9 | grails.project.dependency.resolution = { 10 | inherits("global") { } 11 | log "warn" // 'error', 'warn', 'info', 'debug' or 'verbose' 12 | checksums true // Whether to verify checksums on resolve 13 | 14 | repositories { 15 | inherits true 16 | 17 | grailsPlugins() 18 | grailsHome() 19 | grailsCentral() 20 | mavenLocal() 21 | mavenCentral() 22 | mavenRepo "https://oss.sonatype.org/content/repositories/snapshots/" 23 | } 24 | 25 | dependencies { 26 | runtime 'com.google.guava:guava:14.0.1' 27 | runtime "joda-time:joda-time:2.2" 28 | runtime "org.mongodb:mongo-java-driver:2.11.3" 29 | runtime('org.codehaus.groovy.modules.http-builder:http-builder:0.5.2') { 30 | excludes 'groovy' 31 | excludes 'xml-apis' 32 | excludes 'xalan' 33 | } 34 | runtime 'org.postgresql:postgresql:9.3-1100-jdbc41' 35 | 36 | //runtime 'net.sf.saxon:Saxon-HE:9.5.1-5' 37 | //runtime 'org.apache.commons:commons-io:1.3.2' 38 | //runtime "com.google.code.gson:gson:2.2.4" 39 | //runtime 'xpp3:xpp3:1.1.3.4.O' 40 | //runtime 'xmlpull:xmlpull:1.1.3.4d_b4_min' 41 | compile 'me.fhir:fhir-dstu2:1.0.0.6869-SNAPSHOT' 42 | } 43 | 44 | plugins { 45 | compile ":rest-client-builder:2.0.3" 46 | runtime ":hibernate:3.6.10.17" 47 | runtime ":resources:1.2.8" 48 | runtime ":cors:1.1.6" 49 | build ":tomcat:7.0.54" 50 | compile ":postgresql-extensions:3.2.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /grails-app/conf/Config.groovy: -------------------------------------------------------------------------------- 1 | // locations to search for config files that get merged into the main config; 2 | // config files can be ConfigSlurper scripts, Java properties files, or classes 3 | // in the classpath in ConfigSlurper format 4 | 5 | // grails.config.locations = [ "classpath:${appName}-config.properties", 6 | // "classpath:${appName}-config.groovy", 7 | // "file:${userHome}/.grails/${appName}-config.properties", 8 | // "file:${userHome}/.grails/${appName}-config.groovy"] 9 | 10 | // if (System.properties["${appName}.config.location"]) { 11 | // grails.config.locations << "file:" + System.properties["${appName}.config.location"] 12 | // } 13 | 14 | cors.headers = [ 15 | 'Access-Control-Allow-Headers': 'origin, authorization, accept, content-type, x-requested-with, prefer' 16 | ] 17 | cors.expose.headers = 'Content-Location,Location' 18 | cors.enabled = true 19 | 20 | fhir.namespaces = [ 21 | f: "http://hl7.org/fhir", 22 | xhtml: "http://www.w3.org/1999/xhtml" 23 | ] 24 | 25 | fhir.searchParam.spotFixes = [ 26 | "Patient.deceased": "f:Patient/f:deceasedBoolean", 27 | "Patient.deathdate": "f:Patient/f:deceasedDateTime", 28 | "Condition.onset": 29 | "f:Condition/f:onsetAge | f:Condition/f:onsetDate", 30 | "Group.value": 31 | "f:Condition/*[namespace-uri()='http://hl7.org/fhir' and starts-with(local-name(),'value')]", 32 | "Observation.name": 33 | "f:Observation/f:name | f:Observation/f:component/f:name", 34 | "Observation.value-date": 35 | "f:Observation/f:valuePeriod", 36 | "Observation.value-quantity": 37 | "f:Observation/f:valueQuantity", 38 | "Observation.value-string": 39 | "f:Observation/f:valueString", 40 | "Observation.value-concept": 41 | "f:Observation/f:valueCodeableConcept", 42 | "Observation.date": 43 | "f:Observation/*[namespace-uri()='http://hl7.org/fhir' and starts-with(local-name(),'applies')]", 44 | "Group.type-value": 45 | 'f:Group/f:characteristic$'+ 46 | 'f:type$' + 47 | '*[namespace-uri()="http://hl7.org/fhir" and starts-with(local-name(),"value")]', 48 | "DiagnosticOrder.status-date": 49 | 'f:DiagnosticOrder/f:event$'+ 50 | 'f:status$' + 51 | 'f:date', 52 | "DiagnosticOrder.item-status-date": 53 | 'f:DiagnosticOrder/f:item$'+ 54 | 'f:status$' + 55 | "f:date", 56 | "Observation.name-value": 57 | 'f:Observation/f:component | f:Observation$' + 58 | 'f:name$' + 59 | '*[namespace-uri()="http://hl7.org/fhir" and starts-with(local-name(),"value")]' 60 | ] 61 | 62 | 63 | 64 | grails.project.groupId = appName // change this to alter the default package name and Maven publishing destination 65 | grails.mime.file.extensions = false // enables the parsing of file extensions from URLs into the request format 66 | grails.mime.use.accept.header = true 67 | grails.mime.types = [ 68 | all: '*/*', 69 | atom: 'application/atom+xml', 70 | css: 'text/css', 71 | csv: 'text/csv', 72 | form: 'application/x-www-form-urlencoded', 73 | html: ['text/html','application/xhtml+xml'], 74 | js: 'text/javascript', 75 | json: ['application/json', 'text/json'], 76 | multipartForm: 'multipart/form-data', 77 | rss: 'application/rss+xml', 78 | text: 'text/plain', 79 | xml: ['text/xml', 'application/xml', 'application/fhir+xml'] 80 | ] 81 | 82 | // URL Mapping Cache Max Size, defaults to 5000 83 | //grails.urlmapping.cache.maxsize = 1000 84 | 85 | // What URL patterns should be processed by the resources plugin 86 | grails.resources.adhoc.patterns = ['/images/*', '/css/*', '/js/*', '/plugins/*'] 87 | 88 | // The default codec used to encode data with ${} 89 | grails.views.default.codec = "none" // none, html, base64 90 | grails.views.gsp.encoding = "UTF-8" 91 | grails.converters.encoding = "UTF-8" 92 | // enable Sitemesh preprocessing of GSP pages 93 | grails.views.gsp.sitemesh.preprocess = true 94 | // scaffolding templates configuration 95 | grails.scaffolding.templates.domainSuffix = 'Instance' 96 | 97 | // Set to false to use the new Grails 1.2 JSONBuilder in the render method 98 | grails.json.legacy.builder = false 99 | // enabled native2ascii conversion of i18n properties files 100 | grails.enable.native2ascii = true 101 | // packages to include in Spring bean scanning 102 | grails.spring.bean.packages = [] 103 | // whether to disable processing of multi part requests 104 | grails.web.disable.multipart=false 105 | 106 | // request parameters to mask when logging exceptions 107 | grails.exceptionresolver.params.exclude = ['password'] 108 | 109 | // configure auto-caching of queries by default (if false you can cache individual queries with 'cache: true') 110 | grails.app.context = "/" 111 | grails.converters.json.pretty.print = true; 112 | 113 | environments { 114 | development { 115 | grails.logging.jul.usebridge = true 116 | grails.serverURL = System.env.BASE_URL ?: "http://localhost:9080" 117 | fhir.oauth = [ 118 | enabled: System.env.AUTH ? System.env.AUTH.toBoolean() : false, 119 | tokenCacheSpec: System.env.TOKEN_CACHE_SPEC ?: 'maximumSize=1000,expireAfterWrite=30m', 120 | introspectionUri: System.env.INTROSPECTION_URI ?: 'http://localhost:9085/openid-connect-server-webapp/introspect?token={token}', 121 | clientId: System.env.AUTH_CLIENT_ID ?: 'client', 122 | clientSecret: System.env.AUTH_CLIENT_SECRET ?: 'secret', 123 | registerUri: System.env.REGISTER_URI ?: 'http://localhost:9085/openid-connect-server-webapp/register', 124 | authorizeUri: System.env.AUTHORIZE_URI ?: 'http://localhost:9085/openid-connect-server-webapp/authorize', 125 | tokenUri: System.env.TOKEN_URI ?: 'http://localhost:9085/openid-connect-server-webapp/token' 126 | ] 127 | localAuth = [ 128 | clientId: System.env.CLIENT_ID ?: 'client', 129 | clientSecret: System.env.CLIENT_SECRET ?: 'secret' 130 | ] 131 | } 132 | production { 133 | grails.logging.jul.usebridge = false 134 | grails.serverURL = System.env.BASE_URL ?: "http://localhost:8080/fhir-server" 135 | fhir.oauth = [ 136 | enabled: System.env.AUTH ? System.env.AUTH.toBoolean() : true, 137 | tokenCacheSpec: System.env.TOKEN_CACHE_SPEC ?: 'maximumSize=1000,expireAfterWrite=30m', 138 | introspectionUri: System.env.INTROSPECTION_URI ?: 'http://localhost:8080/openid-connect-server-webapp/introspect?token={token}', 139 | clientId: System.env.AUTH_CLIENT_ID ?: 'client', 140 | clientSecret: System.env.AUTH_CLIENT_SECRET ?: 'secret', 141 | registerUri: System.env.REGISTER_URI ?: 'http://localhost:8080/openid-connect-server-webapp/register', 142 | authorizeUri: System.env.AUTHORIZE_URI ?: 'http://localhost:8080/openid-connect-server-webapp/authorize', 143 | tokenUri: System.env.TOKEN_URI ?: 'http://localhost:8080/openid-connect-server-webapp/token' 144 | ] 145 | localAuth = [ 146 | clientId: System.env.CLIENT_ID ?: 'client', 147 | clientSecret: System.env.CLIENT_SECRET ?: 'secret' 148 | ] 149 | } 150 | } 151 | 152 | // log4j configuration 153 | log4j = { 154 | // Example of changing the log pattern for the default console appender: 155 | // 156 | 157 | appenders { 158 | console name:'stdout', layout:pattern(conversionPattern: '%c{2} %m%n') 159 | } 160 | 161 | debug "grails.app" 162 | error 'org.codehaus.groovy.grails.web.servlet', // controllers 163 | 'org.codehaus.groovy.grails.web.pages', // GSP 164 | 'org.codehaus.groovy.grails.web.sitemesh', // layouts 165 | 'org.codehaus.groovy.grails.web.mapping.filter', // URL mapping 166 | 'org.codehaus.groovy.grails.web.mapping', // URL mapping 167 | 'org.codehaus.groovy.grails.commons', // core / classloading 168 | 'org.codehaus.groovy.grails.plugins', // plugins 169 | 'org.springframework' 170 | } 171 | 172 | // For an example of dynamically loading server URLs from environment vars: 173 | // https://gist.github.com/djensen47/4062384 174 | 175 | // Uncomment and edit the following lines to start using Grails encoding & escaping improvements 176 | 177 | /* remove this line 178 | // GSP settings 179 | grails { 180 | views { 181 | gsp { 182 | encoding = 'UTF-8' 183 | htmlcodec = 'xml' // use xml escaping instead of HTML4 escaping 184 | codecs { 185 | expression = 'html' // escapes values inside null 186 | scriptlet = 'none' // escapes output from scriptlets in GSPs 187 | taglib = 'none' // escapes output from taglibs 188 | staticparts = 'none' // escapes output from static template parts 189 | } 190 | } 191 | // escapes all not-encoded output at final stage of outputting 192 | filteringCodecForContentType { 193 | //'text/html' = 'html' 194 | } 195 | } 196 | } 197 | remove this line */ 198 | -------------------------------------------------------------------------------- /grails-app/conf/DataSource.groovy: -------------------------------------------------------------------------------- 1 | /*dataSource { 2 | pooled = true 3 | 4 | }*/ 5 | dataSource { 6 | pooled = true 7 | driverClassName = "org.postgresql.Driver" 8 | dialect = "net.kaleidos.hibernate.PostgresqlExtensionsDialect" 9 | url = "jdbc:postgresql://localhost/fhir" 10 | username = "fhir" 11 | password = "fhir" 12 | dbCreate = "update" 13 | 14 | properties { 15 | maxActive = 50 16 | maxIdle = 25 17 | minIdle = 1 18 | initialSize = 1 19 | 20 | numTestsPerEvictionRun = 3 21 | maxWait = 10000 22 | 23 | testOnBorrow = true 24 | testWhileIdle = true 25 | testOnReturn = true 26 | 27 | validationQuery = "select 1" 28 | 29 | minEvictableIdleTimeMillis = 1000 * 60 * 5 30 | timeBetweenEvictionRunsMillis = 1000 * 60 * 5 31 | } 32 | } 33 | 34 | 35 | hibernate { } 36 | 37 | environments { 38 | development { 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /grails-app/conf/UrlMappings.groovy: -------------------------------------------------------------------------------- 1 | import fhir.AuthorizationException 2 | import fhir.ResourceDeletedException 3 | 4 | 5 | class UrlMappings { 6 | 7 | static excludes = [ 8 | '/css/*', 9 | '/images/*', 10 | '/js/*', 11 | '/favicon.ico' 12 | ] 13 | static mappings = { 14 | 15 | name base: "/" { 16 | controller="Api" 17 | action=[GET: "welcome", OPTIONS: "conformance", POST: "transaction", DELETE: "delete"] 18 | } 19 | 20 | name metadata: "/metadata"(controller: "Api") { 21 | action = [GET: "conformance"] 22 | } 23 | 24 | name summary: "/BlueButtonSummary" { 25 | controller="Api" 26 | action=[GET: "summary"] 27 | } 28 | 29 | name resourceInstance: "/$resource/$id" { 30 | controller="Api" 31 | action=[GET: "read", PUT: "update", DELETE: "delete"] 32 | } 33 | 34 | name resourceVersion: "/$resource/$id/_history/$vid"(controller: "Api") { 35 | action = [GET: "vread"] 36 | } 37 | 38 | name resourceSearch: "/$resource/_search"(controller: "Api") { 39 | action = [POST: "search"] 40 | } 41 | 42 | name resourceClass: "/$resource"(controller: "Api") { 43 | action = [GET: "search", POST: "create"] 44 | } 45 | 46 | name summary: "/_history" { 47 | controller="Api" 48 | action=[GET: "history"] 49 | } 50 | 51 | name resourceHistory: "/$resource/_history" { 52 | controller="Api" 53 | action=[GET: "history"] 54 | } 55 | 56 | name resourceInstanceHistory: "/$resource/$id/_history" { 57 | controller="Api" 58 | action=[GET: "history"] 59 | } 60 | 61 | name createLaunchContext: "/_services/smart/Launch" { 62 | controller="LaunchContext" 63 | action=[POST: "create"] 64 | } 65 | 66 | name getLaunchContext: "/_services/smart/Launch/$launch_id" { 67 | controller="LaunchContext" 68 | action=[GET: "read"] 69 | } 70 | 71 | name getRecentPatients: "/_services/smart/RecentPatients" { 72 | controller="LaunchContext" 73 | action=[GET: "recentPatients"] 74 | } 75 | 76 | 77 | "401"(controller: 'error', action: 'status401') 78 | "500"(controller: 'error', action: 'deleted', exception: ResourceDeletedException) 79 | "500"(controller: 'error', action: 'status405', exception:BundleValidationException) 80 | "500"(controller: 'error', action: 'status401', exception:AuthorizationException) 81 | "500"(controller: 'error', action: 'status500') 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /grails-app/conf/examples/bundle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | New bundle for transactional posting 4 | urn:uuid:8bbfa2ec-1cdb-4a6c-ad27-3b5fcaf1fb 5 | 2013-08-28T02:47:25Z 6 | 7 | Patient "106" Version "1" 8 | cid:patient-106 9 | 2013-08-26T01:26:51Z 10 | 2013-08-28T02:51:39Z 11 | 12 | 110.143.187.242 13 | 14 | 15 | 16 | 17 | 18 |
West, James. MRN: 577425
19 |
20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 |
45 | 46 |
West, James. MRN: 577425
47 |
48 |
49 | 50 | Condition "example" Version "1" 51 | cid:condition-1 52 | 2013-08-26T01:26:11Z 53 | 2013-08-28T02:47:25Z 54 | 55 | 110.143.187.242 56 | 57 | 58 | 59 | 60 | 61 |
Severe burn of left ear (Date: 24-May 2012)
62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
107 |
108 | 109 |
Severe burn of left ear (Date: 24-May 2012)
110 |
111 |
112 | 113 | Condition "example2" Version "1" 114 | cid:condition-2 115 | 2013-08-26T01:26:12Z 116 | 2013-08-28T02:47:25Z 117 | 118 | 110.143.187.242 119 | 120 | 121 | 122 | 123 | 124 |
Mild Asthma (Date: 21-Nov 2012)
125 |
126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 |
150 |
151 | 152 |
Mild Asthma (Date: 21-Nov 2012)
153 |
154 |
155 |
156 | -------------------------------------------------------------------------------- /grails-app/conf/examples/diagnosticorder.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | Example Diagnostic Order 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | -------------------------------------------------------------------------------- /grails-app/conf/examples/medicationprescription.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |

Penicillin VK 5ml suspension to be administered by oral route

9 |

ONE 5ml spoonful to be taken THREE times a day

10 |

100ml bottle

11 |

to patient ref: a23

12 |

by doctor X

13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
-------------------------------------------------------------------------------- /grails-app/conf/spring/resources.groovy: -------------------------------------------------------------------------------- 1 | // Place your Spring DSL code here 2 | import fhir.auth.TokenCache 3 | 4 | beans = { 5 | tokenCache(TokenCache){ 6 | spec = grailsApplication.config.fhir.oauth.tokenCacheSpec 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /grails-app/controllers/fhir/ApiControllerCommands.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | import java.util.regex.Pattern 4 | import javax.sql.DataSource 5 | import java.lang.* 6 | 7 | import org.bson.types.ObjectId 8 | import org.hibernate.SessionFactory 9 | import org.hl7.fhir.instance.model.Bundle 10 | import org.hl7.fhir.instance.model.Bundle.BundleEntryComponent; 11 | import org.hl7.fhir.instance.model.Binary 12 | import org.hl7.fhir.instance.model.DocumentReference 13 | import org.hl7.fhir.instance.model.Patient 14 | import org.hl7.fhir.instance.model.Resource 15 | 16 | import com.google.gson.JsonObject 17 | import com.google.gson.JsonParser 18 | import com.mongodb.BasicDBObject 19 | 20 | import fhir.searchParam.DateSearchParamHandler 21 | import grails.transaction.Transactional 22 | 23 | class ApiControllerCommands {} 24 | 25 | @grails.validation.Validateable(nullable=true) 26 | class PagingCommand { 27 | Integer total 28 | Integer _count 29 | Integer _skip 30 | 31 | def bind(params, request) { 32 | _count = params._count ? Math.min(Integer.parseInt(params._count), 50) : 50 33 | _skip = params._skip ? Integer.parseInt(params._skip) : 0 34 | } 35 | } 36 | 37 | @grails.validation.Validateable(nullable=true) 38 | class HistoryCommand { 39 | Date _since 40 | Map clauses = [:] 41 | def request 42 | def searchIndexService 43 | def fhirType 44 | def fhirId 45 | 46 | //TODO restrict history by compartment 47 | 48 | def getClauses() { 49 | if (request == null) { 50 | return null; 51 | } 52 | Map params = [:] 53 | List restrictions = [] 54 | 55 | if (_since != null) { 56 | restrictions += "rest_date > :_since" 57 | params += [_since:_since] 58 | } 59 | if (fhirType != null) { 60 | restrictions += "fhir_type= :fhirType" 61 | params += [fhirType:fhirType] 62 | } 63 | if (fhirId != null) { 64 | restrictions += "fhir_id = :fhirId" 65 | params += [fhirId:fhirId] 66 | } 67 | if (request.authorization.accessIsRestricted) { 68 | restrictions += " exists (select fhir_type, fhir_id from resource_compartment c " + 69 | " where v.fhir_type=c.fhir_type and v.fhir_id=c.fhir_id and " + 70 | " compartments && ${request.authorization.compartmentsSql} )" 71 | params += [fhirId:fhirId] 72 | } 73 | 74 | def restrictionsString = "" 75 | if (restrictions.size() > 0) 76 | restrictionsString = " WHERE " + restrictions.join(" AND ") 77 | 78 | return [ 79 | count: "select count(*) from resource_version v $restrictionsString", 80 | content: "select * from resource_version v $restrictionsString", 81 | params: params 82 | ] 83 | } 84 | 85 | def bind(params, request) { 86 | this.request = request 87 | this._since = params._since ? new java.sql.Date(DateSearchParamHandler.precisionInterval(params._since).start.toDate().time) : null 88 | this.fhirType = params.resource ?: null 89 | this.fhirId = params.id ?: null 90 | } 91 | } 92 | 93 | @grails.validation.Validateable(nullable=true) 94 | class SearchCommand { 95 | def params 96 | def request 97 | def searchIndexService 98 | PagingCommand paging 99 | 100 | def getClauses() { 101 | if (params == null || request == null) { 102 | return null; 103 | } 104 | 105 | def clauses = searchIndexService.searchParamsToSql(params, request.authorization, paging) 106 | return clauses 107 | } 108 | 109 | Map includesFor(Collection entries){ 110 | def ret = searchIndexService.includesFor(params, entries, request.authorization) 111 | return ret 112 | } 113 | 114 | def bind(params, request) { 115 | this.params = params 116 | this.request = request 117 | this.paging = new PagingCommand() 118 | this.paging.bind(params, request) 119 | } 120 | } 121 | 122 | -------------------------------------------------------------------------------- /grails-app/controllers/fhir/ErrorController.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | import org.hl7.fhir.instance.model.CodeableConcept 3 | import org.hl7.fhir.instance.model.OperationOutcome 4 | 5 | class ErrorController { 6 | 7 | static scope = "singleton" 8 | 9 | private def status(int s) { 10 | log.debug("Rendering a $s error") 11 | def extra = "Failed with error" 12 | if (request.exception) { 13 | extra = request.exception.message 14 | } 15 | response.status=s 16 | def o = new OperationOutcome() 17 | def i = o.addIssue() 18 | .setSeverity(OperationOutcome.IssueSeverity.ERROR) 19 | .setDetails(new CodeableConcept().setText(extra)) 20 | request.resourceToRender = o 21 | } 22 | 23 | def status401() { 24 | status(401) 25 | } 26 | 27 | def status405() { 28 | status(405) 29 | } 30 | 31 | def status500() { 32 | status(500) 33 | } 34 | 35 | 36 | def deleted(){ 37 | status(410) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /grails-app/controllers/fhir/LaunchContextController.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | import fhir.AuthorizationService.Authorization 4 | import grails.converters.JSON 5 | 6 | import org.codehaus.groovy.grails.web.json.JSONObject 7 | import org.hl7.fhir.instance.model.Bundle 8 | import org.hl7.fhir.instance.model.Patient 9 | 10 | class LaunchContextController { 11 | 12 | static scope = "singleton" 13 | BundleService bundleService 14 | UrlService urlService 15 | SqlService sqlService 16 | 17 | def create() { 18 | Authorization auth = request.authorization; 19 | auth.assertScope("smart/orchestrate_launch"); 20 | 21 | JSONObject req = request.JSON; 22 | req.put 23 | 24 | LaunchContext c = new LaunchContext(); 25 | c.created_by = auth.app ?: "can't tell :/"; 26 | c.client_id = req.getString("client_id"); 27 | c.username = auth.username; 28 | 29 | req.getJSONObject("parameters").each { k,v -> 30 | c.addToParams(new LaunchContextParam(param_name: k, param_value: v)) 31 | } 32 | c.save(flush:true, failOnError: true) 33 | 34 | render c.asJson() 35 | } 36 | 37 | def read() { 38 | Authorization auth = request.authorization; 39 | auth.assertScope("smart/orchestrate_launch"); 40 | 41 | LaunchContext c = LaunchContext.read(params.launch_id) 42 | println "Resolved launch context ${c.asJson()}" 43 | render c.asJson() 44 | } 45 | 46 | def recentPatients() { 47 | Authorization auth = request.authorization; 48 | String username = auth.username; 49 | 50 | Map entries = LaunchContext 51 | .findAllByUsername(username, [max: 10, sort: "created_at", order: "desc"]) 52 | .collectMany { LaunchContext lc -> 53 | lc.params 54 | .findAll { it.param_name == "patient" } 55 | .collect { it.param_value } 56 | } .collect { 57 | sqlService.getLatestByFhirId("Patient", it) 58 | } .collectEntries { 59 | [(it.fhir_type+'/'+it.fhir_id): it.content.decodeFhirJson()] 60 | } 61 | 62 | Bundle feed = bundleService.createFeed([ 63 | entries: entries, 64 | paging: new PagingCommand(total: entries.size(), _count: entries.size(), _skip: 0), 65 | feedId: urlService.fullRequestUrl(request) 66 | ]) 67 | 68 | request.resourceToRender = feed 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /grails-app/domain/fhir/LaunchContext.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | import grails.converters.JSON 4 | import java.text.DateFormat 5 | import java.text.SimpleDateFormat 6 | import java.util.Date; 7 | import org.codehaus.groovy.grails.web.json.JSONObject 8 | 9 | class LaunchContext { 10 | 11 | static hasMany = [params: LaunchContextParam] 12 | 13 | String created_by 14 | String username 15 | Date created_at = new Date() 16 | String client_id 17 | 18 | public static TimeZone tz; 19 | public static DateFormat df; 20 | 21 | static { 22 | tz = TimeZone.getTimeZone("UTC"); 23 | df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); 24 | df.setTimeZone(tz); 25 | } 26 | 27 | static mapping = { 28 | table 'launch_context' 29 | id column: 'launch_id' 30 | version false 31 | } 32 | 33 | public JSON asJson(){ 34 | JSONObject j = new JSONObject(); 35 | j.put("launch_id", id.toString()); 36 | j.put("username", username); 37 | j.put("created_by", created_by); 38 | j.put("created_at", df.format(created_at)); 39 | 40 | JSONObject ps = new JSONObject(); 41 | params.each { 42 | ps.put(it.param_name, it.param_value) 43 | } 44 | 45 | ps.put("need_patient_banner", true) 46 | ps.put("smart_style_url", "https://fhir.smarthealthit.org/stylesheets/smart_v1.json") 47 | 48 | j.put("parameters", ps); 49 | 50 | def ret = j as JSON 51 | ret.setPrettyPrint(true) 52 | return ret 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /grails-app/domain/fhir/LaunchContextParam.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | class LaunchContextParam { 4 | 5 | String param_name; 6 | String param_value; 7 | 8 | 9 | static belongsTo = [launch_context:LaunchContext] 10 | 11 | static mapping = { 12 | table 'launch_context_params' 13 | version false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /grails-app/domain/fhir/ResourceCompartment.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | import net.kaleidos.hibernate.usertype.ArrayType 3 | 4 | class ResourceCompartment implements Serializable { 5 | 6 | String fhir_id 7 | String fhir_type 8 | String[] compartments = [] 9 | 10 | static mapping = { 11 | table 'resource_compartment' 12 | id composite: ['fhir_type', 'fhir_id'] 13 | compartments type:ArrayType, params: [type: String] 14 | version false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /grails-app/domain/fhir/ResourceIndexComposite.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | class ResourceIndexComposite extends ResourceIndexTerm { 4 | String composite_value 5 | 6 | static mapping = { 7 | version false 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /grails-app/domain/fhir/ResourceIndexDate.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | class ResourceIndexDate extends ResourceIndexTerm{ 4 | Date date_min 5 | Date date_max 6 | 7 | static mapping = { 8 | version false 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /grails-app/domain/fhir/ResourceIndexNumber.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | class ResourceIndexNumber extends ResourceIndexTerm{ 4 | float number_min 5 | float number_max 6 | 7 | static mapping = { version false } 8 | static constraints = { 9 | number_min nullable: true 10 | number_max nullable: true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /grails-app/domain/fhir/ResourceIndexReference.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | class ResourceIndexReference extends ResourceIndexTerm{ 4 | String reference_type 5 | String reference_id 6 | String reference_version 7 | String reference_is_external 8 | 9 | static mapping = { 10 | version false 11 | reference_id index: 'reference_index' 12 | reference_type index: 'reference_index' 13 | } 14 | 15 | static constraints = { 16 | reference_id nullable: true 17 | reference_type nullable: true 18 | reference_version nullable: true 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /grails-app/domain/fhir/ResourceIndexString.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | class ResourceIndexString extends ResourceIndexTerm{ 4 | String string_value 5 | 6 | static mapping = { 7 | string_value type: 'text' 8 | version false 9 | string_value index: 'search_string_index' 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /grails-app/domain/fhir/ResourceIndexTerm.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | class ResourceIndexTerm { 4 | long version_id 5 | String fhir_id 6 | String fhir_type 7 | String search_param 8 | 9 | static mapping = { 10 | version false 11 | fhir_id index: 'resource_index' 12 | fhir_type index: 'resource_index,searchparam_index,search_token' 13 | search_param index: 'searchparam_index,search_string_index,search_token' 14 | version_id index:'version_index' 15 | } 16 | 17 | GString insertStatement(versionId){ 18 | 19 | def fields = GString.EMPTY + "version_id, class, fhir_type, fhir_id, search_param" 20 | def values = GString.EMPTY + "$versionId, ${this.class.name.split('\\.')[-1]}, ${fhir_type}, ${fhir_id}, ${search_param}" 21 | 22 | [ 23 | 'string_value', 24 | 'composite_value', 25 | 'date_min', 26 | 'date_max', 27 | 'token_code', 28 | 'token_namespace', 29 | 'token_text', 30 | 'reference_id', 31 | 'reference_is_external', 32 | 'reference_type', 33 | 'reference_version', 34 | 'number_min', 35 | 'number_max' 36 | ].each { fieldName -> 37 | if (fieldName in properties && properties[fieldName]){ 38 | fields += ", "+fieldName 39 | values += ", ${properties[fieldName]}" 40 | } 41 | } 42 | return GString.EMPTY + " insert into resource_index_term (id, "+fields+") values (nextval('seq_resource_version'), "+values+");" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /grails-app/domain/fhir/ResourceIndexToken.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | class ResourceIndexToken extends ResourceIndexTerm{ 4 | String token_namespace 5 | String token_code 6 | String token_text 7 | 8 | static mapping = { version false } 9 | static constraints = { 10 | token_text type: 'text', nullable: true 11 | token_namespace nullable: true, index:'search_token' 12 | token_code index:'search_token' 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /grails-app/domain/fhir/ResourceVersion.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | import org.apache.tools.ant.types.resources.comparators.ResourceComparator; 4 | 5 | class ResourceVersion { 6 | 7 | long version_id 8 | String fhir_id 9 | String fhir_type 10 | 11 | Date rest_date = new Date() 12 | String rest_operation 13 | 14 | String content 15 | 16 | List getCompartments(){ 17 | ResourceCompartment c = ResourceCompartment.find("from ResourceCompartment as c where c.fhir_type=? and c.fhir_id=?", [fhir_type, fhir_id]) 18 | return c.compartments as List 19 | } 20 | 21 | static mapping = { 22 | content type: 'text' 23 | table 'resource_version' 24 | id name: 'version_id' 25 | version false 26 | fhir_type index:'logical_id_index' 27 | fhir_id index:'logical_id_index' 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /grails-app/i18n/messages.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}] 2 | default.invalid.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL 3 | default.invalid.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number 4 | default.invalid.email.message=Property [{0}] of class [{1}] with value [{2}] is not a valid e-mail address 5 | default.invalid.range.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid range from [{3}] to [{4}] 6 | default.invalid.size.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid size range from [{3}] to [{4}] 7 | default.invalid.max.message=Property [{0}] of class [{1}] with value [{2}] exceeds maximum value [{3}] 8 | default.invalid.min.message=Property [{0}] of class [{1}] with value [{2}] is less than minimum value [{3}] 9 | default.invalid.max.size.message=Property [{0}] of class [{1}] with value [{2}] exceeds the maximum size of [{3}] 10 | default.invalid.min.size.message=Property [{0}] of class [{1}] with value [{2}] is less than the minimum size of [{3}] 11 | default.invalid.validator.message=Property [{0}] of class [{1}] with value [{2}] does not pass custom validation 12 | default.not.inlist.message=Property [{0}] of class [{1}] with value [{2}] is not contained within the list [{3}] 13 | default.blank.message=Property [{0}] of class [{1}] cannot be blank 14 | default.not.equal.message=Property [{0}] of class [{1}] with value [{2}] cannot equal [{3}] 15 | default.null.message=Property [{0}] of class [{1}] cannot be null 16 | default.not.unique.message=Property [{0}] of class [{1}] with value [{2}] must be unique 17 | 18 | default.paginate.prev=Previous 19 | default.paginate.next=Next 20 | default.boolean.true=True 21 | default.boolean.false=False 22 | default.date.format=yyyy-MM-dd HH:mm:ss z 23 | default.number.format=0 24 | 25 | default.created.message={0} {1} created 26 | default.updated.message={0} {1} updated 27 | default.deleted.message={0} {1} deleted 28 | default.not.deleted.message={0} {1} could not be deleted 29 | default.not.found.message={0} not found with id {1} 30 | default.optimistic.locking.failure=Another user has updated this {0} while you were editing 31 | 32 | default.home.label=Home 33 | default.list.label={0} List 34 | default.add.label=Add {0} 35 | default.new.label=New {0} 36 | default.create.label=Create {0} 37 | default.show.label=Show {0} 38 | default.edit.label=Edit {0} 39 | 40 | default.button.create.label=Create 41 | default.button.edit.label=Edit 42 | default.button.update.label=Update 43 | default.button.delete.label=Delete 44 | default.button.delete.confirm.message=Are you sure? 45 | 46 | # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) 47 | typeMismatch.java.net.URL=Property {0} must be a valid URL 48 | typeMismatch.java.net.URI=Property {0} must be a valid URI 49 | typeMismatch.java.util.Date=Property {0} must be a valid Date 50 | typeMismatch.java.lang.Double=Property {0} must be a valid number 51 | typeMismatch.java.lang.Integer=Property {0} must be a valid number 52 | typeMismatch.java.lang.Long=Property {0} must be a valid number 53 | typeMismatch.java.lang.Short=Property {0} must be a valid number 54 | typeMismatch.java.math.BigDecimal=Property {0} must be a valid number 55 | typeMismatch.java.math.BigInteger=Property {0} must be a valid number 56 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_cs_CZ.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] neodpovídá požadovanému vzoru [{3}] 2 | default.invalid.url.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] není validní URL 3 | default.invalid.creditCard.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] není validní číslo kreditní karty 4 | default.invalid.email.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] není validní emailová adresa 5 | default.invalid.range.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] není v povoleném rozmezí od [{3}] do [{4}] 6 | default.invalid.size.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] není v povoleném rozmezí od [{3}] do [{4}] 7 | default.invalid.max.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] překračuje maximální povolenou hodnotu [{3}] 8 | default.invalid.min.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] je menší než minimální povolená hodnota [{3}] 9 | default.invalid.max.size.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] překračuje maximální velikost [{3}] 10 | default.invalid.min.size.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] je menší než minimální velikost [{3}] 11 | default.invalid.validator.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] neprošla validací 12 | default.not.inlist.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] není obsažena v seznamu [{3}] 13 | default.blank.message=Položka [{0}] třídy [{1}] nemůže být prázdná 14 | default.not.equal.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] nemůže být stejná jako [{3}] 15 | default.null.message=Položka [{0}] třídy [{1}] nemůže být prázdná 16 | default.not.unique.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] musí být unikátní 17 | 18 | default.paginate.prev=Předcházející 19 | default.paginate.next=Následující 20 | default.boolean.true=Pravda 21 | default.boolean.false=Nepravda 22 | default.date.format=dd. MM. yyyy HH:mm:ss z 23 | default.number.format=0 24 | 25 | default.created.message={0} {1} vytvořeno 26 | default.updated.message={0} {1} aktualizováno 27 | default.deleted.message={0} {1} smazáno 28 | default.not.deleted.message={0} {1} nelze smazat 29 | default.not.found.message={0} nenalezen s id {1} 30 | default.optimistic.locking.failure=Jiný uživatel aktualizoval záznam {0}, právě když byl vámi editován 31 | 32 | default.home.label=Domů 33 | default.list.label={0} Seznam 34 | default.add.label=Přidat {0} 35 | default.new.label=Nový {0} 36 | default.create.label=Vytvořit {0} 37 | default.show.label=Ukázat {0} 38 | default.edit.label=Editovat {0} 39 | 40 | default.button.create.label=Vytvoř 41 | default.button.edit.label=Edituj 42 | default.button.update.label=Aktualizuj 43 | default.button.delete.label=Smaž 44 | default.button.delete.confirm.message=Jste si jistý? 45 | 46 | # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) 47 | typeMismatch.java.net.URL=Položka {0} musí být validní URL 48 | typeMismatch.java.net.URI=Položka {0} musí být validní URI 49 | typeMismatch.java.util.Date=Položka {0} musí být validní datum 50 | typeMismatch.java.lang.Double=Položka {0} musí být validní desetinné číslo 51 | typeMismatch.java.lang.Integer=Položka {0} musí být validní číslo 52 | typeMismatch.java.lang.Long=Položka {0} musí být validní číslo 53 | typeMismatch.java.lang.Short=Položka {0} musí být validní číslo 54 | typeMismatch.java.math.BigDecimal=Položka {0} musí být validní číslo 55 | typeMismatch.java.math.BigInteger=Položka {0} musí být validní číslo -------------------------------------------------------------------------------- /grails-app/i18n/messages_da.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] overholder ikke mønsteret [{3}] 2 | default.invalid.url.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] er ikke en gyldig URL 3 | default.invalid.creditCard.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] er ikke et gyldigt kreditkortnummer 4 | default.invalid.email.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] er ikke en gyldig e-mail adresse 5 | default.invalid.range.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] ligger ikke inden for intervallet fra [{3}] til [{4}] 6 | default.invalid.size.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] ligger ikke inden for størrelsen fra [{3}] til [{4}] 7 | default.invalid.max.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] overstiger den maksimale værdi [{3}] 8 | default.invalid.min.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] er under den minimale værdi [{3}] 9 | default.invalid.max.size.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] overstiger den maksimale størrelse på [{3}] 10 | default.invalid.min.size.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] er under den minimale størrelse på [{3}] 11 | default.invalid.validator.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] overholder ikke den brugerdefinerede validering 12 | default.not.inlist.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] findes ikke i listen [{3}] 13 | default.blank.message=Feltet [{0}] i klassen [{1}] kan ikke være tom 14 | default.not.equal.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] må ikke være [{3}] 15 | default.null.message=Feltet [{0}] i klassen [{1}] kan ikke være null 16 | default.not.unique.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] skal være unik 17 | 18 | default.paginate.prev=Forrige 19 | default.paginate.next=Næste 20 | default.boolean.true=Sand 21 | default.boolean.false=Falsk 22 | default.date.format=yyyy-MM-dd HH:mm:ss z 23 | default.number.format=0 24 | 25 | default.created.message={0} {1} oprettet 26 | default.updated.message={0} {1} opdateret 27 | default.deleted.message={0} {1} slettet 28 | default.not.deleted.message={0} {1} kunne ikke slettes 29 | default.not.found.message={0} med id {1} er ikke fundet 30 | default.optimistic.locking.failure=En anden bruger har opdateret denne {0} imens du har lavet rettelser 31 | 32 | default.home.label=Hjem 33 | default.list.label={0} Liste 34 | default.add.label=Tilføj {0} 35 | default.new.label=Ny {0} 36 | default.create.label=Opret {0} 37 | default.show.label=Vis {0} 38 | default.edit.label=Ret {0} 39 | 40 | default.button.create.label=Opret 41 | default.button.edit.label=Ret 42 | default.button.update.label=Opdater 43 | default.button.delete.label=Slet 44 | default.button.delete.confirm.message=Er du sikker? 45 | 46 | # Databindingsfejl. Brug "typeMismatch.$className.$propertyName for at passe til en given klasse (f.eks typeMismatch.Book.author) 47 | typeMismatch.java.net.URL=Feltet {0} skal være en valid URL 48 | typeMismatch.java.net.URI=Feltet {0} skal være en valid URI 49 | typeMismatch.java.util.Date=Feltet {0} skal være en valid Dato 50 | typeMismatch.java.lang.Double=Feltet {0} skal være et valid tal 51 | typeMismatch.java.lang.Integer=Feltet {0} skal være et valid tal 52 | typeMismatch.java.lang.Long=Feltet {0} skal være et valid tal 53 | typeMismatch.java.lang.Short=Feltet {0} skal være et valid tal 54 | typeMismatch.java.math.BigDecimal=Feltet {0} skal være et valid tal 55 | typeMismatch.java.math.BigInteger=Feltet {0} skal være et valid tal 56 | 57 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_de.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] entspricht nicht dem vorgegebenen Muster [{3}] 2 | default.invalid.url.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist keine gültige URL 3 | default.invalid.creditCard.message=Das Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist keine gültige Kreditkartennummer 4 | default.invalid.email.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist keine gültige E-Mail Adresse 5 | default.invalid.range.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist nicht im Wertebereich von [{3}] bis [{4}] 6 | default.invalid.size.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist nicht im Wertebereich von [{3}] bis [{4}] 7 | default.invalid.max.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist größer als der Höchstwert von [{3}] 8 | default.invalid.min.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist kleiner als der Mindestwert von [{3}] 9 | default.invalid.max.size.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] übersteigt den Höchstwert von [{3}] 10 | default.invalid.min.size.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] unterschreitet den Mindestwert von [{3}] 11 | default.invalid.validator.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist ungültig 12 | default.not.inlist.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist nicht in der Liste [{3}] enthalten. 13 | default.blank.message=Die Eigenschaft [{0}] des Typs [{1}] darf nicht leer sein 14 | default.not.equal.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] darf nicht gleich [{3}] sein 15 | default.null.message=Die Eigenschaft [{0}] des Typs [{1}] darf nicht null sein 16 | default.not.unique.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] darf nur einmal vorkommen 17 | 18 | default.paginate.prev=Vorherige 19 | default.paginate.next=Nächste 20 | default.boolean.true=Wahr 21 | default.boolean.false=Falsch 22 | default.date.format=dd.MM.yyyy HH:mm:ss z 23 | default.number.format=0 24 | 25 | default.created.message={0} {1} wurde angelegt 26 | default.updated.message={0} {1} wurde geändert 27 | default.deleted.message={0} {1} wurde gelöscht 28 | default.not.deleted.message={0} {1} konnte nicht gelöscht werden 29 | default.not.found.message={0} mit der id {1} wurde nicht gefunden 30 | default.optimistic.locking.failure=Ein anderer Benutzer hat das {0} Object geändert während Sie es bearbeitet haben 31 | 32 | default.home.label=Home 33 | default.list.label={0} Liste 34 | default.add.label={0} hinzufügen 35 | default.new.label={0} anlegen 36 | default.create.label={0} anlegen 37 | default.show.label={0} anzeigen 38 | default.edit.label={0} bearbeiten 39 | 40 | default.button.create.label=Anlegen 41 | default.button.edit.label=Bearbeiten 42 | default.button.update.label=Aktualisieren 43 | default.button.delete.label=Löschen 44 | default.button.delete.confirm.message=Sind Sie sicher? 45 | 46 | # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) 47 | typeMismatch.java.net.URL=Die Eigenschaft {0} muss eine gültige URL sein 48 | typeMismatch.java.net.URI=Die Eigenschaft {0} muss eine gültige URI sein 49 | typeMismatch.java.util.Date=Die Eigenschaft {0} muss ein gültiges Datum sein 50 | typeMismatch.java.lang.Double=Die Eigenschaft {0} muss eine gültige Zahl sein 51 | typeMismatch.java.lang.Integer=Die Eigenschaft {0} muss eine gültige Zahl sein 52 | typeMismatch.java.lang.Long=Die Eigenschaft {0} muss eine gültige Zahl sein 53 | typeMismatch.java.lang.Short=Die Eigenschaft {0} muss eine gültige Zahl sein 54 | typeMismatch.java.math.BigDecimal=Die Eigenschaft {0} muss eine gültige Zahl sein 55 | typeMismatch.java.math.BigInteger=Die Eigenschaft {0} muss eine gültige Zahl sein -------------------------------------------------------------------------------- /grails-app/i18n/messages_es.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no corresponde al patrón [{3}] 2 | default.invalid.url.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no es una URL válida 3 | default.invalid.creditCard.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no es un número de tarjeta de crédito válida 4 | default.invalid.email.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no es una dirección de correo electrónico válida 5 | default.invalid.range.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no entra en el rango válido de [{3}] a [{4}] 6 | default.invalid.size.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no entra en el tamaño válido de [{3}] a [{4}] 7 | default.invalid.max.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] excede el valor máximo [{3}] 8 | default.invalid.min.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] es menos que el valor mínimo [{3}] 9 | default.invalid.max.size.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] excede el tamaño máximo de [{3}] 10 | default.invalid.min.size.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] es menor que el tamaño mínimo de [{3}] 11 | default.invalid.validator.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no es válido 12 | default.not.inlist.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no esta contenido dentro de la lista [{3}] 13 | default.blank.message=La propiedad [{0}] de la clase [{1}] no puede ser vacía 14 | default.not.equal.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no puede igualar a [{3}] 15 | default.null.message=La propiedad [{0}] de la clase [{1}] no puede ser nulo 16 | default.not.unique.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] debe ser única 17 | 18 | default.paginate.prev=Anterior 19 | default.paginate.next=Siguiente 20 | default.boolean.true=Verdadero 21 | default.boolean.false=Falso 22 | default.date.format=yyyy-MM-dd HH:mm:ss z 23 | default.number.format=0 24 | 25 | default.created.message={0} {1} creado 26 | default.updated.message={0} {1} actualizado 27 | default.deleted.message={0} {1} eliminado 28 | default.not.deleted.message={0} {1} no puede eliminarse 29 | default.not.found.message=No se encuentra {0} con id {1} 30 | default.optimistic.locking.failure=Mientras usted editaba, otro usuario ha actualizado su {0} 31 | 32 | default.home.label=Principal 33 | default.list.label={0} Lista 34 | default.add.label=Agregar {0} 35 | default.new.label=Nuevo {0} 36 | default.create.label=Crear {0} 37 | default.show.label=Mostrar {0} 38 | default.edit.label=Editar {0} 39 | 40 | default.button.create.label=Crear 41 | default.button.edit.label=Editar 42 | default.button.update.label=Actualizar 43 | default.button.delete.label=Eliminar 44 | default.button.delete.confirm.message=¿Está usted seguro? 45 | 46 | # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) 47 | typeMismatch.java.net.URL=La propiedad {0} debe ser una URL válida 48 | typeMismatch.java.net.URI=La propiedad {0} debe ser una URI válida 49 | typeMismatch.java.util.Date=La propiedad {0} debe ser una fecha válida 50 | typeMismatch.java.lang.Double=La propiedad {0} debe ser un número válido 51 | typeMismatch.java.lang.Integer=La propiedad {0} debe ser un número válido 52 | typeMismatch.java.lang.Long=La propiedad {0} debe ser un número válido 53 | typeMismatch.java.lang.Short=La propiedad {0} debe ser un número válido 54 | typeMismatch.java.math.BigDecimal=La propiedad {0} debe ser un número válido 55 | typeMismatch.java.math.BigInteger=La propiedad {0} debe ser un número válido -------------------------------------------------------------------------------- /grails-app/i18n/messages_fr.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] ne correspond pas au pattern [{3}] 2 | default.invalid.url.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas une URL valide 3 | default.invalid.creditCard.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas un numéro de carte de crédit valide 4 | default.invalid.email.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas une adresse e-mail valide 5 | default.invalid.range.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas contenue dans l'intervalle [{3}] à [{4}] 6 | default.invalid.size.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas contenue dans l'intervalle [{3}] à [{4}] 7 | default.invalid.max.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est supérieure à la valeur maximum [{3}] 8 | default.invalid.min.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est inférieure à la valeur minimum [{3}] 9 | default.invalid.max.size.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est supérieure à la valeur maximum [{3}] 10 | default.invalid.min.size.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est inférieure à la valeur minimum [{3}] 11 | default.invalid.validator.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas valide 12 | default.not.inlist.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] ne fait pas partie de la liste [{3}] 13 | default.blank.message=La propriété [{0}] de la classe [{1}] ne peut pas être vide 14 | default.not.equal.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] ne peut pas être égale à [{3}] 15 | default.null.message=La propriété [{0}] de la classe [{1}] ne peut pas être nulle 16 | default.not.unique.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] doit être unique 17 | 18 | default.paginate.prev=Précédent 19 | default.paginate.next=Suivant 20 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_it.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non corrisponde al pattern [{3}] 2 | default.invalid.url.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non è un URL valido 3 | default.invalid.creditCard.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non è un numero di carta di credito valido 4 | default.invalid.email.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non è un indirizzo email valido 5 | default.invalid.range.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non rientra nell'intervallo valido da [{3}] a [{4}] 6 | default.invalid.size.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non rientra nell'intervallo di dimensioni valide da [{3}] a [{4}] 7 | default.invalid.max.message=La proprietà [{0}] della classe [{1}] con valore [{2}] è maggiore di [{3}] 8 | default.invalid.min.message=La proprietà [{0}] della classe [{1}] con valore [{2}] è minore di [{3}] 9 | default.invalid.max.size.message=La proprietà [{0}] della classe [{1}] con valore [{2}] è maggiore di [{3}] 10 | default.invalid.min.size.message=La proprietà [{0}] della classe [{1}] con valore [{2}] è minore di [{3}] 11 | default.invalid.validator.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non è valida 12 | default.not.inlist.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non è contenuta nella lista [{3}] 13 | default.blank.message=La proprietà [{0}] della classe [{1}] non può essere vuota 14 | default.not.equal.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non può essere uguale a [{3}] 15 | default.null.message=La proprietà [{0}] della classe [{1}] non può essere null 16 | default.not.unique.message=La proprietà [{0}] della classe [{1}] con valore [{2}] deve essere unica 17 | 18 | default.paginate.prev=Precedente 19 | default.paginate.next=Successivo 20 | default.boolean.true=Vero 21 | default.boolean.false=Falso 22 | default.date.format=dd/MM/yyyy HH:mm:ss z 23 | default.number.format=0 24 | 25 | default.created.message={0} {1} creato 26 | default.updated.message={0} {1} aggiornato 27 | default.deleted.message={0} {1} eliminato 28 | default.not.deleted.message={0} {1} non può essere eliminato 29 | default.not.found.message={0} non trovato con id {1} 30 | default.optimistic.locking.failure=Un altro utente ha aggiornato questo {0} mentre si era in modifica 31 | 32 | default.home.label=Home 33 | default.list.label={0} Elenco 34 | default.add.label=Aggiungi {0} 35 | default.new.label=Nuovo {0} 36 | default.create.label=Crea {0} 37 | default.show.label=Mostra {0} 38 | default.edit.label=Modifica {0} 39 | 40 | default.button.create.label=Crea 41 | default.button.edit.label=Modifica 42 | default.button.update.label=Aggiorna 43 | default.button.delete.label=Elimina 44 | default.button.delete.confirm.message=Si è sicuri? 45 | 46 | # Data binding errors. Usa "typeMismatch.$className.$propertyName per la personalizzazione (es typeMismatch.Book.author) 47 | typeMismatch.java.net.URL=La proprietà {0} deve essere un URL valido 48 | typeMismatch.java.net.URI=La proprietà {0} deve essere un URI valido 49 | typeMismatch.java.util.Date=La proprietà {0} deve essere una data valida 50 | typeMismatch.java.lang.Double=La proprietà {0} deve essere un numero valido 51 | typeMismatch.java.lang.Integer=La proprietà {0} deve essere un numero valido 52 | typeMismatch.java.lang.Long=La proprietà {0} deve essere un numero valido 53 | typeMismatch.java.lang.Short=La proprietà {0} deve essere un numero valido 54 | typeMismatch.java.math.BigDecimal=La proprietà {0} deve essere un numero valido 55 | typeMismatch.java.math.BigInteger=La proprietà {0} deve essere un numero valido 56 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_ja.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]パターンと一致していません。 2 | default.invalid.url.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、有効なURLではありません。 3 | default.invalid.creditCard.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、有効なクレジットカード番号ではありません。 4 | default.invalid.email.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、有効なメールアドレスではありません。 5 | default.invalid.range.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]から[{4}]範囲内を指定してください。 6 | default.invalid.size.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]から[{4}]以内を指定してください。 7 | default.invalid.max.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最大値[{3}]より大きいです。 8 | default.invalid.min.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最小値[{3}]より小さいです。 9 | default.invalid.max.size.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最大値[{3}]より大きいです。 10 | default.invalid.min.size.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最小値[{3}]より小さいです。 11 | default.invalid.validator.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、カスタムバリデーションを通過できません。 12 | default.not.inlist.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]リスト内に存在しません。 13 | default.blank.message=[{1}]クラスのプロパティ[{0}]の空白は許可されません。 14 | default.not.equal.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]と同等ではありません。 15 | default.null.message=[{1}]クラスのプロパティ[{0}]にnullは許可されません。 16 | default.not.unique.message=クラス[{1}]プロパティ[{0}]の値[{2}]は既に使用されています。 17 | 18 | default.paginate.prev=戻る 19 | default.paginate.next=次へ 20 | default.boolean.true=はい 21 | default.boolean.false=いいえ 22 | default.date.format=yyyy/MM/dd HH:mm:ss z 23 | default.number.format=0 24 | 25 | default.created.message={0}(id:{1})を作成しました。 26 | default.updated.message={0}(id:{1})を更新しました。 27 | default.deleted.message={0}(id:{1})を削除しました。 28 | default.not.deleted.message={0}(id:{1})は削除できませんでした。 29 | default.not.found.message={0}(id:{1})は見つかりませんでした。 30 | default.optimistic.locking.failure=この{0}は編集中に他のユーザによって先に更新されています。 31 | 32 | default.home.label=ホーム 33 | default.list.label={0}リスト 34 | default.add.label={0}を追加 35 | default.new.label={0}を新規作成 36 | default.create.label={0}を作成 37 | default.show.label={0}詳細 38 | default.edit.label={0}を編集 39 | 40 | default.button.create.label=作成 41 | default.button.edit.label=編集 42 | default.button.update.label=更新 43 | default.button.delete.label=削除 44 | default.button.delete.confirm.message=本当に削除してよろしいですか? 45 | 46 | # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) 47 | typeMismatch.java.net.URL={0}は有効なURLでなければなりません。 48 | typeMismatch.java.net.URI={0}は有効なURIでなければなりません。 49 | typeMismatch.java.util.Date={0}は有効な日付でなければなりません。 50 | typeMismatch.java.lang.Double={0}は有効な数値でなければなりません。 51 | typeMismatch.java.lang.Integer={0}は有効な数値でなければなりません。 52 | typeMismatch.java.lang.Long={0}は有効な数値でなければなりません。 53 | typeMismatch.java.lang.Short={0}は有効な数値でなければなりません。 54 | typeMismatch.java.math.BigDecimal={0}は有効な数値でなければなりません。 55 | typeMismatch.java.math.BigInteger={0}は有効な数値でなければなりません。 56 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_nb.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] overholder ikke mønsteret [{3}] 2 | default.invalid.url.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] er ikke en gyldig URL 3 | default.invalid.creditCard.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] er ikke et gyldig kredittkortnummer 4 | default.invalid.email.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] er ikke en gyldig epostadresse 5 | default.invalid.range.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] er ikke innenfor intervallet [{3}] til [{4}] 6 | default.invalid.size.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] er ikke innenfor intervallet [{3}] til [{4}] 7 | default.invalid.max.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] overstiger maksimumsverdien på [{3}] 8 | default.invalid.min.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] er under minimumsverdien på [{3}] 9 | default.invalid.max.size.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] overstiger maksimumslengden på [{3}] 10 | default.invalid.min.size.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] er kortere enn minimumslengden på [{3}] 11 | default.invalid.validator.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] overholder ikke den brukerdefinerte valideringen 12 | default.not.inlist.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] finnes ikke i listen [{3}] 13 | default.blank.message=Feltet [{0}] i klassen [{1}] kan ikke være tom 14 | default.not.equal.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] kan ikke være [{3}] 15 | default.null.message=Feltet [{0}] i klassen [{1}] kan ikke være null 16 | default.not.unique.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] må være unik 17 | 18 | default.paginate.prev=Forrige 19 | default.paginate.next=Neste 20 | default.boolean.true=Ja 21 | default.boolean.false=Nei 22 | default.date.format=dd.MM.yyyy HH:mm:ss z 23 | default.number.format=0 24 | 25 | default.created.message={0} {1} opprettet 26 | default.updated.message={0} {1} oppdatert 27 | default.deleted.message={0} {1} slettet 28 | default.not.deleted.message={0} {1} kunne ikke slettes 29 | default.not.found.message={0} med id {1} ble ikke funnet 30 | default.optimistic.locking.failure=En annen bruker har oppdatert denne {0} mens du redigerte 31 | 32 | default.home.label=Hjem 33 | default.list.label={0}liste 34 | default.add.label=Legg til {0} 35 | default.new.label=Ny {0} 36 | default.create.label=Opprett {0} 37 | default.show.label=Vis {0} 38 | default.edit.label=Endre {0} 39 | 40 | default.button.create.label=Opprett 41 | default.button.edit.label=Endre 42 | default.button.update.label=Oppdater 43 | default.button.delete.label=Slett 44 | default.button.delete.confirm.message=Er du sikker? 45 | 46 | # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) 47 | typeMismatch.java.net.URL=Feltet {0} må være en gyldig URL 48 | typeMismatch.java.net.URI=Feltet {0} må være en gyldig URI 49 | typeMismatch.java.util.Date=Feltet {0} må være en gyldig dato 50 | typeMismatch.java.lang.Double=Feltet {0} må være et gyldig tall 51 | typeMismatch.java.lang.Integer=Feltet {0} må være et gyldig heltall 52 | typeMismatch.java.lang.Long=Feltet {0} må være et gyldig heltall 53 | typeMismatch.java.lang.Short=Feltet {0} må være et gyldig heltall 54 | typeMismatch.java.math.BigDecimal=Feltet {0} må være et gyldig tall 55 | typeMismatch.java.math.BigInteger=Feltet {0} må være et gyldig heltall 56 | 57 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_nl.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] komt niet overeen met het vereiste patroon [{3}] 2 | default.invalid.url.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is geen geldige URL 3 | default.invalid.creditCard.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is geen geldig credit card nummer 4 | default.invalid.email.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is geen geldig e-mailadres 5 | default.invalid.range.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] valt niet in de geldige waardenreeks van [{3}] tot [{4}] 6 | default.invalid.size.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] valt niet in de geldige grootte van [{3}] tot [{4}] 7 | default.invalid.max.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] overschrijdt de maximumwaarde [{3}] 8 | default.invalid.min.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is minder dan de minimumwaarde [{3}] 9 | default.invalid.max.size.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] overschrijdt de maximumgrootte van [{3}] 10 | default.invalid.min.size.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is minder dan minimumgrootte van [{3}] 11 | default.invalid.validator.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is niet geldig 12 | default.not.inlist.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] komt niet voor in de lijst [{3}] 13 | default.blank.message=Attribuut [{0}] van entiteit [{1}] mag niet leeg zijn 14 | default.not.equal.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] mag niet gelijk zijn aan [{3}] 15 | default.null.message=Attribuut [{0}] van entiteit [{1}] mag niet leeg zijn 16 | default.not.unique.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] moet uniek zijn 17 | 18 | default.paginate.prev=Vorige 19 | default.paginate.next=Volgende 20 | default.boolean.true=Ja 21 | default.boolean.false=Nee 22 | default.date.format=dd-MM-yyyy HH:mm:ss z 23 | default.number.format=0 24 | 25 | default.created.message={0} {1} ingevoerd 26 | default.updated.message={0} {1} gewijzigd 27 | default.deleted.message={0} {1} verwijderd 28 | default.not.deleted.message={0} {1} kon niet worden verwijderd 29 | default.not.found.message={0} met id {1} kon niet worden gevonden 30 | default.optimistic.locking.failure=Een andere gebruiker heeft deze {0} al gewijzigd 31 | 32 | default.home.label=Home 33 | default.list.label={0} Overzicht 34 | default.add.label=Toevoegen {0} 35 | default.new.label=Invoeren {0} 36 | default.create.label=Invoeren {0} 37 | default.show.label=Details {0} 38 | default.edit.label=Wijzigen {0} 39 | 40 | default.button.create.label=Invoeren 41 | default.button.edit.label=Wijzigen 42 | default.button.update.label=Opslaan 43 | default.button.delete.label=Verwijderen 44 | default.button.delete.confirm.message=Weet je het zeker? 45 | 46 | # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) 47 | typeMismatch.java.net.URL=Attribuut {0} is geen geldige URL 48 | typeMismatch.java.net.URI=Attribuut {0} is geen geldige URI 49 | typeMismatch.java.util.Date=Attribuut {0} is geen geldige datum 50 | typeMismatch.java.lang.Double=Attribuut {0} is geen geldig nummer 51 | typeMismatch.java.lang.Integer=Attribuut {0} is geen geldig nummer 52 | typeMismatch.java.lang.Long=Attribuut {0} is geen geldig nummer 53 | typeMismatch.java.lang.Short=Attribuut {0} is geen geldig nummer 54 | typeMismatch.java.math.BigDecimal=Attribuut {0} is geen geldig nummer 55 | typeMismatch.java.math.BigInteger=Attribuut {0} is geen geldig nummer 56 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_pl.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Translated by Matthias Hryniszak - padcom@gmail.com 3 | # 4 | 5 | default.doesnt.match.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] nie pasuje do wymaganego wzorca [{3}] 6 | default.invalid.url.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] jest niepoprawnym adresem URL 7 | default.invalid.creditCard.message=Właściwość [{0}] klasy [{1}] with value [{2}] nie jest poprawnym numerem karty kredytowej 8 | default.invalid.email.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] nie jest poprawnym adresem e-mail 9 | default.invalid.range.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] nie zawiera się zakładanym zakresie od [{3}] do [{4}] 10 | default.invalid.size.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] nie zawiera się w zakładanym zakresie rozmiarów od [{3}] do [{4}] 11 | default.invalid.max.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] przekracza maksymalną wartość [{3}] 12 | default.invalid.min.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] jest mniejsza niż minimalna wartość [{3}] 13 | default.invalid.max.size.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] przekracza maksymalny rozmiar [{3}] 14 | default.invalid.min.size.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] jest mniejsza niż minimalny rozmiar [{3}] 15 | default.invalid.validator.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] nie spełnia założonych niestandardowych warunków 16 | default.not.inlist.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] nie zawiera się w liście [{3}] 17 | default.blank.message=Właściwość [{0}] klasy [{1}] nie może być pusta 18 | default.not.equal.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] nie może równać się [{3}] 19 | default.null.message=Właściwość [{0}] klasy [{1}] nie może być null 20 | default.not.unique.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] musi być unikalna 21 | 22 | default.paginate.prev=Poprzedni 23 | default.paginate.next=Następny 24 | default.boolean.true=Prawda 25 | default.boolean.false=Fałsz 26 | default.date.format=yyyy-MM-dd HH:mm:ss z 27 | default.number.format=0 28 | 29 | default.created.message=Utworzono {0} {1} 30 | default.updated.message=Zaktualizowano {0} {1} 31 | default.deleted.message=Usunięto {0} {1} 32 | default.not.deleted.message={0} {1} nie mógł zostać usunięty 33 | default.not.found.message=Nie znaleziono {0} o id {1} 34 | default.optimistic.locking.failure=Inny użytkownik zaktualizował ten obiekt {0} w trakcie twoich zmian 35 | 36 | default.home.label=Strona domowa 37 | default.list.label=Lista {0} 38 | default.add.label=Dodaj {0} 39 | default.new.label=Utwórz {0} 40 | default.create.label=Utwórz {0} 41 | default.show.label=Pokaż {0} 42 | default.edit.label=Edytuj {0} 43 | 44 | default.button.create.label=Utwórz 45 | default.button.edit.label=Edytuj 46 | default.button.update.label=Zaktualizuj 47 | default.button.delete.label=Usuń 48 | default.button.delete.confirm.message=Czy jesteś pewien? 49 | 50 | # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) 51 | typeMismatch.java.net.URL=Właściwość {0} musi być poprawnym adresem URL 52 | typeMismatch.java.net.URI=Właściwość {0} musi być poprawnym adresem URI 53 | typeMismatch.java.util.Date=Właściwość {0} musi być poprawną datą 54 | typeMismatch.java.lang.Double=Właściwość {0} musi być poprawnyą liczbą 55 | typeMismatch.java.lang.Integer=Właściwość {0} musi być poprawnyą liczbą 56 | typeMismatch.java.lang.Long=Właściwość {0} musi być poprawnyą liczbą 57 | typeMismatch.java.lang.Short=Właściwość {0} musi być poprawnyą liczbą 58 | typeMismatch.java.math.BigDecimal=Właściwość {0} musi być poprawnyą liczbą 59 | typeMismatch.java.math.BigInteger=Właściwość {0} musi być poprawnyą liczbą 60 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_pt_BR.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Translated by Lucas Teixeira - lucastex@gmail.com 3 | # 4 | 5 | default.doesnt.match.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atende ao padrão definido [{3}] 6 | default.invalid.url.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é uma URL válida 7 | default.invalid.creditCard.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um número válido de cartão de crédito 8 | default.invalid.email.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um endereço de email válido. 9 | default.invalid.range.message=O campo [{0}] da classe [{1}] com o valor [{2}] não está entre a faixa de valores válida de [{3}] até [{4}] 10 | default.invalid.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] não está na faixa de tamanho válida de [{3}] até [{4}] 11 | default.invalid.max.message=O campo [{0}] da classe [{1}] com o valor [{2}] ultrapass o valor máximo [{3}] 12 | default.invalid.min.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atinge o valor mínimo [{3}] 13 | default.invalid.max.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] ultrapassa o tamanho máximo de [{3}] 14 | default.invalid.min.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atinge o tamanho mínimo de [{3}] 15 | default.invalid.validator.message=O campo [{0}] da classe [{1}] com o valor [{2}] não passou na validação 16 | default.not.inlist.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um valor dentre os permitidos na lista [{3}] 17 | default.blank.message=O campo [{0}] da classe [{1}] não pode ficar em branco 18 | default.not.equal.message=O campo [{0}] da classe [{1}] com o valor [{2}] não pode ser igual a [{3}] 19 | default.null.message=O campo [{0}] da classe [{1}] não pode ser vazia 20 | default.not.unique.message=O campo [{0}] da classe [{1}] com o valor [{2}] deve ser único 21 | 22 | default.paginate.prev=Anterior 23 | default.paginate.next=Próximo 24 | default.boolean.true=Sim 25 | default.boolean.false=Não 26 | default.date.format=dd/MM/yyyy HH:mm:ss z 27 | default.number.format=0 28 | 29 | default.created.message={0} {1} criado 30 | default.updated.message={0} {1} atualizado 31 | default.deleted.message={0} {1} removido 32 | default.not.deleted.message={0} {1} não pode ser removido 33 | default.not.found.message={0} não foi encontrado com id {1} 34 | default.optimistic.locking.failure=Outro usuário atualizou este [{0}] enquanto você tentou salvá-lo 35 | 36 | default.home.label=Principal 37 | default.list.label={0} Listagem 38 | default.add.label=Adicionar {0} 39 | default.new.label=Novo {0} 40 | default.create.label=Criar {0} 41 | default.show.label=Ver {0} 42 | default.edit.label=Editar {0} 43 | 44 | default.button.create.label=Criar 45 | default.button.edit.label=Editar 46 | default.button.update.label=Alterar 47 | default.button.delete.label=Remover 48 | default.button.delete.confirm.message=Tem certeza? 49 | 50 | # Mensagens de erro em atribuição de valores. Use "typeMismatch.$className.$propertyName" para customizar (eg typeMismatch.Book.author) 51 | typeMismatch.java.net.URL=O campo {0} deve ser uma URL válida. 52 | typeMismatch.java.net.URI=O campo {0} deve ser uma URI válida. 53 | typeMismatch.java.util.Date=O campo {0} deve ser uma data válida 54 | typeMismatch.java.lang.Double=O campo {0} deve ser um número válido. 55 | typeMismatch.java.lang.Integer=O campo {0} deve ser um número válido. 56 | typeMismatch.java.lang.Long=O campo {0} deve ser um número válido. 57 | typeMismatch.java.lang.Short=O campo {0} deve ser um número válido. 58 | typeMismatch.java.math.BigDecimal=O campo {0} deve ser um número válido. 59 | typeMismatch.java.math.BigInteger=O campo {0} deve ser um número válido. -------------------------------------------------------------------------------- /grails-app/i18n/messages_pt_PT.properties: -------------------------------------------------------------------------------- 1 | # 2 | # translation by miguel.ping@gmail.com, based on pt_BR translation by Lucas Teixeira - lucastex@gmail.com 3 | # 4 | 5 | default.doesnt.match.message=O campo [{0}] da classe [{1}] com o valor [{2}] não corresponde ao padrão definido [{3}] 6 | default.invalid.url.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um URL válido 7 | default.invalid.creditCard.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um número válido de cartão de crédito 8 | default.invalid.email.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um endereço de email válido. 9 | default.invalid.range.message=O campo [{0}] da classe [{1}] com o valor [{2}] não está dentro dos limites de valores válidos de [{3}] a [{4}] 10 | default.invalid.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] está fora dos limites de tamanho válido de [{3}] a [{4}] 11 | default.invalid.max.message=O campo [{0}] da classe [{1}] com o valor [{2}] ultrapassa o valor máximo [{3}] 12 | default.invalid.min.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atinge o valor mínimo [{3}] 13 | default.invalid.max.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] ultrapassa o tamanho máximo de [{3}] 14 | default.invalid.min.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atinge o tamanho mínimo de [{3}] 15 | default.invalid.validator.message=O campo [{0}] da classe [{1}] com o valor [{2}] não passou na validação 16 | default.not.inlist.message=O campo [{0}] da classe [{1}] com o valor [{2}] não se encontra nos valores permitidos da lista [{3}] 17 | default.blank.message=O campo [{0}] da classe [{1}] não pode ser vazio 18 | default.not.equal.message=O campo [{0}] da classe [{1}] com o valor [{2}] não pode ser igual a [{3}] 19 | default.null.message=O campo [{0}] da classe [{1}] não pode ser vazio 20 | default.not.unique.message=O campo [{0}] da classe [{1}] com o valor [{2}] deve ser único 21 | 22 | default.paginate.prev=Anterior 23 | default.paginate.next=Próximo 24 | 25 | # Mensagens de erro em atribuição de valores. Use "typeMismatch.$className.$propertyName" para personalizar(eg typeMismatch.Book.author) 26 | typeMismatch.java.net.URL=O campo {0} deve ser um URL válido. 27 | typeMismatch.java.net.URI=O campo {0} deve ser um URI válido. 28 | typeMismatch.java.util.Date=O campo {0} deve ser uma data válida 29 | typeMismatch.java.lang.Double=O campo {0} deve ser um número válido. 30 | typeMismatch.java.lang.Integer=O campo {0} deve ser um número válido. 31 | typeMismatch.java.lang.Long=O campo {0} deve ser um número valido. 32 | typeMismatch.java.lang.Short=O campo {0} deve ser um número válido. 33 | typeMismatch.java.math.BigDecimal=O campo {0} deve ser um número válido. 34 | typeMismatch.java.math.BigInteger=O campo {0} deve ser um número válido. 35 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_ru.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=Значение [{2}] поля [{0}] класса [{1}] не соответствует образцу [{3}] 2 | default.invalid.url.message=Значение [{2}] поля [{0}] класса [{1}] не является допустимым URL-адресом 3 | default.invalid.creditCard.message=Значение [{2}] поля [{0}] класса [{1}] не является допустимым номером кредитной карты 4 | default.invalid.email.message=Значение [{2}] поля [{0}] класса [{1}] не является допустимым e-mail адресом 5 | default.invalid.range.message=Значение [{2}] поля [{0}] класса [{1}] не попадает в допустимый интервал от [{3}] до [{4}] 6 | default.invalid.size.message=Размер поля [{0}] класса [{1}] (значение: [{2}]) не попадает в допустимый интервал от [{3}] до [{4}] 7 | default.invalid.max.message=Значение [{2}] поля [{0}] класса [{1}] больше чем максимально допустимое значение [{3}] 8 | default.invalid.min.message=Значение [{2}] поля [{0}] класса [{1}] меньше чем минимально допустимое значение [{3}] 9 | default.invalid.max.size.message=Размер поля [{0}] класса [{1}] (значение: [{2}]) больше чем максимально допустимый размер [{3}] 10 | default.invalid.min.size.message=Размер поля [{0}] класса [{1}] (значение: [{2}]) меньше чем минимально допустимый размер [{3}] 11 | default.invalid.validator.message=Значение [{2}] поля [{0}] класса [{1}] не допустимо 12 | default.not.inlist.message=Значение [{2}] поля [{0}] класса [{1}] не попадает в список допустимых значений [{3}] 13 | default.blank.message=Поле [{0}] класса [{1}] не может быть пустым 14 | default.not.equal.message=Значение [{2}] поля [{0}] класса [{1}] не может быть равно [{3}] 15 | default.null.message=Поле [{0}] класса [{1}] не может иметь значение null 16 | default.not.unique.message=Значение [{2}] поля [{0}] класса [{1}] должно быть уникальным 17 | 18 | default.paginate.prev=Предыдушая страница 19 | default.paginate.next=Следующая страница 20 | 21 | # Ошибки при присвоении данных. Для точной настройки для полей классов используйте 22 | # формат "typeMismatch.$className.$propertyName" (например, typeMismatch.Book.author) 23 | typeMismatch.java.net.URL=Значение поля {0} не является допустимым URL 24 | typeMismatch.java.net.URI=Значение поля {0} не является допустимым URI 25 | typeMismatch.java.util.Date=Значение поля {0} не является допустимой датой 26 | typeMismatch.java.lang.Double=Значение поля {0} не является допустимым числом 27 | typeMismatch.java.lang.Integer=Значение поля {0} не является допустимым числом 28 | typeMismatch.java.lang.Long=Значение поля {0} не является допустимым числом 29 | typeMismatch.java.lang.Short=Значение поля {0} не является допустимым числом 30 | typeMismatch.java.math.BigDecimal=Значение поля {0} не является допустимым числом 31 | typeMismatch.java.math.BigInteger=Значение поля {0} не является допустимым числом 32 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_sv.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=Attributet [{0}] för klassen [{1}] med värde [{2}] matchar inte mot uttrycket [{3}] 2 | default.invalid.url.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är inte en giltig URL 3 | default.invalid.creditCard.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är inte ett giltigt kreditkortsnummer 4 | default.invalid.email.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är inte en giltig e-postadress 5 | default.invalid.range.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är inte inom intervallet [{3}] till [{4}] 6 | default.invalid.size.message=Attributet [{0}] för klassen [{1}] med värde [{2}] har en storlek som inte är inom [{3}] till [{4}] 7 | default.invalid.max.message=Attributet [{0}] för klassen [{1}] med värde [{2}] överskrider maxvärdet [{3}] 8 | default.invalid.min.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är mindre än minimivärdet [{3}] 9 | default.invalid.max.size.message=Attributet [{0}] för klassen [{1}] med värde [{2}] överskrider maxstorleken [{3}] 10 | default.invalid.min.size.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är mindre än minimistorleken [{3}] 11 | default.invalid.validator.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är inte giltigt enligt anpassad regel 12 | default.not.inlist.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är inte giltigt, måste vara ett av [{3}] 13 | default.blank.message=Attributet [{0}] för klassen [{1}] får inte vara tomt 14 | default.not.equal.message=Attributet [{0}] för klassen [{1}] med värde [{2}] får inte vara lika med [{3}] 15 | default.null.message=Attributet [{0}] för klassen [{1}] får inte vara tomt 16 | default.not.unique.message=Attributet [{0}] för klassen [{1}] med värde [{2}] måste vara unikt 17 | 18 | default.paginate.prev=Föregående 19 | default.paginate.next=Nästa 20 | default.boolean.true=Sant 21 | default.boolean.false=Falskt 22 | default.date.format=yyyy-MM-dd HH:mm:ss z 23 | default.number.format=0 24 | 25 | default.created.message={0} {1} skapades 26 | default.updated.message={0} {1} uppdaterades 27 | default.deleted.message={0} {1} borttagen 28 | default.not.deleted.message={0} {1} kunde inte tas bort 29 | default.not.found.message={0} med id {1} kunde inte hittas 30 | default.optimistic.locking.failure=En annan användare har uppdaterat det här {0} objektet medan du redigerade det 31 | 32 | default.home.label=Hem 33 | default.list.label= {0} - Lista 34 | default.add.label=Lägg till {0} 35 | default.new.label=Skapa {0} 36 | default.create.label=Skapa {0} 37 | default.show.label=Visa {0} 38 | default.edit.label=Ändra {0} 39 | 40 | default.button.create.label=Skapa 41 | default.button.edit.label=Ändra 42 | default.button.update.label=Uppdatera 43 | default.button.delete.label=Ta bort 44 | default.button.delete.confirm.message=Är du säker? 45 | 46 | # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) 47 | typeMismatch.java.net.URL=Värdet för {0} måste vara en giltig URL 48 | typeMismatch.java.net.URI=Värdet för {0} måste vara en giltig URI 49 | typeMismatch.java.util.Date=Värdet {0} måste vara ett giltigt datum 50 | typeMismatch.java.lang.Double=Värdet {0} måste vara ett giltigt nummer 51 | typeMismatch.java.lang.Integer=Värdet {0} måste vara ett giltigt heltal 52 | typeMismatch.java.lang.Long=Värdet {0} måste vara ett giltigt heltal 53 | typeMismatch.java.lang.Short=Värdet {0} måste vara ett giltigt heltal 54 | typeMismatch.java.math.BigDecimal=Värdet {0} måste vara ett giltigt nummer 55 | typeMismatch.java.math.BigInteger=Värdet {0} måste vara ett giltigt heltal -------------------------------------------------------------------------------- /grails-app/i18n/messages_th.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ถูกต้องตามรูปแบบที่กำหนดไว้ใน [{3}] 2 | default.invalid.url.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ถูกต้องตามรูปแบบ URL 3 | default.invalid.creditCard.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ถูกต้องตามรูปแบบหมายเลขบัตรเครดิต 4 | default.invalid.email.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ถูกต้องตามรูปแบบอีเมล์ 5 | default.invalid.range.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ได้มีค่าที่ถูกต้องในช่วงจาก [{3}] ถึง [{4}] 6 | default.invalid.size.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ได้มีขนาดที่ถูกต้องในช่วงจาก [{3}] ถึง [{4}] 7 | default.invalid.max.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] มีค่าเกิดกว่าค่ามากสุด [{3}] 8 | default.invalid.min.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] มีค่าน้อยกว่าค่าต่ำสุด [{3}] 9 | default.invalid.max.size.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] มีขนาดเกินกว่าขนาดมากสุดของ [{3}] 10 | default.invalid.min.size.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] มีขนาดต่ำกว่าขนาดต่ำสุดของ [{3}] 11 | default.invalid.validator.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ผ่านการทวนสอบค่าที่ตั้งขึ้น 12 | default.not.inlist.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ได้อยู่ในรายการต่อไปนี้ [{3}] 13 | default.blank.message=คุณสมบัติ [{0}] ของคลาส [{1}] ไม่สามารถเป็นค่าว่างได้ 14 | default.not.equal.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่สามารถเท่ากับ [{3}] ได้ 15 | default.null.message=คุณสมบัติ [{0}] ของคลาส [{1}] ไม่สามารถเป็น null ได้ 16 | default.not.unique.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] จะต้องไม่ซ้ำ (unique) 17 | 18 | default.paginate.prev=ก่อนหน้า 19 | default.paginate.next=ถัดไป 20 | default.boolean.true=จริง 21 | default.boolean.false=เท็จ 22 | default.date.format=dd-MM-yyyy HH:mm:ss z 23 | default.number.format=0 24 | 25 | default.created.message=สร้าง {0} {1} เรียบร้อยแล้ว 26 | default.updated.message=ปรับปรุง {0} {1} เรียบร้อยแล้ว 27 | default.deleted.message=ลบ {0} {1} เรียบร้อยแล้ว 28 | default.not.deleted.message=ไม่สามารถลบ {0} {1} 29 | default.not.found.message=ไม่พบ {0} ด้วย id {1} นี้ 30 | default.optimistic.locking.failure=มีผู้ใช้ท่านอื่นปรับปรุง {0} ขณะที่คุณกำลังแก้ไขข้อมูลอยู่ 31 | 32 | default.home.label=หน้าแรก 33 | default.list.label=รายการ {0} 34 | default.add.label=เพิ่ม {0} 35 | default.new.label=สร้าง {0} ใหม่ 36 | default.create.label=สร้าง {0} 37 | default.show.label=แสดง {0} 38 | default.edit.label=แก้ไข {0} 39 | 40 | default.button.create.label=สร้าง 41 | default.button.edit.label=แก้ไข 42 | default.button.update.label=ปรับปรุง 43 | default.button.delete.label=ลบ 44 | default.button.delete.confirm.message=คุณแน่ใจหรือไม่ ? 45 | 46 | # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) 47 | typeMismatch.java.net.URL=คุณสมบัติ '{0}' จะต้องเป็นค่า URL ที่ถูกต้อง 48 | typeMismatch.java.net.URI=คุณสมบัติ '{0}' จะต้องเป็นค่า URI ที่ถูกต้อง 49 | typeMismatch.java.util.Date=คุณสมบัติ '{0}' จะต้องมีค่าเป็นวันที่ 50 | typeMismatch.java.lang.Double=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท Double 51 | typeMismatch.java.lang.Integer=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท Integer 52 | typeMismatch.java.lang.Long=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท Long 53 | typeMismatch.java.lang.Short=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท Short 54 | typeMismatch.java.math.BigDecimal=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท BigDecimal 55 | typeMismatch.java.math.BigInteger=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท BigInteger 56 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_zh_CN.properties: -------------------------------------------------------------------------------- 1 | default.blank.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u4E0D\u80FD\u4E3A\u7A7A 2 | default.doesnt.match.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0E\u5B9A\u4E49\u7684\u6A21\u5F0F [{3}]\u4E0D\u5339\u914D 3 | default.invalid.creditCard.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684\u4FE1\u7528\u5361\u53F7 4 | default.invalid.email.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u662F\u4E00\u4E2A\u5408\u6CD5\u7684\u7535\u5B50\u90AE\u4EF6\u5730\u5740 5 | default.invalid.max.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u6BD4\u6700\u5927\u503C [{3}]\u8FD8\u5927 6 | default.invalid.max.size.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u7684\u5927\u5C0F\u6BD4\u6700\u5927\u503C [{3}]\u8FD8\u5927 7 | default.invalid.min.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u6BD4\u6700\u5C0F\u503C [{3}]\u8FD8\u5C0F 8 | default.invalid.min.size.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u7684\u5927\u5C0F\u6BD4\u6700\u5C0F\u503C [{3}]\u8FD8\u5C0F 9 | default.invalid.range.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u5728\u5408\u6CD5\u7684\u8303\u56F4\u5185( [{3}] \uFF5E [{4}] ) 10 | default.invalid.size.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u7684\u5927\u5C0F\u4E0D\u5728\u5408\u6CD5\u7684\u8303\u56F4\u5185( [{3}] \uFF5E [{4}] ) 11 | default.invalid.url.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u662F\u4E00\u4E2A\u5408\u6CD5\u7684URL 12 | default.invalid.validator.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u672A\u80FD\u901A\u8FC7\u81EA\u5B9A\u4E49\u7684\u9A8C\u8BC1 13 | default.not.equal.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0E[{3}]\u4E0D\u76F8\u7B49 14 | default.not.inlist.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u5728\u5217\u8868\u7684\u53D6\u503C\u8303\u56F4\u5185 15 | default.not.unique.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u5FC5\u987B\u662F\u552F\u4E00\u7684 16 | default.null.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u4E0D\u80FD\u4E3Anull 17 | default.paginate.next=\u4E0B\u9875 18 | default.paginate.prev=\u4E0A\u9875 19 | -------------------------------------------------------------------------------- /grails-app/services/fhir/AuthorizationService.groovy: -------------------------------------------------------------------------------- 1 | package fhir; 2 | 3 | import com.mongodb.BasicDBObject 4 | import grails.plugins.rest.client.RestBuilder 5 | import fhir.searchParam.SearchParamHandler 6 | import org.hl7.fhir.instance.model.Patient 7 | 8 | import javax.annotation.PostConstruct 9 | class AuthorizationException extends Exception { 10 | def AuthorizationException(String msg){ 11 | super(msg) 12 | } 13 | } 14 | 15 | /** 16 | * @author jmandel 17 | * 18 | */ 19 | class AuthorizationService{ 20 | 21 | def transactional = false 22 | def tokenCache 23 | def grailsApplication 24 | def rest = new RestBuilder(connectTimeout:2000, readTimeout:2000) 25 | String authHeader 26 | Map oauth 27 | Map localAuth 28 | 29 | 30 | @PostConstruct 31 | void init() { 32 | oauth = grailsApplication.config.fhir.oauth 33 | localAuth = grailsApplication.config.localAuth 34 | } 35 | 36 | /** 37 | * @param toCheck 38 | * @return JSON structure with token introspection details, like 39 | { 40 | "active": true, 41 | "scope": "summary:", 42 | "exp": "2013-08-23T20:16:07-0400", 43 | "sub": "admin", 44 | "client_id": "3ed9d143-c7f6-40e5-b36f-4ad5665c31de", 45 | "token_type": "Bearer" 46 | } 47 | */ 48 | def lookup(String toCheck){ 49 | String url = oauth.introspectionUri.replace("{token}", toCheck) 50 | log.debug("GET " + url) 51 | def response = rest.get(url) { 52 | auth(oauth.clientId, oauth.clientSecret) 53 | } 54 | if (response.status != 200) 55 | return null 56 | 57 | return response.json 58 | } 59 | 60 | Authorization asBasicAuth(request){ 61 | def header = request.getHeader("Authorization") =~ /^Basic (.*)$/ 62 | if(header){ 63 | log.debug("exampine an admin access password") 64 | 65 | String[] decoded = new String(header[0][1].decodeBase64()).split(':') 66 | log.debug("try an admin access password" + decoded) 67 | if (decoded[0] == localAuth.clientId && decoded[1] == localAuth.clientSecret) { 68 | def ret = new Authorization(isAdmin: true, app: localAuth.clientId) 69 | return ret 70 | } 71 | } 72 | return null 73 | } 74 | 75 | Authorization asBearerAuth(request){ 76 | def header = request.getHeader("Authorization") =~ /^Bearer (.*)$/ 77 | if(header){ 78 | def token = header[0][1] 79 | log.debug("Issue a cache req for |$token|" ) 80 | def status = tokenCache.cache.get(token, { lookup(token) }) 81 | log.debug("remapping: $status") 82 | 83 | // Token introspection param names have changed; support old + new 84 | /* def mappedStatus = [ 85 | active: status.active ?: status.valid, 86 | exp: status.exp ?: status.expires_at, 87 | sub: status.sub ?: status.subject, 88 | client_id: status.client_id, 89 | scope: status.scope 90 | ]*/ 91 | 92 | // status = mappedStatus 93 | log.debug("Status: $status") 94 | 95 | if (!status.active) return null; 96 | Date exp = org.joda.time.format.ISODateTimeFormat.dateTimeParser() 97 | .parseDateTime(status.exp).toDate() 98 | 99 | if (status.scope.class == String) status.scope = status.scope.split("\\s+") as List 100 | 101 | // TODO: make *.read produce read-only permissions 102 | def ret = new Authorization( 103 | isAdmin: "fhir_complete" in status.scope || "user/*.*" in status.scope || "user/*.read" in status.scope, 104 | isActive:status.active, 105 | expiration: exp, 106 | username: status.sub, 107 | app: status.client_id) 108 | 109 | println "is admin ${ret.isAdmin} because ${status.scope} + ${status.scope.class}" 110 | 111 | ret.scopes = status.scope 112 | 113 | ret.compartments = status.scope.collect { 114 | def m = (it =~ /(summary|search):(.*)/) 115 | if (!m.matches()) return null 116 | return "Patient/" + (m[0][2] ?: "example") 117 | }.findAll { it != null} 118 | 119 | if (status.patient) { 120 | ret.compartments += "Patient/"+status.patient 121 | } 122 | 123 | log.debug("Found bearer authorization for this request: $ret") 124 | return ret 125 | } 126 | return null 127 | } 128 | 129 | 130 | public class Authorization { 131 | private boolean isAdmin 132 | boolean isActive 133 | Date expiration 134 | String username 135 | String app 136 | List scopes = [] 137 | List compartments = [] 138 | 139 | boolean allows(p) { 140 | // String operation, Class resource, List compartmentsToCheck 141 | if (isAdmin) return true 142 | if (!isActive) return false 143 | 144 | p.compartments.every{ String c -> c in compartments } 145 | } 146 | 147 | boolean hasScope(String s){ 148 | return isAdmin || scopes.contains(s); 149 | } 150 | 151 | void assertScope(String s) { 152 | if (isAdmin) return 153 | if (!hasScope(s)) 154 | throw new AuthorizationException("Unauthorized: Need scope $s but you only have " + scopes) 155 | } 156 | 157 | void assertAccessAny(p) { 158 | if (isAdmin || p.compartments.empty) return 159 | println(" P's compartments" + p.compartments.properties) 160 | 161 | if (!(p.compartments as List).any {it in compartments}) 162 | throw new AuthorizationException("Unauthorized: Need any one of ${p.compartments} but you only have " + compartments) 163 | } 164 | 165 | void assertAccessEvery(p) { 166 | if (isAdmin || p.compartments.empty) return 167 | 168 | if (!(p.compartments as List).every {it in compartments}) 169 | throw new AuthorizationException("Unauthorized: you only have access to " + compartments + "not $p") 170 | } 171 | 172 | 173 | String getCompartmentsSql() { 174 | return "'{"+compartments.join(",")+"}'" 175 | } 176 | 177 | def restrictSearch(clauses) { 178 | if (isAdmin) return clauses 179 | def extra = new BasicDBObject([compartments: [$in: compartments]]) 180 | return SearchParamHandler.andList([clauses, extra]) 181 | } 182 | 183 | boolean getAccessIsRestricted() { 184 | return !isAdmin 185 | } 186 | 187 | } 188 | 189 | def evaluate(request){ 190 | println("Evaluating request $request with " + request.params) 191 | // If auth is disabled, treat everyone as an admin 192 | if (!grailsApplication.config.fhir.oauth.enabled) { 193 | request.authorization = new Authorization(isAdmin: true) 194 | //request.authorization = new Authorization(isAdmin: false) 195 | //request.authorization.compartments = ['Patient/123'] 196 | return true 197 | } 198 | 199 | println "HEADER: "+ request.getHeader("Authorization") 200 | 201 | 202 | def basicAuthAccess = asBasicAuth(request) 203 | if (basicAuthAccess) { 204 | request.authorization = basicAuthAccess 205 | return true 206 | } 207 | 208 | def bearerAuthAccess = asBearerAuth(request) 209 | if (bearerAuthAccess) { 210 | request.authorization = bearerAuthAccess 211 | return true 212 | } 213 | 214 | return false 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /grails-app/services/fhir/BundleService.groovy: -------------------------------------------------------------------------------- 1 | package fhir; 2 | 3 | import groovyx.net.http.URIBuilder 4 | 5 | import java.util.regex.Pattern 6 | 7 | import org.bson.types.ObjectId 8 | import org.hl7.fhir.instance.model.Bundle 9 | import org.hl7.fhir.instance.model.Bundle.BundleEntryComponent; 10 | import org.hl7.fhir.instance.model.DomainResource 11 | import org.hl7.fhir.instance.model.Resource 12 | import org.hl7.fhir.instance.model.DateTimeType 13 | import org.hl7.fhir.instance.model.Bundle.BundleType; 14 | import org.hl7.fhir.instance.model.Bundle.BundleTypeEnumFactory; 15 | import org.hl7.fhir.instance.model.Bundle.HTTPVerb; 16 | import org.hl7.fhir.instance.model.Bundle 17 | import org.hl7.fhir.instance.model.Bundle.BundleEntrySearchComponent; 18 | import org.hl7.fhir.instance.model.Bundle.SearchEntryMode; 19 | 20 | import org.apache.commons.lang3.time.FastDateFormat; 21 | 22 | class BundleValidationException extends Exception{ 23 | } 24 | 25 | class BundleService{ 26 | 27 | def transactional = false 28 | def searchIndexService 29 | def urlService 30 | 31 | void validateFeed(Bundle feed) { 32 | if (feed == null) { 33 | throw new BundleValidationException('Could not parse a bundle. Ensure you have set an appropriate content-type.') 34 | } 35 | 36 | if (feed.entry == null || feed.entry.size() == 0) { 37 | throw new BundleValidationException('Did not find any resources in the posted bundle.') 38 | return 39 | } 40 | } 41 | 42 | String getResourceName(Resource r) { 43 | r.class.toString().split('\\.')[-1] 44 | } 45 | 46 | Bundle createFeed(p) { 47 | def feedId = p.feedId 48 | def entries = p.entries 49 | def paging = p.paging 50 | 51 | Bundle feed = new Bundle() 52 | 53 | feed.total = paging.total 54 | feed.type = p.type ?: BundleType.SEARCHSET 55 | 56 | feed.addLink().setRelation("self").setUrl(feedId) 57 | if (paging._skip + paging._count < paging.total) { 58 | def nextPageUrl = nextPageFor(feedId, paging) 59 | feed.addLink().setRelation("next").setUrl(nextPageUrl) 60 | } 61 | 62 | feed.entry.addAll entries 63 | feed 64 | } 65 | 66 | String nextPageFor(String url, PagingCommand paging) { 67 | url = url.replaceAll(Pattern.quote("|"), "%7C") 68 | URIBuilder u = new URIBuilder(url) 69 | 70 | if ('_count' in u.query) { 71 | u.removeQueryParam("_count") 72 | } 73 | u.addQueryParam("_count", paging._count) 74 | 75 | if ('_skip' in u.query) { 76 | u.removeQueryParam("_skip") 77 | } 78 | u.addQueryParam("_skip", paging._skip + paging._count) 79 | 80 | return u.toString() 81 | } 82 | 83 | Bundle assignURIs(Bundle f) { 84 | 85 | Map assignments = [:] 86 | 87 | // Determine any entry IDs that 88 | // need reassignment. That means: 89 | // 1. IDs that are URNs but not URLs (assign a new ID) 90 | // 2. IDs that are absolute links to resources on this server (convert to relative) 91 | // For IDs that already exist in our system, rewrite them to ensure they always 92 | // appear as relative URIs (relative to [service-base] 93 | 94 | f.entry.each { BundleEntryComponent e -> 95 | 96 | boolean needsAssignment = false 97 | Resource res = e.resource 98 | Class c = res.class 99 | String resourceType = res.resourceType.path 100 | 101 | if (e.request.method == HTTPVerb.POST) { 102 | String oldid = e.resource.id 103 | e.resource.id = new ObjectId().toString() 104 | 105 | if (oldid != null) { 106 | assignments[urlService.relativeResourceLink(resourceType, oldid)] = 107 | urlService.relativeResourceLink(resourceType, e.resource.id) 108 | } 109 | } 110 | 111 | } 112 | 113 | def xml = f.encodeAsFhirXml() 114 | assignments.each {from, to -> 115 | log.debug("Replacing: $from -> $to") 116 | xml = xml.replaceAll(Pattern.quote("value=\"$from\""), to) 117 | } 118 | 119 | return xml.decodeFhirXml() 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /grails-app/services/fhir/ConformanceService.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | import java.util.Map; 3 | import java.util.regex.Matcher 4 | 5 | import javax.annotation.PostConstruct; 6 | 7 | import org.apache.commons.io.IOUtils 8 | import org.codehaus.groovy.grails.commons.GrailsApplication 9 | import org.hl7.fhir.instance.formats.XmlParser 10 | import org.hl7.fhir.instance.model.Bundle 11 | import org.hl7.fhir.instance.model.Bundle.BundleEntryComponent; 12 | import org.hl7.fhir.instance.model.CodeableConcept 13 | import org.hl7.fhir.instance.model.Coding 14 | import org.hl7.fhir.instance.model.Conformance 15 | import org.hl7.fhir.instance.model.DateTimeType 16 | import org.hl7.fhir.instance.model.Extension 17 | import org.hl7.fhir.instance.model.Resource 18 | import org.hl7.fhir.instance.model.SearchParameter; 19 | import org.hl7.fhir.instance.model.UriType 20 | import org.hl7.fhir.instance.model.Conformance.ConformanceRestComponent 21 | import org.hl7.fhir.instance.model.Conformance.ConformanceRestOperationComponent 22 | import org.hl7.fhir.instance.model.Conformance.ConformanceRestResourceComponent 23 | import org.hl7.fhir.instance.model.Conformance.ConformanceRestSecurityComponent 24 | import org.hl7.fhir.instance.model.Conformance.ResourceInteractionComponent; 25 | import org.hl7.fhir.instance.model.Conformance.SystemInteractionComponent; 26 | import org.hl7.fhir.instance.model.Conformance.SystemRestfulInteraction 27 | import org.hl7.fhir.instance.model.Conformance.TypeRestfulInteraction 28 | import org.hl7.fhir.utilities.xhtml.NodeType 29 | import org.hl7.fhir.utilities.xhtml.XhtmlNode 30 | 31 | import com.google.common.collect.ImmutableMap 32 | 33 | class ConformanceService { 34 | 35 | def grailsApplication 36 | static XmlService xmlService 37 | //static GrailsApplication grailsApplication 38 | static Conformance conformance 39 | static Map searchParamXpaths 40 | static Map> searchParamReferenceTypes 41 | static XmlParser fhirXml = new XmlParser() 42 | static UrlService urlService 43 | Map oauth 44 | 45 | @PostConstruct 46 | void init() { 47 | oauth = grailsApplication.config.fhir.oauth 48 | } 49 | 50 | public static ClassLoader getClassLoader(){ 51 | Thread.currentThread().contextClassLoader 52 | } 53 | 54 | private Resource resourceFromFile(String file) { 55 | def stream = classLoader.getResourceAsStream(file) 56 | fhirXml.parse(stream) 57 | } 58 | 59 | def xpathToFhirPath(String xp){ 60 | return xp.replace("/f:",".")[2..-1] 61 | } 62 | 63 | def generateConformance(){ 64 | def xpathFixes = ImmutableMap. builder() 65 | def xpathReferenceTypes = ImmutableMap.> builder() 66 | 67 | Map spotFixes = grailsApplication.config.fhir.searchParam.spotFixes 68 | spotFixes.each { uri, xpath -> 69 | xpathFixes.put(uri, xpath) 70 | } 71 | 72 | conformance = resourceFromFile "resources/conformance-base.xml" 73 | 74 | conformance.text.div = new XhtmlNode(NodeType.Element, "div"); 75 | conformance.text.div.addText("Generated Conformance Statement -- see structured representation.") 76 | conformance.url = urlService.fhirBase + '/conformance' 77 | conformance.publisher = "SMART on FHIR" 78 | conformance.name = "SMART on FHIR Conformance Statement" 79 | conformance.description = "Describes capabilities of this SMART on FHIR server" 80 | 81 | conformance.setDate(new Date()) 82 | 83 | if (oauth.enabled) { 84 | Extension smartAuthExtension = new Extension() 85 | smartAuthExtension.setUrl( "http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris") 86 | 87 | Extension registerUriExtension = new Extension() 88 | registerUriExtension.setUrl( "register") 89 | UriType registerUri = new UriType() 90 | registerUri.setValue(oauth.registerUri) 91 | registerUriExtension.setValue(registerUri) 92 | 93 | Extension authorizeUriExtension = new Extension() 94 | authorizeUriExtension.setUrl("authorize") 95 | UriType authorizeUri = new UriType() 96 | authorizeUri.setValue(oauth.authorizeUri) 97 | authorizeUriExtension.setValue(authorizeUri) 98 | 99 | Extension tokenUriExtension = new Extension() 100 | tokenUriExtension.setUrl("token") 101 | UriType tokenUri = new UriType(); 102 | tokenUri.setValue(oauth.tokenUri) 103 | tokenUriExtension.setValue(tokenUri) 104 | 105 | CodeableConcept newService = new CodeableConcept() 106 | Coding newCoding = new Coding() 107 | newCoding.setSystem("http://hl7.org/fhir/restful-security-service") 108 | newCoding.setCode("SMART-on-FHIR") 109 | newService.getCoding().add(newCoding) 110 | newService.setText("OAuth2 using SMART-on-FHIR profile (see http://docs.smarthealthit.org)") 111 | 112 | ConformanceRestSecurityComponent newSecurity = new ConformanceRestSecurityComponent() 113 | newSecurity.setDescription("SMART on FHIR uses OAuth2 for authorization") 114 | newSecurity.getService().add(newService) 115 | 116 | smartAuthExtension.getExtension().add(registerUriExtension) 117 | smartAuthExtension.getExtension().add(authorizeUriExtension) 118 | smartAuthExtension.getExtension().add(tokenUriExtension) 119 | newSecurity.getExtension().add(smartAuthExtension) 120 | 121 | conformance.getRest().get(0).setSecurity(newSecurity) 122 | } 123 | 124 | List supportedOps = [ 125 | TypeRestfulInteraction.READ, 126 | TypeRestfulInteraction.VREAD, 127 | TypeRestfulInteraction.UPDATE, 128 | TypeRestfulInteraction.SEARCHTYPE, 129 | TypeRestfulInteraction.CREATE, 130 | TypeRestfulInteraction.HISTORYTYPE, 131 | TypeRestfulInteraction.HISTORYINSTANCE, 132 | SystemRestfulInteraction.TRANSACTION, 133 | SystemRestfulInteraction.HISTORYSYSTEM 134 | ] 135 | 136 | 137 | // TODO get search param names lined up so we can index correctly 138 | // 1. From conformance look at each search param URL 139 | // 2. Transform to lower-case, strip "#", add dashes 140 | 141 | // If we don't have a spot fix already loaded for this searchParam 142 | // then use the xpath we discovered in the default bundle of SearchParameteres 143 | 144 | Bundle allSearchParams = resourceFromFile("resources/search-parameters.xml") 145 | allSearchParams.entry.each { BundleEntryComponent be -> 146 | SearchParameter sp = be.resource 147 | String key = sp.base + ':' + sp.name 148 | String xpath = sp.xpath 149 | println "eval param ${key} ${xpath}" 150 | 151 | if (xpath) { 152 | def types = sp.target.collect {t -> t.value} 153 | //println "Types for ${key}: ${types}" 154 | if (types) { 155 | xpathReferenceTypes.put(key, types); 156 | } else { 157 | println "but not ref types" 158 | 159 | } 160 | } 161 | if (xpath && !spotFixes[key]) xpathFixes.put(key, xpath) 162 | } 163 | 164 | 165 | conformance.rest.each { ConformanceRestComponent r -> 166 | 167 | r.interaction = r.interaction.findAll { SystemInteractionComponent o -> 168 | o.hasCode() && o.codeElement in supportedOps 169 | } 170 | r.resource.each { ConformanceRestResourceComponent rc -> 171 | 172 | rc.interaction = rc.interaction.findAll { ResourceInteractionComponent o -> 173 | o.code in supportedOps 174 | } 175 | } 176 | } 177 | 178 | searchParamXpaths = xpathFixes.build() 179 | println "Got som xpaths with ${searchParamXpaths['Condition:patient']}" 180 | searchParamReferenceTypes = xpathReferenceTypes.build() 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /grails-app/services/fhir/SqlService.groovy: -------------------------------------------------------------------------------- 1 | package fhir; 2 | 3 | 4 | import org.apache.commons.io.IOUtils; 5 | import org.hl7.fhir.instance.model.Patient 6 | import org.hl7.fhir.instance.model.DomainResource 7 | import org.hl7.fhir.instance.model.Resource 8 | import org.hl7.fhir.instance.model.DocumentReference 9 | 10 | import fhir.searchParam.IndexedValue 11 | import fhir.AuthorizationService.Authorization 12 | import groovy.sql.GroovyRowResult 13 | import groovy.sql.Sql 14 | 15 | import com.google.gson.JsonObject 16 | import com.google.gson.JsonParser 17 | 18 | class SqlService{ 19 | 20 | def dataSource 21 | UrlService urlService 22 | AuthorizationService authorizationService 23 | SearchIndexService searchIndexService 24 | 25 | JsonParser jsonParser= new JsonParser() 26 | 27 | Sql getSql() { 28 | new Sql(dataSource) 29 | } 30 | 31 | def generator = { String alphabet, int n -> 32 | new Random().with { 33 | (1..n).collect { alphabet[ nextInt( alphabet.length() ) ] }.join() 34 | } 35 | } 36 | 37 | List rows(String q, Map params) { 38 | def label = generator( (('A'..'Z')+('0'..'9')).join(), 9 ) 39 | 40 | //println("Params $params " + params.size()) 41 | if (params.size() == 0) { 42 | return sql.rows(q) 43 | } 44 | def ret = sql.rows(q, params) 45 | println label + " q: " + q 46 | println label + " p: " + params 47 | println label + " s: " + ret.size() 48 | return ret 49 | } 50 | 51 | 52 | ResourceVersion getLatestByFhirId(String fhir_type, fhir_id) { 53 | return ResourceVersion.find("from ResourceVersion as v where v.fhir_type=? and v.fhir_id=? order by v.version_id desc", [fhir_type, fhir_id]) 54 | } 55 | 56 | ResourceVersion getFhirVersion(String fhir_type, String fhir_id, Long version_id) { 57 | return ResourceVersion.find("from ResourceVersion as v where v.fhir_type=? and v.fhir_id=? and v.version_id=? order by v.version_id desc", [fhir_type, fhir_id, version_id]) 58 | } 59 | 60 | Resource getLatestSummary(Authorization a) { 61 | 62 | def q = """select min(content) as content from resource_version where (fhir_type, fhir_id) in ( 63 | select fhir_type, fhir_id from resource_compartment where fhir_type='DocumentReference' 64 | and compartments && """ +a.compartmentsSql+""" 65 | ) group by fhir_type, fhir_id order by max(version_id) desc limit 1""" 66 | def content = rows(q, [:]) 67 | if (!content) return null 68 | 69 | DocumentReference r = content[0].content.decodeFhirJson() 70 | Map location = urlService.fhirUrlParts(r.locationSimple) 71 | return getLatestByFhirId(location.type, location.id).content.decodeFhirJson() 72 | } 73 | 74 | void insertIndexTerms(List inserts, versionId, String fhirType, String fhirId, Resource r) { 75 | def indexTerms = searchIndexService.indexResource(r); 76 | 77 | indexTerms.collect { IndexedValue val -> 78 | val.handler.createIndex(val, versionId, fhirId, fhirType) 79 | }.each { ResourceIndexTerm term -> 80 | inserts.add(term.insertStatement(versionId)) 81 | } 82 | 83 | if (r instanceof DomainResource) { 84 | r.contained.each { Resource it -> 85 | insertIndexTerms(inserts, 0, it.class.name.split("\\.")[-1], fhirId+"_contained_"+it.id, it) 86 | } 87 | } 88 | return 89 | } 90 | 91 | /** 92 | * @param r Resource or ResourceVersion to inspect for compartments 93 | * @param fhirId ID of the Resource (only required if it's a Patient resource) 94 | * @param compartments List of compartments that must to check for authorization 95 | * @return List of all compartments needed 96 | * @throws AuthorizatinException if user doesn't have access to _all_ compartments 97 | */ 98 | private List compartmentsForResource(r, String fhirId) { 99 | def compartments = [] 100 | 101 | log.debug("Authorizing compartments start from: $compartments") 102 | 103 | if (r instanceof Patient) { 104 | compartments.add("Patient/$fhirId") 105 | } else if ("subject" in r.properties) { 106 | compartments.add(r.subject.reference) 107 | } else if ("patient" in r.properties) { 108 | compartments.add(r.patient.reference) 109 | } 110 | 111 | return compartments 112 | } 113 | 114 | private def deleteResource(ResourceVersion h, authorization) { 115 | authorization.assertAccessAny(operation:'DELETE', compartments:h.compartments) 116 | 117 | String fhirType = h.fhir_type 118 | 119 | Map dParams = [ 120 | fhir_id: h.fhir_id, 121 | fhir_type: h.fhir_type, 122 | rest_operation: 'DELETE', 123 | content: 'deleted' 124 | ] 125 | log.debug("Deleting $dParams") 126 | ResourceVersion deleteEntry = new ResourceVersion(dParams) 127 | deleteEntry.save(failOnError: true) 128 | 129 | log.debug("Deleted $deleteEntry") 130 | sql.execute("delete from resource_index_term where fhir_type=:type and fhir_id=:id", [ 131 | type: h.fhir_type, 132 | id: h.fhir_id 133 | ]) 134 | 135 | log.debug("Deleted resource: " + h.fhir_id) 136 | } 137 | 138 | 139 | public def updateResource(Resource r, String resourceName, String fhirId, List needCompartments, authorization) { 140 | 141 | List compartments = needCompartments + compartmentsForResource(r, fhirId) 142 | authorization.assertAccessEvery(compartments: compartments) 143 | 144 | def h = new ResourceVersion( 145 | fhir_id: fhirId, 146 | fhir_type: resourceName, 147 | rest_operation: 'POST', 148 | content: "placeholder") 149 | h.save(failOnError: true) 150 | 151 | r.meta.setVersionId(h.version_id.toString()) 152 | r.meta.setLastUpdated(new Date()) 153 | 154 | JsonObject rjson = jsonParser.parse(r.encodeAsFhirJson()) 155 | 156 | log.debug("Updating to version id: ${h.fhir_id} /_history/ " + h.version_id) 157 | log.debug("raw " + rjson) 158 | log.debug("Parsed a $rjson.resourceType.asString") 159 | 160 | String fhirType = rjson.resourceType.asString 161 | String expectedType = resourceName 162 | 163 | if (fhirType != expectedType){ 164 | log.debug("Got a request whose type didn't match: $expectedType vs. $fhirType") 165 | throw new Exception("Can't post a $fhirType to the $expectedType endpoint"); 166 | } 167 | 168 | if (fhirId != r.id){ 169 | log.debug("Got a resource to create ids type didn't match: $r.id vs. $fhirId") 170 | throw new Exception("Can't post a $fhirType with id $fhirId when content id is $r.id"); 171 | } 172 | 173 | h.content = rjson.toString() 174 | h.save(failOnError: true) 175 | 176 | String versionUrl 177 | 178 | def inserts = [] 179 | 180 | // remove indexing from contained resources 181 | inserts.add ("""delete from resource_index_term where (fhir_type, fhir_id) in 182 | (select reference_type, reference_id from resource_index_term where 183 | fhir_type='$fhirType' and fhir_id='$fhirId' and reference_id like '%_contained_%');""") 184 | 185 | // remove indexing from the resoure 186 | inserts.add ("delete from resource_index_term where fhir_type=$fhirType and fhir_id=$fhirId;" ) 187 | 188 | inserts.add ("delete from resource_compartment where fhir_type= $fhirType and fhir_id= $fhirId;" ) 189 | inserts.add("insert into resource_compartment (fhir_type, fhir_id, compartments) values ($fhirType, $fhirId, '{" +compartments.join(",")+"}');") 190 | insertIndexTerms(inserts, h.version_id, fhirType, fhirId, r) 191 | 192 | inserts.each {println it; sql.execute(it) } 193 | 194 | versionUrl = urlService.resourceVersionLink(resourceName, fhirId, h.version_id) 195 | log.debug("Created version: " + versionUrl) 196 | return versionUrl 197 | 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /grails-app/services/fhir/UrlService.groovy: -------------------------------------------------------------------------------- 1 | package fhir; 2 | 3 | 4 | 5 | class UrlService{ 6 | def transactional = false 7 | def grailsLinkGenerator 8 | def regex = /([^\/]+)\/([^\/]+)(?:\/_history\/([^\/]+))?/ 9 | 10 | String fhirCombinedId(String p) { 11 | String ret = null 12 | Map parts = fhirUrlParts(p) 13 | if (parts.size() == 0) return ret 14 | return "${parts.type}/${parts.id}" 15 | } 16 | 17 | Map fhirUrlParts(String p) { 18 | Map ret = [:] 19 | def m = (p =~ regex) 20 | if (m.size()) { 21 | ret['type'] = m[0][1] 22 | ret['id'] = m[0][2] 23 | if (m.size() > 2) { 24 | ret['version'] = m[0][3] 25 | } else { 26 | ret['version'] = null 27 | } 28 | } 29 | return ret 30 | } 31 | 32 | String getBaseURI() { 33 | grailsLinkGenerator.link(uri:'', absolute:true) 34 | } 35 | 36 | String getDomain() { 37 | def m = baseURI =~ /(https?:\/\/[^\/]+)/ 38 | return m[0][1] 39 | } 40 | 41 | String getFhirBase() { 42 | baseURI 43 | } 44 | 45 | URL getFhirBaseAbsolute() { 46 | new URL(fhirBase + '/') 47 | } 48 | 49 | String relativeResourceLink(String resource, String id) { 50 | "$resource/$id" 51 | } 52 | 53 | String resourceLink(String resourceName, String fhirId) { 54 | grailsLinkGenerator.link( 55 | mapping: 'resourceInstance', 56 | absolute: true, 57 | params: [ 58 | resource: resourceName, 59 | id: fhirId 60 | ]) 61 | } 62 | 63 | String resourceVersionLink(String resourceName, String fhirId, vid) { 64 | grailsLinkGenerator.link( 65 | mapping: 'resourceVersion', 66 | absolute: true, 67 | params: [ 68 | resource: resourceName, 69 | id: fhirId, 70 | vid:vid.toString() 71 | ]) 72 | } 73 | 74 | String fullRequestUrl(request) { 75 | return domain + request.forwardURI + '?' + (request.queryString ?: "") 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /grails-app/services/fhir/XmlService.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | import javax.xml.parsers.DocumentBuilder 4 | import javax.xml.parsers.DocumentBuilderFactory 5 | import javax.xml.xpath.XPath 6 | import javax.xml.xpath.XPathFactory 7 | 8 | import org.codehaus.groovy.grails.commons.GrailsApplication 9 | import org.hl7.fhir.instance.model.Resource 10 | import org.springframework.util.xml.SimpleNamespaceContext 11 | import org.xml.sax.InputSource 12 | 13 | 14 | class XmlService { 15 | 16 | static def transactional = false 17 | static def lazyInit = false 18 | 19 | static SimpleNamespaceContext nsContext 20 | GrailsApplication grailsApplication 21 | XPath xpathEvaluator = XPathFactory.newInstance().newXPath() 22 | 23 | private configureXpathSettings() { 24 | nsContext = new SimpleNamespaceContext(); 25 | 26 | grailsApplication.config.fhir.namespaces.each { prefix, uri -> 27 | nsContext.bindNamespaceUri(prefix, uri) 28 | } 29 | xpathEvaluator.setNamespaceContext(nsContext) 30 | } 31 | 32 | public org.w3c.dom.Document fromResource(Resource r) throws IOException, Exception { 33 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 34 | factory.setNamespaceAware(true); 35 | DocumentBuilder builder = factory.newDocumentBuilder(); 36 | org.w3c.dom.Document d = builder.parse(new InputSource(new StringReader(r.encodeAsFhirXml()))); 37 | return d; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /grails-app/utils/fhir/DbObjectCodec.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | import com.mongodb.DBObject 3 | import com.mongodb.util.JSON 4 | 5 | class DbObjectCodec { 6 | static decode = { str -> 7 | return (DBObject) JSON.parse(str); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /grails-app/utils/fhir/FhirJsonCodec.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | import org.apache.commons.io.IOUtils 3 | import org.hl7.fhir.instance.formats.JsonParser 4 | import org.hl7.fhir.instance.formats.IParser 5 | 6 | class FhirJsonCodec { 7 | 8 | 9 | static decode = { str -> 10 | JsonParser jp= new JsonParser() 11 | jp.outputStyle = IParser.OutputStyle.PRETTY 12 | jp.parse(IOUtils.toInputStream(str)); 13 | } 14 | static encode = { resource -> 15 | JsonParser jp= new JsonParser() 16 | jp.outputStyle = IParser.OutputStyle.PRETTY 17 | ByteArrayOutputStream jsonStream = new ByteArrayOutputStream() 18 | jp.compose(jsonStream,resource) 19 | jsonStream.toString() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /grails-app/utils/fhir/FhirXmlCodec.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | import org.apache.commons.io.IOUtils 3 | import org.hl7.fhir.instance.formats.IParser; 4 | import org.hl7.fhir.instance.formats.XmlParser 5 | 6 | class FhirXmlCodec { 7 | 8 | 9 | static decode = { str -> 10 | XmlParser jp= new XmlParser() 11 | jp.outputStyle = IParser.OutputStyle.PRETTY 12 | jp.parse(IOUtils.toInputStream(str)); 13 | } 14 | 15 | static encode = { resource -> 16 | XmlParser jp= new XmlParser() 17 | jp.outputStyle = IParser.OutputStyle.PRETTY 18 | ByteArrayOutputStream xmlStream = new ByteArrayOutputStream() 19 | jp.compose(xmlStream,resource) 20 | xmlStream.toString() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /grails-app/utils/fhir/RequestFilters.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | class RequestFilters { 4 | def authorizationService 5 | def grailsApplication 6 | 7 | def filters = { 8 | 9 | authorizeRequest(controllerExclude:'error', action: '*') { 10 | before = { 11 | 12 | if(params.action[request.method] in ['welcome', 'conformance']){ 13 | return true 14 | } 15 | 16 | if (!authorizationService.evaluate(request)) { 17 | forward controller: 'error', action: 'status401' 18 | return false 19 | } 20 | request.t0 = new Date().getTime() 21 | return true 22 | } 23 | } 24 | 25 | parseResourceBody(controllerExclude: 'error', action: '*') { 26 | before = { 27 | 28 | def providingFormat = request.getHeaders('content-type')*.toLowerCase() + params._format 29 | def acceptableFormat = request.getHeaders('accept')*.toLowerCase() + params._format 30 | 31 | if (acceptableFormat.any {it =~ /json/}) { 32 | request.acceptableFormat = "json" 33 | } else { 34 | request.acceptableFormat = "xml" 35 | } 36 | 37 | if (providingFormat.any {it =~ /json/}) { 38 | request.providingFormat = "json" 39 | } else { 40 | request.providingFormat = "xml" 41 | } 42 | println("Formats: ${request.acceptableFormat} + ${request.providingFormat}") 43 | return true 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /grails-app/utils/fhir/ResponseFilters.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | import org.hibernate.SessionFactory 3 | import org.hl7.fhir.instance.model.Bundle; 4 | import org.hl7.fhir.instance.model.Binary 5 | import org.hl7.fhir.instance.model.Resource 6 | 7 | class ResponseFilters { 8 | SessionFactory sessionFactory 9 | 10 | def filters = { 11 | renderContent(controller: '*', action: '*') { 12 | after = { 13 | 14 | sessionFactory.currentSession.flush() 15 | sessionFactory.currentSession.clear() 16 | 17 | if (response.status == 204) return false 18 | 19 | def r = request?.resourceToRender 20 | if (!r) { 21 | return true 22 | } 23 | 24 | if (!(r instanceof Resource || r instanceof Bundle)) { 25 | log.debug("Got a " + r) 26 | r = r.content.toString().decodeFhirJson() 27 | } 28 | 29 | if (r.class == Binary && !params.noraw) { 30 | response.contentType = r.contentType 31 | response.outputStream << r.content 32 | response.outputStream.flush() 33 | return false 34 | } 35 | 36 | if (request.acceptableFormat == "json") 37 | render(text: r.encodeAsFhirJson(), contentType:"application/json+fhir") 38 | else 39 | render(text: r.encodeAsFhirXml(), contentType:"application/xml+fhir") 40 | 41 | if (request?.t0) 42 | log.debug("rendered after: " + (new Date().getTime() - request.t0)) 43 | 44 | return false 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /grails-app/views/error.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <g:if env="development">Grails Runtime Exception</g:if><g:else>Error</g:else> 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
    17 |
  • An error has occurred
  • 18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /grails-app/views/index.gsp: -------------------------------------------------------------------------------- 1 | 2 | <% 3 | def urlService = grailsApplication.mainContext.getBean("urlService"); 4 | %> 5 | 6 | 7 | 8 | SMART on FHIR 9 | 11 | 14 | 54 | 55 | 56 |

SMART on FHIR

57 |

58 | Service URL: 59 | ${urlService.fhirBase} 60 |
Source: GitHub
62 |

63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /grails-app/views/layouts/main.gsp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/grails-app/views/layouts/main.gsp -------------------------------------------------------------------------------- /grailsWrapper/grails-wrapper-runtime-2.4.4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/grailsWrapper/grails-wrapper-runtime-2.4.4.jar -------------------------------------------------------------------------------- /grailsWrapper/grails-wrapper.properties: -------------------------------------------------------------------------------- 1 | wrapper.dist.url=http://dist.springframework.org.s3.amazonaws.com/release/GRAILS/ 2 | -------------------------------------------------------------------------------- /grailsWrapper/springloaded-1.2.1.RELEASE.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/grailsWrapper/springloaded-1.2.1.RELEASE.jar -------------------------------------------------------------------------------- /grailsw.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem ## 4 | @rem Grails JVM Bootstrap for Windows ## 5 | @rem ## 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set CLASS=org.grails.wrapper.GrailsWrapper 12 | 13 | if exist "%USERPROFILE%/.groovy/preinit.bat" call "%USERPROFILE%/.groovy/preinit.bat" 14 | 15 | @rem Determine the command interpreter to execute the "CD" later 16 | set COMMAND_COM="cmd.exe" 17 | if exist "%SystemRoot%\system32\cmd.exe" set COMMAND_COM="%SystemRoot%\system32\cmd.exe" 18 | if exist "%SystemRoot%\command.com" set COMMAND_COM="%SystemRoot%\command.com" 19 | 20 | @rem Use explicit find.exe to prevent cygwin and others find.exe from being used 21 | set FIND_EXE="find.exe" 22 | if exist "%SystemRoot%\system32\find.exe" set FIND_EXE="%SystemRoot%\system32\find.exe" 23 | if exist "%SystemRoot%\command\find.exe" set FIND_EXE="%SystemRoot%\command\find.exe" 24 | 25 | :check_JAVA_HOME 26 | @rem Make sure we have a valid JAVA_HOME 27 | if not "%JAVA_HOME%" == "" goto have_JAVA_HOME 28 | 29 | echo. 30 | echo ERROR: Environment variable JAVA_HOME has not been set. 31 | echo. 32 | echo Please set the JAVA_HOME variable in your environment to match the 33 | echo location of your Java installation. 34 | echo. 35 | goto end 36 | 37 | :have_JAVA_HOME 38 | @rem Remove trailing slash from JAVA_HOME if found 39 | if "%JAVA_HOME:~-1%"=="\" SET JAVA_HOME=%JAVA_HOME:~0,-1% 40 | 41 | @rem Validate JAVA_HOME 42 | %COMMAND_COM% /C DIR "%JAVA_HOME%" 2>&1 | %FIND_EXE% /I /C "%JAVA_HOME%" >nul 43 | if not errorlevel 1 goto check_GRAILS_HOME 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | echo. 51 | goto end 52 | 53 | :check_GRAILS_HOME 54 | @rem Define GRAILS_HOME if not set 55 | if "%GRAILS_HOME%" == "" set GRAILS_HOME=%DIRNAME%.. 56 | 57 | @rem Remove trailing slash from GRAILS_HOME if found 58 | if "%GRAILS_HOME:~-1%"=="\" SET GRAILS_HOME=%GRAILS_HOME:~0,-1% 59 | 60 | :init 61 | 62 | for %%x in ("%USERPROFILE%") do set SHORTHOME=%%~fsx 63 | if "x%GRAILS_AGENT_CACHE_DIR%" == "x" set GRAILS_AGENT_CACHE_DIR=%SHORTHOME%/.grails/2.4.4/ 64 | set SPRINGLOADED_PARAMS="profile=grails;cacheDir=%GRAILS_AGENT_CACHE_DIR%" 65 | if not exist "%GRAILS_AGENT_CACHE_DIR%" mkdir "%GRAILS_AGENT_CACHE_DIR%" 66 | 67 | if "%GRAILS_NO_PERMGEN%" == "" ( 68 | type "%JAVA_HOME%\include\classfile_constants.h" 2>nul | findstr /R /C:"#define JVM_CLASSFILE_MAJOR_VERSION 5[23]" >nul 69 | if not errorlevel 1 set GRAILS_NO_PERMGEN=1 70 | ) 71 | 72 | set AGENT_STRING=-javaagent:grailsWrapper/springloaded-1.2.1.RELEASE.jar -Xverify:none -Dspringloaded.synchronize=true -Djdk.reflect.allowGetCallerClass=true -Dspringloaded=\"%SPRINGLOADED_PARAMS%\" 73 | set DISABLE_RELOADING= 74 | if "%GRAILS_OPTS%" == "" ( 75 | set GRAILS_OPTS=-server -Xmx768M -Xms64M -Dfile.encoding=UTF-8 76 | if not "%GRAILS_NO_PERMGEN%" == "1" ( 77 | set GRAILS_OPTS=-server -Xmx768M -Xms64M -XX:PermSize=32m -XX:MaxPermSize=256m -Dfile.encoding=UTF-8 78 | ) 79 | ) 80 | 81 | @rem Get command-line arguments, handling Windows variants 82 | if "%@eval[2+2]" == "4" goto 4NT_args 83 | 84 | @rem Slurp the command line arguments. 85 | set CMD_LINE_ARGS= 86 | set CP= 87 | set INTERACTIVE=true 88 | 89 | :win9xME_args_slurp 90 | if "x%~1" == "x" goto execute 91 | set CURR_ARG=%~1 92 | if "%CURR_ARG:~0,2%" == "-D" ( 93 | set CMD_LINE_ARGS=%CMD_LINE_ARGS% %~1=%~2 94 | shift 95 | shift 96 | goto win9xME_args_slurp 97 | ) 98 | if "x%~1" == "x-cp" ( 99 | set CP=%~2 100 | shift 101 | shift 102 | goto win9xME_args_slurp 103 | ) 104 | if "x%~1" == "x-debug" ( 105 | set JAVA_OPTS=%JAVA_OPTS% -Xdebug -Xnoagent -Dgrails.full.stacktrace=true -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005 106 | shift 107 | goto win9xME_args_slurp 108 | ) 109 | if "x%~1" == "x-classpath" ( 110 | set CP=%~2 111 | shift 112 | shift 113 | goto win9xME_args_slurp 114 | ) 115 | if "x%~1" == "x-reloading" ( 116 | set AGENT=%AGENT_STRING% 117 | shift 118 | goto win9xME_args_slurp 119 | ) 120 | if "x%~1" == "xrun-app" ( 121 | set AGENT=%AGENT_STRING% 122 | set INTERACTIVE= 123 | set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1 124 | shift 125 | goto win9xME_args_slurp 126 | ) 127 | if "x%~1" == "x-noreloading" ( 128 | set DISABLE_RELOADING=true 129 | shift 130 | goto win9xME_args_slurp 131 | ) 132 | set INTERACTIVE= 133 | set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1 134 | shift 135 | goto win9xME_args_slurp 136 | 137 | :4NT_args 138 | @rem Get arguments from the 4NT Shell from JP Software 139 | set CMD_LINE_ARGS=%$ 140 | 141 | :execute 142 | @rem Setup the command line 143 | set STARTER_CLASSPATH=grailsWrapper/grails-wrapper-runtime-2.4.4.jar;grailsWrapper;. 144 | 145 | if exist "%USERPROFILE%/.groovy/init.bat" call "%USERPROFILE%/.groovy/init.bat" 146 | 147 | @rem Setting a classpath using the -cp or -classpath option means not to use 148 | @rem the global classpath. Groovy behaves then the same as the java interpreter 149 | 150 | if "x" == "x%CLASSPATH%" goto after_classpath 151 | set CP=%CP%;%CLASSPATH% 152 | :after_classpath 153 | 154 | if "x%DISABLE_RELOADING%" == "xtrue" ( 155 | set AGENT= 156 | ) else ( 157 | if "x%INTERACTIVE%" == "xtrue" ( 158 | set AGENT=%AGENT_STRING% 159 | ) 160 | ) 161 | 162 | set STARTER_MAIN_CLASS=org.grails.wrapper.GrailsWrapper 163 | set STARTER_CONF=%GRAILS_HOME%\conf\groovy-starter.conf 164 | 165 | set JAVA_EXE=%JAVA_HOME%\bin\java.exe 166 | set TOOLS_JAR=%JAVA_HOME%\lib\tools.jar 167 | 168 | set JAVA_OPTS=%GRAILS_OPTS% %JAVA_OPTS% %AGENT% 169 | 170 | set JAVA_OPTS=%JAVA_OPTS% -Dprogram.name="%PROGNAME%" 171 | set JAVA_OPTS=%JAVA_OPTS% -Dgrails.home="%GRAILS_HOME%" 172 | set JAVA_OPTS=%JAVA_OPTS% -Dgrails.version=2.4.4 173 | set JAVA_OPTS=%JAVA_OPTS% -Dbase.dir=. 174 | set JAVA_OPTS=%JAVA_OPTS% -Dtools.jar="%TOOLS_JAR%" 175 | set JAVA_OPTS=%JAVA_OPTS% -Dgroovy.starter.conf="%STARTER_CONF%" 176 | 177 | if exist "%USERPROFILE%/.groovy/postinit.bat" call "%USERPROFILE%/.groovy/postinit.bat" 178 | 179 | @rem Execute Grails 180 | CALL "%JAVA_EXE%" %JAVA_OPTS% -classpath "%STARTER_CLASSPATH%" %STARTER_MAIN_CLASS% --main %CLASS% --conf "%STARTER_CONF%" --classpath "%CP%" "%CMD_LINE_ARGS%" 181 | :end 182 | @rem End local scope for the variables with windows NT shell 183 | if "%OS%"=="Windows_NT" endlocal 184 | 185 | @rem Optional pause the batch file 186 | if "%GROOVY_BATCH_PAUSE%" == "on" pause 187 | -------------------------------------------------------------------------------- /load-emerge-patients/.gradle/1.7/taskArtifacts/cache.properties: -------------------------------------------------------------------------------- 1 | #Fri Nov 15 18:31:43 EST 2013 2 | -------------------------------------------------------------------------------- /load-emerge-patients/.gradle/1.7/taskArtifacts/cache.properties.lock: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /load-emerge-patients/.gradle/1.7/taskArtifacts/fileHashes.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/load-emerge-patients/.gradle/1.7/taskArtifacts/fileHashes.bin -------------------------------------------------------------------------------- /load-emerge-patients/.gradle/1.7/taskArtifacts/fileSnapshots.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/load-emerge-patients/.gradle/1.7/taskArtifacts/fileSnapshots.bin -------------------------------------------------------------------------------- /load-emerge-patients/.gradle/1.7/taskArtifacts/outputFileStates.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/load-emerge-patients/.gradle/1.7/taskArtifacts/outputFileStates.bin -------------------------------------------------------------------------------- /load-emerge-patients/.gradle/1.7/taskArtifacts/taskArtifacts.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/load-emerge-patients/.gradle/1.7/taskArtifacts/taskArtifacts.bin -------------------------------------------------------------------------------- /load-emerge-patients/.gradle/1.9-rc-3/taskArtifacts/cache.properties: -------------------------------------------------------------------------------- 1 | #Fri Nov 15 19:38:56 EST 2013 2 | -------------------------------------------------------------------------------- /load-emerge-patients/.gradle/1.9-rc-3/taskArtifacts/fileHashes.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/load-emerge-patients/.gradle/1.9-rc-3/taskArtifacts/fileHashes.bin -------------------------------------------------------------------------------- /load-emerge-patients/.gradle/1.9-rc-3/taskArtifacts/fileSnapshots.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/load-emerge-patients/.gradle/1.9-rc-3/taskArtifacts/fileSnapshots.bin -------------------------------------------------------------------------------- /load-emerge-patients/.gradle/1.9-rc-3/taskArtifacts/outputFileStates.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/load-emerge-patients/.gradle/1.9-rc-3/taskArtifacts/outputFileStates.bin -------------------------------------------------------------------------------- /load-emerge-patients/.gradle/1.9-rc-3/taskArtifacts/taskArtifacts.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/load-emerge-patients/.gradle/1.9-rc-3/taskArtifacts/taskArtifacts.bin -------------------------------------------------------------------------------- /load-emerge-patients/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | apply plugin: 'application' 3 | 4 | repositories { 5 | mavenCentral() 6 | maven { 7 | url "https://oss.sonatype.org/content/repositories/snapshots/" 8 | } 9 | } 10 | 11 | dependencies { 12 | compile 'me.fhir:fhir-0.12:0.5-SNAPSHOT' 13 | compile 'org.codehaus.groovy:groovy-all:2.0.5' 14 | compile 'org.codehaus.groovy.modules.http-builder:http-builder:0.6' 15 | compile "joda-time:joda-time:2.2" 16 | } 17 | 18 | task loadPatients (dependsOn: 'classes', type: JavaExec) { 19 | main = 'LoadCCDA' 20 | classpath = sourceSets.main.runtimeClasspath 21 | environment.BASE_DIR = project.hasProperty('emergeDir') ? emergeDir : "sample_ccdas/EMERGE" 22 | environment.BASE_URL = project.hasProperty('fhirBase') ? fhirBase : "http://localhost:8080" 23 | environment.USERNAME = project.hasProperty('username') ? username : "client" 24 | environment.PASSWORD = project.hasProperty('password') ? password : "secret" 25 | } 26 | 27 | task wrapper(type: Wrapper) { 28 | gradleVersion = '1.9-rc-3' 29 | } 30 | -------------------------------------------------------------------------------- /load-emerge-patients/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/load-emerge-patients/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /load-emerge-patients/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Nov 15 19:34:22 EST 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=http\://services.gradle.org/distributions/gradle-1.9-rc-3-bin.zip 7 | -------------------------------------------------------------------------------- /load-emerge-patients/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /load-emerge-patients/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /load-emerge-patients/src/main/groovy/LoadCCDA.groovy: -------------------------------------------------------------------------------- 1 | import org.hl7.fhir.instance.model.Resource 2 | import org.hl7.fhir.instance.model.AtomEntry 3 | import org.hl7.fhir.instance.model.AtomFeed 4 | import org.hl7.fhir.instance.model.Binary 5 | import org.hl7.fhir.instance.model.Period 6 | import org.hl7.fhir.instance.model.CodeableConcept 7 | import org.hl7.fhir.instance.model.Coding 8 | import org.hl7.fhir.instance.model.DocumentReference 9 | import org.hl7.fhir.instance.model.DateAndTime 10 | import org.hl7.fhir.instance.model.Identifier 11 | import org.hl7.fhir.instance.model.ResourceReference 12 | import org.hl7.fhir.instance.formats.XmlParser 13 | import org.hl7.fhir.instance.formats.XmlComposer 14 | import org.apache.commons.io.FileUtils 15 | import org.apache.commons.io.IOUtils 16 | 17 | import org.xmlpull.v1.XmlPullParserFactory; 18 | import org.joda.time.format.ISODateTimeFormat 19 | import groovy.xml.MarkupBuilder 20 | import groovy.transform.Field 21 | 22 | import java.nio.file.Files 23 | import java.nio.file.Paths 24 | import java.nio.ByteBuffer 25 | import java.nio.charset.StandardCharsets 26 | 27 | import static groovyx.net.http.ContentType.* 28 | import static groovyx.net.http.Method.* 29 | 30 | import groovy.io.FileType 31 | 32 | 33 | import groovyx.net.http.HTTPBuilder 34 | 35 | import org.apache.http.auth.UsernamePasswordCredentials 36 | import org.apache.http.impl.auth.BasicScheme 37 | 38 | fhirBase = System.env.BASE_URL // e.g. "http://localhost:8001"; 39 | String filePath = System.env.BASE_DIR // e.g. '/home/jmandel/smart/sample_ccdas/EMERGE'; 40 | username = System.env.USERNAME 41 | password = System.env.PASSWORD 42 | 43 | 44 | fhir = [:] 45 | fhir.namespaces = [ f: "http://hl7.org/fhir", xhtml: "http://www.w3.org/1999/xhtml" ]; 46 | 47 | 48 | AtomFeed feed = new AtomFeed(); 49 | def allFiles = [] 50 | new File(filePath).eachFileRecurse (FileType.FILES) { file -> 51 | allFiles << file 52 | } 53 | 54 | allFiles.each { 55 | def m = it.path.toString() =~ /.*Patient-(.*)\.xml/; 56 | if (!m.matches()) return; 57 | processOneFile(it, m[0][1]); 58 | } 59 | 60 | Resource makePatient(){ 61 | def patientWriter = new StringWriter(); 62 | def p = new MarkupBuilder(patientWriter); 63 | p.Patient('xmlns':fhir.namespaces.f, 64 | 'xmlns:xhtml':fhir.namespaces.xhtml) { 65 | text() { 66 | status(value:'generated'); 67 | 'xhtml:div'("A BB+ FHIR Sample Patient -- see DocumentReference elements for data."); 68 | } 69 | }; 70 | return toResource(patientWriter.toString()); 71 | } 72 | 73 | String toXml(def resource){ 74 | ByteArrayOutputStream xmlStream = new ByteArrayOutputStream() 75 | new XmlComposer().compose(xmlStream, resource, true) 76 | xmlStream.toString() 77 | } 78 | 79 | Resource toResource(String str){ 80 | def ret = new XmlParser().parseGeneral(IOUtils.toInputStream(str)); 81 | return ret.resource ?: ret.feed 82 | } 83 | 84 | 85 | def processOneFile(File file, String pid) { 86 | byte[] bytes = Files.readAllBytes(Paths.get(file.path)) 87 | println("Posting a new C-CDA"); 88 | println("Patient ID: " + pid); 89 | 90 | Resource p = makePatient(); 91 | 92 | def x = new groovy.util.XmlParser().parse(file); 93 | def f = new groovy.xml.Namespace('http://hl7.org/fhir'); 94 | def h = new groovy.xml.Namespace('urn:hl7-org:v3'); 95 | 96 | DocumentReference doc = new DocumentReference(); 97 | 98 | def subject = new ResourceReference(); 99 | subject.referenceSimple = "Patient/$pid"; 100 | 101 | doc.subject = subject; 102 | 103 | def masterIdentifier = new Identifier(); 104 | masterIdentifier.systemSimple = x.id[0].@root; 105 | masterIdentifier.valueSimple = x.id[0].@extension; 106 | masterIdentifier.labelSimple = "Document ID ${masterIdentifier.systemSimple}" + 107 | (x.id[0].@extension ? "/${masterIdentifier.valueSimple}" : ""); 108 | 109 | doc.masterIdentifier = masterIdentifier; 110 | 111 | Map systems = [ '2.16.840.1.113883.6.1': 'http://loinc.org', 112 | '2.16.840.1.113883.6.96': 'http://snomed.info/id' ]; 113 | 114 | 115 | doc.context = { 116 | def c = new DocumentReference.DocumentReferenceContextComponent(); 117 | def dateParser = ISODateTimeFormat.basicDate(); 118 | def dateFormatter = ISODateTimeFormat.date(); 119 | c.period = { 120 | def period = new Period(); 121 | def start = dateParser.parseDateTime(x.documentationOf.serviceEvent.effectiveTime.low[0].@value[0..7]) 122 | def end = dateParser.parseDateTime(x.documentationOf.serviceEvent.effectiveTime.high[0].@value[0..7]) 123 | 124 | // lie about the start time because EMERGE has a patient's birthdate here by mistake 125 | period.startSimple = new DateAndTime(dateFormatter.print(end.minus(24 * 60 * 60 * 1000))); 126 | period.endSimple = new DateAndTime(dateFormatter.print(end)); 127 | return period 128 | }() 129 | return c 130 | }() 131 | 132 | def type = new CodeableConcept(); 133 | doc.type = type; 134 | type.coding = [] 135 | 136 | def typeCoding = new Coding(); 137 | type.coding.add(typeCoding); 138 | 139 | if (x.code[0]?.@codeSystem) { 140 | typeCoding.systemSimple = systems[x.code[0].@codeSystem] ?: x.code[0].@codeSystem; 141 | typeCoding.codeSimple = x.code[0].@code; 142 | typeCoding.displaySimple = x.code[0].@displayName; 143 | } else { 144 | typeCoding.displaySimple = "Unknown"; 145 | } 146 | 147 | if (typeCoding.codeSimple == "34133-9") { 148 | type.coding.add({ 149 | typeCoding = new Coding(); 150 | typeCoding.codeSimple = "Summary" 151 | typeCoding.displaySimple = "Continuity of Care Document" 152 | return typeCoding 153 | }()) 154 | } 155 | 156 | def contained = []; 157 | x.author.assignedAuthor.assignedPerson.each { 158 | def author = new ResourceReference() 159 | def name = it.name.given.collect{it.text()}.join(" ")+ " " + it.name.family.text(); 160 | author.displaySimple = name; 161 | doc.author.add(author); 162 | 163 | println it.name + " then given " + it.name.given.collect{it.text()}; 164 | 165 | } 166 | 167 | doc.indexedSimple = DateAndTime.now(); 168 | doc.statusSimple = DocumentReference.DocumentReferenceStatus.current; 169 | doc.author.collect{it.displaySimple}; 170 | 171 | doc.descriptionSimple = x.title.text(); 172 | doc.mimeTypeSimple = "application/hl7-v3+xml"; 173 | 174 | Binary rawResource = new Binary(); 175 | rawResource.setContent(bytes); 176 | rawResource.setContentType(doc.mimeTypeSimple); 177 | println "making closure"; 178 | def now = DateAndTime.now(); 179 | 180 | AtomFeed feed = new AtomFeed(); 181 | /* 182 | AtomEntry patientRef = new AtomEntry(); 183 | patientRef.id = "Patient/$pid"; 184 | patientRef.resource = p; 185 | patientRef.updated = now; 186 | */ 187 | AtomEntry rawEntry = new AtomEntry(); 188 | rawEntry.id = "urn:cid:binary-document"; 189 | rawEntry.resource = rawResource; 190 | rawEntry.updated = now; 191 | 192 | 193 | doc.locationSimple = rawEntry.id; 194 | AtomEntry rawDocRef = new AtomEntry(); 195 | rawDocRef.id = "urn:cid:doc-ref"; 196 | rawDocRef.resource = doc; 197 | rawDocRef.updated = now; 198 | 199 | //feed.entryList.add(patientRef); 200 | feed.entryList.add(rawEntry); 201 | feed.entryList.add(rawDocRef); 202 | 203 | feed.authorName = "groovy.config.atom.author-name"; 204 | feed.authorUri = "groovy.config.atom.author-uri"; 205 | feed.id = "feed-id"; 206 | feed.updated = now; 207 | 208 | HTTPBuilder rest = new HTTPBuilder( fhirBase ); 209 | 210 | UsernamePasswordCredentials creds = new UsernamePasswordCredentials(username, password); 211 | def basic = BasicScheme.authenticate(creds, "UTF-8", false); 212 | 213 | def docPost = rest.request(POST, XML) { req -> 214 | uri.query = [compartments: "Patient/$pid"] 215 | headers[basic.name] = basic.value 216 | headers.'Content-Type' = "application/xml" 217 | body = toXml(feed) 218 | } 219 | 220 | } 221 | -------------------------------------------------------------------------------- /postgres-tables.sql: -------------------------------------------------------------------------------- 1 | SET client_encoding = 'UTF8'; 2 | SET standard_conforming_strings = on; 3 | SET check_function_bodies = false; 4 | SET client_min_messages = warning; 5 | 6 | 7 | CREATE SEQUENCE hibernate_sequence 8 | START WITH 1 9 | INCREMENT BY 1 10 | NO MINVALUE 11 | NO MAXVALUE 12 | CACHE 1; 13 | 14 | 15 | SET default_tablespace = ''; 16 | 17 | SET default_with_oids = false; 18 | 19 | CREATE SEQUENCE seq_launch_context 20 | START WITH 1 21 | INCREMENT BY 1 22 | NO MINVALUE 23 | NO MAXVALUE 24 | CACHE 1; 25 | 26 | CREATE SEQUENCE seq_launch_context_params 27 | START WITH 1 28 | INCREMENT BY 1 29 | NO MINVALUE 30 | NO MAXVALUE 31 | CACHE 1; 32 | 33 | CREATE TABLE launch_context ( 34 | launch_id bigint DEFAULT nextval('seq_launch_context'::regclass) NOT NULL PRIMARY KEY, 35 | username character varying(255), 36 | created_by character varying(255), 37 | created_at timestamp without time zone, 38 | client_id character varying(255) 39 | ); 40 | 41 | CREATE TABLE launch_context_params ( 42 | id bigint DEFAULT nextval('seq_launch_context_params'::regclass) NOT NULL PRIMARY KEY, 43 | launch_context bigint references launch_context(launch_id), 44 | param_name character varying(255), 45 | param_value character varying(255) 46 | ); 47 | 48 | CREATE TABLE resource_compartment ( 49 | fhir_type character varying(255) NOT NULL, 50 | fhir_id character varying(255) NOT NULL, 51 | compartments character varying[] NOT NULL 52 | ); 53 | 54 | 55 | 56 | CREATE SEQUENCE seq_resource_index_term 57 | START WITH 1 58 | INCREMENT BY 1 59 | NO MINVALUE 60 | NO MAXVALUE 61 | CACHE 1; 62 | 63 | 64 | 65 | CREATE TABLE resource_index_term ( 66 | id bigint DEFAULT nextval('seq_resource_index_term'::regclass) NOT NULL, 67 | fhir_id character varying(255) NOT NULL, 68 | fhir_type character varying(255) NOT NULL, 69 | search_param character varying(255) NOT NULL, 70 | version_id bigint NOT NULL, 71 | class character varying(255) NOT NULL, 72 | string_value text, 73 | composite_value character varying(255), 74 | date_max timestamp without time zone, 75 | date_min timestamp without time zone, 76 | token_code character varying(255), 77 | token_namespace character varying(255), 78 | token_text character varying(255), 79 | reference_id character varying(255), 80 | reference_is_external character varying(255), 81 | reference_type character varying(255), 82 | reference_version character varying(255), 83 | number_max real, 84 | number_min real 85 | ); 86 | 87 | 88 | 89 | CREATE TABLE resource_version ( 90 | version_id bigint NOT NULL, 91 | content text NOT NULL, 92 | fhir_id character varying(255) NOT NULL, 93 | fhir_type character varying(255) NOT NULL, 94 | rest_date timestamp without time zone NOT NULL, 95 | rest_operation character varying(255) NOT NULL 96 | ); 97 | 98 | 99 | CREATE SEQUENCE seq_resource_compartment 100 | START WITH 1 101 | INCREMENT BY 1 102 | NO MINVALUE 103 | NO MAXVALUE 104 | CACHE 1; 105 | 106 | CREATE SEQUENCE seq_resource_version 107 | START WITH 1 108 | INCREMENT BY 1 109 | NO MINVALUE 110 | NO MAXVALUE 111 | CACHE 1; 112 | 113 | SELECT pg_catalog.setval('hibernate_sequence', 1219157, true); 114 | 115 | 116 | 117 | SELECT pg_catalog.setval('seq_resource_compartment', 13448, true); 118 | 119 | 120 | 121 | SELECT pg_catalog.setval('seq_resource_index_term', 412599, true); 122 | 123 | 124 | 125 | SELECT pg_catalog.setval('seq_resource_version', 107747, true); 126 | 127 | 128 | 129 | ALTER TABLE ONLY resource_compartment 130 | ADD CONSTRAINT resource_compartment_pkey PRIMARY KEY (fhir_type, fhir_id); 131 | 132 | 133 | 134 | ALTER TABLE ONLY resource_index_term 135 | ADD CONSTRAINT resource_index_term_pkey PRIMARY KEY (id); 136 | 137 | 138 | 139 | ALTER TABLE ONLY resource_version 140 | ADD CONSTRAINT resource_version_pkey PRIMARY KEY (version_id); 141 | 142 | 143 | 144 | CREATE INDEX logical_id ON resource_index_term USING btree (fhir_id, fhir_type); 145 | 146 | 147 | 148 | CREATE INDEX logical_id_searchparam_value ON resource_index_term USING btree (fhir_type, search_param); 149 | 150 | 151 | 152 | CREATE INDEX ref_logical_id ON resource_index_term USING btree (reference_id, reference_type); 153 | 154 | 155 | 156 | CREATE INDEX search_param_reference ON resource_index_term USING btree (search_param, reference_type, reference_id); 157 | 158 | 159 | 160 | CREATE INDEX search_param_string ON resource_index_term USING btree (search_param, string_value); 161 | 162 | 163 | 164 | CREATE INDEX search_param_token ON resource_index_term USING btree (fhir_type, search_param, token_code, token_namespace); 165 | 166 | 167 | 168 | CREATE INDEX term_version_id ON resource_index_term USING btree (version_id); 169 | 170 | 171 | 172 | CREATE INDEX version_logical_id ON resource_version USING btree (fhir_type, fhir_id); 173 | 174 | 175 | 176 | CREATE INDEX version_version_id ON resource_version USING btree (version_id); 177 | 178 | 179 | 180 | REVOKE ALL ON SCHEMA public FROM PUBLIC; 181 | REVOKE ALL ON SCHEMA public FROM postgres; 182 | GRANT ALL ON SCHEMA public TO postgres; 183 | GRANT ALL ON SCHEMA public TO PUBLIC; 184 | 185 | -------------------------------------------------------------------------------- /reset-db.sql: -------------------------------------------------------------------------------- 1 | DROP SCHEMA public CASCADE; 2 | CREATE SCHEMA public; 3 | GRANT ALL ON SCHEMA public TO postgres; 4 | GRANT ALL ON SCHEMA public TO fhir; 5 | -------------------------------------------------------------------------------- /scripts/CreateDatabase.groovy: -------------------------------------------------------------------------------- 1 | import java.io.File 2 | import fhir.SqlService 3 | 4 | SqlService sqlService = ctx.sqlService 5 | 6 | String todo = new File('postgres-tables.sql').text 7 | println ("Creating SQL tables...") 8 | sqlService.sql.execute(todo) 9 | println "Creation complete." 10 | -------------------------------------------------------------------------------- /src/groovy/fhir/auth/TokenCache.groovy: -------------------------------------------------------------------------------- 1 | package fhir.auth 2 | 3 | import com.google.common.cache.CacheBuilder 4 | import groovy.util.logging.Log4j 5 | 6 | import com.google.common.cache.Cache 7 | 8 | import org.springframework.beans.factory.InitializingBean 9 | 10 | @Log4j 11 | class TokenCache implements InitializingBean { 12 | 13 | String spec; 14 | Cache cache; 15 | 16 | void setSpecification(String inSpec) { 17 | spec = inSpec 18 | } 19 | 20 | @Override 21 | public void afterPropertiesSet() throws Exception { 22 | log.debug("Initialized token cache") 23 | cache = CacheBuilder 24 | .from(spec) 25 | .build(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/groovy/fhir/searchParam/CompositeSearchParamHandler.groovy: -------------------------------------------------------------------------------- 1 | package fhir.searchParam 2 | 3 | import java.util.Map; 4 | 5 | import org.hl7.fhir.instance.model.Resource 6 | //import org.hl7.fhir.instance.model.Conformance.SearchParamType 7 | import org.w3c.dom.Node 8 | import org.w3c.dom.NodeList 9 | 10 | import com.mongodb.BasicDBObject; 11 | 12 | import fhir.ResourceIndexComposite; 13 | import fhir.ResourceIndexTerm 14 | 15 | // Generates "composite" matches based on FHIR spec. But unclear whether/how 16 | // composites interact with modifiers. Are the terms in a composite individually 17 | // modifiable? It not, how can one express "before this date" in a status-date pair? 18 | 19 | public class CompositeSearchParamHandler extends SearchParamHandler { 20 | 21 | String orderByColumn = "composite_value" 22 | 23 | private String parent; 24 | private List children = [] 25 | 26 | @Override 27 | protected void init(){ 28 | if (xpath == null) { 29 | println("No composite xpath for " + searchParamName) 30 | return 31 | } 32 | def paths = xpath.split('\\$'); 33 | parent = paths[0]; 34 | children = paths[1..-1].collect { "./$it/@value"; } 35 | } 36 | 37 | @Override 38 | protected String paramXpath() { 39 | "//$parent" 40 | } 41 | 42 | @Override 43 | public ResourceIndexTerm createIndex(IndexedValue indexedValue, versionId, fhirId, fhirType) { 44 | def ret = new ResourceIndexComposite() 45 | ret.search_param = indexedValue.handler.searchParamName 46 | ret.version_id = versionId 47 | ret.fhir_id = fhirId 48 | ret.fhir_type = fhirType 49 | ret.composite_value = indexedValue.dbFields.composite 50 | return ret 51 | } 52 | 53 | @Override 54 | public void processMatchingXpaths(List compositeRoots, org.w3c.dom.Document r, List index){ 55 | 56 | for (Node n : compositeRoots) { 57 | List combined = []; 58 | 59 | for (String child : children) { 60 | List childMatches = query(child, n); 61 | if (childMatches.size() > 1) { 62 | throw new Exception("Expected <= 1 composite child for " + 63 | parent + "/" + child); 64 | } else if (childMatches.size() == 1){ 65 | combined.add(childMatches.get(0).nodeValue); 66 | } 67 | } 68 | 69 | if (combined.size() == children.size()) { 70 | index.add(value([ 71 | composite: combined.join('$') 72 | ])) 73 | } 74 | 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/groovy/fhir/searchParam/DateSearchParamHandler.groovy: -------------------------------------------------------------------------------- 1 | package fhir.searchParam 2 | 3 | import java.util.regex.Matcher 4 | import java.util.regex.Pattern 5 | 6 | import org.hl7.fhir.instance.model.Resource 7 | //import org.hl7.fhir.instance.model.Conformance.SearchParamType 8 | import org.joda.time.DateTime 9 | import org.joda.time.DateTimeZone 10 | import org.joda.time.Interval 11 | import org.joda.time.ReadablePeriod 12 | import org.joda.time.format.ISODateTimeFormat 13 | import org.w3c.dom.Node 14 | import org.w3c.dom.NodeList 15 | 16 | import com.sun.org.apache.xerces.internal.impl.dv.xs.PrecisionDecimalDV; 17 | import fhir.ResourceIndexDate 18 | import fhir.ResourceIndexString 19 | import fhir.ResourceIndexTerm 20 | 21 | public class DateSearchParamHandler extends SearchParamHandler { 22 | 23 | String orderByColumn = "date_min" 24 | 25 | @Override 26 | protected String paramXpath() { 27 | return "//"+this.xpath 28 | } 29 | 30 | @Override 31 | public void processMatchingXpaths(List nodes, org.w3c.dom.Document r, List index) { 32 | 33 | nodes.each { 34 | 35 | def low=null, high=null; 36 | 37 | if (query("./@value", it)){ // A simple dateTime finds a @value directly 38 | low = query("./@value", it)[0].nodeValue 39 | high = query("./@value", it)[0].nodeValue 40 | } else { // a Period finds a start and end value 41 | def startVals = query("./f:start/@value", it) 42 | if (startVals) low = startVals[0].nodeValue 43 | def endVals = query("./f:end/@value", it) 44 | if (endVals) high = endVals[0].nodeValue 45 | } 46 | 47 | Map m = [:] 48 | if (low) m.date_min = toSqlDate(precisionInterval(low).start) 49 | if (high) m.date_max = toSqlDate(precisionInterval(high).end) 50 | if (m.size()) index.add(value(m)) 51 | } 52 | } 53 | 54 | public static Interval precisionInterval(String s) { 55 | DateTime earliest = ISODateTimeFormat 56 | .dateOptionalTimeParser() 57 | .parseDateTime(s) 58 | .withZone(DateTimeZone.UTC); 59 | 60 | DateTime latest = earliest.plus(precisionOf(s)); 61 | return new Interval(earliest, latest); 62 | } 63 | 64 | private static ReadablePeriod precisionOf(String s) { 65 | 66 | // Determine precision by length of string 67 | // up to where the time zone (if any) begins 68 | Pattern t = ~/(.*?T.*?)[Z\\.\\+\\-]/ 69 | Matcher m = t.matcher(s); 70 | 71 | int len; 72 | if (m.matches()){ 73 | len = m.group(1).length(); 74 | } else { 75 | len = s.length(); 76 | } 77 | 78 | if (len <= 5){ // "yyyy-".length 79 | return org.joda.time.Years.ONE; 80 | } else if (len <= 8) { // "yyyy-mm-".length 81 | return org.joda.time.Months.ONE; 82 | } else if (len <= 11) { // "yyyy-mm-ddT" 83 | return org.joda.time.Days.ONE; 84 | } else if (len <= 14) { //yyyy-mm-ddThh:" 85 | return org.joda.time.Hours.ONE; 86 | } else if (len <= 17) { //yyyy-mm-ddThh:mm:" 87 | return org.joda.time.Minutes.ONE; 88 | } else if (len <= 20) { //yyyy-mm-ddT10:hh:mm:ss:" 89 | return org.joda.time.Seconds.ONE; 90 | } 91 | 92 | // Assume it's perfectly precise if it's subsecond 93 | // (FHIR explicitly allows ignoring seconds on dateTimes). 94 | return org.joda.time.Seconds.ZERO; 95 | } 96 | 97 | @Override 98 | public ResourceIndexTerm createIndex(IndexedValue indexedValue, versionId, fhirId, fhirType) { 99 | def ret = new ResourceIndexDate() 100 | ret.search_param = indexedValue.handler.searchParamName 101 | ret.version_id = versionId 102 | ret.fhir_id = fhirId 103 | ret.fhir_type = fhirType 104 | ret.date_min = indexedValue.dbFields.date_min 105 | ret.date_max = indexedValue.dbFields.date_max 106 | return ret 107 | } 108 | 109 | private java.sql.Timestamp toSqlDate(DateTime d){ 110 | print "coverting $d to ${d.toInstant().millis}" 111 | return new java.sql.Timestamp(d.toInstant().millis) 112 | } 113 | 114 | @Override 115 | def joinOn(SearchedValue v) { 116 | v.values.split(",").collect { value -> 117 | boolean before = false 118 | boolean after = false 119 | 120 | def inequality = (value =~ /(>=|>|<=|<)(.*)/) 121 | if (inequality.matches()){ 122 | String op = inequality[0][1] 123 | if (op.startsWith(">")) after = true 124 | if (op.startsWith("<")) before = true 125 | value = inequality[0][2] 126 | } 127 | 128 | Interval precision = precisionInterval(value) 129 | List fields = [] 130 | 131 | if (before == false) { 132 | fields += [ 133 | name: 'date_min', 134 | value: toSqlDate(precision.start), 135 | operation: '>=' 136 | ] 137 | } 138 | 139 | if (after == false) { 140 | fields += [ 141 | name: 'date_max', 142 | value: toSqlDate(precision.end), 143 | operation: '<=' 144 | ] 145 | } 146 | 147 | return fields 148 | } 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/groovy/fhir/searchParam/IdSearchParamHandler.groovy: -------------------------------------------------------------------------------- 1 | package fhir.searchParam 2 | 3 | import java.util.List; 4 | 5 | //import org.hl7.fhir.instance.model.Conformance.SearchParamType 6 | import org.w3c.dom.Node 7 | 8 | public class IdSearchParamHandler extends SearchParamHandler { 9 | 10 | @Override 11 | protected String paramXpath() { 12 | throw new Exception("Should not use Id Search Parameter to index a resource"); 13 | } 14 | 15 | @Override 16 | protected void processMatchingXpaths(List nodes, org.w3c.dom.Document r, 17 | List index) { 18 | throw new Exception("Should not use Id Search Parameter to index a resource"); 19 | } 20 | 21 | def joinOn(SearchedValue v) { 22 | v.values.split(",").collect { 23 | List fields = [] 24 | fields += [ 25 | name: 'fhir_id', 26 | value: it 27 | ] 28 | return fields 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/groovy/fhir/searchParam/IndexedValue.groovy: -------------------------------------------------------------------------------- 1 | package fhir.searchParam 2 | 3 | import java.util.Map; 4 | 5 | //import org.hl7.fhir.instance.model.Conformance.SearchParamType; 6 | 7 | // Simple name-value tuple to represent indexed Search Parameters. 8 | // For example: 9 | // name="diagnosisDate", value="2000" 10 | // name="diagnosisDate:before", value="2000-01-01T00:00:00Z" 11 | class IndexedValue{ 12 | public Map dbFields; 13 | public String paramName; 14 | public SearchParamHandler handler; 15 | } 16 | -------------------------------------------------------------------------------- /src/groovy/fhir/searchParam/NumberSearchParamHandler.groovy: -------------------------------------------------------------------------------- 1 | package fhir.searchParam 2 | 3 | import org.hl7.fhir.instance.model.Resource 4 | //import org.hl7.fhir.instance.model.Conformance.SearchParamType 5 | import org.w3c.dom.Node 6 | 7 | import fhir.ResourceIndexNumber 8 | import fhir.ResourceIndexTerm 9 | 10 | public class NumberSearchParamHandler extends SearchParamHandler { 11 | 12 | String orderByField = "number_min" 13 | 14 | @Override 15 | protected String paramXpath() { 16 | return "//"+this.xpath; 17 | } 18 | 19 | @Override 20 | public void processMatchingXpaths(List numberNodes, org.w3c.dom.Document r, List index) { 21 | 22 | for (Node n : numberNodes) { 23 | 24 | // plain numbers 25 | query("./@value", n).each { plainNumber-> 26 | index.add(value([ 27 | number_min: plainNumber, 28 | number_max: plainNumber 29 | ])) 30 | } 31 | 32 | // numbers in Quantity or one of its subtypes 33 | query("./f:value", n).each { q -> 34 | def number = query("./@value", q) 35 | def comparator = query("../f:comparator/@value", q) 36 | 37 | def number_min = null; 38 | def number_max = null; 39 | 40 | if (!comparator) { 41 | number_min = number 42 | number_max = number 43 | } else if (comparator == '<' || comparator == '<=') { 44 | number_max = number; 45 | } else if (comparator == '>' || comparator == '>=') { 46 | number_min = number; 47 | } 48 | 49 | index.add(value([ 50 | number_min: number_min, 51 | number_max: number_max 52 | ])) 53 | } 54 | } 55 | } 56 | 57 | @Override 58 | public ResourceIndexTerm createIndex(IndexedValue indexedValue, versionId, fhirId, fhirType) { 59 | def ret = new ResourceIndexNumber() 60 | ret.search_param = indexedValue.handler.searchParamName 61 | ret.version_id = versionId 62 | ret.fhir_id = fhirId 63 | ret.fhir_type = fhirType 64 | ret.number_min = indexedValue.dbFields.number_min 65 | ret.number_max = indexedValue.dbFields.number_max 66 | return ret 67 | } 68 | 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/groovy/fhir/searchParam/QuantitySearchParamHandler.groovy: -------------------------------------------------------------------------------- 1 | package fhir.searchParam 2 | 3 | import org.hl7.fhir.instance.model.Resource 4 | //import org.hl7.fhir.instance.model.Conformance.SearchParamType 5 | import org.w3c.dom.Node 6 | 7 | import fhir.ResourceIndexNumber 8 | import fhir.ResourceIndexTerm 9 | 10 | public class QuantitySearchParamHandler extends SearchParamHandler { 11 | 12 | String orderByField = "number_min" 13 | 14 | @Override 15 | protected String paramXpath() { 16 | return "//"+this.xpath; 17 | } 18 | 19 | @Override 20 | public void processMatchingXpaths(List numberNodes, org.w3c.dom.Document r, List index) { 21 | 22 | for (Node n : numberNodes) { 23 | 24 | // plain numbers 25 | query("./@value", n).each { plainNumber-> 26 | index.add(value([ 27 | number_min: plainNumber, 28 | number_max: plainNumber 29 | ])) 30 | } 31 | 32 | // numbers in Quantity or one of its subtypes 33 | query("./f:value", n).each { q -> 34 | def number = query("./@value", q) 35 | if (!number) return 36 | 37 | number = Float.parseFloat(number[0].nodeValue) 38 | def comparator = query("../f:comparator/@value", q) 39 | if (comparator) comparator = comparator[0].nodeValue 40 | 41 | def number_min = Float.NEGATIVE_INFINITY; 42 | def number_max = Float.POSITIVE_INFINITY; 43 | 44 | if (!comparator) { 45 | number_min = number 46 | number_max = number 47 | } else if (comparator == '<' || comparator == '<=') { 48 | number_max = number; 49 | } else if (comparator == '>' || comparator == '>=') { 50 | number_min = number; 51 | } 52 | 53 | index.add(value([ 54 | number_min: number_min, 55 | number_max: number_max 56 | ])) 57 | } 58 | } 59 | } 60 | 61 | @Override 62 | public ResourceIndexTerm createIndex(IndexedValue indexedValue, versionId, fhirId, fhirType) { 63 | def ret = new ResourceIndexNumber() 64 | ret.search_param = indexedValue.handler.searchParamName 65 | ret.version_id = versionId 66 | ret.fhir_id = fhirId 67 | ret.fhir_type = fhirType 68 | if (indexedValue.dbFields.number_min) 69 | ret.number_min = indexedValue.dbFields.number_min 70 | if (indexedValue.dbFields.number_max) 71 | ret.number_max = indexedValue.dbFields.number_max 72 | return ret 73 | } 74 | 75 | def joinOn(SearchedValue v) { 76 | v.values.split(",").collect { value-> 77 | 78 | List fields = [] 79 | 80 | boolean greater = false 81 | boolean less = false 82 | 83 | def inequality = (value =~ /(>=|>|<=|<)(.*)/) 84 | 85 | if (inequality.matches()){ 86 | String op = inequality[0][1] 87 | if (op.startsWith(">")) greater = true 88 | if (op.startsWith("<")) less = true 89 | value = inequality[0][2] 90 | } 91 | 92 | value = Float.parseFloat(value) 93 | 94 | if (less == false) { 95 | fields += [ 96 | name: 'number_max', 97 | value: value, 98 | operation: '>=' 99 | ] 100 | } 101 | 102 | if (greater == false) { 103 | fields += [ 104 | name: 'number_min', 105 | value: value, 106 | operation: '<=' 107 | ] 108 | } 109 | 110 | return fields 111 | } 112 | } 113 | } 114 | 115 | -------------------------------------------------------------------------------- /src/groovy/fhir/searchParam/ReferenceSearchParamHandler.groovy: -------------------------------------------------------------------------------- 1 | package fhir.searchParam 2 | 3 | //import org.hl7.fhir.instance.model.Conformance.SearchParamType 4 | import org.w3c.dom.Node 5 | 6 | import fhir.ResourceIndexReference 7 | import fhir.ResourceIndexTerm 8 | public class ReferenceSearchParamHandler extends SearchParamHandler { 9 | 10 | String orderByColumn = "reference_id" 11 | 12 | @Override 13 | protected void processMatchingXpaths(List nodes, org.w3c.dom.Document r, List index) { 14 | nodes.each { 15 | String ref = query('./f:reference/@value', it).collect{it.nodeValue}.join(""); 16 | Map parts = urlService.fhirUrlParts(ref) 17 | 18 | if (ref.startsWith("#")) { 19 | index.add(value([ 20 | contained_id: ref[1..-1], 21 | contained_type: query("//f:contained/*/f:id[@value='"+ref[1..-1]+"']/..", r).collect{it.nodeName}.join("") 22 | ])) 23 | } 24 | else if (!parts['type']) { 25 | index.add(value([ 26 | reference_is_external: true, 27 | reference_id: ref 28 | ])) 29 | } else { 30 | index.add(value([ 31 | raw: ref, 32 | reference_is_external: false, 33 | reference_id: parts.id, 34 | reference_type: parts.type, 35 | reference_version: parts.version 36 | ])) 37 | } 38 | } 39 | } 40 | 41 | def joinOn(SearchedValue v) { 42 | if (v.values == null) return [] 43 | v.values.split(",").collect { 44 | List fields = [] 45 | if (it){ 46 | fields += [ name: 'reference_id', value: it] 47 | } 48 | if (v.modifier) { 49 | fields += [ 50 | name: 'reference_type', 51 | value: v.modifier 52 | ] 53 | } 54 | return fields 55 | } 56 | } 57 | 58 | 59 | @Override 60 | public ResourceIndexTerm createIndex(IndexedValue indexedValue, versionId, fhirId, fhirType) { 61 | def ret = new ResourceIndexReference() 62 | ret.search_param = indexedValue.handler.searchParamName 63 | ret.version_id = versionId 64 | ret.fhir_id = fhirId 65 | ret.fhir_type = fhirType 66 | 67 | if (indexedValue.dbFields.contained_id) { 68 | ret.reference_id = fhirId+"_contained_"+indexedValue.dbFields.contained_id 69 | ret.reference_type = indexedValue.dbFields.contained_type 70 | } else { 71 | ret.reference_type = indexedValue.dbFields.reference_type 72 | ret.reference_id = indexedValue.dbFields.reference_id 73 | } 74 | 75 | ret.reference_version = indexedValue.dbFields.resource_version 76 | 77 | ret.reference_is_external = false // TODO support external refs 78 | return ret 79 | } 80 | 81 | @Override 82 | protected String paramXpath() { 83 | return "//"+this.xpath; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/groovy/fhir/searchParam/SearchParamHandler.groovy: -------------------------------------------------------------------------------- 1 | package fhir.searchParam 2 | 3 | import fhir.ResourceIndexTerm 4 | import fhir.UrlService 5 | import groovy.util.logging.Log4j 6 | 7 | import javax.xml.xpath.XPath 8 | import javax.xml.xpath.XPathConstants 9 | 10 | import org.hl7.fhir.instance.formats.XmlParser 11 | import org.hl7.fhir.instance.model.Resource 12 | import org.hl7.fhir.instance.model.Conformance; 13 | import org.w3c.dom.Node 14 | 15 | 16 | /** 17 | * @author jmandel 18 | * 19 | * Instances of this class generate database-ready index terms for a given 20 | * FHIR resource, based on the declared "searchParam" support in our 21 | * server-wide conformance profile. 22 | */ 23 | @Log4j 24 | public abstract class SearchParamHandler { 25 | 26 | static XmlParser parser = new XmlParser() 27 | static XPath xpathEvaluator; 28 | static UrlService urlService; 29 | 30 | String searchParamName; 31 | def /*SearchParamType */ fieldType; 32 | String xpath; 33 | String orderByColumn; 34 | String resourceName; 35 | List referenceTypes; 36 | 37 | public static SearchParamHandler create(String searchParamName, 38 | /*SearchParamType */ fieldType, 39 | String resourceName, 40 | String xpath, 41 | List referenceTypes) { 42 | 43 | String ft = fieldType.toString().toLowerCase().capitalize(); 44 | String className = SearchParamHandler.class.canonicalName.replace( 45 | "SearchParamHandler", ft + "SearchParamHandler") 46 | 47 | Class c = Class.forName(className, 48 | true, 49 | Thread.currentThread().contextClassLoader); 50 | 51 | SearchParamHandler ret = c.newInstance( 52 | searchParamName: searchParamName, 53 | fieldType: fieldType, 54 | xpath: xpath, 55 | resourceName: resourceName, 56 | referenceTypes: referenceTypes 57 | ); 58 | ret.init(); 59 | return ret; 60 | } 61 | 62 | static void injectXpathEvaluator(XPath injectedXpathEvaluator){ 63 | xpathEvaluator = injectedXpathEvaluator; 64 | } 65 | static void injectUrlService(UrlService urlServiceIn){ 66 | urlService = urlServiceIn; 67 | } 68 | 69 | protected void init(){ } 70 | 71 | protected abstract void processMatchingXpaths(List nodes, org.w3c.dom.Document r, List index); 72 | 73 | protected abstract String paramXpath() 74 | 75 | List selectNodes(String path, Node node) { 76 | 77 | // collect to take NodeList --> List 78 | xpathEvaluator.evaluate(path, node, XPathConstants.NODESET).collect { it } 79 | } 80 | 81 | List query(String xpath, Node n){ 82 | selectNodes(xpath, n) 83 | } 84 | 85 | 86 | public ResourceIndexTerm createIndex(IndexedValue indexedValue, versionId, fhirId, fhirType) { 87 | throw new Exception("createIndex not implemented") 88 | } 89 | 90 | public IndexedValue value(Object v){ 91 | new IndexedValue( 92 | dbFields: v, 93 | paramName: searchParamName, 94 | handler: this 95 | ); 96 | } 97 | 98 | public String queryString(String xpath, Node n){ 99 | query(xpath, n).collect { it.nodeValue }.join " " 100 | } 101 | 102 | public List execute(org.w3c.dom.Document r) throws Exception { 103 | List index = [] 104 | List nodes = query(paramXpath(), r) 105 | processMatchingXpaths(nodes, r, index); 106 | return index; 107 | } 108 | 109 | 110 | def joinOn(SearchedValue v) { 111 | throw new Exception("joinOn must be implemented in subclasses"); 112 | } 113 | 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/groovy/fhir/searchParam/SearchedValue.groovy: -------------------------------------------------------------------------------- 1 | package fhir.searchParam 2 | 3 | import java.util.Map; 4 | 5 | //import org.hl7.fhir.instance.model.Conformance.SearchParamType; 6 | 7 | // Simple name-value tuple to represent indexed Search Parameters. 8 | // For example: 9 | // name="diagnosisDate", value="2000" 10 | // name="diagnosisDate:before", value="2000-01-01T00:00:00Z" 11 | class SearchedValue{ 12 | public SearchParamHandler handler; 13 | public String modifier; 14 | public String values; 15 | public SearchedValue chained; 16 | } 17 | -------------------------------------------------------------------------------- /src/groovy/fhir/searchParam/StringSearchParamHandler.groovy: -------------------------------------------------------------------------------- 1 | package fhir.searchParam 2 | 3 | import org.hl7.fhir.instance.model.Resource 4 | //import org.hl7.fhir.instance.model.Conformance.SearchParamType 5 | import org.w3c.dom.Node 6 | 7 | import fhir.ResourceIndexString 8 | import fhir.ResourceIndexTerm 9 | 10 | 11 | public class StringSearchParamHandler extends SearchParamHandler { 12 | 13 | String orderByColumn = "string_value" 14 | 15 | @Override 16 | protected String paramXpath() { 17 | return "//$xpath//@value"; 18 | } 19 | 20 | @Override 21 | public void processMatchingXpaths(List nodes, org.w3c.dom.Document r, List index) { 22 | String parts = nodes.collect {it.nodeValue}.join(" ") 23 | index.add(value([ 24 | string: parts 25 | ])) 26 | } 27 | 28 | @Override 29 | public ResourceIndexTerm createIndex(IndexedValue indexedValue, versionId, fhirId, fhirType) { 30 | def ret = new ResourceIndexString() 31 | ret.search_param = indexedValue.handler.searchParamName 32 | ret.version_id = versionId 33 | ret.fhir_id = fhirId 34 | ret.fhir_type = fhirType 35 | ret.string_value = indexedValue.dbFields.string 36 | return ret 37 | } 38 | 39 | def joinOn(SearchedValue v) { 40 | v.values.split(",").collect { 41 | List fields = [] 42 | fields += [ name: 'string_value', value: '%'+it+'%', operation: 'ILIKE'] 43 | return fields 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/groovy/fhir/searchParam/TokenSearchParamHandler.groovy: -------------------------------------------------------------------------------- 1 | package fhir.searchParam 2 | 3 | //import org.hl7.fhir.instance.model.Conformance.SearchParamType 4 | import org.w3c.dom.Node 5 | 6 | import fhir.ResourceIndexTerm 7 | import fhir.ResourceIndexToken 8 | 9 | 10 | /** 11 | * @author jmandel 12 | * Per FHIR spec, token-type search params can search through 13 | * - text, displayname, code and code/codesystem (for codes) 14 | * - label, system and key (for identifier) 15 | */ 16 | public class TokenSearchParamHandler extends SearchParamHandler { 17 | /* :text (the match does a partial searches on 18 | * - the text portion of a CodeableConcept or 19 | * - the display portion of a Coding) 20 | * :code (a match on code and system of 21 | * - the coding/codeable concept) 22 | * :anyns matches all codes irrespective of the namespace. 23 | */ 24 | 25 | String orderByColumn = "token_text" 26 | 27 | @Override 28 | protected String paramXpath() { 29 | return "//"+this.xpath; 30 | } 31 | 32 | 33 | void processMatchingXpaths(List tokens, org.w3c.dom.Document r, List index){ 34 | 35 | for (Node n : tokens) { 36 | 37 | // :text (the match does a partial searches on 38 | // * the text portion of a CodeableConcept or 39 | // the display portion of a Coding or 40 | // the label portion of an Identifier) 41 | String text = queryString(".//f:label/@value | .//f:display/@value | .//f:text/@value", n) 42 | 43 | // For CodeableConcept and Coding, list the code as "system/code" 44 | // For Identifier, list the code as "system/value" 45 | query(".//f:code | .//f:value", n).each { systemPart -> 46 | String system = queryString("../f:system/@value", systemPart); 47 | String code = queryString("./@value", systemPart); 48 | index.add(value([ 49 | namespace: system, 50 | code: code, 51 | text: text 52 | ])) 53 | } 54 | 55 | // For plain 'ol Code elements, we'll at least pull out the value 56 | // (We won't try to determine the implicit system for now, since 57 | // it's not available in instance data or profile.xml) 58 | query("./@value", n).each {Node codePart-> 59 | index.add(value([ 60 | code: codePart.nodeValue 61 | ])) 62 | } 63 | } 64 | } 65 | 66 | @Override 67 | public ResourceIndexTerm createIndex(IndexedValue indexedValue, versionId, fhirId, fhirType) { 68 | def ret = new ResourceIndexToken() 69 | ret.search_param = indexedValue.handler.searchParamName 70 | ret.version_id = versionId 71 | ret.fhir_id = fhirId 72 | ret.fhir_type = fhirType 73 | ret.token_code = indexedValue.dbFields.code 74 | ret.token_namespace = indexedValue.dbFields.namespace 75 | ret.token_text = indexedValue.dbFields.text 76 | return ret 77 | } 78 | 79 | private List splitToken(String t) { 80 | List v = t.split("\\|") 81 | if (v.size() == 1) { 82 | if (t.startsWith("\\|")) return [null, v[0]] 83 | return ["anyns", v[0]] 84 | } 85 | return [v[0], v[1]] 86 | } 87 | 88 | @Override 89 | def joinOn(SearchedValue v) { 90 | v.values.split(",").collect { 91 | def (namespace, code) = splitToken(it) 92 | List fields = [] 93 | 94 | if (v.modifier == null){ 95 | if (namespace == null) { 96 | fields += [ name: 'token_namespace', operation: 'is null' ] 97 | } 98 | if (!(namespace in [null, "anyns"])) { 99 | fields += [ name: 'token_namespace', value: namespace ] 100 | } 101 | fields += [ name: 'token_code', value: code ] 102 | } 103 | 104 | if (v.modifier == "text"){ 105 | fields += [ name: 'token_text', operation:'ILIKE', value: '%'+it+'%' ] 106 | } 107 | 108 | return fields 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/groovy/fhir/searchParam/UriSearchParamHandler.groovy: -------------------------------------------------------------------------------- 1 | package fhir.searchParam 2 | 3 | import org.hl7.fhir.instance.model.Resource 4 | //import org.hl7.fhir.instance.model.Conformance.SearchParamType 5 | import org.w3c.dom.Node 6 | 7 | import fhir.ResourceIndexString 8 | import fhir.ResourceIndexTerm 9 | 10 | 11 | public class UriSearchParamHandler extends SearchParamHandler { 12 | 13 | String orderByColumn = "string_value" 14 | 15 | @Override 16 | protected String paramXpath() { 17 | return "//$xpath//@value"; 18 | } 19 | 20 | @Override 21 | public void processMatchingXpaths(List nodes, org.w3c.dom.Document r, List index) { 22 | String parts = nodes.collect {it.nodeValue}.join(" ") 23 | index.add(value([ 24 | string: parts 25 | ])) 26 | } 27 | 28 | @Override 29 | public ResourceIndexTerm createIndex(IndexedValue indexedValue, versionId, fhirId, fhirType) { 30 | def ret = new ResourceIndexString() 31 | ret.search_param = indexedValue.handler.searchParamName 32 | ret.version_id = versionId 33 | ret.fhir_id = fhirId 34 | ret.fhir_type = fhirType 35 | ret.string_value = indexedValue.dbFields.string 36 | return ret 37 | } 38 | 39 | def joinOn(SearchedValue v) { 40 | v.values.split(",").collect { 41 | List fields = [] 42 | fields += [ name: 'string_value', value: it, operation: 'ILIKE'] 43 | return fields 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/unit/fhir/ApiControllerTests.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | 4 | 5 | import grails.test.mixin.* 6 | import org.junit.* 7 | 8 | /** 9 | * See the API for {@link grails.test.mixin.web.ControllerUnitTestMixin} for usage instructions 10 | */ 11 | @TestFor(ApiController) 12 | class ApiControllerTests { 13 | 14 | void testSomething() { 15 | fail "Implement me" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/unit/fhir/LaunchContextControllerSpec.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | import grails.test.mixin.TestFor 4 | import spock.lang.Specification 5 | 6 | /** 7 | * See the API for {@link grails.test.mixin.web.ControllerUnitTestMixin} for usage instructions 8 | */ 9 | @TestFor(LaunchContextController) 10 | class LaunchContextControllerSpec extends Specification { 11 | 12 | def setup() { 13 | } 14 | 15 | def cleanup() { 16 | } 17 | 18 | void "test something"() { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/unit/fhir/LaunchContextParamSpec.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | import grails.test.mixin.TestFor 4 | import spock.lang.Specification 5 | 6 | /** 7 | * See the API for {@link grails.test.mixin.domain.DomainClassUnitTestMixin} for usage instructions 8 | */ 9 | @TestFor(LaunchContextParam) 10 | class LaunchContextParamSpec extends Specification { 11 | 12 | def setup() { 13 | } 14 | 15 | def cleanup() { 16 | } 17 | 18 | void "test something"() { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/unit/fhir/LaunchContextSpec.groovy: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | import grails.test.mixin.TestFor 4 | import spock.lang.Specification 5 | 6 | /** 7 | * See the API for {@link grails.test.mixin.domain.DomainClassUnitTestMixin} for usage instructions 8 | */ 9 | @TestFor(LaunchContext) 10 | class LaunchContextSpec extends Specification { 11 | 12 | def setup() { 13 | } 14 | 15 | def cleanup() { 16 | } 17 | 18 | void "test something"() { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web-app/WEB-INF/applicationContext.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Grails application factory bean 8 | 9 | 10 | 11 | 12 | A bean that manages Grails plugins 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | utf-8 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /web-app/WEB-INF/sitemesh.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /web-app/css/errors.css: -------------------------------------------------------------------------------- 1 | h1, h2 { 2 | margin: 10px 25px 5px; 3 | } 4 | 5 | h2 { 6 | font-size: 1.1em; 7 | } 8 | 9 | .filename { 10 | font-style: italic; 11 | } 12 | 13 | .exceptionMessage { 14 | margin: 10px; 15 | border: 1px solid #000; 16 | padding: 5px; 17 | background-color: #E9E9E9; 18 | } 19 | 20 | .stack, 21 | .snippet { 22 | margin: 0 25px 10px; 23 | } 24 | 25 | .stack, 26 | .snippet { 27 | border: 1px solid #ccc; 28 | -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2); 29 | -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2); 30 | box-shadow: 0 0 2px rgba(0,0,0,0.2); 31 | } 32 | 33 | /* error details */ 34 | .error-details { 35 | border-top: 1px solid #FFAAAA; 36 | -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2); 37 | -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2); 38 | box-shadow: 0 0 2px rgba(0,0,0,0.2); 39 | border-bottom: 1px solid #FFAAAA; 40 | -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2); 41 | -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2); 42 | box-shadow: 0 0 2px rgba(0,0,0,0.2); 43 | background-color:#FFF3F3; 44 | line-height: 1.5; 45 | overflow: hidden; 46 | padding: 5px; 47 | padding-left:25px; 48 | } 49 | 50 | .error-details dt { 51 | clear: left; 52 | float: left; 53 | font-weight: bold; 54 | margin-right: 5px; 55 | } 56 | 57 | .error-details dt:after { 58 | content: ":"; 59 | } 60 | 61 | .error-details dd { 62 | display: block; 63 | } 64 | 65 | /* stack trace */ 66 | .stack { 67 | padding: 5px; 68 | overflow: auto; 69 | height: 150px; 70 | } 71 | 72 | /* code snippet */ 73 | .snippet { 74 | background-color: #fff; 75 | font-family: monospace; 76 | } 77 | 78 | .snippet .line { 79 | display: block; 80 | } 81 | 82 | .snippet .lineNumber { 83 | background-color: #ddd; 84 | color: #999; 85 | display: inline-block; 86 | margin-right: 5px; 87 | padding: 0 3px; 88 | text-align: right; 89 | width: 3em; 90 | } 91 | 92 | .snippet .error { 93 | background-color: #fff3f3; 94 | font-weight: bold; 95 | } 96 | 97 | .snippet .error .lineNumber { 98 | background-color: #faa; 99 | color: #333; 100 | font-weight: bold; 101 | } 102 | 103 | .snippet .line:first-child .lineNumber { 104 | padding-top: 5px; 105 | } 106 | 107 | .snippet .line:last-child .lineNumber { 108 | padding-bottom: 5px; 109 | } -------------------------------------------------------------------------------- /web-app/css/mobile.css: -------------------------------------------------------------------------------- 1 | /* Styles for mobile devices */ 2 | 3 | @media screen and (max-width: 480px) { 4 | .nav { 5 | padding: 0.5em; 6 | } 7 | 8 | .nav li { 9 | margin: 0 0.5em 0 0; 10 | padding: 0.25em; 11 | } 12 | 13 | /* Hide individual steps in pagination, just have next & previous */ 14 | .pagination .step, .pagination .currentStep { 15 | display: none; 16 | } 17 | 18 | .pagination .prevLink { 19 | float: left; 20 | } 21 | 22 | .pagination .nextLink { 23 | float: right; 24 | } 25 | 26 | /* pagination needs to wrap around floated buttons */ 27 | .pagination { 28 | overflow: hidden; 29 | } 30 | 31 | /* slightly smaller margin around content body */ 32 | fieldset, 33 | .property-list { 34 | padding: 0.3em 1em 1em; 35 | } 36 | 37 | input, textarea { 38 | width: 100%; 39 | -moz-box-sizing: border-box; 40 | -webkit-box-sizing: border-box; 41 | -ms-box-sizing: border-box; 42 | box-sizing: border-box; 43 | } 44 | 45 | select, input[type=checkbox], input[type=radio], input[type=submit], input[type=button], input[type=reset] { 46 | width: auto; 47 | } 48 | 49 | /* hide all but the first column of list tables */ 50 | .scaffold-list td:not(:first-child), 51 | .scaffold-list th:not(:first-child) { 52 | display: none; 53 | } 54 | 55 | .scaffold-list thead th { 56 | text-align: center; 57 | } 58 | 59 | /* stack form elements */ 60 | .fieldcontain { 61 | margin-top: 0.6em; 62 | } 63 | 64 | .fieldcontain label, 65 | .fieldcontain .property-label, 66 | .fieldcontain .property-value { 67 | display: block; 68 | float: none; 69 | margin: 0 0 0.25em 0; 70 | text-align: left; 71 | width: auto; 72 | } 73 | 74 | .errors ul, 75 | .message p { 76 | margin: 0.5em; 77 | } 78 | 79 | .error ul { 80 | margin-left: 0; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /web-app/images/apple-touch-icon-retina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/apple-touch-icon-retina.png -------------------------------------------------------------------------------- /web-app/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/apple-touch-icon.png -------------------------------------------------------------------------------- /web-app/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/favicon.ico -------------------------------------------------------------------------------- /web-app/images/fire-shot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/fire-shot.jpg -------------------------------------------------------------------------------- /web-app/images/grails_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/grails_logo.jpg -------------------------------------------------------------------------------- /web-app/images/grails_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/grails_logo.png -------------------------------------------------------------------------------- /web-app/images/leftnav_btm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/leftnav_btm.png -------------------------------------------------------------------------------- /web-app/images/leftnav_midstretch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/leftnav_midstretch.png -------------------------------------------------------------------------------- /web-app/images/leftnav_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/leftnav_top.png -------------------------------------------------------------------------------- /web-app/images/skin/database_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/skin/database_add.png -------------------------------------------------------------------------------- /web-app/images/skin/database_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/skin/database_delete.png -------------------------------------------------------------------------------- /web-app/images/skin/database_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/skin/database_edit.png -------------------------------------------------------------------------------- /web-app/images/skin/database_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/skin/database_save.png -------------------------------------------------------------------------------- /web-app/images/skin/database_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/skin/database_table.png -------------------------------------------------------------------------------- /web-app/images/skin/exclamation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/skin/exclamation.png -------------------------------------------------------------------------------- /web-app/images/skin/house.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/skin/house.png -------------------------------------------------------------------------------- /web-app/images/skin/information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/skin/information.png -------------------------------------------------------------------------------- /web-app/images/skin/shadow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/skin/shadow.jpg -------------------------------------------------------------------------------- /web-app/images/skin/sorted_asc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/skin/sorted_asc.gif -------------------------------------------------------------------------------- /web-app/images/skin/sorted_desc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/skin/sorted_desc.gif -------------------------------------------------------------------------------- /web-app/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/spinner.gif -------------------------------------------------------------------------------- /web-app/images/springsource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/api-server/00861ce5cb3da1f6f603ab9514286be2f436d185/web-app/images/springsource.png -------------------------------------------------------------------------------- /web-app/js/application.js: -------------------------------------------------------------------------------- 1 | if (typeof jQuery !== 'undefined') { 2 | (function($) { 3 | $('#spinner').ajaxStart(function() { 4 | $(this).fadeIn(); 5 | }).ajaxStop(function() { 6 | $(this).fadeOut(); 7 | }); 8 | })(jQuery); 9 | } 10 | --------------------------------------------------------------------------------