From 127e9d7f7550c2a27b4ba67bb2bf0ef4266fd5a9 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Fri, 31 May 2024 17:05:53 -0700 Subject: [PATCH 01/17] DTO object for Project --- .../resources/cwms/cda/data/dto/project.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/project.json diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/project.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/project.json new file mode 100644 index 000000000..302370870 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/project.json @@ -0,0 +1,23 @@ +{ + "office-id": "SPK", + "name" : "Project Id", + "federal-cost" : 100.0, + "non-federal-cost" : 50.0, + "cost-year" : 1717199914902, + "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-id" : "Pumpback Location Id", + "pump-back-office-id" : "SPK", + "near-gage-location-id" : "Near Gage Location Id", + "near-gage-office-id" : "SPK", + "yield-time-frame-start" : 1717199914902, + "yield-time-frame-end" : 1717199914902, + "project-remarks" : "Remarks" +} \ No newline at end of file From 26e73adbd0ff20d0e6fa3bd9a36115fbadd7e5aa Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Fri, 31 May 2024 17:27:52 -0700 Subject: [PATCH 02/17] DTO object for Project --- .../main/java/cwms/cda/data/dto/Project.java | 317 ++++++++++++++++++ .../java/cwms/cda/data/dto/ProjectTest.java | 95 ++++++ 2 files changed, 412 insertions(+) create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java create mode 100644 cwms-data-api/src/test/java/cwms/cda/data/dto/ProjectTest.java diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java new file mode 100644 index 000000000..7c59c55f7 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java @@ -0,0 +1,317 @@ +package cwms.cda.data.dto; + + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +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.time.Instant; + +@JsonDeserialize(builder = Project.Builder.class) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) +@FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class) +public class Project extends CwmsDTO { + + private final String name; + private final Double federalCost; + private final Double nonFederalCost; + private final Instant costYear; + private final String costUnit; + private final Double federalOAndMCost; + private final Double nonFederalOAndMCost; + private final String authorizingLaw; + private final String projectOwner; + private final String hydropowerDesc; + private final String sedimentationDesc; + private final String downstreamUrbanDesc; + private final String bankFullCapacityDesc; + private final String pumpBackLocationId; + private final String pumpBackOfficeId; + private final String nearGageLocationId; + private final String nearGageOfficeId; + private final Instant yieldTimeFrameStart; + private final Instant yieldTimeFrameEnd; + private final String projectRemarks; + + + private Project(Project.Builder builder) { + super(builder.officeId); + this.name = builder.name; + this.federalCost = builder.federalCost; + this.nonFederalCost = builder.nonFederalCost; + this.costYear = builder.costYear; + this.costUnit = builder.costUnit; + this.federalOAndMCost = builder.federalOAndMCost; + this.nonFederalOAndMCost = builder.nonFederalOandMCost; + this.authorizingLaw = builder.authorizingLaw; + this.projectOwner = builder.projectOwner; + this.hydropowerDesc = builder.hydropowerDesc; + this.sedimentationDesc = builder.sedimentationDesc; + this.downstreamUrbanDesc = builder.downstreamUrbanDesc; + this.bankFullCapacityDesc = builder.bankFullCapacityDesc; + this.pumpBackLocationId = builder.pumpBackLocationId; + this.pumpBackOfficeId = builder.pumpBackOfficeId; + this.nearGageLocationId = builder.nearGageLocationId; + this.nearGageOfficeId = builder.nearGageOfficeId; + this.yieldTimeFrameStart = builder.yieldTimeFrameStart; + this.yieldTimeFrameEnd = builder.yieldTimeFrameEnd; + this.projectRemarks = builder.projectRemarks; + } + + @Override + public void validate() throws FieldException { + + } + + public String getAuthorizingLaw() { + return authorizingLaw; + } + + public String getBankFullCapacityDesc() { + return bankFullCapacityDesc; + } + + public String getDownstreamUrbanDesc() { + return downstreamUrbanDesc; + } + + public Double getFederalCost() { + return federalCost; + } + + public String getHydropowerDesc() { + return hydropowerDesc; + } + + public String getNearGageLocationId() { + return nearGageLocationId; + } + + public String getNearGageOfficeId() { + return nearGageOfficeId; + } + + public Double getNonFederalCost() { + return nonFederalCost; + } + + public Double getFederalOAndMCost() { + return federalOAndMCost; + } + + public Double getNonFederalOAndMCost() { + return nonFederalOAndMCost; + } + + public Instant getCostYear() { + return costYear; + } + + public String getCostUnit() { + return costUnit; + } + + public String getName() { + return name; + } + + public String getProjectOwner() { + return projectOwner; + } + + public String getProjectRemarks() { + return projectRemarks; + } + + public String getPumpBackLocationId() { + return pumpBackLocationId; + } + + public String getPumpBackOfficeId() { + return pumpBackOfficeId; + } + + public String getSedimentationDesc() { + return sedimentationDesc; + } + + public Instant getYieldTimeFrameEnd() { + return yieldTimeFrameEnd; + } + + public Instant getYieldTimeFrameStart() { + return yieldTimeFrameStart; + } + + @JsonPOJOBuilder + @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) + public static class Builder { + private String officeId; + private String name; + private Double federalCost; + private Double nonFederalCost; + private Instant costYear; + private String costUnit; + private Double federalOAndMCost; + private Double nonFederalOandMCost; + private String authorizingLaw; + private String projectOwner; + private String hydropowerDesc; + private String sedimentationDesc; + private String downstreamUrbanDesc; + private String bankFullCapacityDesc; + private String pumpBackLocationId; + private String pumpBackOfficeId; + private String nearGageLocationId; + private String nearGageOfficeId; + private Instant yieldTimeFrameStart; + private Instant yieldTimeFrameEnd; + private String projectRemarks; + + + public Project build() { + return new Project(this); + } + + /** + * Copy the values from the given project into this builder. + * @param project the project to copy values from + * @return this builder + */ + public Builder from(Project project) { + return this.withOfficeId(project.getOfficeId()) + .withName(project.getName()) + .withFederalCost(project.getFederalCost()) + .withNonFederalCost(project.getNonFederalCost()) + .withCostYear(project.getCostYear()) + .withCostUnit(project.getCostUnit()) + .withFederalOAndMCost(project.getFederalOAndMCost()) + .withNonFederalOAndMCost(project.getNonFederalOAndMCost()) + .withAuthorizingLaw(project.getAuthorizingLaw()) + .withProjectOwner(project.getProjectOwner()) + .withHydropowerDesc(project.getHydropowerDesc()) + .withSedimentationDesc(project.getSedimentationDesc()) + .withDownstreamUrbanDesc(project.getDownstreamUrbanDesc()) + .withBankFullCapacityDesc(project.getBankFullCapacityDesc()) + .withPumpBackLocationId(project.getPumpBackLocationId()) + .withPumpBackOfficeId(project.getPumpBackOfficeId()) + .withNearGageLocationId(project.getNearGageLocationId()) + .withNearGageOfficeId(project.getNearGageOfficeId()) + .withYieldTimeFrameStart(project.getYieldTimeFrameStart()) + .withYieldTimeFrameEnd(project.getYieldTimeFrameEnd()) + .withProjectRemarks(project.getProjectRemarks()); + } + + public Builder withOfficeId(String officeId) { + this.officeId = officeId; + return this; + } + + public Builder withName(String projectId) { + this.name = projectId; + return this; + } + + public Builder withFederalCost(Double federalCost) { + this.federalCost = federalCost; + return this; + } + + public Builder withNonFederalCost(Double nonFederalCost) { + this.nonFederalCost = nonFederalCost; + return this; + } + + public Builder withCostYear(Instant costYear) { + this.costYear = costYear; + return this; + } + + public Builder withCostUnit(String costUnit) { + this.costUnit = costUnit; + return this; + } + + public Builder withFederalOAndMCost(Double federalOandMCost) { + this.federalOAndMCost = federalOandMCost; + return this; + } + + public Builder withNonFederalOAndMCost(Double nonFederalOandMCost) { + this.nonFederalOandMCost = nonFederalOandMCost; + return this; + } + + public Builder withAuthorizingLaw(String authorizingLaw) { + this.authorizingLaw = authorizingLaw; + return this; + } + + public Builder withProjectOwner(String projectOwner) { + this.projectOwner = projectOwner; + return this; + } + + public Builder withHydropowerDesc(String hydropowerDesc) { + this.hydropowerDesc = hydropowerDesc; + return this; + } + + public Builder withSedimentationDesc(String sedimentationDesc) { + this.sedimentationDesc = sedimentationDesc; + return this; + } + + public Builder withDownstreamUrbanDesc(String downstreamUrbanDesc) { + this.downstreamUrbanDesc = downstreamUrbanDesc; + return this; + } + + public Builder withBankFullCapacityDesc(String bankFullCapacityDesc) { + this.bankFullCapacityDesc = bankFullCapacityDesc; + return this; + } + + public Builder withPumpBackLocationId(String pumpBackLocationId) { + this.pumpBackLocationId = pumpBackLocationId; + return this; + } + + public Builder withNearGageLocationId(String nearGageLocationId) { + this.nearGageLocationId = nearGageLocationId; + return this; + } + + public Builder withNearGageOfficeId(String nearGageOfficeId) { + this.nearGageOfficeId = nearGageOfficeId; + return this; + } + + public Builder withYieldTimeFrameStart(Instant yieldTimeFrameStart) { + this.yieldTimeFrameStart = yieldTimeFrameStart; + return this; + } + + public Builder withYieldTimeFrameEnd(Instant yieldTimeFrameEnd) { + this.yieldTimeFrameEnd = yieldTimeFrameEnd; + return this; + } + + public Builder withProjectRemarks(String projectRemarks) { + this.projectRemarks = projectRemarks; + return this; + } + + public Builder withPumpBackOfficeId(String spk) { + this.pumpBackOfficeId = spk; + return this; + } + } + +} 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 new file mode 100644 index 000000000..fcb177f42 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/ProjectTest.java @@ -0,0 +1,95 @@ +package cwms.cda.data.dto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import cwms.cda.formatters.json.JsonV2; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; + +public class ProjectTest { + + + @Test + void testProject() throws JsonProcessingException { + Project project = new Project.Builder() + .withOfficeId("SPK") + .withName("Project Id") + .withProjectOwner("Project Owner") + .withAuthorizingLaw("Authorizing Law") + .withFederalCost(100.0) + .withNonFederalCost(50.0) + .withFederalOAndMCost(10.0) + .withNonFederalOAndMCost(5.0) + .withCostYear(Instant.now()) + .withCostUnit("$") + .withYieldTimeFrameEnd(Instant.now()) + .withYieldTimeFrameStart(Instant.now()) + .withFederalOAndMCost(10.0) + .withNonFederalOAndMCost(5.0) + .withProjectRemarks("Remarks") + .withPumpBackLocationId("Pumpback Location Id") + .withPumpBackOfficeId("SPK") + .withNearGageLocationId("Near Gage Location Id") + .withNearGageOfficeId("SPK") + .withBankFullCapacityDesc("Bank Full Capacity Description") + .withDownstreamUrbanDesc("Downstream Urban Description") + .withHydropowerDesc("Hydropower Description") + .withSedimentationDesc("Sedimentation Description") + .build(); + + ObjectMapper om = JsonV2.buildObjectMapper(); + ObjectWriter ow = om.writerWithDefaultPrettyPrinter(); + + String json = ow.writeValueAsString(project); + assertNotNull(json); + + + } + + @Test + void testDeserialize() throws IOException { + InputStream stream = ProjectTest.class.getClassLoader().getResourceAsStream( + "cwms/cda/data/dto/project.json"); + assertNotNull(stream); + String input = IOUtils.toString(stream, StandardCharsets.UTF_8); + + ObjectMapper om = JsonV2.buildObjectMapper(); + Project project = om.readValue(input, Project.class); + + assertNotNull(project); + + + assertEquals("SPK", project.getOfficeId()); + assertEquals("Project Id", project.getName()); + assertEquals("Project Owner", project.getProjectOwner()); + assertEquals("Authorizing Law", project.getAuthorizingLaw()); + assertEquals(100.0, project.getFederalCost()); + assertEquals(50.0, project.getNonFederalCost()); + assertEquals(10.0, project.getFederalOAndMCost()); + assertEquals(5.0, project.getNonFederalOAndMCost()); + assertEquals(1717199914902L, project.getCostYear().toEpochMilli()); + assertEquals("$", project.getCostUnit()); + assertEquals(1717199914902L, project.getYieldTimeFrameStart().toEpochMilli()); + assertEquals(1717199914902L, project.getYieldTimeFrameEnd().toEpochMilli()); + assertEquals("Remarks", project.getProjectRemarks()); + assertEquals("Pumpback Location Id", project.getPumpBackLocationId()); + assertEquals("SPK", project.getPumpBackOfficeId()); + assertEquals("Near Gage Location Id", project.getNearGageLocationId()); + assertEquals("SPK", project.getNearGageOfficeId()); + assertEquals("Bank Full Capacity Description", project.getBankFullCapacityDesc()); + assertEquals("Downstream Urban Description", project.getDownstreamUrbanDesc()); + assertEquals("Hydropower Description", project.getHydropowerDesc()); + assertEquals("Sedimentation Description", project.getSedimentationDesc()); + + + + } +} From 9565ced8e51c2e50b98cb71aefaac5917cdfa5e3 Mon Sep 17 00:00:00 2001 From: Adam Korynta Date: Fri, 31 May 2024 12:07:42 -0700 Subject: [PATCH 03/17] add DTO for CWMS Property --- .../main/java/cwms/cda/data/dto/Property.java | 149 ++++++++++++++++++ .../java/cwms/cda/data/dto/PropertyTest.java | 130 +++++++++++++++ .../resources/cwms/cda/data/dto/property.json | 6 + 3 files changed, 285 insertions(+) create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java create mode 100644 cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java create mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/property.json diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java new file mode 100644 index 000000000..cebefbc76 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java @@ -0,0 +1,149 @@ +/* + * 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.data.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import cwms.cda.api.errors.FieldException; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV2; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Objects; + +@XmlRootElement(name = "property") +@XmlAccessorType(XmlAccessType.FIELD) +@FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class) +@JsonDeserialize(builder = Property.Builder.class) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) +public final class Property implements CwmsDTOBase { + + private final String category; + private final String name; + private final String office; + private final String value; + + private Property(Builder builder) { + this.category = builder.category; + this.name = builder.name; + this.office = builder.office; + this.value = builder.value; + } + + @Override + public void validate() throws FieldException { + + if (this.category == null || this.category.trim().isEmpty()) { + throw new FieldException("The 'category' field of a Property cannot be null or empty."); + } + if (this.name == null || this.name.trim().isEmpty()) { + throw new FieldException("The 'name' field of a Property cannot be null or empty."); + } + if (this.office == null || this.office.trim().isEmpty()) { + throw new FieldException("The 'office' field of a Property cannot be null or empty."); + } + if (this.value == null || this.value.trim().isEmpty()) { + throw new FieldException("The 'value' field of a Property cannot be null or empty."); + } + } + + public String getOffice() { + return office; + } + + public String getName() { + return name; + } + + public String getCategory() { + return category; + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Property property = (Property) o; + return Objects.equals(getCategory(), property.getCategory()) + && Objects.equals(getName(), property.getName()) + && Objects.equals(getOffice(), property.getOffice()) + && Objects.equals(getValue(), property.getValue()); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(getCategory()); + result = 31 * result + Objects.hashCode(getName()); + result = 31 * result + Objects.hashCode(getOffice()); + result = 31 * result + Objects.hashCode(getValue()); + return result; + } + + public static class Builder { + private String category; + private String name; + private String office; + private String value; + + public Builder withCategory(String category) { + this.category = category; + return this; + } + + public Builder withName(String name) { + this.name = name; + return this; + } + + public Builder withOffice(String office) { + this.office = office; + return this; + } + + public Builder withValue(String value) { + this.value = value; + return this; + } + + public Property build() { + return new Property(this); + } + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java new file mode 100644 index 000000000..b72a6d001 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java @@ -0,0 +1,130 @@ +/* + * 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.data.dto; + +import com.fasterxml.jackson.databind.ObjectMapper; +import cwms.cda.api.errors.FieldException; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.json.JsonV2; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +final class PropertyTest { + + @Test + void createProperty_allFieldsProvided_success() { + Property item = new Property.Builder() + .withCategory("TestCategory") + .withOffice("TestOffice") + .withName("TestName") + .withValue("TestValue") + .build(); + assertAll(() -> assertEquals("TestCategory", item.getCategory(), "The category does not match the provided value"), + () -> assertEquals("TestOffice", item.getOffice(), "The office does not match the provided value"), + () -> assertEquals("TestName", item.getName(), "The name does not match the provided value"), + () -> assertEquals("TestValue", item.getValue(), "The value does not match the provided value")); + } + + @Test + void createProperty_missingField_throwsFieldException() { + assertAll( + // When Office is missing + () -> assertThrows(FieldException.class, () -> { + Property item = new Property.Builder() + .withCategory("TestCategory") + // missing Office + .withName("TestName") + .withValue("TestValue") + .build(); + item.validate(); + }, "The validate method should have thrown a FieldException because the office field is missing"), + + // When Category is missing + () -> assertThrows(FieldException.class, () -> { + Property item = new Property.Builder() + // missing Category + .withOffice("TestOffice") + .withName("TestName") + .withValue("TestValue") + .build(); + item.validate(); + }, "The validate method should have thrown a FieldException because the category field is missing"), + + // When Name is missing + () -> assertThrows(FieldException.class, () -> { + Property item = new Property.Builder() + .withCategory("TestCategory") + .withOffice("TestOffice") + // missing Name + .withValue("TestValue") + .build(); + item.validate(); + }, "The validate method should have thrown a FieldException because the name field is missing"), + // When Value is missing + () -> assertThrows(FieldException.class, () -> new Property.Builder() + .withCategory("TestCategory") + .withOffice("TestOffice") + .withName("TestName") + // missing value + .build() + .validate(), "The validate method should have thrown a FieldException because the value field is missing")); + } + + @Test + void createProperty_serialize_roundtrip() throws Exception { + Property property = new Property.Builder() + .withCategory("TestCategory") + .withOffice("TestOffice") + .withName("TestName") + .withValue("TestValue") + .build(); + String json = Formats.format(new ContentType(Formats.JSONV2), property); + ObjectMapper om = JsonV2.buildObjectMapper(); + Property deserialized = om.readValue(json, Property.class); + assertEquals(property, deserialized, "Property deserialized from JSON doesn't equal original"); + } + + @Test + void createProperty_deserialize() throws Exception { + Property property = new Property.Builder() + .withCategory("TestCategory") + .withOffice("TestOffice") + .withName("TestName") + .withValue("TestValue") + .build(); + ObjectMapper om = JsonV2.buildObjectMapper(); + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/property.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + Property deserialized = om.readValue(json, Property.class); + assertEquals(property, deserialized, "Property deserialized from JSON doesn't equal original"); + } +} diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/property.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/property.json new file mode 100644 index 000000000..c6ed27464 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/property.json @@ -0,0 +1,6 @@ +{ + "category": "TestCategory", + "name": "TestName", + "office": "TestOffice", + "value": "TestValue" +} \ No newline at end of file From b54619117ab1f8384a1af9a692589eff1d8295ac Mon Sep 17 00:00:00 2001 From: Adam Korynta Date: Fri, 31 May 2024 12:27:12 -0700 Subject: [PATCH 04/17] add in missing comment field and change "office" to "office-id" --- .../main/java/cwms/cda/data/dto/Property.java | 36 +++++++++++++------ .../java/cwms/cda/data/dto/PropertyTest.java | 16 +++++---- .../resources/cwms/cda/data/dto/property.json | 5 +-- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java index cebefbc76..0a6aed83d 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java @@ -48,14 +48,16 @@ public final class Property implements CwmsDTOBase { private final String category; private final String name; - private final String office; + private final String officeId; private final String value; + private final String comment; // added comment field private Property(Builder builder) { this.category = builder.category; this.name = builder.name; - this.office = builder.office; + this.officeId = builder.officeId; this.value = builder.value; + this.comment = builder.comment; // included comment in constructor } @Override @@ -67,7 +69,7 @@ public void validate() throws FieldException { if (this.name == null || this.name.trim().isEmpty()) { throw new FieldException("The 'name' field of a Property cannot be null or empty."); } - if (this.office == null || this.office.trim().isEmpty()) { + if (this.officeId == null || this.officeId.trim().isEmpty()) { throw new FieldException("The 'office' field of a Property cannot be null or empty."); } if (this.value == null || this.value.trim().isEmpty()) { @@ -75,8 +77,8 @@ public void validate() throws FieldException { } } - public String getOffice() { - return office; + public String getOfficeId() { + return officeId; } public String getName() { @@ -91,6 +93,10 @@ public String getValue() { return value; } + public String getComment() { + return comment; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -103,24 +109,27 @@ public boolean equals(Object o) { Property property = (Property) o; return Objects.equals(getCategory(), property.getCategory()) && Objects.equals(getName(), property.getName()) - && Objects.equals(getOffice(), property.getOffice()) - && Objects.equals(getValue(), property.getValue()); + && Objects.equals(getOfficeId(), property.getOfficeId()) + && Objects.equals(getValue(), property.getValue()) + && Objects.equals(getComment(), property.getComment()); } @Override public int hashCode() { int result = Objects.hashCode(getCategory()); result = 31 * result + Objects.hashCode(getName()); - result = 31 * result + Objects.hashCode(getOffice()); + result = 31 * result + Objects.hashCode(getOfficeId()); result = 31 * result + Objects.hashCode(getValue()); + result = 31 * result + Objects.hashCode(getComment()); // incorporated comment in hashcode return result; } public static class Builder { private String category; private String name; - private String office; + private String officeId; private String value; + private String comment; // added comment to builder public Builder withCategory(String category) { this.category = category; @@ -132,8 +141,8 @@ public Builder withName(String name) { return this; } - public Builder withOffice(String office) { - this.office = office; + public Builder withOfficeId(String office) { + this.officeId = office; return this; } @@ -142,6 +151,11 @@ public Builder withValue(String value) { return this; } + public Builder withComment(String comment) { // new method to include comment in builder + this.comment = comment; + return this; + } + public Property build() { return new Property(this); } diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java index b72a6d001..23756687c 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java @@ -43,12 +43,12 @@ final class PropertyTest { void createProperty_allFieldsProvided_success() { Property item = new Property.Builder() .withCategory("TestCategory") - .withOffice("TestOffice") + .withOfficeId("TestOffice") .withName("TestName") .withValue("TestValue") .build(); assertAll(() -> assertEquals("TestCategory", item.getCategory(), "The category does not match the provided value"), - () -> assertEquals("TestOffice", item.getOffice(), "The office does not match the provided value"), + () -> assertEquals("TestOffice", item.getOfficeId(), "The office does not match the provided value"), () -> assertEquals("TestName", item.getName(), "The name does not match the provided value"), () -> assertEquals("TestValue", item.getValue(), "The value does not match the provided value")); } @@ -71,7 +71,7 @@ void createProperty_missingField_throwsFieldException() { () -> assertThrows(FieldException.class, () -> { Property item = new Property.Builder() // missing Category - .withOffice("TestOffice") + .withOfficeId("TestOffice") .withName("TestName") .withValue("TestValue") .build(); @@ -82,7 +82,7 @@ void createProperty_missingField_throwsFieldException() { () -> assertThrows(FieldException.class, () -> { Property item = new Property.Builder() .withCategory("TestCategory") - .withOffice("TestOffice") + .withOfficeId("TestOffice") // missing Name .withValue("TestValue") .build(); @@ -91,7 +91,7 @@ void createProperty_missingField_throwsFieldException() { // When Value is missing () -> assertThrows(FieldException.class, () -> new Property.Builder() .withCategory("TestCategory") - .withOffice("TestOffice") + .withOfficeId("TestOffice") .withName("TestName") // missing value .build() @@ -102,9 +102,10 @@ void createProperty_missingField_throwsFieldException() { void createProperty_serialize_roundtrip() throws Exception { Property property = new Property.Builder() .withCategory("TestCategory") - .withOffice("TestOffice") + .withOfficeId("TestOffice") .withName("TestName") .withValue("TestValue") + .withComment("TestComment") .build(); String json = Formats.format(new ContentType(Formats.JSONV2), property); ObjectMapper om = JsonV2.buildObjectMapper(); @@ -116,9 +117,10 @@ void createProperty_serialize_roundtrip() throws Exception { void createProperty_deserialize() throws Exception { Property property = new Property.Builder() .withCategory("TestCategory") - .withOffice("TestOffice") + .withOfficeId("TestOffice") .withName("TestName") .withValue("TestValue") + .withComment("TestComment") .build(); ObjectMapper om = JsonV2.buildObjectMapper(); InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/property.json"); diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/property.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/property.json index c6ed27464..dd675d5e2 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/data/dto/property.json +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/property.json @@ -1,6 +1,7 @@ { "category": "TestCategory", "name": "TestName", - "office": "TestOffice", - "value": "TestValue" + "office-id": "TestOffice", + "value": "TestValue", + "comment": "TestComment" } \ No newline at end of file From 8cb8bf472581a665d92a310dc03c9748661066af Mon Sep 17 00:00:00 2001 From: Adam Korynta Date: Fri, 31 May 2024 15:21:09 -0700 Subject: [PATCH 05/17] remove the value null or empty validation the cwms database allows the column to be null --- .../src/main/java/cwms/cda/data/dto/Property.java | 3 --- .../src/test/java/cwms/cda/data/dto/PropertyTest.java | 10 +--------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java index 0a6aed83d..242f3d4a9 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java @@ -72,9 +72,6 @@ public void validate() throws FieldException { if (this.officeId == null || this.officeId.trim().isEmpty()) { throw new FieldException("The 'office' field of a Property cannot be null or empty."); } - if (this.value == null || this.value.trim().isEmpty()) { - throw new FieldException("The 'value' field of a Property cannot be null or empty."); - } } public String getOfficeId() { diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java index 23756687c..f5c522660 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java @@ -87,15 +87,7 @@ void createProperty_missingField_throwsFieldException() { .withValue("TestValue") .build(); item.validate(); - }, "The validate method should have thrown a FieldException because the name field is missing"), - // When Value is missing - () -> assertThrows(FieldException.class, () -> new Property.Builder() - .withCategory("TestCategory") - .withOfficeId("TestOffice") - .withName("TestName") - // missing value - .build() - .validate(), "The validate method should have thrown a FieldException because the value field is missing")); + })); } @Test From 0a508eb3331fddbfc9fcaf7428681fe230a7a129 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Wed, 22 May 2024 15:54:58 -0700 Subject: [PATCH 06/17] Adding exclude-empty-extents --- .../java/cwms/cda/api/CatalogController.java | 28 +++++++++- .../main/java/cwms/cda/api/Controllers.java | 2 + .../main/java/cwms/cda/data/dto/Catalog.java | 52 +++++++++++++------ 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java b/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java index ffd983284..0a7f2b175 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java @@ -129,6 +129,14 @@ public void getAll(Context ctx) { @OpenApiParam(name = BOUNDING_OFFICE_LIKE, description = "Posix regular expression " + "matching against the location bounding office. " + "When this field is used items with no bounding office set will not be present in results."), + @OpenApiParam(name = Controllers.INCLUDE_EXTENTS, type = Boolean.class, description = "Whether the returned " + + "catalog entries should include timeseries extents. Only valid for TIMESERIES. " + + "Default is true."), + @OpenApiParam(name = Controllers.EXCLUDE_EMPTY_EXTENTS, type = Boolean.class, description = "Specifies " + + "whether Timeseries that have only empty extents [null, null, null, null] " + + "should be excluded from the results. This does not control whether the " + + "extents are returned to the user, only whether matching timeseries are " + + "excluded. Only valid for TIMESERIES. Default is true."), }, pathParams = { @OpenApiParam(name = "dataset", @@ -192,8 +200,24 @@ public void getOne(@NotNull Context ctx, @NotNull String dataSet) { Catalog cat = null; if (TIMESERIES.equalsIgnoreCase(valDataSet)) { TimeSeriesDao tsDao = new TimeSeriesDaoImpl(dsl, metrics); - cat = tsDao.getTimeSeriesCatalog(cursor, pageSize, office, like, locCategoryLike, - locGroupLike, tsCategoryLike, tsGroupLike, boundingOfficeLike); + + boolean includeExtents = ctx.queryParamAsClass(Controllers.INCLUDE_EXTENTS, Boolean.class).getOrDefault(true); + boolean excludeExtents = ctx.queryParamAsClass(Controllers.EXCLUDE_EMPTY_EXTENTS, Boolean.class).getOrDefault(true); + + TimeSeriesDaoImpl.CatalogRequestParameters parameters = new TimeSeriesDaoImpl.CatalogRequestParameters.Builder() + .withOffice(office) + .withIdLike(like) + .withLocCatLike(locCategoryLike) + .withLocGroupLike(locGroupLike) + .withTsCatLike(tsCategoryLike) + .withTsGroupLike(tsGroupLike) + .withBoundingOfficeLike(boundingOfficeLike) + .withIncludeExtents(includeExtents) + .withExcludeEmptyExtents(excludeExtents) + .build(); + + cat = tsDao.getTimeSeriesCatalog(cursor, pageSize, parameters); + } else if (LOCATIONS.equalsIgnoreCase(valDataSet)) { LocationsDao dao = new LocationsDaoImpl(dsl); cat = dao.getLocationCatalog(cursor, pageSize, unitSystem, office, like, diff --git a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java index 01e46201f..b4c90b0a5 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java @@ -165,6 +165,8 @@ public final class Controllers { public static final String TRIM = "trim"; public static final String DESIGNATOR = "designator"; public static final String DESIGNATOR_MASK = "designator-mask"; + public static final String INCLUDE_EXTENTS = "include-extents"; + public static final String EXCLUDE_EMPTY_EXTENTS = "exclude-empty-extents"; static { diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Catalog.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Catalog.java index 146d30362..b3ff037b4 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/Catalog.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Catalog.java @@ -39,24 +39,22 @@ private Catalog() { } public Catalog(String page, int total, int pageSize, List entries) { - this(page, total, pageSize, entries, null, null, null, null, null, null, null); + this(page, total, pageSize, entries, + null, + null, null, null, + null, + null, null, + false, true); } - @SuppressWarnings("java:S107") // This just has this many parameters. - public Catalog(String page, int total, int pageSize, List entries, - String office, - String idLike, String locCategoryLike, String locGroupLike, - String tsCategoryLike, - String tsGroupLike) { - this(page, total, pageSize, entries, office, idLike, locCategoryLike, locGroupLike, tsCategoryLike, tsGroupLike, null); - } @SuppressWarnings("java:S107") // This just has this many parameters. public Catalog(String page, int total, int pageSize, List entries, String office, String idLike, String locCategoryLike, String locGroupLike, String tsCategoryLike, - String tsGroupLike, String boundingOfficeLike) { + String tsGroupLike, String boundingOfficeLike, + boolean includeExtents, boolean excludeEmpty) { super(page, pageSize, total); Objects.requireNonNull(entries, "List of catalog entries must be a valid list, even if empty"); @@ -70,7 +68,9 @@ public Catalog(String page, int total, int pageSize, List Date: Wed, 22 May 2024 15:56:34 -0700 Subject: [PATCH 07/17] Moving request parameter class into interface signature --- .../src/main/java/cwms/cda/data/dao/TimeSeriesDao.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDao.java index 7562ef6f5..bd8d8317d 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDao.java @@ -11,9 +11,7 @@ public interface TimeSeriesDao { - Catalog getTimeSeriesCatalog(String cursor, int pageSize, String office, String idLike, - String locCategoryLike, String locGroupLike, - String tsCategoryLike, String tsGroupLike, String boundingOfficeLike); + Catalog getTimeSeriesCatalog(String page, int pageSize, TimeSeriesDaoImpl.CatalogRequestParameters inputParams); void create(TimeSeries input); From 5ad8d87c6dc1ab935647bd77e4fae88c97d56a00 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Wed, 22 May 2024 17:16:03 -0700 Subject: [PATCH 08/17] Making Catalog and CatalogPage aware of CatalogRequestParameters. Moving request parameter class into interface signatures. --- .../java/cwms/cda/api/CatalogController.java | 57 +++- .../main/java/cwms/cda/api/Controllers.java | 11 +- .../data/dao/CatalogRequestParameters.java | 186 +++++++++++++ .../java/cwms/cda/data/dao/LocationsDao.java | 5 +- .../cwms/cda/data/dao/LocationsDaoImpl.java | 46 +-- .../java/cwms/cda/data/dao/TimeSeriesDao.java | 2 +- .../cwms/cda/data/dao/TimeSeriesDaoImpl.java | 263 ++++-------------- .../main/java/cwms/cda/data/dto/Catalog.java | 54 ++-- .../cwms/cda/api/CatalogControllerTestIT.java | 21 +- .../cda/api/TimeseriesControllerTestIT.java | 129 ++++++++- .../java/cwms/cda/data/dto/CatalogTest.java | 13 +- 11 files changed, 487 insertions(+), 300 deletions(-) create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dao/CatalogRequestParameters.java diff --git a/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java b/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java index 0a7f2b175..b10d0fc1c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java @@ -4,7 +4,9 @@ import static cwms.cda.api.Controllers.ACCEPT; import static cwms.cda.api.Controllers.BOUNDING_OFFICE_LIKE; import static cwms.cda.api.Controllers.CURSOR; +import static cwms.cda.api.Controllers.EXCLUDE_EMPTY; import static cwms.cda.api.Controllers.GET_ONE; +import static cwms.cda.api.Controllers.INCLUDE_EXTENTS; import static cwms.cda.api.Controllers.LIKE; import static cwms.cda.api.Controllers.LOCATIONS; import static cwms.cda.api.Controllers.LOCATION_CATEGORY_LIKE; @@ -26,6 +28,7 @@ import com.codahale.metrics.Timer; import cwms.cda.api.enums.UnitSystem; import cwms.cda.api.errors.CdaError; +import cwms.cda.data.dao.CatalogRequestParameters; import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dao.LocationsDao; import cwms.cda.data.dao.LocationsDaoImpl; @@ -42,6 +45,10 @@ import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.logging.Logger; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -57,7 +64,7 @@ public class CatalogController implements CrudHandler { private final Histogram requestResultSize; - private final int defaultPageSize = 500; + private static final int DEFAULT_PAGE_SIZE = 500; public CatalogController(MetricRegistry metrics) { this.metrics = metrics; @@ -129,10 +136,11 @@ public void getAll(Context ctx) { @OpenApiParam(name = BOUNDING_OFFICE_LIKE, description = "Posix regular expression " + "matching against the location bounding office. " + "When this field is used items with no bounding office set will not be present in results."), - @OpenApiParam(name = Controllers.INCLUDE_EXTENTS, type = Boolean.class, description = "Whether the returned " - + "catalog entries should include timeseries extents. Only valid for TIMESERIES. " - + "Default is true."), - @OpenApiParam(name = Controllers.EXCLUDE_EMPTY_EXTENTS, type = Boolean.class, description = "Specifies " + @OpenApiParam(name = Controllers.INCLUDE_EXTENTS, type = Boolean.class, + description = "Whether the returned catalog entries should include timeseries " + + "extents. Only valid for TIMESERIES. Default is true."), + @OpenApiParam(name = Controllers.EXCLUDE_EMPTY, type = Boolean.class, + description = "Specifies " + "whether Timeseries that have only empty extents [null, null, null, null] " + "should be excluded from the results. This does not control whether the " + "extents are returned to the user, only whether matching timeseries are " @@ -166,7 +174,7 @@ public void getOne(@NotNull Context ctx, @NotNull String dataSet) { String.class, "", metrics, name(CatalogController.class.getName(), GET_ONE)); int pageSize = queryParamAsClass(ctx, new String[]{PAGE_SIZE }, - Integer.class, defaultPageSize, metrics, + Integer.class, DEFAULT_PAGE_SIZE, metrics, name(CatalogController.class.getName(), GET_ONE)); String unitSystem = queryParamAsClass(ctx, @@ -201,10 +209,12 @@ public void getOne(@NotNull Context ctx, @NotNull String dataSet) { if (TIMESERIES.equalsIgnoreCase(valDataSet)) { TimeSeriesDao tsDao = new TimeSeriesDaoImpl(dsl, metrics); - boolean includeExtents = ctx.queryParamAsClass(Controllers.INCLUDE_EXTENTS, Boolean.class).getOrDefault(true); - boolean excludeExtents = ctx.queryParamAsClass(Controllers.EXCLUDE_EMPTY_EXTENTS, Boolean.class).getOrDefault(true); + boolean includeExtents = ctx.queryParamAsClass(INCLUDE_EXTENTS, Boolean.class) + .getOrDefault(true); + boolean excludeExtents = ctx.queryParamAsClass(EXCLUDE_EMPTY, Boolean.class) + .getOrDefault(true); - TimeSeriesDaoImpl.CatalogRequestParameters parameters = new TimeSeriesDaoImpl.CatalogRequestParameters.Builder() + CatalogRequestParameters parameters = new CatalogRequestParameters.Builder() .withOffice(office) .withIdLike(like) .withLocCatLike(locCategoryLike) @@ -213,15 +223,38 @@ public void getOne(@NotNull Context ctx, @NotNull String dataSet) { .withTsGroupLike(tsGroupLike) .withBoundingOfficeLike(boundingOfficeLike) .withIncludeExtents(includeExtents) - .withExcludeEmptyExtents(excludeExtents) + .withExcludeEmpty(excludeExtents) .build(); cat = tsDao.getTimeSeriesCatalog(cursor, pageSize, parameters); } else if (LOCATIONS.equalsIgnoreCase(valDataSet)) { + + Set notSupported = new LinkedHashSet<>(); + notSupported.add(TIMESERIES_CATEGORY_LIKE); + notSupported.add(TIMESERIES_GROUP_LIKE); + notSupported.add(EXCLUDE_EMPTY); + notSupported.add(INCLUDE_EXTENTS); + + Map> queryParamMap = ctx.queryParamMap(); + notSupported.retainAll(queryParamMap.keySet()); + + if (!notSupported.isEmpty()) { + throw new IllegalArgumentException("The following parameters are not yet " + + "supported for location: " + notSupported); + } + + CatalogRequestParameters parameters = new CatalogRequestParameters.Builder() + .withUnitSystem(unitSystem) + .withOffice(office) + .withIdLike(like) + .withLocCatLike(locCategoryLike) + .withLocGroupLike(locGroupLike) + .withBoundingOfficeLike(boundingOfficeLike) + .build(); + LocationsDao dao = new LocationsDaoImpl(dsl); - cat = dao.getLocationCatalog(cursor, pageSize, unitSystem, office, like, - locCategoryLike, locGroupLike, boundingOfficeLike); + cat = dao.getLocationCatalog(cursor, pageSize, parameters); } if (cat != null) { String data = Formats.format(contentType, cat); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java index b4c90b0a5..7eb7385eb 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java @@ -35,14 +35,11 @@ import io.javalin.core.validation.JavalinValidation; import io.javalin.core.validation.Validator; import io.javalin.http.Context; - import java.time.Instant; import java.time.ZonedDateTime; import org.jetbrains.annotations.Nullable; - import static com.codahale.metrics.MetricRegistry.name; - public final class Controllers { @@ -166,7 +163,7 @@ public final class Controllers { public static final String DESIGNATOR = "designator"; public static final String DESIGNATOR_MASK = "designator-mask"; public static final String INCLUDE_EXTENTS = "include-extents"; - public static final String EXCLUDE_EMPTY_EXTENTS = "exclude-empty-extents"; + public static final String EXCLUDE_EMPTY = "exclude-empty"; static { @@ -315,9 +312,11 @@ public static ZonedDateTime queryParamAsZdt(Context ctx, String param) { @Nullable public static Instant queryParamAsInstant(Context ctx, String param) { - ZonedDateTime zonedDateTime = queryParamAsZdt(ctx, param, ctx.queryParamAsClass(TIMEZONE, String.class).getOrDefault("UTC")); + ZonedDateTime zonedDateTime = queryParamAsZdt(ctx, param, + ctx.queryParamAsClass(TIMEZONE, String.class) + .getOrDefault("UTC")); Instant retval = null; - if(zonedDateTime != null) { + if (zonedDateTime != null) { retval = zonedDateTime.toInstant(); } return retval; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/CatalogRequestParameters.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/CatalogRequestParameters.java new file mode 100644 index 000000000..e81a7bcec --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/CatalogRequestParameters.java @@ -0,0 +1,186 @@ +package cwms.cda.data.dao; + +import static usace.cwms.db.jooq.codegen.tables.AV_TS_EXTENTS_UTC.AV_TS_EXTENTS_UTC; + +import org.jooq.Table; +import usace.cwms.db.jooq.codegen.tables.AV_LOC; +import usace.cwms.db.jooq.codegen.tables.AV_LOC_GRP_ASSGN; +import usace.cwms.db.jooq.codegen.tables.AV_TS_GRP_ASSGN; + +public class CatalogRequestParameters { + private final String office; + private final String idLike; + private final String unitSystem; + private final String locCatLike; + private final String locGroupLike; + private final String tsCatLike; + private final String tsGroupLike; + private final String boundingOfficeLike; + private final boolean includeExtents; + private final boolean excludeEmpty; + + private CatalogRequestParameters(Builder builder) { + this.office = builder.office; + this.idLike = builder.idLike; + this.unitSystem = builder.unitSystem; + this.locCatLike = builder.locCatLike; + this.locGroupLike = builder.locGroupLike; + this.tsCatLike = builder.tsCatLike; + this.tsGroupLike = builder.tsGroupLike; + this.boundingOfficeLike = builder.boundingOfficeLike; + this.includeExtents = builder.includeExtents; + this.excludeEmpty = builder.excludeEmpty; + } + + public String getBoundingOfficeLike() { + return boundingOfficeLike; + } + + public String getIdLike() { + return idLike; + } + + public boolean isIncludeExtents() { + return includeExtents; + } + + public String getLocCatLike() { + return locCatLike; + } + + public String getLocGroupLike() { + return locGroupLike; + } + + public String getOffice() { + return office; + } + + public String getTsCatLike() { + return tsCatLike; + } + + public String getTsGroupLike() { + return tsGroupLike; + } + + public String getUnitSystem() { + return unitSystem; + } + + public boolean isExcludeEmpty() { + return excludeEmpty; + } + + public static class Builder { + String office; + String idLike; + String unitSystem; + String locCatLike; + String locGroupLike; + String tsCatLike; + String tsGroupLike; + String boundingOfficeLike; + boolean includeExtents = false; + private boolean excludeEmpty = true; + + public Builder() { + + } + + public Builder withOffice(String office) { + this.office = office; + return this; + } + + public Builder withIdLike(String idLike) { + this.idLike = idLike; + return this; + } + + public Builder withUnitSystem(String unitSystem) { + this.unitSystem = unitSystem; + return this; + } + + public Builder withLocCatLike(String locCatLike) { + this.locCatLike = locCatLike; + return this; + } + + public Builder withLocGroupLike(String locGroupLike) { + this.locGroupLike = locGroupLike; + return this; + } + + public Builder withTsCatLike(String tsCatLike) { + this.tsCatLike = tsCatLike; + return this; + } + + public Builder withTsGroupLike(String tsGroupLike) { + this.tsGroupLike = tsGroupLike; + return this; + } + + public Builder withBoundingOfficeLike(String boundingOfficeLike) { + this.boundingOfficeLike = boundingOfficeLike; + return this; + } + + public Builder withIncludeExtents(boolean includeExtents) { + this.includeExtents = includeExtents; + return this; + } + + public Builder withExcludeEmpty(boolean excludeExtents) { + this.excludeEmpty = excludeExtents; + return this; + } + + public static Builder from(CatalogRequestParameters params) { + // This NEEDS to include every field in the CatalogRequestParameters + return new Builder() + .withOffice(params.office) + .withIdLike(params.idLike) + .withUnitSystem(params.unitSystem) + .withLocCatLike(params.locCatLike) + .withLocGroupLike(params.locGroupLike) + .withTsCatLike(params.tsCatLike) + .withTsGroupLike(params.tsGroupLike) + .withBoundingOfficeLike(params.boundingOfficeLike) + .withIncludeExtents(params.includeExtents) + .withExcludeEmpty(params.excludeEmpty) + ; + } + + + public CatalogRequestParameters build() { + return new CatalogRequestParameters(this); + } + + } + + // This is supposed to answer whether the current set of request parameters + // needs the specified table to be joined into the query. + public boolean needs(Table table) { + if (table == AV_LOC_GRP_ASSGN.AV_LOC_GRP_ASSGN) { + return locCatLike != null || locGroupLike != null; + } + + if (table == AV_TS_GRP_ASSGN.AV_TS_GRP_ASSGN) { + return tsCatLike != null || tsGroupLike != null; + } + + if (table == AV_LOC.AV_LOC) { + return boundingOfficeLike != null; + } + + if (table == AV_TS_EXTENTS_UTC) { + return includeExtents || excludeEmpty; + } + + return false; + } + +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDao.java index 6ee022b1f..f4dab5993 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDao.java @@ -47,7 +47,6 @@ public interface LocationsDao { FeatureCollection buildFeatureCollection(String names, String units, String officeId); - Catalog getLocationCatalog(String cursor, int pageSize, String unitSystem, String office, - String idLike, String categoryLike, String groupLike, - String boundingOfficeLike); + Catalog getLocationCatalog(String cursor, int pageSize, CatalogRequestParameters params); + } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDaoImpl.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDaoImpl.java index 4fb685c2e..49f343207 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDaoImpl.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDaoImpl.java @@ -202,9 +202,11 @@ public void deleteLocation(String locationName, String officeId, boolean cascade connection(dsl, c -> { Configuration configuration = getDslContext(c, officeId).configuration(); if (cascadeDelete) { - CWMS_LOC_PACKAGE.call_DELETE_LOCATION(configuration, locationName, DELETE_LOC_CASCADE.getRule(), officeId); + CWMS_LOC_PACKAGE.call_DELETE_LOCATION(configuration, locationName, + DELETE_LOC_CASCADE.getRule(), officeId); } else { - CWMS_LOC_PACKAGE.call_DELETE_LOCATION(configuration, locationName, DELETE_LOC.getRule(), officeId); + CWMS_LOC_PACKAGE.call_DELETE_LOCATION(configuration, locationName, + DELETE_LOC.getRule(), officeId); } }); } @@ -332,9 +334,8 @@ public static Feature buildFeatureFromAvLocRecord(Record avLocRecord) { } - @Override - public Catalog getLocationCatalog(String page, int pageSize, String unitSystem, String office, - String idLike, String categoryLike, String groupLike, String boundingOfficeLike) { + + public Catalog getLocationCatalog(String page, int pageSize, CatalogRequestParameters param) { // Parse provided page and pull out the parameters @@ -345,28 +346,38 @@ public Catalog getLocationCatalog(String page, int pageSize, String unitSystem, // The cursor urlencodes the initial query parameters, We should decode them and use the cursor values. // If the user provides a page parameter and query parameters they should match. // If they don't match its weird and we will log it. - office = warnIfMismatch(OFFICE, catPage.getSearchOffice(), office); - idLike = warnIfMismatch(LIKE, catPage.getIdLike(), idLike); - categoryLike = warnIfMismatch(LOCATION_CATEGORY_LIKE, catPage.getLocCategoryLike(), categoryLike); - groupLike = warnIfMismatch(LOCATION_GROUP_LIKE, catPage.getLocGroupLike(), groupLike); - boundingOfficeLike = warnIfMismatch(BOUNDING_OFFICE_LIKE, catPage.getBoundingOfficeLike(), boundingOfficeLike); + CatalogRequestParameters.Builder.from(param) + .withOffice(warnIfMismatch(OFFICE, + catPage.getSearchOffice(), param.getOffice())) + .withIdLike(warnIfMismatch(LIKE, + catPage.getIdLike(), param.getIdLike())) + .withLocCatLike(warnIfMismatch(LOCATION_CATEGORY_LIKE, + catPage.getLocCategoryLike(), param.getLocCatLike())) + .withLocGroupLike(warnIfMismatch(LOCATION_GROUP_LIKE, + catPage.getLocGroupLike(), param.getLocGroupLike())) + .withBoundingOfficeLike(warnIfMismatch(BOUNDING_OFFICE_LIKE, + catPage.getBoundingOfficeLike(), param.getBoundingOfficeLike())) + .build(); + } - return getLocationCatalog(catPage, pageSize, unitSystem, office, idLike, categoryLike, groupLike, boundingOfficeLike); + return getLocationCatalog(catPage, pageSize, param); } - private Catalog getLocationCatalog(Catalog.CatalogPage catPage, int pageSize, String unitSystem, String office, - String idLike, String categoryLike, String groupLike, String boundingOfficeLike) { + private Catalog getLocationCatalog(Catalog.CatalogPage catPage, int pageSize, CatalogRequestParameters params) { final AV_LOC2 avLoc2 = AV_LOC2.AV_LOC2; // ref the view just shorten the jooq //Now querying against AV_LOC2 as it gives us back the same information as querying against //location group views. This makes the code clearer and improves performance. //If there is a performance improvement by switching back to location groups and querying against //location codes (previous implementation used location_id) for joins, feel free to implement. - Objects.requireNonNull(idLike, "A value must be provided for the idLike field. Specifiy .* if you don't care."); + Objects.requireNonNull(params.getIdLike(), + "A value must be provided for the idLike field. Specify .* if you don't care."); // "condition" needs to be used by the count query and the results query. - Condition condition = buildWhereCondition(unitSystem, office, idLike, categoryLike, groupLike, boundingOfficeLike); + Condition condition = buildWhereCondition(params.getUnitSystem(), params.getOffice(), + params.getIdLike(), params.getLocCatLike(), params.getLocGroupLike(), + params.getBoundingOfficeLike()); int total; String cursorLocation; // The location-id of the cursor in the results @@ -435,9 +446,8 @@ private Catalog getLocationCatalog(Catalog.CatalogPage catPage, int pageSize, St return buildCatalogEntry(row, aliases); }) .collect(toList()); - return new Catalog(cursorLocation, total, pageSize, entries, office, - idLike, categoryLike, groupLike, - null, null, boundingOfficeLike); + + return new Catalog(cursorLocation, total, pageSize, entries, params); } private static Condition addCursorConditions(Condition condition, String cursorOffice, String cursorLocation) { diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDao.java index bd8d8317d..8750b24f3 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDao.java @@ -11,7 +11,7 @@ public interface TimeSeriesDao { - Catalog getTimeSeriesCatalog(String page, int pageSize, TimeSeriesDaoImpl.CatalogRequestParameters inputParams); + Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestParameters inputParams); void create(TimeSeries input); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java index 2dac2a436..041b9d4d9 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java @@ -246,8 +246,6 @@ public TimeSeries getTimeseries(String page, int pageSize, String names, String Field qualityNormCol = CWMS_TS_PACKAGE.call_NORMALIZE_QUALITY( DSL.nvl(qualityCol, DSL.inline(5))).as("QUALITY_NORM"); - - Long beginTimeMilli = beginTime.toInstant().toEpochMilli(); Long endTimeMilli = endTime.toInstant().toEpochMilli(); String trim = OracleTypeMap.formatBool(shouldTrim); @@ -480,35 +478,14 @@ public static VerticalDatumInfo parseVerticalDatumInfo(String body) { } @Override - public Catalog getTimeSeriesCatalog(String page, int pageSize, String office, - String idLike, String locCategoryLike, String locGroupLike, - String tsCategoryLike, String tsGroupLike, String boundingOfficeLike) { - - if (".*".equals(idLike)) { - idLike = null; - } - - CatalogRequestParameters parameters = new CatalogRequestParameters.Builder() - .withOffice(office) - .withIdLike(idLike) - .withLocCatLike(locCategoryLike) - .withLocGroupLike(locGroupLike) - .withTsCatLike(tsCategoryLike) - .withTsGroupLike(tsGroupLike) - .withBoundingOfficeLike(boundingOfficeLike) - .withIncludeExtents(true) - .build(); - - return getTimeSeriesCatalog(page, pageSize, parameters); - } - public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestParameters inputParams) { int total; String cursorTsId = "*"; String cursorOffice = null; Catalog.CatalogPage catPage = null; if (page == null || page.isEmpty()) { - SelectConditionStep> totalQuery = dsl.select(countDistinct(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE)) + SelectConditionStep> totalQuery = dsl + .select(countDistinct(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE)) .from(buildFromTable(inputParams)) .where(buildWhereConditions(inputParams)); logger.fine(() -> totalQuery.getSQL(ParamType.INLINED)); @@ -530,20 +507,22 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar .withTsCatLike(catPage.getTsCategoryLike()) .withTsGroupLike(catPage.getTsGroupLike()) .withBoundingOfficeLike(catPage.getBoundingOfficeLike()) + .withIncludeExtents(catPage.isIncludeExtents()) + .withExcludeEmpty(catPage.isExcludeEmpty()) .build(); } final CatalogRequestParameters params = inputParams; List pageEntryFields = new ArrayList<>(getCwmsTsIdFields()); - if (params.needs(AV_TS_EXTENTS_UTC)) { + if (params.isIncludeExtents()) { pageEntryFields.addAll(getExtentsFields()); } TableLike fromTable = AV_CWMS_TS_ID.AV_CWMS_TS_ID; if (params.needs(AV_TS_EXTENTS_UTC)) { - fromTable = AV_CWMS_TS_ID.AV_CWMS_TS_ID.leftJoin(AV_TS_EXTENTS_UTC.AV_TS_EXTENTS_UTC) + fromTable = AV_CWMS_TS_ID.AV_CWMS_TS_ID.leftJoin(AV_TS_EXTENTS_UTC) .on(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE - .eq(AV_TS_EXTENTS_UTC.AV_TS_EXTENTS_UTC.TS_CODE + .eq(AV_TS_EXTENTS_UTC.TS_CODE .coerce(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE))); } @@ -592,14 +571,13 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar if (this.getDbVersion() > Dao.CWMS_21_1_1) { builder.timeZone(row.get("TIME_ZONE_ID", String.class)); } - if (params.needs(AV_TS_EXTENTS_UTC.AV_TS_EXTENTS_UTC)) { + if (params.isIncludeExtents()) { builder.withExtents(new ArrayList<>()); } tsIdExtentMap.put(officeTsId, builder); } - if (params.needs(AV_TS_EXTENTS_UTC.AV_TS_EXTENTS_UTC) && row.get(AV_TS_EXTENTS_UTC.EARLIEST_TIME) != null) { - //tsIdExtentMap.get(tsId) + if (params.isIncludeExtents()) { TimeSeriesExtents extents = new TimeSeriesExtents(row.get(AV_TS_EXTENTS_UTC.VERSION_TIME), row.get(AV_TS_EXTENTS_UTC.EARLIEST_TIME), @@ -615,9 +593,7 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar .collect(Collectors.toList()); return new Catalog(catPage != null ? catPage.toString() : null, - total, pageSize, entries, params.getOffice(), - params.getIdLike(),params.getLocCatLike(), params.getLocGroupLike(), - params.getTsCatLike(), params.getTsGroupLike()); + total, pageSize, entries, params); } private static @NotNull List buildPagingConditions(String cursorOffice, String cursorTsId) { @@ -626,7 +602,7 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar // Can't do the rownum thing here b/c we want global ordering, not ordering within the page. pagingConditions.add(DSL.noCondition()); - if (cursorOffice != null){ + if (cursorOffice != null) { Condition moreInSameOffice = AV_CWMS_TS_ID.AV_CWMS_TS_ID.DB_OFFICE_ID .eq(cursorOffice.toUpperCase()) .and(DSL.upper(AV_CWMS_TS_ID.AV_CWMS_TS_ID.CWMS_TS_ID) @@ -666,6 +642,7 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar conditions.addAll(buildLocGrpAssgnConditions(params)); conditions.addAll(buildTsGrpAssgnConditions(params)); conditions.addAll(buildLocConditions(params)); + conditions.addAll(buildExtentsConditions(params)); return conditions; } @@ -711,6 +688,24 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar } fromTable = on; } + + if (params.isExcludeEmpty()) { + if (on == null) { + on = AV_CWMS_TS_ID.AV_CWMS_TS_ID + .leftJoin(AV_TS_EXTENTS_UTC) + .on(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE + .eq(AV_TS_EXTENTS_UTC.TS_CODE + .coerce(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE))); + } else { + on = on + .leftJoin(AV_TS_EXTENTS_UTC) + .on(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE + .eq(AV_TS_EXTENTS_UTC.TS_CODE + .coerce(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE))); + } + fromTable = on; + } + return fromTable; } @@ -718,7 +713,8 @@ private Collection buildLocConditions(CatalogRequestParamet List retval = new ArrayList<>(); if (params.needs(AV_LOC.AV_LOC)) { - retval.add(caseInsensitiveLikeRegexNullTrue(AV_LOC.AV_LOC.BOUNDING_OFFICE_ID, params.boundingOfficeLike)); + retval.add(caseInsensitiveLikeRegexNullTrue(AV_LOC.AV_LOC.BOUNDING_OFFICE_ID, + params.getBoundingOfficeLike())); // we could add location_type here too... // or maybe conditions based on lat/lon // or any bool fields. @@ -727,12 +723,27 @@ private Collection buildLocConditions(CatalogRequestParamet return retval; } + private Collection buildExtentsConditions(CatalogRequestParameters params) { + List retval = new ArrayList<>(); + + if (params.isExcludeEmpty()) { + retval.add(DSL.or(AV_TS_EXTENTS_UTC.VERSION_TIME.isNotNull(), + AV_TS_EXTENTS_UTC.EARLIEST_TIME.isNotNull(), + AV_TS_EXTENTS_UTC.LATEST_TIME.isNotNull(), + AV_TS_EXTENTS_UTC.LAST_UPDATE.isNotNull())); + } + + return retval; + } + private Collection buildLocGrpAssgnConditions(CatalogRequestParameters params) { List retval = new ArrayList<>(); if (params.needs(AV_LOC_GRP_ASSGN.AV_LOC_GRP_ASSGN)) { - retval.add(caseInsensitiveLikeRegexNullTrue(AV_LOC_GRP_ASSGN.AV_LOC_GRP_ASSGN.GROUP_ID, params.locGroupLike)); - retval.add(caseInsensitiveLikeRegexNullTrue(AV_LOC_GRP_ASSGN.AV_LOC_GRP_ASSGN.CATEGORY_ID, params.locCatLike)); + retval.add(caseInsensitiveLikeRegexNullTrue(AV_LOC_GRP_ASSGN.AV_LOC_GRP_ASSGN.GROUP_ID, + params.getLocGroupLike())); + retval.add(caseInsensitiveLikeRegexNullTrue(AV_LOC_GRP_ASSGN.AV_LOC_GRP_ASSGN.CATEGORY_ID, + params.getLocCatLike())); } return retval; } @@ -741,8 +752,10 @@ private Collection buildTsGrpAssgnConditions(CatalogRequest List retval = new ArrayList<>(); if (params.needs(AV_TS_GRP_ASSGN.AV_TS_GRP_ASSGN)) { - retval.add(caseInsensitiveLikeRegexNullTrue(AV_TS_GRP_ASSGN.AV_TS_GRP_ASSGN.GROUP_ID, params.tsGroupLike)); - retval.add(caseInsensitiveLikeRegexNullTrue(AV_TS_GRP_ASSGN.AV_TS_GRP_ASSGN.CATEGORY_ID, params.tsCatLike)); + retval.add(caseInsensitiveLikeRegexNullTrue(AV_TS_GRP_ASSGN.AV_TS_GRP_ASSGN.GROUP_ID, + params.getTsGroupLike())); + retval.add(caseInsensitiveLikeRegexNullTrue(AV_TS_GRP_ASSGN.AV_TS_GRP_ASSGN.CATEGORY_ID, + params.getTsCatLike())); } return retval; } @@ -750,11 +763,11 @@ private Collection buildTsGrpAssgnConditions(CatalogRequest private Collection buildCwmsTsIdConditions(CatalogRequestParameters params) { List retval = new ArrayList<>(); - if (params.office != null) { - retval.add(AV_CWMS_TS_ID.AV_CWMS_TS_ID.DB_OFFICE_ID.eq(params.office.toUpperCase())); + if (params.getOffice() != null) { + retval.add(AV_CWMS_TS_ID.AV_CWMS_TS_ID.DB_OFFICE_ID.eq(params.getOffice().toUpperCase())); } - retval.add(caseInsensitiveLikeRegexNullTrue(AV_CWMS_TS_ID.AV_CWMS_TS_ID.CWMS_TS_ID, params.idLike)); + retval.add(caseInsensitiveLikeRegexNullTrue(AV_CWMS_TS_ID.AV_CWMS_TS_ID.CWMS_TS_ID, params.getIdLike())); return retval; } @@ -986,7 +999,7 @@ public List findRecentsInRange(String office, String categoryId, St } Field defUnitsField = CWMS_UTIL_PACKAGE.call_GET_DEFAULT_UNITS( - CWMS_TS_PACKAGE.call_GET_BASE_PARAMETER_ID(tsvView.AV_TSV_DQU.TS_CODE), + CWMS_TS_PACKAGE.call_GET_BASE_PARAMETER_ID(AV_TSV_DQU.AV_TSV_DQU.TS_CODE), DSL.val(unitSystem, String.class)) .as(DEFAULT_UNITS); Field maxDateTimeField = max(tsvView.DATE_TIME).over(partitionBy(tsvView.TS_CODE)) @@ -1324,166 +1337,4 @@ public DeleteOptions build() { } } - public static class CatalogRequestParameters { - private final String office; - private final String idLike; - private final String unitSystem; - private final String locCatLike; - private final String locGroupLike; - private final String tsCatLike; - private final String tsGroupLike; - private final String boundingOfficeLike; - private final boolean includeExtents; - - private CatalogRequestParameters(Builder builder) { - this.office = builder.office; - this.idLike = builder.idLike; - this.unitSystem = builder.unitSystem; - this.locCatLike = builder.locCatLike; - this.locGroupLike = builder.locGroupLike; - this.tsCatLike = builder.tsCatLike; - this.tsGroupLike = builder.tsGroupLike; - this.boundingOfficeLike = builder.boundingOfficeLike; - this.includeExtents = builder.includeExtents; - } - - public String getBoundingOfficeLike() { - return boundingOfficeLike; - } - - public String getIdLike() { - return idLike; - } - - public boolean isIncludeExtents() { - return includeExtents; - } - - public String getLocCatLike() { - return locCatLike; - } - - public String getLocGroupLike() { - return locGroupLike; - } - - public String getOffice() { - return office; - } - - public String getTsCatLike() { - return tsCatLike; - } - - public String getTsGroupLike() { - return tsGroupLike; - } - - public String getUnitSystem() { - return unitSystem; - } - - public static class Builder { - String office; - String idLike; - String unitSystem; - String locCatLike; - String locGroupLike; - String tsCatLike; - String tsGroupLike; - String boundingOfficeLike; - boolean includeExtents; - - public Builder() { - - } - - public Builder withOffice(String office) { - this.office = office; - return this; - } - - public Builder withIdLike(String idLike) { - this.idLike = idLike; - return this; - } - - public Builder withUnitSystem(String unitSystem) { - this.unitSystem = unitSystem; - return this; - } - - public Builder withLocCatLike(String locCatLike) { - this.locCatLike = locCatLike; - return this; - } - - public Builder withLocGroupLike(String locGroupLike) { - this.locGroupLike = locGroupLike; - return this; - } - - public Builder withTsCatLike(String tsCatLike) { - this.tsCatLike = tsCatLike; - return this; - } - - public Builder withTsGroupLike(String tsGroupLike) { - this.tsGroupLike = tsGroupLike; - return this; - } - - public Builder withBoundingOfficeLike(String boundingOfficeLike) { - this.boundingOfficeLike = boundingOfficeLike; - return this; - } - - public Builder withIncludeExtents(boolean includeExtents) { - this.includeExtents = includeExtents; - return this; - } - - public static Builder from(CatalogRequestParameters params) { - // This NEEDS to include every field in the CatalogRequestParameters - return new CatalogRequestParameters.Builder() - .withOffice(params.office) - .withIdLike(params.idLike) - .withUnitSystem(params.unitSystem) - .withLocCatLike(params.locCatLike) - .withLocGroupLike(params.locGroupLike) - .withTsCatLike(params.tsCatLike) - .withTsGroupLike(params.tsGroupLike) - .withBoundingOfficeLike(params.boundingOfficeLike) - .withIncludeExtents(params.includeExtents); - } - - - public CatalogRequestParameters build() { - return new CatalogRequestParameters(this); - } - } - - // This is supposed to answer the question of whether the current set of request parameters - // needs the specified table to be joined into the query. - public boolean needs(Table table) { - if (table == AV_LOC_GRP_ASSGN.AV_LOC_GRP_ASSGN) { - return locCatLike != null || locGroupLike != null; - } - - if (table == AV_TS_GRP_ASSGN.AV_TS_GRP_ASSGN) { - return tsCatLike != null || tsGroupLike != null; - } - - if (table == AV_LOC.AV_LOC) { - return boundingOfficeLike != null; - } - - if (table == AV_TS_EXTENTS_UTC) { - return includeExtents; - } - - return false; - } - - } } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Catalog.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Catalog.java index b3ff037b4..da298dc82 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/Catalog.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Catalog.java @@ -1,6 +1,7 @@ package cwms.cda.data.dto; import cwms.cda.api.errors.FieldException; +import cwms.cda.data.dao.CatalogRequestParameters; import cwms.cda.data.dto.catalog.CatalogEntry; import cwms.cda.data.dto.catalog.LocationCatalogEntry; import cwms.cda.data.dto.catalog.TimeseriesCatalogEntry; @@ -39,22 +40,12 @@ private Catalog() { } public Catalog(String page, int total, int pageSize, List entries) { - this(page, total, pageSize, entries, - null, - null, null, null, - null, - null, null, - false, true); + this(page, total, pageSize, entries, new CatalogRequestParameters.Builder().build()); } - @SuppressWarnings("java:S107") // This just has this many parameters. public Catalog(String page, int total, int pageSize, List entries, - String office, - String idLike, String locCategoryLike, String locGroupLike, - String tsCategoryLike, - String tsGroupLike, String boundingOfficeLike, - boolean includeExtents, boolean excludeEmpty) { + CatalogRequestParameters param) { super(page, pageSize, total); Objects.requireNonNull(entries, "List of catalog entries must be a valid list, even if empty"); @@ -62,15 +53,7 @@ public Catalog(String page, int total, int pageSize, List db = CwmsDataApiSetupCallback.getDatabaseLink(); + db.connection((c)-> { + try(PreparedStatement stmt = c.prepareStatement("declare\n" + + " p_location varchar2(64) := ?;\n" + + " p_office varchar2(10) := ?;\n" + + "begin\n" + + "cwms_loc.delete_location(\n" + + " p_location_id => p_location,\n" + + " p_delete_action => cwms_util.delete_all,\n" + + " p_db_office_id => p_office);\n" + + "end;")) { + stmt.setString(1, location); + stmt.setString(2, officeId); + stmt.execute(); + + } catch (SQLException ex) { + throw new RuntimeException("Unable to delete location",ex); + } + }, "cwms_20"); + } + } diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/CatalogTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/CatalogTest.java index 7983acd10..ff0a466c1 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/CatalogTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/CatalogTest.java @@ -1,5 +1,6 @@ package cwms.cda.data.dto; +import cwms.cda.data.dao.CatalogRequestParameters; import java.util.ArrayList; import org.junit.jupiter.api.Test; @@ -30,14 +31,12 @@ void test_xml_format(){ @Test void test_catalog_page_round_trip() { + CatalogRequestParameters params = new CatalogRequestParameters.Builder() + .withIdLike(".*") + .build(); + final CatalogPage page = new CatalogPage("SPK/a", - null, - ".*", - null, - null, - null, - null, - null); + params); final String pageString = Catalog.encodeCursor(page.toString(),10,100); final CatalogPage fromString = new CatalogPage(pageString); assertEquals(100,fromString.getTotal()); From 749e7f2c919de4ba907480f14f4b680fa2b76783 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Fri, 31 May 2024 17:19:21 -0700 Subject: [PATCH 09/17] Pulled the default values into a constant. Added detail to the swagger doc. --- .../java/cwms/cda/api/CatalogController.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java b/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java index b10d0fc1c..f54647bb5 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java @@ -58,7 +58,8 @@ public class CatalogController implements CrudHandler { private static final Logger logger = Logger.getLogger(CatalogController.class.getName()); private static final String TAG = "Catalog"; - + public static final boolean INCLUDE_EXTENTS_DEFAULT = true; + public static final boolean EXCLUDE_EMPTY_DEFAULT = true; private final MetricRegistry metrics; @@ -138,13 +139,17 @@ public void getAll(Context ctx) { + "When this field is used items with no bounding office set will not be present in results."), @OpenApiParam(name = Controllers.INCLUDE_EXTENTS, type = Boolean.class, description = "Whether the returned catalog entries should include timeseries " - + "extents. Only valid for TIMESERIES. Default is true."), + + "extents. Only valid for TIMESERIES. " + + "Default is " + INCLUDE_EXTENTS_DEFAULT + "."), @OpenApiParam(name = Controllers.EXCLUDE_EMPTY, type = Boolean.class, description = "Specifies " - + "whether Timeseries that have only empty extents [null, null, null, null] " - + "should be excluded from the results. This does not control whether the " - + "extents are returned to the user, only whether matching timeseries are " - + "excluded. Only valid for TIMESERIES. Default is true."), + + "whether Timeseries that have empty extents " + + "should be excluded from the results. For purposes of this parameter " + + "'empty' is defined as VERSION_TIME, EARLIEST_TIME, LATEST_TIME " + + "and LAST_UPDATE all being null. This parameter does not control " + + "whether the extents are returned to the user, only whether matching " + + "timeseries are excluded. Only valid for TIMESERIES. " + + "Default is " + EXCLUDE_EMPTY_DEFAULT + "."), }, pathParams = { @OpenApiParam(name = "dataset", @@ -210,9 +215,9 @@ public void getOne(@NotNull Context ctx, @NotNull String dataSet) { TimeSeriesDao tsDao = new TimeSeriesDaoImpl(dsl, metrics); boolean includeExtents = ctx.queryParamAsClass(INCLUDE_EXTENTS, Boolean.class) - .getOrDefault(true); + .getOrDefault(INCLUDE_EXTENTS_DEFAULT); boolean excludeExtents = ctx.queryParamAsClass(EXCLUDE_EMPTY, Boolean.class) - .getOrDefault(true); + .getOrDefault(EXCLUDE_EMPTY_DEFAULT); CatalogRequestParameters parameters = new CatalogRequestParameters.Builder() .withOffice(office) From 99db42233f5d35fe0bc381d52741ed79de937717 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Fri, 31 May 2024 18:04:11 -0700 Subject: [PATCH 10/17] Updated mock query. --- cwms-data-api/src/test/resources/ratings_db.txt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cwms-data-api/src/test/resources/ratings_db.txt b/cwms-data-api/src/test/resources/ratings_db.txt index 35a808133..4ac948329 100644 --- a/cwms-data-api/src/test/resources/ratings_db.txt +++ b/cwms-data-api/src/test/resources/ratings_db.txt @@ -86,6 +86,12 @@ select count(distinct "CWMS_20"."AV_CWMS_TS_ID"."TS_CODE") from "CWMS_20"."AV_CW > ----- > 5 @ rows: 1 +# For catalog catalog_returns_only_original_ids_by_default +select count(distinct "CWMS_20"."AV_CWMS_TS_ID"."TS_CODE") from "CWMS_20"."AV_CWMS_TS_ID" left outer join "CWMS_20"."AV_TS_EXTENTS_UTC" on "CWMS_20"."AV_CWMS_TS_ID"."TS_CODE" = "CWMS_20"."AV_TS_EXTENTS_UTC"."TS_CODE" where ((regexp_like("CWMS_20"."AV_CWMS_TS_ID"."CWMS_TS_ID", ?, 'i')) and ("CWMS_20"."AV_TS_EXTENTS_UTC"."VERSION_TIME" is not null or "CWMS_20"."AV_TS_EXTENTS_UTC"."EARLIEST_TIME" is not null or "CWMS_20"."AV_TS_EXTENTS_UTC"."LATEST_TIME" is not null or "CWMS_20"."AV_TS_EXTENTS_UTC"."LAST_UPDATE" is not null)); +> count +> ----- +> 5 +@ rows: 1 select "limiter"."DB_OFFICE_ID", "limiter"."CWMS_TS_ID", "limiter"."TS_CODE", "limiter"."UNIT_ID", "limiter"."INTERVAL_ID", "limiter"."INTERVAL_UTC_OFFSET", "CWMS_20"."AV_TS_EXTENTS_UTC"."VERSION_TIME", "CWMS_20"."AV_TS_EXTENTS_UTC"."EARLIEST_TIME", "CWMS_20"."AV_TS_EXTENTS_UTC"."LATEST_TIME", "CWMS_20"."AV_TS_EXTENTS_UTC"."LAST_UPDATE" from (select "data"."DB_OFFICE_ID", "data"."CWMS_TS_ID", "data"."TS_CODE", "data"."UNIT_ID", "data"."INTERVAL_ID", "data"."INTERVAL_UTC_OFFSET" from (select "CWMS_20"."AV_CWMS_TS_ID2"."DB_OFFICE_ID", "CWMS_20"."AV_CWMS_TS_ID2"."CWMS_TS_ID", "CWMS_20"."AV_CWMS_TS_ID2"."TS_CODE", "CWMS_20"."AV_CWMS_TS_ID2"."UNIT_ID", "CWMS_20"."AV_CWMS_TS_ID2"."INTERVAL_ID", "CWMS_20"."AV_CWMS_TS_ID2"."INTERVAL_UTC_OFFSET" from "CWMS_20"."AV_CWMS_TS_ID2" where ("CWMS_20"."AV_CWMS_TS_ID2"."ALIASED_ITEM" is null and (regexp_like("CWMS_20"."AV_CWMS_TS_ID2"."CWMS_TS_ID", ?, 'i')) and upper("CWMS_20"."AV_CWMS_TS_ID2"."CWMS_TS_ID") > ?) order by upper("CWMS_20"."AV_CWMS_TS_ID2"."DB_OFFICE_ID"), upper("CWMS_20"."AV_CWMS_TS_ID2"."CWMS_TS_ID")) "data" where rownum <= ?) "limiter" left outer join "CWMS_20"."AV_TS_EXTENTS_UTC" on ("CWMS_20"."AV_TS_EXTENTS_UTC"."TS_CODE" = "limiter"."TS_CODE") order by upper("limiter"."DB_OFFICE_ID"), upper("limiter"."CWMS_TS_ID"); > DB_OFFICE_ID CWMS_TS_ID TS_CODE UNIT_ID INTERVAL_ID INTERVAL_UTC_OFFSET VERSION_TIME EARLIEST_TIME LATEST_TIME LAST_UPDATE @@ -99,7 +105,8 @@ select "limiter"."DB_OFFICE_ID", "limiter"."CWMS_TS_ID", "limiter"."TS_CODE", "l @ rows: 6 -select "CWMS_20"."AV_CWMS_TS_ID"."DB_OFFICE_ID", "CWMS_20"."AV_CWMS_TS_ID"."CWMS_TS_ID", "CWMS_20"."AV_CWMS_TS_ID"."UNIT_ID", "CWMS_20"."AV_CWMS_TS_ID"."INTERVAL_ID", "CWMS_20"."AV_CWMS_TS_ID"."INTERVAL_UTC_OFFSET", "CWMS_20"."AV_TS_EXTENTS_UTC"."VERSION_TIME", "CWMS_20"."AV_TS_EXTENTS_UTC"."EARLIEST_TIME", "CWMS_20"."AV_TS_EXTENTS_UTC"."LATEST_TIME", "CWMS_20"."AV_TS_EXTENTS_UTC"."LAST_UPDATE" from "CWMS_20"."AV_CWMS_TS_ID" left outer join "CWMS_20"."AV_TS_EXTENTS_UTC" on "CWMS_20"."AV_CWMS_TS_ID"."TS_CODE" = "CWMS_20"."AV_TS_EXTENTS_UTC"."TS_CODE" where "CWMS_20"."AV_CWMS_TS_ID"."TS_CODE" in (select "alias_47325944"."TS_CODE" from (select "CWMS_20"."AV_CWMS_TS_ID"."TS_CODE" from "CWMS_20"."AV_CWMS_TS_ID" order by "CWMS_20"."AV_CWMS_TS_ID"."DB_OFFICE_ID", upper("CWMS_20"."AV_CWMS_TS_ID"."CWMS_TS_ID")) "alias_47325944" where rownum <= ?) order by "CWMS_20"."AV_CWMS_TS_ID"."DB_OFFICE_ID", upper("CWMS_20"."AV_CWMS_TS_ID"."CWMS_TS_ID"); +# Be careful, queries here have to end in semicolon to be matched. +select "CWMS_20"."AV_CWMS_TS_ID"."DB_OFFICE_ID", "CWMS_20"."AV_CWMS_TS_ID"."CWMS_TS_ID", "CWMS_20"."AV_CWMS_TS_ID"."UNIT_ID", "CWMS_20"."AV_CWMS_TS_ID"."INTERVAL_ID", "CWMS_20"."AV_CWMS_TS_ID"."INTERVAL_UTC_OFFSET", "CWMS_20"."AV_TS_EXTENTS_UTC"."VERSION_TIME", "CWMS_20"."AV_TS_EXTENTS_UTC"."EARLIEST_TIME", "CWMS_20"."AV_TS_EXTENTS_UTC"."LATEST_TIME", "CWMS_20"."AV_TS_EXTENTS_UTC"."LAST_UPDATE" from "CWMS_20"."AV_CWMS_TS_ID" left outer join "CWMS_20"."AV_TS_EXTENTS_UTC" on "CWMS_20"."AV_CWMS_TS_ID"."TS_CODE" = "CWMS_20"."AV_TS_EXTENTS_UTC"."TS_CODE" where "CWMS_20"."AV_CWMS_TS_ID"."TS_CODE" in (select "alias_78096721"."TS_CODE" from (select "CWMS_20"."AV_CWMS_TS_ID"."TS_CODE" from "CWMS_20"."AV_CWMS_TS_ID" left outer join "CWMS_20"."AV_TS_EXTENTS_UTC" on "CWMS_20"."AV_CWMS_TS_ID"."TS_CODE" = "CWMS_20"."AV_TS_EXTENTS_UTC"."TS_CODE" where ((regexp_like("CWMS_20"."AV_CWMS_TS_ID"."CWMS_TS_ID", '.*', 'i')) and ("CWMS_20"."AV_TS_EXTENTS_UTC"."VERSION_TIME" is not null or "CWMS_20"."AV_TS_EXTENTS_UTC"."EARLIEST_TIME" is not null or "CWMS_20"."AV_TS_EXTENTS_UTC"."LATEST_TIME" is not null or "CWMS_20"."AV_TS_EXTENTS_UTC"."LAST_UPDATE" is not null)) order by "CWMS_20"."AV_CWMS_TS_ID"."DB_OFFICE_ID", upper("CWMS_20"."AV_CWMS_TS_ID"."CWMS_TS_ID")) "alias_78096721" where rownum <= 500) order by "CWMS_20"."AV_CWMS_TS_ID"."DB_OFFICE_ID", upper("CWMS_20"."AV_CWMS_TS_ID"."CWMS_TS_ID"); > DB_OFFICE_ID CWMS_TS_ID UNIT_ID INTERVAL_ID INTERVAL_UTC_OFFSET VERSION_TIME EARLIEST_TIME LATEST_TIME LAST_UPDATE > ------------ ----------------------------------------------------- ------- ----------- ------------------- ------------ ------------- ----------- -------------------------- > LRB CHCN6.Volt.Inst.30Minutes.0.GOES-Rev volt 30Minutes 2147483647 {null} {null} {null} 2024-03-08 22:00:02.845431 From 334992d56bba3eb5241c4239ffaaf1259cfa1688 Mon Sep 17 00:00:00 2001 From: Adam Korynta Date: Fri, 31 May 2024 15:47:49 -0700 Subject: [PATCH 11/17] add property controller, dao, and integration tests --- .../src/main/java/cwms/cda/ApiServlet.java | 8 +- .../main/java/cwms/cda/api/Controllers.java | 3 + .../java/cwms/cda/api/PropertyController.java | 296 ++++++++++++++++++ .../java/cwms/cda/data/dao/PropertyDao.java | 102 ++++++ .../cwms/cda/api/PropertyControllerIT.java | 258 +++++++++++++++ .../cwms/cda/data/dao/PropertyDaoTestIT.java | 71 +++++ .../test/resources/cwms/cda/api/property.json | 7 + .../cwms/cda/api/property_bogus.json | 7 + 8 files changed, 749 insertions(+), 3 deletions(-) create mode 100644 cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dao/PropertyDao.java create mode 100644 cwms-data-api/src/test/java/cwms/cda/api/PropertyControllerIT.java create mode 100644 cwms-data-api/src/test/java/cwms/cda/data/dao/PropertyDaoTestIT.java create mode 100644 cwms-data-api/src/test/resources/cwms/cda/api/property.json create mode 100644 cwms-data-api/src/test/resources/cwms/cda/api/property_bogus.json diff --git a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java index 7e4633b79..e3deb1f95 100644 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java @@ -57,6 +57,7 @@ import cwms.cda.api.OfficeController; import cwms.cda.api.ParametersController; import cwms.cda.api.PoolController; +import cwms.cda.api.PropertyController; import cwms.cda.api.RatingController; import cwms.cda.api.RatingMetadataController; import cwms.cda.api.RatingSpecController; @@ -163,7 +164,8 @@ "/specified-levels/*", "/forecast-spec/*", "/forecast-instance/*", - "/standard-text-id/*" + "/standard-text-id/*", + "/properties/*" }) public class ApiServlet extends HttpServlet { @@ -457,8 +459,8 @@ protected void configureRoutes() { String forecastFilePath = "/forecast-instance/{" + NAME + "}/file-data"; get(forecastFilePath, new ForecastFileController(metrics)); addCacheControl(forecastFilePath, 1, TimeUnit.DAYS); - - + cdaCrudCache(format("/properties/{%s}", Controllers.NAME), + new PropertyController(metrics), requiredRoles,1, TimeUnit.DAYS); } /** diff --git a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java index 7eb7385eb..5d4b3abfb 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java @@ -110,6 +110,7 @@ public final class Controllers { public static final String INTERVAL_OFFSET = "interval-offset"; public static final String INTERVAL = "interval"; public static final String CATEGORY_ID = "category-id"; + public static final String CATEGORY_ID_MASK = "category-id-mask"; public static final String EXAMPLE_DATE = "2021-06-10T13:00:00-0700[PST8PDT]"; public static final String VERSION_DATE = "version-date"; @@ -150,6 +151,7 @@ public final class Controllers { public static final String HAS_DATA = "has-data"; public static final String STATUS_200 = "200"; public static final String STATUS_201 = "201"; + public static final String STATUS_204 = "204"; public static final String STATUS_404 = "404"; public static final String STATUS_501 = "501"; public static final String STATUS_400 = "400"; @@ -164,6 +166,7 @@ public final class Controllers { public static final String DESIGNATOR_MASK = "designator-mask"; public static final String INCLUDE_EXTENTS = "include-extents"; public static final String EXCLUDE_EMPTY = "exclude-empty"; + public static final String DEFAULT_VALUE = "default-value"; static { diff --git a/cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java b/cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java new file mode 100644 index 000000000..aceedde2d --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java @@ -0,0 +1,296 @@ +/* + * 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 com.codahale.metrics.Histogram; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.flogger.FluentLogger; +import cwms.cda.data.dao.PropertyDao; +import cwms.cda.data.dto.Property; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.FormattingException; +import cwms.cda.formatters.json.JsonV2; +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 org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +import static com.codahale.metrics.MetricRegistry.name; +import static cwms.cda.api.Controllers.*; +import static cwms.cda.data.dao.JooqDao.getDslContext; + +public final class PropertyController implements CrudHandler { + + static final String TAG = "Properties"; + private static final FluentLogger LOGGER = FluentLogger.forEnclosingClass(); + private final MetricRegistry metrics; + + private final Histogram requestResultSize; + + + public PropertyController(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( + pathParams = { + }, + queryParams = { + @OpenApiParam(name = OFFICE_MASK, description = "Filters properties to the specified office mask"), + @OpenApiParam(name = CATEGORY_ID, description = "Filters properties to the specified category mask"), + @OpenApiParam(name = NAME_MASK, description = "Filters properties to the specified name mask"), + }, + responses = { + @OpenApiResponse(status = STATUS_200, content = { + @OpenApiContent(isArray = true, type = Formats.JSONV2, from = Property.class) + }) + }, + description = "Returns matching CWMS Property Data.", + tags = {TAG} + ) + @Override + public void getAll(Context ctx) { + String officeMask = ctx.queryParam(OFFICE_MASK); + String categoryMask = ctx.queryParam(CATEGORY_ID_MASK); + String nameMask = ctx.queryParam(NAME_MASK); + try (Timer.Context ignored = markAndTime(GET_ALL)) { + DSLContext dsl = getDslContext(ctx); + PropertyDao dao = new PropertyDao(dsl); + List properties = dao.retrieveProperties(officeMask, categoryMask, nameMask); + ContentType contentType = getContentType(ctx); + ctx.contentType(contentType.toString()); + ObjectMapper om = getObjectMapperForFormat(contentType); + String serialized = om.writeValueAsString(properties); + ctx.result(serialized); + ctx.status(HttpServletResponse.SC_OK); + requestResultSize.update(serialized.length()); + } catch (IOException ex) { + String errorMsg = "Error retrieving properties."; + LOGGER.atWarning().withCause(ex).log("Error deserializing properties" + + " with parameters: " + ctx.queryParamMap()); + throw new FormattingException(errorMsg, ex); + } + } + + @OpenApi( + pathParams = { + @OpenApiParam(name = NAME, description = "Specifies the name of " + + "the property to be retrieved."), + }, + queryParams = { + @OpenApiParam(name = OFFICE, description = "Specifies the owning office of " + + "the property to be retrieved."), + @OpenApiParam(name = CATEGORY_ID, description = "Specifies the category id of " + + "the property to be retrieved."), + @OpenApiParam(name = DEFAULT_VALUE, description = "Specifies the default value " + + "if the property does not exist."), + }, + responses = { + @OpenApiResponse(status = STATUS_200, + content = { + @OpenApiContent(type = Formats.JSONV2, from = Property.class) + }) + }, + description = "Returns CWMS Property Data", + tags = {TAG} + ) + @Override + public void getOne(Context ctx, String name) { + String office = ctx.queryParam(OFFICE); + String category = ctx.queryParam(CATEGORY_ID); + String defaultValue = ctx.queryParam(DEFAULT_VALUE); + try (Timer.Context ignored = markAndTime(GET_ONE)) { + DSLContext dsl = getDslContext(ctx); + PropertyDao dao = new PropertyDao(dsl); + Property property = dao.retrieveProperty(office, category, name, defaultValue); + ContentType contentType = getContentType(ctx); + ctx.contentType(contentType.toString()); + ObjectMapper om = getObjectMapperForFormat(contentType); + String serialized = om.writeValueAsString(property); + ctx.result(serialized); + ctx.status(HttpServletResponse.SC_OK); + requestResultSize.update(serialized.length()); + } catch (IOException ex) { + String errorMsg = "Error retrieving property " + name; + LOGGER.atWarning().withCause(ex).log("Error deserializing property: " + name + + " with parameters: " + ctx.queryParamMap()); + throw new FormattingException(errorMsg, ex); + } + } + + private static @NotNull ContentType getContentType(Context ctx) { + String formatHeader = ctx.header(Header.ACCEPT) != null ? ctx.header(Header.ACCEPT) : + Formats.JSONV2; + ContentType contentType = Formats.parseHeader(formatHeader); + if (contentType == null) { + throw new FormattingException("Format header could not be parsed"); + } + return contentType; + } + + + @OpenApi( + requestBody = @OpenApiRequestBody( + content = { + @OpenApiContent(from = Property.class, type = Formats.JSONV2) + }, + required = true), + description = "Create CWMS Property", + method = HttpMethod.POST, + tags = {TAG}, + responses = { + @OpenApiResponse(status = STATUS_204, description = "Property successfully stored to CWMS.") + } + ) + @Override + public void create(Context ctx) { + try (Timer.Context ignored = markAndTime(CREATE)) { + String acceptHeader = ctx.req.getContentType(); + String formatHeader = acceptHeader != null ? acceptHeader : Formats.JSONV2; + ContentType contentType = Formats.parseHeader(formatHeader); + if (contentType == null) { + throw new FormattingException("Format header could not be parsed"); + } + Property property = deserializeProperty(ctx.body(), contentType); + property.validate(); + DSLContext dsl = getDslContext(ctx); + PropertyDao dao = new PropertyDao(dsl); + dao.storeProperty(property); + ctx.status(HttpServletResponse.SC_CREATED).json("Created Property"); + } catch (IOException ex) { + throw new IllegalArgumentException("Unable to parse property from content body", ex); + } + + } + + @OpenApi( + requestBody = @OpenApiRequestBody( + content = { + @OpenApiContent(from = Property.class, type = Formats.JSONV2) + }, + required = true), + description = "Update CWMS Property", + method = HttpMethod.PATCH, + tags = {TAG}, + responses = { + @OpenApiResponse(status = STATUS_204, description = "Property successfully stored to CWMS.") + } + ) + @Override + public void update(Context ctx, String name) { + try (Timer.Context ignored = markAndTime(UPDATE)) { + String acceptHeader = ctx.req.getContentType(); + String formatHeader = acceptHeader != null ? acceptHeader : Formats.JSONV2; + ContentType contentType = Formats.parseHeader(formatHeader); + if (contentType == null) { + throw new FormattingException("Format header could not be parsed"); + } + Property property = deserializeProperty(ctx.body(), contentType); + property.validate(); + DSLContext dsl = getDslContext(ctx); + PropertyDao dao = new PropertyDao(dsl); + dao.updateProperty(property); + ctx.status(HttpServletResponse.SC_OK).json("Updated Property"); + } catch (IOException ex) { + throw new IllegalArgumentException("Unable to parse property from content body", ex); + } + + } + + @OpenApi( + pathParams = { + @OpenApiParam(name = NAME, description = "Specifies the name of " + + "the property to be deleted."), + }, + queryParams = { + @OpenApiParam(name = OFFICE, description = "Specifies the owning office of " + + "the property to be deleted."), + @OpenApiParam(name = CATEGORY_ID, description = "Specifies the category id of " + + "the property to be deleted."), + }, + description = "Delete CWMS Property", + method = HttpMethod.DELETE, + tags = {TAG}, + responses = { + @OpenApiResponse(status = STATUS_204, description = "Property successfully deleted from CWMS."), + @OpenApiResponse(status = STATUS_404, description = "Based on the combination of " + + "inputs provided the property was not found.") + } + ) + @Override + public void delete(Context ctx, String name) { + String office = ctx.queryParam(OFFICE); + String category = ctx.queryParam(CATEGORY_ID); + try (Timer.Context ignored = markAndTime(DELETE)) { + DSLContext dsl = getDslContext(ctx); + PropertyDao dao = new PropertyDao(dsl); + dao.deleteProperty(office, category, name); + ctx.status(HttpServletResponse.SC_NO_CONTENT).json(name + " Deleted"); + } + } + + private static Property deserializeProperty(String body, ContentType contentType) + throws IOException { + ObjectMapper om = getObjectMapperForFormat(contentType); + Property retVal; + try { + retVal = om.readValue(body, Property.class); + } catch (Exception e) { + throw new IOException("Failed to deserialize property", e); + } + return retVal; + } + + private static ObjectMapper getObjectMapperForFormat(ContentType contentType) { + ObjectMapper om; + if (ContentType.equivalent(Formats.JSONV2, contentType.toString())) { + om = JsonV2.buildObjectMapper(); + } else { + throw new FormattingException("Format is not currently supported for Properties"); + } + return om; + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/PropertyDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/PropertyDao.java new file mode 100644 index 000000000..43b414533 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/PropertyDao.java @@ -0,0 +1,102 @@ +/* + * 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.data.dao; + +import cwms.cda.api.errors.NotFoundException; +import cwms.cda.data.dto.Property; +import org.jooq.DSLContext; +import org.jooq.impl.DSL; +import usace.cwms.db.jooq.codegen.packages.CWMS_PROPERTIES_PACKAGE; +import usace.cwms.db.jooq.codegen.packages.cwms_properties.GET_PROPERTY__2; +import usace.cwms.db.jooq.codegen.tables.AV_PROPERTY; +import usace.cwms.db.jooq.codegen.udt.records.PROPERTY_INFO_T; + +import java.util.List; + +public final class PropertyDao extends JooqDao { + + public PropertyDao(DSLContext dsl) { + super(dsl); + } + + public Property retrieveProperty(String office, String category, String name, String defaultValue) { + return connectionResult(dsl, conn -> { + setOffice(conn, office); + GET_PROPERTY__2 value = CWMS_PROPERTIES_PACKAGE.call_GET_PROPERTY__2(DSL.using(conn).configuration(), category, name, defaultValue, office); + return new Property.Builder() + .withOfficeId(office) + .withCategory(category) + .withName(name) + .withValue(value.getP_VALUE()) + .withComment(value.getP_COMMENT()) + .build(); + }); + } + + public List retrieveProperties(String officeIdMask, String categoryMask, String idMask) { + return connectionResult(dsl, conn -> { + PROPERTY_INFO_T propInfo = new PROPERTY_INFO_T(officeIdMask, categoryMask, idMask); + return CWMS_PROPERTIES_PACKAGE.call_GET_PROPERTIES__4(DSL.using(conn).configuration(), propInfo) + .map(r -> new Property.Builder() + .withOfficeId(r.get(AV_PROPERTY.AV_PROPERTY.OFFICE_ID)) + .withCategory(r.get(AV_PROPERTY.AV_PROPERTY.PROP_CATEGORY)) + .withName(r.get(AV_PROPERTY.AV_PROPERTY.PROP_ID)) + .withValue(r.get(AV_PROPERTY.AV_PROPERTY.PROP_VALUE)) + .withComment(r.get(AV_PROPERTY.AV_PROPERTY.PROP_COMMENT)) + .build()); + }); + } + + public void updateProperty(Property property) { + List properties = retrieveProperties(property.getOfficeId(), property.getCategory(), property.getName()); + if (properties.isEmpty()) { + throw new NotFoundException("Could not find property to update."); + } + connection(dsl, conn -> { + setOffice(conn, property.getOfficeId()); + CWMS_PROPERTIES_PACKAGE.call_SET_PROPERTY(DSL.using(conn).configuration(), property.getCategory(), + property.getName(), property.getValue(), property.getComment(), property.getOfficeId()); + }); + } + + public void storeProperty(Property property) { + connection(dsl, conn -> { + setOffice(conn, property.getOfficeId()); + CWMS_PROPERTIES_PACKAGE.call_SET_PROPERTY(DSL.using(conn).configuration(), property.getCategory(), + property.getName(), property.getValue(), property.getComment(), property.getOfficeId()); + }); + } + + public void deleteProperty(String office, String category, String name) { + List properties = retrieveProperties(office, category, name); + if (properties.isEmpty()) { + throw new NotFoundException("Could not find property to delete."); + } + connection(dsl, conn -> { + setOffice(conn, office); + CWMS_PROPERTIES_PACKAGE.call_DELETE_PROPERTY(DSL.using(conn).configuration(), category, name, office); + }); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/PropertyControllerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/PropertyControllerIT.java new file mode 100644 index 000000000..5412920a5 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/PropertyControllerIT.java @@ -0,0 +1,258 @@ +/* + * 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 cwms.cda.data.dto.Property; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.json.JsonV2; +import fixtures.TestAccounts; +import io.restassured.filter.log.LogDetail; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.Instant; + +import static cwms.cda.security.KeyAccessManager.AUTH_HEADER; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Tag("integration") +final class PropertyControllerIT extends DataApiTestIT { + + + + @Test + void test_get_create_delete() throws IOException { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/api/property.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + assertNotNull(json); + Property property = JsonV2.buildObjectMapper().readValue(json, Property.class); + + // Structure of test: + // 1)Create the Property + // 2)Retrieve the Property and assert that it exists + // 3)Delete the Property + // 4)Retrieve the Property and assert that it does not exist + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + //Create the property + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/properties/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)) + ; + String office = property.getOfficeId(); + // Retrieve the property and assert that it exists + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .queryParam(Controllers.OFFICE, office) + .queryParam(Controllers.CATEGORY_ID, property.getCategory()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("properties/" + property.getName()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("category", equalTo(property.getCategory())) + .body("office-id", equalTo(office)) + .body("comment", equalTo(property.getComment())) + .body("value", equalTo(property.getValue())) + .body("name", equalTo(property.getName())) + ; + + // Delete a Property + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .queryParam(Controllers.OFFICE, office) + .queryParam(Controllers.CATEGORY_ID, property.getCategory()) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("properties/" + property.getName()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)) + ; + + // Retrieve a Property and assert that it does not exist + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .queryParam(Controllers.OFFICE, office) + .queryParam(Controllers.CATEGORY_ID, property.getCategory()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("properties/" + property.getName()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("value", nullValue()) + ; + } + + @Test + void test_update_does_not_exist() throws IOException { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/api/property_bogus.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + assertNotNull(json); + Property property = JsonV2.buildObjectMapper().readValue(json, Property.class); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + //Create the property + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/properties/" + property.getName()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)) + ; + } + + @Test + void test_delete_does_not_exist() throws IOException { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + // Delete a Property + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .queryParam(Controllers.OFFICE, user.getOperatingOffice()) + .queryParam(Controllers.CATEGORY_ID, Instant.now().toEpochMilli()) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("properties/" + Instant.now().toEpochMilli()) + .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/property.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + assertNotNull(json); + Property property = JsonV2.buildObjectMapper().readValue(json, Property.class); + + // Structure of test: + // 1)Create the Property + // 2)Retrieve the Property with getAll and assert that it exists + // 3)Delete the Property + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + //Create the property + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/properties/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)) + ; + String office = property.getOfficeId(); + // Retrieve the property and assert that it exists + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .queryParam(Controllers.OFFICE_MASK, office) + .queryParam(Controllers.CATEGORY_ID_MASK, property.getCategory()) + .queryParam(Controllers.NAME_MASK, property.getName()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("properties/") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("[0].category", equalTo(property.getCategory())) + .body("[0].office-id", equalTo(office)) + .body("[0].comment", equalTo(property.getComment())) + .body("[0].value", equalTo(property.getValue())) + .body("[0].name", equalTo(property.getName())) + ; + + // Delete a Property + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .queryParam(Controllers.OFFICE, office) + .queryParam(Controllers.CATEGORY_ID, property.getCategory()) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("properties/" + property.getName()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)) + ; + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/PropertyDaoTestIT.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/PropertyDaoTestIT.java new file mode 100644 index 000000000..f16e9818a --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/PropertyDaoTestIT.java @@ -0,0 +1,71 @@ +/* + * 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.data.dao; + +import cwms.cda.api.DataApiTestIT; +import cwms.cda.data.dto.Property; +import fixtures.CwmsDataApiSetupCallback; +import mil.army.usace.hec.test.database.CwmsDatabaseContainer; +import org.jooq.DSLContext; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static cwms.cda.data.dao.DaoTest.getDslContext; +import static org.junit.jupiter.api.Assertions.*; + +@Tag("integration") +final class PropertyDaoTestIT extends DataApiTestIT { + + @Test + void testPropertyRoundTrip() throws Exception { + CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); + Property property = new Property.Builder() + .withCategory("PropertyDaoTestIT") + .withOfficeId(databaseLink.getOfficeId()) + .withName("testPropertyRoundTrip") + .withValue("value") + .withComment("CDA integration test property") + .build(); + databaseLink.connection(c -> { + DSLContext context = getDslContext(c, databaseLink.getOfficeId()); + PropertyDao propertyDao = new PropertyDao(context); + propertyDao.storeProperty(property); + Property fromDb = propertyDao.retrieveProperty(property.getOfficeId(), + property.getCategory(), property.getName(), null); + assertEquals(property, fromDb, "Property retrieved from database does not match original property"); + Property fromDbMulti = propertyDao.retrieveProperties(databaseLink.getOfficeId(), property.getCategory(), property.getName()) + .get(0); + assertEquals(property, fromDbMulti, "Property multi retrieved from database does not match original property"); + propertyDao.deleteProperty(databaseLink.getOfficeId(), property.getCategory(), property.getName()); + Property deleted = propertyDao.retrieveProperty(property.getOfficeId(), + property.getCategory(), property.getName(), null); + assertNull(deleted.getValue(), "Property has been deleted, value should be null"); + List empty = propertyDao.retrieveProperties(databaseLink.getOfficeId(), property.getCategory(), property.getName()); + assertTrue(empty.isEmpty(), "Property has been deleted, it should not show up in multi retrieve query"); + }); + } +} diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/property.json b/cwms-data-api/src/test/resources/cwms/cda/api/property.json new file mode 100644 index 000000000..ac988474d --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/property.json @@ -0,0 +1,7 @@ +{ + "category": "PropertyControllerIT", + "name": "test_get_create_delete", + "office-id": "SPK", + "value": "test value", + "comment": "integration test" +} \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/property_bogus.json b/cwms-data-api/src/test/resources/cwms/cda/api/property_bogus.json new file mode 100644 index 000000000..80a59f740 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/property_bogus.json @@ -0,0 +1,7 @@ +{ + "category": "PropertyControllerIT", + "name": "test_update_does_not_exist", + "office-id": "SPK", + "value": "test value", + "comment": "integration test" +} \ No newline at end of file From 1b24f9d6a1e6e5209994e159c0e4562618ab82fa Mon Sep 17 00:00:00 2001 From: Adam Korynta Date: Mon, 3 Jun 2024 09:23:13 -0700 Subject: [PATCH 12/17] implement parser logic to follow formatter factory logic this simplifies how parsing and formatting is done so that we can remove duplicate code --- .../java/cwms/cda/api/PropertyController.java | 63 ++++--------------- .../main/java/cwms/cda/data/dto/Property.java | 4 +- .../java/cwms/cda/formatters/Formats.java | 18 ++++++ .../cwms/cda/formatters/OutputFormatter.java | 7 ++- .../java/cwms/cda/formatters/csv/CsvV1.java | 11 ++++ .../formatters/csv/CsvV1LocationGroup.java | 6 ++ .../cwms/cda/formatters/csv/CsvV1Office.java | 6 ++ .../java/cwms/cda/formatters/json/JsonV1.java | 9 +++ .../java/cwms/cda/formatters/json/JsonV2.java | 9 +++ .../formatters/json/NamedPgJsonFormatter.java | 10 ++- .../cda/formatters/json/PgJsonFormatter.java | 6 ++ .../java/cwms/cda/formatters/tab/TabV1.java | 9 +++ .../cwms/cda/formatters/tab/TabV1Office.java | 6 ++ .../java/cwms/cda/formatters/xml/XMLv1.java | 6 ++ .../java/cwms/cda/formatters/xml/XMLv2.java | 17 +++++ .../cwms/cda/api/PropertyControllerIT.java | 54 ++++++++-------- .../java/cwms/cda/data/dto/PropertyTest.java | 12 ++-- 17 files changed, 161 insertions(+), 92 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java b/cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java index aceedde2d..5977669cf 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java @@ -27,14 +27,12 @@ import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.flogger.FluentLogger; import cwms.cda.data.dao.PropertyDao; import cwms.cda.data.dto.Property; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; import cwms.cda.formatters.FormattingException; -import cwms.cda.formatters.json.JsonV2; import io.javalin.apibuilder.CrudHandler; import io.javalin.core.util.Header; import io.javalin.http.Context; @@ -48,7 +46,6 @@ import org.jooq.DSLContext; import javax.servlet.http.HttpServletResponse; -import java.io.IOException; import java.util.List; import static com.codahale.metrics.MetricRegistry.name; @@ -85,7 +82,7 @@ private Timer.Context markAndTime(String subject) { }, responses = { @OpenApiResponse(status = STATUS_200, content = { - @OpenApiContent(isArray = true, type = Formats.JSONV2, from = Property.class) + @OpenApiContent(isArray = true, type = Formats.JSON, from = Property.class) }) }, description = "Returns matching CWMS Property Data.", @@ -102,16 +99,10 @@ public void getAll(Context ctx) { List properties = dao.retrieveProperties(officeMask, categoryMask, nameMask); ContentType contentType = getContentType(ctx); ctx.contentType(contentType.toString()); - ObjectMapper om = getObjectMapperForFormat(contentType); - String serialized = om.writeValueAsString(properties); + String serialized = Formats.format(contentType, properties, Property.class); ctx.result(serialized); ctx.status(HttpServletResponse.SC_OK); requestResultSize.update(serialized.length()); - } catch (IOException ex) { - String errorMsg = "Error retrieving properties."; - LOGGER.atWarning().withCause(ex).log("Error deserializing properties" - + " with parameters: " + ctx.queryParamMap()); - throw new FormattingException(errorMsg, ex); } } @@ -131,7 +122,7 @@ public void getAll(Context ctx) { responses = { @OpenApiResponse(status = STATUS_200, content = { - @OpenApiContent(type = Formats.JSONV2, from = Property.class) + @OpenApiContent(type = Formats.JSON, from = Property.class) }) }, description = "Returns CWMS Property Data", @@ -148,22 +139,16 @@ public void getOne(Context ctx, String name) { Property property = dao.retrieveProperty(office, category, name, defaultValue); ContentType contentType = getContentType(ctx); ctx.contentType(contentType.toString()); - ObjectMapper om = getObjectMapperForFormat(contentType); - String serialized = om.writeValueAsString(property); + String serialized = Formats.format(contentType, property); ctx.result(serialized); ctx.status(HttpServletResponse.SC_OK); requestResultSize.update(serialized.length()); - } catch (IOException ex) { - String errorMsg = "Error retrieving property " + name; - LOGGER.atWarning().withCause(ex).log("Error deserializing property: " + name - + " with parameters: " + ctx.queryParamMap()); - throw new FormattingException(errorMsg, ex); } } private static @NotNull ContentType getContentType(Context ctx) { String formatHeader = ctx.header(Header.ACCEPT) != null ? ctx.header(Header.ACCEPT) : - Formats.JSONV2; + Formats.JSON; ContentType contentType = Formats.parseHeader(formatHeader); if (contentType == null) { throw new FormattingException("Format header could not be parsed"); @@ -175,7 +160,7 @@ public void getOne(Context ctx, String name) { @OpenApi( requestBody = @OpenApiRequestBody( content = { - @OpenApiContent(from = Property.class, type = Formats.JSONV2) + @OpenApiContent(from = Property.class, type = Formats.JSON) }, required = true), description = "Create CWMS Property", @@ -189,19 +174,17 @@ public void getOne(Context ctx, String name) { public void create(Context ctx) { try (Timer.Context ignored = markAndTime(CREATE)) { String acceptHeader = ctx.req.getContentType(); - String formatHeader = acceptHeader != null ? acceptHeader : Formats.JSONV2; + String formatHeader = acceptHeader != null ? acceptHeader : Formats.JSON; ContentType contentType = Formats.parseHeader(formatHeader); if (contentType == null) { throw new FormattingException("Format header could not be parsed"); } - Property property = deserializeProperty(ctx.body(), contentType); + Property property = Formats.parseContent(contentType, ctx.body(), Property.class); property.validate(); DSLContext dsl = getDslContext(ctx); PropertyDao dao = new PropertyDao(dsl); dao.storeProperty(property); ctx.status(HttpServletResponse.SC_CREATED).json("Created Property"); - } catch (IOException ex) { - throw new IllegalArgumentException("Unable to parse property from content body", ex); } } @@ -209,7 +192,7 @@ public void create(Context ctx) { @OpenApi( requestBody = @OpenApiRequestBody( content = { - @OpenApiContent(from = Property.class, type = Formats.JSONV2) + @OpenApiContent(from = Property.class, type = Formats.JSON) }, required = true), description = "Update CWMS Property", @@ -223,19 +206,17 @@ public void create(Context ctx) { public void update(Context ctx, String name) { try (Timer.Context ignored = markAndTime(UPDATE)) { String acceptHeader = ctx.req.getContentType(); - String formatHeader = acceptHeader != null ? acceptHeader : Formats.JSONV2; + String formatHeader = acceptHeader != null ? acceptHeader : Formats.JSON; ContentType contentType = Formats.parseHeader(formatHeader); if (contentType == null) { throw new FormattingException("Format header could not be parsed"); } - Property property = deserializeProperty(ctx.body(), contentType); + Property property = Formats.parseContent(contentType, ctx.body(), Property.class); property.validate(); DSLContext dsl = getDslContext(ctx); PropertyDao dao = new PropertyDao(dsl); dao.updateProperty(property); ctx.status(HttpServletResponse.SC_OK).json("Updated Property"); - } catch (IOException ex) { - throw new IllegalArgumentException("Unable to parse property from content body", ex); } } @@ -271,26 +252,4 @@ public void delete(Context ctx, String name) { ctx.status(HttpServletResponse.SC_NO_CONTENT).json(name + " Deleted"); } } - - private static Property deserializeProperty(String body, ContentType contentType) - throws IOException { - ObjectMapper om = getObjectMapperForFormat(contentType); - Property retVal; - try { - retVal = om.readValue(body, Property.class); - } catch (Exception e) { - throw new IOException("Failed to deserialize property", e); - } - return retVal; - } - - private static ObjectMapper getObjectMapperForFormat(ContentType contentType) { - ObjectMapper om; - if (ContentType.equivalent(Formats.JSONV2, contentType.toString())) { - om = JsonV2.buildObjectMapper(); - } else { - throw new FormattingException("Format is not currently supported for Properties"); - } - return om; - } } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java index 242f3d4a9..d847b830f 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Property.java @@ -31,7 +31,7 @@ import cwms.cda.api.errors.FieldException; import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; -import cwms.cda.formatters.json.JsonV2; +import cwms.cda.formatters.json.JsonV1; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; @@ -40,7 +40,7 @@ @XmlRootElement(name = "property") @XmlAccessorType(XmlAccessType.FIELD) -@FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class) +@FormattableWith(contentType = Formats.JSON, formatter = JsonV1.class) @JsonDeserialize(builder = Property.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/Formats.java b/cwms-data-api/src/main/java/cwms/cda/formatters/Formats.java index 73b0eaa41..92c1773b0 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/Formats.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/Formats.java @@ -118,6 +118,20 @@ private String getFormatted(ContentType type, List dtos, } } + private T parseContentFromType(ContentType type, String content, Class rootType) + throws FormattingException { + OutputFormatter outputFormatter = getOutputFormatter(type, rootType); + if (outputFormatter != null) { + T retval = outputFormatter.parseContent(content, rootType); + retval.validate(); + return retval; + } else { + String message = String.format("No Format for this content-type and data type : (%s, %s)", + type.toString(), rootType.getName()); + throw new FormattingException(message); + } + } + private OutputFormatter getOutputFormatter(ContentType type, Class klass) { OutputFormatter outputFormatter = null; @@ -154,6 +168,10 @@ public static String format(ContentType type, List toForm return formats.getFormatted(type, toFormat, rootType); } + public static T parseContent(ContentType type, String content, Class rootType) + throws FormattingException { + return formats.parseContentFromType(type, content, rootType); + } /** * Parses the supplied header param or queryParam to determine the content type. diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/OutputFormatter.java b/cwms-data-api/src/main/java/cwms/cda/formatters/OutputFormatter.java index 577220e80..db8fb2766 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/OutputFormatter.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/OutputFormatter.java @@ -5,7 +5,8 @@ import cwms.cda.data.dto.CwmsDTOBase; public interface OutputFormatter { - public String getContentType(); - public String format(CwmsDTOBase dto); - public String format(List dtoList); + String getContentType(); + String format(CwmsDTOBase dto); + String format(List dtoList); + T parseContent(String content, Class type); } diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/csv/CsvV1.java b/cwms-data-api/src/main/java/cwms/cda/formatters/csv/CsvV1.java index 0f42afe98..7700fb9cc 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/csv/CsvV1.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/csv/CsvV1.java @@ -40,4 +40,15 @@ public String format(List dtoList) { } return retVal; } + + @Override + public T parseContent(String content, Class type) { + T retVal = null; + if (type.isAssignableFrom(Office.class)) { + retVal = new CsvV1Office().parseContent(content, type); + } else if (type.isAssignableFrom(LocationGroup.class)) { + retVal = new CsvV1LocationGroup().parseContent(content, type); + } + return retVal; + } } diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/csv/CsvV1LocationGroup.java b/cwms-data-api/src/main/java/cwms/cda/formatters/csv/CsvV1LocationGroup.java index e9b49641c..1b19fd4de 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/csv/CsvV1LocationGroup.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/csv/CsvV1LocationGroup.java @@ -81,6 +81,12 @@ public String format(List dtoList) { return null; } + @Override + public T parseContent(String content, Class type) { + throw new UnsupportedOperationException("Unable to process your request. Deserialization of " + + getContentType() + " not yet supported."); + } + // Mixin for LocationGroup // This class doesn't have to be related to LocationGroup, it just has to look like it. diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/csv/CsvV1Office.java b/cwms-data-api/src/main/java/cwms/cda/formatters/csv/CsvV1Office.java index 55ae0f664..9ce59efe4 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/csv/CsvV1Office.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/csv/CsvV1Office.java @@ -53,6 +53,12 @@ public String format(List dtoList) { return builder.toString(); } + @Override + public T parseContent(String content, Class type) { + throw new UnsupportedOperationException("Unable to process your request. Deserialization of " + + getContentType() + " not yet supported."); + } + private String getOfficeTabHeader() { return "#Office Name,Long Name,Office Type,Reports To Office"; } diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV1.java b/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV1.java index 86eb22b34..23e3f2110 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV1.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV1.java @@ -76,6 +76,15 @@ public String format(List dtoList) { } } + @Override + public T parseContent(String content, Class type) { + try { + return om.readValue(content, type); + } catch (JsonProcessingException e) { + throw new FormattingException("Could not deserialize:" + content, e); + } + } + private Object buildFormatting(CwmsDTOBase dto) { Object retVal = null; diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV2.java b/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV2.java index c82998a5a..023dc5627 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV2.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV2.java @@ -97,4 +97,13 @@ public String format(List dtoList) { } } + @Override + public T parseContent(String content, Class type) { + try { + return om.readValue(content, type); + } catch (JsonProcessingException e) { + throw new FormattingException("Could not deserialize:" + content, e); + } + } + } diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/json/NamedPgJsonFormatter.java b/cwms-data-api/src/main/java/cwms/cda/formatters/json/NamedPgJsonFormatter.java index c3052fe15..687c73e34 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/json/NamedPgJsonFormatter.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/json/NamedPgJsonFormatter.java @@ -15,6 +15,8 @@ import java.util.ArrayList; import java.util.List; +import static cwms.cda.formatters.Formats.NAMED_PGJSON; + public class NamedPgJsonFormatter implements OutputFormatter { private final ObjectMapper om; @@ -24,7 +26,7 @@ public NamedPgJsonFormatter() { @Override public String getContentType() { - return Formats.NAMED_PGJSON; + return NAMED_PGJSON; } @Override @@ -54,6 +56,12 @@ public String format(List dtoList) { return retVal.toString(); } + @Override + public T parseContent(String content, Class type) { + throw new UnsupportedOperationException("Unable to process your request. Deserialization of " + + getContentType() + " not yet supported."); + } + private String formatNamedGraph(String name, Graph graph) throws JsonProcessingException { String retVal = getDefaultNamedPgJson(name); if (!graph.isEmpty()) { diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/json/PgJsonFormatter.java b/cwms-data-api/src/main/java/cwms/cda/formatters/json/PgJsonFormatter.java index 583acf3ee..aac931f04 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/json/PgJsonFormatter.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/json/PgJsonFormatter.java @@ -119,4 +119,10 @@ public String format(List dtoList) { } return retVal.toString(); } + + @Override + public T parseContent(String content, Class type) { + throw new UnsupportedOperationException("Unable to process your request. Deserialization of " + + getContentType() + " not yet supported."); + } } diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/tab/TabV1.java b/cwms-data-api/src/main/java/cwms/cda/formatters/tab/TabV1.java index d5453a138..55c551694 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/tab/TabV1.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/tab/TabV1.java @@ -31,4 +31,13 @@ public String format(List dtoList) { return null; } } + + @Override + public T parseContent(String content, Class type) { + if (type.isAssignableFrom(Office.class)) { + return new TabV1Office().parseContent(content, type); + } else { + return null; + } + } } diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/tab/TabV1Office.java b/cwms-data-api/src/main/java/cwms/cda/formatters/tab/TabV1Office.java index b3553dcd9..8ec03d375 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/tab/TabV1Office.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/tab/TabV1Office.java @@ -52,6 +52,12 @@ public String format(List dtoList) { return builder.toString(); } + @Override + public T parseContent(String content, Class type) { + throw new UnsupportedOperationException("Unable to process your request. Deserialization of " + + getContentType() + " not yet supported."); + } + private String getOfficeTabHeader() { return "#Office Name Long Name Office Type Reports To Office"; } diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv1.java b/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv1.java index ea0ae6a0e..eeea0dec8 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv1.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv1.java @@ -75,4 +75,10 @@ public String format(List dtoList) { throw new UnsupportedOperationException("Unable to process your request"); } + @Override + public T parseContent(String content, Class type) { + throw new UnsupportedOperationException("Unable to process your request. Deserialization of " + + getContentType() + " not yet supported."); + } + } diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java b/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java index d396d8dbf..965946334 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java @@ -1,7 +1,11 @@ package cwms.cda.formatters.xml; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; import cwms.cda.data.dto.CwmsDTOBase; import cwms.cda.formatters.Formats; +import cwms.cda.formatters.FormattingException; import cwms.cda.formatters.OutputFormatter; import io.javalin.http.InternalServerErrorResponse; import java.io.PrintWriter; @@ -50,4 +54,17 @@ public String format(List dtoList) { throw new UnsupportedOperationException("Unable to process your request"); } + @Override + public T parseContent(String content, Class type) { + + try { + JacksonXmlModule module = new JacksonXmlModule(); + module.setDefaultUseWrapper(false); + XmlMapper om = new XmlMapper(module); + return om.readValue(content, type); + } catch (JsonProcessingException e) { + throw new FormattingException("Could not deserialize:" + content, e); + } + } + } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/PropertyControllerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/PropertyControllerIT.java index 5412920a5..c4174e4a8 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/PropertyControllerIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/PropertyControllerIT.java @@ -25,8 +25,8 @@ package cwms.cda.api; import cwms.cda.data.dto.Property; +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 org.apache.commons.io.IOUtils; @@ -55,7 +55,7 @@ void test_get_create_delete() throws IOException { assertNotNull(resource); String json = IOUtils.toString(resource, StandardCharsets.UTF_8); assertNotNull(json); - Property property = JsonV2.buildObjectMapper().readValue(json, Property.class); + Property property = Formats.parseContent(new ContentType(Formats.JSON), json, Property.class); // Structure of test: // 1)Create the Property @@ -67,8 +67,8 @@ void test_get_create_delete() throws IOException { //Create the property given() .log().ifValidationFails(LogDetail.ALL, true) - .accept(Formats.JSONV2) - .contentType(Formats.JSONV2) + .accept(Formats.JSON) + .contentType(Formats.JSON) .body(json) .header(AUTH_HEADER, user.toHeaderValue()) .when() @@ -84,7 +84,7 @@ void test_get_create_delete() throws IOException { // Retrieve the property and assert that it exists given() .log().ifValidationFails(LogDetail.ALL,true) - .accept(Formats.JSONV2) + .accept(Formats.JSON) .queryParam(Controllers.OFFICE, office) .queryParam(Controllers.CATEGORY_ID, property.getCategory()) .when() @@ -105,7 +105,7 @@ void test_get_create_delete() throws IOException { // Delete a Property given() .log().ifValidationFails(LogDetail.ALL,true) - .accept(Formats.JSONV2) + .accept(Formats.JSON) .queryParam(Controllers.OFFICE, office) .queryParam(Controllers.CATEGORY_ID, property.getCategory()) .header(AUTH_HEADER, user.toHeaderValue()) @@ -122,7 +122,7 @@ void test_get_create_delete() throws IOException { // Retrieve a Property and assert that it does not exist given() .log().ifValidationFails(LogDetail.ALL,true) - .accept(Formats.JSONV2) + .accept(Formats.JSON) .queryParam(Controllers.OFFICE, office) .queryParam(Controllers.CATEGORY_ID, property.getCategory()) .when() @@ -143,24 +143,24 @@ void test_update_does_not_exist() throws IOException { assertNotNull(resource); String json = IOUtils.toString(resource, StandardCharsets.UTF_8); assertNotNull(json); - Property property = JsonV2.buildObjectMapper().readValue(json, Property.class); + Property property = Formats.parseContent(new ContentType(Formats.JSON), json, Property.class); TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; //Create the property given() - .log().ifValidationFails(LogDetail.ALL, true) - .accept(Formats.JSONV2) - .contentType(Formats.JSONV2) - .body(json) - .header(AUTH_HEADER, user.toHeaderValue()) - .when() - .redirects().follow(true) - .redirects().max(3) - .patch("/properties/" + property.getName()) - .then() - .log().ifValidationFails(LogDetail.ALL, true) - .assertThat() - .statusCode(is(HttpServletResponse.SC_NOT_FOUND)) + .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("/properties/" + property.getName()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)) ; } @@ -170,7 +170,7 @@ void test_delete_does_not_exist() throws IOException { // Delete a Property given() .log().ifValidationFails(LogDetail.ALL,true) - .accept(Formats.JSONV2) + .accept(Formats.JSON) .queryParam(Controllers.OFFICE, user.getOperatingOffice()) .queryParam(Controllers.CATEGORY_ID, Instant.now().toEpochMilli()) .header(AUTH_HEADER, user.toHeaderValue()) @@ -191,7 +191,7 @@ void test_get_all() throws IOException { assertNotNull(resource); String json = IOUtils.toString(resource, StandardCharsets.UTF_8); assertNotNull(json); - Property property = JsonV2.buildObjectMapper().readValue(json, Property.class); + Property property = Formats.parseContent(new ContentType(Formats.JSON), json, Property.class); // Structure of test: // 1)Create the Property @@ -202,8 +202,8 @@ void test_get_all() throws IOException { //Create the property given() .log().ifValidationFails(LogDetail.ALL, true) - .accept(Formats.JSONV2) - .contentType(Formats.JSONV2) + .accept(Formats.JSON) + .contentType(Formats.JSON) .body(json) .header(AUTH_HEADER, user.toHeaderValue()) .when() @@ -219,7 +219,7 @@ void test_get_all() throws IOException { // Retrieve the property and assert that it exists given() .log().ifValidationFails(LogDetail.ALL,true) - .accept(Formats.JSONV2) + .accept(Formats.JSON) .queryParam(Controllers.OFFICE_MASK, office) .queryParam(Controllers.CATEGORY_ID_MASK, property.getCategory()) .queryParam(Controllers.NAME_MASK, property.getName()) @@ -241,7 +241,7 @@ void test_get_all() throws IOException { // Delete a Property given() .log().ifValidationFails(LogDetail.ALL,true) - .accept(Formats.JSONV2) + .accept(Formats.JSON) .queryParam(Controllers.OFFICE, office) .queryParam(Controllers.CATEGORY_ID, property.getCategory()) .header(AUTH_HEADER, user.toHeaderValue()) diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java index f5c522660..6ad885f08 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/PropertyTest.java @@ -24,11 +24,9 @@ package cwms.cda.data.dto; -import com.fasterxml.jackson.databind.ObjectMapper; import cwms.cda.api.errors.FieldException; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; -import cwms.cda.formatters.json.JsonV2; import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; @@ -99,9 +97,9 @@ void createProperty_serialize_roundtrip() throws Exception { .withValue("TestValue") .withComment("TestComment") .build(); - String json = Formats.format(new ContentType(Formats.JSONV2), property); - ObjectMapper om = JsonV2.buildObjectMapper(); - Property deserialized = om.readValue(json, Property.class); + ContentType contentType = new ContentType(Formats.JSON); + String json = Formats.format(contentType, property); + Property deserialized = Formats.parseContent(contentType, json, Property.class); assertEquals(property, deserialized, "Property deserialized from JSON doesn't equal original"); } @@ -114,11 +112,11 @@ void createProperty_deserialize() throws Exception { .withValue("TestValue") .withComment("TestComment") .build(); - ObjectMapper om = JsonV2.buildObjectMapper(); InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/property.json"); assertNotNull(resource); String json = IOUtils.toString(resource, StandardCharsets.UTF_8); - Property deserialized = om.readValue(json, Property.class); + ContentType contentType = new ContentType(Formats.JSON); + Property deserialized = Formats.parseContent(contentType, json, Property.class); assertEquals(property, deserialized, "Property deserialized from JSON doesn't equal original"); } } From 4e182b64bdd44ebdb62eef4bb8658de64368c530 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Thu, 30 May 2024 13:15:27 -0700 Subject: [PATCH 13/17] Removed use of JDomLocationLevelImpl because it was converting values and units to SI. Added builder methods minor cleanup Added IT of getAll method with no units, SI units and EN units. Removed JSONv2 units==SI requirement for levels getAll. --- .../java/cwms/cda/api/LevelsController.java | 13 +- .../cda/data/dao/LocationLevelsDaoImpl.java | 331 ++++++++---------- .../java/cwms/cda/data/dto/LocationLevel.java | 47 ++- .../cwms/cda/api/LevelsControllerTestIT.java | 190 +++++++++- 4 files changed, 370 insertions(+), 211 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java b/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java index df67d8d6d..5e37ea63a 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java @@ -223,8 +223,7 @@ public void delete(@NotNull Context ctx, @NotNull String levelId) { + "the default SI units for their parameters." + "\n* `Other` " + "Any unit returned in the response to the units URI request that is " - + "appropriate for the requested parameters. The " + Formats.JSONV2 - + " format currently only supports SI."), + + "appropriate for the requested parameters. "), @OpenApiParam(name = DATUM, description = "Specifies the elevation datum of" + " the response. This field affects only elevation location levels. " + "Valid values for this field are:" @@ -284,7 +283,7 @@ public void getAll(@NotNull Context ctx) { name(LevelsController.class.getName(), GET_ALL)); String office = ctx.queryParam(OFFICE); - String unit = ctx.queryParam(UNIT); + String unit = ctx.queryParamAsClass(UNIT, String.class).getOrDefault(UnitSystem.SI.getValue()); String datum = ctx.queryParam(DATUM); String begin = ctx.queryParam(BEGIN); String end = ctx.queryParam(END); @@ -294,14 +293,6 @@ public void getAll(@NotNull Context ctx) { if ("2".equals(version)) { - if (unit == null) { - // The dao currently only supports SI. - unit = UnitSystem.SI.getValue(); - } - if (!UnitSystem.SI.getValue().equals(unit)) { - throw new IllegalArgumentException("Levels Version 2 currently only supports SI"); - } - String cursor = ctx.queryParamAsClass(PAGE, String.class) .getOrDefault(""); int pageSize = ctx.queryParamAsClass(PAGE_SIZE, Integer.class) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationLevelsDaoImpl.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationLevelsDaoImpl.java index 86e203326..3053f6552 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationLevelsDaoImpl.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationLevelsDaoImpl.java @@ -37,23 +37,20 @@ import cwms.cda.data.dto.LocationLevels; import cwms.cda.data.dto.SeasonalValueBean; import cwms.cda.data.dto.TimeSeries; -import hec.data.DataSetException; -import hec.data.DataSetIllegalArgumentException; +import hec.data.Duration; import hec.data.Parameter; +import hec.data.ParameterType; import hec.data.Units; -import hec.data.UnitsConversionException; import hec.data.level.IAttributeParameterTypedValue; import hec.data.level.ILocationLevelRef; -import hec.data.level.ISeasonalValue; -import hec.data.level.JDomLocationLevelImpl; +import hec.data.level.IParameterTypedValue; +import hec.data.level.ISpecifiedLevel; +import hec.data.level.JDomLocationLevelRef; import hec.data.level.JDomSeasonalIntervalImpl; import hec.data.level.JDomSeasonalValueImpl; -import hec.data.level.JDomSeasonalValuesImpl; import hec.data.location.LocationTemplate; import java.math.BigDecimal; import java.math.BigInteger; -import java.math.MathContext; -import java.math.RoundingMode; import java.sql.Timestamp; import java.time.Instant; import java.time.ZoneId; @@ -70,7 +67,6 @@ import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.regex.Matcher; import java.util.regex.Pattern; import mil.army.usace.hec.metadata.Interval; import mil.army.usace.hec.metadata.IntervalFactory; @@ -84,9 +80,9 @@ import org.jooq.Record1; import org.jooq.SelectLimitPercentAfterOffsetStep; import org.jooq.TableField; +import org.jooq.conf.ParamType; import org.jooq.exception.DataAccessException; import org.jooq.impl.DSL; -import org.jooq.types.DayToSecond; import usace.cwms.db.dao.ifc.level.CwmsDbLevel; import usace.cwms.db.dao.ifc.level.LocationLevelPojo; import usace.cwms.db.dao.util.OracleTypeMap; @@ -143,8 +139,6 @@ public LocationLevels getLocationLevels(String cursor, int pageSize, usace.cwms.db.jooq.codegen.tables.AV_LOCATION_LEVEL view = AV_LOCATION_LEVEL; - - // Only supports SI for now. Controller will throw an exception if not SI. Condition whereCondition = DSL.upper(view.UNIT_SYSTEM).eq(unit.toUpperCase()); if (office != null && !office.isEmpty()) { @@ -165,7 +159,7 @@ public LocationLevels getLocationLevels(String cursor, int pageSize, Timestamp.from(endZdt.toInstant()))); } - Map levelMap = new LinkedHashMap<>(); + Map builderMap = new LinkedHashMap<>(); SelectLimitPercentAfterOffsetStep query = dsl.selectDistinct(getAddSeasonalValueFields()) .from(view) @@ -176,14 +170,13 @@ public LocationLevels getLocationLevels(String cursor, int pageSize, .offset(offset) .limit(pageSize); - // logger.fine(() -> "getLocationLevels query: " + query.getSQL(ParamType.INLINED)); + logger.info(() -> "getLocationLevels query: " + query.getSQL(ParamType.INLINED)); - query.stream().forEach(r -> addSeasonalValue(r, levelMap)); + query.stream().forEach(r -> addSeasonalValue(r, builderMap)); List levels = new java.util.ArrayList<>(); - for (JDomLocationLevelImpl levelImpl : levelMap.values()) { - LocationLevel level = new LocationLevel.Builder(levelImpl).build(); - levels.add(level); + for (LocationLevel.Builder builder : builderMap.values()) { + levels.add(builder.build()); } LocationLevels.Builder builder = new LocationLevels.Builder(offset, pageSize, total); @@ -191,6 +184,40 @@ public LocationLevels getLocationLevels(String cursor, int pageSize, return builder.build(); } + private static class LevelLookup { + private final JDomLocationLevelRef locationLevelRef; + private final Date effectiveDate; + + public LevelLookup(String officeId, String locLevelId, String attributeId, String attributeValue, String attributeUnits, Date effectiveDate) { + this(new JDomLocationLevelRef(officeId, locLevelId, attributeId, attributeValue, attributeUnits), effectiveDate); + } + + public LevelLookup(JDomLocationLevelRef locationLevelRef, Date effectiveDate) { + this.locationLevelRef = locationLevelRef; + this.effectiveDate = effectiveDate; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + LevelLookup that = (LevelLookup) o; + return Objects.equals(locationLevelRef, that.locationLevelRef) && Objects.equals(effectiveDate, that.effectiveDate); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(locationLevelRef); + result = 31 * result + Objects.hashCode(effectiveDate); + return result; + } + } + @Override public void storeLocationLevel(LocationLevel locationLevel, ZoneId zoneId) { @@ -283,22 +310,20 @@ public static SeasonalValueBean buildSeasonalValue(usace.cwms.db.dao.ifc.level.S } @NotNull - private static JDomSeasonalValueImpl buildSeasonalValue(JDomLocationLevelImpl locationLevelImpl, - String levelSiUnit, Double seasLevel, - JDomSeasonalIntervalImpl newSeasonalOffset) { + private static JDomSeasonalValueImpl buildSeasonalValue(String levelSiUnit, Double seasLevel, + JDomSeasonalIntervalImpl newSeasonalOffset, IParameterTypedValue prototypeLevel, String parameterUnits) { // create new seasonal value with current record information JDomSeasonalValueImpl newSeasonalValue = new JDomSeasonalValueImpl(); newSeasonalValue.setOffset(newSeasonalOffset); - newSeasonalValue.setPrototypeParameterType(locationLevelImpl.getPrototypeLevel()); + newSeasonalValue.setPrototypeParameterType(prototypeLevel); // make sure that it is in the correct units. - String parameterUnits = locationLevelImpl.getParameter().getUnitsString(); if (Units.canConvertBetweenUnits(levelSiUnit, parameterUnits)) { seasLevel = Units.convertUnits(seasLevel, levelSiUnit, parameterUnits); // constant value newSeasonalValue.setSiParameterUnitsValue(seasLevel); - locationLevelImpl.setUnits(parameterUnits); + //locationLevelImpl.setUnits(parameterUnits); // pretty sure we don't have to do this. } else { newSeasonalValue.setSiParameterUnitsValue(seasLevel); } @@ -403,8 +428,8 @@ private LocationLevel getLevelFromPojo(LocationLevelPojo copyFromPojo, // These are all the fields that we need to pull out of jOOQ record for addSeasonalValue - private Collection getAddSeasonalValueFields() { - Set retval = new LinkedHashSet<>(); + private Collection> getAddSeasonalValueFields() { + Set> retval = new LinkedHashSet<>(); retval.add(AV_LOCATION_LEVEL.OFFICE_ID); retval.add(AV_LOCATION_LEVEL.LOCATION_LEVEL_ID); @@ -425,104 +450,133 @@ private Collection getAddSeasonalValueFields() { return retval; } + private void addSeasonalValue(Record r, - Map levelMap) { + Map builderMap) { usace.cwms.db.jooq.codegen.tables.AV_LOCATION_LEVEL view = AV_LOCATION_LEVEL; - JDomLocationLevelImpl locationLevelImpl = buildLocationLevel(r); - - String levelSiUnit = r.get(view.LEVEL_UNIT); + Timestamp levelDateTimestamp = r.get(view.LEVEL_DATE); + String attrId = r.get(view.ATTRIBUTE_ID); + Double oattrVal = r.get(view.ATTRIBUTE_VALUE); + String locLevelId = r.get(view.LOCATION_LEVEL_ID); + String officeId = r.get(view.OFFICE_ID); + String levelUnit = r.get(view.LEVEL_UNIT); + String attrUnit = r.get(AV_LOCATION_LEVEL.ATTRIBUTE_UNIT); - String interp = r.get(view.INTERPOLATE); - Boolean boolInterp = null; - if (interp != null) { - boolInterp = OracleTypeMap.parseBool(interp); + Date levelDate = null; + if (levelDateTimestamp != null) { + levelDate = new Date(levelDateTimestamp.getTime()); } - // set interpolated value - locationLevelImpl.setInterpolateSeasonal(boolInterp); - String levelComment = r.get(view.LEVEL_COMMENT); - if (levelMap.containsKey(locationLevelImpl)) { - locationLevelImpl = levelMap.get(locationLevelImpl); - } else { - levelMap.put(locationLevelImpl, locationLevelImpl); - - // always SI parameter units - Double constLevel = r.get(view.CONSTANT_LEVEL); - String attrComment = r.get(view.ATTRIBUTE_COMMENT); - - setLevelData(r.get(view.TSID), constLevel, attrComment, locationLevelImpl, - levelSiUnit, levelComment); + String attrStr = null; + if (oattrVal != null) { + attrStr = oattrVal.toString(); // this is weird. allow it for now but maybe this should be doing some rounding? } + JDomLocationLevelRef locationLevelRef = new JDomLocationLevelRef(officeId, locLevelId, attrId, attrStr, attrUnit); + LevelLookup levelLookup = new LevelLookup(locationLevelRef, levelDate); - // seasonal stuff - Timestamp intervalOriginDateTimeStamp = r.get(view.INTERVAL_ORIGIN); - Date intervalOriginDate = null; - if (intervalOriginDateTimeStamp != null) { - intervalOriginDate = new Date(intervalOriginDateTimeStamp.getTime()); - } - - parseSeasonalValues(r, intervalOriginDate, locationLevelImpl, levelSiUnit); - } + LocationLevel.Builder builder; + if (builderMap.containsKey(levelLookup)) { + builder = builderMap.get(levelLookup); + } else { + ZonedDateTime levelZdt = null; + if (levelDate != null) { + levelZdt = ZonedDateTime.ofInstant(levelDate.toInstant(), ZoneId.of("UTC")); + } + builder = new LocationLevel.Builder(locLevelId, levelZdt); + builder = withLocationLevelRef(builder, locationLevelRef); - @NotNull - private JDomLocationLevelImpl buildLocationLevel(Record r) { - usace.cwms.db.jooq.codegen.tables.AV_LOCATION_LEVEL view = AV_LOCATION_LEVEL; + builder.withAttributeParameterId(attrId); + builder.withAttributeUnitsId(attrUnit); + builder.withLevelUnitsId(levelUnit); - Timestamp levelDateTimestamp = r.get(view.LEVEL_DATE); + if (oattrVal != null) { + builder.withAttributeValue(BigDecimal.valueOf(oattrVal)); + } + builder.withLevelComment(r.get(view.LEVEL_COMMENT)); + builder.withAttributeComment(r.get(view.ATTRIBUTE_COMMENT)); + builder.withConstantValue(r.get(view.CONSTANT_LEVEL)); + builder.withSeasonalTimeSeriesId(r.get(view.TSID)); - Date levelDate = null; - if (levelDateTimestamp != null) { - levelDate = new Date(levelDateTimestamp.getTime()); + builderMap.put(levelLookup, builder); } - String attrId = r.get(view.ATTRIBUTE_ID); - Double oattrVal = r.get(view.ATTRIBUTE_VALUE); - StringValueUnits attrVal = parseAttributeValue(r, attrId, oattrVal); - String locLevelId = r.get(view.LOCATION_LEVEL_ID); - - String officeId = r.get(view.OFFICE_ID); - String levelSiUnit = r.get(view.LEVEL_UNIT); + String interp = r.get(view.INTERPOLATE); + builder.withInterpolateString(interp); - // built to compare the loc level ref and eff date. + Double seasonalLevel = r.get(view.SEASONAL_LEVEL); - return new JDomLocationLevelImpl(officeId, locLevelId, - levelDate, levelSiUnit, attrId, attrVal.value, attrVal.units); + if (seasonalLevel != null) { +// JDomSeasonalValuesImpl seasonalValuesImpl = new JDomSeasonalValuesImpl(); +// +// Timestamp intervalOriginDateTimeStamp = r.get(view.INTERVAL_ORIGIN); +// // seasonal stuff +// Date intervalOriginDate = null; +// if (intervalOriginDateTimeStamp != null) { +// intervalOriginDate = new Date(intervalOriginDateTimeStamp.getTime()); +// } +// +// seasonalValuesImpl.setOrigin(intervalOriginDate); +// +// String calInterval = r.get(view.CALENDAR_INTERVAL); +// DayToSecond dayToSecond = r.get(view.TIME_INTERVAL); +// JDomSeasonalIntervalImpl offset = new JDomSeasonalIntervalImpl(); +// offset.setYearMonthString(calInterval); +// if (dayToSecond != null) { +// offset.setDaysHoursMinutesString(dayToSecond.toString()); +// } +// seasonalValuesImpl.setOffset(offset); + // TODO: LocationLevel is missing seasonal origin and offset. + + String calOffset = r.get(view.CALENDAR_OFFSET); + String timeOffset = r.get(view.TIME_OFFSET); + JDomSeasonalIntervalImpl newSeasonalOffset = buildSeasonalOffset(calOffset, timeOffset); + SeasonalValueBean seasonalValue = buildSeasonalValueBean(seasonalLevel, newSeasonalOffset) ; + builder.withSeasonalValue(seasonalValue); + } } - private void setLevelData(String tsid, Double constLevel, String attrComment, - JDomLocationLevelImpl locationLevelImpl, String levelSiUnit, - String levelComment) throws UnitsConversionException { - locationLevelImpl.setLevelComment(levelComment); + private LocationLevel.Builder withLocationLevelRef(LocationLevel.Builder builder, JDomLocationLevelRef locationLevelRef) { + ISpecifiedLevel specifiedLevel = locationLevelRef.getSpecifiedLevel(); + if (specifiedLevel != null) { + builder = builder.withSpecifiedLevelId(specifiedLevel.getId()); + } - if (locationLevelImpl.getLocationLevelRef().getAttribute() != null) { - locationLevelImpl.getLocationLevelRef().getAttribute().setComment(attrComment); + Parameter parameter = locationLevelRef.getParameter(); + if (parameter != null) { + builder = builder.withParameterId(parameter.toString()); } - // set the level value - if (constLevel != null) { - // make sure that it is in the correct units. - String parameterUnits = locationLevelImpl.getParameter().getUnitsString(); - if (Units.canConvertBetweenUnits(levelSiUnit, parameterUnits)) { - constLevel = Units.convertUnits(constLevel, levelSiUnit, parameterUnits); - // constant value - locationLevelImpl.setSiParameterUnitsConstantValue(constLevel); - locationLevelImpl.setUnits(parameterUnits); - } else { - locationLevelImpl.setSiParameterUnitsConstantValue(constLevel); - } + + ParameterType parameterType = locationLevelRef.getParameterType(); + if (parameterType != null) { + builder = builder.withParameterTypeId(parameterType.toString()); } - // seasonal time series - if (tsid != null) { - locationLevelImpl.setSeasonalTimeSeriesId(tsid); + Duration duration = locationLevelRef.getDuration(); + if (duration != null) { + builder = builder.withDurationId(duration.toString()); } + + + return builder + .withOfficeId(locationLevelRef.getOfficeId()) + ; + } + + private SeasonalValueBean buildSeasonalValueBean(Double seasonalLevel, + JDomSeasonalIntervalImpl offset) { + // Avoiding JDomSeasonalValueImpl b/c it does units conversion to SI. + return new SeasonalValueBean.Builder(seasonalLevel) + .withOffsetMinutes(BigInteger.valueOf(offset.getTotalMinutes())) + .withOffsetMonths(offset.getTotalMonths()) + .build(); } // These are all the fields that we need to pull out of jOOQ record for parseSeasonalValues - private Collection getParseSeasonalValuesFields() { - Set retval = new LinkedHashSet<>(); + private Collection> getParseSeasonalValuesFields() { + Set> retval = new LinkedHashSet<>(); retval.add(AV_LOCATION_LEVEL.SEASONAL_LEVEL); retval.add(AV_LOCATION_LEVEL.CALENDAR_INTERVAL); @@ -534,44 +588,6 @@ private Collection getParseSeasonalValuesFields() { } - private void parseSeasonalValues(Record rs, Date intervalOriginDate, - JDomLocationLevelImpl locationLevelImpl, String levelSiUnit) - throws DataSetException { - usace.cwms.db.jooq.codegen.tables.AV_LOCATION_LEVEL view = AV_LOCATION_LEVEL; - // seasonal val - Double seasonalLevel = rs.get(view.SEASONAL_LEVEL); - - if (seasonalLevel != null) { - // retrieve existing seasonal value stuff - JDomSeasonalValuesImpl seasonalValuesImpl = locationLevelImpl.getSeasonalValuesObject(); - if (seasonalValuesImpl == null) { - seasonalValuesImpl = new JDomSeasonalValuesImpl(); - seasonalValuesImpl.setOrigin(intervalOriginDate); - JDomSeasonalIntervalImpl offset = new JDomSeasonalIntervalImpl(); - String calInterval = rs.get(view.CALENDAR_INTERVAL); - offset.setYearMonthString(calInterval); - DayToSecond dayToSecond = rs.get(view.TIME_INTERVAL); - if (dayToSecond != null) { - offset.setDaysHoursMinutesString(dayToSecond.toString()); - } - seasonalValuesImpl.setOffset(offset); - locationLevelImpl.setSeasonalValuesObject(seasonalValuesImpl); - } - - // retrieve list of existing seasonal values - List seasonalValues = seasonalValuesImpl.getSeasonalValues(); - - String calOffset = rs.get(view.CALENDAR_OFFSET); - String timeOffset = rs.get(view.TIME_OFFSET); - JDomSeasonalIntervalImpl newSeasonalOffset = buildSeasonalOffset(calOffset, timeOffset); - JDomSeasonalValueImpl newSeasonalValue = buildSeasonalValue(locationLevelImpl, - levelSiUnit, seasonalLevel, newSeasonalOffset); - // add new seasonal value to existing seasonal values - seasonalValues.add(newSeasonalValue); - seasonalValuesImpl.setSeasonalValues(seasonalValues); - } - } - @NotNull private static JDomSeasonalIntervalImpl buildSeasonalOffset(String calOffset, String timeOffset) { @@ -582,51 +598,6 @@ private static JDomSeasonalIntervalImpl buildSeasonalOffset(String calOffset, return newSeasonalOffset; } - private static class StringValueUnits { - String value; - String units; - } - - private StringValueUnits parseAttributeValue(Record rs, String attrId, Double oattrVal) { - // query pulls SI parameter units - String attrSiUnit = rs.get(AV_LOCATION_LEVEL.ATTRIBUTE_UNIT); - StringValueUnits attrVal = new StringValueUnits(); - if (attrId != null) { - // we want are attributes in en parameter units. - // this should be done via an oracle procedure call. - Matcher matcher = attributeIdParsingPattern.matcher(attrId); - if (!matcher.matches() || matcher.groupCount() != 3) { - throw new DataSetException("Illegal location level attribute identifier: " + attrId); - } - // Flow.Max.6Hours - String sattrParam = matcher.group(1); - getEnAttributeValue(attrVal, oattrVal, attrSiUnit, sattrParam); - } - return attrVal; - } - - private void getEnAttributeValue(StringValueUnits stringValueUnits, Double oattrVal, - String attrSiUnit, - String parameterId) throws DataSetIllegalArgumentException, - UnitsConversionException { - String attrEnUnit = null; - String attrVal = null; - if (oattrVal != null && attrSiUnit != null && parameterId != null) { - Parameter parameter = new Parameter(parameterId); - attrEnUnit = parameter.getUnitsStringForSystem(Units.ENGLISH_ID); - double siAttrVal = ((Number) oattrVal).doubleValue(); - if (Units.canConvertBetweenUnits(attrSiUnit, attrEnUnit)) { - double enSiVal = Units.convertUnits(siAttrVal, attrSiUnit, attrEnUnit); - BigDecimal bd = BigDecimal.valueOf(enSiVal); - BigDecimal setScale = bd.setScale(9, RoundingMode.HALF_UP); - BigDecimal round = setScale.round(new MathContext(9, RoundingMode.HALF_UP)); - attrVal = round.toPlainString(); - } - } - stringValueUnits.value = attrVal; - stringValueUnits.units = attrEnUnit; - } - @Override public TimeSeries retrieveLocationLevelAsTimeSeries(ILocationLevelRef levelRef, Instant start, Instant end, diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/LocationLevel.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/LocationLevel.java index 5c009196f..93af9da31 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/LocationLevel.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/LocationLevel.java @@ -291,10 +291,8 @@ public Builder(JDomLocationLevelImpl copyFrom) { withParameterId(copyFrom.getParameterId()); withParameterTypeId(copyFrom.getParameterTypeId()); withSeasonalTimeSeriesId(copyFrom.getSeasonalTimeSeriesId()); - ISeasonalValues values = copyFrom.getSeasonalValues(); - if (values != null) { - withSeasonalValues(buildSeasonalValues(values)); - } + withSeasonalValues(copyFrom.getSeasonalValues()); + IParameterTypedValue constantLevel = copyFrom.getConstantLevel(); if (constantLevel != null) { withConstantValue(constantLevel.getSiParameterUnitsValue()); @@ -374,13 +372,42 @@ public Builder withSeasonalValues(List seasonalValues) { return this; } + public Builder withSeasonalValues(ISeasonalValues values) { + if (values != null) { + // TODO: handle values.offset and values.origin + withSeasonalValues(buildSeasonalValues(values)); + } else { + this.seasonalValues = null; + } + + return this; + } + + public Builder withSeasonalValue(SeasonalValueBean seasonalValue) { + if (seasonalValues == null) { + seasonalValues = new ArrayList<>(); + } + seasonalValues.add(seasonalValue); + return this; + } + public static SeasonalValueBean buildSeasonalValueBean(ISeasonalValue seasonalValue) { - ISeasonalInterval offset = seasonalValue.getOffset(); - IParameterTypedValue value = seasonalValue.getValue(); - return new SeasonalValueBean.Builder(value.getSiParameterUnitsValue()) - .withOffsetMinutes(BigInteger.valueOf(offset.getTotalMinutes())) - .withOffsetMonths(offset.getTotalMonths()) - .build(); + SeasonalValueBean retval = null; + if (seasonalValue != null) { + IParameterTypedValue value = seasonalValue.getValue(); + + if(value != null){ + SeasonalValueBean.Builder builder = new SeasonalValueBean.Builder(value.getSiParameterUnitsValue()); + + ISeasonalInterval offset = seasonalValue.getOffset(); + if(offset != null){ + builder.withOffsetMinutes(BigInteger.valueOf(offset.getTotalMinutes())) + .withOffsetMonths(offset.getTotalMonths()); + } + retval = builder.build(); + } + } + return retval; } public static List buildSeasonalValues(ISeasonalValues seasonalValues) { diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java index 70aa1f93e..858ae7501 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java @@ -32,12 +32,13 @@ import fixtures.TestAccounts; import io.restassured.filter.log.LogDetail; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; import org.jooq.DSLContext; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import javax.servlet.http.HttpServletResponse; -import java.sql.Connection; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -47,6 +48,7 @@ import static cwms.cda.api.Controllers.*; import static io.restassured.RestAssured.given; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -66,7 +68,7 @@ void test_location_level() throws Exception { .withLevelUnitsId("ac-ft") .build(); CwmsDataApiSetupCallback.getDatabaseLink().connection(c -> { - DSLContext dsl = dslContext((Connection) c, OFFICE); + DSLContext dsl = dslContext(c, OFFICE); LocationLevelsDaoImpl dao = new LocationLevelsDaoImpl(dsl); dao.storeLocationLevel(level, level.getLevelDate().getZone()); }); @@ -86,11 +88,11 @@ void test_location_level() throws Exception { .assertThat() .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_OK)) - .body("level-units-id",equalTo("m3")) + .body("level-units-id", equalTo("m3")) // I think we need to create a custom matcher. // This really shouldn't use equals but due to a quirk in // RestAssured it appears to be necessary. - .body("constant-value",equalTo(1233.4818f)); // 1 ac-ft to m3 + .body("constant-value", equalTo(1233.4818f)); // 1 ac-ft to m3 given() .log().ifValidationFails(LogDetail.ALL,true) @@ -129,7 +131,7 @@ void test_level_as_timeseries() throws Exception { .build(); levels.put(level.getLevelDate().toInstant(), level); CwmsDataApiSetupCallback.getDatabaseLink().connection(c -> { - DSLContext dsl = dslContext((Connection) c, OFFICE); + DSLContext dsl = dslContext(c, OFFICE); LocationLevelsDaoImpl dao = new LocationLevelsDaoImpl(dsl); dao.storeLocationLevel(level, level.getLevelDate().getZone()); }); @@ -165,13 +167,181 @@ void test_level_as_timeseries() throws Exception { assertEquals(24 * effectiveDateCount + 1, timeSeries.getTotal()); List values = timeSeries.getValues(); for (int i = 0; i < values.size(); i++) { - TimeSeries.Record record = values.get(i); - assertEquals(time.plusHours(i).toInstant(), record.getDateTime().toInstant(), "Time check failed at iteration: " + i); - assertEquals(0, record.getQualityCode(), "Quality check failed at iteration: " + i); - Double constantValue = levels.floorEntry(record.getDateTime().toInstant()) + TimeSeries.Record tsrec = values.get(i); + assertEquals(time.plusHours(i).toInstant(), tsrec.getDateTime().toInstant(), "Time check failed at iteration: " + i); + assertEquals(0, tsrec.getQualityCode(), "Quality check failed at iteration: " + i); + Double constantValue = levels.floorEntry(tsrec.getDateTime().toInstant()) .getValue() .getConstantValue(); - assertEquals(constantValue, record.getValue(), 0.0001, "Value check failed at iteration: " + i); + assertEquals(constantValue, tsrec.getValue(), 0.0001, "Value check failed at iteration: " + i); } } + + + @Test + void test_get_all_location_level() throws Exception { + String locId = "level_get_all_loc1"; + String levelId = locId + ".Stor.Ave.1Day.Regulating"; + createLocation(locId, true, OFFICE); + final ZonedDateTime time = ZonedDateTime.of(2023, 6, 1, 0, 0, 0, 0, ZoneId.of("America" + + "/Los_Angeles")); + CwmsDataApiSetupCallback.getDatabaseLink().connection(c -> { + LocationLevel level = new LocationLevel.Builder(levelId, time) + .withOfficeId(OFFICE) + .withConstantValue(1.0) + .withLevelUnitsId("ac-ft") + .build(); + DSLContext dsl = dslContext(c, OFFICE); + LocationLevelsDaoImpl dao = new LocationLevelsDaoImpl(dsl); + dao.storeLocationLevel(level, level.getLevelDate().getZone()); + }); + + String locId2 = "level_get_all_loc2"; + String levelId2 = locId2 + ".Stor.Ave.1Day.Regulating"; + createLocation(locId2, true, OFFICE); + CwmsDataApiSetupCallback.getDatabaseLink().connection(c -> { + + LocationLevel level = new LocationLevel.Builder(levelId2, time) + .withOfficeId(OFFICE) + .withConstantValue(2.0) + .withLevelUnitsId("ac-ft") + .build(); + DSLContext dsl = dslContext(c, OFFICE); + LocationLevelsDaoImpl dao = new LocationLevelsDaoImpl(dsl); + dao.storeLocationLevel(level, level.getLevelDate().getZone()); + }); + + String startStr = "2023-06-01T00:00:00Z"; + String endStr = "2023-06-02T00:00:00Z"; + + + + //Read level without unit + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .queryParam("office", OFFICE) + .queryParam(LEVEL_ID_MASK, "level_get_all.*") + .queryParam(BEGIN, startStr) + .queryParam(END, endStr) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/levels/") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .extract(); + + assertThat(response.path("levels.size()"),is(2)); + + assertThat(response.path("levels[0].office-id"),equalTo(OFFICE)); + assertThat(response.path("levels[0].location-level-id"),equalTo(levelId)); + assertThat(response.path("levels[0].specified-level-id"),equalTo("Regulating")); + assertThat(response.path("levels[0].parameter-type-id"),equalTo("Ave")); + assertThat(response.path("levels[0].parameter-id"),equalTo("Stor")); + assertThat(response.path("levels[0].level-units-id"),equalTo("m3")); + assertThat(response.path("levels[0].level-date"),equalTo("2023-06-01T07:00:00Z")); + assertThat(response.path("levels[0].duration-id"),equalTo("1Day")); + double actual0 = Float.valueOf((float) response.path("levels[0].constant-value")).doubleValue(); + assertThat(actual0, closeTo(1233.0, 10.0)); + + assertThat(response.path("levels[1].office-id"),equalTo(OFFICE)); + assertThat(response.path("levels[1].location-level-id"),equalTo(levelId2)); + assertThat(response.path("levels[1].specified-level-id"),equalTo("Regulating")); + assertThat(response.path("levels[1].parameter-type-id"),equalTo("Ave")); + assertThat(response.path("levels[1].parameter-id"),equalTo("Stor")); + assertThat(response.path("levels[1].level-units-id"),equalTo("m3")); + assertThat(response.path("levels[1].level-date"),equalTo("2023-06-01T07:00:00Z")); + assertThat(response.path("levels[1].duration-id"),equalTo("1Day")); + double actual1 = Float.valueOf((float) response.path("levels[1].constant-value")).doubleValue(); + assertThat(actual1, closeTo(2466.9636f, 1.0)); + + response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .queryParam("office", OFFICE) + .queryParam(UNIT, "SI") + .queryParam(LEVEL_ID_MASK, "level_get_all.*") + .queryParam(BEGIN, startStr) + .queryParam(END, endStr) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/levels/") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .extract(); + assertThat(response.path("levels.size()"),is(2)); + + assertThat(response.path("levels[0].office-id"),equalTo(OFFICE)); + assertThat(response.path("levels[0].location-level-id"),equalTo(levelId)); + assertThat(response.path("levels[0].specified-level-id"),equalTo("Regulating")); + assertThat(response.path("levels[0].parameter-type-id"),equalTo("Ave")); + assertThat(response.path("levels[0].parameter-id"),equalTo("Stor")); + assertThat(response.path("levels[0].level-units-id"),equalTo("m3")); + assertThat(response.path("levels[0].level-date"),equalTo("2023-06-01T07:00:00Z")); + assertThat(response.path("levels[0].duration-id"),equalTo("1Day")); + actual0 = Float.valueOf((float) response.path("levels[0].constant-value")).doubleValue(); + assertThat(actual0, closeTo(1233.4818, 1.0)); + + assertThat(response.path("levels[1].office-id"),equalTo(OFFICE)); + assertThat(response.path("levels[1].location-level-id"),equalTo(levelId2)); + assertThat(response.path("levels[1].specified-level-id"),equalTo("Regulating")); + assertThat(response.path("levels[1].parameter-type-id"),equalTo("Ave")); + assertThat(response.path("levels[1].parameter-id"),equalTo("Stor")); + assertThat(response.path("levels[1].level-units-id"),equalTo("m3")); + assertThat(response.path("levels[1].level-date"),equalTo("2023-06-01T07:00:00Z")); + assertThat(response.path("levels[1].duration-id"),equalTo("1Day")); + actual1 = Float.valueOf((float) response.path("levels[1].constant-value")).doubleValue(); + assertThat(actual1, closeTo(2466.9636, 1.0)); + + response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .queryParam("office", OFFICE) + .queryParam(UNIT, "EN") + .queryParam(LEVEL_ID_MASK, "level_get_all.*") + .queryParam(BEGIN, startStr) + .queryParam(END, endStr) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/levels/") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .extract(); + assertThat(response.path("levels.size()"),is(2)); + + assertThat(response.path("levels[0].office-id"),equalTo(OFFICE)); + assertThat(response.path("levels[0].location-level-id"),equalTo(levelId)); + assertThat(response.path("levels[0].specified-level-id"),equalTo("Regulating")); + assertThat(response.path("levels[0].parameter-type-id"),equalTo("Ave")); + assertThat(response.path("levels[0].parameter-id"),equalTo("Stor")); + assertThat(response.path("levels[0].level-units-id"),equalTo("ac-ft")); + assertThat(response.path("levels[0].level-date"),equalTo("2023-06-01T07:00:00Z")); + assertThat(response.path("levels[0].duration-id"),equalTo("1Day")); + actual0 = Float.valueOf((float) response.path("levels[0].constant-value")).doubleValue(); + assertThat(actual0, closeTo(1.0, 0.01)); + + assertThat(response.path("levels[1].office-id"),equalTo(OFFICE)); + assertThat(response.path("levels[1].location-level-id"),equalTo(levelId2)); + assertThat(response.path("levels[1].specified-level-id"),equalTo("Regulating")); + assertThat(response.path("levels[1].parameter-type-id"),equalTo("Ave")); + assertThat(response.path("levels[1].parameter-id"),equalTo("Stor")); + assertThat(response.path("levels[1].level-units-id"),equalTo("ac-ft")); + assertThat(response.path("levels[1].level-date"),equalTo("2023-06-01T07:00:00Z")); + assertThat(response.path("levels[1].duration-id"),equalTo("1Day")); + actual1 = Float.valueOf((float) response.path("levels[1].constant-value")).doubleValue(); + assertThat(actual1, closeTo(2.0, 0.01)); + } + } From da47d8d2d4ac9043f11ded324f5c799b241b8e62 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Fri, 31 May 2024 10:55:17 -0700 Subject: [PATCH 14/17] Renamed withSeasonalValues method and marked as JsonIgnore to fix Jackson error. --- .../java/cwms/cda/data/dto/LocationLevel.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/LocationLevel.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/LocationLevel.java index 93af9da31..55d4681db 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/LocationLevel.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/LocationLevel.java @@ -50,8 +50,8 @@ public final class LocationLevel extends CwmsDTO { @Schema(description = "Generic name of this location level. Common names are 'Top of Dam', " + "'Streambed', 'Bottom of Dam'.") private final String specifiedLevelId; - @Schema(description = "To indicate if single or aggregate value", allowableValues = {"Inst", - "Ave", "Min", "Max", "Total"}) + @Schema(description = "To indicate if single or aggregate value", + allowableValues = {"Inst", "Ave", "Min", "Max", "Total"}) private final String parameterTypeId; @Schema(description = "Data Type such as Stage, Elevation, or others.") private final String parameterId; @@ -291,7 +291,7 @@ public Builder(JDomLocationLevelImpl copyFrom) { withParameterId(copyFrom.getParameterId()); withParameterTypeId(copyFrom.getParameterTypeId()); withSeasonalTimeSeriesId(copyFrom.getSeasonalTimeSeriesId()); - withSeasonalValues(copyFrom.getSeasonalValues()); + withISeasonalValues(copyFrom.getSeasonalValues()); IParameterTypedValue constantLevel = copyFrom.getConstantLevel(); if (constantLevel != null) { @@ -372,7 +372,8 @@ public Builder withSeasonalValues(List seasonalValues) { return this; } - public Builder withSeasonalValues(ISeasonalValues values) { + @JsonIgnore + public Builder withISeasonalValues(ISeasonalValues values) { if (values != null) { // TODO: handle values.offset and values.origin withSeasonalValues(buildSeasonalValues(values)); @@ -396,11 +397,12 @@ public static SeasonalValueBean buildSeasonalValueBean(ISeasonalValue seasonalVa if (seasonalValue != null) { IParameterTypedValue value = seasonalValue.getValue(); - if(value != null){ - SeasonalValueBean.Builder builder = new SeasonalValueBean.Builder(value.getSiParameterUnitsValue()); + if (value != null) { + SeasonalValueBean.Builder builder = + new SeasonalValueBean.Builder(value.getSiParameterUnitsValue()); - ISeasonalInterval offset = seasonalValue.getOffset(); - if(offset != null){ + ISeasonalInterval offset = seasonalValue.getOffset(); + if (offset != null) { builder.withOffsetMinutes(BigInteger.valueOf(offset.getTotalMinutes())) .withOffsetMonths(offset.getTotalMonths()); } From ae447765e7634f2c1efc23a5b7d0ad7f4d89ad48 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Wed, 5 Jun 2024 09:28:58 -0700 Subject: [PATCH 15/17] Changed JSONV2 to JSON --- cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java | 2 +- cwms-data-api/src/test/java/cwms/cda/data/dto/ProjectTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java index 7c59c55f7..0139530c4 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java @@ -15,7 +15,7 @@ @JsonDeserialize(builder = Project.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) -@FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class) +@FormattableWith(contentType = Formats.JSON, formatter = JsonV2.class) public class Project extends CwmsDTO { private final String name; 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 fcb177f42..3dabc9347 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 @@ -14,7 +14,7 @@ import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; -public class ProjectTest { +class ProjectTest { @Test From 647aaa337ca679030323c7072614590c0661b611 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:23:23 -0700 Subject: [PATCH 16/17] Switching to Location for the NearGage and PumpBack. Making it possible to build a Location with null PublicName and Active so that it can be serialized as just office and id. --- .../main/java/cwms/cda/data/dto/Location.java | 9 ++- .../main/java/cwms/cda/data/dto/Project.java | 58 ++++++------------- .../java/cwms/cda/data/dto/ProjectTest.java | 29 ++++++---- .../resources/cwms/cda/data/dto/project.json | 12 ++-- 4 files changed, 48 insertions(+), 60 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Location.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Location.java index abf2a9e67..8cec624ce 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/Location.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Location.java @@ -15,7 +15,6 @@ import cwms.cda.formatters.annotations.FormattableWith; import cwms.cda.formatters.json.JsonV1; import cwms.cda.formatters.json.JsonV2; - import java.time.ZoneId; import java.util.ArrayList; import java.util.HashMap; @@ -237,7 +236,7 @@ public static class Builder { private Double latitude; private Double longitude; private String officeId; - private boolean active = true; + private Boolean active = true; private String publicName; private String longName; private String description; @@ -277,6 +276,10 @@ public Builder(@JsonProperty(value = "name") String name, @JsonProperty(value = buildPropertyFunctions(); } + public Builder(String office, String name) { + this(name, null, null, null, null, null, office); + } + public Builder(Location location) { this.name = location.getName(); this.latitude = location.getLatitude(); @@ -398,7 +401,7 @@ public Builder withLongName(String longName) { return this; } - public Builder withActive(boolean active) { + public Builder withActive(Boolean active) { this.active = active; return this; } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java index 0139530c4..992b58b3c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java @@ -31,10 +31,8 @@ public class Project extends CwmsDTO { private final String sedimentationDesc; private final String downstreamUrbanDesc; private final String bankFullCapacityDesc; - private final String pumpBackLocationId; - private final String pumpBackOfficeId; - private final String nearGageLocationId; - private final String nearGageOfficeId; + private final Location pumpBack; + private final Location nearGage; private final Instant yieldTimeFrameStart; private final Instant yieldTimeFrameEnd; private final String projectRemarks; @@ -55,10 +53,8 @@ private Project(Project.Builder builder) { this.sedimentationDesc = builder.sedimentationDesc; this.downstreamUrbanDesc = builder.downstreamUrbanDesc; this.bankFullCapacityDesc = builder.bankFullCapacityDesc; - this.pumpBackLocationId = builder.pumpBackLocationId; - this.pumpBackOfficeId = builder.pumpBackOfficeId; - this.nearGageLocationId = builder.nearGageLocationId; - this.nearGageOfficeId = builder.nearGageOfficeId; + this.pumpBack = builder.pumpBack; + this.nearGage = builder.nearGage; this.yieldTimeFrameStart = builder.yieldTimeFrameStart; this.yieldTimeFrameEnd = builder.yieldTimeFrameEnd; this.projectRemarks = builder.projectRemarks; @@ -89,12 +85,8 @@ public String getHydropowerDesc() { return hydropowerDesc; } - public String getNearGageLocationId() { - return nearGageLocationId; - } - - public String getNearGageOfficeId() { - return nearGageOfficeId; + public Location getNearGage() { + return nearGage; } public Double getNonFederalCost() { @@ -129,12 +121,8 @@ public String getProjectRemarks() { return projectRemarks; } - public String getPumpBackLocationId() { - return pumpBackLocationId; - } - - public String getPumpBackOfficeId() { - return pumpBackOfficeId; + public Location getPumpBack() { + return pumpBack; } public String getSedimentationDesc() { @@ -166,10 +154,8 @@ public static class Builder { private String sedimentationDesc; private String downstreamUrbanDesc; private String bankFullCapacityDesc; - private String pumpBackLocationId; - private String pumpBackOfficeId; - private String nearGageLocationId; - private String nearGageOfficeId; + private Location pumpBack; + private Location nearGage; private Instant yieldTimeFrameStart; private Instant yieldTimeFrameEnd; private String projectRemarks; @@ -181,6 +167,7 @@ public Project build() { /** * Copy the values from the given project into this builder. + * * @param project the project to copy values from * @return this builder */ @@ -199,10 +186,8 @@ public Builder from(Project project) { .withSedimentationDesc(project.getSedimentationDesc()) .withDownstreamUrbanDesc(project.getDownstreamUrbanDesc()) .withBankFullCapacityDesc(project.getBankFullCapacityDesc()) - .withPumpBackLocationId(project.getPumpBackLocationId()) - .withPumpBackOfficeId(project.getPumpBackOfficeId()) - .withNearGageLocationId(project.getNearGageLocationId()) - .withNearGageOfficeId(project.getNearGageOfficeId()) + .withPumpBack(project.getPumpBack()) + .withNearGage(project.getNearGage()) .withYieldTimeFrameStart(project.getYieldTimeFrameStart()) .withYieldTimeFrameEnd(project.getYieldTimeFrameEnd()) .withProjectRemarks(project.getProjectRemarks()); @@ -278,18 +263,13 @@ public Builder withBankFullCapacityDesc(String bankFullCapacityDesc) { return this; } - public Builder withPumpBackLocationId(String pumpBackLocationId) { - this.pumpBackLocationId = pumpBackLocationId; + public Builder withPumpBack(Location pbLoc) { + this.pumpBack = pbLoc; return this; } - public Builder withNearGageLocationId(String nearGageLocationId) { - this.nearGageLocationId = nearGageLocationId; - return this; - } - - public Builder withNearGageOfficeId(String nearGageOfficeId) { - this.nearGageOfficeId = nearGageOfficeId; + public Builder withNearGage(Location ngLoc) { + this.nearGage = ngLoc; return this; } @@ -308,10 +288,6 @@ public Builder withProjectRemarks(String projectRemarks) { return this; } - public Builder withPumpBackOfficeId(String spk) { - this.pumpBackOfficeId = spk; - return this; - } } } 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 3dabc9347..bae613019 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 @@ -19,6 +19,16 @@ class ProjectTest { @Test void testProject() throws JsonProcessingException { + + Location pbLoc = new Location.Builder("SPK","Pumpback Location Id") + .withPublicName(null) + .withActive(null) + .build(); + Location ngLoc = new Location.Builder("SPK","Near Gage Location Id") + .withPublicName(null) + .withActive(null) + .build(); + Project project = new Project.Builder() .withOfficeId("SPK") .withName("Project Id") @@ -35,10 +45,8 @@ void testProject() throws JsonProcessingException { .withFederalOAndMCost(10.0) .withNonFederalOAndMCost(5.0) .withProjectRemarks("Remarks") - .withPumpBackLocationId("Pumpback Location Id") - .withPumpBackOfficeId("SPK") - .withNearGageLocationId("Near Gage Location Id") - .withNearGageOfficeId("SPK") + .withPumpBack(pbLoc) + .withNearGage(ngLoc) .withBankFullCapacityDesc("Bank Full Capacity Description") .withDownstreamUrbanDesc("Downstream Urban Description") .withHydropowerDesc("Hydropower Description") @@ -49,8 +57,8 @@ void testProject() throws JsonProcessingException { ObjectWriter ow = om.writerWithDefaultPrettyPrinter(); String json = ow.writeValueAsString(project); - assertNotNull(json); + assertNotNull(json); } @@ -66,7 +74,6 @@ void testDeserialize() throws IOException { assertNotNull(project); - assertEquals("SPK", project.getOfficeId()); assertEquals("Project Id", project.getName()); assertEquals("Project Owner", project.getProjectOwner()); @@ -80,16 +87,14 @@ void testDeserialize() throws IOException { assertEquals(1717199914902L, project.getYieldTimeFrameStart().toEpochMilli()); assertEquals(1717199914902L, project.getYieldTimeFrameEnd().toEpochMilli()); assertEquals("Remarks", project.getProjectRemarks()); - assertEquals("Pumpback Location Id", project.getPumpBackLocationId()); - assertEquals("SPK", project.getPumpBackOfficeId()); - assertEquals("Near Gage Location Id", project.getNearGageLocationId()); - assertEquals("SPK", project.getNearGageOfficeId()); + assertEquals("Pumpback Location Id", project.getPumpBack().getName()); + assertEquals("SPK", project.getPumpBack().getOfficeId()); + assertEquals("Near Gage Location Id", project.getNearGage().getName()); + assertEquals("SPK", project.getNearGage().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/resources/cwms/cda/data/dto/project.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/project.json index 302370870..4021702b7 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/data/dto/project.json +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/project.json @@ -13,10 +13,14 @@ "sedimentation-desc" : "Sedimentation Description", "downstream-urban-desc" : "Downstream Urban Description", "bank-full-capacity-desc" : "Bank Full Capacity Description", - "pump-back-location-id" : "Pumpback Location Id", - "pump-back-office-id" : "SPK", - "near-gage-location-id" : "Near Gage Location Id", - "near-gage-office-id" : "SPK", + "pump-back" : { + "office-id" : "SPK", + "name" : "Pumpback Location Id" + }, + "near-gage" : { + "office-id" : "SPK", + "name" : "Near Gage Location Id" + }, "yield-time-frame-start" : 1717199914902, "yield-time-frame-end" : 1717199914902, "project-remarks" : "Remarks" From b7d98d9afa7146c1f0c0ff749899657a3d67d740 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:51:41 -0700 Subject: [PATCH 17/17] Adding 'location' to pumpBack and nearGage --- .../main/java/cwms/cda/data/dto/Location.java | 4 ++- .../main/java/cwms/cda/data/dto/Project.java | 32 +++++++++---------- .../java/cwms/cda/data/dto/ProjectTest.java | 15 ++++----- .../resources/cwms/cda/data/dto/project.json | 4 +-- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Location.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Location.java index 8cec624ce..e7f0f5f47 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/Location.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Location.java @@ -277,7 +277,9 @@ public Builder(@JsonProperty(value = "name") String name, @JsonProperty(value = } public Builder(String office, String name) { - this(name, null, null, null, null, null, office); + this.officeId = office; + this.name = name; + buildPropertyFunctions(); } public Builder(Location location) { diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java index 992b58b3c..87be4c425 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Project.java @@ -31,8 +31,8 @@ public class Project extends CwmsDTO { private final String sedimentationDesc; private final String downstreamUrbanDesc; private final String bankFullCapacityDesc; - private final Location pumpBack; - private final Location nearGage; + private final Location pumpBackLocation; + private final Location nearGageLocation; private final Instant yieldTimeFrameStart; private final Instant yieldTimeFrameEnd; private final String projectRemarks; @@ -53,8 +53,8 @@ private Project(Project.Builder builder) { this.sedimentationDesc = builder.sedimentationDesc; this.downstreamUrbanDesc = builder.downstreamUrbanDesc; this.bankFullCapacityDesc = builder.bankFullCapacityDesc; - this.pumpBack = builder.pumpBack; - this.nearGage = builder.nearGage; + this.pumpBackLocation = builder.pumpBackLocation; + this.nearGageLocation = builder.nearGageLocation; this.yieldTimeFrameStart = builder.yieldTimeFrameStart; this.yieldTimeFrameEnd = builder.yieldTimeFrameEnd; this.projectRemarks = builder.projectRemarks; @@ -85,8 +85,8 @@ public String getHydropowerDesc() { return hydropowerDesc; } - public Location getNearGage() { - return nearGage; + public Location getNearGageLocation() { + return nearGageLocation; } public Double getNonFederalCost() { @@ -121,8 +121,8 @@ public String getProjectRemarks() { return projectRemarks; } - public Location getPumpBack() { - return pumpBack; + public Location getPumpBackLocation() { + return pumpBackLocation; } public String getSedimentationDesc() { @@ -154,8 +154,8 @@ public static class Builder { private String sedimentationDesc; private String downstreamUrbanDesc; private String bankFullCapacityDesc; - private Location pumpBack; - private Location nearGage; + private Location pumpBackLocation; + private Location nearGageLocation; private Instant yieldTimeFrameStart; private Instant yieldTimeFrameEnd; private String projectRemarks; @@ -186,8 +186,8 @@ public Builder from(Project project) { .withSedimentationDesc(project.getSedimentationDesc()) .withDownstreamUrbanDesc(project.getDownstreamUrbanDesc()) .withBankFullCapacityDesc(project.getBankFullCapacityDesc()) - .withPumpBack(project.getPumpBack()) - .withNearGage(project.getNearGage()) + .withPumpBackLocation(project.getPumpBackLocation()) + .withNearGageLocation(project.getNearGageLocation()) .withYieldTimeFrameStart(project.getYieldTimeFrameStart()) .withYieldTimeFrameEnd(project.getYieldTimeFrameEnd()) .withProjectRemarks(project.getProjectRemarks()); @@ -263,13 +263,13 @@ public Builder withBankFullCapacityDesc(String bankFullCapacityDesc) { return this; } - public Builder withPumpBack(Location pbLoc) { - this.pumpBack = pbLoc; + public Builder withPumpBackLocation(Location pbLoc) { + this.pumpBackLocation = pbLoc; return this; } - public Builder withNearGage(Location ngLoc) { - this.nearGage = ngLoc; + public Builder withNearGageLocation(Location ngLoc) { + this.nearGageLocation = ngLoc; return this; } 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 bae613019..051d681cf 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 @@ -21,11 +21,9 @@ class ProjectTest { void testProject() throws JsonProcessingException { Location pbLoc = new Location.Builder("SPK","Pumpback Location Id") - .withPublicName(null) .withActive(null) .build(); Location ngLoc = new Location.Builder("SPK","Near Gage Location Id") - .withPublicName(null) .withActive(null) .build(); @@ -45,8 +43,8 @@ void testProject() throws JsonProcessingException { .withFederalOAndMCost(10.0) .withNonFederalOAndMCost(5.0) .withProjectRemarks("Remarks") - .withPumpBack(pbLoc) - .withNearGage(ngLoc) + .withPumpBackLocation(pbLoc) + .withNearGageLocation(ngLoc) .withBankFullCapacityDesc("Bank Full Capacity Description") .withDownstreamUrbanDesc("Downstream Urban Description") .withHydropowerDesc("Hydropower Description") @@ -59,7 +57,6 @@ void testProject() throws JsonProcessingException { String json = ow.writeValueAsString(project); assertNotNull(json); - } @Test @@ -87,10 +84,10 @@ void testDeserialize() throws IOException { assertEquals(1717199914902L, project.getYieldTimeFrameStart().toEpochMilli()); assertEquals(1717199914902L, project.getYieldTimeFrameEnd().toEpochMilli()); assertEquals("Remarks", project.getProjectRemarks()); - assertEquals("Pumpback Location Id", project.getPumpBack().getName()); - assertEquals("SPK", project.getPumpBack().getOfficeId()); - assertEquals("Near Gage Location Id", project.getNearGage().getName()); - assertEquals("SPK", project.getNearGage().getOfficeId()); + assertEquals("Pumpback Location Id", project.getPumpBackLocation().getName()); + 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()); diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/project.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/project.json index 4021702b7..59670e1f0 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/data/dto/project.json +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/project.json @@ -13,11 +13,11 @@ "sedimentation-desc" : "Sedimentation Description", "downstream-urban-desc" : "Downstream Urban Description", "bank-full-capacity-desc" : "Bank Full Capacity Description", - "pump-back" : { + "pump-back-location" : { "office-id" : "SPK", "name" : "Pumpback Location Id" }, - "near-gage" : { + "near-gage-location" : { "office-id" : "SPK", "name" : "Near Gage Location Id" },