diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java index 965a84b86..b477afdb7 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java @@ -65,7 +65,7 @@ public class RatingSpecController implements CrudHandler { private final MetricRegistry metrics; - private static final int DEFAULT_PAGE_SIZE = 100; + static final int DEFAULT_PAGE_SIZE = 100; private final Histogram requestResultSize; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java index 5cb7ddf5e..99e9f3c22 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java @@ -251,6 +251,9 @@ protected void withDefaultDatum(@Nullable VerticalDatum targetDatum, DSLContext * an easy to read manner without having to worry about the syntax. */ public static Condition caseInsensitiveLikeRegex(Field field, String regex) { + if("*".equals(regex) || ".*".equals(regex)) { + return DSL.noCondition(); + } return new CustomCondition() { @Override public void accept(org.jooq.Context ctx) { diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingAdapter.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingAdapter.java index 9e047e769..c2729ac3d 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingAdapter.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingAdapter.java @@ -104,10 +104,14 @@ private static TransitionalRating toTransitional( withAbstractFields(builder, rating); String[] evaluationStrings = rating.getEvaluationStrings(); - builder.withEvaluations(Arrays.asList(evaluationStrings)); + if(evaluationStrings != null) { + builder.withEvaluations(Arrays.asList(evaluationStrings)); + } String[] conditionStrings = rating.getConditionStrings(); - builder.withConditions(Arrays.asList(conditionStrings)); + if(conditionStrings != null) { + builder.withConditions(Arrays.asList(conditionStrings)); + } List ratingSpecIds = new ArrayList<>(); SourceRating[] sourceRatings = rating.getSourceRatings(); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java index 1524daf22..7365bf10c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java @@ -27,6 +27,7 @@ import static com.google.common.flogger.LazyArgs.lazy; import static cwms.cda.data.dto.rating.RatingSpec.Builder.buildIndependentRoundingSpecs; +import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList; import com.fasterxml.jackson.core.JsonProcessingException; @@ -48,15 +49,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; -import java.util.Collection; +import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.NavigableMap; import java.util.NavigableSet; +import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.TimeZone; import java.util.TreeMap; import java.util.TreeSet; @@ -114,69 +114,8 @@ public RatingSpecDao(DSLContext dsl) { super(dsl); } - public Collection retrieveRatingSpecs(String office, String specIdMask) { - - AV_RATING_SPEC specView = AV_RATING_SPEC.AV_RATING_SPEC; - AV_RATING ratView = AV_RATING.AV_RATING; - - // We don't want to also check AV_RATING_SPEC.ALIASED_ITEM b/c we - // don't care whether the specs returned are an alias or not. - // We do want to exclude the aliased ratings b/c we only want one - // copy of each matching rating. - - Condition condition = ratView.ALIASED_ITEM.isNull(); - - if (office != null) { - condition = condition.and(specView.OFFICE_ID.eq(office)); - } - - if (specIdMask != null) { - Condition likeRegex = JooqDao.caseInsensitiveLikeRegex(specView.RATING_ID, specIdMask); - condition = condition.and(likeRegex); - } - - ResultQuery query = dsl.select(specView.RATING_SPEC_CODE, - specView.OFFICE_ID, specView.RATING_ID, specView.TEMPLATE_ID, - specView.LOCATION_ID, specView.VERSION, specView.SOURCE_AGENCY, - specView.ACTIVE_FLAG, specView.AUTO_UPDATE_FLAG, - specView.AUTO_ACTIVATE_FLAG, - specView.AUTO_MIGRATE_EXT_FLAG, specView.IND_ROUNDING_SPECS, - specView.DEP_ROUNDING_SPEC, specView.DATE_METHODS, specView.DESCRIPTION, - ratView.RATING_SPEC_CODE, ratView.EFFECTIVE_DATE) - .from(specView) - .leftOuterJoin(ratView) - .on(specView.RATING_SPEC_CODE.eq(ratView.RATING_SPEC_CODE)) - .where(condition) - .fetchSize(DEFAULT_FETCH_SIZE); - - logger.atFine().log("%s", lazy(() -> query.getSQL(ParamType.INLINED))); - - Map> map = new LinkedHashMap<>(); - try (Stream stream = query.fetchStream()) { - stream.forEach(rec -> { - RatingSpec template = buildRatingSpec(rec); - - Timestamp effectiveDate = rec.get(ratView.EFFECTIVE_DATE); - ZonedDateTime effective = toZdt(effectiveDate); - - List list = map.computeIfAbsent(template, k -> new ArrayList<>()); - if (effective != null) { - list.add(effective); - } - }); - } - - return map.entrySet().stream() - .map(entry -> new RatingSpec.Builder() - .fromRatingSpec(entry.getKey()) - .withEffectiveDates(entry.getValue()) - .build()) - .collect(Collectors.toCollection(LinkedHashSet::new)); - } - - - public RatingSpecs retrieveRatingSpecs(String cursor, int pageSize, String office, - String specIdMask) { + @NotNull + public RatingSpecs retrieveRatingSpecs(String cursor, int pageSize, String office, String specIdMask) { Integer total = null; int offset = 0; @@ -196,145 +135,119 @@ public RatingSpecs retrieveRatingSpecs(String cursor, int pageSize, String offic } } - Set retval = getRatingSpecs(office, specIdMask, offset, pageSize); - - RatingSpecs.Builder builder = new RatingSpecs.Builder(offset, pageSize, total); - builder.withSpecs(new ArrayList<>(retval)); - return builder.build(); - } - - @NotNull - public Set getRatingSpecs(String office, String specIdMask, int firstRow, - int pageSize) { - Set retVal; - AV_RATING_SPEC specView = AV_RATING_SPEC.AV_RATING_SPEC; AV_RATING ratView = AV_RATING.AV_RATING; - // We don't want to also check AV_RATING_SPEC.ALIASED_ITEM b/c we - // don't care whether the specs returned are an alias or not. - // We do want to exclude the aliased ratings b/c we only want one - // copy of each matching rating. - Condition condition = ratView.ALIASED_ITEM.isNull(); + // Conditions that define WHICH SPECS match + Condition specCondition = specView.TEMPLATE_ID.notLike("%Stage-Offset%") + .and(specView.TEMPLATE_ID.notLike("%Stage-Shift%")) + .and(specView.ALIASED_ITEM.isNull()); if (office != null) { - condition = condition.and(specView.OFFICE_ID.eq(office)); + specCondition = specCondition.and(specView.OFFICE_ID.eq(office.toUpperCase())); } if (specIdMask != null) { Condition maskRegex = JooqDao.caseInsensitiveLikeRegex(specView.RATING_ID, specIdMask); - condition = condition.and(maskRegex); + specCondition = specCondition.and(maskRegex); } - ResultQuery query = dsl.select(specView.RATING_SPEC_CODE, - specView.OFFICE_ID, specView.RATING_ID, specView.DATE_METHODS, - specView.TEMPLATE_ID, specView.LOCATION_ID, specView.VERSION, - specView.SOURCE_AGENCY, specView.ACTIVE_FLAG, specView.AUTO_UPDATE_FLAG, - specView.AUTO_ACTIVATE_FLAG, specView.AUTO_MIGRATE_EXT_FLAG, - specView.IND_ROUNDING_SPECS, specView.DEP_ROUNDING_SPEC, - specView.DESCRIPTION, specView.ALIASED_ITEM, - ratView.RATING_SPEC_CODE, ratView.EFFECTIVE_DATE) - .from(specView) - .leftOuterJoin(ratView) - .on(specView.RATING_SPEC_CODE.eq(ratView.RATING_SPEC_CODE)) - .where(condition) - .orderBy(specView.OFFICE_ID, specView.TEMPLATE_ID, ratView.RATING_ID, - ratView.EFFECTIVE_DATE) - .limit(pageSize) - .offset(firstRow); - - logger.atFine().log("%s", lazy(() -> query.getSQL(ParamType.INLINED))); - - Map> map = new LinkedHashMap<>(); - try (Stream stream = query.fetchStream()) { - stream.forEach(rec -> { - RatingSpec template = buildRatingSpec(rec); - - Timestamp effectiveDate = rec.get(ratView.EFFECTIVE_DATE); - ZonedDateTime effective = toZdt(effectiveDate); - - List list = map.computeIfAbsent(template, k -> new ArrayList<>()); - if (effective != null) { - list.add(effective); - } - }); - } - - retVal = map.entrySet().stream() - .map(entry -> new RatingSpec.Builder() - .fromRatingSpec(entry.getKey()) - .withEffectiveDates(entry.getValue()) - .build()) - .collect(Collectors.toCollection(LinkedHashSet::new)); - return retVal; - } - - - public Optional retrieveRatingSpec(String office, String specId) { - Set retVal; - - AV_RATING_SPEC specView = AV_RATING_SPEC.AV_RATING_SPEC; - AV_RATING ratView = AV_RATING.AV_RATING; - - Condition condition = ratView.ALIASED_ITEM.isNull(); - - if (specId != null) { - condition = condition.and(specView.RATING_ID.eq(specId)); + if (total == null) { + total = dsl.fetchCount(specView, specCondition); } - if (office != null) { - condition = condition.and(specView.OFFICE_ID.eq(office)); - } + var specPage = dsl + .select( + specView.RATING_SPEC_CODE, + specView.OFFICE_ID, specView.RATING_ID, specView.DATE_METHODS, + specView.TEMPLATE_ID, specView.LOCATION_ID, specView.VERSION, + specView.SOURCE_AGENCY, specView.ACTIVE_FLAG, specView.AUTO_UPDATE_FLAG, + specView.AUTO_ACTIVATE_FLAG, specView.AUTO_MIGRATE_EXT_FLAG, + specView.IND_ROUNDING_SPECS, specView.DEP_ROUNDING_SPEC, + specView.DESCRIPTION, specView.ALIASED_ITEM + ) + .from(specView) + .where(specCondition) + .orderBy(specView.OFFICE_ID, specView.RATING_ID) + .limit(pageSize) + .offset(offset) + .asTable("spec_page"); + + Field spSpecCode = specPage.field(specView.RATING_SPEC_CODE); + Field spOfficeId = specPage.field(specView.OFFICE_ID); + Field spRatingId = specPage.field(specView.RATING_ID); + + Field> effectiveDates = + DSL.multiset( + dsl.select(ratView.EFFECTIVE_DATE) + .from(ratView) + .where(ratView.RATING_SPEC_CODE.eq(spSpecCode)) + .and(ratView.ALIASED_ITEM.isNull()) + .orderBy(ratView.EFFECTIVE_DATE) + ) + .convertFrom(r -> + r.getValues(ratView.EFFECTIVE_DATE).stream() + .map(RatingSpecDao::toZdt) + .filter(Objects::nonNull) + .collect(Collectors.toList()) + ) + .as("effective_dates"); ResultQuery query = dsl.select( - specView.RATING_SPEC_CODE, - specView.OFFICE_ID, specView.RATING_ID, specView.TEMPLATE_ID, - specView.LOCATION_ID, specView.VERSION, specView.SOURCE_AGENCY, - specView.ACTIVE_FLAG, specView.AUTO_UPDATE_FLAG, - specView.AUTO_ACTIVATE_FLAG, specView.AUTO_MIGRATE_EXT_FLAG, - specView.IND_ROUNDING_SPECS, specView.DEP_ROUNDING_SPEC, - specView.DATE_METHODS, specView.DESCRIPTION, - ratView.RATING_SPEC_CODE, ratView.EFFECTIVE_DATE - ) - .from(specView) - .leftOuterJoin(ratView) - .on(specView.RATING_SPEC_CODE.eq(ratView.RATING_SPEC_CODE)) - .where(condition) - .orderBy(specView.OFFICE_ID, specView.RATING_ID, ratView.EFFECTIVE_DATE) - .fetchSize(DEFAULT_FETCH_SIZE); + spSpecCode, + spOfficeId, + spRatingId, + specPage.field(specView.DATE_METHODS), + specPage.field(specView.TEMPLATE_ID), + specPage.field(specView.LOCATION_ID), + specPage.field(specView.VERSION), + specPage.field(specView.SOURCE_AGENCY), + specPage.field(specView.ACTIVE_FLAG), + specPage.field(specView.AUTO_UPDATE_FLAG), + specPage.field(specView.AUTO_ACTIVATE_FLAG), + specPage.field(specView.AUTO_MIGRATE_EXT_FLAG), + specPage.field(specView.IND_ROUNDING_SPECS), + specPage.field(specView.DEP_ROUNDING_SPEC), + specPage.field(specView.DESCRIPTION), + specPage.field(specView.ALIASED_ITEM), + effectiveDates + ) + .from(specPage) + .orderBy(spOfficeId, spRatingId) + .fetchSize(DEFAULT_FETCH_SIZE); logger.atFine().log("%s", lazy(() -> query.getSQL(ParamType.INLINED))); - Map> map = new LinkedHashMap<>(); - try (Stream stream = query.fetchStream()) { - stream.forEach(rec -> { + List specs = query.fetch() + .stream() + .map(rec -> { RatingSpec template = buildRatingSpec(rec); + List dates = rec.get(effectiveDates); + return new RatingSpec.Builder() + .fromRatingSpec(template) + .withEffectiveDates(dates == null ? List.of() : dates) + .build(); + }) + .collect(toList()); - Timestamp effectiveDate = rec.get(ratView.EFFECTIVE_DATE); - ZonedDateTime effective = toZdt(effectiveDate); - - List list = map.computeIfAbsent(template, k -> new ArrayList<>()); - if (effective != null) { - list.add(effective); - } - }); - } + RatingSpecs.Builder builder = new RatingSpecs.Builder(offset, pageSize, total); + builder.withSpecs(specs); + return builder.build(); + } - retVal = map.entrySet().stream() - .map(entry -> new RatingSpec.Builder() - .fromRatingSpec(entry.getKey()) - .withEffectiveDates(entry.getValue()) - .build()) - .collect(Collectors.toCollection(LinkedHashSet::new)); - // There should only be one key in the map - if (retVal.size() > 1) { - throw new IllegalStateException("More than one rating spec found for id: " + specId); + public Optional retrieveRatingSpec(String office, String specId) { + RatingSpecs ratingSpecs = retrieveRatingSpecs(null, 1, office, specId); + List specs = ratingSpecs.getSpecs(); + if(specs.size() > 1) { + throw new IllegalStateException("More than one rating spec found for specId: " + specId); + } else if(specs.isEmpty()) { + return Optional.empty(); + } else { + return Optional.of(specs.get(0)); + } } - return retVal.stream().findFirst(); - } - public static ZonedDateTime toZdt(final Timestamp time) { if (time != null) { return ZonedDateTime.ofInstant(time.toInstant(), ZoneId.of("UTC")); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java index 1e5e27b40..9685033b5 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java @@ -73,7 +73,7 @@ public static class Builder { private int offset; private int pageSize; private Integer total; - private List specs; + private List specs = new ArrayList<>(); public Builder() { } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerPagingIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerPagingIT.java new file mode 100644 index 000000000..13e4485fb --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerPagingIT.java @@ -0,0 +1,108 @@ +/* + * MIT License + * + * Copyright (c) 2025 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.rating; + +import static cwms.cda.api.Controllers.OFFICE; +import static cwms.cda.api.Controllers.PAGE; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import cwms.cda.api.DataApiTestIT; +import cwms.cda.formatters.Formats; +import fixtures.TestAccounts; +import hec.data.RatingException; +import hec.data.cwmsRating.RatingSet; +import io.restassured.filter.log.LogDetail; +import javax.servlet.http.HttpServletResponse; +import mil.army.usace.hec.cwms.rating.io.jdbc.RatingJdbcFactory; +import mil.army.usace.hec.cwms.rating.io.xml.RatingXmlFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import usace.cwms.db.jooq.codegen.packages.CWMS_ENV_PACKAGE; + +@Tag("integration") +class RatingSpecControllerPagingIT extends DataApiTestIT { + + @BeforeAll + static void beforeAll() throws Exception { + String locationId = "RatingSpecGetAllPaged"; + String ratingXml = readResourceFile("cwms/cda/api/RatingSpecGetAllPaged_Stage_Flow_COE_Production.xml"); + String officeId = "SPK"; + createLocation(locationId, true, officeId); + connectionAsWebUser(c ->{ + CWMS_ENV_PACKAGE.call_SET_SESSION_OFFICE_ID(dslContext(c).configuration(), "SPK"); + for(int i = 0; i < RatingSpecController.DEFAULT_PAGE_SIZE * 2; i++) { + try { + String xml = ratingXml.replace("{location-id}", locationId) + .replace("{version-template}", "Test" + i); + RatingSet ratingSet = RatingXmlFactory.ratingSet(xml); + RatingJdbcFactory.store(ratingSet, c, true, true); + } catch (RatingException e) { + throw new RuntimeException(e); + } + } + }); + } + + @Test + void test_getAll_paged() { + var response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .queryParam(OFFICE, "SPK") + .queryParam("rating-id-mask", ".*") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/spec") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("page-size", equalTo(RatingSpecController.DEFAULT_PAGE_SIZE)) + .body("total", greaterThan(RatingSpecController.DEFAULT_PAGE_SIZE)) + .body("next-page", notNullValue()) + .extract(); + String nextPage = response.path("next-page"); + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .queryParam(OFFICE, "SPK") + .queryParam(PAGE, nextPage) + .queryParam("rating-id-mask", ".*") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/spec") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java index 10267792e..a28b25c12 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java @@ -45,6 +45,7 @@ import static cwms.cda.api.Controllers.METHOD; import static cwms.cda.api.Controllers.OFFICE; +import static cwms.cda.api.Controllers.RATING_ID_MASK; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -106,7 +107,8 @@ void test_empty_rating_spec() throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .accept(Formats.JSONV2) .contentType(Formats.JSONV2) - .queryParam("office", officeId) + .queryParam(OFFICE, officeId) + .queryParam(RATING_ID_MASK, specContainer.specId) .when() .redirects().follow(true) .redirects().max(3) diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecDaoTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecDaoTest.java index cb3c9e709..77b3f1533 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecDaoTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecDaoTest.java @@ -32,7 +32,8 @@ void testRetrieveRatingSpecs() throws SQLException, JsonProcessingException { DSLContext lrl = getDslContext(getConnection(), OFFICE_ID); RatingSpecDao dao = new RatingSpecDao(lrl); - Collection ratingSpecs = dao.retrieveRatingSpecs(OFFICE_ID, "^ARTH"); + Collection ratingSpecs = dao.retrieveRatingSpecs(null, 1000, OFFICE_ID, "^ARTH") + .getSpecs(); assertNotNull(ratingSpecs); assertFalse(ratingSpecs.isEmpty()); diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/RatingSpecGetAllPaged_Stage_Flow_COE_Production.xml b/cwms-data-api/src/test/resources/cwms/cda/api/RatingSpecGetAllPaged_Stage_Flow_COE_Production.xml new file mode 100644 index 000000000..f5c1346d5 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/RatingSpecGetAllPaged_Stage_Flow_COE_Production.xml @@ -0,0 +1,50 @@ + + + + Stage;Flow + COE + + + Stage + LINEAR + ERROR + ERROR + + + Flow + + + + {location-id}.Stage;Flow.COE.{version-template} + Stage;Flow.COE + {location-id} + {version-template} + + LINEAR + NULL + NEAREST + true + false + false + false + + 4444444444 + + 4444444444 + + + + {location-id}.Stage;Flow.COE.{version-template} + ft;cfs + 2002-04-09T13:53:01Z + 2014-06-11T14:46:00Z + true + + + + 2.37744006 + 14.1584233 + + + +