diff --git a/src/main/java/gov/usgs/earthquake/nshmp/internal/www/NshmpMicronautServlet.java b/src/main/java/gov/usgs/earthquake/nshmp/internal/www/NshmpMicronautServlet.java new file mode 100644 index 0000000000000000000000000000000000000000..8178878370a40c0ec5123b855882b4923b577848 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/internal/www/NshmpMicronautServlet.java @@ -0,0 +1,51 @@ +package gov.usgs.earthquake.nshmp.internal.www; + +import org.reactivestreams.Publisher; + +import io.micronaut.core.type.MutableHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.filter.HttpServerFilter; +import io.micronaut.http.filter.ServerFilterChain; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * Custom NSHMP servlet implementation and URL helper class for Micronaut + * services. + * + * <p>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. + * + * @author U.S. Geological Survey + */ +@Filter("/**") +public class NshmpMicronautServlet implements HttpServerFilter { + + /* + * Set CORS headers and content type. + * + * Because NSHMP services may be called by both the USGS website, other + * websites, and directly by 3rd party applications, responses 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. + */ + @Override + public Publisher<MutableHttpResponse<?>> doFilter( + HttpRequest<?> request, + ServerFilterChain chain) { + return Flowable.just(chain).subscribeOn(Schedulers.io()) + .switchMap(bool -> chain.proceed(request)) + .doOnNext(res -> { + MutableHeaders headers = res.getHeaders(); + headers.add("Access-Control-Allow-Origin", "*"); + headers.add("Access-Control-Allow-Methods", "*"); + headers.add("Access-Control-Allow-Headers", "*"); + }); + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/internal/www/ResponseBody.java b/src/main/java/gov/usgs/earthquake/nshmp/internal/www/ResponseBody.java new file mode 100644 index 0000000000000000000000000000000000000000..650056c312352c118178816cca2e9d92bbcca6bc --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/internal/www/ResponseBody.java @@ -0,0 +1,213 @@ +package gov.usgs.earthquake.nshmp.internal.www; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.time.ZonedDateTime; + +import gov.usgs.earthquake.nshmp.internal.www.meta.Status; + +/** + * Generic wrapper around a web service response object that is typically + * serialized to JSON and sent back to requestor as an HttpResponse 'body'. + * + * <p>To create a response, use one of the three static builder methods: + * {@link Builder#error()}, {@link Builder#success()}, or + * {@link Builder#usage()}. + * + * @author U.S. Geological Survey + * + * @param <T> The request type + * @param <V> The response type + */ +public class ResponseBody<T, V> { + + private final String name; + private final String date; + private final String status; + private final String url; + private final T request; + private final V response; + private final ResponseMetadata metadata; + + private ResponseBody(Builder<T, V> builder) { + name = builder.name; + date = ZonedDateTime.now().format(WsUtils.DATE_FMT); + status = builder.status; + url = builder.url; + request = builder.request; + response = builder.response; + metadata = builder.metadata; + } + + protected ResponseBody() { + date = null; + metadata = null; + name = null; + request = null; + response = null; + status = null; + url = null; + } + + /** + * The date and time this request/response. + */ + public String getDate() { + return date; + } + + /** + * The metadata. + */ + public ResponseMetadata getMetadata() { + return metadata; + } + + /** + * The name of the service. + */ + public String getName() { + return name; + } + + /** + * The request object. + */ + public T getRequest() { + return request; + } + + /** + * The response object. + */ + public V getResponse() { + return response; + } + + /** + * The response status. + */ + public String getStatus() { + return status; + } + + /** + * The URL used to call the service + */ + public String getUrl() { + return url; + } + + /** + * Create a new builder initialized to an error response. + * + * @param <T> The request type + * @param <V> The response type + */ + public static <T, V> Builder<T, V> error() { + return new Builder<T, V>(Status.ERROR); + } + + /** + * Create a new builder initialized to a success response. + * + * @param <T> The request type + * @param <V> The response type + */ + public static <T, V> Builder<T, V> success() { + return new Builder<T, V>(Status.SUCCESS); + } + + /** + * Create a new builder initialized to a usage response. + * + * @param <T> The request type + * @param <V> The response type + */ + public static <T, V> Builder<T, V> usage() { + return new Builder<T, V>(Status.USAGE); + } + + /** + * A {@code ResponseBody} builder. + * + * @param <T> The request type + * @param <V> The response type + */ + public static class Builder<T, V> { + + private String name; + private String status; + private String url; + private ResponseMetadata metadata; + private T request; + private V response; + + private Builder(String status) { + this.status = status; + } + + /** + * Set the metadata. + * + * @param metadata The servie metadata + */ + public Builder<T, V> metadata(ResponseMetadata metadata) { + this.metadata = metadata; + return this; + } + + /** + * Set the service name. + * + * @param name of the service being called + */ + public Builder<T, V> name(String name) { + this.name = name; + return this; + } + + /** + * Set the request object. + * + * @param request object + */ + public Builder<T, V> request(T request) { + this.request = request; + return this; + } + + /** + * Set the response object. + * + * @param response object + */ + public Builder<T, V> response(V response) { + this.response = response; + return this; + } + + /** + * Set the url used to call the service. + * + * @param url used to generate this response + */ + public Builder<T, V> url(String url) { + this.url = url; + return this; + } + + /** + * Returns a new Response + */ + public ResponseBody<T, V> build() { + checkNotNull(metadata); + checkNotNull(name); + checkNotNull(request); + checkNotNull(response); + checkNotNull(url); + checkNotNull(status); + return new ResponseBody<T, V>(this); + } + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/internal/www/ResponseMetadata.java b/src/main/java/gov/usgs/earthquake/nshmp/internal/www/ResponseMetadata.java new file mode 100644 index 0000000000000000000000000000000000000000..f526c118c7ebf0cc301e9ef66c1d285c7bedf188 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/internal/www/ResponseMetadata.java @@ -0,0 +1,18 @@ +package gov.usgs.earthquake.nshmp.internal.www; + +import gov.usgs.earthquake.nshmp.internal.AppVersion.VersionInfo; + +/** + * The response metadata with version info. + */ +public class ResponseMetadata { + public final VersionInfo[] repositories; + + public ResponseMetadata(VersionInfo... repositories) { + this.repositories = repositories; + } + + public VersionInfo[] getRepositories() { + return repositories; + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/internal/www/WsUtils.java b/src/main/java/gov/usgs/earthquake/nshmp/internal/www/WsUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..e45eb6f8da8150a5956fc5f1308132deb1956a40 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/internal/www/WsUtils.java @@ -0,0 +1,100 @@ +package gov.usgs.earthquake.nshmp.internal.www; + +import java.lang.reflect.Type; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +import com.google.common.collect.Range; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import gov.usgs.earthquake.nshmp.gmm.GmmInput; +import gov.usgs.earthquake.nshmp.gmm.GmmInput.Field; + +/** + * Web service utilities. + * + * @author U.S. Geological Survey + */ +public class WsUtils { + + public static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern( + "yyyy-MM-dd'T'HH:mm:ssXXX"); + + public static <T, E extends Enum<E>> T checkValue(E key, T value) { + if (value == null) { + throw new IllegalStateException("Missing [" + key.toString() + "]"); + } + + return value; + } + + /* Constrain all doubles to 8 decimal places */ + public static final class DoubleSerializer implements JsonSerializer<Double> { + @Override + public JsonElement serialize(Double d, Type type, JsonSerializationContext context) { + double dOut = Double.valueOf(String.format("%.8g", d)); + return new JsonPrimitive(dOut); + } + } + + /* Convert NaN to null */ + public static final class NaNSerializer implements JsonSerializer<Double> { + @Override + public JsonElement serialize(Double d, Type type, JsonSerializationContext context) { + return Double.isNaN(d) ? null : new JsonPrimitive(d); + } + } + + public static final class ConstraintsSerializer implements JsonSerializer<GmmInput.Constraints> { + @Override + public JsonElement serialize( + GmmInput.Constraints constraints, + Type type, + JsonSerializationContext context) { + JsonArray json = new JsonArray(); + + for (Field field : Field.values()) { + Optional<?> opt = constraints.get(field); + if (opt.isPresent()) { + Range<?> value = (Range<?>) opt.orElseThrow(); + Constraint constraint = new Constraint( + field.id, + value.lowerEndpoint(), + value.upperEndpoint()); + json.add(context.serialize(constraint)); + } + } + + return json; + } + } + + public static final class EnumSerializer<E extends Enum<E>> implements JsonSerializer<E> { + @Override + public JsonElement serialize(E src, Type type, JsonSerializationContext context) { + JsonObject jObj = new JsonObject(); + jObj.addProperty("value", src.name()); + jObj.addProperty("display", src.toString()); + + return jObj; + } + } + + @SuppressWarnings("unused") + private static class Constraint { + final String id; + final Object min; + final Object max; + + Constraint(String id, Object min, Object max) { + this.id = id; + this.min = min; + this.max = max; + } + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/internal/www/meta/EnumParameter.java b/src/main/java/gov/usgs/earthquake/nshmp/internal/www/meta/EnumParameter.java new file mode 100644 index 0000000000000000000000000000000000000000..2e0956fef9f521de38c04d6e3363bea23b41388c --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/internal/www/meta/EnumParameter.java @@ -0,0 +1,22 @@ +package gov.usgs.earthquake.nshmp.internal.www.meta; + +import java.util.Set; + +/** + * An enum parameter. + * + * @author U.S. Geological Survey + * + * @param <E> The enum type + */ +public final class EnumParameter<E extends Enum<E>> { + + private final String label; + private final Set<E> values; + + public EnumParameter(String label, Set<E> values) { + this.label = label; + this.values = values; + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/internal/www/meta/Status.java b/src/main/java/gov/usgs/earthquake/nshmp/internal/www/meta/Status.java new file mode 100644 index 0000000000000000000000000000000000000000..b41c20599cad92cd3c5213a48e28d7f1c93d4276 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/internal/www/meta/Status.java @@ -0,0 +1,18 @@ +package gov.usgs.earthquake.nshmp.internal.www.meta; + +/** + * Service request status identifier. + * + * @author U.S. Geological Survey + */ +public class Status { + + /** Error reponse status. */ + public static final String ERROR = "error"; + + /** Success reponse status. */ + public static final String SUCCESS = "success"; + + /** Usage reponse status. */ + public static final String USAGE = "usage"; +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/internal/www/meta/StringParameter.java b/src/main/java/gov/usgs/earthquake/nshmp/internal/www/meta/StringParameter.java new file mode 100644 index 0000000000000000000000000000000000000000..14a5ab1f4e8a8841be577d6f704a9fad3867fc44 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/internal/www/meta/StringParameter.java @@ -0,0 +1,20 @@ +package gov.usgs.earthquake.nshmp.internal.www.meta; + +import java.util.Set; + +/** + * A string parameter. + * + * @author U.S. Geological Survey + */ +public class StringParameter { + + public final String label; + public final Set<String> values; + + public StringParameter(String label, Set<String> values) { + this.label = label; + this.values = values; + } + +}