Skip to content

Commit c15971f

Browse files
committed
CDA-98 Updated name of test method
CDA-98 Brought back removal of specs CDA-98 Cleaned up the spec removal CDA-98 Adds workaround to update location id of forecast spec Show's that storing a spec with same id-desginator, but different location id, does not overwrite the original spec, but creates a new one for the new location.
1 parent 058064c commit c15971f

3 files changed

Lines changed: 272 additions & 14 deletions

File tree

cwms-data-api/src/main/java/cwms/cda/data/dao/ForecastSpecDao.java

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
import cwms.cda.api.errors.NotFoundException;
44
import cwms.cda.data.dto.forecast.ForecastSpec;
5+
import cwms.cda.data.dto.forecast.ForecastInstance;
6+
import cwms.cda.helpers.ReplaceUtils;
57

68
import org.jetbrains.annotations.NotNull;
79
import org.jooq.SelectConditionStep;
8-
import org.jooq.TableField;
910
import usace.cwms.db.jooq.codegen.packages.CWMS_FCST_PACKAGE;
1011
import usace.cwms.db.jooq.codegen.tables.AV_FCST_LOCATION;
1112
import usace.cwms.db.jooq.codegen.tables.AV_FCST_SPEC;
@@ -18,16 +19,22 @@
1819
import org.jooq.SelectOnConditionStep;
1920
import org.jooq.Table;
2021
import org.jooq.impl.DSL;
22+
import org.jooq.impl.DefaultBinding;
2123

2224
import java.util.ArrayList;
2325
import java.util.Arrays;
2426
import java.util.List;
27+
import java.util.TimeZone;
28+
import java.util.Calendar;
29+
import java.sql.Timestamp;
2530

2631
import static java.lang.String.format;
2732
import static java.util.stream.Collectors.toList;
2833

2934
public final class ForecastSpecDao extends JooqDao<ForecastSpec> {
3035

36+
private static final Calendar UTC_CALENDAR = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
37+
3138
public ForecastSpecDao(DSLContext dsl) {
3239
super(dsl);
3340
}
@@ -36,19 +43,71 @@ public ForecastSpecDao(DSLContext dsl) {
3643
public void create(ForecastSpec forecastSpec) {
3744

3845
connection(dsl, conn -> {
39-
setOffice(conn, forecastSpec.getOfficeId());
46+
// Ensure office is set and operate within a single transaction
47+
final String officeId = forecastSpec.getOfficeId();
48+
final String specId = forecastSpec.getSpecId();
49+
final String designator = forecastSpec.getDesignator();
50+
final String sourceEntityId = forecastSpec.getSourceEntityId();
51+
final String description = forecastSpec.getDescription();
52+
final String locationId = forecastSpec.getLocationId();
4053

4154
String timeSeriesIds = null;
4255
if (forecastSpec.getTimeSeriesIds() != null) {
4356
timeSeriesIds = String.join("\n", forecastSpec.getTimeSeriesIds());
4457
}
45-
CWMS_FCST_PACKAGE.call_STORE_FCST_SPEC(DSL.using(conn).configuration(), forecastSpec.getSpecId(),
46-
forecastSpec.getDesignator(), forecastSpec.getSourceEntityId(),
47-
forecastSpec.getDescription(), forecastSpec.getLocationId(),
48-
timeSeriesIds, "F", "F", forecastSpec.getOfficeId());
58+
final String tsIdsFinal = timeSeriesIds;
59+
60+
// Use office-scoped DSL context
61+
DSLContext ctx = getDslContext(conn, officeId);
62+
63+
ctx.transaction(configuration -> {
64+
DSLContext tx = DSL.using(configuration);
65+
66+
// 1) Capture and remove existing forecast instances for this spec/designator
67+
ReplaceUtils.OperatorBuilder noopUrlBuilder = new ReplaceUtils.OperatorBuilder()
68+
.withTemplate("")
69+
.withOperatorKey("{noop}");
70+
ForecastInstanceDao instDao = new ForecastInstanceDao(tx);
71+
List<ForecastInstance> existingInstances = instDao.getForecastInstances(Integer.MAX_VALUE, noopUrlBuilder,
72+
officeId, specId, designator);
73+
74+
// Delete instances to avoid conflicts during spec/location update
75+
for (ForecastInstance fi : existingInstances) {
76+
instDao.delete(officeId, fi.getSpec().getSpecId(), fi.getSpec().getDesignator(), fi.getDateTime(), fi.getIssueDateTime());
77+
}
78+
79+
// 2) Clear existing forecast location association(s) and store the spec
80+
removeExistingForecastSpec(tx, officeId, specId, designator);
81+
82+
CWMS_FCST_PACKAGE.call_STORE_FCST_SPEC(tx.configuration(), specId, designator, sourceEntityId, description, locationId,
83+
tsIdsFinal, "F", "F", officeId);
84+
85+
// 3) Re-store the previously removed forecast instances
86+
for (ForecastInstance fi : existingInstances) {
87+
instDao.create(fi);
88+
}
89+
});
4990
});
5091
}
5192

93+
private void removeExistingForecastSpec(DSLContext ctx, String officeId, String specId, String designator) {
94+
// Find any existing designators (including NULL) for this spec in this office. Probably is only one, but just to be safe.
95+
List<String> existingDesignators = ctx
96+
.select(AV_FCST_SPEC.AV_FCST_SPEC.FCST_DESIGNATOR)
97+
.from(AV_FCST_SPEC.AV_FCST_SPEC)
98+
.where(AV_FCST_SPEC.AV_FCST_SPEC.OFFICE_ID.equalIgnoreCase(officeId.toUpperCase()))
99+
.and(AV_FCST_SPEC.AV_FCST_SPEC.FCST_SPEC_ID.equalIgnoreCase(specId))
100+
.and(designator == null ?
101+
AV_FCST_SPEC.AV_FCST_SPEC.FCST_DESIGNATOR.isNull() :
102+
AV_FCST_SPEC.AV_FCST_SPEC.FCST_DESIGNATOR.eq(designator))
103+
.fetchInto(String.class);
104+
105+
for (String existingDesignator : existingDesignators) {
106+
CWMS_FCST_PACKAGE.call_DELETE_FCST_SPEC(ctx.configuration(), specId,
107+
existingDesignator, DeleteRule.DELETE_KEY.getRule(), officeId);
108+
}
109+
}
110+
52111
public void delete(String office, String specId, String designator, DeleteRule deleteRule) {
53112
connection(dsl, conn -> {
54113
setOffice(conn, office);

cwms-data-api/src/test/java/cwms/cda/api/ForecastSpecControllerTestIT.java

Lines changed: 206 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@ final class ForecastSpecControllerTestIT extends DataApiTestIT {
4949

5050
@BeforeAll
5151
static void create() throws Exception {
52+
deleteSpec();
5253
createLocation(locationId, true, OFFICE);
5354
createLocation(locationId2, true, OFFICE);
5455
createTimeSeries(locationId);
56+
createTimeSeries(locationId2);
5557
createTimeseries(OFFICE, "TsBinTestLoc.Elev.Inst.~1Day.0.SPK-cavi-fct");
5658
createTimeseries(OFFICE, "TsBinTestLoc.Flow-Outflow.Inst.~1Day.0.SPK-cavi-fct");
5759
createTimeseries(OFFICE, "TsBinTestLoc2.Elev.Inst.~1Day.0.SPK-cavi-fct");
@@ -88,17 +90,64 @@ static void truncateFcstTimeSeries() throws SQLException {
8890
}
8991

9092
static void deleteSpec() throws SQLException {
91-
try {
9293
CwmsDataApiSetupCallback.getDatabaseLink()
9394
.connection(c -> {
94-
CWMS_FCST_PACKAGE.call_DELETE_FCST_SPEC(OracleDSL.using(c).configuration(), SPEC_ID, "designator",
95-
DeleteRule.DELETE_ALL.getRule(), OFFICE);
96-
CWMS_FCST_PACKAGE.call_DELETE_FCST_SPEC(OracleDSL.using(c).configuration(), SPEC_ID + "-NULL-DESIGNATOR", null,
97-
DeleteRule.DELETE_ALL.getRule(), OFFICE);
95+
try {
96+
CWMS_FCST_PACKAGE.call_DELETE_FCST_SPEC(OracleDSL.using(c).configuration(), SPEC_ID, "designator",
97+
DeleteRule.DELETE_ALL.getRule(), OFFICE);
98+
} catch (DataAccessException e) {
99+
LOGGER.atFine().withCause(e).log("Couldn't clean up forecast spec before executing tests. Probably didn't exist");
100+
}
101+
try {
102+
CWMS_FCST_PACKAGE.call_DELETE_FCST_SPEC(OracleDSL.using(c).configuration(), SPEC_ID + "-NULL-DESIGNATOR", null,
103+
DeleteRule.DELETE_ALL.getRule(), OFFICE);
104+
} catch (DataAccessException e) {
105+
LOGGER.atFine().withCause(e).log("Couldn't clean up forecast spec before executing tests. Probably didn't exist");
106+
}
107+
try {
108+
CWMS_FCST_PACKAGE.call_DELETE_FCST_SPEC(OracleDSL.using(c).configuration(), SPEC_ID + "-TEST", "designator",
109+
DeleteRule.DELETE_ALL.getRule(), OFFICE);
110+
} catch (DataAccessException e) {
111+
LOGGER.atFine().withCause(e).log("Couldn't clean up forecast spec before executing tests. Probably didn't exist");
112+
}
113+
try {
114+
CWMS_FCST_PACKAGE.call_DELETE_FCST_SPEC(OracleDSL.using(c).configuration(), SPEC_ID + "TEST", "designator",
115+
DeleteRule.DELETE_ALL.getRule(), OFFICE);
116+
} catch (DataAccessException e) {
117+
LOGGER.atFine().withCause(e).log("Couldn't clean up forecast spec before executing tests. Probably didn't exist");
118+
}
119+
try {
120+
CWMS_FCST_PACKAGE.call_DELETE_FCST_SPEC(OracleDSL.using(c).configuration(), SPEC_ID + "TEST-2", "designator",
121+
DeleteRule.DELETE_ALL.getRule(), OFFICE);
122+
} catch (DataAccessException e) {
123+
LOGGER.atFine().withCause(e).log("Couldn't clean up forecast spec before executing tests. Probably didn't exist");
124+
}
125+
try {
126+
CWMS_FCST_PACKAGE.call_DELETE_FCST_SPEC(OracleDSL.using(c).configuration(), SPEC_ID + "-TEST-2", "designator",
127+
DeleteRule.DELETE_ALL.getRule(), OFFICE);
128+
} catch (DataAccessException e) {
129+
LOGGER.atFine().withCause(e).log("Couldn't clean up forecast spec before executing tests. Probably didn't exist");
130+
}
131+
try {
132+
CWMS_FCST_PACKAGE.call_DELETE_FCST_SPEC(OracleDSL.using(c).configuration(), "TEST_SPEC_2", "designator",
133+
DeleteRule.DELETE_ALL.getRule(), OFFICE);
134+
} catch (DataAccessException e) {
135+
LOGGER.atFine().withCause(e).log("Couldn't clean up forecast spec before executing tests. Probably didn't exist");
136+
}
137+
try {
138+
CWMS_FCST_PACKAGE.call_DELETE_FCST_SPEC(OracleDSL.using(c).configuration(), SPEC_ID + "-LRTS", "designator",
139+
DeleteRule.DELETE_ALL.getRule(), OFFICE);
140+
} catch (DataAccessException e) {
141+
LOGGER.atFine().withCause(e).log("Couldn't clean up forecast spec before executing tests. Probably didn't exist");
142+
}
143+
try {
144+
CWMS_FCST_PACKAGE.call_DELETE_FCST_SPEC(OracleDSL.using(c).configuration(), SPEC_ID + "-2", "designator",
145+
DeleteRule.DELETE_ALL.getRule(), OFFICE);
146+
} catch (DataAccessException e) {
147+
LOGGER.atFine().withCause(e).log("Couldn't clean up forecast spec before executing tests. Probably didn't exist");
148+
}
98149
});
99-
} catch (DataAccessException e) {
100-
LOGGER.atFine().withCause(e).log("Couldn't clean up forecast spec before executing tests. Probably didn't exist");
101-
}
150+
102151
}
103152

104153

@@ -477,6 +526,155 @@ void create_getAll_delete_getAll(String format) throws Exception {
477526
;
478527
}
479528

529+
@ParameterizedTest
530+
@ValueSource(strings = {Formats.JSONV2, Formats.DEFAULT})
531+
void create_then_create_with_diff_location_then_getAll_then_delete(String format) throws Exception {
532+
533+
// Structure of test:
534+
// 1) Create a spec with a location id
535+
// 2) Create a forecast instance with that spec
536+
// 3) Create a second spec with the same designator but different location id
537+
// 4) Call getAll and verify a list/array is returned containing only one spec with the updated location id
538+
// 5) Verify the forecast instance is still associated with the spec with the updated location id
539+
// 6) Delete spec and forecast instance
540+
// 7) Call getAll again and verify they are not returned
541+
542+
// Step 1) Create two specs
543+
InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/api/spk/forecast_spec_create.json");
544+
assertNotNull(resource);
545+
String tsData = IOUtils.toString(resource, StandardCharsets.UTF_8);
546+
assertNotNull(tsData);
547+
548+
String specId = SPEC_ID + "TEST";
549+
tsData = tsData.replace("\"spec-id\": \"" + SPEC_ID + "\"", "\"spec-id\": \"" + specId + "\"");
550+
551+
String tsData2 = tsData.replace(locationId, locationId2);
552+
553+
TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL;
554+
555+
// Create first spec
556+
given()
557+
.log().ifValidationFails(LogDetail.ALL, true)
558+
.accept(format)
559+
.contentType(Formats.JSONV2)
560+
.body(tsData)
561+
.header(AUTH_HEADER, user.toHeaderValue())
562+
.when()
563+
.redirects().follow(true)
564+
.redirects().max(3)
565+
.post(PATH)
566+
.then()
567+
.log().ifValidationFails(LogDetail.ALL, true)
568+
.assertThat()
569+
.statusCode(is(HttpServletResponse.SC_CREATED));
570+
571+
// Step 2) Create a forecast instance for the first spec (at locationId)
572+
InputStream instResource = this.getClass().getResourceAsStream("/cwms/cda/api/spk/forecast_inst_create.json");
573+
assertNotNull(instResource);
574+
String instData = IOUtils.toString(instResource, StandardCharsets.UTF_8);
575+
// Ensure the instance spec-id and timeseries location match the spec we just created
576+
instData = instData.replace("\"spec-id\": \"" + SPEC_ID + "\"", "\"spec-id\": \"" + specId + "\"");
577+
// The instance resource uses FcstInstTestLoc.* TSIDs; switch them to our locationId TSIDs
578+
instData = instData.replace("FcstInstTestLoc", locationId);
579+
580+
given()
581+
.log().ifValidationFails(LogDetail.ALL, true)
582+
.accept(format)
583+
.contentType(Formats.JSONV2)
584+
.body(instData)
585+
.header(AUTH_HEADER, user.toHeaderValue())
586+
.when()
587+
.redirects().follow(true)
588+
.redirects().max(3)
589+
.post(ForecastInstanceControllerTestIT.PATH)
590+
.then()
591+
.log().ifValidationFails(LogDetail.ALL, true)
592+
.assertThat()
593+
.statusCode(is(HttpServletResponse.SC_CREATED));
594+
595+
// Create second spec
596+
given()
597+
.log().ifValidationFails(LogDetail.ALL, true)
598+
.accept(format)
599+
.contentType(Formats.JSONV2)
600+
.body(tsData2)
601+
.header(AUTH_HEADER, user.toHeaderValue())
602+
.when()
603+
.redirects().follow(true)
604+
.redirects().max(3)
605+
.post(PATH)
606+
.then()
607+
.log().ifValidationFails(LogDetail.ALL, true)
608+
.assertThat()
609+
.statusCode(is(HttpServletResponse.SC_CREATED));
610+
611+
// Step 3) getAll should return a list only containing the one spec with the updated location id
612+
given()
613+
.log().ifValidationFails(LogDetail.ALL, true)
614+
.accept(format)
615+
.queryParam(Controllers.OFFICE, OFFICE)
616+
.queryParam(Controllers.DESIGNATOR_MASK, "*")
617+
.queryParam(Controllers.ID_MASK, specId)
618+
.queryParam(Controllers.SOURCE_ENTITY, ".*")
619+
.when()
620+
.redirects().follow(true)
621+
.redirects().max(3)
622+
.get(PATH)
623+
.then()
624+
.log().ifValidationFails(LogDetail.ALL, true)
625+
.assertThat()
626+
.statusCode(is(HttpServletResponse.SC_OK))
627+
// verify it is an array with 2 elements and contains both spec-ids
628+
.body("size()", equalTo(1))
629+
.body("[0].designator", equalTo(designator))
630+
.body("[0].location-id", equalTo(locationId2))
631+
;
632+
633+
// Step 4) Verify the forecast instance is still retrievable and points to the updated spec/location
634+
given()
635+
.log().ifValidationFails(LogDetail.ALL, true)
636+
.accept(format)
637+
.queryParam(Controllers.OFFICE, OFFICE)
638+
.queryParam(Controllers.DESIGNATOR, designator)
639+
// These match the values in forecast_inst_create.json
640+
.queryParam(Controllers.FORECAST_DATE, "2021-06-21T14:00:00+00:00")
641+
.queryParam(Controllers.ISSUE_DATE, "2022-05-22T12:03:00+00:00")
642+
.when()
643+
.redirects().follow(true)
644+
.redirects().max(3)
645+
.get(ForecastInstanceControllerTestIT.PATH + specId)
646+
.then()
647+
.log().ifValidationFails(LogDetail.ALL, true)
648+
.assertThat()
649+
.statusCode(is(HttpServletResponse.SC_OK))
650+
.body("spec.spec-id", equalTo(specId))
651+
.body("spec.designator", equalTo(designator))
652+
.body("spec.location-id", equalTo(locationId2));
653+
654+
// Step 5) Delete specs/instances
655+
truncateFcstTimeSeries();
656+
657+
// Step 6) Verify getAll no longer returns the deleted specs
658+
given()
659+
.log().ifValidationFails(LogDetail.ALL, true)
660+
.accept(format)
661+
.queryParam(Controllers.OFFICE, OFFICE)
662+
.queryParam(DESIGNATOR, "*")
663+
.queryParam(ID_MASK, specId + "*")
664+
.queryParam(Controllers.SOURCE_ENTITY, ".*")
665+
.when()
666+
.redirects().follow(true)
667+
.redirects().max(3)
668+
.get(PATH)
669+
.then()
670+
.log().ifValidationFails(LogDetail.ALL, true)
671+
.assertThat()
672+
.statusCode(is(HttpServletResponse.SC_OK))
673+
// Expect empty array
674+
.body("size()", equalTo(0))
675+
;
676+
}
677+
480678
@ParameterizedTest
481679
@ValueSource(strings = {Formats.JSONV2, Formats.DEFAULT})
482680
void create_getAll_with_entity_like_delete_getAll(String format) throws Exception {

cwms-data-api/src/test/resources/cwms/cda/api/spk/forecast_spec_create.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"spec-id": "TEST-SPEC",
3+
"location-id": "TsBinTestLoc",
34
"office-id": "SPK",
45
"source-entity-id": "USACE",
56
"designator": "designator",

0 commit comments

Comments
 (0)