diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0f1b4c2..68bffdf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,18 +14,20 @@ retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref= "retrofit" retrofit-jackson = { module = "com.squareup.retrofit2:converter-jackson", version.ref= "retrofit" } retrofit-scalars = { module = "com.squareup.retrofit2:converter-scalars", version.ref= "retrofit" } -dropwizard-core = { module = "io.dropwizard:dropwizard-core", version.ref = "dropwizard" } dropwizard-assets = { module = "io.dropwizard:dropwizard-assets", version.ref = "dropwizard" } +dropwizard-core = { module = "io.dropwizard:dropwizard-core", version.ref = "dropwizard" } +dropwizard-swagger = { module = "com.smoketurner:dropwizard-swagger", version = "4.0.5-1" } jersey-media-sse = { module = "org.glassfish.jersey.media:jersey-media-sse", version = "3.1.5" } jdbf = { module = "com.github.spyhunter99:jdbf", version = "2.2.4" } junit = { module = "junit:junit", version = "4.13.2" } +swagger-annotations = { module = "io.swagger:swagger-annotations", version = "1.6.14" } + [bundles] jackson = ["jackson-bind", "jackson-yaml", "jackson-xml", "jackson-jsr310", "jackson-jdk8"] retrofit = ["retrofit", "retrofit-jackson", "retrofit-scalars"] -dropwizard = ["dropwizard-core", "dropwizard-assets"] [plugins] versions = { id = "com.github.ben-manes.versions", version = "0.51.0" } diff --git a/verkeersdrukte/build.gradle b/verkeersdrukte/build.gradle index 212697c..467be4d 100644 --- a/verkeersdrukte/build.gradle +++ b/verkeersdrukte/build.gradle @@ -7,10 +7,12 @@ application { dependencies { implementation libs.bundles.retrofit implementation libs.bundles.jackson - implementation libs.bundles.dropwizard - implementation libs.jdbf - // https://mvnrepository.com/artifact/org.glassfish.jersey.media/jersey-media-sse + implementation libs.dropwizard.core + implementation libs.dropwizard.assets + implementation libs.dropwizard.swagger + implementation libs.jdbf implementation libs.jersey.media.sse + implementation libs.swagger.annotations } diff --git a/verkeersdrukte/src/main/java/nl/bertriksikken/verkeersdrukte/app/IVerkeersDrukteResource.java b/verkeersdrukte/src/main/java/nl/bertriksikken/verkeersdrukte/app/IVerkeersDrukteResource.java new file mode 100644 index 0000000..9513574 --- /dev/null +++ b/verkeersdrukte/src/main/java/nl/bertriksikken/verkeersdrukte/app/IVerkeersDrukteResource.java @@ -0,0 +1,44 @@ +package nl.bertriksikken.verkeersdrukte.app; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.servers.Server; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.sse.Sse; +import jakarta.ws.rs.sse.SseEventSink; +import nl.bertriksikken.geojson.FeatureCollection; + +import java.net.URISyntaxException; +import java.util.Optional; + +@OpenAPIDefinition( + info = @Info( + title = "Verkeersdrukte", + description = "Provides near real-time speed/intensity data for motorways in the Netherlands", + contact = @Contact(name = "Bertrik Sikken", email = "bertrik@gmail.com")), + servers = {@Server(url = "https://stofradar.nl"), @Server(url = "http://stofradar.nl:9002")}, + tags = {@Tag(name = "static"), @Tag(name = "dynamic")}) +public interface IVerkeersDrukteResource { + @Operation(hidden = true) + void redirectSwagger() throws URISyntaxException; + + @Operation(summary = "Get GeoJSON containing all locations") + @Tag(name = "static") + FeatureCollection getStatic(); + + @Operation(summary = "Get static data for a specific location") + @Tag(name = "static") + Optional getStatic(@PathParam("location") String location); + + @Operation(summary = "Get dynamic traffic data for a specific location") + @Tag(name = "dynamic") + Optional getDynamic(@PathParam("location") String location); + + @Operation(summary = "Get event stream with dynamic traffic data for a specific location") + @Tag(name = "dynamic") + void getTrafficEvents(@Context Sse sse, @Context SseEventSink sseEventSink, @PathParam("location") String location); +} diff --git a/verkeersdrukte/src/main/java/nl/bertriksikken/verkeersdrukte/app/VerkeersDrukteApp.java b/verkeersdrukte/src/main/java/nl/bertriksikken/verkeersdrukte/app/VerkeersDrukteApp.java index 87d0b20..884efd3 100644 --- a/verkeersdrukte/src/main/java/nl/bertriksikken/verkeersdrukte/app/VerkeersDrukteApp.java +++ b/verkeersdrukte/src/main/java/nl/bertriksikken/verkeersdrukte/app/VerkeersDrukteApp.java @@ -4,8 +4,11 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import io.dropwizard.assets.AssetsBundle; import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; import io.dropwizard.core.setup.Bootstrap; import io.dropwizard.core.setup.Environment; +import io.federecio.dropwizard.swagger.SwaggerBundle; +import io.federecio.dropwizard.swagger.SwaggerBundleConfiguration; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseFilter; @@ -29,6 +32,10 @@ private VerkeersDrukteApp() { public void initialize(Bootstrap bootstrap) { bootstrap.getObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); bootstrap.addBundle(new AssetsBundle("/assets/verkeersdrukte.png", "/favicon.ico")); + + SwaggerBundleConfiguration swaggerBundleConfiguration = new SwaggerBundleConfiguration(); + swaggerBundleConfiguration.setResourcePackage(VerkeersDrukteResource.class.getPackage().getName()); + bootstrap.addBundle(new TrafficSwaggerBundle(swaggerBundleConfiguration)); } @Override @@ -46,7 +53,7 @@ public void run(VerkeersDrukteAppConfig configuration, Environment environment) } private void addHeaders(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { - headers.forEach((header,value) -> responseContext.getHeaders().add(header, value)); + headers.forEach((header, value) -> responseContext.getHeaders().add(header, value)); } public static void main(String[] args) throws Exception { @@ -63,4 +70,17 @@ public static void main(String[] args) throws Exception { app.run("server", CONFIG_FILE); } + private static final class TrafficSwaggerBundle extends SwaggerBundle { + private final SwaggerBundleConfiguration swaggerBundleConfiguration; + + TrafficSwaggerBundle(SwaggerBundleConfiguration configuration) { + this.swaggerBundleConfiguration = configuration; + } + + @Override + protected SwaggerBundleConfiguration getSwaggerBundleConfiguration(Configuration configuration) { + return swaggerBundleConfiguration; + } + } + } diff --git a/verkeersdrukte/src/main/java/nl/bertriksikken/verkeersdrukte/app/VerkeersDrukteResource.java b/verkeersdrukte/src/main/java/nl/bertriksikken/verkeersdrukte/app/VerkeersDrukteResource.java index 8f90a10..48aa629 100644 --- a/verkeersdrukte/src/main/java/nl/bertriksikken/verkeersdrukte/app/VerkeersDrukteResource.java +++ b/verkeersdrukte/src/main/java/nl/bertriksikken/verkeersdrukte/app/VerkeersDrukteResource.java @@ -20,6 +20,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import java.net.URI; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.Locale; @@ -32,7 +33,7 @@ @Path(VerkeersDrukteResource.TRAFFIC_PATH) @Produces(MediaType.APPLICATION_JSON) -public final class VerkeersDrukteResource { +public final class VerkeersDrukteResource implements IVerkeersDrukteResource { private static final Logger LOG = LoggerFactory.getLogger(VerkeersDrukteResource.class); static final String TRAFFIC_PATH = "/traffic"; @@ -52,6 +53,14 @@ public final class VerkeersDrukteResource { mapper.findAndRegisterModules(); } + @Override + @GET + @Path("/") + public void redirectSwagger() { + throw new RedirectionException(301, URI.create("/swagger")); + } + + @Override @GET @Path(STATIC_PATH) public FeatureCollection getStatic() { @@ -81,6 +90,7 @@ private FeatureCollection.Feature addUrlProperties(FeatureCollection.Feature f) return feature; } + @Override @GET @Path(STATIC_PATH + "/{location}") public Optional getStatic(@PathParam("location") String location) { @@ -91,6 +101,7 @@ public Optional getStatic(@PathParam("location") Stri return Optional.ofNullable(feature); } + @Override @GET @Path(DYNAMIC_PATH + "/{location}") @CacheControl(maxAge = 1, maxAgeUnit = TimeUnit.MINUTES) @@ -103,6 +114,7 @@ public Optional getDynamic(@PathParam("location") String loca return Optional.of(new MeasurementResult(aggregateMeasurement)); } + @Override @GET @Path(DYNAMIC_PATH + "/{location}/events") @Produces(MediaType.SERVER_SENT_EVENTS)