diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/AppResources.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/AppResources.java index 42b8cad3..4bea3be9 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/AppResources.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/AppResources.java @@ -17,11 +17,13 @@ import java.io.IOException; import java.net.ConnectException; -import java.sql.SQLException; import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import javax.annotation.security.RolesAllowed; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -35,35 +37,39 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import decodes.sql.DbKey; +import decodes.tsdb.CompAppInfo; +import decodes.tsdb.ConstraintException; +import decodes.tsdb.DbIoException; +import decodes.tsdb.LockBusyException; +import decodes.tsdb.NoSuchObjectException; +import decodes.tsdb.TsdbCompLock; +import ilex.cmdline.StdAppSettings; +import opendcs.dai.LoadingAppDAI; import org.opendcs.odcsapi.appmon.ApiEventClient; import org.opendcs.odcsapi.beans.ApiAppEvent; import org.opendcs.odcsapi.beans.ApiAppRef; import org.opendcs.odcsapi.beans.ApiAppStatus; import org.opendcs.odcsapi.beans.ApiLoadingApp; -import org.opendcs.odcsapi.dao.ApiAppDAO; import org.opendcs.odcsapi.dao.DbException; import org.opendcs.odcsapi.errorhandling.ErrorCodes; import org.opendcs.odcsapi.errorhandling.WebAppException; -import org.opendcs.odcsapi.hydrojson.DbInterface; import org.opendcs.odcsapi.lrgsclient.ClientConnectionCache; import org.opendcs.odcsapi.sec.AuthorizationCheck; import org.opendcs.odcsapi.util.ApiEnvExpander; -import org.opendcs.odcsapi.util.ApiHttpUtil; import org.opendcs.odcsapi.util.ApiPropertiesUtil; import org.opendcs.odcsapi.util.ProcWaiterCallback; import org.opendcs.odcsapi.util.ProcWaiterThread; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Resources for editing, monitoring, stopping, and starting processes. */ @Path("/") -public class AppResources +public class AppResources extends OpenDcsResource { - private static final Logger LOGGER = LoggerFactory.getLogger(AppResources.class); @Context private HttpServletRequest request; @Context private HttpHeaders httpHeaders; + private static final String NO_APP_FOUND = "No such app with ID: %s"; @GET @Path("apprefs") @@ -71,31 +77,57 @@ public class AppResources @RolesAllowed({AuthorizationCheck.ODCS_API_GUEST}) public Response getAppRefs() throws DbException { - LOGGER.trace("Getting App Refs."); - try (DbInterface dbi = new DbInterface(); - ApiAppDAO dao = new ApiAppDAO(dbi)) + try (LoadingAppDAI dai = getLegacyDatabase().makeLoadingAppDAO()) { - ArrayList ret = dao.getAppRefs(); - LOGGER.trace("Returning {} apps.", ret.size()); - return ApiHttpUtil.createResponse(ret); + List ret = dai.listComputationApps(false) + .stream() + .map(AppResources::map) + .collect(Collectors.toList()); + return Response.status(HttpServletResponse.SC_OK) + .entity(ret).build(); } + catch (DbIoException ex) + { + throw new DbException("Unable to retrieve apps", ex); + } + } + + static ApiAppRef map(CompAppInfo app) + { + ApiAppRef ret = new ApiAppRef(); + ret.setAppId(app.getAppId().getValue()); + ret.setAppName(app.getAppName()); + ret.setAppType(app.getAppType()); + ret.setComment(app.getComment()); + ret.setLastModified(app.getLastModified()); + return ret; } @GET @Path("app") @Produces(MediaType.APPLICATION_JSON) @RolesAllowed({AuthorizationCheck.ODCS_API_GUEST}) - public Response getApp(@QueryParam("appid") Long appId) - throws WebAppException, DbException, SQLException + public Response getApp(@QueryParam("appid") Long appId) + throws WebAppException, DbException { if (appId == null) - throw new WebAppException(ErrorCodes.MISSING_ID, - "Missing required appid parameter."); - LOGGER.debug("Getting app with id {}", appId); - try (DbInterface dbi = new DbInterface(); - ApiAppDAO dao = new ApiAppDAO(dbi)) { - return ApiHttpUtil.createResponse(dao.getApp(appId)); + throw new WebAppException(HttpServletResponse.SC_BAD_REQUEST, + "Missing required appid parameter."); + } + try (LoadingAppDAI dai = getLegacyDatabase().makeLoadingAppDAO()) + { + return Response.status(HttpServletResponse.SC_OK) + .entity(mapLoading(dai.getComputationApp(DbKey.createDbKey(appId)))).build(); + } + catch (NoSuchObjectException e) + { + return Response.status(HttpServletResponse.SC_NOT_FOUND) + .entity(String.format(NO_APP_FOUND, appId)).build(); + } + catch (DbIoException ex) + { + throw new DbException("No such app with ID " + appId, ex); } } @@ -105,33 +137,78 @@ public Response getApp(@QueryParam("appid") Long appId) @Produces(MediaType.APPLICATION_JSON) @RolesAllowed({AuthorizationCheck.ODCS_API_ADMIN, AuthorizationCheck.ODCS_API_USER}) public Response postApp(ApiLoadingApp app) - throws WebAppException, DbException, SQLException + throws DbException { - LOGGER.debug("Post app received app {} with id {}", app.getAppName(), app.getAppId()); - try (DbInterface dbi = new DbInterface(); - ApiAppDAO dao = new ApiAppDAO(dbi)) + try (LoadingAppDAI dai = getLegacyDatabase().makeLoadingAppDAO()) { - dao.writeApp(app); - return ApiHttpUtil.createResponse(app); + CompAppInfo compApp = map(app); + dai.writeComputationApp(compApp); + return Response.status(HttpServletResponse.SC_OK) + .entity(map(compApp)) + .build(); + } + catch(DbIoException ex) + { + throw new DbException("Unable to store app", ex); } } + static CompAppInfo map(ApiLoadingApp app) + { + CompAppInfo ret = new CompAppInfo(); + if (app.getAppId() != null) + { + ret.setAppId(DbKey.createDbKey(app.getAppId())); + } + else + { + ret.setAppId(DbKey.NullKey); + } + ret.setAppName(app.getAppName()); + ret.setComment(app.getComment()); + ret.setLastModified(app.getLastModified()); + ret.setProperties(app.getProperties()); + ret.setManualEditApp(app.isManualEditingApp()); + String appType = app.getProperties().getProperty("appType"); + if (appType == null) + { + ret.setProperty("appType", app.getAppType()); + } + return ret; + } + @DELETE @Path("app") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @RolesAllowed({AuthorizationCheck.ODCS_API_ADMIN, AuthorizationCheck.ODCS_API_USER}) - public Response deletApp(@QueryParam("appid") Long appId) - throws WebAppException, DbException, SQLException + public Response deleteApp(@QueryParam("appid") Long appId) + throws DbException, WebAppException { - LOGGER.debug("Delete app received request to delete app with id {}", appId); - - // Use username and password to attempt to connect to the database - try (DbInterface dbi = new DbInterface(); - ApiAppDAO dao = new ApiAppDAO(dbi)) + if (appId == null) { - dao.deleteApp(appId); - return ApiHttpUtil.createResponse("appId with ID " + appId + " deleted"); + throw new WebAppException(HttpServletResponse.SC_BAD_REQUEST, "Missing required appid parameter."); + } + + try (LoadingAppDAI dai = getLegacyDatabase().makeLoadingAppDAO()) + { + CompAppInfo app = dai.getComputationApp(DbKey.createDbKey(appId)); + if (app == null) + { + throw new DbException(String.format(NO_APP_FOUND, appId)); + } + dai.deleteComputationApp(app); + return Response.status(HttpServletResponse.SC_OK) + .entity("appId with ID " + appId + " deleted").build(); + } + catch (NoSuchObjectException e) + { + return Response.status(HttpServletResponse.SC_NOT_FOUND) + .entity(String.format(NO_APP_FOUND, appId)).build(); + } + catch (DbIoException | ConstraintException ex) + { + throw new DbException(String.format(NO_APP_FOUND, appId), ex); } } @@ -139,54 +216,96 @@ public Response deletApp(@QueryParam("appid") Long appId) @Path("appstat") @Produces(MediaType.APPLICATION_JSON) @RolesAllowed({AuthorizationCheck.ODCS_API_ADMIN, AuthorizationCheck.ODCS_API_USER}) - public Response getAppStat() throws DbException + public Response getAppStatus() throws DbException { - LOGGER.debug("Getting app stats"); - try (DbInterface dbi = new DbInterface(); - ApiAppDAO dao = new ApiAppDAO(dbi)) + List ret = new ArrayList<>(); + try (LoadingAppDAI dai = getLegacyDatabase().makeLoadingAppDAO()) { - return ApiHttpUtil.createResponse(dao.getAppStatus()); + for (TsdbCompLock lock : dai.getAllCompProcLocks()) + { + ret.add(map(dai, lock)); + } + return Response.status(HttpServletResponse.SC_OK) + .entity(ret).build(); + } + catch (DbIoException ex) + { + throw new DbException("Unable to retrieve app status", ex); } } + static ApiAppStatus map(LoadingAppDAI dai, TsdbCompLock lock) throws DbIoException + { + ApiAppStatus ret = new ApiAppStatus(); + ret.setAppId(lock.getAppId().getValue()); + ret.setAppName(lock.getAppName()); + ret.setHostname(lock.getHost()); + ret.setPid((long) lock.getPID()); + ret.setHeartbeat(lock.getHeartbeat()); + ret.setStatus(lock.getStatus()); + if (dai != null) + { + try { + ApiLoadingApp app = mapLoading(dai.getComputationApp(lock.getAppId())); + if (app.getProperties() == null || app.getProperties().getProperty("EventPort") == null) + { + throw new DbIoException("EventPort property not found"); + } + ret.setEventPort(Integer.parseInt(app.getProperties().getProperty("EventPort"))); + ret.setAppType(app.getAppType()); + } + catch (DbIoException | NoSuchObjectException | NumberFormatException ex) + { + throw new DbIoException("Error mapping app status", ex); + } + } + return ret; + } + @GET @Path("appevents") @Produces(MediaType.APPLICATION_JSON) @RolesAllowed({AuthorizationCheck.ODCS_API_ADMIN, AuthorizationCheck.ODCS_API_USER}) - public Response getAppEvents(@QueryParam("appid") Long appId) - throws WebAppException, DbException, SQLException + public Response getAppEvents(@QueryParam("appid") Long appId) + throws WebAppException, DbException { - LOGGER.debug("Getting app events for app with id {}", appId); HttpSession session = request.getSession(true); ClientConnectionCache clientConnectionCache = ClientConnectionCache.getInstance(); Optional cli = clientConnectionCache.getApiEventClient(appId, session.getId()); ApiAppStatus appStat = null; - try (DbInterface dbi = new DbInterface(); - ApiAppDAO dao = new ApiAppDAO(dbi)) + try(LoadingAppDAI dai = getLegacyDatabase().makeLoadingAppDAO()) { ApiEventClient apiEventClient = null; - appStat = dao.getAppStatus(appId); - if (appStat.getPid() == null) + appStat = getAppStatus(dai, appId); + if (appStat == null) { cli.ifPresent(c -> clientConnectionCache.removeApiEventClient(c, session.getId())); - throw new WebAppException(ErrorCodes.NO_SUCH_OBJECT, "appid " + appId - + " (" + appStat.getAppName() + ") is not running."); + throw new WebAppException(HttpServletResponse.SC_NOT_FOUND, "appid " + appId + + " is not running (no lock found)."); + } + else if (appStat.getPid() == null) + { + cli.ifPresent(c -> clientConnectionCache.removeApiEventClient(c, session.getId())); + throw new WebAppException(HttpServletResponse.SC_NOT_FOUND, "appid " + appId + + " (" + appStat.getAppName() + ") is not running."); } else if (System.currentTimeMillis() - appStat.getHeartbeat().getTime() > 20000L) { cli.ifPresent(c -> clientConnectionCache.removeApiEventClient(c, session.getId())); - throw new WebAppException(ErrorCodes.NO_SUCH_OBJECT, "appid " + appId - + " (" + appStat.getAppName() + ") is not running (stale heartbeat)."); + throw new WebAppException(HttpServletResponse.SC_NOT_FOUND, "appid " + appId + + " (" + appStat.getAppName() + ") is not running (stale heartbeat)."); } else if (!cli.isPresent()) { Integer port = appStat.getEventPort(); if (port == null) - return ApiHttpUtil.createResponse(new ArrayList()); + { + return Response.status(HttpServletResponse.SC_OK) + .entity(new ArrayList()).build(); + } apiEventClient = new ApiEventClient(appId, appStat.getHostname(), port, appStat.getAppName(), appStat.getPid()); apiEventClient.connect(); - LOGGER.debug("Connected to {}:{}", appStat.getHostname(), port); clientConnectionCache.addApiEventClient(apiEventClient, session.getId()); } else if (appStat.getPid() != null && appStat.getPid() != cli.get().getPid()) @@ -194,23 +313,26 @@ else if (appStat.getPid() != null && appStat.getPid() != cli.get().getPid()) // This means that the app was stopped and restarted since we last checked for events. // Close the old client and open a new one with the correct PID. cli.ifPresent(c -> clientConnectionCache.removeApiEventClient(c, session.getId())); - + Integer port = appStat.getEventPort(); if (port == null) - return ApiHttpUtil.createResponse(new ArrayList()); // app not running + { + return Response.status(HttpServletResponse.SC_OK) + .entity(new ArrayList()).build(); + } apiEventClient = new ApiEventClient(appId, appStat.getHostname(), port, appStat.getAppName(), appStat.getPid()); apiEventClient.connect(); - LOGGER.debug("Connected to {}:{}", appStat.getHostname(), port); clientConnectionCache.addApiEventClient(apiEventClient, session.getId()); } if(apiEventClient == null) { - throw new WebAppException(ErrorCodes.NO_SUCH_OBJECT, "No API Event Client found or created"); + throw new WebAppException(HttpServletResponse.SC_NOT_FOUND, "No API Event Client found or created"); } - return ApiHttpUtil.createResponse(apiEventClient.getNewEvents()); + return Response.status(HttpServletResponse.SC_OK) + .entity(apiEventClient.getNewEvents()).build(); } catch(ConnectException ex) - { + { throw new WebAppException(ErrorCodes.IO_ERROR, String.format("Cannot connect to %s.", appStat.getAppName()), ex); // NOTE: event client added to user token ONLY if connect succeeds. @@ -222,86 +344,166 @@ else if (appStat.getPid() != null && appStat.getPid() != cli.get().getPid()) String.format("Event socket to %s closed by app", appStat.getAppId()), ex); } } - + @POST @Path("appstart") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @RolesAllowed({AuthorizationCheck.ODCS_API_ADMIN, AuthorizationCheck.ODCS_API_USER}) public Response postAppStart(@QueryParam("appid") Long appId) - throws WebAppException, DbException, SQLException + throws WebAppException, DbException { - LOGGER.debug("Post for appstart received with appId={}", appId); if (appId == null) - throw new WebAppException(ErrorCodes.MISSING_ID, "appId parameter required for this operation."); - - try (DbInterface dbi = new DbInterface(); - ApiAppDAO dao = new ApiAppDAO(dbi)) + { + throw new WebAppException(HttpServletResponse.SC_BAD_REQUEST, + "appId parameter required for this operation."); + } + + try (LoadingAppDAI dai = getLegacyDatabase().makeLoadingAppDAO()) { // Retrieve ApiLoadingApp and ApiAppStatus - ApiLoadingApp loadingApp = dao.getApp(appId); - ApiAppStatus appStat = dao.getAppStatus(appId); - + ApiLoadingApp loadingApp = mapLoading(dai.getComputationApp(DbKey.createDbKey(appId))); + ApiAppStatus appStat = getAppStatus(dai, appId); + // Error if already running and heartbeat is current - if (appStat.getPid() != null && appStat.getHeartbeat() != null - && (System.currentTimeMillis() - appStat.getHeartbeat().getTime() < 20000L)) + if (appStat != null && appStat.getPid() != null && appStat.getHeartbeat() != null + && (System.currentTimeMillis() - appStat.getHeartbeat().getTime() < 20000L)) throw new WebAppException(ErrorCodes.NOT_ALLOWED, - "App id=" + appId + " (" + loadingApp.getAppName() + ") is already running."); - + "App id=" + appId + " (" + loadingApp.getAppName() + ") is already running."); + // Error if no "startCmd" property String startCmd = ApiPropertiesUtil.getIgnoreCase(loadingApp.getProperties(), "startCmd"); if (startCmd == null) throw new WebAppException(ErrorCodes.BAD_CONFIG, - "App id=" + appId + " (" + loadingApp.getAppName() + ") has no 'startCmd' property."); + "App id=" + appId + " (" + loadingApp.getAppName() + ") has no 'startCmd' property."); // ProcWaiterThread runBackground to execute command, use callback. ProcWaiterCallback pwcb = (procName, obj, exitStatus) -> - { - ApiLoadingApp loadingApp1 = (ApiLoadingApp)obj; - LOGGER.info("App Termination: app {} was terminated with exit status {}", - loadingApp1.getAppName(), exitStatus); - }; + { + ApiLoadingApp loadingApp1 = (ApiLoadingApp)obj; + }; + + StdAppSettings settings = new StdAppSettings(); - ProcWaiterThread.runBackground(ApiEnvExpander.expand(startCmd), "App:" + loadingApp.getAppName(), - pwcb, loadingApp); + int pid; - return ApiHttpUtil.createResponse("App with ID " + appId + " (" + loadingApp.getAppName() + ") started."); + if (appStat == null || appStat.getPid() == null) + { + pid = appId.intValue(); + } + else + { + pid = appStat.getPid().intValue(); + } + + // Obtain a lock on the app with a processId chosen from the DB and standard hostname + dai.obtainCompProcLock(dai.getComputationApp(DbKey.createDbKey(loadingApp.getAppId())), + pid, settings.getHostName()); + + ProcWaiterThread.runBackground(ApiEnvExpander.expand(startCmd), "App:" + loadingApp.getAppName(), + pwcb, loadingApp); + + return Response.status(HttpServletResponse.SC_OK) + .entity("App with ID " + appId + " (" + loadingApp.getAppName() + ") started.").build(); } - catch (IOException ex) + catch (DbIoException | NoSuchObjectException | IOException | LockBusyException ex) { throw new WebAppException(ErrorCodes.DATABASE_ERROR, String.format("Error attempting to start appId=%s", appId), ex); } } + static ApiLoadingApp mapLoading(CompAppInfo app) + { + ApiLoadingApp ret = new ApiLoadingApp(); + ret.setAppId(app.getAppId().getValue()); + ret.setAppName(app.getAppName()); + ret.setComment(app.getComment()); + ret.setLastModified(app.getLastModified()); + ret.setManualEditingApp(app.getManualEditApp()); + ret.setAppType(app.getAppType()); + ret.setProperties(app.getProperties()); + return ret; + } + @POST @Path("appstop") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @RolesAllowed({AuthorizationCheck.ODCS_API_ADMIN, AuthorizationCheck.ODCS_API_USER}) public Response postAppStop(@QueryParam("appid") Long appId) - throws WebAppException, DbException, SQLException + throws WebAppException, DbException { - LOGGER.debug("Post appstop received on app with id {}", appId); if (appId == null) - throw new WebAppException(ErrorCodes.MISSING_ID, "appId parameter required for this operation."); - - try (DbInterface dbi = new DbInterface(); - ApiAppDAO dao = new ApiAppDAO(dbi)) + { + throw new WebAppException(HttpServletResponse.SC_BAD_REQUEST, + "appId parameter required for this operation."); + } + + try (LoadingAppDAI dai = getLegacyDatabase().makeLoadingAppDAO()) { // Retrieve ApiLoadingApp and ApiAppStatus - ApiLoadingApp loadingApp = dao.getApp(appId); - - ApiAppStatus appStat = dao.getAppStatus(appId); - - if (appStat.getPid() == null) - throw new WebAppException(ErrorCodes.NO_SUCH_OBJECT, - "appId " + appId + "(" + loadingApp.getAppName() + ") not currently running."); - - dao.terminateApp(appId); - - return ApiHttpUtil.createResponse("App with ID " + appId + " (" + loadingApp.getAppName() + ") terminated."); + ApiLoadingApp loadingApp = mapLoading(dai.getComputationApp(DbKey.createDbKey(appId))); + + ApiAppStatus appStat = getAppStatus(dai, appId); + + if (appStat == null || appStat.getPid() == null) + { + throw new WebAppException(HttpServletResponse.SC_NOT_FOUND, + "appId " + appId + "(" + loadingApp.getAppName() + ") not currently running."); + } + + dai.releaseCompProcLock(new TsdbCompLock(DbKey.createDbKey(appId), appStat.getPid().intValue(), + appStat.getHostname(), appStat.getHeartbeat(), appStat.getStatus())); + + return Response.status(HttpServletResponse.SC_OK) + .entity("App with ID " + appId + " (" + loadingApp.getAppName() + ") terminated.").build(); + } + catch (NoSuchObjectException e) + { + return Response.status(HttpServletResponse.SC_NOT_FOUND) + .entity("No such app found with ID " + appId).build(); + } + catch (DbIoException ex) + { + throw new WebAppException(ErrorCodes.DATABASE_ERROR, + String.format("Error attempting to stop appId=%s", appId), ex); + } + } + private static ApiAppStatus getAppStatus(LoadingAppDAI dai, Long appId) throws DbException + { + try + { + List locks = dai.getAllCompProcLocks(); + for (TsdbCompLock lock : locks) + { + if(lock.getAppId().getValue() == appId) + { + return map(dai, lock); + } + } + List apps = dai.listComputationApps(false) + .stream() + .map(AppResources::map) + .collect(Collectors.toList()); + for (ApiAppRef app : apps) + { + if (app.getAppId().equals(appId)) + { + ApiAppStatus ret = new ApiAppStatus(); + ret.setAppId(appId); + ret.setAppName(app.getAppName()); + ret.setStatus("Not Running"); + ret.setAppType(app.getAppType()); + return ret; + } + } + return null; + } + catch (DbIoException ex) + { + throw new DbException(String.format("Error retrieving locks for app ID: %s", appId), ex); } } diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/OpenDcsResource.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/OpenDcsResource.java index 439cc7b6..22bfe8a4 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/OpenDcsResource.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/OpenDcsResource.java @@ -28,6 +28,8 @@ import decodes.tsdb.TimeSeriesDb; import decodes.util.DecodesSettings; import opendcs.opentsdb.OpenTsdbProvider; +import decodes.db.DatabaseIO; +import decodes.tsdb.TimeSeriesDb; import org.opendcs.database.DatabaseService; import org.opendcs.database.api.OpenDcsDao; import org.opendcs.database.api.OpenDcsDatabase; diff --git a/opendcs-rest-api/src/test/java/org/opendcs/odcsapi/res/AppResourcesTest.java b/opendcs-rest-api/src/test/java/org/opendcs/odcsapi/res/AppResourcesTest.java new file mode 100644 index 00000000..ed08d89f --- /dev/null +++ b/opendcs-rest-api/src/test/java/org/opendcs/odcsapi/res/AppResourcesTest.java @@ -0,0 +1,117 @@ +package org.opendcs.odcsapi.res; + +import java.time.Instant; +import java.util.Date; +import java.util.Properties; + +import decodes.sql.DbKey; +import decodes.tsdb.CompAppInfo; +import decodes.tsdb.TsdbCompLock; +import org.junit.jupiter.api.Test; +import org.opendcs.odcsapi.beans.ApiAppRef; +import org.opendcs.odcsapi.beans.ApiAppStatus; +import org.opendcs.odcsapi.beans.ApiLoadingApp; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.opendcs.odcsapi.res.AppResources.map; +import static org.opendcs.odcsapi.res.AppResources.mapLoading; + +final class AppResourcesTest +{ + @Test + void testAppRefMap() + { + CompAppInfo compAppInfo = new CompAppInfo(); + compAppInfo.setAppName("Computation application"); + compAppInfo.setComment("Computation to find the volume of a river"); + compAppInfo.setLastModified(Date.from(Instant.parse("2021-07-01T00:00:00Z"))); + compAppInfo.setAppId(DbKey.createDbKey(151615L)); + compAppInfo.setNumComputations(1); + Properties properties = new Properties(); + properties.setProperty("compRef", "applicationValue"); + compAppInfo.setProperties(properties); + + ApiAppRef appRef = map(compAppInfo); + + assertNotNull(appRef); + assertEquals(compAppInfo.getAppName(), appRef.getAppName()); + assertEquals(compAppInfo.getComment(), appRef.getComment()); + assertEquals(compAppInfo.getLastModified(), appRef.getLastModified()); + assertEquals(compAppInfo.getAppId().getValue(), appRef.getAppId()); + assertEquals(compAppInfo.getAppType(), appRef.getAppType()); + } + + @Test + void testCompAppMap() + { + ApiLoadingApp app = new ApiLoadingApp(); + app.setAppId(151615L); + app.setAppName("Calculation application"); + app.setAppType("Computation"); + app.setComment("Application to calculate the flow of a river"); + app.setLastModified(Date.from(Instant.parse("2021-07-01T00:00:00Z"))); + app.setManualEditingApp(true); + Properties properties = new Properties(); + properties.setProperty("algorithm", "processName"); + app.setProperties(properties); + + CompAppInfo compAppInfo = map(app); + + assertNotNull(compAppInfo); + assertEquals(app.getAppId(), compAppInfo.getAppId().getValue()); + assertEquals(app.getAppName(), compAppInfo.getAppName()); + assertEquals(app.getAppType(), compAppInfo.getAppType()); + assertEquals(app.getComment(), compAppInfo.getComment()); + assertEquals(app.getLastModified(), compAppInfo.getLastModified()); + assertEquals(app.isManualEditingApp(), compAppInfo.getManualEditApp()); + assertEquals(app.getProperties(), compAppInfo.getProperties()); + } + + @Test + void testStatusMap() throws Exception + { + DbKey appId = DbKey.createDbKey(151615L); + int pid = 12345; + String host = "localhost"; + Date heartbeat = Date.from(Instant.parse("2021-07-01T00:00:00Z")); + String status = "Running"; + TsdbCompLock compLock = new TsdbCompLock(appId, pid, host, heartbeat, status); + compLock.setAppName("Computation Application"); + + ApiAppStatus appStatus = map(null, compLock); + + assertNotNull(appStatus); + assertEquals(compLock.getAppId().getValue(), appStatus.getAppId()); + assertEquals(compLock.getPID(), appStatus.getPid()); + assertEquals(compLock.getHost(), appStatus.getHostname()); + assertEquals(compLock.getHeartbeat(), appStatus.getHeartbeat()); + assertEquals(compLock.getStatus(), appStatus.getStatus()); + assertEquals(compLock.getAppName(), appStatus.getAppName()); + } + + @Test + void testLoadingAppMap() + { + CompAppInfo compAppInfo = new CompAppInfo(); + compAppInfo.setAppName("Dijkstra's Algorithm"); + compAppInfo.setComment("Runs Dijkstra's algorithm on a graph"); + compAppInfo.setLastModified(Date.from(Instant.parse("2021-07-01T00:00:00Z"))); + compAppInfo.setAppId(DbKey.createDbKey(151615L)); + compAppInfo.setNumComputations(1); + Properties properties = new Properties(); + properties.setProperty("appType", "computation"); + compAppInfo.setProperties(properties); + + ApiLoadingApp app = mapLoading(compAppInfo); + + assertNotNull(app); + assertEquals(compAppInfo.getAppName(), app.getAppName()); + assertEquals(compAppInfo.getComment(), app.getComment()); + assertEquals(compAppInfo.getLastModified(), app.getLastModified()); + assertEquals(compAppInfo.getAppId().getValue(), app.getAppId()); + assertEquals(compAppInfo.getAppType(), app.getAppType()); + assertEquals(compAppInfo.getManualEditApp(), app.isManualEditingApp()); + assertEquals(compAppInfo.getProperties(), app.getProperties()); + } +} diff --git a/opendcs-rest-api/src/test/java/org/opendcs/odcsapi/res/it/AppResourcesIT.java b/opendcs-rest-api/src/test/java/org/opendcs/odcsapi/res/it/AppResourcesIT.java new file mode 100644 index 00000000..0442c740 --- /dev/null +++ b/opendcs-rest-api/src/test/java/org/opendcs/odcsapi/res/it/AppResourcesIT.java @@ -0,0 +1,434 @@ +package org.opendcs.odcsapi.res.it; + +import java.sql.Date; +import java.time.Instant; +import java.util.Properties; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.MediaType; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.restassured.filter.log.LogDetail; +import io.restassured.filter.session.SessionFilter; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.opendcs.odcsapi.beans.ApiLoadingApp; +import org.opendcs.odcsapi.fixtures.DatabaseContextProvider; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; + +@Tag("integration") +@ExtendWith(DatabaseContextProvider.class) +final class AppResourcesIT extends BaseIT +{ + private static Long appid; + private static SessionFilter sessionFilter; + private static Properties properties; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeAll + static void setUpAll() + { + properties = new Properties(); + properties.setProperty("startCmd", "$DCSTOOL_HOME\\bin\\compproc.bat"); + } + + @BeforeEach + void setUp() throws Exception + { + setUpCreds(); + sessionFilter = new SessionFilter(); + + authenticate(sessionFilter); + + ApiLoadingApp app = new ApiLoadingApp(); + app.setAppType("Computation"); + app.setAppName("River Flow Calculation"); + app.setAppId(9965L); + app.setProperties(properties); + app.setLastModified(Date.from(Instant.parse("2021-02-01T00:00:00Z"))); + app.setComment("Computation to calculate the flow of a river"); + + String appJson = objectMapper.writeValueAsString(app); + + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .body(appJson) + .filter(sessionFilter) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("app") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .extract() + ; + + appid = response.body().jsonPath().getLong("appId"); + } + + @AfterEach + void tearDown() + { + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .queryParam("appid", appid) + .header("Authorization", authHeader) + .filter(sessionFilter) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("app") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + ; + + logout(sessionFilter); + } + + @TestTemplate + void testGetAppRefsRoundTrip() + { + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .filter(sessionFilter) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("apprefs") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + ; + } + + @TestTemplate + void testGetAppRoundTrip() + { + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .filter(sessionFilter) + .queryParam("appid", appid) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("app") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("appName", is("River Flow Calculation")) + ; + } + + @TestTemplate + void testPostAppRoundTrip() throws Exception + { + ApiLoadingApp app = new ApiLoadingApp(); + app.setAppType("Utility"); + app.setAppName("Hostname Generator"); + app.setLastModified(Date.from(Instant.parse("2021-01-01T00:00:00Z"))); + app.setComment("Generates a pseudo-random hostname"); + + String appJson = objectMapper.writeValueAsString(app); + + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .body(appJson) + .filter(sessionFilter) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("app") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .extract() + ; + + Long appId = response.body().jsonPath().getLong("appId"); + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .queryParam("appid", appId) + .filter(sessionFilter) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("app") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + ; + } + + @TestTemplate + void testDeleteAppRoundTrip() throws Exception + { + ApiLoadingApp app = new ApiLoadingApp(); + app.setAppType("Loading"); + app.setAppName("Application Wrapper"); + app.setProperties(properties); + app.setLastModified(Date.from(Instant.parse("2021-01-01T00:00:00Z"))); + app.setComment("Wraps a custom application in a usable interface"); + + String appJson = objectMapper.writeValueAsString(app); + + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .body(appJson) + .filter(sessionFilter) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("app") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .extract() + ; + + Long appId = response.body().jsonPath().getLong("appId"); + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .queryParam("appid", appId) + .header("Authorization", authHeader) + .filter(sessionFilter) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("app") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + ; + } + + @TestTemplate + void testGetAppStatRoundTrip() + { + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .filter(sessionFilter) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("appstat") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + ; + } + + @TestTemplate + void testGetAppEventsRoundTrip() throws Exception + { + ApiLoadingApp app = new ApiLoadingApp(); + app.setAppType("Utility"); + app.setAppName("User Properties GUI"); + app.setProperties(properties); + app.setLastModified(Date.from(Instant.parse("2021-01-01T00:00:00Z"))); + app.setComment("Opens a GUI to edit user properties"); + + String appJson = objectMapper.writeValueAsString(app); + + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .contentType(MediaType.APPLICATION_JSON) + .body(appJson) + .filter(sessionFilter) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("app") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .extract() + ; + + Long appId = response.body().jsonPath().getLong("appId"); + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .contentType(MediaType.APPLICATION_JSON) + .queryParam("appid", appId) + .filter(sessionFilter) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("appstart") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + ; + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .filter(sessionFilter) + .queryParam("appid", appId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("appevents") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + ; + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .contentType(MediaType.APPLICATION_JSON) + .queryParam("appid", appId) + .filter(sessionFilter) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("appstop") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + ; + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .queryParam("appid", appId) + .filter(sessionFilter) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("app") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + ; + } + + @TestTemplate + void testPostAppStartRoundTrip() + { + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .contentType(MediaType.APPLICATION_JSON) + .filter(sessionFilter) + .queryParam("appid", appid) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("appstart") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + ; + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .contentType(MediaType.APPLICATION_JSON) + .filter(sessionFilter) + .queryParam("appid", appid) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("appstop") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + ; + } + + @TestTemplate + void testPostAppStopRoundTrip() + { + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .filter(sessionFilter) + .queryParam("appid", appid) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("appstart") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + ; + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", authHeader) + .filter(sessionFilter) + .queryParam("appid", appid) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("appstop") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + ; + } +} diff --git a/opendcs-rest-api/src/test/java/org/opendcs/odcsapi/res/it/BaseIT.java b/opendcs-rest-api/src/test/java/org/opendcs/odcsapi/res/it/BaseIT.java index 999509a9..c1b57e79 100644 --- a/opendcs-rest-api/src/test/java/org/opendcs/odcsapi/res/it/BaseIT.java +++ b/opendcs-rest-api/src/test/java/org/opendcs/odcsapi/res/it/BaseIT.java @@ -128,7 +128,7 @@ void authenticate(SessionFilter sessionFilter) void logout(SessionFilter sessionFilter) { - if(DatabaseSetupExtension.getCurrentDbType() == DbType.OPEN_TSDB) + if (DatabaseSetupExtension.getCurrentDbType() == DbType.OPEN_TSDB) { given() .log().ifValidationFails(LogDetail.ALL, true)