diff --git a/Dockerfile b/Dockerfile index a528c9d6e793fc6f7c31ddc68391cb8e243c5b3b..2912436f2bd8f35716d1c550a43061d7a811a829 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,8 @@ ARG project=nshmp-haz-v2 ARG builder_workdir=/app/${project} ARG libs_dir=${builder_workdir}/build/libs +ARG jar_file=${libs_dir}/${project}.jar +ARG ws_file=${libs_dir}/${project}-ws.jar #### # Builder image: Build jar and war file. @@ -31,6 +33,8 @@ FROM usgs/centos:8 as builder ENV LANG="en_US.UTF-8" ARG builder_workdir +ARG libs_dir +ARG ws_file WORKDIR ${builder_workdir} @@ -39,7 +43,8 @@ COPY . . RUN yum install -y java-11-openjdk-devel which git RUN mv nshmp-lib ../. \ - && ./gradlew --no-daemon assemble + && ./gradlew --no-daemon assemble \ + && mv ${libs_dir}/*-all.jar ${ws_file} #### # Application image: Run jar or war file. @@ -51,13 +56,13 @@ LABEL maintainer="Peter Powers <pmpowers@usgs.gov>, Brandon Clayton <bclayton@us ENV LANG="en_US.UTF-8" ARG libs_dir +ARG ws_file ARG builder_workdir ARG project -ARG TOMCAT_MAJOR=8 -ARG TOMCAT_VERSION=${TOMCAT_MAJOR}.5.40 ENV PROJECT ${project} -ENV JAVA_XMX 8g +ENV CONTEXT_PATH "/" +ENV MODEL_PATH /app/models # Whether to run hazard jar file or web services war file ENV RUN_HAZARD true @@ -72,30 +77,13 @@ ENV IML "" ENV CONFIG_FILE "config.json" VOLUME [ "/app/output" ] -# Tomcat -ENV CONTEXT_PATH "" -ENV CATALINA_HOME /usr/local/tomcat -ENV TOMCAT_WEBAPPS ${CATALINA_HOME}/webapps -ENV PATH ${CATALINA_HOME}/bin:${PATH} -ENV TOMCAT_SOURCE http://archive.apache.org/dist/tomcat -ENV TOMCAT_URL ${TOMCAT_SOURCE}/tomcat-${TOMCAT_MAJOR}/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.tar.gz -ENV JAVA_OPTS -Xmx${JAVA_XMX} - -ENV WS_HOME ${CATALINA_HOME} -ENV HAZ_HOME /app - -WORKDIR ${HAZ_HOME} +WORKDIR /app COPY --from=builder ${libs_dir}/* ./ COPY docker-entrypoint.sh . -WORKDIR ${WS_HOME} - RUN yum update -y \ - && yum install -y file jq zip java-11-openjdk-headless \ - && curl -L ${TOMCAT_URL} | tar -xz --strip-components=1 - -WORKDIR ${HAZ_HOME} + && yum install -y file jq zip java-11-openjdk-headless EXPOSE 8080 ENTRYPOINT [ "bash", "docker-entrypoint.sh" ] diff --git a/build.gradle b/build.gradle index c18eead82cc949c081022f3f234040b14665f256..4a89efb7cfc6168eb289f0580c5f2e3532e1151c 100644 --- a/build.gradle +++ b/build.gradle @@ -63,10 +63,6 @@ repositories { dependencies { implementation project(":nshmp-lib") - // Tomcat - implementation "org.apache.tomcat:tomcat-catalina:8.0.45" - implementation "javax.websocket:javax.websocket-api:1.1" - // AWS implementation "com.amazonaws:aws-lambda-java-core:${awsLambdaCoreVersion}" implementation "com.amazonaws:aws-java-sdk-lambda:${awsLambdaVersion}" diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index b114c559ce870c7b7dd4250283bc7d78aab6bd89..45ee388fb68a2ea88c2edd5210d07267ab16e692 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -107,7 +107,7 @@ run_hazard() { # Run nshmp-haz java -Xms${JAVA_XMS} -Xmx${JAVA_XMX} \ - -cp ${HAZ_HOME}/${PROJECT}.jar \ + -cp /app/${PROJECT}.jar \ gov.usgs.earthquake.nshmp.${nshmp_program} \ "${nshmp_model_path}" \ "${site_file}" \ @@ -130,9 +130,15 @@ run_hazard() { # None #### run_ws() { - unpack_war; get_ws_models; - catalina.sh run 2>&1; + + if [ -z ${CONTEXT_PATH} ]; then + CONTEXT_PATH="/nshmp/${MODEL}"; + fi + + java -jar ${PROJECT}-ws.jar \ + "-Dmicronaut.server.context-path=${CONTEXT_PATH}" \ + -model=/app/models } #### @@ -510,34 +516,6 @@ move_to_output_volume() { cp -r ${hazout}/* output/. 2> ${LOG_FILE}; } -#### -# Unpack war file into webapps/<CONTEXT_PATH>. -# Globals: -# (string) CONTEXT_PATH - The context path for the web services -# (string) LOG_FILE - The log file -# (string) MODEL - The model to use -# (string) PROJECT - The project name -# (string) TOMCAT_WEBAPPS - The Tomcat webapps dir -# Arguments: -# None -# Returns: -# None -#### -unpack_war() { - cd ${TOMCAT_WEBAPPS} 2> ${LOG_FILE}; - - if [ -z ${CONTEXT_PATH} ]; then - CONTEXT_PATH="nshmp/${MODEL}"; - fi - - CONTEXT_PATH=$(echo ${CONTEXT_PATH//\//#} | awk {'print tolower($0)'}) 2> ${LOG_FILE}; - mkdir ${CONTEXT_PATH} 2> ${LOG_FILE}; - cd ${CONTEXT_PATH} 2> ${LOG_FILE}; - cp ${HAZ_HOME}/${PROJECT}.war . 2> ${LOG_FILE}; - unzip ${PROJECT}.war 2> ${LOG_FILE} &> /dev/null; - rm ${PROJECT}.war 2> ${LOG_FILE}; -} - #### # Run main #### diff --git a/scripts/nshmp-haz.yml b/scripts/nshmp-haz.yml index 24a997ef5fc1dd49bcf57f08a260685a2fb9c697..e2282207a31365ef41e93959bf8898186a094a00 100644 --- a/scripts/nshmp-haz.yml +++ b/scripts/nshmp-haz.yml @@ -23,7 +23,7 @@ services: environment: RUN_HAZARD: 'false' MODEL: CONUS-2018 - CONTEXT_PATH: nshmp/conus-2018 + CONTEXT_PATH: /nshmp/conus-2018 # # Deploy nshmp-haz with CONUS-2014 # nshmp-haz-conus-2014: diff --git a/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultSliceLambda.java b/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultSliceLambda.java index 64ab2cb6625785d0c2124447856b1c6d2c19c058..813b121651f06815913be42513002691d97ad99d 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultSliceLambda.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultSliceLambda.java @@ -3,7 +3,7 @@ package gov.usgs.earthquake.nshmp.aws; import static com.google.common.base.Preconditions.checkState; import static gov.usgs.earthquake.nshmp.aws.Util.CURVES_FILE; import static gov.usgs.earthquake.nshmp.aws.Util.MAP_FILE; -import static gov.usgs.earthquake.nshmp.www.ServletUtil.GSON; +import static gov.usgs.earthquake.nshmp.www.services.ServletUtil.GSON; import java.io.BufferedReader; import java.io.ByteArrayInputStream; @@ -37,8 +37,8 @@ import gov.usgs.earthquake.nshmp.data.Interpolator; import gov.usgs.earthquake.nshmp.internal.Parsing; import gov.usgs.earthquake.nshmp.internal.Parsing.Delimiter; import gov.usgs.earthquake.nshmp.internal.www.meta.Status; -import gov.usgs.earthquake.nshmp.www.ServletUtil; import gov.usgs.earthquake.nshmp.www.meta.Metadata; +import gov.usgs.earthquake.nshmp.www.services.ServletUtil; /** * AWS Lambda function to read in a curves file from AWS S3 and create slices at diff --git a/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsMetadataLambda.java b/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsMetadataLambda.java index e6738a36e29958979f61fe2212a450c1766e816b..d470a01809d33cdabae28a1959075c318eb60cb4 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsMetadataLambda.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsMetadataLambda.java @@ -2,7 +2,7 @@ package gov.usgs.earthquake.nshmp.aws; import static gov.usgs.earthquake.nshmp.aws.Util.CURVES_FILE; import static gov.usgs.earthquake.nshmp.aws.Util.MAP_FILE; -import static gov.usgs.earthquake.nshmp.www.ServletUtil.GSON; +import static gov.usgs.earthquake.nshmp.www.services.ServletUtil.GSON; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -36,8 +36,8 @@ import gov.usgs.earthquake.nshmp.gmm.Imt; import gov.usgs.earthquake.nshmp.internal.Parsing; import gov.usgs.earthquake.nshmp.internal.Parsing.Delimiter; import gov.usgs.earthquake.nshmp.internal.www.meta.Status; -import gov.usgs.earthquake.nshmp.www.ServletUtil; import gov.usgs.earthquake.nshmp.www.meta.Metadata; +import gov.usgs.earthquake.nshmp.www.services.ServletUtil; /** * AWS Lambda function to list all hazard results in the nshmp-hazout S3 bucket diff --git a/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsSlicerLambda.java b/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsSlicerLambda.java index 9480ce954b4ce4d975d81783b9573a76268ea060..8f6f6c6df6358d96c06997980c5d0c4fb0cbe0af 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsSlicerLambda.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsSlicerLambda.java @@ -1,7 +1,7 @@ package gov.usgs.earthquake.nshmp.aws; import static gov.usgs.earthquake.nshmp.aws.Util.CURVES_FILE; -import static gov.usgs.earthquake.nshmp.www.ServletUtil.GSON; +import static gov.usgs.earthquake.nshmp.www.services.ServletUtil.GSON; import java.io.IOException; import java.io.InputStream; @@ -34,8 +34,8 @@ import gov.usgs.earthquake.nshmp.aws.Util.LambdaHelper; import gov.usgs.earthquake.nshmp.internal.Parsing; import gov.usgs.earthquake.nshmp.internal.Parsing.Delimiter; import gov.usgs.earthquake.nshmp.internal.www.meta.Status; -import gov.usgs.earthquake.nshmp.www.ServletUtil; import gov.usgs.earthquake.nshmp.www.meta.Metadata; +import gov.usgs.earthquake.nshmp.www.services.ServletUtil; /** * AWS Lambda function to read in hazard results from S3 and to create slices of diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/DeaggEpsilonController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/DeaggEpsilonController.java new file mode 100644 index 0000000000000000000000000000000000000000..df7472f331b7c56e392bd60ce07b0a22d8774c21 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/DeaggEpsilonController.java @@ -0,0 +1,118 @@ +package gov.usgs.earthquake.nshmp.www; + +import java.util.EnumMap; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.internal.www.NshmpMicronautServlet; +import gov.usgs.earthquake.nshmp.www.services.DeaggEpsilonService; +import gov.usgs.earthquake.nshmp.www.services.DeaggEpsilonService.Query; +import gov.usgs.earthquake.nshmp.www.services.HazardService; + +import io.micronaut.context.event.StartupEvent; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.PathVariable; +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.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Epsilon Deaggregation Service (experimental)") +@Controller("/deagg-epsilon") +public class DeaggEpsilonController { + + @Inject + private NshmpMicronautServlet servlet; + + @EventListener + public void init(StartupEvent event) { + DeaggEpsilonService.init(); + } + + @Get(uri = "/usage", produces = MediaType.APPLICATION_JSON) + public HttpResponse<String> doGetUsage(HttpRequest<?> request) { + var urlHelper = servlet.urlHelper(request); + return HazardService.handleDoGetUsage(urlHelper); + } + + /** + * GET method to return usage or hazard curves, query based. + * + * @param request The HTTP request + * @param longitude Longitude (in decimal degrees) ([-360, 360]) + * @param latitude Latitude (in decimal degrees) ([-90, 90]) + * @param vs30 The site soil class + * @param basin Whether to use basin service + */ + @Operation( + summary = "Compute epsilon deaggregation", + description = "Compute epsilon deaggregation given longitude, latitude, Vs30 and IMT-IML map", + operationId = "deaggEpsilon_doGetDeaggEpsilon") + @ApiResponse( + description = "Epsilon deaggregations", + responseCode = "200") + @Get(uri = "{?longitude,latitude,vs30,basin}") + public HttpResponse<String> doGetDeaggEpsilon( + HttpRequest<?> request, + @Schema( + required = true, + minimum = "-360", + maximum = "360") @QueryValue @Nullable Double longitude, + @Schema( + required = true, + minimum = "-90", + maximum = "90") @QueryValue @Nullable Double latitude, + @Schema(required = true) @QueryValue @Nullable Integer vs30, + @Schema(defaultValue = "false") @QueryValue @Nullable Boolean basin, + @Schema( + defaultValue = "{\"SA0P01\": 0.01}", + required = true) @QueryValue @Nullable EnumMap<Imt, Double> imtImls) { + var urlHelper = servlet.urlHelper(request); + var query = new Query(request, longitude, latitude, vs30, basin); + return DeaggEpsilonService.handleDoGetDeaggEpsilon(query, urlHelper); + } + + /** + * GET method to return usage or hazard curves, slash based. + * + * @param request The HTTP request + * @param longitude Longitude (in decimal degrees) ([-360, 360]) + * @param latitude Latitude (in decimal degrees) ([-90, 90]) + * @param vs30 The site soil class + * @param basin Whether to use basin service + */ + @Operation( + summary = "Compute epsilon deaggregation", + description = "Compute epsilon deaggregation given longitude, latitude, Vs30 and IMT-IML map", + operationId = "deaggEpsilon_doGetDeaggEpsilonSlash") + @ApiResponse( + description = "Epsilon deaggregations", + responseCode = "200") + @Get(uri = "{/longitude}{/latitude}{/vs30}{/basin}") + public HttpResponse<String> doGetDeaggEpsilonSlash( + HttpRequest<?> request, + @Schema( + required = true, + minimum = "-360", + maximum = "360") @PathVariable @Nullable Double longitude, + @Schema( + required = true, + minimum = "-90", + maximum = "90") @PathVariable @Nullable Double latitude, + @Schema(required = true) @PathVariable @Nullable Integer vs30, + @Schema(defaultValue = "false") @PathVariable @Nullable Boolean basin, + @Schema( + defaultValue = "{\"SA0P01\": 0.01}", + required = true) @QueryValue @Nullable EnumMap<Imt, Double> imtImls) { + return doGetDeaggEpsilon(request, longitude, latitude, vs30, basin, null); + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/DeaggEpsilonService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/DeaggEpsilonService.java deleted file mode 100644 index ae17b6301861e795ec88d6aeb76fd37cfdf262ed..0000000000000000000000000000000000000000 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/DeaggEpsilonService.java +++ /dev/null @@ -1,355 +0,0 @@ -package gov.usgs.earthquake.nshmp.www; - -import static com.google.common.base.Preconditions.checkNotNull; -import static gov.usgs.earthquake.nshmp.www.ServletUtil.GSON; -import static gov.usgs.earthquake.nshmp.www.ServletUtil.emptyRequest; -import static gov.usgs.earthquake.nshmp.www.WsUtil.Key.BASIN; -import static gov.usgs.earthquake.nshmp.www.WsUtil.Key.LATITUDE; -import static gov.usgs.earthquake.nshmp.www.WsUtil.Key.LONGITUDE; -import static gov.usgs.earthquake.nshmp.www.WsUtil.Key.MODEL; -import static gov.usgs.earthquake.nshmp.www.WsUtil.Key.VS30; -import static gov.usgs.earthquake.nshmp.www.WsUtil.readBoolean; -import static gov.usgs.earthquake.nshmp.www.WsUtil.readDouble; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.time.ZonedDateTime; -import java.util.Arrays; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Properties; -import java.util.Set; - -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.google.common.cache.LoadingCache; -import com.google.common.collect.ImmutableList; - -import gov.usgs.earthquake.nshmp.calc.CalcConfig; -import gov.usgs.earthquake.nshmp.calc.Deaggregation; -import gov.usgs.earthquake.nshmp.calc.Hazard; -import gov.usgs.earthquake.nshmp.calc.HazardCalcs; -import gov.usgs.earthquake.nshmp.calc.Site; -import gov.usgs.earthquake.nshmp.eq.model.HazardModel; -import gov.usgs.earthquake.nshmp.geo.Location; -import gov.usgs.earthquake.nshmp.gmm.Imt; -import gov.usgs.earthquake.nshmp.internal.www.meta.Status; -import gov.usgs.earthquake.nshmp.www.ServletUtil.TimedTaskContext; -import gov.usgs.earthquake.nshmp.www.ServletUtil.Timer; -import gov.usgs.earthquake.nshmp.www.meta.Metadata; -import gov.usgs.earthquake.nshmp.www.services.SourceServices; - -/** - * Hazard deaggregation service. - * - * @author U.S. Geological Survey - */ -@SuppressWarnings("unused") -@WebServlet( - name = "Epsilon Deaggregation Service (experimental)", - description = "USGS NSHMP Hazard Deaggregator", - urlPatterns = { "/deagg-epsilon" }) -public final class DeaggEpsilonService extends NshmpServlet { - - /* Developer notes: See HazardService. */ - - private LoadingCache<Model, HazardModel> modelCache; - private URL basinUrl; - - private static final String USAGE = SourceServices.GSON.toJson( - new SourceServices.ResponseData()); - - @Override - @SuppressWarnings("unchecked") - public void init() throws ServletException { - - ServletContext context = getServletConfig().getServletContext(); - Object modelCache = context.getAttribute(""); - this.modelCache = (LoadingCache<Model, HazardModel>) modelCache; - - try (InputStream config = - DeaggEpsilonService.class.getResourceAsStream("/config.properties")) { - - checkNotNull(config, "Missing config.properties"); - - Properties props = new Properties(); - props.load(config); - if (props.containsKey("basin_host")) { - /* - * TODO Site builder tests if service is working, which may be - * inefficient for single call services. - */ - URL url = new URL(props.getProperty("basin_host") + "/nshmp-site-ws/basin/local-data"); - this.basinUrl = url; - } - } catch (IOException | NullPointerException e) { - throw new ServletException(e); - } - } - - @Override - protected void doGet( - HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException { - - UrlHelper urlHelper = urlHelper(request, response); - - if (emptyRequest(request)) { - urlHelper.writeResponse(USAGE); - return; - } - - try { - RequestData requestData = buildRequestData(request); - - /* Submit as task to job executor */ - Deagg2Task task = new Deagg2Task(urlHelper.url, getServletContext(), requestData); - Result result = ServletUtil.TASK_EXECUTOR.submit(task).get(); - GSON.toJson(result, response.getWriter()); - - } catch (Exception e) { - String message = Metadata.errorMessage(urlHelper.url, e, false); - response.getWriter().print(message); - getServletContext().log(urlHelper.url, e); - } - } - - /* Reduce query string key-value pairs. */ - static RequestData buildRequestData(HttpServletRequest request) { - - try { - - /* process query '?' request */ - List<Model> models = readModelsFromQuery(request); - double lon = readDouble(LONGITUDE, request); - double lat = readDouble(LATITUDE, request); - Map<Imt, Double> imtImls = readImtsFromQuery(request); - double vs30 = readDouble(VS30, request); - boolean basin = readBoolean(BASIN, request); - - return new RequestData( - models, - lon, - lat, - imtImls, - vs30, - basin); - - } catch (Exception e) { - throw new IllegalArgumentException("Error parsing request URL", e); - } - } - - private static List<Model> readModelsFromQuery(HttpServletRequest request) { - String[] ids = WsUtil.readValues(MODEL, request); - return Arrays.stream(ids) - .map(Model::valueOf) - .distinct() - .collect(ImmutableList.toImmutableList()); - } - - /* Create map of IMT to deagg IML. */ - private static Map<Imt, Double> readImtsFromQuery(HttpServletRequest request) { - EnumMap<Imt, Double> imtImls = new EnumMap<>(Imt.class); - for (Entry<String, String[]> param : request.getParameterMap().entrySet()) { - if (isImtParam(param.getKey())) { - imtImls.put( - Imt.valueOf(param.getKey()), - Double.valueOf(param.getValue()[0])); - } - } - return imtImls; - } - - private static boolean isImtParam(String key) { - return key.equals("PGA") || key.startsWith("SA"); - } - - private class Deagg2Task extends TimedTaskContext<Result> { - - RequestData data; - - Deagg2Task(String url, ServletContext context, RequestData data) { - super(url, context); - this.data = data; - } - - @Override - public Result calc() throws Exception { - Deaggregation deagg = calcDeagg(data); - - return new Result.Builder() - .requestData(data) - .url(url) - .timer(timer) - .deagg(deagg) - .build(); - } - } - - /* - * Developer notes: - * - * We're opting here to fetch basin terms ourselves. If we were to set the - * basin provider in the config, which requires additions to config, the URL - * is tested every time a site is created for a servlet request. While this - * worked for maps it's not good here. - * - * Site has logic for parsing the basin service response, which perhaps it - * shouldn't. TODO is it worth decomposing data objects and services - */ - Deaggregation calcDeagg(RequestData data) { - Location loc = Location.create(data.latitude, data.longitude); - - Site site = Site.builder() - .location(Location.create(data.latitude, data.longitude)) - .basinDataProvider(data.basin ? this.basinUrl : null) - .vs30(data.vs30) - .build(); - - Hazard[] hazards = new Hazard[data.models.size()]; - for (int i = 0; i < data.models.size(); i++) { - HazardModel model = modelCache.getUnchecked(data.models.get(i)); - hazards[i] = process(model, site, data.imtImls.keySet()); - } - Hazard hazard = Hazard.merge(hazards); - return Deaggregation.atImls(hazard, data.imtImls, ServletUtil.CALC_EXECUTOR); - } - - private static Hazard process(HazardModel model, Site site, Set<Imt> imts) { - CalcConfig config = CalcConfig.copyOf(model.config()) - .imts(imts) - .build(); - // System.out.println(config); - return HazardCalcs.hazard(model, config, site, ServletUtil.CALC_EXECUTOR); - } - - static final class RequestData { - - final List<Model> models; - final double latitude; - final double longitude; - final Map<Imt, Double> imtImls; - final double vs30; - final boolean basin; - - RequestData( - List<Model> models, - double longitude, - double latitude, - Map<Imt, Double> imtImls, - double vs30, - boolean basin) { - - this.models = models; - this.latitude = latitude; - this.longitude = longitude; - this.imtImls = imtImls; - this.vs30 = vs30; - this.basin = basin; - } - } - - private static final class ResponseData { - - final List<Model> models; - final double longitude; - final double latitude; - final String imt; - final double iml; - final double vs30; - final String rlabel = "Closest Distance, rRup (km)"; - final String mlabel = "Magnitude (Mw)"; - final String εlabel = "% Contribution to Hazard"; - final Object εbins; - - ResponseData(Deaggregation deagg, RequestData request, Imt imt) { - this.models = request.models; - this.longitude = request.longitude; - this.latitude = request.latitude; - this.imt = imt.toString(); - this.iml = request.imtImls.get(imt); - this.vs30 = request.vs30; - this.εbins = deagg.εBins(); - } - } - - private static final class Response { - - final ResponseData metadata; - final Object data; - - Response(ResponseData metadata, Object data) { - this.metadata = metadata; - this.data = data; - } - } - - private static final class Result { - - final String status = Status.SUCCESS.toString(); - final String date = ZonedDateTime.now().format(ServletUtil.DATE_FMT); - final String url; - final Object server; - final List<Response> response; - - Result(String url, Object server, List<Response> response) { - this.url = url; - this.server = server; - this.response = response; - } - - static final class Builder { - - String url; - Timer timer; - RequestData request; - Deaggregation deagg; - - Builder deagg(Deaggregation deagg) { - this.deagg = deagg; - return this; - } - - Builder url(String url) { - this.url = url; - return this; - } - - Builder timer(Timer timer) { - this.timer = timer; - return this; - } - - Builder requestData(RequestData request) { - this.request = request; - return this; - } - - Result build() { - ImmutableList.Builder<Response> responseListBuilder = ImmutableList.builder(); - - for (Imt imt : request.imtImls.keySet()) { - ResponseData responseData = new ResponseData(deagg, request, imt); - Object deaggs = deagg.toJsonCompact(imt); - Response response = new Response(responseData, deaggs); - responseListBuilder.add(response); - } - - List<Response> responseList = responseListBuilder.build(); - Object server = Metadata.serverData(ServletUtil.THREAD_COUNT, timer); - - return new Result(url, server, responseList); - } - } - } - -} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/NshmpServlet.java b/src/main/java/gov/usgs/earthquake/nshmp/www/NshmpServlet.java deleted file mode 100644 index d81dee5cea2578f96965574c514324cf0b8659a6..0000000000000000000000000000000000000000 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/NshmpServlet.java +++ /dev/null @@ -1,105 +0,0 @@ -package gov.usgs.earthquake.nshmp.www; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * Custom NSHMP servlet implementation and URL helper class. - * - * <p>All nshmp-haz-v2 services should extend this class. This class sets custom - * response headers and provides a helper class to ensure serialized response - * URLs propagate the correct host and protocol from requests on USGS servers - * and caches that may have been forwarded. - * - * <p>Class provides one convenience method, - * {@code urlHelper.writeResponse(String)}, to write a servlet response wherein - * any URL strings may be formatted with the correct protocol and host. Such URL - * strings should start with: - * - * "%s://%s/service-name/..." - * - * @author U.S. Geological Survey - */ -public abstract class NshmpServlet extends HttpServlet { - - @Override - protected void service( - HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException { - - /* - * Set CORS headers and content type. - * - * Because nshmp-haz-v2 services may be called by both the USGS website, - * other websites, and directly by 3rd party applications, reponses - * generated by direct requests will not have the necessary header - * information that would be required by security protocols for web - * requests. This means that any initial direct request will pollute - * intermediate caches with a response that a browser will deem invalid. - */ - response.setContentType("application/json; charset=UTF-8"); - response.setHeader("Access-Control-Allow-Origin", "*"); - response.setHeader("Access-Control-Allow-Methods", "*"); - response.setHeader("Access-Control-Allow-Headers", "accept,origin,authorization,content-type"); - - super.service(request, response); - } - - public static UrlHelper urlHelper(HttpServletRequest request, HttpServletResponse response) - throws IOException { - return new UrlHelper(request, response); - } - - public static class UrlHelper { - - private final HttpServletResponse response; - private final String urlPrefix; - public final String url; - - UrlHelper(HttpServletRequest request, HttpServletResponse response) { - - /* - * Check custom header for a forwarded protocol so generated links can use - * the same protocol and not cause mixed content errors. - */ - String host = request.getServerName(); - String protocol = request.getHeader("X-FORWARDED-PROTO"); - String contextPath = request.getContextPath(); - if (protocol == null) { - /* Not a forwarded request. Honor reported protocol and port. */ - protocol = request.getScheme(); - host += ":" + request.getServerPort(); - } - - /* - * For convenience, store a url field with the (possibly updated) request - * protocol and - */ - StringBuffer urlBuf = request.getRequestURL(); - String query = request.getQueryString(); - if (query != null) urlBuf.append('?').append(query); - String url = urlBuf.toString().replace("http://", protocol + "://"); - - this.response = response; - this.urlPrefix = String.format("%s://%s%s", protocol, host, contextPath); - this.url = url; - } - - /** - * Convenience method to update a string response with the correct protocol - * and host in URLs. URL strings should start with: - * - * "%s://%s/service-name/..." - */ - public void writeResponse(String usage) throws IOException { - // TODO had to add duplicate fields to handle haz and g syntax strings - response.getWriter().printf(usage, urlPrefix, urlPrefix); - } - } - -} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/WsUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/WsUtil.java deleted file mode 100644 index bf52c0e8b13e0c38a8a2f1941c0520d5268b5006..0000000000000000000000000000000000000000 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/WsUtil.java +++ /dev/null @@ -1,213 +0,0 @@ -package gov.usgs.earthquake.nshmp.www; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - -import java.util.Arrays; -import java.util.Map; -import java.util.Set; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -import javax.servlet.ServletRequest; - -import com.google.gson.GsonBuilder; - -import gov.usgs.earthquake.nshmp.internal.Parsing; -import gov.usgs.earthquake.nshmp.internal.Parsing.Delimiter; -import gov.usgs.earthquake.nshmp.internal.www.NshmpMicronautServlet.UrlHelper; -import gov.usgs.earthquake.nshmp.internal.www.Response; -import gov.usgs.earthquake.nshmp.internal.www.WsUtils; -import gov.usgs.earthquake.nshmp.internal.www.meta.Status; -import gov.usgs.earthquake.nshmp.www.services.SourceServices.SourceModel; - -import io.micronaut.http.HttpResponse; - -public class WsUtil { - - public static HttpResponse<String> handleError( - Throwable e, - String name, - Logger logger, - UrlHelper urlHelper) { - var msg = e.getMessage() + " (see logs)"; - var svcResponse = new Response<>(Status.ERROR, name, urlHelper.url, msg, urlHelper); - var gson = new GsonBuilder().setPrettyPrinting().create(); - var response = gson.toJson(svcResponse); - logger.severe(name + " -\n" + response); - e.printStackTrace(); - return HttpResponse.serverError(response); - } - - public static interface ServiceQuery { - boolean isNull(); - - void checkValues(); - } - - public static class ServiceQueryData implements ServiceQuery { - public final Double longitude; - public final Double latitude; - - public ServiceQueryData(Double longitude, Double latitude) { - this.longitude = longitude; - this.latitude = latitude; - } - - @Override - public boolean isNull() { - return longitude == null && latitude == null; - } - - @Override - public void checkValues() { - WsUtils.checkValue(Key.LONGITUDE, longitude); - WsUtils.checkValue(Key.LATITUDE, latitude); - } - } - - public static class ServiceRequestData { - public final SourceModel model; - public final double longitude; - public final double latitude; - - public ServiceRequestData(ServiceQueryData query) { - model = new SourceModel(ServletUtil.installedModel()); - longitude = query.longitude; - latitude = query.latitude; - } - } - - /** - * Returns the value of a servlet request parameter as a boolean. - * - * @param key of value to get - * @param request servlet request - */ - public static <E extends Enum<E>> boolean readBoolean(E key, ServletRequest request) { - return Boolean.valueOf(readValue(key, request)); - } - - /** - * Returns the value of a servlet request parameter as a double. - * - * @param key of value to get - * @param request servlet request - */ - public static <E extends Enum<E>> double readDouble(E key, ServletRequest request) { - return Double.valueOf(readValue(key, request)); - } - - /** - * Returns the value of a servlet request parameter as an integer. - * - * @param key of value to get - * @param request servlet request - */ - public static <E extends Enum<E>> int readInteger(E key, ServletRequest request) { - return Integer.valueOf(readValue(key, request)); - } - - /** - * Returns the value of a servlet request parameter as a string. - * - * @param key of value to get - * @param request servlet request - */ - public static <E extends Enum<E>> String readValue(E key, ServletRequest request) { - return readValues(key, request)[0]; - } - - /** - * Returns the value of a servlet request parameter as a enum of specified - * type. - * - * @param key of value to get - * @param request servlet request - * @param type of enum to return - */ - public static <T extends Enum<T>, E extends Enum<E>> T readValue( - E key, - ServletRequest request, - Class<T> type) { - return Enum.valueOf(type, readValue(key, request)); - } - - /** - * Returns the value of a servlet request parameter as a string array. - * - * @param key of value to get - * @param request servlet request - */ - public static <E extends Enum<E>> String[] readValues(E key, ServletRequest request) { - return checkNotNull( - request.getParameterValues(key.toString()), - "Missing query key [" + key.toString() + "]"); - } - - /** - * Returns the value of a servlet request parameter as a enum set of specified - * type. - * - * @param key of value to get - * @param request servlet request - * @param type of enum to return - */ - public static <T extends Enum<T>, E extends Enum<E>> Set<T> readValues( - E key, - ServletRequest request, - Class<T> type) { - - return Arrays.stream(readValues(key, request)) - .map((name) -> Enum.valueOf(type, name)) - .collect(Collectors.toSet()); - } - - public enum Key { - EDITION, - REGION, - MODEL, - VS30, - LATITUDE, - LONGITUDE, - IMT, - RETURNPERIOD, - DISTANCE, - FORMAT, - TIMESPAN, - BASIN; - - private String label; - - private Key() { - label = name().toLowerCase(); - } - - @Override - public String toString() { - return label; - } - } - - static <T extends Enum<T>> Set<T> readValues(String values, Class<T> type) { - return Parsing.splitToList(values, Delimiter.COMMA).stream() - .map((name) -> Enum.valueOf(type, name)) - .collect(Collectors.toSet()); - } - - static <E extends Enum<E>> String readValue(E key, Map<String, String[]> paramMap) { - String keyStr = key.toString(); - String[] values = paramMap.get(keyStr); - checkNotNull(values, "Missing query key: %s", keyStr); - checkState(values.length > 0, "Empty value array for key: %s", key); - return values[0]; - } - - static <T extends Enum<T>, E extends Enum<E>> T readValue( - E key, - Map<String, String[]> paramMap, - Class<T> type) { - return Enum.valueOf(type, readValue(key, paramMap)); - } - -} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/XY_DataGroup.java b/src/main/java/gov/usgs/earthquake/nshmp/www/XY_DataGroup.java deleted file mode 100644 index 697006fbfc5e34026bc462ee09c0cd70c74f8ca3..0000000000000000000000000000000000000000 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/XY_DataGroup.java +++ /dev/null @@ -1,53 +0,0 @@ -package gov.usgs.earthquake.nshmp.www; - -import java.util.ArrayList; -import java.util.List; - -import gov.usgs.earthquake.nshmp.data.XySequence; - -/** - * Container class of XY data sequences prior to Json serialization. This - * implementation is for data series that share the same x-values - * - * @author U.S. Geological Survey - */ -@SuppressWarnings("unused") -@Deprecated -public class XY_DataGroup { - - private final String label; - private final String xLabel; - private final String yLabel; - protected final List<Series> data; - - protected XY_DataGroup(String label, String xLabel, String yLabel) { - this.label = label; - this.xLabel = xLabel; - this.yLabel = yLabel; - this.data = new ArrayList<>(); - } - - /** Create a data group. */ - public static XY_DataGroup create(String name, String xLabel, String yLabel) { - return new XY_DataGroup(name, xLabel, yLabel); - } - - /** Add a data sequence */ - public XY_DataGroup add(String id, String name, XySequence data) { - this.data.add(new Series(id, name, data)); - return this; - } - - static class Series { - private final String id; - private final String label; - private final XySequence data; - - Series(String id, String label, XySequence data) { - this.id = id; - this.label = label; - this.data = data; - } - } - -} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Metadata.java b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Metadata.java index 01f7370dcfbc95845aa92ef753780a6af43f4b47..992aa92c7267bc7e009e0079e4b07106098f83c4 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Metadata.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Metadata.java @@ -6,8 +6,8 @@ import com.google.gson.annotations.SerializedName; import gov.usgs.earthquake.nshmp.geo.Coordinates; import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType; import gov.usgs.earthquake.nshmp.internal.www.meta.Status; -import gov.usgs.earthquake.nshmp.www.ServletUtil; -import gov.usgs.earthquake.nshmp.www.ServletUtil.Timer; +import gov.usgs.earthquake.nshmp.www.services.ServletUtil; +import gov.usgs.earthquake.nshmp.www.services.ServletUtil.Timer; /** * Service metadata, parameterization, and constraint strings, in JSON format. diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/DeaggEpsilonService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/DeaggEpsilonService.java new file mode 100644 index 0000000000000000000000000000000000000000..1036e20e12320af73e4830aa34ff4da55e72fef0 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/DeaggEpsilonService.java @@ -0,0 +1,297 @@ +package gov.usgs.earthquake.nshmp.www.services; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.EnumMap; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; +import java.util.logging.Logger; + +import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; + +import gov.usgs.earthquake.nshmp.calc.CalcConfig; +import gov.usgs.earthquake.nshmp.calc.Deaggregation; +import gov.usgs.earthquake.nshmp.calc.Site; +import gov.usgs.earthquake.nshmp.calc.Vs30; +import gov.usgs.earthquake.nshmp.geo.Location; +import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.internal.www.NshmpMicronautServlet.UrlHelper; +import gov.usgs.earthquake.nshmp.internal.www.Response; +import gov.usgs.earthquake.nshmp.internal.www.WsUtils; +import gov.usgs.earthquake.nshmp.internal.www.meta.Status; +import gov.usgs.earthquake.nshmp.www.BaseModel; +import gov.usgs.earthquake.nshmp.www.DeaggEpsilonController; +import gov.usgs.earthquake.nshmp.www.meta.Metadata; +import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.Key; +import gov.usgs.earthquake.nshmp.www.services.ServletUtil.Timer; +import gov.usgs.earthquake.nshmp.www.services.SourceServices.SourceModel; + +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; + +/** + * Hazard deaggregation handler for {@link DeaggEpsilonController}. + * + * @author U.S. Geological Survey + */ +public final class DeaggEpsilonService { + + /* Developer notes: See HazardService. */ + + private static final String NAME = "Epsilon Deaggregation"; + private static final Logger LOGGER = Logger.getLogger(DeaggEpsilonService.class.getName()); + private static URL basinUrl; + + public static void init() { + try (InputStream config = Resources.getResource("config.properties").openStream()) { + checkNotNull(config, "Missing config.properties"); + + Properties props = new Properties(); + props.load(config); + if (props.containsKey("basin_host")) { + /* + * TODO Site builder tests if service is working, which may be + * inefficient for single call services. + */ + var url = new URL(props.getProperty("basin_host") + "/nshmp/ws/data/basin"); + basinUrl = url; + } + } catch (IOException | NullPointerException e) { + throw new RuntimeException(e); + } + } + + /** + * Handler for {@link DeaggEpsilonController#doGetDeaggEpsilon}. Returns the + * usage or the deagg result. + * + * @param query The query + * @param urlHelper The URL helper + */ + public static HttpResponse<String> handleDoGetDeaggEpsilon(Query query, UrlHelper urlHelper) { + try { + var timer = ServletUtil.timer(); + LOGGER.info(NAME + " - Request:\n" + ServletUtil.GSON.toJson(query)); + + if (query.isNull()) { + return HazardService.handleDoGetUsage(urlHelper); + } + + query.checkValues(); + var data = new RequestData(query, Vs30.fromValue(query.vs30)); + var response = process(data, timer, urlHelper); + var svcResponse = ServletUtil.GSON.toJson(response); + LOGGER.info(NAME + " - Response:\n" + svcResponse); + return HttpResponse.ok(svcResponse); + } catch (Exception e) { + return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper); + } + } + + /* Create map of IMT to deagg IML. */ + private static EnumMap<Imt, Double> readImtsFromQuery(HttpRequest<?> request) { + var imtImls = new EnumMap<Imt, Double>(Imt.class); + for (var param : request.getParameters().asMap().entrySet()) { + if (isImtParam(param.getKey())) { + imtImls.put( + Imt.valueOf(param.getKey()), + Double.valueOf(param.getValue().get(0))); + } + } + return imtImls; + } + + private static boolean isImtParam(String key) { + return key.equals("PGA") || key.startsWith("SA"); + } + + private static Response<RequestData, ResponseData> process( + RequestData data, + Timer timer, + UrlHelper urlHelper) + throws InterruptedException, ExecutionException { + var configFunction = new ConfigFunction(); + var siteFunction = new SiteFunction(data); + var hazard = ServicesUtil.calcHazard(configFunction, siteFunction); + var deagg = Deaggregation.atImls(hazard, data.imtImls, ServletUtil.CALC_EXECUTOR); + return new ResultBuilder() + .deagg(deagg) + .requestData(data) + .timer(timer) + .urlHelper(urlHelper) + .build(); + } + + public static class Query extends HazardService.Query { + final EnumMap<Imt, Double> imtImls; + final Boolean basin; + + public Query( + HttpRequest<?> request, + Double longitude, + Double latitude, + Integer vs30, + Boolean basin) { + super(longitude, latitude, vs30); + imtImls = readImtsFromQuery(request); + this.basin = basin == null ? false : basin; + } + + @Override + public boolean isNull() { + return super.isNull() && vs30 == null; + } + + @Override + public void checkValues() { + super.checkValues(); + WsUtils.checkValue(Key.BASIN, basin); + } + } + + static class ConfigFunction implements Function<BaseModel, CalcConfig> { + @Override + public CalcConfig apply(BaseModel baseModel) { + var hazardModel = ServletUtil.hazardModels().get(baseModel); + var configBuilder = CalcConfig.copyOf(hazardModel.config()); + configBuilder.imts(baseModel.imts); + return configBuilder.build(); + } + } + + /* + * Developer notes: + * + * We're opting here to fetch basin terms ourselves. If we were to set the + * basin provider in the config, which requires additions to config, the URL + * is tested every time a site is created for a servlet request. While this + * worked for maps it's not good here. + * + * Site has logic for parsing the basin service response, which perhaps it + * shouldn't. TODO is it worth decomposing data objects and services + */ + static class SiteFunction implements Function<CalcConfig, Site> { + final RequestData data; + + private SiteFunction(RequestData data) { + this.data = data; + } + + @Override + public Site apply(CalcConfig config) { + return Site.builder() + .location(Location.create(data.latitude, data.longitude)) + .basinDataProvider(data.basin ? basinUrl : null) + .vs30(data.vs30.value()) + .build(); + } + } + + static final class RequestData extends HazardService.RequestData { + final EnumMap<Imt, Double> imtImls; + final boolean basin; + + RequestData(Query query, Vs30 vs30) { + super(query, vs30); + imtImls = query.imtImls; + basin = query.basin; + } + } + + @SuppressWarnings("unused") + private static final class ResponseMetadata { + final SourceModel model; + final double longitude; + final double latitude; + final String imt; + final double iml; + final Vs30 vs30; + final String rlabel = "Closest Distance, rRup (km)"; + final String mlabel = "Magnitude (Mw)"; + final String εlabel = "% Contribution to Hazard"; + final Object εbins; + + ResponseMetadata(Deaggregation deagg, RequestData request, Imt imt) { + this.model = request.model; + this.longitude = request.longitude; + this.latitude = request.latitude; + this.imt = imt.toString(); + this.iml = imt.period(); + this.vs30 = request.vs30; + this.εbins = deagg.εBins(); + } + } + + @SuppressWarnings("unused") + private static final class ResponseData { + final Object server; + final List<DeaggResponse> deaggs; + + ResponseData(Object server, List<DeaggResponse> deaggs) { + this.server = server; + this.deaggs = deaggs; + } + } + + @SuppressWarnings("unused") + private static final class DeaggResponse { + final ResponseMetadata metadata; + final Object data; + + DeaggResponse(ResponseMetadata metadata, Object data) { + this.metadata = metadata; + this.data = data; + } + } + + static final class ResultBuilder { + UrlHelper urlHelper; + Timer timer; + RequestData request; + Deaggregation deagg; + + ResultBuilder deagg(Deaggregation deagg) { + this.deagg = deagg; + return this; + } + + ResultBuilder urlHelper(UrlHelper urlHelper) { + this.urlHelper = urlHelper; + return this; + } + + ResultBuilder timer(Timer timer) { + this.timer = timer; + return this; + } + + ResultBuilder requestData(RequestData request) { + this.request = request; + return this; + } + + Response<RequestData, ResponseData> build() { + ImmutableList.Builder<DeaggResponse> responseListBuilder = ImmutableList.builder(); + + for (Imt imt : request.imtImls.keySet()) { + ResponseMetadata responseData = new ResponseMetadata(deagg, request, imt); + Object deaggs = deagg.toJsonCompact(imt); + DeaggResponse response = new DeaggResponse(responseData, deaggs); + responseListBuilder.add(response); + } + + List<DeaggResponse> responseList = responseListBuilder.build(); + Object server = Metadata.serverData(ServletUtil.THREAD_COUNT, timer); + var response = new ResponseData(server, responseList); + + return new Response<>(Status.SUCCESS, NAME, request, response, urlHelper); + } + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java index 68bc040de6f1bb48898eb98dde59c06e2753304d..4039651427c294977b67c1a5ba36d224bdc90e52 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java @@ -7,19 +7,16 @@ import java.util.ArrayList; import java.util.EnumMap; import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.function.Function; import java.util.logging.Logger; -import java.util.stream.Collectors; import gov.usgs.earthquake.nshmp.calc.CalcConfig; import gov.usgs.earthquake.nshmp.calc.Hazard; -import gov.usgs.earthquake.nshmp.calc.HazardCalcs; import gov.usgs.earthquake.nshmp.calc.Site; import gov.usgs.earthquake.nshmp.calc.Vs30; import gov.usgs.earthquake.nshmp.data.MutableXySequence; import gov.usgs.earthquake.nshmp.data.XySequence; -import gov.usgs.earthquake.nshmp.eq.model.HazardModel; import gov.usgs.earthquake.nshmp.eq.model.SourceType; import gov.usgs.earthquake.nshmp.geo.Location; import gov.usgs.earthquake.nshmp.gmm.Imt; @@ -29,13 +26,11 @@ import gov.usgs.earthquake.nshmp.internal.www.WsUtils; import gov.usgs.earthquake.nshmp.internal.www.meta.Status; import gov.usgs.earthquake.nshmp.www.BaseModel; import gov.usgs.earthquake.nshmp.www.HazardController; -import gov.usgs.earthquake.nshmp.www.ServletUtil; -import gov.usgs.earthquake.nshmp.www.ServletUtil.Timer; -import gov.usgs.earthquake.nshmp.www.WsUtil; -import gov.usgs.earthquake.nshmp.www.WsUtil.Key; -import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceQueryData; -import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceRequestData; import gov.usgs.earthquake.nshmp.www.meta.Metadata; +import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.Key; +import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceQueryData; +import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceRequestData; +import gov.usgs.earthquake.nshmp.www.services.ServletUtil.Timer; import gov.usgs.earthquake.nshmp.www.services.SourceServices.SourceModel; import io.micronaut.http.HttpResponse; @@ -70,10 +65,10 @@ public final class HazardService { var usage = new SourceServices.ResponseData(); var response = new Response<>(Status.USAGE, NAME, urlHelper.url, usage, urlHelper); var svcResponse = ServletUtil.GSON.toJson(response); - LOGGER.info(NAME + " - Response:\n" + ServletUtil.GSON.toJson(svcResponse)); + LOGGER.info(NAME + " - Response:\n" + svcResponse); return HttpResponse.ok(svcResponse); } catch (Exception e) { - return WsUtil.handleError(e, NAME, LOGGER, urlHelper); + return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper); } } @@ -97,10 +92,10 @@ public final class HazardService { var data = new RequestData(query, Vs30.fromValue(query.vs30)); var response = process(data, timer, urlHelper); var svcResponse = ServletUtil.GSON.toJson(response); - LOGGER.info(NAME + " - Response:\n" + ServletUtil.GSON.toJson(svcResponse)); + LOGGER.info(NAME + " - Response:\n" + svcResponse); return HttpResponse.ok(svcResponse); } catch (Exception e) { - return WsUtil.handleError(e, NAME, LOGGER, urlHelper); + return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper); } } @@ -108,7 +103,10 @@ public final class HazardService { RequestData data, Timer timer, UrlHelper urlHelper) throws InterruptedException, ExecutionException { - var hazard = calc(data); + var configFunction = new ConfigFunction(); + var siteFunction = new SiteFunction(data); + var hazard = ServicesUtil.calcHazard(configFunction, siteFunction); + return new ResultBuilder() .hazard(hazard) .requestData(data) @@ -117,41 +115,31 @@ public final class HazardService { .build(); } - static Hazard calc(RequestData data) throws InterruptedException, ExecutionException { - var futuresList = ServletUtil.installedModel().models().stream() - .map(baseModel -> calcHazard(baseModel, ServletUtil.hazardModels().get(baseModel), data)) - .collect(Collectors.toList()); - - var hazardsFuture = CompletableFuture - .allOf(futuresList.toArray(new CompletableFuture[futuresList.size()])) - .thenApply(v -> { - return futuresList.stream() - .map(future -> future.join()) - .collect(Collectors.toList()); - }); - - var hazards = hazardsFuture.get().toArray(new Hazard[] {}); - return Hazard.merge(hazards); + static class ConfigFunction implements Function<BaseModel, CalcConfig> { + @Override + public CalcConfig apply(BaseModel baseModel) { + var hazardModel = ServletUtil.hazardModels().get(baseModel); + var configBuilder = CalcConfig.copyOf(hazardModel.config()); + configBuilder.imts(baseModel.imts); + return configBuilder.build(); + } } - static CompletableFuture<Hazard> calcHazard( - BaseModel baseModel, - HazardModel hazardModel, - RequestData data) { - var location = Location.create(data.latitude, data.longitude); - var configBuilder = CalcConfig.copyOf(hazardModel.config()); - configBuilder.imts(baseModel.imts); - var config = configBuilder.build(); - var site = Site.builder() - .basinDataProvider(config.siteData.basinDataProvider) - .location(location) - .vs30(data.vs30.value()) - .build(); + static class SiteFunction implements Function<CalcConfig, Site> { + final RequestData data; - return CompletableFuture - .supplyAsync( - () -> HazardCalcs.hazard(hazardModel, config, site, ServletUtil.CALC_EXECUTOR), - ServletUtil.TASK_EXECUTOR); + private SiteFunction(RequestData data) { + this.data = data; + } + + @Override + public Site apply(CalcConfig config) { + return Site.builder() + .basinDataProvider(config.siteData.basinDataProvider) + .location(Location.create(data.latitude, data.longitude)) + .vs30(data.vs30.value()) + .build(); + } } public static class Query extends ServiceQueryData { @@ -174,7 +162,7 @@ public final class HazardService { } } - static final class RequestData extends ServiceRequestData { + static class RequestData extends ServiceRequestData { final Vs30 vs30; RequestData(Query query, Vs30 vs30) { diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java index b24499194e513af09f819943eddda6ffa80c6d29..d88510f5baf37aa7d3ddd9c55f065365e680ee6c 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java @@ -21,15 +21,13 @@ import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType; import gov.usgs.earthquake.nshmp.internal.www.meta.Status; import gov.usgs.earthquake.nshmp.mfd.Mfds; import gov.usgs.earthquake.nshmp.www.RateController; -import gov.usgs.earthquake.nshmp.www.ServletUtil; -import gov.usgs.earthquake.nshmp.www.ServletUtil.Timer; -import gov.usgs.earthquake.nshmp.www.WsUtil; -import gov.usgs.earthquake.nshmp.www.WsUtil.Key; -import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceQueryData; -import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceRequestData; import gov.usgs.earthquake.nshmp.www.meta.DoubleParameter; import gov.usgs.earthquake.nshmp.www.meta.Metadata; import gov.usgs.earthquake.nshmp.www.meta.Metadata.DefaultParameters; +import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.Key; +import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceQueryData; +import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceRequestData; +import gov.usgs.earthquake.nshmp.www.services.ServletUtil.Timer; import io.micronaut.http.HttpResponse; @@ -66,7 +64,7 @@ public final class RateService { var svcResponse = ServletUtil.GSON.toJson(response); return HttpResponse.ok(String.format(svcResponse, urlHelper.urlPrefix, urlHelper.urlPrefix)); } catch (Exception e) { - return WsUtil.handleError(e, service.name, LOGGER, urlHelper); + return ServicesUtil.handleError(e, service.name, LOGGER, urlHelper); } } @@ -98,7 +96,7 @@ public final class RateService { LOGGER.info(service.name + " - Response:\n" + svcResponse); return HttpResponse.ok(svcResponse); } catch (Exception e) { - return WsUtil.handleError(e, service.name, LOGGER, urlHelper); + return ServicesUtil.handleError(e, service.name, LOGGER, urlHelper); } } diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..f3ef2cb6e46c2893e78789beecd021866731255d --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java @@ -0,0 +1,139 @@ +package gov.usgs.earthquake.nshmp.www.services; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import com.google.gson.GsonBuilder; + +import gov.usgs.earthquake.nshmp.calc.CalcConfig; +import gov.usgs.earthquake.nshmp.calc.Hazard; +import gov.usgs.earthquake.nshmp.calc.HazardCalcs; +import gov.usgs.earthquake.nshmp.calc.Site; +import gov.usgs.earthquake.nshmp.internal.www.NshmpMicronautServlet.UrlHelper; +import gov.usgs.earthquake.nshmp.internal.www.Response; +import gov.usgs.earthquake.nshmp.internal.www.WsUtils; +import gov.usgs.earthquake.nshmp.internal.www.meta.Status; +import gov.usgs.earthquake.nshmp.www.BaseModel; +import gov.usgs.earthquake.nshmp.www.services.SourceServices.SourceModel; + +import io.micronaut.http.HttpResponse; + +class ServicesUtil { + + static HttpResponse<String> handleError( + Throwable e, + String name, + Logger logger, + UrlHelper urlHelper) { + var msg = e.getMessage() + " (see logs)"; + var svcResponse = new Response<>(Status.ERROR, name, urlHelper.url, msg, urlHelper); + var gson = new GsonBuilder().setPrettyPrinting().create(); + var response = gson.toJson(svcResponse); + logger.severe(name + " -\n" + response); + e.printStackTrace(); + return HttpResponse.serverError(response); + } + + static Hazard calcHazard( + Function<BaseModel, CalcConfig> configFunction, + Function<CalcConfig, Site> siteFunction) throws InterruptedException, ExecutionException { + var futuresList = ServletUtil.installedModel().models().stream() + .map(baseModel -> { + var config = configFunction.apply(baseModel); + var site = siteFunction.apply(config); + return calcHazard(baseModel, config, site); + }) + .collect(Collectors.toList()); + + var hazardsFuture = CompletableFuture + .allOf(futuresList.toArray(new CompletableFuture[futuresList.size()])) + .thenApply(v -> { + return futuresList.stream() + .map(future -> future.join()) + .collect(Collectors.toList()); + }); + + var hazards = hazardsFuture.get().toArray(new Hazard[] {}); + return Hazard.merge(hazards); + } + + static class ServiceQueryData implements ServiceQuery { + public final Double longitude; + public final Double latitude; + + ServiceQueryData(Double longitude, Double latitude) { + this.longitude = longitude; + this.latitude = latitude; + } + + @Override + public boolean isNull() { + return longitude == null && latitude == null; + } + + @Override + public void checkValues() { + WsUtils.checkValue(Key.LONGITUDE, longitude); + WsUtils.checkValue(Key.LATITUDE, latitude); + } + } + + static class ServiceRequestData { + public final SourceModel model; + public final double longitude; + public final double latitude; + + public ServiceRequestData(ServiceQueryData query) { + model = new SourceModel(ServletUtil.installedModel()); + longitude = query.longitude; + latitude = query.latitude; + } + } + + enum Key { + EDITION, + REGION, + MODEL, + VS30, + LATITUDE, + LONGITUDE, + IMT, + RETURNPERIOD, + DISTANCE, + FORMAT, + TIMESPAN, + BASIN; + + private String label; + + private Key() { + label = name().toLowerCase(); + } + + @Override + public String toString() { + return label; + } + } + + private static interface ServiceQuery { + boolean isNull(); + + void checkValues(); + } + + private static CompletableFuture<Hazard> calcHazard( + BaseModel baseModel, + CalcConfig config, + Site site) { + return CompletableFuture + .supplyAsync( + () -> HazardCalcs.hazard( + ServletUtil.hazardModels().get(baseModel), config, site, ServletUtil.CALC_EXECUTOR), + ServletUtil.TASK_EXECUTOR); + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java similarity index 78% rename from src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java rename to src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java index 83a58c0724a21e0a796254530229c1b1430d53d9..e11d2e22e19cf7f4c0e74453c0d8be5c1d287fdf 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java @@ -1,6 +1,5 @@ -package gov.usgs.earthquake.nshmp.www; +package gov.usgs.earthquake.nshmp.www.services; -import static com.google.common.base.Strings.isNullOrEmpty; import static java.lang.Runtime.getRuntime; import java.net.URI; @@ -14,13 +13,9 @@ import java.time.format.DateTimeFormatter; import java.util.EnumMap; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import javax.servlet.ServletContext; -import javax.servlet.http.HttpServletRequest; - import com.google.common.base.Stopwatch; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; @@ -33,6 +28,8 @@ import gov.usgs.earthquake.nshmp.calc.Vs30; import gov.usgs.earthquake.nshmp.eq.model.HazardModel; import gov.usgs.earthquake.nshmp.gmm.Imt; import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType; +import gov.usgs.earthquake.nshmp.www.BaseModel; +import gov.usgs.earthquake.nshmp.www.Model; import gov.usgs.earthquake.nshmp.www.meta.MetaUtil; import gov.usgs.earthquake.nshmp.www.meta.Region; @@ -48,25 +45,25 @@ import io.micronaut.runtime.event.annotation.EventListener; */ public class ServletUtil { - @Value("${nshmp-haz.installed-model}") - private Model model; - - private static Model INSTALLED_MODEL; - private static Map<BaseModel, HazardModel> HAZARD_MODELS = new EnumMap<>(BaseModel.class); + public static final Gson GSON; public static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern( "yyyy-MM-dd'T'HH:mm:ssXXX"); - public static final ListeningExecutorService CALC_EXECUTOR; - public static final ExecutorService TASK_EXECUTOR; + static final ListeningExecutorService CALC_EXECUTOR; + static final ExecutorService TASK_EXECUTOR; - public static final int THREAD_COUNT; - - public static final Gson GSON; + static final int THREAD_COUNT; /* Stateful flag to reject requests while a result is pending. */ - public static boolean uhtBusy = false; - public static long hitCount = 0; - public static long missCount = 0; + static boolean uhtBusy = false; + static long hitCount = 0; + static long missCount = 0; + + @Value("${nshmp-haz.installed-model}") + private Model model; + + private static Model INSTALLED_MODEL; + private static Map<BaseModel, HazardModel> HAZARD_MODELS = new EnumMap<>(BaseModel.class); static { /* TODO modified for deagg-epsilon branch; should be context var */ @@ -87,11 +84,11 @@ public class ServletUtil { .create(); } - public static Model installedModel() { + static Model installedModel() { return INSTALLED_MODEL; } - public static Map<BaseModel, HazardModel> hazardModels() { + static Map<BaseModel, HazardModel> hazardModels() { return HAZARD_MODELS; } @@ -157,11 +154,6 @@ public class ServletUtil { } } - static boolean emptyRequest(HttpServletRequest request) { - return isNullOrEmpty(request.getQueryString()) && - (request.getPathInfo() == null || request.getPathInfo().equals("/")); - } - public static Timer timer() { return new Timer(); } @@ -171,11 +163,10 @@ public class ServletUtil { * be started later. */ public static final class Timer { - Stopwatch servlet = Stopwatch.createStarted(); Stopwatch calc = Stopwatch.createUnstarted(); - Timer start() { + public Timer start() { calc.start(); return this; } @@ -189,32 +180,4 @@ public class ServletUtil { } } - public abstract static class TimedTask<T> implements Callable<T> { - - final String url; - final Timer timer; - - public TimedTask(String url) { - this.url = url; - this.timer = ServletUtil.timer(); - } - - public abstract T calc() throws Exception; - - @Override - public T call() throws Exception { - timer.start(); - return calc(); - } - } - - public abstract static class TimedTaskContext<T> extends TimedTask<T> { - ServletContext context; - - public TimedTaskContext(String url, ServletContext context) { - super(url); - this.context = context; - } - } - } diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java index 8201509398c34895250165517a3c19b26f14aa3f..99c80fb53d9eb1befd88f6ed6f7bd379ededb303 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java @@ -23,8 +23,6 @@ import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType; import gov.usgs.earthquake.nshmp.internal.www.meta.Status; import gov.usgs.earthquake.nshmp.www.BaseModel; import gov.usgs.earthquake.nshmp.www.Model; -import gov.usgs.earthquake.nshmp.www.ServletUtil; -import gov.usgs.earthquake.nshmp.www.WsUtil; import gov.usgs.earthquake.nshmp.www.meta.DoubleParameter; import gov.usgs.earthquake.nshmp.www.meta.MetaUtil; import gov.usgs.earthquake.nshmp.www.meta.Metadata; @@ -71,7 +69,7 @@ public class SourceServices { LOGGER.info(NAME + "- Response:\n" + jsonString); return HttpResponse.ok(jsonString); } catch (Exception e) { - return WsUtil.handleError(e, NAME, LOGGER, urlHelper); + return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper); } }