diff --git a/cwms-data-api/src/main/java/cwms/cda/api/ProjectController.java b/cwms-data-api/src/main/java/cwms/cda/api/ProjectController.java new file mode 100644 index 000000000..64f170dc0 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/ProjectController.java @@ -0,0 +1,296 @@ +package cwms.cda.api; + +import static com.codahale.metrics.MetricRegistry.name; +import static cwms.cda.api.Controllers.CREATE; +import static cwms.cda.api.Controllers.DELETE; +import static cwms.cda.api.Controllers.GET_ALL; +import static cwms.cda.api.Controllers.GET_ONE; +import static cwms.cda.api.Controllers.ID_MASK; +import static cwms.cda.api.Controllers.METHOD; +import static cwms.cda.api.Controllers.NAME; +import static cwms.cda.api.Controllers.OFFICE; +import static cwms.cda.api.Controllers.PAGE; +import static cwms.cda.api.Controllers.PAGE_SIZE; +import static cwms.cda.api.Controllers.RESULTS; +import static cwms.cda.api.Controllers.SIZE; +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 static cwms.cda.api.Controllers.UPDATE; +import static cwms.cda.api.Controllers.queryParamAsClass; +import static cwms.cda.api.Controllers.requiredParam; +import static cwms.cda.data.dao.JooqDao.getDslContext; + +import com.codahale.metrics.Histogram; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import cwms.cda.api.errors.CdaError; +import cwms.cda.data.dao.DeleteRule; +import cwms.cda.data.dao.JooqDao; +import cwms.cda.data.dao.ProjectDao; +import cwms.cda.data.dto.Project; +import cwms.cda.data.dto.Projects; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.FormattingException; +import io.javalin.apibuilder.CrudHandler; +import io.javalin.core.util.Header; +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.util.logging.Logger; +import javax.servlet.http.HttpServletResponse; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; + +public class ProjectController implements CrudHandler { + public static final Logger logger = Logger.getLogger(ProjectController.class.getName()); + private static final int DEFAULT_PAGE_SIZE = 100; + public static final String TAG = "Projects"; + + private final MetricRegistry metrics; + + private final Histogram requestResultSize; + + public ProjectController(MetricRegistry metrics) { + this.metrics = metrics; + String className = this.getClass().getName(); + + requestResultSize = this.metrics.histogram((name(className, RESULTS, SIZE))); + } + + private Timer.Context markAndTime(String subject) { + return Controllers.markAndTime(metrics, getClass().getName(), subject); + } + + @OpenApi(queryParams = { + @OpenApiParam(name = OFFICE, description = "Specifies the owning office of the data" + + " in the response. If this field is not specified, matching items from all" + + " offices shall be returned."), + @OpenApiParam(name = ID_MASK, description = "Project Id mask."), + @OpenApiParam(name = PAGE, + description = "This end point can return a lot of data, this identifies where" + + " in the request you are. This is an opaque value, and can be" + + " obtained from the 'next-page' value in the response."), + @OpenApiParam(name = PAGE_SIZE, type = Integer.class, + description = "How many entries per page returned. " + + "Default " + DEFAULT_PAGE_SIZE + ".") + }, + responses = { + @OpenApiResponse(status = STATUS_200, content = { + @OpenApiContent(type = Formats.JSON, from = Projects.class)}), + @OpenApiResponse(status = STATUS_404, description = "Based on the combination of" + + " inputs provided the projects were not found."), + @OpenApiResponse(status = STATUS_501, description = "request format is not" + + " implemented")}, + description = "Returns Projects Data", + tags = {TAG}) + @Override + public void getAll(@NotNull Context ctx) { + try (final Timer.Context ignored = markAndTime(GET_ALL)) { + DSLContext dsl = getDslContext(ctx); + + ProjectDao dao = new ProjectDao(dsl); + String office = ctx.queryParam(OFFICE); + + String projectIdMask = ctx.queryParam(ID_MASK); + + String cursor = queryParamAsClass(ctx, new String[]{PAGE}, + String.class, "", metrics, name(ProjectController.class.getName(), + GET_ALL)); + + int pageSize = queryParamAsClass(ctx, new String[]{PAGE_SIZE}, + Integer.class, DEFAULT_PAGE_SIZE, metrics, + name(ProjectController.class.getName(), GET_ALL)); + + Projects projects = dao.retrieveProjectsFromTable(cursor, pageSize, projectIdMask, office); + + ContentType contentType = getContentType(ctx); + ctx.contentType(contentType.toString()); + String serialized = Formats.format(contentType, projects); + ctx.result(serialized); + ctx.status(HttpServletResponse.SC_OK); + requestResultSize.update(serialized.length()); + + } + + } + + private static @NotNull ContentType getContentType(Context ctx) { + String formatHeader = ctx.header(Header.ACCEPT) != null ? ctx.header(Header.ACCEPT) : Formats.JSON; + ContentType contentType = Formats.parseHeader(formatHeader); + if (contentType == null) { + throw new FormattingException("Format header could not be parsed"); + } + return contentType; + } + + @OpenApi( + pathParams = { + @OpenApiParam(name = NAME, required = true, description = "Specifies the" + + " project to be included in the response."), + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the" + + " owning office of the Project whose data is to be included in the" + + " response."), + }, + responses = { + @OpenApiResponse(status = STATUS_200, content = { + @OpenApiContent(from = Project.class, type = Formats.JSON)}), + @OpenApiResponse(status = STATUS_404, description = "Based on the combination of " + + "inputs provided the Project was not found."), + @OpenApiResponse(status = STATUS_501, description = "request format is not " + + "implemented")}, + description = "Retrieves requested Project", tags = {"Projects"}) + @Override + public void getOne(@NotNull Context ctx, @NotNull String name) { + try (final Timer.Context ignored = markAndTime(GET_ONE)) { + DSLContext dsl = getDslContext(ctx); + + ProjectDao dao = new ProjectDao(dsl); + + // These are required + String office = requiredParam(ctx, OFFICE); + + Project project = dao.retrieveProject(office, name); + + if (project == null) { + CdaError re = new CdaError("Unable to find Project based on parameters given"); + logger.info(() -> { + String fullUrl = ctx.fullUrl(); + return re + System.lineSeparator() + "for request " + fullUrl; + }); + ctx.status(HttpServletResponse.SC_NOT_FOUND).json(re); + } else { + String formatHeader = ctx.header(Header.ACCEPT); + ContentType contentType = Formats.parseHeaderAndQueryParm(formatHeader, ""); + ctx.contentType(contentType.toString()); + + String result = Formats.format(contentType, project); + + ctx.result(result); + requestResultSize.update(result.length()); + + ctx.status(HttpServletResponse.SC_OK); + } + } + } + + @OpenApi( + description = "Create new Project", + requestBody = @OpenApiRequestBody(required = true, + content = {@OpenApiContent(from = Project.class, type = Formats.JSON)} + ), + method = HttpMethod.POST, + tags = {TAG} + ) + @Override + public void create(@NotNull Context ctx) { + try (Timer.Context ignored = markAndTime(CREATE)) { + DSLContext dsl = getDslContext(ctx); + + String reqContentType = ctx.req.getContentType(); + String formatHeader = reqContentType != null ? reqContentType : Formats.JSON; + ContentType contentType = Formats.parseHeader(formatHeader); + if (contentType == null) { + throw new FormattingException("Format header could not be parsed"); + } + Project project = Formats.parseContent(contentType, ctx.body(), Project.class); + project.validate(); + + ProjectDao dao = new ProjectDao(dsl); + + dao.create(project); + ctx.status(HttpServletResponse.SC_CREATED); + } + } + + @OpenApi( + description = "Updates a project", + pathParams = { + @OpenApiParam(name = NAME, description = "The id of the project to be updated"), + }, + requestBody = @OpenApiRequestBody( + content = { + @OpenApiContent(from = Project.class, type = Formats.JSON), + }, + required = true + ), + method = HttpMethod.PATCH, + tags = {TAG} + ) + @Override + public void update(@NotNull Context ctx, @NotNull String name) { + + try (Timer.Context ignored = markAndTime(UPDATE)) { + String reqContentType = ctx.req.getContentType(); + String formatHeader = reqContentType != null ? reqContentType : Formats.JSON; + ContentType contentType = Formats.parseHeader(formatHeader); + if (contentType == null) { + throw new FormattingException("Format header could not be parsed"); + } + Project project = Formats.parseContent(contentType, ctx.body(), Project.class); + project.validate(); + DSLContext dsl = getDslContext(ctx); + + ProjectDao dao = new ProjectDao(dsl); + dao.update(project); + } + } + + + @OpenApi( + description = "Deletes requested reservoir project", + pathParams = { + @OpenApiParam(name = NAME, description = "The project identifier to be deleted"), + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + + "owning office of the project to be deleted"), + @OpenApiParam(name = METHOD, type = JooqDao.DeleteMethod.class, + description = "Specifies the delete method used. " + + "Defaults to \"DELETE_KEY\"") + }, + method = HttpMethod.DELETE, + tags = {TAG} + ) + @Override + public void delete(@NotNull Context ctx, @NotNull String name) { + try (Timer.Context ignored = markAndTime(DELETE)) { + DSLContext dsl = getDslContext(ctx); + String office = requiredParam(ctx, OFFICE); + + JooqDao.DeleteMethod deleteMethod = ctx.queryParamAsClass(METHOD, JooqDao.DeleteMethod.class) + .getOrDefault(JooqDao.DeleteMethod.DELETE_KEY); + + ProjectDao dao = new ProjectDao(dsl); + dao.delete(office, name, getDeleteRule(deleteMethod)); + + ctx.status(HttpServletResponse.SC_NO_CONTENT); + } + } + + private static @NotNull DeleteRule getDeleteRule(JooqDao.DeleteMethod deleteMethod) { + DeleteRule deleteRule; + switch (deleteMethod) { + case DELETE_ALL: + deleteRule = DeleteRule.DELETE_ALL; + break; + case DELETE_DATA: + deleteRule = DeleteRule.DELETE_DATA; + break; + case DELETE_KEY: + deleteRule = DeleteRule.DELETE_KEY; + break; + default: + throw new IllegalArgumentException("Delete Method provided does not match accepted rule constants: " + + deleteMethod); + } + return deleteRule; + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/ProjectDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/ProjectDao.java new file mode 100644 index 000000000..955c9f461 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/ProjectDao.java @@ -0,0 +1,571 @@ +package cwms.cda.data.dao; + +import static org.jooq.impl.DSL.asterisk; +import static org.jooq.impl.DSL.count; +import static org.jooq.impl.DSL.noCondition; + +import cwms.cda.api.errors.NotFoundException; +import cwms.cda.data.dto.CwmsDTOPaginated; +import cwms.cda.data.dto.Location; +import cwms.cda.data.dto.Project; +import cwms.cda.data.dto.Projects; +import cwms.cda.helpers.ResourceHelper; +import java.math.BigDecimal; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.TimeZone; +import java.util.logging.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jooq.Condition; +import org.jooq.DSLContext; +import org.jooq.Record1; +import org.jooq.SelectConditionStep; +import org.jooq.SelectLimitPercentStep; +import usace.cwms.db.dao.ifc.loc.LocationRefType; +import usace.cwms.db.dao.ifc.loc.LocationType; +import usace.cwms.db.dao.util.OracleTypeMap; +import usace.cwms.db.jooq.codegen.packages.CWMS_PROJECT_PACKAGE; +import usace.cwms.db.jooq.codegen.tables.AV_PROJECT; +import usace.cwms.db.jooq.codegen.udt.records.LOCATION_OBJ_T; +import usace.cwms.db.jooq.codegen.udt.records.LOCATION_REF_T; +import usace.cwms.db.jooq.codegen.udt.records.PROJECT_OBJ_T; +import usace.cwms.db.jooq.dao.util.LocationTypeUtil; + +public class ProjectDao extends JooqDao { + private static final Logger logger = Logger.getLogger(ProjectDao.class.getName()); + public static final String OFFICE_ID = "office_id"; + public static final String PROJECT_ID = "project_id"; + public static final String AUTHORIZING_LAW = "authorizing_law"; + public static final String PROJECT_OWNER = "project_owner"; + public static final String HYDROPOWER_DESCRIPTION = "hydropower_description"; + public static final String SEDIMENTATION_DESCRIPTION = "sedimentation_description"; + public static final String DOWNSTREAM_URBAN_DESCRIPTION = "downstream_urban_description"; + public static final String BANK_FULL_CAPACITY_DESCRIPTION = "bank_full_capacity_description"; + public static final String PUMP_BACK_OFFICE_ID = "pump_back_office_id"; + public static final String PUMP_BACK_LOCATION_ID = "pump_back_location_id"; + public static final String NEAR_GAGE_OFFICE_ID = "near_gage_office_id"; + public static final String NEAR_GAGE_LOCATION_ID = "near_gage_location_id"; + public static final String PROJECT_REMARKS = "project_remarks"; + public static final String FEDERAL_COST = "federal_cost"; + public static final String NONFEDERAL_COST = "nonfederal_cost"; + public static final String FEDERAL_OM_COST = "FEDERAL_OM_COST"; + public static final String NONFEDERAL_OM_COST = "NONFEDERAL_OM_COST"; + public static final String COST_YEAR = "COST_YEAR"; + public static final String YIELD_TIME_FRAME_START = "yield_time_frame_start"; + public static final String YIELD_TIME_FRAME_END = "yield_time_frame_end"; + + private final Calendar UTC_CALENDAR = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + + private static final String SELECT_PART = + ResourceHelper.getResourceAsString("/cwms/data/sql/project/project_select.sql", ProjectDao.class); + + + public ProjectDao(DSLContext dsl) { + super(dsl); + } + + public Project retrieveProject(String office, String projectId) { + + PROJECT_OBJ_T projectObjT = connectionResult(dsl, + c -> CWMS_PROJECT_PACKAGE.call_RETRIEVE_PROJECT( + getDslContext(c, office).configuration(), projectId, office) + ); + + return projectObjT == null ? null : buildProject(projectObjT); + } + + private Project buildProject(PROJECT_OBJ_T projectObjT) { + Project.Builder builder = new Project.Builder(); + + LOCATION_OBJ_T projectLocation = projectObjT.getPROJECT_LOCATION(); + LOCATION_REF_T locRef = projectLocation.getLOCATION_REF(); + String office = locRef.getOFFICE_ID(); + builder.withOfficeId(office); + + String id = getLocationId(locRef.getBASE_LOCATION_ID(), locRef.getSUB_LOCATION_ID()); + builder.withName(id); + + String authorizingLaw = projectObjT.getAUTHORIZING_LAW(); + builder.withAuthorizingLaw(authorizingLaw); + + String bankFullCapacityDescription = projectObjT.getBANK_FULL_CAPACITY_DESCRIPTION(); + builder.withBankFullCapacityDesc(bankFullCapacityDescription); + + String downstreamUrbanDescription = projectObjT.getDOWNSTREAM_URBAN_DESCRIPTION(); + builder.withDownstreamUrbanDesc(downstreamUrbanDescription); + + String costUnitsId = projectObjT.getCOST_UNITS_ID(); + builder.withCostUnit(costUnitsId); + + String projectOwner = projectObjT.getPROJECT_OWNER(); + builder.withProjectOwner(projectOwner); + + String hydropowerDescription = projectObjT.getHYDROPOWER_DESCRIPTION(); + builder.withHydropowerDesc(hydropowerDescription); + + String remarks = projectObjT.getREMARKS(); + builder.withProjectRemarks(remarks); + + String sedimentationDescription = projectObjT.getSEDIMENTATION_DESCRIPTION(); + builder.withSedimentationDesc(sedimentationDescription); + + LocationType neargageLocationType = + LocationTypeUtil.toLocationType(projectObjT.getNEAR_GAGE_LOCATION()); + LocationRefType nearLocRef = null; + if (neargageLocationType != null) { + nearLocRef = neargageLocationType.getLocationRef(); + } + if (nearLocRef != null) { + builder = builder.withNearGageLocation(new Location.Builder(nearLocRef.getOfficeId(), + getLocationId(nearLocRef.getBaseLocationId(), nearLocRef.getSubLocationId())) + .withActive(null) + .build() + ); + } + + LocationType pumpbackLocationType = + LocationTypeUtil.toLocationType(projectObjT.getPUMP_BACK_LOCATION()); + LocationRefType pumpbackLocRef = null; + if (pumpbackLocationType != null) { + pumpbackLocRef = pumpbackLocationType.getLocationRef(); + } + if (pumpbackLocRef != null) { + builder = builder.withPumpBackLocation(new Location.Builder(pumpbackLocRef.getOfficeId(), + getLocationId(pumpbackLocRef.getBaseLocationId(), pumpbackLocRef.getSubLocationId())) + .withActive(null) + .build() + ); + } + + BigDecimal federalCost = projectObjT.getFEDERAL_COST(); + if (federalCost != null) { + builder = builder.withFederalCost(federalCost.doubleValue()); + } + + BigDecimal federalOandMCost = projectObjT.getFEDERAL_OM_COST(); + if (federalOandMCost != null) { + builder = builder.withFederalOAndMCost(federalOandMCost.doubleValue()); + } + + BigDecimal nonFederalCost = projectObjT.getNONFEDERAL_COST(); + if (nonFederalCost != null) { + builder = builder.withNonFederalCost(nonFederalCost.doubleValue()); + } + + BigDecimal nonFederalOandMCost = projectObjT.getNONFEDERAL_OM_COST(); + if (nonFederalOandMCost != null) { + builder = builder.withNonFederalOAndMCost(nonFederalOandMCost.doubleValue()); + } + + Timestamp costYear = projectObjT.getCOST_YEAR(); + if (costYear != null) { + builder = builder.withCostYear(costYear.toInstant()); + } + + Timestamp yieldTimeFrameEnd = projectObjT.getYIELD_TIME_FRAME_END(); + if (yieldTimeFrameEnd != null) { + builder = builder.withYieldTimeFrameEnd(yieldTimeFrameEnd.toInstant()); + } + + Timestamp yieldTimeFrameStart = projectObjT.getYIELD_TIME_FRAME_START(); + if (yieldTimeFrameStart != null) { + builder = builder.withYieldTimeFrameStart(yieldTimeFrameStart.toInstant()); + } + + return builder.build(); + } + + + + private static Project buildProject(usace.cwms.db.jooq.codegen.tables.records.AV_PROJECT r) { + Project.Builder builder = new Project.Builder(); + builder.withOfficeId(r.getOFFICE_ID()); + builder.withName(r.getPROJECT_ID()); + builder.withPumpBackLocation(new Location.Builder(r.getOFFICE_ID(), r.getPUMP_BACK_LOCATION_ID()) + .withActive(null) + .build() + ); // Can we assume same office? + builder.withNearGageLocation(new Location.Builder(r.getOFFICE_ID(), r.getNEAR_GAGE_LOCATION_ID()) + .withActive(null) + .build() + ); // Can we assume same office? + + builder.withAuthorizingLaw(r.getAUTHORIZING_LAW()); + builder.withProjectRemarks(r.getPROJECT_REMARKS()); + builder.withProjectOwner(r.getPROJECT_OWNER()); + builder.withHydropowerDesc(r.getHYDROPOWER_DESCRIPTION()); + builder.withSedimentationDesc(r.getSEDIMENTATION_DESCRIPTION()); + builder.withDownstreamUrbanDesc(r.getDOWNSTREAM_URBAN_DESCRIPTION()); + builder.withBankFullCapacityDesc(r.getBANK_FULL_CAPACITY_DESCRIPTION()); + BigDecimal federalCost = r.getFEDERAL_COST(); + if (federalCost != null) { + builder.withFederalCost(federalCost.doubleValue()); + } + BigDecimal nonfederalCost = r.getNONFEDERAL_COST(); + if (nonfederalCost != null) { + builder.withNonFederalCost(nonfederalCost.doubleValue()); + } + Timestamp yieldTimeFrameStart = r.getYIELD_TIME_FRAME_START(); + if (yieldTimeFrameStart != null) { + builder.withYieldTimeFrameStart(yieldTimeFrameStart.toInstant()); + } + Timestamp yieldTimeFrameEnd = r.getYIELD_TIME_FRAME_END(); + if (yieldTimeFrameEnd != null) { + builder.withYieldTimeFrameEnd(yieldTimeFrameEnd.toInstant()); + } + + // The view is missing cost-year, fed_om_cat and nonfed_om_cost and the pump office and + // near gage office. + + return builder.build(); + } + + public static String getLocationId(String base, String sub) { + boolean hasSub = sub != null && !sub.isEmpty(); + return hasSub ? base + "-" + sub : base; + } + + public Projects retrieveProjectsFromView(String cursor, int pageSize, String projectIdMask, + String office) { + + Condition whereClause = + JooqDao.caseInsensitiveLikeRegexNullTrue(AV_PROJECT.AV_PROJECT.PROJECT_ID, + projectIdMask); + if (office != null) { + whereClause = whereClause.and(AV_PROJECT.AV_PROJECT.OFFICE_ID.eq(office)); + } + + String cursorOffice = null; + String cursorProjectId = null; + int total = 0; + if (cursor == null || cursor.isEmpty()) { + SelectConditionStep> count = + dsl.select(count(asterisk())) + .from(AV_PROJECT.AV_PROJECT) + .where(whereClause); + total = count.fetchOne().value1(); + } else { + String[] parts = CwmsDTOPaginated.decodeCursor(cursor); + if (parts.length == 4) { + cursorOffice = parts[0]; + cursorProjectId = parts[1]; + pageSize = Integer.parseInt(parts[2]); + total = Integer.parseInt(parts[3]); + } + } + + Condition pagingCondition = noCondition(); + if (cursorOffice != null || cursorProjectId != null) { + Condition inSameOffice = AV_PROJECT.AV_PROJECT.OFFICE_ID.eq(cursorOffice) + .and(AV_PROJECT.AV_PROJECT.PROJECT_ID.gt(cursorProjectId)); + Condition nextOffice = AV_PROJECT.AV_PROJECT.OFFICE_ID.gt(cursorOffice); + pagingCondition = inSameOffice.or(nextOffice); + } + + SelectLimitPercentStep query = + dsl.selectFrom(AV_PROJECT.AV_PROJECT) + .where(whereClause.and(pagingCondition)) + .orderBy(AV_PROJECT.AV_PROJECT.OFFICE_ID, AV_PROJECT.AV_PROJECT.PROJECT_ID) + .limit(pageSize); + + List projs = query.fetch().map(ProjectDao::buildProject); + + Projects.Builder builder = new Projects.Builder(cursor, pageSize, total); + builder.addAll(projs); + return builder.build(); + } + + public Projects retrieveProjectsFromTable(String cursor, int pageSize, + @Nullable String projectIdMask, + @Nullable String office) { + final String cursorOffice; + final String cursorProjectId; + int total; + if (cursor == null || cursor.isEmpty()) { + cursorOffice = null; + cursorProjectId = null; + + Condition whereClause = + JooqDao.caseInsensitiveLikeRegexNullTrue(AV_PROJECT.AV_PROJECT.PROJECT_ID, + projectIdMask); + if (office != null) { + whereClause = whereClause.and(AV_PROJECT.AV_PROJECT.OFFICE_ID.eq(office)); + } + + SelectConditionStep> count = + dsl.select(count(asterisk())) + .from(AV_PROJECT.AV_PROJECT) + .where(whereClause); + + total = count.fetchOne().value1(); + } else { + cursorOffice = Projects.getOffice(cursor); + cursorProjectId = Projects.getId(cursor); + pageSize = Projects.getPageSize(cursor); + total = Projects.getTotal(cursor); + } + + // There are lots of ways the variables can be null or not so we need to build the query + // based on the parameters. + String query = buildTableQuery(projectIdMask, office, cursorOffice, cursorProjectId); + + int finalPageSize = pageSize; + List projs = connectionResult(dsl, c -> { + List projects; + try (PreparedStatement ps = c.prepareStatement(query)) { + fillTableQueryParameters(ps, projectIdMask, office, cursorOffice, cursorProjectId + , finalPageSize); + + try (ResultSet resultSet = ps.executeQuery()) { + projects = new ArrayList<>(); + while (resultSet.next()) { + Project built = buildProjectFromTableRow(resultSet); + projects.add(built); + } + } + } + return projects; + }); + + Projects.Builder builder = new Projects.Builder(cursor, pageSize, total); + builder.addAll(projs); + return builder.build(); + } + + private Project buildProjectFromTableRow(ResultSet resultSet) throws SQLException { + Project.Builder builder = new Project.Builder(); + builder.withOfficeId(resultSet.getString(OFFICE_ID)); + builder.withName(resultSet.getString(PROJECT_ID)); + builder.withAuthorizingLaw(resultSet.getString(AUTHORIZING_LAW)); + builder.withProjectOwner(resultSet.getString(PROJECT_OWNER)); + builder.withHydropowerDesc(resultSet.getString(HYDROPOWER_DESCRIPTION)); + builder.withSedimentationDesc(resultSet.getString(SEDIMENTATION_DESCRIPTION)); + builder.withDownstreamUrbanDesc(resultSet.getString(DOWNSTREAM_URBAN_DESCRIPTION)); + builder.withBankFullCapacityDesc(resultSet.getString(BANK_FULL_CAPACITY_DESCRIPTION)); + builder.withPumpBackLocation( + new Location.Builder(resultSet.getString(PUMP_BACK_OFFICE_ID), + resultSet.getString(PUMP_BACK_LOCATION_ID)) + .build() + ); + + builder.withNearGageLocation( + new Location.Builder(resultSet.getString(NEAR_GAGE_OFFICE_ID), + resultSet.getString(NEAR_GAGE_LOCATION_ID)) + .build() + ); + + builder.withProjectRemarks(resultSet.getString(PROJECT_REMARKS)); + + BigDecimal federalCost = resultSet.getBigDecimal(FEDERAL_COST); + if (federalCost != null) { + builder.withFederalCost(federalCost.doubleValue()); + } + + BigDecimal nonfederalCost = resultSet.getBigDecimal(NONFEDERAL_COST); + if (nonfederalCost != null) { + builder.withNonFederalCost(nonfederalCost.doubleValue()); + } + + BigDecimal federalOmCost = resultSet.getBigDecimal(FEDERAL_OM_COST); + if (federalOmCost != null) { + builder.withFederalOAndMCost(federalOmCost.doubleValue()); + } + BigDecimal nonfederalOmCost = resultSet.getBigDecimal(NONFEDERAL_OM_COST); + if (nonfederalOmCost != null) { + builder.withNonFederalOAndMCost(nonfederalOmCost.doubleValue()); + } + + Timestamp costStamp = resultSet.getTimestamp(COST_YEAR, UTC_CALENDAR); + if (costStamp != null) { + builder.withCostYear(costStamp.toInstant()); + } + + Timestamp yieldTimeFrameStart = resultSet.getTimestamp(YIELD_TIME_FRAME_START, + UTC_CALENDAR); + if (yieldTimeFrameStart != null) { + builder.withYieldTimeFrameStart(yieldTimeFrameStart.toInstant()); + } + Timestamp yieldTimeFrameEnd = resultSet.getTimestamp(YIELD_TIME_FRAME_END, UTC_CALENDAR); + if (yieldTimeFrameEnd != null) { + builder.withYieldTimeFrameEnd(yieldTimeFrameEnd.toInstant()); + } + + return builder.build(); + } + + private void fillTableQueryParameters(PreparedStatement ps, String projectIdMask, String office, + String cursorOffice, String cursorProjectId, + int finalPageSize) throws SQLException { + int index = 1; + if (projectIdMask != null) { + ps.setString(index++, projectIdMask); + } + + if (office != null) { + ps.setString(index++, office); + } + + if (cursorOffice != null) { + ps.setString(index++, cursorOffice); + ps.setString(index++, cursorProjectId); + ps.setString(index++, cursorOffice); // its in there twice.... + } + + ps.setInt(index, finalPageSize); + } + + private static String buildTableQuery(@Nullable String projectIdMask, @Nullable String office, + String cursorOffice, String cursorProjectId) { + String sql = SELECT_PART; + + if (projectIdMask != null || office != null || cursorOffice != null || cursorProjectId != null) { + sql += "where ("; + + if (projectIdMask != null && office != null) { + sql += "(regexp_like(project.location_id, ?, 'i'))\n" // projectIdMask + + " and office_id = ?\n"; + } else if (projectIdMask != null) { + sql += "(regexp_like(project.location_id, ?, 'i'))\n"; // projectIdMask + } else if (office != null) { + sql += "office_id = ?\n"; // office + } + + if (cursorOffice != null || cursorProjectId != null) { + sql += " and (\n" + + " (\n" + + " OFFICE_ID = ?\n" // cursorOffice + + " and project.location_id > ?\n" //cursorProjectId + + " )\n" + + " or OFFICE_ID > ?\n" // cursorOffice + + " )\n"; + } + + sql += ")\n"; + } + + sql += "order by office_id, project_id fetch next ? rows only"; // pageSize + + return sql; + } + + public void create(Project project) { + boolean failIfExists = true; + String office = project.getOfficeId(); + + PROJECT_OBJ_T projectT = toProjectT(project); + connection(dsl, + c -> CWMS_PROJECT_PACKAGE.call_STORE_PROJECT(getDslContext(c, office).configuration(), + projectT, OracleTypeMap.formatBool(failIfExists))); + } + + public static LOCATION_REF_T toLocationRefT(String base, String sub, String office) { + return new LOCATION_REF_T(base, sub, office); + } + + + private static @NotNull LOCATION_REF_T getLocationRefT(String locationId, String office) { + String base; + String sub; + if (locationId == null) { + base = null; + sub = null; + } else { + int fieldIndex = locationId.indexOf("-"); + if (fieldIndex == -1) { + base = locationId; + sub = null; + } else { + base = locationId.substring(0, fieldIndex); + sub = locationId.substring(fieldIndex + 1); + } + } + + return toLocationRefT(base, sub, office); + } + + + public void store(Project project, boolean failIfExists) { + String office = project.getOfficeId(); + + PROJECT_OBJ_T projectT = toProjectT(project); + connection(dsl, + c -> CWMS_PROJECT_PACKAGE.call_STORE_PROJECT(getDslContext(c, office).configuration(), + projectT, OracleTypeMap.formatBool(failIfExists))); + + } + + public void update(Project project) { + String office = project.getOfficeId(); + Project existingProject = retrieveProject(office, project.getName()); + if (existingProject == null) { + throw new NotFoundException("Could not find project to update."); + } + + PROJECT_OBJ_T projectT = toProjectT(project); + connection(dsl, + c -> CWMS_PROJECT_PACKAGE.call_STORE_PROJECT(getDslContext(c, office).configuration(), + projectT, OracleTypeMap.formatBool(false))); + + } + + private PROJECT_OBJ_T toProjectT(Project project) { + LOCATION_OBJ_T projectLocation = new LOCATION_OBJ_T(); + projectLocation.setLOCATION_REF(getLocationRefT(project.getName(), project.getOfficeId())); + + LOCATION_OBJ_T pumpBackLocation = null; + Location pb = project.getPumpBackLocation(); + if (pb != null) { + pumpBackLocation = new LOCATION_OBJ_T(); + pumpBackLocation.setLOCATION_REF(getLocationRefT(pb.getName(), pb.getOfficeId())); + } + + LOCATION_OBJ_T nearGageLocation = null; + Location ng = project.getNearGageLocation(); + if (ng != null) { + nearGageLocation = new LOCATION_OBJ_T(); + nearGageLocation.setLOCATION_REF(getLocationRefT(ng.getName(), ng.getOfficeId())); + } + + String authorizingLaw = project.getAuthorizingLaw(); + Timestamp costYear = project.getCostYear() != null + ? Timestamp.from(project.getCostYear()) : null; + BigDecimal federalCost = project.getFederalCost() != null + ? BigDecimal.valueOf(project.getFederalCost()) : null; + BigDecimal nonFederalCost = (project.getNonFederalCost() != null) + ? BigDecimal.valueOf(project.getNonFederalCost()) : null; + BigDecimal federalOandMCost = (project.getFederalOAndMCost() != null) + ? BigDecimal.valueOf(project.getFederalOAndMCost()) : null; + BigDecimal nonFederalOandMCost = (project.getNonFederalOAndMCost() != null) + ? BigDecimal.valueOf(project.getNonFederalOAndMCost()) : null; + String costUnitsId = project.getCostUnit(); + String remarks = project.getProjectRemarks(); + String projectOwner = project.getProjectOwner(); + String hydropowerDescription = project.getHydropowerDesc(); + String sedimentationDescription = project.getSedimentationDesc(); + String downstreamUrbanDescription = project.getDownstreamUrbanDesc(); + String bankFullCapacityDescription = project.getBankFullCapacityDesc(); + Timestamp yieldTimeFrameStartTimestamp = (project.getYieldTimeFrameStart() != null) + ? Timestamp.from(project.getYieldTimeFrameStart()) : null; + Timestamp yieldTimeFrameEndTimestamp = (project.getYieldTimeFrameEnd() != null) + ? Timestamp.from(project.getYieldTimeFrameEnd()) : null; + return new PROJECT_OBJ_T(projectLocation, pumpBackLocation, nearGageLocation, + authorizingLaw, costYear, federalCost, nonFederalCost, federalOandMCost, + nonFederalOandMCost, costUnitsId, remarks, projectOwner, hydropowerDescription, + sedimentationDescription, downstreamUrbanDescription, bankFullCapacityDescription, + yieldTimeFrameStartTimestamp, yieldTimeFrameEndTimestamp); + } + + public void delete(String office, String id, DeleteRule deleteRule) { + + connection(dsl, + c -> CWMS_PROJECT_PACKAGE.call_DELETE_PROJECT(getDslContext(c, office).configuration(), + id, deleteRule.getRule(), office + )); + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Projects.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Projects.java new file mode 100644 index 000000000..61a1eef6d --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Projects.java @@ -0,0 +1,122 @@ +package cwms.cda.data.dto; + +import cwms.cda.api.errors.FieldException; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV2; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + + +@FormattableWith(contentType = Formats.JSON, formatter = JsonV2.class) +public class Projects extends CwmsDTOPaginated { + + List projects; + + private Projects() { + } + + public Projects(String page, int pageSize, Integer total) { + super(page, pageSize, total); + projects = new ArrayList<>(); + } + + public List getProjects() { + return Collections.unmodifiableList(projects); + } + + + /** + * Extract the office from the cursor. + * + * @param cursor the cursor + * @return office + */ + public static String getOffice(String cursor) { + String[] parts = CwmsDTOPaginated.decodeCursor(cursor); + if (parts.length > 1) { + String[] idAndOffice = CwmsDTOPaginated.decodeCursor(parts[0]); + if (idAndOffice.length > 0) { + return idAndOffice[0]; + } + } + return null; + } + + /** + * Extract the id from the cursor. + * + * @param cursor the cursor + * @return id + */ + public static String getId(String cursor) { + String[] parts = CwmsDTOPaginated.decodeCursor(cursor); + if (parts.length > 1) { + String[] idAndOffice = CwmsDTOPaginated.decodeCursor(parts[0]); + if (idAndOffice.length > 1) { + return idAndOffice[1]; + } + } + return null; + } + + public static int getPageSize(String cursor) { + String[] parts = CwmsDTOPaginated.decodeCursor(cursor); + if (parts.length > 2) { + return Integer.parseInt(parts[2]); + } + return 0; + } + + public static int getTotal(String cursor) { + String[] parts = CwmsDTOPaginated.decodeCursor(cursor); + if (parts.length > 1) { + return Integer.parseInt(parts[1]); + } + return 0; + } + + + public static class Builder { + private Projects workingProjects; + + public Builder(String currentPage, int pageSize, Integer total) { + workingProjects = new Projects(currentPage, pageSize, total); + } + + public Projects build() { + if (this.workingProjects.projects.size() == this.workingProjects.pageSize) { + Project last = + this.workingProjects.projects.get(this.workingProjects.projects.size() - 1); + String cursor = encodeCursor(CwmsDTOPaginated.delimiter, last.getOfficeId(), + last.getName()); + this.workingProjects.nextPage = encodeCursor(cursor, + this.workingProjects.pageSize, this.workingProjects.total); + } else { + this.workingProjects.nextPage = null; + } + return workingProjects; + } + + public Builder add(Project project) { + this.workingProjects.projects.add(project); + return this; + } + + public Builder addAll(Collection projects) { + this.workingProjects.projects.addAll(projects); + return this; + } + } + + + @Override + public void validate() throws FieldException { + // TODO Auto-generated method stub + + } + + +} diff --git a/cwms-data-api/src/main/resources/cwms/data/sql/project/project_select.sql b/cwms-data-api/src/main/resources/cwms/data/sql/project/project_select.sql new file mode 100644 index 000000000..0dbf5be4b --- /dev/null +++ b/cwms-data-api/src/main/resources/cwms/data/sql/project/project_select.sql @@ -0,0 +1,76 @@ +select project.office_id, + project.location_id as project_id, + project.COST_YEAR, + project.federal_cost, + project.nonfederal_cost, + project.FEDERAL_OM_COST, + project.NONFEDERAL_OM_COST, + project.authorizing_law, + project.project_owner, + project.hydropower_description, + project.sedimentation_description, + project.downstream_urban_description, + project.bank_full_capacity_description, + pumpback.location_id as pump_back_location_id, + pumpback.p_office_id as pump_back_office_id, + neargage.location_id as near_gage_location_id, + neargage.n_office_id as near_gage_office_id, + project.yield_time_frame_start, + project.yield_time_frame_end, + project.project_remarks +from ( select o.office_id as office_id, + bl.base_location_id + ||substr('-', 1, length(pl.sub_location_id)) + ||pl.sub_location_id as location_id, + p.COST_YEAR, + p.federal_cost, + p.nonfederal_cost, + p.FEDERAL_OM_COST, + p.NONFEDERAL_OM_COST, + p.authorizing_law, + p.project_owner, + p.hydropower_description, + p.sedimentation_description, + p.downstream_urban_description, + p.bank_full_capacity_description, + p.pump_back_location_code, + p.near_gage_location_code, + p.yield_time_frame_start, + p.yield_time_frame_end, + p.project_remarks + from cwms_20.cwms_office o, + cwms_20.at_base_location bl, + cwms_20.at_physical_location pl, + cwms_20.at_project p + where bl.db_office_code = o.office_code + and pl.base_location_code = bl.base_location_code + and p.project_location_code = pl.location_code + ) project + left outer join + ( select pl.location_code, + o.office_id as p_office_id, + bl.base_location_id + ||substr('-', 1, length(pl.sub_location_id)) + ||pl.sub_location_id as location_id + from cwms_20.cwms_office o, + cwms_20.at_base_location bl, + cwms_20.at_physical_location pl, + cwms_20.at_project p + where bl.db_office_code = o.office_code + and pl.base_location_code = bl.base_location_code + and p.project_location_code = pl.location_code + ) pumpback on pumpback.location_code = project.pump_back_location_code + left outer join + ( select pl.location_code, + o.office_id as n_office_id, + bl.base_location_id + ||substr('-', 1, length(pl.sub_location_id)) + ||pl.sub_location_id as location_id + from cwms_20.cwms_office o, + cwms_20.at_base_location bl, + cwms_20.at_physical_location pl, + cwms_20.at_project p + where bl.db_office_code = o.office_code + and pl.base_location_code = bl.base_location_code + and p.project_location_code = pl.location_code + ) neargage on neargage.location_code = project.near_gage_location_code diff --git a/cwms-data-api/src/test/java/cwms/cda/api/ProjectControllerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/ProjectControllerIT.java new file mode 100644 index 000000000..8d8823e20 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/ProjectControllerIT.java @@ -0,0 +1,388 @@ +/* + * MIT License + * + * Copyright (c) 2024 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package cwms.cda.api; + +import static cwms.cda.security.KeyAccessManager.AUTH_HEADER; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.databind.ObjectMapper; +import cwms.cda.data.dto.Project; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.json.JsonV2; +import fixtures.TestAccounts; +import io.restassured.filter.log.LogDetail; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("integration") +final class ProjectControllerIT extends DataApiTestIT { + + + @Test + void test_get_create_delete() throws IOException { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/api/project.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + assertNotNull(json); + Project project = Formats.parseContent(new ContentType(Formats.JSON), json, Project.class); + + // Structure of test: + // 1)Create the Project + // 2)Retrieve the Project and assert that it exists + // 3)Delete the Project + // 4)Retrieve the Project and assert that it does not exist + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + //Create the project + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/projects/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)) + ; + + String office = project.getOfficeId(); + // Retrieve the project and assert that it exists + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE, office) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("projects/" + project.getName()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("office-id", equalTo(office)) + .body("name", equalTo(project.getName())) + ; + + // Delete a Project + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE, office) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("projects/" + project.getName()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)) + ; + + // Retrieve a Project and assert that it does not exist + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE, office) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("projects/" + project.getName()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)) + ; + } + + @Test + void test_update_does_not_exist() throws IOException { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/api/project_new.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + assertNotNull(json); + Project project = Formats.parseContent(new ContentType(Formats.JSON), json, Project.class); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + //Try to update the project - should fail b/c it does not exist + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/projects/" + project.getName()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)) + ; + + } + + @Test + void test_delete_does_not_exist() { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + // Delete a Project + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE, user.getOperatingOffice()) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("projects/" + "blah" + Math.random()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)) + ; + } + + @Test + void test_get_all() throws IOException { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/api/project.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + assertNotNull(json); + Project project = Formats.parseContent(new ContentType(Formats.JSON), json, Project.class); + + // Structure of test: + // 1)Create the Project + // 2)Retrieve the Project with getAll and assert that it exists + // 3)Delete the Project + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + //Create the project + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/projects/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)) + ; + + String office = project.getOfficeId(); + + long expectedCostYear = 1717282800000L; + + // Retrieve the project and assert that it exists + long expectedStart = 1717282800000L; + long expectedEnd = 1717308000000L; + + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE, office) + .queryParam(Controllers.ID_MASK, "^" + project.getName() + "$") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("projects/") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("projects[0].office-id", equalTo(office)) + .body("projects[0].name", equalTo(project.getName())) + .body("projects[0].federal-cost", equalTo(100.0f)) + .body("projects[0].non-federal-cost", equalTo(50.0f)) + .body("projects[0].federal-o-and-m-cost", equalTo(10.0f)) + .body("projects[0].non-federal-o-and-m-cost", equalTo(5.0f)) + .body("projects[0].authorizing-law", equalTo("Authorizing Law")) + .body("projects[0].project-owner", equalTo("Project Owner")) + .body("projects[0].hydropower-desc", equalTo("Hydropower Description")) + .body("projects[0].sedimentation-desc", equalTo("Sedimentation Description")) + .body("projects[0].downstream-urban-desc", equalTo("Downstream Urban Description")) + .body("projects[0].bank-full-capacity-desc", equalTo("Bank Full Capacity Description")) + .body("projects[0].project-remarks", equalTo("Remarks")) + .body("projects[0].yield-time-frame-start", equalTo(expectedStart)) + .body("projects[0].yield-time-frame-end", equalTo(expectedEnd)) + .body("projects[0].cost-year", equalTo(expectedCostYear)) +// TODO: .body("projects[0].pump-back-location-id", equalTo("Pumpback Location Id")) +// TODO: .body("projects[0].pump-back-office-id", equalTo("SPK")) +// TODO: .body("projects[0].near-gage-location-id", equalTo("Near Gage Location Id")) +// TODO: .body("projects[0].near-gage-office-id", equalTo("SPK")) +// TODO: .body("projects[0].cost-unit", equalTo("$")) + + ; + + // Delete a Project + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE, office) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("projects/" + project.getName()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)) + ; + } + + @Test + void test_get_all_paged() throws IOException { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/api/project.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + assertNotNull(json); + Project project = Formats.parseContent(new ContentType(Formats.JSON), json, Project.class); + + // Structure of test: + // 1)Create Projects + // 2)Retrieve the Project with getAll and assert that it exists + // 3)Delete the Projects + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + ObjectMapper om = JsonV2.buildObjectMapper(); + + for (int i = 0; i < 15; i++) { + Project.Builder builder = new Project.Builder(); + builder.from(project) + .withName(String.format("PageTest%2d", i)); + Project build = builder.build(); + String projJson = om.writeValueAsString(build); + + //Create the project + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(projJson) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/projects/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + } + + String office = project.getOfficeId(); + try { + ExtractableResponse extractableResponse = given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE, office) + .queryParam(Controllers.PAGE_SIZE, 5) + .queryParam(Controllers.ID_MASK, "^PageTest.*$") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("projects/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("projects[0].office-id", equalTo(office)) + .body("projects[0].name", equalTo("PageTest 0")) + .body("projects[1].name", equalTo("PageTest 1")) + .body("projects[2].name", equalTo("PageTest 2")) + .body("projects[3].name", equalTo("PageTest 3")) + .body("projects[4].name", equalTo("PageTest 4")) + .extract(); + + String next = extractableResponse.path("next-page"); + assertNotNull(next); + assertFalse(next.isEmpty()); + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE, office) + .queryParam(Controllers.PAGE, next) + .queryParam(Controllers.PAGE_SIZE, 5) + .queryParam(Controllers.ID_MASK, "^PageTest.*$") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("projects/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("projects[0].office-id", equalTo(office)) + .body("projects[0].name", equalTo("PageTest 5")) + .body("projects[1].name", equalTo("PageTest 6")) + .body("projects[2].name", equalTo("PageTest 7")) + .body("projects[3].name", equalTo("PageTest 8")) + .body("projects[4].name", equalTo("PageTest 9")) + ; + } finally { + for (int i = 0; i < 15; i++) { + // Delete the Projects + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE, office) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete(String.format("projects/PageTest%2d", i)) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + ; + + } + } + } + +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/ProjectTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/ProjectTest.java index 051d681cf..ec90cf572 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/ProjectTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/ProjectTest.java @@ -55,7 +55,6 @@ void testProject() throws JsonProcessingException { ObjectWriter ow = om.writerWithDefaultPrettyPrinter(); String json = ow.writeValueAsString(project); - assertNotNull(json); } @@ -88,10 +87,10 @@ void testDeserialize() throws IOException { assertEquals("SPK", project.getPumpBackLocation().getOfficeId()); assertEquals("Near Gage Location Id", project.getNearGageLocation().getName()); assertEquals("SPK", project.getNearGageLocation().getOfficeId()); + assertEquals("Bank Full Capacity Description", project.getBankFullCapacityDesc()); assertEquals("Downstream Urban Description", project.getDownstreamUrbanDesc()); assertEquals("Hydropower Description", project.getHydropowerDesc()); assertEquals("Sedimentation Description", project.getSedimentationDesc()); - } } diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/ProjectsTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/ProjectsTest.java new file mode 100644 index 000000000..98fc775ac --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/ProjectsTest.java @@ -0,0 +1,123 @@ +package cwms.cda.data.dto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ProjectsTest { + + public static final int PAGE_SIZE = 10; + public static final int TOTAL = 37; + public static final String OFFICE = "SPK"; + + @Test + void testPaging() { + + List projList = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Project project = buildProject(i); + projList.add(project); + } + + Projects first = new Projects.Builder(null, PAGE_SIZE, TOTAL) + .addAll(projList) + .build(); + + List listInObj = first.getProjects(); + assertNotNull(listInObj); + assertEquals(10, listInObj.size()); + + List pages = new ArrayList<>(); + + pages.add(first.getPage()); + + String nextPage = first.getNextPage(); + + String id = Projects.getId(nextPage); + assertEquals("Test Project9", id, "Expected last project to be 9"); + String office = Projects.getOffice(nextPage); + assertEquals(OFFICE, office, "Expected office to be SPK"); + int pageSize = Projects.getPageSize(nextPage); + assertEquals(PAGE_SIZE, pageSize, "Expected page size to be " + PAGE_SIZE); + int total = Projects.getTotal(nextPage); + assertEquals(TOTAL, total, "Expected total to be " + TOTAL); + + pages.add(nextPage); + String currentPage = nextPage; + projList.clear(); + + for (int i = 10; i < 20; i++) { + Project project = buildProject(i); + projList.add(project); + } + + Projects second = new Projects.Builder(currentPage, PAGE_SIZE, TOTAL) + .addAll(projList) + .build(); + + nextPage = second.getNextPage(); + + id = Projects.getId(nextPage); + assertEquals("Test Project19", id, "Expected last project to be 19"); + office = Projects.getOffice(nextPage); + assertEquals(OFFICE, office, "Expected office to be SPK"); + pageSize = Projects.getPageSize(nextPage); + assertEquals(PAGE_SIZE, pageSize, "Expected page size to be " + PAGE_SIZE); + total = Projects.getTotal(nextPage); + assertEquals(TOTAL, total, "Expected total to be " + TOTAL); + + pages.add(nextPage); + currentPage = nextPage; + projList.clear(); + + for (int i = 20; i < 30; i++) { + Project project = buildProject(i); + projList.add(project); + } + + Projects third = new Projects.Builder(currentPage, PAGE_SIZE, TOTAL) + .addAll(projList) + .build(); + + + nextPage = third.getNextPage(); + + id = Projects.getId(nextPage); + assertEquals("Test Project29", id, "Expected last project to be 29"); + office = Projects.getOffice(nextPage); + assertEquals(OFFICE, office, "Expected office to be " + OFFICE); + pageSize = Projects.getPageSize(nextPage); + assertEquals(PAGE_SIZE, pageSize, "Expected page size to be " + PAGE_SIZE); + total = Projects.getTotal(nextPage); + assertEquals(TOTAL, total, "Expected total to be " + TOTAL); + + pages.add(nextPage); + currentPage = nextPage; + projList.clear(); + + for (int i = 30; i < 37; i++) { + Project project = buildProject(i); + projList.add(project); + } + + Projects fourth = new Projects.Builder(currentPage, PAGE_SIZE, 37) + .addAll(projList) + .build(); + + nextPage = fourth.getNextPage(); + + assertNull(nextPage, "Expected no next page"); + } + + private static Project buildProject(int i) { + Project project = new Project.Builder() + .withOfficeId(OFFICE) + .withName("Test Project" + i) + .build(); + return project; + } +} \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/project.json b/cwms-data-api/src/test/resources/cwms/cda/api/project.json new file mode 100644 index 000000000..2b7221c92 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/project.json @@ -0,0 +1,27 @@ +{ + "office-id": "SPK", + "name" : "test_get_create_delete", + "federal-cost" : 100.0, + "non-federal-cost" : 50.0, + "cost-year" : 1717282800000, + "cost-unit" : "$", + "federal-o-and-m-cost" : 10.0, + "non-federal-o-and-m-cost" : 5.0, + "authorizing-law" : "Authorizing Law", + "project-owner" : "Project Owner", + "hydropower-desc" : "Hydropower Description", + "sedimentation-desc" : "Sedimentation Description", + "downstream-urban-desc" : "Downstream Urban Description", + "bank-full-capacity-desc" : "Bank Full Capacity Description", + "pump-back-location" : { + "office-id" : "SPK", + "name" : "Pumpback Location Id" + }, + "near-gage-location" : { + "office-id" : "SPK", + "name" : "Near Gage Location Id" + }, + "yield-time-frame-start" : 1717282800000, + "yield-time-frame-end" : 1717308000000, + "project-remarks" : "Remarks" +} \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/project_new.json b/cwms-data-api/src/test/resources/cwms/cda/api/project_new.json new file mode 100644 index 000000000..c81fa8a2f --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/project_new.json @@ -0,0 +1,5 @@ +{ + "office-id": "SPK", + "name" : "test_update_not_exist", + "project-remarks" : "this shouldn't exist and the client is going to try and update it." +} \ No newline at end of file