From 3e639c98174bbd01292438613be0e88561d57017 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Mon, 22 Jan 2024 11:41:08 -0800 Subject: [PATCH 01/16] Async related test files. --- .../src/main/java/cwms/cda/ApiServlet.java | 72 ++++++--- .../cwms/cda/api/ClobAsyncController.java | 153 ++++++++++++++++++ .../java/cwms/cda/api/PingController.java | 132 +++++++++++++++ cwms-data-api/src/main/webapp/WEB-INF/web.xml | 1 + cwms-data-api/src/main/webapp/js/ws_demo.js | 31 ++++ cwms-data-api/src/main/webapp/sse_demo.html | 32 ++++ cwms-data-api/src/main/webapp/ws_demo.html | 17 ++ 7 files changed, 414 insertions(+), 24 deletions(-) create mode 100644 cwms-data-api/src/main/java/cwms/cda/api/ClobAsyncController.java create mode 100644 cwms-data-api/src/main/java/cwms/cda/api/PingController.java create mode 100644 cwms-data-api/src/main/webapp/js/ws_demo.js create mode 100644 cwms-data-api/src/main/webapp/sse_demo.html create mode 100644 cwms-data-api/src/main/webapp/ws_demo.html diff --git a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java index b9b1ed807..c94b4e801 100644 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java @@ -24,6 +24,12 @@ package cwms.cda; +import static io.javalin.apibuilder.ApiBuilder.crud; +import static io.javalin.apibuilder.ApiBuilder.get; +import static io.javalin.apibuilder.ApiBuilder.prefixPath; +import static io.javalin.apibuilder.ApiBuilder.sse; +import static io.javalin.apibuilder.ApiBuilder.staticInstance; + import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.servlets.MetricsServlet; @@ -35,6 +41,7 @@ import cwms.cda.api.BlobController; import cwms.cda.api.CatalogController; import cwms.cda.api.ClobController; +import cwms.cda.api.ClobAsyncController; import cwms.cda.api.Controllers; import cwms.cda.api.CountyController; import cwms.cda.api.LevelsController; @@ -43,6 +50,7 @@ import cwms.cda.api.LocationGroupController; import cwms.cda.api.OfficeController; import cwms.cda.api.ParametersController; +import cwms.cda.api.PingController; import cwms.cda.api.PoolController; import cwms.cda.api.RatingController; import cwms.cda.api.RatingMetadataController; @@ -91,21 +99,6 @@ import io.swagger.v3.oas.models.PathItem; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.security.SecurityRequirement; -import java.util.concurrent.TimeUnit; -import org.apache.http.entity.ContentType; -import org.jetbrains.annotations.NotNull; -import org.owasp.html.HtmlPolicyBuilder; -import org.owasp.html.PolicyFactory; - -import javax.annotation.Resource; -import javax.management.ServiceNotFoundException; -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.sql.DataSource; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; @@ -117,9 +110,21 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.jar.Manifest; - -import static io.javalin.apibuilder.ApiBuilder.*; +import javax.annotation.Resource; +import javax.management.ServiceNotFoundException; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.sql.DataSource; +import org.apache.http.entity.ContentType; +import org.jetbrains.annotations.NotNull; +import org.owasp.html.HtmlPolicyBuilder; +import org.owasp.html.PolicyFactory; /** @@ -144,8 +149,13 @@ "/blobs/*", "/clobs/*", "/pools/*", - "/specified-levels/*" -}) + "/specified-levels/*", + "/sse/*", + "/ws/*", + "/ping/*" +}, + asyncSupported = true +) public class ApiServlet extends HttpServlet { public static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -178,7 +188,6 @@ public class ApiServlet extends HttpServlet { DataSource cwms; - @Override public void destroy() { javalin.destroy(); @@ -395,8 +404,22 @@ protected void configureRoutes() { new PoolController(metrics), requiredRoles,5, TimeUnit.MINUTES); cdaCrudCache("/specified-levels/{specified-level-id}", new SpecifiedLevelController(metrics), requiredRoles,5, TimeUnit.MINUTES); + + cdaCrud("/ping/{ping-id}", + new PingController(), requiredRoles); + + + ClobAsyncController clobSSEController = new ClobAsyncController(); + + sse("/sse/clob", clobSSEController.getSseConsumer()); + + staticInstance().ws("/ws/clob", clobSSEController.getWsConsumer()); + + } + + /** * This method delegates to the cdaCrud method but also adds an after filter for the specified * path. If the request was a GET request and the response does not already include @@ -470,6 +493,7 @@ public static void cdaCrud(@NotNull String path, @NotNull CrudHandler crudHandle instance.delete(fullPath, crudFunctions.get(CrudFunction.DELETE), roles); } + /** * Given a path like "/location/category/{category-id}" this method returns "{category-id}" * @param fullPath @@ -510,7 +534,7 @@ private void getOpenApiOptions(JavalinConfig config) { CdaAccessManager am = buildAccessManager(provider); Components components = new Components(); final ArrayList secReqs = new ArrayList<>(); - am.getContainedManagers().forEach((manager)->{ + am.getContainedManagers().forEach(manager -> { components.addSecuritySchemes(manager.getName(),manager.getScheme()); SecurityRequirement req = new SecurityRequirement(); if (!manager.getName().equalsIgnoreCase("guestauth") && !manager.getName().equalsIgnoreCase("noauth")) { @@ -529,9 +553,7 @@ private void getOpenApiOptions(JavalinConfig config) { ); ops.path("/swagger-docs") .responseModifier((ctx,api) -> { - api.getPaths().forEach((key,path) -> { - setSecurityRequirements(key,path,secReqs); - }); + api.getPaths().forEach((key,path) -> setSecurityRequirements(key,path,secReqs)); return api; }) .defaultDocumentation(doc -> { @@ -544,6 +566,8 @@ private void getOpenApiOptions(JavalinConfig config) { .activateAnnotationScanningFor("cwms.cda.api"); config.registerPlugin(new OpenApiPlugin(ops)); + + } private static void setSecurityRequirements(String key, PathItem path,List secReqs) { diff --git a/cwms-data-api/src/main/java/cwms/cda/api/ClobAsyncController.java b/cwms-data-api/src/main/java/cwms/cda/api/ClobAsyncController.java new file mode 100644 index 000000000..94105c793 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/ClobAsyncController.java @@ -0,0 +1,153 @@ +package cwms.cda.api; + +import com.google.common.flogger.FluentLogger; +import cwms.cda.data.dao.JooqDao; +import io.javalin.http.Context; +import io.javalin.http.sse.SseClient; +import io.javalin.websocket.WsConfig; +import java.io.IOException; +import java.io.Reader; +import java.sql.Clob; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; +import java.util.function.Consumer; +import org.jooq.DSLContext; + +public class ClobAsyncController { + public static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + public Consumer getSseConsumer() { + return this::accept; + } + + public Consumer getWsConsumer() { + logger.atInfo().log("getWsConsumer"); + return this::acceptWs; + } + + private void acceptWs(WsConfig ws) { + logger.atInfo().log("acceptWs"); + + ws.onConnect(ctx -> { + logger.atInfo().log("ws onConnect"); + + String clobId = ctx.queryParam("id"); + String officeId= ctx.queryParam("officeId"); + + ctx.send("Hello clob Client! "); + + new Thread(() -> sendClob(null, officeId, clobId, reader -> { + sendToClient(reader, str -> ctx.send( str)); + })).start(); + + }); + ws.onClose(ctx -> { + logger.atInfo().log("ws onClose"); + }); + ws.onMessage(ctx -> { + logger.atInfo().log("ws onMessage"); + }); + ws.onError(ctx -> { + logger.atInfo().log("ws onError"); + }); + + } + + public void accept(SseClient client) { + String clobId = client.ctx.queryParam("id"); + String officeId= client.ctx.queryParam("officeId"); + + logger.atInfo().log("got an sse clob request for:" + clobId ); + + client.sendEvent("retry", "10000\n\n", null); // could not get this to work... + + client.sendEvent("message", "Hello clob Client! Get ready for your clob!\n"); + client.onClose(() -> logger.atInfo().log("Clob client left.")); + + // call sendClob on new thread + new Thread(() -> sendClob(client.ctx, officeId, clobId, reader -> { +// client.sendEvent("message", "clob data would go here\n"); + sendToClient(reader, str -> client.sendEvent("message", str)); + client.sendEvent("close", ""); // normally the server just closing the connection would make the browser restart their side. +// client.close(); + })).start(); + } + + private static void sendToClient( Reader reader, Consumer strConsumer) { + // read from reader in chunks and send to client + char[] buffer = new char[32768]; + long total = 0; + int bytesRead = 0; + try { + while (true) { + if ((bytesRead = reader.read(buffer)) == -1) break; + total += bytesRead; + logger.atInfo().log("Sending " + bytesRead + " bytes to client. Total sent: " + total); + strConsumer.accept(new String(buffer, 0, bytesRead)); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + + private static void sendClob(Context ctx, String officeId, String clobId, Consumer readerConsumer) { + DSLContext dslContext = JooqDao.getDslContext(ctx); + dslContext.connection( + connection -> { + String sql = "select cwms_20.AV_CLOB.VALUE from " + + "cwms_20.av_clob join cwms_20.av_office on av_clob.office_code = av_office.office_code " + + "where av_office.office_id = ? and av_clob.id = ?"; + try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) { + preparedStatement.setString(1, officeId); + preparedStatement.setString(2, clobId); + + try (ResultSet resultSet = preparedStatement.executeQuery()) { + if (resultSet.next()) { + // Get the CLOB column + Clob clob = resultSet.getClob("VALUE"); + if(clob != null) { + // Open a Reader to stream CLOB data + try (Reader reader = clob.getCharacterStream()) { + if (reader != null) { + readerConsumer.accept(reader); + } else { + logger.atInfo().log("clob.getCharacterStream returned null."); + } + + } + } else { + logger.atInfo().log("clob returned for " + clobId + " was null."); + } + } + } + } catch (Exception e) { + logger.atSevere().withCause(e).log("Error getting clob."); + throw new RuntimeException(e); + } + } + ); + } + + + public static String getNowStr() { + OffsetDateTime currentDateTimeWithOffset = OffsetDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z"); + return currentDateTimeWithOffset.format(formatter); + } + + private static Optional getUser(Context ctx) { + Optional retval = Optional.empty(); + if (ctx != null && ctx.req != null && ctx.req.getUserPrincipal() != null) { + retval = Optional.of(ctx.req.getUserPrincipal().getName()); + } else { + logger.atFine().log( "No user principal found in request."); + } + return retval; + } + +} diff --git a/cwms-data-api/src/main/java/cwms/cda/api/PingController.java b/cwms-data-api/src/main/java/cwms/cda/api/PingController.java new file mode 100644 index 000000000..60a608231 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/PingController.java @@ -0,0 +1,132 @@ +package cwms.cda.api; + +import static cwms.cda.api.Controllers.STATUS_200; +import static cwms.cda.api.Controllers.STATUS_404; +import static cwms.cda.api.Controllers.STATUS_501; + +import cwms.cda.data.dto.Clob; +import cwms.cda.data.dto.Pools; +import cwms.cda.formatters.Formats; +import io.javalin.apibuilder.CrudHandler; +import io.javalin.http.Context; +import io.javalin.plugin.openapi.annotations.HttpMethod; +import io.javalin.plugin.openapi.annotations.OpenApi; +import io.javalin.plugin.openapi.annotations.OpenApiContent; +import io.javalin.plugin.openapi.annotations.OpenApiParam; +import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; +import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.jetbrains.annotations.NotNull; + +public class PingController implements CrudHandler { + + private static final Map entries = new LinkedHashMap<>(); + + public static final String TAG = "Ping"; + + private static final AtomicInteger next = new AtomicInteger(0); + + + @OpenApi( + description = "Create new Ping", + requestBody = @OpenApiRequestBody( + content = {@OpenApiContent(type = Formats.JSON)}, + required = false), + responses = { + @OpenApiResponse(status = STATUS_200, content = { + @OpenApiContent(type = Formats.JSON)}), + }, + method = HttpMethod.POST, + tags = {TAG} + ) + @Override + public void create(@NotNull Context ctx) { + int nextId = next.incrementAndGet(); + + String id = Integer.toString(nextId); + entries.put(id, getNowStr()); + ctx.json(entries.get(id)); + ctx.status(200); + } + + @OpenApi( + description = "Deletes a ping", + pathParams = { + @OpenApiParam(name = "id", description = "The id to be deleted"), + }, + responses = { + @OpenApiResponse(status = "204", content = { @OpenApiContent(type = Formats.JSON)}), + @OpenApiResponse(status = STATUS_404), + }, + method = HttpMethod.DELETE, + tags = {TAG} + ) + @Override + public void delete(@NotNull Context ctx, @NotNull String id) { + + if (entries.containsKey(id)) { + entries.remove(id); + ctx.status(204); + } else { + ctx.status(404); + } + } + + @OpenApi( + tags = {TAG} + ) + @Override + public void getAll(@NotNull Context context) { + context.json(entries); + } + + @OpenApi( + pathParams = { + @OpenApiParam(name = "id", required = true, description = "Specifies the id."), + }, + responses = { + @OpenApiResponse(status = "200", content = { @OpenApiContent(type = Formats.JSON)}), + @OpenApiResponse(status = STATUS_404), + }, + description = "Retrieves a single ping entry or 404 if not found.", tags = {"Pools"}) + @Override + public void getOne(Context ctx, String id) { + if (entries.containsKey(id)) { + ctx.json(entries.get(id)); + } else { + ctx.status(404); + } + } + + @OpenApi( + description = "Updates a ping", + pathParams = { + @OpenApiParam(name = "id", description = "The id to be updated"), + }, + responses = { + @OpenApiResponse(status = "200", content = { @OpenApiContent(type = Formats.JSON)}), + @OpenApiResponse(status = STATUS_404), + }, + method = HttpMethod.PATCH, + tags = {TAG} + ) + public void update(@NotNull Context ctx, @NotNull String id) { + if (entries.containsKey(id)) { + entries.put(id, getNowStr()); + ctx.json(entries.get(id)); + } else { + ctx.status(404); + } + } + + public static String getNowStr() { + OffsetDateTime currentDateTimeWithOffset = OffsetDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z"); + return currentDateTimeWithOffset.format(formatter); + } +} diff --git a/cwms-data-api/src/main/webapp/WEB-INF/web.xml b/cwms-data-api/src/main/webapp/WEB-INF/web.xml index 2393fa8ed..0fcf9b8db 100644 --- a/cwms-data-api/src/main/webapp/WEB-INF/web.xml +++ b/cwms-data-api/src/main/webapp/WEB-INF/web.xml @@ -29,6 +29,7 @@ cors.allowed.methods GET,HEAD,OPTIONS + true CorsFilter diff --git a/cwms-data-api/src/main/webapp/js/ws_demo.js b/cwms-data-api/src/main/webapp/js/ws_demo.js new file mode 100644 index 000000000..396a60e29 --- /dev/null +++ b/cwms-data-api/src/main/webapp/js/ws_demo.js @@ -0,0 +1,31 @@ +// small helper function for selecting element by id +let id = id => document.getElementById(id); + +let clobId = "/TIME SERIES TEXT/6261044"; + +let addr = "ws://localhost:7000/cwms-data/ws/clob" + // + "?id=" + encodeURI(clobId) + "&officeId=SPK" +let ws = new WebSocket(addr); +ws.onmessage = msg => updateChat(msg); +// ws.onclose = () => alert("WebSocket connection closed"); + +// Add event listeners to button and input field +id("send").addEventListener("click", () => sendAndClear(id("message").value)); +id("message").addEventListener("keypress", function (e) { + if (e.keyCode === 13) { // Send message if enter is pressed in input field + sendAndClear(e.target.value); + } +}); + +function sendAndClear(message) { + if (message !== "") { + ws.send(message); + id("message").value = ""; + } +} + +function updateChat(msg) { // Update chat-panel and list of connected users + let data = JSON.parse(msg.data); + id("chat").insertAdjacentHTML("afterbegin", data.userMessage); + id("userlist").innerHTML = data.userlist.map(user => "
  • " + user + "
  • ").join(""); +} \ No newline at end of file diff --git a/cwms-data-api/src/main/webapp/sse_demo.html b/cwms-data-api/src/main/webapp/sse_demo.html new file mode 100644 index 000000000..79366e542 --- /dev/null +++ b/cwms-data-api/src/main/webapp/sse_demo.html @@ -0,0 +1,32 @@ + + + + +

    Getting server updates

    +
    + + + + + diff --git a/cwms-data-api/src/main/webapp/ws_demo.html b/cwms-data-api/src/main/webapp/ws_demo.html new file mode 100644 index 000000000..76abc79ef --- /dev/null +++ b/cwms-data-api/src/main/webapp/ws_demo.html @@ -0,0 +1,17 @@ + + + + + WebsSockets + + + +
    + + +
    +
    +
    + + + \ No newline at end of file From d83789780fb7c32d29cde09ab43d3d1b6a8892f4 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Mon, 22 Jan 2024 11:48:20 -0800 Subject: [PATCH 02/16] Added clob insert code. --- .../java/cwms/cda/data/dao/InsertClob.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 cwms-data-api/src/test/java/cwms/cda/data/dao/InsertClob.java diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/InsertClob.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/InsertClob.java new file mode 100644 index 000000000..e0cc96ee8 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/InsertClob.java @@ -0,0 +1,122 @@ +package cwms.cda.data.dao; + +import static cwms.cda.data.dao.DaoTest.getDslContext; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import org.jooq.DSLContext; +import usace.cwms.db.jooq.codegen.packages.CWMS_TEXT_PACKAGE; + +public class InsertClob { + + + public static final String OFFICE = "SPK"; + String clobId = "/TIME SERIES TEXT/6261044"; + String filePath = "C:\\Temp\\out.log"; // just a biggish text file. + +// @Test //not an actual test + void test_insert() throws SQLException, FileNotFoundException { + + long millis = System.currentTimeMillis(); + + DSLContext dsl = getDslContext(OFFICE); + + + try { + dsl.connection(c -> { + CWMS_TEXT_PACKAGE.call_STORE_TEXT( + dsl.configuration(), + "asdf", + clobId, + "this is a clob for a test", + ClobDao.getBoolean(true), + OFFICE); + }); + } catch (Exception e) { + System.out.println("Exception - This is fine. " +// + e.getMessage() + ); + // Don't really care if it inserted or already existed. Just needs to exist for this + // next bit. + } + + + // Get a binary stream from filepath + + File file = new File(filePath); + + assert(file.exists()); + assert(file.length() > 1000); + + dsl.connection(connection -> { + updateClobValue(connection, OFFICE, clobId, file); + }); + + + } + +// @Test // This isn't a real test - just a way to load a bunch of data... + void manual_connection() throws SQLException, ClassNotFoundException, IOException { + + String cdaJdbcUrl = System.getenv("CDA_JDBC_URL"); + if (cdaJdbcUrl == null) { + cdaJdbcUrl = System.getProperty("CDA_JDBC_URL"); + } + + String username = System.getenv("CDA_JDBC_USERNAME"); + if (username == null) { + username = System.getProperty("CDA_JDBC_USERNAME"); + } + + String password = System.getenv("CDA_JDBC_PASSWORD"); + if (password == null) { + password = System.getProperty("CDA_JDBC_PASSWORD"); + } + + + // File path and size + File file = new File(filePath); + System.out.println("File exists: " + file.exists() + " size: " + file.length()); + + // Load the Oracle JDBC driver + Class.forName("oracle.jdbc.driver.OracleDriver"); + + // Establish the database connection + Connection connection = DriverManager.getConnection(cdaJdbcUrl, username, password); + + updateClobValue(connection, OFFICE, clobId, file); // 42Mb took 4.4s + + connection.close(); + } + + private static void updateClobValue(Connection connection, String office, String clobId, + File file) throws SQLException, IOException { + + + String sql = "UPDATE CWMS_20.AV_CLOB\n" + + "SET VALUE = ?\n" + + "where CWMS_20.AV_CLOB.ID = ?\n" + + " and CWMS_20.AV_CLOB.OFFICE_CODE in (select AV_OFFICE.OFFICE_CODE from CWMS_20.AV_OFFICE where AV_OFFICE.OFFICE_ID = ?)"; + + Reader reader = new FileReader(file ); + + try(PreparedStatement preparedStatement = connection.prepareStatement(sql)) { + preparedStatement.setCharacterStream(1, reader); + preparedStatement.setString(2, clobId); + preparedStatement.setString(3, office); + System.out.println("Executing statement: "); + preparedStatement.executeUpdate(); + } + + System.out.println("CLOB data updated successfully with reader data."); + } + + +} From 8c118024c54bdccb435f12f1461a7e47c087d7e6 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Wed, 31 Jan 2024 10:31:50 -0800 Subject: [PATCH 03/16] switched to create instead of createStandalone and work-arounds for not deploying into tomcat. --- .../src/main/java/cwms/cda/ApiServlet.java | 191 ++++++++++++++++-- 1 file changed, 171 insertions(+), 20 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java index c94b4e801..ca69c884e 100644 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java @@ -29,6 +29,7 @@ import static io.javalin.apibuilder.ApiBuilder.prefixPath; import static io.javalin.apibuilder.ApiBuilder.sse; import static io.javalin.apibuilder.ApiBuilder.staticInstance; +import static io.javalin.apibuilder.ApiBuilder.ws; import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistry; @@ -40,8 +41,8 @@ import cwms.cda.api.BasinController; import cwms.cda.api.BlobController; import cwms.cda.api.CatalogController; -import cwms.cda.api.ClobController; import cwms.cda.api.ClobAsyncController; +import cwms.cda.api.ClobController; import cwms.cda.api.Controllers; import cwms.cda.api.CountyController; import cwms.cda.api.LevelsController; @@ -74,7 +75,6 @@ import cwms.cda.api.errors.JsonFieldsException; import cwms.cda.api.errors.NotFoundException; import cwms.cda.data.dao.JooqDao; -import cwms.cda.formatters.Formats; import cwms.cda.formatters.FormattingException; import cwms.cda.security.CwmsAuthException; import cwms.cda.security.Role; @@ -91,6 +91,7 @@ import io.javalin.http.BadRequestResponse; import io.javalin.http.Handler; import io.javalin.http.JavalinServlet; +import io.javalin.http.staticfiles.Location; import io.javalin.plugin.openapi.OpenApiOptions; import io.javalin.plugin.openapi.OpenApiPlugin; import io.swagger.v3.oas.models.Components; @@ -105,6 +106,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.sql.SQLException; import java.time.DateTimeException; import java.util.ArrayList; import java.util.Arrays; @@ -121,7 +123,15 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.sql.DataSource; +import oracle.jdbc.pool.OracleDataSource; import org.apache.http.entity.ContentType; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.StdErrLog; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.webapp.WebAppContext; import org.jetbrains.annotations.NotNull; import org.owasp.html.HtmlPolicyBuilder; import org.owasp.html.PolicyFactory; @@ -177,20 +187,27 @@ public class ApiServlet extends HttpServlet { public static final String DEFAULT_OFFICE_KEY = "cwms.dataapi.default.office"; public static final String DEFAULT_PROVIDER = "MultipleAccessManager"; - private MetricRegistry metrics; + private MetricRegistry metrics = new MetricRegistry(); private Meter totalRequests; private static final long serialVersionUID = 1L; - JavalinServlet javalin = null; + JavalinServlet javalinServlet = null; + public Javalin javalin = null; @Resource(name = "jdbc/CWMS3") DataSource cwms; + public ApiServlet() { + super(); + } + @Override public void destroy() { - javalin.destroy(); + if(javalinServlet != null) { + javalinServlet.destroy(); + } } @Override @@ -198,32 +215,81 @@ public void init(ServletConfig config) throws ServletException { logger.atInfo().log("Initializing CWMS Data API Version: " + obtainFullVersion(config)); metrics = (MetricRegistry)config.getServletContext() .getAttribute(MetricsServlet.METRICS_REGISTRY); + + if(metrics == null){ + metrics = new MetricRegistry(); + config.getServletContext().setAttribute(MetricsServlet.METRICS_REGISTRY, metrics); + } + totalRequests = metrics.meter("cwms.dataapi.total_requests"); super.init(config); } @SuppressWarnings({"java:S125","java:S2095"}) // closed in destroy handler @Override - public void init() { + public void init() throws ServletException { + super.init(); JavalinValidation.register(UnitSystem.class, UnitSystem::systemFor); JavalinValidation.register(JooqDao.DeleteMethod.class, Controllers::getDeleteMethod); + + javalin = createJavalin(); + + javalinServlet = javalin.javalinServlet(); + + } + + public Javalin createJavalin() { + ObjectMapper om = new ObjectMapper(); om.setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE); om.registerModule(new JavaTimeModule()); PolicyFactory sanitizer = new HtmlPolicyBuilder().disallowElements(" + - -
    - - -
    -
    -
    - + +

    Html5 WebSockets Example.

    +
    +

    Please input some text to Send :

    + + + + \ No newline at end of file From 490c102be8d85ee61e22d502d6bb971bf36fc4c5 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Wed, 31 Jan 2024 10:36:52 -0800 Subject: [PATCH 11/16] removing tomcat specific logging. --- cwms-data-api/logging.properties | 50 ++------------------------------ 1 file changed, 2 insertions(+), 48 deletions(-) diff --git a/cwms-data-api/logging.properties b/cwms-data-api/logging.properties index 5683fc81f..6ee4a4a6c 100644 --- a/cwms-data-api/logging.properties +++ b/cwms-data-api/logging.properties @@ -13,62 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -handlers = 1catalina.org.apache.juli.AsyncFileHandler, \ - 2localhost.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler +handlers = \ + java.util.logging.ConsoleHandler -.handlers = 1catalina.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler -############################################################ -# Handler specific properties. -# Describes specific configuration info for Handlers. -############################################################ - -1catalina.org.apache.juli.AsyncFileHandler.level = FINE -1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs -1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina. -1catalina.org.apache.juli.AsyncFileHandler.maxDays = 4 -1catalina.org.apache.juli.AsyncFileHandler.encoding = UTF-8 - -2localhost.org.apache.juli.AsyncFileHandler.level = FINE -2localhost.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs -2localhost.org.apache.juli.AsyncFileHandler.prefix = localhost. -2localhost.org.apache.juli.AsyncFileHandler.maxDays = 4 -2localhost.org.apache.juli.AsyncFileHandler.encoding = UTF-8 java.util.logging.ConsoleHandler.level = INFO java.util.logging.ConsoleHandler.formatter = org.apache.juli.OneLineFormatter java.util.logging.ConsoleHandler.encoding = UTF-8 -############################################################ -# Facility specific properties. -# Provides extra control for each logger. -############################################################ - -org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = INFO -org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers = 2localhost.org.apache.juli.AsyncFileHandler - -org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/cwms-data].level = FINE -org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/cwms-data].handlers = 2localhost.org.apache.juli.AsyncFileHandler - -# For example, set the org.apache.catalina.util.LifecycleBase logger to log -# each component that extends LifecycleBase changing state: -#org.apache.catalina.util.LifecycleBase.level = FINE - -# To see debug messages in TldLocationsCache, uncomment the following line: -#org.apache.jasper.compiler.TldLocationsCache.level = FINE - -# To see debug messages for HTTP/2 handling, uncomment the following line: -#org.apache.coyote.http2.level = FINE - -# To see debug messages for WebSocket handling, uncomment the following line: -#org.apache.tomcat.websocket.level = FINE - fixtures.level=FINE cwms.cda.datasource.level=FINE -org.apache.level=FINE -org.apache.catalina.realm.level=INFO -org.apache.catalina.realm.useParentHandlers=true -org.apache.catalina.authenticator.level=INFO -org.apache.catalina.authenticator.useParentHandlers=true From c0bc6c2656b7ec0a993270c99be99a06fe5e1182 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Wed, 31 Jan 2024 10:37:22 -0800 Subject: [PATCH 12/16] Added null check to avoid npe in ws request --- .../src/main/java/cwms/cda/data/dao/AuthDao.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/AuthDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/AuthDao.java index 09aaa3ea1..22a2f4c6f 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/AuthDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/AuthDao.java @@ -106,10 +106,12 @@ private AuthDao(DSLContext dsl, String defaultOffice) { */ public static AuthDao getInstance(DSLContext dsl, String defaultOffice) { AuthDao dao = instance.get(); - if (dao == null) + if (dao == null ) { - dao = new AuthDao(dsl,defaultOffice); - instance.set(dao); + if(dsl != null) { + dao = new AuthDao(dsl, defaultOffice); + instance.set(dao); + } } else { dao.resetContext(dsl); } From 3d2624a3d249b3347d2588f3b94a33725398f3c7 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Wed, 31 Jan 2024 10:38:22 -0800 Subject: [PATCH 13/16] removed tomcat specific filters. --- cwms-data-api/src/main/webapp/WEB-INF/web.xml | 29 +++++-------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/cwms-data-api/src/main/webapp/WEB-INF/web.xml b/cwms-data-api/src/main/webapp/WEB-INF/web.xml index 0fcf9b8db..1dc1f74bf 100644 --- a/cwms-data-api/src/main/webapp/WEB-INF/web.xml +++ b/cwms-data-api/src/main/webapp/WEB-INF/web.xml @@ -1,33 +1,20 @@ CWMS REST API - - metrics - com.codahale.metrics.servlets.AdminServlet - - - metrics - /status/* - - - - metrics-prometheus - io.prometheus.client.exporter.MetricsServlet - - - metrics-prometheus - /status/prometheus - CorsFilter - org.apache.catalina.filters.CorsFilter + org.eclipse.jetty.servlets.CrossOriginFilter - cors.allowed.origins + allowedOrigins * - cors.allowed.methods - GET,HEAD,OPTIONS + allowedMethods + * + + + allowedHeaders + * true From 48df9edf71b47ebcefae4280576b08808ff184e8 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Fri, 2 Feb 2024 13:22:17 -0800 Subject: [PATCH 14/16] Added a clob2 end-point for a second type of ws demo --- .../src/main/java/cwms/cda/ApiServlet.java | 5 +- .../cwms/cda/api/ClobAsyncController.java | 103 +++++++++++++++++- cwms-data-api/src/main/webapp/js/ws_demo.js | 2 +- cwms-data-api/src/main/webapp/js/ws_demo2.js | 98 +++++++++++++++++ cwms-data-api/src/main/webapp/sse_demo.html | 2 +- cwms-data-api/src/main/webapp/ws_demo2.html | 23 ++++ 6 files changed, 226 insertions(+), 7 deletions(-) create mode 100644 cwms-data-api/src/main/webapp/js/ws_demo2.js create mode 100644 cwms-data-api/src/main/webapp/ws_demo2.html diff --git a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java index ca69c884e..8fc5e90b3 100644 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java @@ -506,10 +506,9 @@ protected void configureRoutes() { sse("/sse/clob", clobSSEController.getSseConsumer()); - String wsPath = "/ws/clob"; - logger.atInfo().log("registering:" + wsPath); - ws(wsPath, clobSSEController.getWsConsumer(), new RouteRole[0]); + ws("/ws/clob", clobSSEController.getWsConsumer(), new RouteRole[0]); + ws("/ws/clob2", clobSSEController.getWsConsumer2(), new RouteRole[0]); } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/ClobAsyncController.java b/cwms-data-api/src/main/java/cwms/cda/api/ClobAsyncController.java index a28eab8d5..c994640b7 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/ClobAsyncController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/ClobAsyncController.java @@ -8,6 +8,8 @@ import io.javalin.websocket.WsConfig; import java.io.IOException; import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.sql.Clob; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -17,6 +19,7 @@ import java.util.Optional; import java.util.function.Consumer; import javax.sql.DataSource; +import org.eclipse.jetty.server.Request; import org.jooq.DSLContext; import org.jooq.SQLDialect; import org.jooq.impl.DSL; @@ -32,13 +35,17 @@ public Consumer getWsConsumer() { return this::acceptWs; } + public Consumer getWsConsumer2() { + return this::acceptWs2; + } + private void acceptWs(WsConfig ws) { ws.onConnect(ctx -> { logger.atInfo().log("#5--controller ws onConnect"); //#5 happens after #4 String clobId = ctx.queryParam("id"); - String officeId= ctx.queryParam("officeId"); + String officeId= ctx.queryParam(Controllers.OFFICE); Map servletAttributes = ctx.getUpgradeReq$javalin().getServletAttributes(); @@ -128,9 +135,101 @@ private void acceptWs(WsConfig ws) { } + + private void acceptWs2(WsConfig ws) { + + // The goal here is to demo a different way the ws stuff can work. + // Don't stream data to the client over the WS. + // Just send some progress messages + // Then tell the client where to get their data. + + ws.onConnect(ctx -> { + logger.atInfo().log("#5--controller ws onConnect"); //#5 happens after #4 + + String clobId = ctx.queryParam("id"); + String officeId= ctx.queryParam(Controllers.OFFICE); + + // fyi + //ctx.matchedPath(); // /ws/clob2 + // ctx.host() // localhost + // ctx.attributeMap() // has + // "office_id" -> "SPK" + //"javalin-ws-upgrade-context" -> {Context@5279} io.javalin.http.Context@1c4864d0 + //"org.eclipse.jetty.server.HttpConnection.UPGRADE" -> {WebSocketServerConnection@5136} "WebSocketServerConnection@640a1e6a::SocketChannelEndPoint@6f366275{l=/127.0.0.1:7000,r=/127.0.0.1:55394,OPEN,fill=-,flush=-,to=217631/300000}{io=0/0,kio=0,kro=1}->WebSocketServerConnection@640a1e6a[s=ConnectionState@269ef16b[OPENING],f=Flusher@2fc69888[IDLE][queueSize=0,aggregateSize=-1,terminated=null],g=Generator[SERVER,validating,+rsv1],p=Parser@7d140ac8[ExtensionStack,s=START,c=0,len=0,f=null]]" + //"javalin-ws-upgrade-allowed" -> {Boolean@5282} true + //"jetty-target" -> "/ws/clob2" + //"javalin-ws-upgrade-http-session" -> null + //"jetty-request" -> {Request@5287} "Request[GET //localhost:7000/cwms-data/ws/clob2?id=/TIME%20SERIES%20TEXT/6261044&officeId=SPK]@3ab35135" + //"data_source" -> {OracleDataSource@5289} + + Request req = ctx.attribute("jetty-request"); + String requestURI = req.getRequestURI(); + + + new Thread(() -> { + + // Pretend we are starting to do something + ctx.send('{' + + " \"status\": \"in progress\"," + + " \"message\": \"Starting to fetch.\"" + + '}'); + + // give it some fake time to work on it. + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + // progress message + ctx.send('{' + + " \"status\": \"in progress\"," + + " \"message\": \"Your clob is being prepared.\"" + + '}'); + + // still thinking. + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + // uri encode the clobId + String encodedClobId; + try { + encodedClobId = URLEncoder.encode(clobId, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + + // Now we're done + // url should look like: http://localhost:7000/cwms-data/clobs/ignored?office=SPK&clob-id=%2FTIME%20SERIES%20TEXT%2F6261044 + String link = "http://localhost:7000/cwms-data/clobs/ignored" + + "?" + + Controllers.OFFICE + "=" + officeId + "&" + + Controllers.CLOB_ID + "=" + encodedClobId ; + + ctx.send('{' + + " \"status\": \"done\"," + + " \"message\": \"" + link + "\"" + + '}'); + }).start(); + }); + ws.onClose(ctx -> { + logger.atInfo().log("ws onClose"); + }); + ws.onMessage(ctx -> { + logger.atInfo().log("ws onMessage"); + }); + ws.onError(ctx -> { + logger.atInfo().log("ws onError"); + }); + + } + public void accept(SseClient client) { String clobId = client.ctx.queryParam("id"); - String officeId= client.ctx.queryParam("officeId"); + String officeId= client.ctx.queryParam(Controllers.OFFICE); logger.atInfo().log("got an sse clob request for:" + clobId ); diff --git a/cwms-data-api/src/main/webapp/js/ws_demo.js b/cwms-data-api/src/main/webapp/js/ws_demo.js index c0e5ac530..405c80908 100644 --- a/cwms-data-api/src/main/webapp/js/ws_demo.js +++ b/cwms-data-api/src/main/webapp/js/ws_demo.js @@ -26,7 +26,7 @@ function connect(){ try{ var clobId = "/TIME SERIES TEXT/6261044"; - webSocket = new WebSocket("ws://localhost:7000/cwms-data/ws/clob" + "?id=" + encodeURI(clobId) + "&officeId=SPK"); + webSocket = new WebSocket("ws://localhost:7000/cwms-data/ws/clob" + "?id=" + encodeURI(clobId) + "&office=SPK"); messageDiv.innerHTML = "

    Socket status:" + websocketReadyStateArray[webSocket.readyState] + "

    "; webSocket.onopen = function(){ messageDiv.innerHTML += "

    Socket status:" + websocketReadyStateArray[webSocket.readyState] + "

    "; diff --git a/cwms-data-api/src/main/webapp/js/ws_demo2.js b/cwms-data-api/src/main/webapp/js/ws_demo2.js new file mode 100644 index 000000000..6197ba455 --- /dev/null +++ b/cwms-data-api/src/main/webapp/js/ws_demo2.js @@ -0,0 +1,98 @@ + +var webSocket; +var messageDiv; +var textInput; + +var websocketReadyStateArray; +var connectBtn; +var sendTextBtn; +var sendJSONObjectBtn; +var disconnectBtn; +function init(){ + messageDiv = document.getElementById("message"); + textInput = document.getElementById("text"); + + websocketReadyStateArray = new Array('Connecting', 'Connected', 'Closing', 'Closed'); + connectBtn = document.getElementById('connect'); + sendTextBtn = document.getElementById('sendText'); + sendJSONObjectBtn = document.getElementById('sendJSONObject'); + disconnectBtn = document.getElementById('disconnect'); + connectBtn.disabled = false; + sendTextBtn.disabled = true; + sendJSONObjectBtn.disabled = true; + disconnectBtn.disabled = true; +} +function connect(){ + try{ + + // var clobId = "/TIME SERIES TEXT/6261044"; + var clobId = "/TIME SERIES TEXT/1952044" + webSocket = new WebSocket("ws://localhost:7000/cwms-data/ws/clob2" + "?id=" + encodeURI(clobId) + "&office=SPK"); + messageDiv.innerHTML = "

    Socket status:" + websocketReadyStateArray[webSocket.readyState] + "

    "; + webSocket.onopen = function(){ + messageDiv.innerHTML += "

    Socket status:" + websocketReadyStateArray[webSocket.readyState] + "

    "; + connectBtn.disabled = true; + sendTextBtn.disabled = false; + sendJSONObjectBtn.disabled = false; + disconnectBtn.disabled = false; + } + webSocket.onmessage = function(msg){ + messageDiv.innerHTML += "

    Server response : " + msg.data + "

    "; + } + webSocket.onclose = function(){ + messageDiv.innerHTML += "

    Socket status:" + websocketReadyStateArray[webSocket.readyState] + "

    "; + connectBtn.disabled = false; + sendTextBtn.disabled = true; + sendJSONObjectBtn.disabled = true; + disconnectBtn.disabled = true; + } + }catch(exception){ + messageDiv.innerHTML += 'Exception happen, ' + exception; + } +} +function sendText(){ + var sendText = textInput.value.trim(); + if(sendText==''){ + + messageDiv.innerHTML = '

    Please input some text to send.

    '; + return; + }else{ + try{ + + webSocket.send(sendText); + messageDiv.innerHTML = '

    Send text : ' + sendText + '

    ' + }catch(exception){ + messageDiv.innerHTML = '

    Send error : ' + exception + '

    ' + } + } +} +function sendJSONOjbect(){ + var sendText = textInput.value.trim(); + if(sendText==''){ + + messageDiv.innerHTML = '

    Please input some text to send.

    '; + return; + }else{ + try{ + currDate = new Date(); + currHour = currDate.getHours(); + currMinutes = currDate.getMinutes(); + currSeconds = currDate.getSeconds(); + + currentTime = currHour + ':' + currMinutes + ':' + currSeconds; + jsonObj = {time:currentTime, text:sendText} + tmpSendText = JSON.stringify(jsonObj) + webSocket.send(tmpSendText); + messageDiv.innerHTML = '

    Send JSON object : ' + tmpSendText + '

    ' + }catch(exception){ + messageDiv.innerHTML = '

    Send error : ' + exception + '

    ' + } + } +} +// When you focus on the input text box, it will call this function to select all the text in the input text box. +function selectAll(){ + textInput.select(); +} +function disconnect(){ + webSocket.close(); +} \ No newline at end of file diff --git a/cwms-data-api/src/main/webapp/sse_demo.html b/cwms-data-api/src/main/webapp/sse_demo.html index 79366e542..8b47896c8 100644 --- a/cwms-data-api/src/main/webapp/sse_demo.html +++ b/cwms-data-api/src/main/webapp/sse_demo.html @@ -12,7 +12,7 @@

    Getting server updates

    var clobId = "/TIME SERIES TEXT/6261044"; console.log("clobId: " + clobId + " encodeURIComponent: " + encodeURIComponent(clobId) + " encodeURI: " + encodeURI(clobId)); var source = new EventSource("http://localhost:7000/cwms-data/sse/clob" - + "?id=" + encodeURI(clobId) + "&officeId=SPK" + + "?id=" + encodeURI(clobId) + "&office=SPK" ); source.onmessage = function (event) { document.getElementById("result").innerHTML += event.data + "
    "; diff --git a/cwms-data-api/src/main/webapp/ws_demo2.html b/cwms-data-api/src/main/webapp/ws_demo2.html new file mode 100644 index 000000000..a3170d910 --- /dev/null +++ b/cwms-data-api/src/main/webapp/ws_demo2.html @@ -0,0 +1,23 @@ + + + + + Html5 WebSockets Example + + + + +

    Html5 WebSockets Example.

    +
    +

    Please input some text to Send :

    + + + + + + \ No newline at end of file From 99b57d29bf358fc51b5a51019168e247138ac764 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Fri, 2 Feb 2024 14:08:08 -0800 Subject: [PATCH 15/16] Added time, message and link to fake responses. --- .../java/cwms/cda/api/ClobAsyncController.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/ClobAsyncController.java b/cwms-data-api/src/main/java/cwms/cda/api/ClobAsyncController.java index c994640b7..eb26c6a8b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/ClobAsyncController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/ClobAsyncController.java @@ -170,7 +170,8 @@ private void acceptWs2(WsConfig ws) { // Pretend we are starting to do something ctx.send('{' + - " \"status\": \"in progress\"," + + " \"time\": \"" + getNowStr() + "\"," + + " \"progress\": \"0\"," + " \"message\": \"Starting to fetch.\"" + '}'); @@ -183,8 +184,9 @@ private void acceptWs2(WsConfig ws) { // progress message ctx.send('{' + - " \"status\": \"in progress\"," + - " \"message\": \"Your clob is being prepared.\"" + + " \"time\": \"" + getNowStr() + "\"," + + " \"progress\": \"50\"," + + " \"message\": \"Processing.\"" + '}'); // still thinking. @@ -210,8 +212,10 @@ private void acceptWs2(WsConfig ws) { + Controllers.CLOB_ID + "=" + encodedClobId ; ctx.send('{' + - " \"status\": \"done\"," + - " \"message\": \"" + link + "\"" + + " \"time\": \"" + getNowStr() + "\"," + + " \"progress\": \"100\"," + + " \"message\": \"Done.\"," + + " \"link\": \"" + link + "\"" + '}'); }).start(); }); From a296bbef90031d316f29b64e49c68784bccb7eff Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:42:07 -0800 Subject: [PATCH 16/16] Added link to ws_demo2 --- cwms-data-api/src/main/java/cwms/cda/ApiServlet.java | 1 + 1 file changed, 1 insertion(+) diff --git a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java index 8fc5e90b3..4ce611d74 100644 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java @@ -436,6 +436,7 @@ protected void configureRoutes() { + "
  • Swagger Docs
  • " + "
  • Offices
  • " + "
  • websocket clob demo
  • " + + "
  • better websocket clob demo
  • " + "
  • sse clob demo
  • " + "" + "";