diff --git a/Dockerfile b/Dockerfile index 70a507a3596ae16bdfdc22b8ec5e91be70bae9a2..19ddbde3c8583c3966965d50399cb0d8d7a8b667 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,6 @@ #### # Build locally: -# docker build -# --build-arg gitlab_token=<token> -# -t nshmp-ws-static . +# docker build -t nshmp-ws-static . #### ARG BUILD_IMAGE=usgs/amazoncorretto:11 @@ -10,9 +8,7 @@ ARG FROM_IMAGE=usgs/amazoncorretto:11 FROM ${BUILD_IMAGE} as builder -# TODO: Token needed until nshmp-lib is public -ARG GITLAB_TOKEN=null -ARG CI_JOB_TOKEN=null +# For GitLab CI/CD ARG CI_PROJECT_URL=null ARG CI_COMMIT_BRANCH=null diff --git a/README.md b/README.md index 87adcf8a2ce6383fe6336f0af7461a15274f3d97..2ac8c7129425af4c7a1f6f46f1f854a0ce358520 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,5 @@ docker run -e SERVICE=aashto -p 8080:8080 usgs/nshmp-ws-static:production-latest To build the Docker image locally run: ```bash -docker build --build-arg GITLAB_TOKEN="{gitlab_token}" -t nshmp-ws-static . +docker build -t nshmp-ws-static . ``` - -> Replace `{gitlab_token}` with the GitLab API -> [access token](https://code.usgs.gov/-/profile/personal_access_tokens) diff --git a/gradle.properties b/gradle.properties index 33e58e7cd9cd9e17fb4a1aab904938409ee454b9..54580ed9db2de6f8bf341bec3ac945bb1b63a349 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ netcdfVersion = 5.5.2 nodePluginVersion = 3.0.1 nodeVersion = 16.3.0 nshmpLibVersion = 1.0.6 -nshmpWsUtilsVersion = 0.3.9 +nshmpWsUtilsVersion = 0.3.10 openApiVersion = 4.0.0 shadowVersion = 7.1.1 slfVersion = 1.7.30 diff --git a/gradle/repositories.gradle b/gradle/repositories.gradle index 09519817c26f8f1bd4eb5bd436c8862ca8659094..27f42af58505e5d8e31738e3bfd8c0f485b94961 100644 --- a/gradle/repositories.gradle +++ b/gradle/repositories.gradle @@ -7,23 +7,7 @@ repositories { } maven { - url "https://code.usgs.gov/api/v4/groups/160/-/packages/maven" - name "GitLab" - if (System.getenv("CI_JOB_TOKEN") && System.getenv("CI_JOB_TOKEN") != "null") { - credentials(HttpHeaderCredentials) { - name = "Job-Token" - value = System.getenv("CI_JOB_TOKEN") - } - } else if (System.getenv("GITLAB_TOKEN") && System.getenv("GITLAB_TOKEN") != "null") { - credentials(HttpHeaderCredentials) { - name = "Private-Token" - value = System.getenv("GITLAB_TOKEN") - } - } else { - throw new GradleException("CI_JOB_TOKEN or GITLAB_TOKEN environmental variable must be set") - } - authentication { - header(HttpHeaderAuthentication) - } + url "https://code.usgs.gov/api/v4/groups/1352/-/packages/maven" + name "NSHMP GitLab Group" } } diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java index 69f25638e1f10a0db3dda70d2e0b9d8ebb6af596..bb5bc19870dadd7c7fb864fcc397ef05489106fd 100644 --- a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java +++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java @@ -4,9 +4,8 @@ import java.nio.file.Path; import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass; import gov.usgs.earthquake.nshmp.netcdf.NetcdfGroundMotions; -import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfService.RequestDataSiteClass; -import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfService.ResponseData; -import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfService.ServiceResponseMetadata; +import gov.usgs.earthquake.nshmp.netcdf.www.Metadata.ServiceResponseMetadata; +import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataSiteClass; import gov.usgs.earthquake.nshmp.www.NshmpMicronautServlet; import gov.usgs.earthquake.nshmp.www.ResponseBody; @@ -24,6 +23,7 @@ import io.micronaut.runtime.event.annotation.EventListener; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; @@ -64,36 +64,83 @@ public class NetcdfController { * @param longitude Longitude of site, in decimal degrees * @param latitude Latitude of site, in decimal degrees * @param siteClass Site class + * @param format Optional - Response return type: CSV or JSON. Default: JSON */ @Operation( summary = "Returns risk-targeted design response spectra from a slash based call", description = "### Returns a risk-targeted design response spectrum for a " + "user-specified latitude, longitude, and site class.\n" + - "Enter the latitude and longitude select the site class, and press `Execute`.\n" + + "Enter the latitude and longitude, select the site class " + + "and format (optional), and press `Execute`.\n" + - "### Service call pattern\n" + + "### Service Response Types\n" + + "The web service can respond in JSON or CSV format.\n" + + "<br><br>" + + "Choose the format type by adding a `format` query to the service call:" + + "`?format=JSON` or `?format=CSV`\n" + + "<br><br>" + + "Default: `JSON`\n\n" + + + "### Service Call Pattern\n" + "This service call is slashed delimited with pattern: " + "`/spectra/{longitude}/{latitude}/{siteClass}`\n" + "<br><br>" + - "Example: `/spectra/-118/34/A`", + "Example: `/spectra/-118/34/A`\n" + + "<br><br>" + + "CSV Example: `/spectra/-118/34/A?format=CSV`\n", operationId = "aashto-slash") @ApiResponse( description = "Spatially interpolates data from https://doi.org/10.5066/P9Z206HY", responseCode = "200", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Response.class))) - @Get(uri = "/{longitude}/{latitude}/{siteClass}", produces = MediaType.APPLICATION_JSON) - public HttpResponse<String> doGetSlashBySite( + content = { + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Response.class)), + @Content( + mediaType = MediaType.TEXT_CSV, + examples = @ExampleObject( + name = "CSV", + value = "AASHTO-2023 Web Service\n" + + "\n" + + "Longitude,-105.0,Latitude,40.0,SiteClass,BC\n" + + "Period (s),0.0,0.01,0.02,0.03,0.05,0.075, ...\n" + + "Spectral Acceleration (g),0.073,0.08,0.12,0.14,0.18, ...\n")) + }) + @Get(uri = "/{longitude}/{latitude}/{siteClass}{?format}", + produces = { MediaType.APPLICATION_JSON, MediaType.TEXT_CSV }) + public HttpResponse<String> doGetSlashBySiteFormat( HttpRequest<?> request, - @Schema(required = true) @PathVariable @Nullable Double latitude, - @Schema(required = true) @PathVariable @Nullable Double longitude, - @Schema(required = true) @PathVariable @Nullable NehrpSiteClass siteClass) { - var query = new Query(longitude, latitude, siteClass); + @Schema(required = true) @PathVariable Double latitude, + @Schema(required = true) @PathVariable Double longitude, + @Schema(required = true, + implementation = NehrpSiteClass.class) @PathVariable NehrpSiteClass siteClass, + @Schema(required = false, + defaultValue = "JSON") @QueryValue @Nullable ResponseFormat format) { + var query = new Query(longitude, latitude, siteClass, format); return service.handleServiceCall(request, query); } + /** + * GET method to return hazard curves using slash delimited. + * + * @param request The HTTP request + * @param longitude The longitude of the site + * @param latitude Latitude of the site + * @param format Optional - Response return type: CSV or JSON. Default: JSON + */ + @Hidden + @Get(uri = "/{longitude}/{latitude}{?format}", + produces = { MediaType.APPLICATION_JSON, MediaType.TEXT_CSV }) + public HttpResponse<String> doGetSlashFormat( + HttpRequest<?> request, + @Schema(required = true) @PathVariable Double longitude, + @Schema(required = true) @PathVariable Double latitude, + @Schema(required = false, + defaultValue = "JSON") @QueryValue @Nullable ResponseFormat format) { + return doGetSlashBySiteFormat(request, latitude, longitude, null, format); + } + /** * GET method to return a static curve using URL query. * @@ -101,6 +148,7 @@ public class NetcdfController { * @param longitude The longitude of the site * @param latitude Latitude of the site * @param siteClass The site class (optional) + * @param format Optional - Response return type: CSV or JSON. Default: JSON */ @Hidden @Operation( @@ -122,30 +170,15 @@ public class NetcdfController { content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Response.class))) - @Get(uri = "{?longitude,latitude,siteClass}", produces = MediaType.APPLICATION_JSON) + @Get(uri = "{?longitude,latitude,siteClass,format}", + produces = { MediaType.APPLICATION_JSON, MediaType.TEXT_CSV }) public HttpResponse<String> doGet( HttpRequest<?> request, @Schema(required = true) @QueryValue @Nullable Double latitude, @Schema(required = true) @QueryValue @Nullable Double longitude, - @QueryValue @Nullable NehrpSiteClass siteClass) { - var query = new Query(longitude, latitude, siteClass); - return service.handleServiceCall(request, query); - } - - /** - * GET method to return hazard curves using slash delimited. - * - * @param request The HTTP request - * @param longitude The longitude of the site - * @param latitude Latitude of the site - */ - @Hidden - @Get(uri = "/{longitude}/{latitude}", produces = MediaType.APPLICATION_JSON) - public HttpResponse<String> doGetSlash( - HttpRequest<?> request, - @Schema(required = true) @PathVariable @Nullable Double longitude, - @Schema(required = true) @PathVariable @Nullable Double latitude) { - var query = new Query(longitude, latitude, null); + @QueryValue @Nullable NehrpSiteClass siteClass, + @Schema(defaultValue = "JSON") @QueryValue @Nullable ResponseFormat format) { + var query = new Query(longitude, latitude, siteClass, format); return service.handleServiceCall(request, query); } diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceGroundMotions.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceGroundMotions.java index 5b1d333ca294a59f5e102c2fcd05c36afa2c50c0..e5ddcae46b8b3fa7c96fc3f7fb12ce30ecb06804 100644 --- a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceGroundMotions.java +++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceGroundMotions.java @@ -8,8 +8,11 @@ import gov.usgs.earthquake.nshmp.geo.Location; import gov.usgs.earthquake.nshmp.netcdf.NetcdfGroundMotions; import gov.usgs.earthquake.nshmp.netcdf.NetcdfVersion; import gov.usgs.earthquake.nshmp.netcdf.data.StaticData; +import gov.usgs.earthquake.nshmp.netcdf.www.Metadata.ServiceResponseMetadata; import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfWsUtils.Key; import gov.usgs.earthquake.nshmp.netcdf.www.Query.Service; +import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestData; +import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataSiteClass; import gov.usgs.earthquake.nshmp.www.ResponseBody; import gov.usgs.earthquake.nshmp.www.ResponseMetadata; import gov.usgs.earthquake.nshmp.www.WsUtils; @@ -26,7 +29,6 @@ import io.micronaut.http.HttpRequest; public class NetcdfServiceGroundMotions extends NetcdfService<Query> { static final String SERVICE_DESCRIPTION = "Get static ground motions from a NetCDF file"; - static final String SERVICE_NAME = "AASHTO-2023 Web Service"; static final String X_LABEL = "Period (s)"; static final String Y_LABEL = "Spectral Acceleration (g)"; @@ -35,12 +37,12 @@ public class NetcdfServiceGroundMotions extends NetcdfService<Query> { } @Override - ResponseBody<String, Metadata> getMetadataResponse(HttpRequest<?> request) { - var metadata = new Metadata(request, SERVICE_DESCRIPTION); + ResponseBody<String, Metadata<Query>> getMetadataResponse(HttpRequest<?> request) { + var metadata = new Metadata<Query>(request, this, SERVICE_DESCRIPTION); - return ResponseBody.<String, Metadata> usage() + return ResponseBody.<String, Metadata<Query>> usage() .metadata(new ResponseMetadata(NetcdfVersion.appVersions())) - .name(SERVICE_NAME) + .name(getServiceName()) .request(NetcdfWsUtils.getRequestUrl(request)) .response(metadata) .url(NetcdfWsUtils.getRequestUrl(request)) @@ -49,12 +51,12 @@ public class NetcdfServiceGroundMotions extends NetcdfService<Query> { @Override String getServiceName() { - return SERVICE_NAME; + return getSourceModel().name; } @Override SourceModel getSourceModel() { - return new SourceModel(); + return new SourceModel(netcdf()); } @Override @@ -63,18 +65,18 @@ public class NetcdfServiceGroundMotions extends NetcdfService<Query> { } @Override - ResponseBody<?, ?> processRequest(HttpRequest<?> httpRequest, Query query, Service service) { + String processRequest(HttpRequest<?> httpRequest, Query query, Service service) { var site = Location.create(query.longitude, query.latitude); - var requestData = new RequestData(site); + var requestData = new RequestData(site, query.format); var url = NetcdfWsUtils.getRequestUrl(httpRequest); switch (service) { case CURVES: - return processCurves(requestData, url); + return toResponseFromList(processCurves(requestData, url)); case CURVES_BY_SITE_CLASS: - requestData = new RequestDataSiteClass(site, query.siteClass); - return processCurvesSiteClass( - (RequestDataSiteClass) requestData, url); + requestData = new RequestDataSiteClass(site, query.siteClass, query.format); + return toResponse(processCurvesSiteClass( + (RequestDataSiteClass) requestData, url)); default: throw new RuntimeException("Netcdf service [" + service + "] not found"); } @@ -92,7 +94,7 @@ public class NetcdfServiceGroundMotions extends NetcdfService<Query> { return ResponseBody.<RequestDataSiteClass, ResponseData<ServiceResponseMetadata>> success() .metadata(new ResponseMetadata(NetcdfVersion.appVersions())) - .name(SERVICE_NAME) + .name(getServiceName()) .request(request) .response(responseData) .url(url) @@ -106,11 +108,11 @@ public class NetcdfServiceGroundMotions extends NetcdfService<Query> { WsUtils.checkValue(Key.LATITUDE, request.latitude); WsUtils.checkValue(Key.LONGITUDE, request.longitude); var curves = netcdf().staticData(request.site); - var responseData = toList(request.site, curves); + var responseData = toList(request, curves); return ResponseBody.<RequestData, List<ResponseData<ServiceResponseMetadata>>> success() .metadata(new ResponseMetadata(NetcdfVersion.appVersions())) - .name(SERVICE_NAME) + .name(getServiceName()) .request(request) .response(responseData) .url(url) @@ -118,11 +120,12 @@ public class NetcdfServiceGroundMotions extends NetcdfService<Query> { } List<ResponseData<ServiceResponseMetadata>> toList( - Location site, + RequestData requestData, StaticData<XySequence> curves) { return curves.entrySet().stream() .map(entry -> { - var request = new RequestDataSiteClass(site, entry.getKey()); + var request = + new RequestDataSiteClass(requestData.site, entry.getKey(), requestData.format); return toResponseData(request, entry.getValue()); }) .collect(Collectors.toList()); diff --git a/src/lib/src/main/resources/swagger/index.js b/src/aashto/src/main/resources/swagger/index.js similarity index 100% rename from src/lib/src/main/resources/swagger/index.js rename to src/aashto/src/main/resources/swagger/index.js diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/HazardQuery.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/HazardQuery.java index 3b385e560c7d3ef7516e8124ff51436465d54d3e..9d474a7b22d20bfb95a7c70dc80f40f06d931f08 100644 --- a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/HazardQuery.java +++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/HazardQuery.java @@ -6,8 +6,13 @@ import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass; public class HazardQuery extends Query { public final Imt imt; - public HazardQuery(Double longitude, Double latitude, NehrpSiteClass siteClass, Imt imt) { - super(longitude, latitude, siteClass); + public HazardQuery( + Double longitude, + Double latitude, + NehrpSiteClass siteClass, + Imt imt, + ResponseFormat format) { + super(longitude, latitude, siteClass, format); this.imt = imt; } } diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java index ea2eea3699c4ae06360ca804487ce0628ab0d26f..f14d8449a2aa35055936926c95369321a10154d7 100644 --- a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java +++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java @@ -6,10 +6,9 @@ import java.util.List; import gov.usgs.earthquake.nshmp.gmm.Imt; import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass; import gov.usgs.earthquake.nshmp.netcdf.NetcdfHazardCurves; -import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfService.RequestDataImt; -import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfService.RequestDataSiteClass; -import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfService.ResponseData; -import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfServiceHazardCurves.HazardResponseMetadata; +import gov.usgs.earthquake.nshmp.netcdf.www.Metadata.HazardResponseMetadata; +import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataImt; +import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataSiteClass; import gov.usgs.earthquake.nshmp.www.NshmpMicronautServlet; import gov.usgs.earthquake.nshmp.www.ResponseBody; @@ -26,6 +25,7 @@ import io.micronaut.http.annotation.QueryValue; import io.micronaut.runtime.event.annotation.EventListener; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; @@ -73,27 +73,53 @@ public class NetcdfController { description = "### Returns hazard curves for a " + "user-specified latitude, longitude, site class, and IMT.\n" + - "Enter the latitude and longitude select the site class and IMT, and press `Execute`.\n" + + "Enter the latitude and longitude, select the site class, " + + "IMT, and format, and press `Execute`.\n" + + + "### Service Response Types\n" + + "The web service can respond in JSON or CSV format.\n" + + "<br><br>" + + "Choose the format type by adding a `format` query to the service call:" + + "`?format=JSON` or `?format=CSV`\n" + + "<br><br>" + + "Default: `JSON`\n\n" + "### Service call pattern\n" + "This service call is slashed delimited with pattern: " + "`/hazard/{longitude}/{latitude}/{siteClass}/{imt}`\n" + "<br><br>" + - "Example: `/hazard/-118/34/BC/PGA`", + "Example: `/hazard/-118/34/BC/PGA`" + + "<br><br>" + + "CSV Example: `/hazard/-118/34/BC/PGA?format=CSV`", operationId = "hazard-by-imt") @ApiResponse( description = "Spatially interpolated hazard curves", responseCode = "200", - content = @Content( - schema = @Schema(implementation = ResponseByImt.class))) - @Get(uri = "/{longitude}/{latitude}/{siteClass}/{imt}", produces = MediaType.APPLICATION_JSON) + content = { + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ResponseByImt.class)), + @Content( + mediaType = MediaType.TEXT_CSV, + examples = @ExampleObject( + name = "CSV", + value = "Static Hazard Curves\n" + + "\n" + + "Longitude,-104.99,Latitude,39.34,SiteClass,BC,Imt,PGA\n" + + "Ground Motion (g),0.00233,0.0035,0.00524,0.00786,0.0118, ...\n" + + "Annual Frequency of Exceedence,0.036386,0.026034,0.018125,0.012197, ...\n")) + }) + @Get(uri = "/{longitude}/{latitude}/{siteClass}/{imt}{?format}", + produces = MediaType.APPLICATION_JSON) public HttpResponse<String> doGetSlashByImt( HttpRequest<?> request, - @Schema(required = true) @PathVariable @Nullable Double latitude, - @Schema(required = true) @PathVariable @Nullable Double longitude, - @Schema(required = true) @PathVariable @Nullable NehrpSiteClass siteClass, - @Schema(required = true) @PathVariable @Nullable Imt imt) { - return doGet(request, longitude, latitude, siteClass, imt); + @Schema(required = true) @PathVariable Double latitude, + @Schema(required = true) @PathVariable Double longitude, + @Schema(required = true) @PathVariable NehrpSiteClass siteClass, + @Schema(required = true) @PathVariable Imt imt, + @Schema(required = false, defaultValue = "JSON", + implementation = ResponseFormat.class) @QueryValue @Nullable ResponseFormat format) { + return doGet(request, longitude, latitude, siteClass, imt, format); } /** @@ -109,26 +135,54 @@ public class NetcdfController { description = "### Returns hazard curves for a " + "user-specified latitude, longitude, and site class.\n" + - "Enter the latitude and longitude select the site class, and press `Execute`.\n" + + "Enter the latitude and longitude, select the site class and format (optional), " + + " and press `Execute`.\n" + + + "### Service Response Types\n" + + "The web service can respond in JSON or CSV format.\n" + + "<br><br>" + + "Choose the format type by adding a `format` query to the service call:" + + "`?format=JSON` or `?format=CSV`\n" + + "<br><br>" + + "Default: `JSON`\n\n" + "### Service call pattern\n" + "This service call is slashed delimited with pattern: " + "`/hazard/{longitude}/{latitude}/{siteClass}`\n" + "<br><br>" + - "Example: `/hazard/-118/34/BC`", + "Example: `/hazard/-118/34/BC`" + + "<br><br>" + + "Example: `/hazard/-118/34/BC?format=CSV`", operationId = "hazard-by-siteclass") @ApiResponse( description = "Spatially interpolated hazard curves", responseCode = "200", - content = @Content( - schema = @Schema(implementation = ResponseBySiteClass.class))) - @Get(uri = "/{longitude}/{latitude}/{siteClass}", produces = MediaType.APPLICATION_JSON) + content = { + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ResponseBySiteClass.class)), + @Content( + mediaType = MediaType.TEXT_CSV, + examples = @ExampleObject( + name = "CSV", + value = "Static Hazard Curves\n" + + "\n" + + "Longitude,-104.99,Latitude,39.34,SiteClass,BC,Imt,PGA\n" + + "Ground Motion (g),0.00233,0.0035,0.00524,0.00786,0.0118, ...\n" + + "Annual Frequency of Exceedence,0.036386,0.026034,0.018125,0.012197, ...\n" + + "\n" + + "...")) + + }) + @Get(uri = "/{longitude}/{latitude}/{siteClass}{?format}", produces = MediaType.APPLICATION_JSON) public HttpResponse<String> doGetSlashBySite( HttpRequest<?> request, - @Schema(required = true) @PathVariable @Nullable Double latitude, - @Schema(required = true) @PathVariable @Nullable Double longitude, - @Schema(required = true) @PathVariable @Nullable NehrpSiteClass siteClass) { - return doGet(request, longitude, latitude, siteClass, null); + @Schema(required = true) @PathVariable Double latitude, + @Schema(required = true) @PathVariable Double longitude, + @Schema(required = true) @PathVariable NehrpSiteClass siteClass, + @Schema(required = false, defaultValue = "JSON", + implementation = ResponseFormat.class) @QueryValue @Nullable ResponseFormat format) { + return doGet(request, longitude, latitude, siteClass, null, format); } /** @@ -143,25 +197,52 @@ public class NetcdfController { description = "### Returns hazard curves for all site class for a " + "user-specified latitude and longitude.\n" + - "Enter the latitude and longitude and press `Execute`.\n" + + "Enter the latitude and longitude, select the format (optional), and press `Execute`.\n" + + + "### Service Response Types\n" + + "The web service can respond in JSON or CSV format.\n" + + "<br><br>" + + "Choose the format type by adding a `format` query to the service call:" + + "`?format=JSON` or `?format=CSV`\n" + + "<br><br>" + + "Default: `JSON`\n\n" + "### Service call pattern\n" + "This service call is slashed delimited with pattern: " + "`/hazard/{longitude}/{latitude}`\n" + "<br><br>" + - "Example: `/hazard/-118/34`", + "Example: `/hazard/-118/34`" + + "<br><br>" + + "Example: `/hazard/-118/34?format=CSV`", operationId = "hazard") @ApiResponse( description = "Returns static curves from the NSHM NetCDF file", responseCode = "200", - content = @Content( - schema = @Schema(implementation = Response.class))) - @Get(uri = "/{longitude}/{latitude}", produces = MediaType.APPLICATION_JSON) + content = { + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Response.class)), + @Content( + mediaType = MediaType.TEXT_CSV, + examples = @ExampleObject( + name = "CSV", + value = "Static Hazard Curves\n" + + "\n" + + "Longitude,-104.99,Latitude,39.34,SiteClass,BC,Imt,PGA\n" + + "Ground Motion (g),0.00233,0.0035,0.00524,0.00786,0.0118, ...\n" + + "Annual Frequency of Exceedence,0.036386,0.026034,0.018125,0.012197, ...\n" + + "\n" + + "...")) + + }) + @Get(uri = "/{longitude}/{latitude}{?format}", produces = MediaType.APPLICATION_JSON) public HttpResponse<String> doGetSlash( HttpRequest<?> request, - @Schema(required = true) @PathVariable @Nullable Double longitude, - @Schema(required = true) @PathVariable @Nullable Double latitude) { - return doGet(request, longitude, latitude, null, null); + @Schema(required = true) @PathVariable Double longitude, + @Schema(required = true) @PathVariable Double latitude, + @Schema(required = false, defaultValue = "JSON", + implementation = ResponseFormat.class) @QueryValue @Nullable ResponseFormat format) { + return doGet(request, longitude, latitude, null, null, format); } /** @@ -177,28 +258,56 @@ public class NetcdfController { description = "### Returns hazard curves for a " + "user-specified latitude, longitude, site class, and IMT.\n" + - "Enter the latitude and longitude select the site class (optional) and " + - "IMT (optional), and press `Execute`.\n" + + "Enter the latitude and longitude select the site class (optional) " + + "IMT (optional), and format (optional), and press `Execute`.\n" + + + "### Service Response Types\n" + + "The web service can respond in JSON or CSV format.\n" + + "<br><br>" + + "Choose the format type by adding a `format` query to the service call:" + + "`?format=JSON` or `?format=CSV`\n" + + "<br><br>" + + "Default: `JSON`\n\n" + "### Service call pattern\n" + "This service call is query based with pattern: " + - "`/hazard?longitude={number}&latitude={number}&siteClass={string}&imt={string}`\n" + + "`/hazard?longitude={number}&latitude={number}" + + "&siteClass={string}&imt={string}&format={CSV|JSON}`\n" + "<br><br>" + - "Example: `/hazard?longitude=-118&latitude=34&siteClass=A&imt=PGA`", + "Example: `/hazard?longitude=-118&latitude=34&siteClass=A&imt=PGA`" + + "<br><br>" + + "CSV Example: `/hazard?longitude=-118&latitude=34&siteClass=A&imt=PGA?format=CSV`", operationId = "hazard-by-imt") @ApiResponse( description = "Spatially interpolated hazard curves", responseCode = "200", - content = @Content( - schema = @Schema(implementation = ResponseByImt.class))) - @Get(uri = "{?longitude,latitude,siteClass,imt}", produces = MediaType.APPLICATION_JSON) + content = { + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ResponseByImt.class)), + @Content( + mediaType = MediaType.TEXT_CSV, + examples = @ExampleObject( + name = "CSV", + value = "Static Hazard Curves\n" + + "\n" + + "Longitude,-104.99,Latitude,39.34,SiteClass,BC,Imt,PGA\n" + + "Ground Motion (g),0.00233,0.0035,0.00524,0.00786,0.0118, ...\n" + + "Annual Frequency of Exceedence,0.036386,0.026034,0.018125,0.012197, ...\n" + + "\n" + + "...")) + + }) + @Get(uri = "{?longitude,latitude,siteClass,imt,format}", produces = MediaType.APPLICATION_JSON) public HttpResponse<String> doGet( HttpRequest<?> request, @Schema(required = true) @QueryValue @Nullable Double longitude, @Schema(required = true) @QueryValue @Nullable Double latitude, @QueryValue @Nullable NehrpSiteClass siteClass, - @QueryValue @Nullable Imt imt) { - var query = new HazardQuery(longitude, latitude, siteClass, imt); + @QueryValue @Nullable Imt imt, + @Schema(required = false, defaultValue = "JSON", + implementation = ResponseFormat.class) @QueryValue @Nullable ResponseFormat format) { + var query = new HazardQuery(longitude, latitude, siteClass, imt, format); return service.handleServiceCall(request, query); } diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java index 94a1d74c36e8bf2e72ffd0c828a36d61ce719fb6..831ccfc27c2c2ef6ee691fd0fe6891191bf463fe 100644 --- a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java +++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java @@ -6,13 +6,16 @@ import java.util.stream.Collectors; import gov.usgs.earthquake.nshmp.data.XySequence; import gov.usgs.earthquake.nshmp.geo.Location; import gov.usgs.earthquake.nshmp.gmm.Imt; -import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass; import gov.usgs.earthquake.nshmp.netcdf.NetcdfHazardCurves; import gov.usgs.earthquake.nshmp.netcdf.NetcdfVersion; import gov.usgs.earthquake.nshmp.netcdf.data.StaticData; import gov.usgs.earthquake.nshmp.netcdf.data.StaticDataHazardCurves; +import gov.usgs.earthquake.nshmp.netcdf.www.Metadata.HazardResponseMetadata; import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfWsUtils.Key; import gov.usgs.earthquake.nshmp.netcdf.www.Query.Service; +import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestData; +import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataImt; +import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataSiteClass; import gov.usgs.earthquake.nshmp.www.ResponseBody; import gov.usgs.earthquake.nshmp.www.ResponseMetadata; import gov.usgs.earthquake.nshmp.www.WsUtils; @@ -29,7 +32,6 @@ import io.micronaut.http.HttpRequest; public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { static final String SERVICE_DESCRIPTION = "Get static hazard curves from a NetCDF file"; - static final String SERVICE_NAME = "Static Hazard Curves"; static final String X_LABEL = "Ground Motion (g)"; static final String Y_LABEL = "Annual Frequency of Exceedence"; @@ -38,11 +40,11 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { } @Override - ResponseBody<String, Metadata> getMetadataResponse(HttpRequest<?> request) { - var metadata = new Metadata(request, SERVICE_DESCRIPTION); - return ResponseBody.<String, Metadata> usage() + ResponseBody<String, Metadata<HazardQuery>> getMetadataResponse(HttpRequest<?> request) { + var metadata = new Metadata<HazardQuery>(request, this, SERVICE_DESCRIPTION); + return ResponseBody.<String, Metadata<HazardQuery>> usage() .metadata(new ResponseMetadata(NetcdfVersion.appVersions())) - .name(SERVICE_NAME) + .name(getServiceName()) .request(NetcdfWsUtils.getRequestUrl(request)) .response(metadata) .url(NetcdfWsUtils.getRequestUrl(request)) @@ -62,7 +64,7 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { @Override String getServiceName() { - return SERVICE_NAME; + return getSourceModel().name; } @Override @@ -82,11 +84,11 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { WsUtils.checkValue(Key.LATITUDE, request.latitude); WsUtils.checkValue(Key.LONGITUDE, request.longitude); var curves = netcdf().staticData(request.site); - var curvesAsList = toList(request.site, curves); + var curvesAsList = toList(request, curves); return ResponseBody.<RequestData, List<List<ResponseData<HazardResponseMetadata>>>> success() .metadata(new ResponseMetadata(NetcdfVersion.appVersions())) - .name(SERVICE_NAME) + .name(getServiceName()) .request(request) .response(curvesAsList) .url(url) @@ -105,7 +107,7 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { return ResponseBody.<RequestDataSiteClass, List<ResponseData<HazardResponseMetadata>>> success() .metadata(new ResponseMetadata(NetcdfVersion.appVersions())) - .name(SERVICE_NAME) + .name(getServiceName()) .request(request) .response(curvesAsList) .url(url) @@ -123,7 +125,7 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { return ResponseBody.<RequestDataSiteClass, ResponseData<HazardResponseMetadata>> success() .metadata(new ResponseMetadata(NetcdfVersion.appVersions())) - .name(SERVICE_NAME) + .name(getServiceName()) .request(request) .response(toResponseData(request, request.imt, curves.get(request.imt))) .url(url) @@ -131,26 +133,43 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { } @Override - ResponseBody<?, ?> processRequest(HttpRequest<?> httpRequest, HazardQuery query, + String processRequest(HttpRequest<?> httpRequest, HazardQuery query, Service service) { var site = Location.create(query.longitude, query.latitude); var url = NetcdfWsUtils.getRequestUrl(httpRequest); switch (service) { case CURVES: - var requestData = new RequestData(site); - return processCurves(requestData, url); + var requestData = new RequestData(site, query.format); + return toResponseFromListOfList(processCurves(requestData, url)); case CURVES_BY_SITE_CLASS: - var requestDataSiteClass = new RequestDataSiteClass(site, query.siteClass); - return processCurvesSiteClass(requestDataSiteClass, url); + var requestDataSiteClass = new RequestDataSiteClass(site, query.siteClass, query.format); + return toResponseFromList(processCurvesSiteClass(requestDataSiteClass, url)); case CURVES_BY_IMT: - var requestDataImt = new RequestDataImt(site, query.siteClass, query.imt); - return processCurvesImt(requestDataImt, url); + var requestDataImt = new RequestDataImt(site, query.siteClass, query.imt, query.format); + return toResponse(processCurvesImt(requestDataImt, url)); default: throw new RuntimeException("Netcdf service [" + service + "] not found"); } } + private String toResponseFromListOfList( + ResponseBody<RequestData, List<List<ResponseData<HazardResponseMetadata>>>> serviceResponse) { + if (serviceResponse.getRequest().format == ResponseFormat.CSV) { + var csvResponse = + toCsvFromListOfList(serviceResponse.getRequest(), serviceResponse.getResponse()); + return String.format("%s\n\n%s", getServiceName(), csvResponse); + } else { + return NetcdfWsUtils.GSON.toJson(serviceResponse); + } + } + + private String toCsvFromListOfList(RequestData requestData, + List<List<ResponseData<HazardResponseMetadata>>> responseData) { + return responseData.stream().map(responses -> toCsvResponseFromList(requestData, responses)) + .collect(Collectors.joining("\n\n")); + } + List<ResponseData<HazardResponseMetadata>> toList( RequestDataSiteClass request, StaticDataHazardCurves curves) { @@ -160,11 +179,12 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { } List<List<ResponseData<HazardResponseMetadata>>> toList( - Location site, + RequestData requestData, StaticData<StaticDataHazardCurves> curves) { return curves.entrySet().stream() .map(entry -> { - var request = new RequestDataSiteClass(site, entry.getKey()); + var request = + new RequestDataSiteClass(requestData.site, entry.getKey(), requestData.format); return toList(request, entry.getValue()); }) .collect(Collectors.toList()); @@ -183,17 +203,8 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { public final List<Imt> imts; HazardSourceModel() { - super(); + super(netcdf()); imts = netcdf().netcdfData().imts().stream().sorted().collect(Collectors.toList()); } } - - static class HazardResponseMetadata extends ServiceResponseMetadata { - public final Imt imt; - - HazardResponseMetadata(NehrpSiteClass siteClass, Imt imt, String xLabel, String yLabel) { - super(siteClass, xLabel, yLabel); - this.imt = imt; - } - } } diff --git a/src/hazard/src/main/resources/swagger/index.js b/src/hazard/src/main/resources/swagger/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2a6fbfd60d9634ea5dff2ddbd01738b4e600fc60 --- /dev/null +++ b/src/hazard/src/main/resources/swagger/index.js @@ -0,0 +1,41 @@ +window.onload = function() { + let contextPath = window.location.pathname; + contextPath = contextPath.endsWith('/') ? contextPath.slice(0, -1) : contextPath; + + const ui = SwaggerUIBundle({ + defaultModelsExpandDepth: 0, + deepLinking: true, + dom_id: '#swagger-ui', + layout: 'BaseLayout', + plugins: [SwaggerUIBundle.plugins.DownloadUrl, updateContextPath(contextPath)], + presets: [SwaggerUIBundle.presets.apis], + tagsSorter: 'alpha', + tryItOutEnabled: true, + validatorUrl: null, + url: `./swagger`, + }); + + window.ui = ui; +}; + +function updateContextPath(contextPath) { + return { + statePlugins: { + spec: { + wrapActions: { + updateJsonSpec: (oriAction) => (...args) => { + const [spec] = args; + if (spec && spec.paths) { + const newPaths = {}; + Object.entries(spec.paths).forEach( + ([path, value]) => (newPaths[contextPath + path] = value) + ); + spec.paths = newPaths; + } + oriAction(...args); + }, + }, + }, + }, + }; +} diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Metadata.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Metadata.java new file mode 100644 index 0000000000000000000000000000000000000000..c228a0b1223ea92d2b75d1ed60b817ab0d4e7e53 --- /dev/null +++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Metadata.java @@ -0,0 +1,111 @@ +package gov.usgs.earthquake.nshmp.netcdf.www; + +import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass; +import gov.usgs.earthquake.nshmp.netcdf.Netcdf; +import gov.usgs.earthquake.nshmp.netcdf.data.ScienceBaseMetadata; +import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfService.SourceModel; +import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestData; +import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataImt; +import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataSiteClass; +import gov.usgs.earthquake.nshmp.netcdf.www.meta.DoubleParameter; + +import io.micronaut.http.HttpRequest; + +/** + * Web service metadata. + * + * @author U.S. Geological Survey + */ +public class Metadata<T extends Query> { + public final String description; + public final String[] syntax; + public final SourceModel model; + public final DoubleParameter longitude; + public final DoubleParameter latitude; + public final DoubleParameter vs30; + public final NetcdfMetadata netcdfMetadata; + + Metadata(HttpRequest<?> request, NetcdfService<T> netcdfService, String description) { + var netcdf = netcdfService.netcdf(); + var url = request.getUri().toString(); + url = url.endsWith("/") ? url.substring(0, url.length() - 1) : url; + this.description = description; + syntax = new String[] { + url + "/{longitude:number}/{latitude:number}", + url + "?longitude={number}&latitude={number}", + url + "/{longitude:number}/{latitude:number}/{siteClass:NehrpSiteClass}", + url + "?longitude={number}&latitude={number}&siteClass={NehrpSiteClass}", + }; + + var min = netcdf.netcdfData().minimumBounds(); + var max = netcdf.netcdfData().maximumBounds(); + + longitude = new DoubleParameter( + "Longitude", + "°", + min.longitude, + max.longitude); + latitude = new DoubleParameter( + "Latitude", + "°", + min.latitude, + max.latitude); + model = netcdfService.getSourceModel(); + vs30 = new DoubleParameter( + "Vs30", + "m/s", + 150, + 1500); + netcdfMetadata = new NetcdfMetadata(netcdf); + } + + class NetcdfMetadata { + public final String netcdfFile; + public final ScienceBaseMetadata scienceBaseMetadata; + + NetcdfMetadata(Netcdf<?> netcdf) { + var fileName = netcdf.netcdfPath().getFileName(); + netcdfFile = fileName == null ? netcdf.netcdfPath().toString() : fileName.toString(); + scienceBaseMetadata = netcdf.netcdfData().scienceBaseMetadata(); + } + } + + interface ServiceMetadata { + RequestData toRequestMetadata(RequestData requestData); + } + + static class ServiceResponseMetadata implements ServiceMetadata { + public NehrpSiteClass siteClass; + public String xLabel; + public String yLabel; + + ServiceResponseMetadata( + NehrpSiteClass siteClass, + String xLabel, + String yLabel) { + this.siteClass = siteClass; + this.xLabel = xLabel; + this.yLabel = yLabel; + } + + @Override + public RequestDataSiteClass toRequestMetadata(RequestData requestData) { + return new RequestDataSiteClass(requestData.site, siteClass, requestData.format); + } + } + + static class HazardResponseMetadata extends ServiceResponseMetadata { + public final Imt imt; + + HazardResponseMetadata(NehrpSiteClass siteClass, Imt imt, String xLabel, String yLabel) { + super(siteClass, xLabel, yLabel); + this.imt = imt; + } + + @Override + public RequestDataImt toRequestMetadata(RequestData requestData) { + return new RequestDataImt(requestData.site, siteClass, imt, requestData.format); + } + } +} diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfService.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfService.java index c6f12ebcb46b5442b27c176e4fca3979fc84bacc..243ab9418da29a3112e98a87576af188613d6c18 100644 --- a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfService.java +++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfService.java @@ -2,22 +2,24 @@ package gov.usgs.earthquake.nshmp.netcdf.www; import static gov.usgs.earthquake.nshmp.netcdf.www.NetcdfWsUtils.GSON; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.logging.Logger; +import java.util.stream.Collectors; -import gov.usgs.earthquake.nshmp.data.XySequence; -import gov.usgs.earthquake.nshmp.geo.Location; -import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.Text; +import gov.usgs.earthquake.nshmp.Text.Delimiter; import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass; import gov.usgs.earthquake.nshmp.netcdf.Netcdf; -import gov.usgs.earthquake.nshmp.netcdf.data.ScienceBaseMetadata; +import gov.usgs.earthquake.nshmp.netcdf.www.Metadata.ServiceResponseMetadata; import gov.usgs.earthquake.nshmp.netcdf.www.Query.Service; -import gov.usgs.earthquake.nshmp.netcdf.www.meta.DoubleParameter; +import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestData; +import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataSiteClass; import gov.usgs.earthquake.nshmp.www.ResponseBody; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; -import io.swagger.v3.oas.annotations.media.Schema; /** * Abstract service handler for {@code NetcdfController}. @@ -41,7 +43,7 @@ public abstract class NetcdfService<T extends Query> { * * @param httpRequest The HTTP request */ - abstract ResponseBody<String, Metadata> getMetadataResponse(HttpRequest<?> httpRequest); + abstract ResponseBody<String, Metadata<T>> getMetadataResponse(HttpRequest<?> httpRequest); /** * Returns the service name @@ -79,13 +81,13 @@ public abstract class NetcdfService<T extends Query> { String url); /** - * Process the service request and returns the reponse. + * Process the service request and returns the string response. * * @param httpRequest The HTTP request * @param query The HTTP query * @param service The NetCDF service */ - abstract ResponseBody<?, ?> processRequest( + abstract String processRequest( HttpRequest<?> httpRequest, T query, Service service); @@ -108,8 +110,7 @@ public abstract class NetcdfService<T extends Query> { return metadata(httpRequest); } var service = getService(query); - var svcResponse = processRequest(httpRequest, query, service); - var response = GSON.toJson(svcResponse); + var response = processRequest(httpRequest, query, service); LOGGER.fine("Result:\n" + response); return HttpResponse.ok(response); } catch (Exception e) { @@ -126,143 +127,78 @@ public abstract class NetcdfService<T extends Query> { } } - private HttpResponse<String> metadata(HttpRequest<?> httpRequest) { - var svcResponse = getMetadataResponse(httpRequest); - var response = GSON.toJson(svcResponse); - LOGGER.fine("Result:\n" + response); - return HttpResponse.ok(response); + <U extends ServiceResponseMetadata> String toCsvResponse( + RequestData requestData, + ResponseData<U> responseData) { + return toCsv(requestData, responseData); } - class Metadata { - public final String description; - public final String[] syntax; - public final SourceModel model; - public final DoubleParameter longitude; - public final DoubleParameter latitude; - public final DoubleParameter vs30; - public final NetcdfMetadata netcdfMetadata; + <U extends ServiceResponseMetadata> String toCsvResponseFromList( + RequestData requestData, + List<ResponseData<U>> responseData) { + var response = responseData.stream().map(data -> { + return toCsv(requestData, data); + }).collect(Collectors.joining("\n\n")); - Metadata(HttpRequest<?> request, String description) { - var url = request.getUri().toString(); - url = url.endsWith("/") ? url.substring(0, url.length() - 1) : url; - this.description = description; - syntax = new String[] { - url + "/{longitude:number}/{latitude:number}", - url + "?longitude={number}&latitude={number}", - url + "/{longitude:number}/{latitude:number}/{siteClass:NehrpSiteClass}", - url + "?longitude={number}&latitude={number}&siteClass={NehrpSiteClass}", - }; - - var min = netcdf().netcdfData().minimumBounds(); - var max = netcdf.netcdfData().maximumBounds(); - - longitude = new DoubleParameter( - "Longitude", - "°", - min.longitude, - max.longitude); - latitude = new DoubleParameter( - "Latitude", - "°", - min.latitude, - max.latitude); - model = getSourceModel(); - vs30 = new DoubleParameter( - "Vs30", - "m/s", - 150, - 1500); - netcdfMetadata = new NetcdfMetadata(); - } + return response; } - class NetcdfMetadata { - public final String netcdfFile; - public final ScienceBaseMetadata scienceBaseMetadata; - - NetcdfMetadata() { - var fileName = netcdf.netcdfPath().getFileName(); - netcdfFile = fileName == null ? netcdf().netcdfPath().toString() : fileName.toString(); - scienceBaseMetadata = netcdf().netcdfData().scienceBaseMetadata(); + <U extends RequestData, V extends ServiceResponseMetadata> String toResponse( + ResponseBody<U, ResponseData<V>> serviceResponse) { + if (serviceResponse.getRequest().format == ResponseFormat.CSV) { + var csvResponse = toCsvResponse(serviceResponse.getRequest(), serviceResponse.getResponse()); + return String.format("%s\n\n%s", getServiceName(), csvResponse); + } else { + return NetcdfWsUtils.GSON.toJson(serviceResponse); } } - class SourceModel { - public final String name; - public final Map<NehrpSiteClass, Double> siteClasses; - - SourceModel() { - name = netcdf().netcdfData().scienceBaseMetadata().label; - siteClasses = netcdf().netcdfData().vs30Map(); + <U extends RequestData, V extends ServiceResponseMetadata> String toResponseFromList( + ResponseBody<U, List<ResponseData<V>>> serviceResponse) { + if (serviceResponse.getRequest().format == ResponseFormat.CSV) { + var csvResponse = + toCsvResponseFromList(serviceResponse.getRequest(), serviceResponse.getResponse()); + return String.format("%s\n\n%s", getServiceName(), csvResponse); + } else { + return NetcdfWsUtils.GSON.toJson(serviceResponse); } } - static class ServiceResponseMetadata { - public NehrpSiteClass siteClass; - public String xLabel; - public String yLabel; - - ServiceResponseMetadata( - NehrpSiteClass siteClass, - String xLabel, - String yLabel) { - this.siteClass = siteClass; - this.xLabel = xLabel; - this.yLabel = yLabel; - } + private HttpResponse<String> metadata(HttpRequest<?> httpRequest) { + var svcResponse = getMetadataResponse(httpRequest); + var response = GSON.toJson(svcResponse); + LOGGER.fine("Result:\n" + response); + return HttpResponse.ok(response); } - static class ResponseData<T extends ServiceResponseMetadata> { - final T metadata; - final XySequence data; + private <U extends RequestData, V extends ServiceResponseMetadata> String toCsv( + U requestData, + ResponseData<V> responseData) { + var request = responseData.metadata.toRequestMetadata(requestData); - ResponseData(T metadata, XySequence data) { - this.metadata = metadata; - this.data = data; - } + List<Object> xs = new ArrayList<>(); + xs.add(responseData.metadata.xLabel); + xs.addAll(responseData.data.xValues().boxed().collect(Collectors.toList())); - public T getMetadata() { - return metadata; - } + List<Object> ys = new ArrayList<>(); + ys.add(responseData.metadata.yLabel); + ys.addAll(responseData.data.yValues().boxed().collect(Collectors.toList())); - @Schema(implementation = XySequenceSchema.class) - public XySequence getData() { - return data; - } - } + var lines = new ArrayList<String>(); + lines.add(request.toCsv()); + lines.add(Text.join(xs, Delimiter.COMMA)); + lines.add(Text.join(ys, Delimiter.COMMA)); - static class RequestData { - public Double latitude; - public Double longitude; - public transient Location site; - - RequestData(Location site) { - latitude = site.latitude; - longitude = site.longitude; - this.site = site; - } + return lines.stream().collect(Collectors.joining(Text.NEWLINE)); } - static class RequestDataImt extends RequestDataSiteClass { - public Imt imt; - - RequestDataImt(Location site, NehrpSiteClass siteClass, Imt imt) { - super(site, siteClass); - this.imt = imt; - } - } - - static class RequestDataSiteClass extends RequestData { - public NehrpSiteClass siteClass; + static class SourceModel { + public final String name; + public final Map<NehrpSiteClass, Double> siteClasses; - RequestDataSiteClass(Location site, NehrpSiteClass siteClass) { - super(site); - this.siteClass = siteClass; + SourceModel(Netcdf<?> netcdf) { + name = netcdf.netcdfData().scienceBaseMetadata().label; + siteClasses = netcdf.netcdfData().vs30Map(); } } - - private static class XySequenceSchema { - public double[] xs; - public double[] ys; - } } diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfWsUtils.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfWsUtils.java index 70216205cdd87b608250c53141bdd8bb479d4429..c4233ad557e292cf3b4a1fd20eb1a9790c8ac8fb 100644 --- a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfWsUtils.java +++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfWsUtils.java @@ -86,6 +86,7 @@ public class NetcdfWsUtils { // Update description var description = new StringBuilder() .append(scienceBaseMetadata.description + "\n") + .append(swaggerResponseFormatSection()) .append(serviceSection) .append(swaggerParameterSection(netcdfData)) .append(swaggerScienceBaseSection(scienceBaseMetadata)) @@ -95,7 +96,7 @@ public class NetcdfWsUtils { return openApi; } - static enum Key { + public static enum Key { LATITUDE, LONGITUDE, SITE_CLASS, @@ -120,6 +121,22 @@ public class NetcdfWsUtils { .toString(); } + private static String swaggerResponseFormatSection() { + return new StringBuilder() + .append( + "<details>\n" + + "<summary>Response Format: CSV or JSON</summary>\n") + .append( + "The web service can respond in JSON or CSV format.\n" + + "<br><br>" + + "Choose the format type by adding a `format` query to the service call:" + + "`?format=JSON` or `?format=CSV`\n\n" + + "<br><br>" + + "Default: `JSON`") + .append("</details>") + .toString(); + } + private static String swaggerScienceBaseSection(ScienceBaseMetadata scienceBaseMetadata) { return new StringBuilder() .append( diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Query.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Query.java index 0df439675424a85db25094b81722a6f75e57091f..de913518cdf6931006f97598209f3d2b4d359926 100644 --- a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Query.java +++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Query.java @@ -6,11 +6,13 @@ public class Query { public final Double longitude; public final Double latitude; public final NehrpSiteClass siteClass; + public final ResponseFormat format; - public Query(Double longitude, Double latitude, NehrpSiteClass siteClass) { + public Query(Double longitude, Double latitude, NehrpSiteClass siteClass, ResponseFormat format) { this.longitude = longitude; this.latitude = latitude; this.siteClass = siteClass; + this.format = format == null ? ResponseFormat.JSON : format; } public static enum Service { diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Request.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Request.java new file mode 100644 index 0000000000000000000000000000000000000000..eb193cbb2c40d17be105336c9a1f8610b185b5cc --- /dev/null +++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Request.java @@ -0,0 +1,85 @@ +package gov.usgs.earthquake.nshmp.netcdf.www; + +import java.util.List; + +import gov.usgs.earthquake.nshmp.Text; +import gov.usgs.earthquake.nshmp.Text.Delimiter; +import gov.usgs.earthquake.nshmp.geo.Location; +import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass; +import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfWsUtils.Key; + +/** + * Web service request data. + * + * @author U.S. Geological Survey + */ +public class Request { + + interface RequestDataToCsv { + /** + * Convert the request data into a CSV line. + */ + String toCsv(); + } + + /** + * Basic request data with latitude, longitude, and format. + */ + static class RequestData implements RequestDataToCsv { + public Double latitude; + public Double longitude; + public ResponseFormat format; + public transient Location site; + + RequestData(Location site, ResponseFormat format) { + latitude = site.latitude; + longitude = site.longitude; + this.format = format; + this.site = site; + } + + public String toCsv() { + return Text.join( + List.of(Key.LONGITUDE.toString(), longitude, Key.LATITUDE.toString(), latitude), + Delimiter.COMMA); + } + } + + /** + * Request data with site class and imt + */ + static class RequestDataImt extends RequestDataSiteClass { + public Imt imt; + + RequestDataImt(Location site, NehrpSiteClass siteClass, Imt imt, ResponseFormat format) { + super(site, siteClass, format); + this.imt = imt; + } + + @Override + public String toCsv() { + return String.format("%s,%s", + super.toCsv(), Text.join(List.of(Key.IMT.toString(), imt.name()), Delimiter.COMMA)); + } + } + + /** + * Request data with site class + */ + static class RequestDataSiteClass extends RequestData { + public NehrpSiteClass siteClass; + + RequestDataSiteClass(Location site, NehrpSiteClass siteClass, ResponseFormat format) { + super(site, format); + this.siteClass = siteClass; + } + + @Override + public String toCsv() { + return String.format("%s,%s", + super.toCsv(), Text.join(List.of(Key.SITE_CLASS.toString(), siteClass), Delimiter.COMMA)); + } + } + +} diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/ResponseData.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/ResponseData.java new file mode 100644 index 0000000000000000000000000000000000000000..431e0f630ebc531fa5180dbfdf0d2d0a1464c6a4 --- /dev/null +++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/ResponseData.java @@ -0,0 +1,35 @@ +package gov.usgs.earthquake.nshmp.netcdf.www; + +import gov.usgs.earthquake.nshmp.data.XySequence; +import gov.usgs.earthquake.nshmp.netcdf.www.Metadata.ServiceResponseMetadata; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * Web service response data. + * + * @author U.S. Geological Survey + */ +public class ResponseData<T extends ServiceResponseMetadata> { + final T metadata; + final XySequence data; + + ResponseData(T metadata, XySequence data) { + this.metadata = metadata; + this.data = data; + } + + public T getMetadata() { + return metadata; + } + + @Schema(implementation = XySequenceSchema.class) + public XySequence getData() { + return data; + } + + private static class XySequenceSchema { + public double[] xs; + public double[] ys; + } +} diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/ResponseFormat.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/ResponseFormat.java new file mode 100644 index 0000000000000000000000000000000000000000..f1ca6ea50ab4380a905d5675006afab64ce08422 --- /dev/null +++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/ResponseFormat.java @@ -0,0 +1,11 @@ +package gov.usgs.earthquake.nshmp.netcdf.www; + +/** + * Web service return format. + * + * @author U.S. Geological Survey + */ +public enum ResponseFormat { + JSON, + CSV; +} diff --git a/src/lib/src/main/resources/application.yml b/src/lib/src/main/resources/application.yml index 448a7d535a1b583240dcdbd0dacac534c08ad192..0c5312ed4339fd496c60809e06df9d15483ed83a 100644 --- a/src/lib/src/main/resources/application.yml +++ b/src/lib/src/main/resources/application.yml @@ -9,9 +9,3 @@ micronaut: enabled: true paths: classpath:swagger mapping: /** - -nshmp-ws-static: - # Hazard example - netcdf-file: ${netcdf:src/main/resources/hazard-example.nc} - # Ground motions example - # netcdf-file: ${netcdf:src/main/resources/rtsa-example.nc} diff --git a/src/lib/src/main/resources/swagger/index.css b/src/lib/src/main/resources/swagger/index.css index b514b935545bc9401d16f678674db2c91e44f4bb..cef94f5156369b9dac439a652b9c52bd0ce673b5 100644 --- a/src/lib/src/main/resources/swagger/index.css +++ b/src/lib/src/main/resources/swagger/index.css @@ -117,6 +117,10 @@ details { box-shadow: 0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f; } +details:first-of-type { + margin-top: 1.5em; +} + details:hover { background-color: #f3f3f3; }