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 fe6f715ae5..74f7240154 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 @@ -2,6 +2,7 @@ import static com.codahale.metrics.MetricRegistry.name; import static cwms.cda.api.Controllers.ACCEPT; +import static cwms.cda.api.Controllers.INCLUDE_ALIASES; import static cwms.cda.api.Controllers.BOUNDING_OFFICE_LIKE; import static cwms.cda.api.Controllers.CURSOR; import static cwms.cda.api.Controllers.EXCLUDE_EMPTY; @@ -168,6 +169,10 @@ public void getAll(Context ctx) { description = "Posix regular expression matching " + "against the location type." ), + @OpenApiParam(name = INCLUDE_ALIASES, type = Boolean.class, + description = "Whether to add aliases to the catalog entries. " + + "Default is false. If true, the aliases will be added to the " + + "catalog entries in the response."), }, pathParams = { @OpenApiParam(name = "dataset", @@ -230,7 +235,8 @@ public void getOne(@NotNull Context ctx, @NotNull String dataSet) { String locationType = queryParamAsClass(ctx, new String[]{LOCATION_TYPE_LIKE}, String.class, null, metrics, name(CatalogController.class.getName(), GET_ONE)); - + boolean includeAliases = ctx.queryParamAsClass(INCLUDE_ALIASES, Boolean.class) + .getOrDefault(false); String acceptHeader = ctx.header(ACCEPT); ContentType contentType = Formats.parseHeader(acceptHeader, Catalog.class); Catalog cat = null; @@ -254,6 +260,7 @@ public void getOne(@NotNull Context ctx, @NotNull String dataSet) { .withExcludeEmpty(excludeExtents) .withLocationKind(locationKind) .withLocationType(locationType) + .withIncludeAliases(includeAliases) .build(); cat = tsDao.getTimeSeriesCatalog(cursor, pageSize, parameters); 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 b8be777f1b..c899567f70 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 @@ -146,6 +146,7 @@ public final class Controllers { public static final String ISSUE_DATE = "issue-date"; public static final String LOCATION_KIND_LIKE = "location-kind-like"; public static final String LOCATION_TYPE_LIKE = "location-type-like"; + public static final String INCLUDE_ALIASES = "include-aliases"; public static final String MIN_NUMBER = "min-number"; public static final String MAX_NUMBER = "max-number"; public static final String MIN_HEIGHT = "min-height"; 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 index f652d55384..60faf36d1b 100644 --- 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 @@ -20,6 +20,7 @@ public class CatalogRequestParameters { private final boolean excludeEmpty; private final String locationKind; private final String locationType; + private final boolean includeAliases; private CatalogRequestParameters(Builder builder) { this.office = builder.office; @@ -34,6 +35,7 @@ private CatalogRequestParameters(Builder builder) { this.excludeEmpty = builder.excludeEmpty; this.locationKind = builder.locationKind; this.locationType = builder.locationType; + this.includeAliases = builder.includeAliases; } public String getBoundingOfficeLike() { @@ -84,6 +86,10 @@ public String getLocationType() { return locationType; } + public boolean includeAliases() { + return includeAliases; + } + public static class Builder { String office; @@ -98,6 +104,7 @@ public static class Builder { private boolean excludeEmpty = true; String locationKind; String locationType; + private boolean includeAliases = false; public Builder() { @@ -163,6 +170,11 @@ public Builder withLocationType(String locationType) { return this; } + public Builder withIncludeAliases(boolean includeAliases) { + this.includeAliases = includeAliases; + return this; + } + public static Builder from(CatalogRequestParameters params) { // This NEEDS to include every field in the CatalogRequestParameters return new Builder() 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 dce0f3344a..702f28cc11 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 @@ -1,6 +1,9 @@ package cwms.cda.data.dao; +import cwms.cda.data.dto.catalog.TimeSeriesAlias; import cwms.cda.helpers.DateUtils; +import java.util.HashSet; +import java.util.Set; import static org.jooq.impl.DSL.asterisk; import static org.jooq.impl.DSL.countDistinct; import static org.jooq.impl.DSL.field; @@ -9,6 +12,7 @@ import static org.jooq.impl.DSL.partitionBy; import static org.jooq.impl.DSL.select; import static org.jooq.impl.DSL.selectDistinct; +import usace.cwms.db.jooq.codegen.tables.AV_CWMS_TS_ID; import static org.jooq.impl.DSL.table; import static usace.cwms.db.jooq.codegen.tables.AV_CWMS_TS_ID2.AV_CWMS_TS_ID2; import static usace.cwms.db.jooq.codegen.tables.AV_TS_EXTENTS_UTC.AV_TS_EXTENTS_UTC; @@ -84,7 +88,6 @@ import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_PACKAGE; import usace.cwms.db.jooq.codegen.packages.CWMS_TS_PACKAGE; import usace.cwms.db.jooq.codegen.packages.CWMS_UTIL_PACKAGE; -import usace.cwms.db.jooq.codegen.tables.AV_CWMS_TS_ID; 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_TSV; @@ -107,7 +110,6 @@ public class TimeSeriesDaoImpl extends JooqDao implements TimeSeries /** To be able to use a named inner table (otherwise JOOQ creates a random alias which messes * with the planner) we need to use fixed names to be able to reference the required columns. ) */ - private static final AV_CWMS_TS_ID cwmsTsIdView = AV_CWMS_TS_ID.AV_CWMS_TS_ID; private static final AV_TS_GRP_ASSGN tsGroupView = AV_TS_GRP_ASSGN.AV_TS_GRP_ASSGN; private static final AV_LOC_GRP_ASSGN locGroupView = AV_LOC_GRP_ASSGN.AV_LOC_GRP_ASSGN; @@ -128,7 +130,8 @@ public class TimeSeriesDaoImpl extends JooqDao implements TimeSeries + ".expireAfterSeconds", 600), TimeUnit.SECONDS) .recordStats() .build(); - + private static final FieldMapping AV_CWMS_TS_ID2_FIELD_MAP = new CwmsTsId2FieldMapping(); + private static final FieldMapping AV_CWMS_TS_ID_FIELD_MAP = new CwmsTsIdFieldMapping(); public TimeSeriesDaoImpl(DSLContext dsl) { this(dsl, null); @@ -162,7 +165,6 @@ public String getTimeseries(String format, String names, String office, String u timezone.getId(), office); } - @Override public TimeSeries getTimeseries(String page, int pageSize, String names, String office, String units, @@ -513,11 +515,13 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar String cursorTsId = "*"; String cursorOffice = null; Catalog.CatalogPage catPage = null; + FieldMapping cwmsTsIdFields = inputParams.includeAliases() ? AV_CWMS_TS_ID2_FIELD_MAP : AV_CWMS_TS_ID_FIELD_MAP; + Table table = inputParams.includeAliases() ? AV_CWMS_TS_ID2 : AV_CWMS_TS_ID.AV_CWMS_TS_ID; if (page == null || page.isEmpty()) { - CommonTableExpression limiter = buildWithClause(inputParams, buildWhereConditions(inputParams), + CommonTableExpression limiter = buildWithClause(cwmsTsIdFields, inputParams, buildWhereConditions(inputParams), new ArrayList<>(), pageSize, true); SelectJoinStep> totalQuery = dsl.with(limiter) - .select(countDistinct(limiter.field(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE))) + .select(countDistinct(limiter.field(cwmsTsIdFields.getTsCode()))) .from(limiter); logger.fine(() -> totalQuery.getSQL(ParamType.INLINED)); total = totalQuery.fetchOne(0, int.class); @@ -544,20 +548,19 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar } final CatalogRequestParameters params = inputParams; - List> pageEntryFields = new ArrayList<>(getCwmsTsIdFields()); + List> pageEntryFields = new ArrayList<>(getCwmsTsIdFieldsToIncludeInQuery(cwmsTsIdFields)); if (params.isIncludeExtents()) { pageEntryFields.addAll(getExtentsFields()); } List whereConditions = buildWhereConditions(params); - List pagingConditions = buildPagingConditions(cursorOffice, cursorTsId); - CommonTableExpression limiter = buildWithClause(params, whereConditions, pagingConditions, pageSize, false); - Field limiterCode = limiter.field(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE); + List pagingConditions = buildPagingConditions(cwmsTsIdFields, cursorOffice, cursorTsId); + CommonTableExpression limiter = buildWithClause(cwmsTsIdFields, params, whereConditions, pagingConditions, pageSize, false); + Field limiterCode = limiter.field(cwmsTsIdFields.getTsCode()); SelectJoinStep tmpQuery = dsl.with(limiter) .select(pageEntryFields) .from(limiter) - .join(AV_CWMS_TS_ID.AV_CWMS_TS_ID) - .on(limiterCode.eq(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE)); + .join(table).on(limiterCode.eq(cwmsTsIdFields.getTsCode())); if (params.isIncludeExtents()) { @@ -566,30 +569,43 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar .eq(AV_TS_EXTENTS_UTC.TS_CODE.coerce(limiterCode))); } final SelectSeekStep2 overallQuery = tmpQuery - .orderBy(AV_CWMS_TS_ID.AV_CWMS_TS_ID.DB_OFFICE_ID, - AV_CWMS_TS_ID.AV_CWMS_TS_ID.CWMS_TS_ID); + .orderBy(cwmsTsIdFields.getDbOfficeId(), + cwmsTsIdFields.getCwmsTsId()); logger.fine(() -> overallQuery.getSQL(ParamType.INLINED)); Result result = overallQuery.fetch(); Map tsIdExtentMap = new LinkedHashMap<>(); + Map> tsCodeAliasMap = new LinkedHashMap<>(); + Map tsIdToCodeMap = new LinkedHashMap<>(); //this is only for non-aliases + boolean includeAliases = params.includeAliases(); result.forEach(row -> { - String officeTsId = row.get(AV_CWMS_TS_ID.AV_CWMS_TS_ID.DB_OFFICE_ID) + String officeTsId = row.get(cwmsTsIdFields.getDbOfficeId()) + "/" - + row.get(AV_CWMS_TS_ID.AV_CWMS_TS_ID.CWMS_TS_ID); + + row.get(cwmsTsIdFields.getCwmsTsId()); if (!tsIdExtentMap.containsKey(officeTsId)) { TimeseriesCatalogEntry.Builder builder = new TimeseriesCatalogEntry.Builder() - .officeId(row.get(AV_CWMS_TS_ID.AV_CWMS_TS_ID.DB_OFFICE_ID)) - .cwmsTsId(row.get(AV_CWMS_TS_ID.AV_CWMS_TS_ID.CWMS_TS_ID)) - .units(row.get(AV_CWMS_TS_ID.AV_CWMS_TS_ID.UNIT_ID)) - .interval(row.get(AV_CWMS_TS_ID.AV_CWMS_TS_ID.INTERVAL_ID)) - .intervalOffset(row.get(AV_CWMS_TS_ID.AV_CWMS_TS_ID.INTERVAL_UTC_OFFSET)); + .officeId(row.get(cwmsTsIdFields.getDbOfficeId())) + .cwmsTsId(row.get(cwmsTsIdFields.getCwmsTsId())) + .units(row.get(cwmsTsIdFields.getUnitId())) + .interval(row.get(cwmsTsIdFields.getIntervalId())) + .intervalOffset(row.get(cwmsTsIdFields.getIntervalUtcOffset())); builder.timeZone(row.get("TIME_ZONE_ID", String.class)); if (params.isIncludeExtents()) { builder.withExtents(new ArrayList<>()); } - tsIdExtentMap.put(officeTsId, builder); + if(includeAliases) { + if(row.get(AV_CWMS_TS_ID2.ALIASED_ITEM) == null) { + tsIdExtentMap.put(officeTsId, builder); //only add non-aliases... aliases get added as a node to each entry later + } + } else { + tsIdExtentMap.put(officeTsId, builder); + } + + } + if(includeAliases) { + updateAliasMapping(tsCodeAliasMap, tsIdToCodeMap, row, officeTsId); } if (params.isIncludeExtents()) { @@ -599,10 +615,17 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar .withLastUpdate(DateUtils.toZdt(row.get(AV_TS_EXTENTS_UTC.LAST_UPDATE))) .withVersionTime(DateUtils.toZdt(row.get(AV_TS_EXTENTS_UTC.VERSION_TIME))) .build(); - tsIdExtentMap.get(officeTsId).withExtent(extents); + TimeseriesCatalogEntry.Builder entryBuilder = tsIdExtentMap.get(officeTsId); + if(entryBuilder != null) { + entryBuilder.withExtent(extents); + } } }); + if(includeAliases) { + addAliasesToBuilders(tsIdExtentMap, tsCodeAliasMap, tsIdToCodeMap); + } + List entries = tsIdExtentMap.values().stream() .map(TimeseriesCatalogEntry.Builder::build) .collect(Collectors.toList()); @@ -611,18 +634,48 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar total, pageSize, entries, params); } - private static @NotNull List buildPagingConditions(String cursorOffice, String cursorTsId) { + + private void addAliasesToBuilders(Map tsIdExtentMap, Map> tsCodeAliasMap, Map tsIdToCodeMap) { + for (Map.Entry> entry : tsCodeAliasMap.entrySet()) { + String tsCode = entry.getKey(); + Set aliases = entry.getValue(); + for (Map.Entry e : tsIdToCodeMap.entrySet()) { + String tsId = e.getKey(); + String code = e.getValue(); + if (code.equals(tsCode)) { + tsIdExtentMap.get(tsId).withAliases(aliases); + break; + } + } + } + } + + private void updateAliasMapping(Map> tsCodeAliasMap, Map tsIdToCodeMap, Record row, String officeTsId) { + boolean isAlias = row.get(AV_CWMS_TS_ID2.ALIASED_ITEM) != null; + String tsCode = row.get(AV_CWMS_TS_ID2.TS_CODE).toString(); + if(isAlias) { + tsCodeAliasMap.computeIfAbsent(tsCode, k -> new HashSet<>()) + .add(new TimeSeriesAlias.Builder() + .withName(row.get(AV_CWMS_TS_ID2.TS_ALIAS_CATEGORY) + "-" + row.get(AV_CWMS_TS_ID2.TS_ALIAS_GROUP)) + .withValue(row.get(AV_CWMS_TS_ID2.CWMS_TS_ID)) + .build()); + } else { + tsIdToCodeMap.put(officeTsId, tsCode); + } + } + + private static @NotNull List buildPagingConditions(FieldMapping cwmsTsIdFields, String cursorOffice, String cursorTsId) { List pagingConditions = new ArrayList<>(); // 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) { - Condition moreInSameOffice = AV_CWMS_TS_ID.AV_CWMS_TS_ID.DB_OFFICE_ID + Condition moreInSameOffice = cwmsTsIdFields.getDbOfficeId() .eq(cursorOffice.toUpperCase()) - .and(DSL.upper(AV_CWMS_TS_ID.AV_CWMS_TS_ID.CWMS_TS_ID) + .and(DSL.upper(cwmsTsIdFields.getCwmsTsId()) .greaterThan(cursorTsId)); - Condition nextOffice = AV_CWMS_TS_ID.AV_CWMS_TS_ID.DB_OFFICE_ID + Condition nextOffice = cwmsTsIdFields.getDbOfficeId() .greaterThan(cursorOffice.toUpperCase()); pagingConditions.add(moreInSameOffice.or(nextOffice)); } @@ -638,16 +691,21 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar return extentsFields; } - private @NotNull List> getCwmsTsIdFields() { - List> cwmsTsIdFields = new ArrayList<>(); - cwmsTsIdFields.add(AV_CWMS_TS_ID.AV_CWMS_TS_ID.DB_OFFICE_ID); - cwmsTsIdFields.add(AV_CWMS_TS_ID.AV_CWMS_TS_ID.CWMS_TS_ID); - cwmsTsIdFields.add(AV_CWMS_TS_ID.AV_CWMS_TS_ID.UNIT_ID); - cwmsTsIdFields.add(AV_CWMS_TS_ID.AV_CWMS_TS_ID.INTERVAL_ID); - cwmsTsIdFields.add(AV_CWMS_TS_ID.AV_CWMS_TS_ID.INTERVAL_UTC_OFFSET); - cwmsTsIdFields.add(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TIME_ZONE_ID); - - return cwmsTsIdFields; + private @NotNull List> getCwmsTsIdFieldsToIncludeInQuery(FieldMapping cwmsTsIdFields) { + List> retVal = new ArrayList<>(); + retVal.add(cwmsTsIdFields.getDbOfficeId()); + retVal.add(cwmsTsIdFields.getCwmsTsId()); + retVal.add(cwmsTsIdFields.getUnitId()); + retVal.add(cwmsTsIdFields.getIntervalId()); + retVal.add(cwmsTsIdFields.getIntervalUtcOffset()); + retVal.add(cwmsTsIdFields.getTimeZoneId()); + if(cwmsTsIdFields.includesAliases()) { + retVal.add(AV_CWMS_TS_ID2.ALIASED_ITEM); + retVal.add(AV_CWMS_TS_ID2.TS_CODE); + retVal.add(AV_CWMS_TS_ID2.TS_ALIAS_CATEGORY); + retVal.add(AV_CWMS_TS_ID2.TS_ALIAS_GROUP); + } + return retVal; } private @NotNull List buildWhereConditions(CatalogRequestParameters params) { @@ -660,34 +718,33 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar return conditions; } - private static @NotNull CommonTableExpression buildWithClause(CatalogRequestParameters params, - List whereConditions, List pagingConditions, int pageSize, boolean forCount) { - TableLike fromTable = AV_CWMS_TS_ID.AV_CWMS_TS_ID; - + private static @NotNull CommonTableExpression buildWithClause(FieldMapping cwmsTsIdFields, CatalogRequestParameters params, + List whereConditions, List pagingConditions, int pageSize, boolean forCount) { + Table fromTable = params.includeAliases() ? AV_CWMS_TS_ID2 : AV_CWMS_TS_ID.AV_CWMS_TS_ID; List> selectFields = new ArrayList<>(); - selectFields.add(fromTable.field(cwmsTsIdView.TS_CODE)); - selectFields.add(fromTable.field(cwmsTsIdView.DB_OFFICE_ID)); + selectFields.add(fromTable.field(cwmsTsIdFields.getTsCode())); + selectFields.add(fromTable.field(cwmsTsIdFields.getDbOfficeId())); - selectFields.add(fromTable.field(cwmsTsIdView.CWMS_TS_ID)); + selectFields.add(fromTable.field(cwmsTsIdFields.getCwmsTsId())); TableOnConditionStep on = null; - + Table table = params.includeAliases() ? AV_CWMS_TS_ID2 : AV_CWMS_TS_ID.AV_CWMS_TS_ID; if (params.needs(tsGroupView)) { - on = AV_CWMS_TS_ID.AV_CWMS_TS_ID + on = table .join(tsGroupView) - .on(cwmsTsIdView.TS_CODE.eq(tsGroupView.TS_CODE)); + .on(cwmsTsIdFields.getTsCode().eq(tsGroupView.TS_CODE)); fromTable = on; } if (params.needs(AV_LOC_GRP_ASSGN.AV_LOC_GRP_ASSGN)) { if (on == null) { - on = AV_CWMS_TS_ID.AV_CWMS_TS_ID + on = table .leftJoin(AV_LOC_GRP_ASSGN.AV_LOC_GRP_ASSGN) - .on(AV_CWMS_TS_ID.AV_CWMS_TS_ID.LOCATION_CODE + .on(cwmsTsIdFields.getLocationCode() .eq(AV_LOC_GRP_ASSGN.AV_LOC_GRP_ASSGN.LOCATION_CODE)); } else { on = on .leftJoin(AV_LOC_GRP_ASSGN.AV_LOC_GRP_ASSGN) - .on(AV_CWMS_TS_ID.AV_CWMS_TS_ID.LOCATION_CODE + .on(cwmsTsIdFields.getLocationCode() .eq(AV_LOC_GRP_ASSGN.AV_LOC_GRP_ASSGN.LOCATION_CODE)); } fromTable = on; @@ -695,16 +752,16 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar if (params.needs(AV_LOC.AV_LOC)) { if (on == null) { - on = AV_CWMS_TS_ID.AV_CWMS_TS_ID + on = table .leftJoin(AV_LOC.AV_LOC) .on(AV_LOC.AV_LOC.LOCATION_CODE - .eq(AV_CWMS_TS_ID.AV_CWMS_TS_ID.LOCATION_CODE + .eq(cwmsTsIdFields.getLocationCode() .coerce(AV_LOC.AV_LOC.LOCATION_CODE))); } else { on = on .leftJoin(AV_LOC.AV_LOC) .on(AV_LOC.AV_LOC.LOCATION_CODE - .eq(AV_CWMS_TS_ID.AV_CWMS_TS_ID.LOCATION_CODE + .eq(cwmsTsIdFields.getLocationCode() .coerce(AV_LOC.AV_LOC.LOCATION_CODE))); } selectFields.add(AV_LOC.AV_LOC.BOUNDING_OFFICE_ID); @@ -713,17 +770,17 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar if (params.isExcludeEmpty()) { if (on == null) { - on = AV_CWMS_TS_ID.AV_CWMS_TS_ID + on = table .leftJoin(AV_TS_EXTENTS_UTC) - .on(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE + .on(cwmsTsIdFields.getTsCode() .eq(AV_TS_EXTENTS_UTC.TS_CODE - .coerce(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE))); + .coerce(cwmsTsIdFields.getTsCode()))); } else { on = on .leftJoin(AV_TS_EXTENTS_UTC) - .on(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE + .on(cwmsTsIdFields.getTsCode() .eq(AV_TS_EXTENTS_UTC.TS_CODE - .coerce(AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE))); + .coerce(cwmsTsIdFields.getTsCode()))); } fromTable = on; } @@ -731,23 +788,23 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar TableLike innerSelect = selectDistinct(selectFields) .from(fromTable) .where(whereConditions).and(DSL.and(pagingConditions)) - .orderBy(AV_CWMS_TS_ID.AV_CWMS_TS_ID.DB_OFFICE_ID, - AV_CWMS_TS_ID.AV_CWMS_TS_ID.CWMS_TS_ID) + .orderBy(cwmsTsIdFields.getDbOfficeId(), + cwmsTsIdFields.getCwmsTsId()) .asTable("limiterInner"); if (forCount) { return name("limiter").as( select(asterisk()) .from(innerSelect) - .orderBy(innerSelect.field(AV_CWMS_TS_ID.AV_CWMS_TS_ID.DB_OFFICE_ID), - innerSelect.field(AV_CWMS_TS_ID.AV_CWMS_TS_ID.CWMS_TS_ID)) + .orderBy(innerSelect.field(cwmsTsIdFields.getDbOfficeId()), + innerSelect.field(cwmsTsIdFields.getCwmsTsId())) ); } else { return name("limiter").as( select(asterisk()) .from(innerSelect) .where(field("rownum").lessOrEqual(pageSize)) - .orderBy(innerSelect.field(AV_CWMS_TS_ID.AV_CWMS_TS_ID.DB_OFFICE_ID), - innerSelect.field(AV_CWMS_TS_ID.AV_CWMS_TS_ID.CWMS_TS_ID)) + .orderBy(innerSelect.field(cwmsTsIdFields.getDbOfficeId()), + innerSelect.field(cwmsTsIdFields.getCwmsTsId())) ); } } @@ -808,14 +865,14 @@ private Collection buildTsGrpAssgnConditions(CatalogRequest private Collection buildCwmsTsIdConditions(CatalogRequestParameters params) { List retval = new ArrayList<>(); - + FieldMapping cwmsTsIdFields = params.includeAliases() ? AV_CWMS_TS_ID2_FIELD_MAP : AV_CWMS_TS_ID_FIELD_MAP; if (params.getOffice() != null) { - retval.add(AV_CWMS_TS_ID.AV_CWMS_TS_ID.DB_OFFICE_ID.eq(params.getOffice().toUpperCase())); + retval.add(cwmsTsIdFields.getDbOfficeId().eq(params.getOffice().toUpperCase())); } retval.add( caseInsensitiveLikeRegexNullTrue( - AV_CWMS_TS_ID.AV_CWMS_TS_ID.CWMS_TS_ID, + cwmsTsIdFields.getCwmsTsId(), params.getIdLike())); return retval; @@ -1386,4 +1443,110 @@ public DeleteOptions build() { } } + private interface FieldMapping { + Field getTsCode(); + Field getLocationCode(); + Field getDbOfficeId(); + Field getCwmsTsId(); + Field getUnitId(); + Field getIntervalId(); + Field getIntervalUtcOffset(); + Field getTimeZoneId(); + boolean includesAliases(); + } + + private static class CwmsTsIdFieldMapping implements FieldMapping { + @Override + public Field getTsCode() { + return AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE; + } + + @Override + public Field getLocationCode() { + return AV_CWMS_TS_ID.AV_CWMS_TS_ID.LOCATION_CODE; + } + + @Override + public Field getDbOfficeId() { + return AV_CWMS_TS_ID.AV_CWMS_TS_ID.DB_OFFICE_ID; + } + + @Override + public Field getCwmsTsId() { + return AV_CWMS_TS_ID.AV_CWMS_TS_ID.CWMS_TS_ID; + } + + @Override + public Field getUnitId() { + return AV_CWMS_TS_ID.AV_CWMS_TS_ID.UNIT_ID; + } + + @Override + public Field getIntervalId() { + return AV_CWMS_TS_ID.AV_CWMS_TS_ID.INTERVAL_ID; + } + + @Override + public Field getIntervalUtcOffset() { + return AV_CWMS_TS_ID.AV_CWMS_TS_ID.INTERVAL_UTC_OFFSET; + } + + @Override + public Field getTimeZoneId() { + return AV_CWMS_TS_ID.AV_CWMS_TS_ID.TIME_ZONE_ID; + } + + @Override + public boolean includesAliases() { + return false; + } + } + + private static class CwmsTsId2FieldMapping implements FieldMapping { + @Override + public Field getTsCode() { + return AV_CWMS_TS_ID2.TS_CODE; + } + + @Override + public Field getLocationCode() { + return AV_CWMS_TS_ID2.LOCATION_CODE; + } + + @Override + public Field getDbOfficeId() { + return AV_CWMS_TS_ID2.DB_OFFICE_ID; + } + + @Override + public Field getCwmsTsId() { + return AV_CWMS_TS_ID2.CWMS_TS_ID; + } + + @Override + public Field getUnitId() { + return AV_CWMS_TS_ID2.UNIT_ID; + } + + @Override + public Field getIntervalId() { + return AV_CWMS_TS_ID2.INTERVAL_ID; + } + + @Override + public Field getIntervalUtcOffset() { + return AV_CWMS_TS_ID2.INTERVAL_UTC_OFFSET; + } + + @Override + public Field getTimeZoneId() { + return AV_CWMS_TS_ID2.TIME_ZONE_ID; + } + + @Override + public boolean includesAliases() { + return true; + } + } + } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/catalog/TimeSeriesAlias.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/catalog/TimeSeriesAlias.java new file mode 100644 index 0000000000..5dae448608 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/catalog/TimeSeriesAlias.java @@ -0,0 +1,76 @@ +package cwms.cda.data.dto.catalog; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV2; +import java.util.Objects; + +@JsonRootName("alias") +@FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class, aliases = {Formats.DEFAULT, Formats.JSON}) +@JsonDeserialize(builder = TimeSeriesAlias.Builder.class) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) +public final class TimeSeriesAlias { + + @JacksonXmlProperty(isAttribute = true) + private final String name; + private final String value; + + private TimeSeriesAlias(Builder builder) { + this.name = builder.name; + this.value = builder.value; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TimeSeriesAlias that = (TimeSeriesAlias) o; + return Objects.equals(name, that.name) && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(name, value); + } + + public static final class Builder { + private String name; + private String value; + + public Builder() { + } + + public Builder withName(String name) { + this.name = name; + return this; + } + + public Builder withValue(String value) { + this.value = value; + return this; + } + + public TimeSeriesAlias build() { + return new TimeSeriesAlias(this); + } + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/catalog/TimeseriesCatalogEntry.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/catalog/TimeseriesCatalogEntry.java index 1493288f9a..44a3c8e613 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/catalog/TimeseriesCatalogEntry.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/catalog/TimeseriesCatalogEntry.java @@ -3,6 +3,7 @@ import java.math.BigDecimal; import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import com.fasterxml.jackson.databind.PropertyNamingStrategies; @@ -29,6 +30,9 @@ public class TimeseriesCatalogEntry extends CatalogEntry { @JacksonXmlProperty(localName = "extents") private List extents; + @JacksonXmlElementWrapper(localName = "aliases") + @JacksonXmlProperty(localName = "alias") + private Collection aliases; public String getName() { return this.name; @@ -52,18 +56,23 @@ public List getExtents() { return extents; } + public Collection getAliases() { + return aliases; + } + private TimeseriesCatalogEntry() { super(null); } - private TimeseriesCatalogEntry(String office, String name, String units, String interval, Long intervalOffset, String timeZone, List extents) { - super(office); - this.name = name; - this.units = units; - this.interval = interval; - this.intervalOffset = intervalOffset; - this.timeZone = timeZone; - this.extents = extents; + private TimeseriesCatalogEntry(Builder builder) { + super(builder.office); + this.name = builder.tsName; + this.units = builder. units; + this.interval = builder.interval; + this.intervalOffset = builder.intervalOffset; + this.timeZone = builder.timeZone; + this.extents = builder.extents; + this.aliases = builder.aliases; } public String getUnits() { @@ -90,6 +99,7 @@ public static class Builder { private ZonedDateTime earliestTime; private ZonedDateTime latestTime; private List extents = null; + private Collection aliases = null; public Builder officeId(final String office) { this.office = office; @@ -143,8 +153,17 @@ public Builder withExtents(final List newExtents) { return this; } + public Builder withAliases(final Collection aliases) { + if (aliases == null) { + this.aliases = null; + } else { + this.aliases = new ArrayList<>(aliases); + } + return this; + } + public TimeseriesCatalogEntry build() { - return new TimeseriesCatalogEntry(office, tsName, units, interval, intervalOffset, timeZone, extents); + return new TimeseriesCatalogEntry(this); } } } 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 085fdd196a..f357b8f646 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 @@ -87,7 +87,7 @@ public T parseContent(InputStream content, Class type } } - private static @NotNull XmlMapper buildObjectMapper() { + public static @NotNull XmlMapper buildObjectMapper() { XmlMapper retval = new XmlMapper(); retval.findAndRegisterModules(); diff --git a/cwms-data-api/src/test/java/cwms/cda/api/CatalogControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/CatalogControllerTestIT.java index 88e89f9952..e87e647c9a 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/CatalogControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/CatalogControllerTestIT.java @@ -1,13 +1,24 @@ package cwms.cda.api; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import static cwms.cda.api.Controllers.BOUNDING_OFFICE_LIKE; import static cwms.cda.api.Controllers.EXCLUDE_EMPTY; +import static cwms.cda.api.Controllers.INCLUDE_ALIASES; import static cwms.cda.api.Controllers.LIKE; import static cwms.cda.api.Controllers.LOCATION_CATEGORY_LIKE; import static cwms.cda.api.Controllers.LOCATION_GROUP_LIKE; import static cwms.cda.api.Controllers.LOCATION_KIND_LIKE; import static cwms.cda.api.Controllers.TIMESERIES_CATEGORY_LIKE; import static cwms.cda.api.Controllers.TIMESERIES_GROUP_LIKE; +import cwms.cda.data.dto.catalog.TimeSeriesAlias; +import cwms.cda.data.dto.catalog.TimeseriesCatalogEntry; +import cwms.cda.formatters.json.JsonV2; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.*; import cwms.cda.data.dao.DeleteRule; @@ -125,6 +136,80 @@ void test_no_aliased_results_returned() { .body("entries.size()",is(4)); } + @Test + void test_no_aliases_returned() { + Integer numAliases = given().accept(Formats.JSONV2) + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(EXCLUDE_EMPTY, false) + .when() + .get("/catalog/TIMESERIES") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(200)) + .extract() + .jsonPath() + .getObject("entries.aliases.aliases.size()", Integer.class); + assertEquals(0, (int) numAliases, "Expected no aliases, but found some."); + } + + @Test + void test_aliases_returned() { + Integer numAliases = given().accept(Formats.JSONV2) + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(EXCLUDE_EMPTY,false) + .queryParam(INCLUDE_ALIASES,true) + .when() + .get("/catalog/TIMESERIES") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(200)) + .extract() + .jsonPath() + .getObject("entries.aliases.aliases.size()", Integer.class); + assertTrue(numAliases > 0, "Expected aliases, but found none."); + } + + @Test + void test_alias_is_correct() throws JsonProcessingException { + Response response = given().accept(Formats.JSONV2) + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(EXCLUDE_EMPTY, false) + .queryParam(INCLUDE_ALIASES, true) + .when() + .get("/catalog/TIMESERIES"); + String json = response.body().asPrettyString(); + ObjectMapper om = JsonV2.buildObjectMapper(); + JsonNode root = om.readTree(json); + JsonNode entriesNode = root.get("entries"); + String entriesJson = om.writeValueAsString(entriesNode); + List entries = om.readValue(entriesJson, new TypeReference>() {}); + assertNotNull(entries); + TimeseriesCatalogEntry alias = entries + .stream() + .filter(Objects::nonNull) + .collect(Collectors.toSet()).stream() + .filter(e -> e.getName().equals("Pine Flat-Outflow.Stage.Inst.15Minutes.0.one")) + .findFirst() + .orElse(null); + assertNotNull(alias); + assertTrue(alias.getAliases().contains(new TimeSeriesAlias.Builder() + .withName("Test Category-LessThan3") + .withValue("test alias 1") + .build())); + //make sure no entries exist with name "test alias 1" + List aliasesAsAnEntry = entries + .stream() + .filter(Objects::nonNull) + .filter(e -> e.getName().equals("test alias 1")) + .collect(Collectors.toList()); + assertTrue(aliasesAsAnEntry.isEmpty(), "Found entries with name 'test alias 1', which should not exist."); + } + @Test void test_queries_are_case_insensitive() { diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/catalog/TimeSeriesAliasTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/catalog/TimeSeriesAliasTest.java new file mode 100644 index 0000000000..e3683f6aad --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/catalog/TimeSeriesAliasTest.java @@ -0,0 +1,57 @@ +package cwms.cda.data.dto.catalog; + +import com.fasterxml.jackson.core.JsonProcessingException; +import cwms.cda.formatters.json.JsonV2; +import cwms.cda.formatters.xml.XMLv1; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.apache.commons.io.IOUtils; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.Test; + +final class TimeSeriesAliasTest { + + @Test + void testJsonDeserialization() throws IOException { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/time-series-alias.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + TimeSeriesAlias deserialized = JsonV2.buildObjectMapper().readValue(json, TimeSeriesAlias.class); + assertEquals(deserialized.getName(), "Test Category-LessThan3"); + assertEquals(deserialized.getValue(), "test alias 1"); + } + + @Test + void testJsonSerializationRoundTrip() throws JsonProcessingException { + TimeSeriesAlias alias = new TimeSeriesAlias.Builder() + .withName("Test Category-LessThan3") + .withValue("test alias 1") + .build(); + + String json = JsonV2.buildObjectMapper().writeValueAsString(alias); + + assertNotNull(json); + + TimeSeriesAlias returned = JsonV2.buildObjectMapper().readValue(json, TimeSeriesAlias.class); + + assertEquals(alias, returned); + } + + @Test + void testXmlSerializationRoundTip() throws JsonProcessingException { + TimeSeriesAlias alias = new TimeSeriesAlias.Builder() + .withName("Test Category-LessThan3") + .withValue("test alias 1") + .build(); + + String xml = XMLv1.buildObjectMapper().writeValueAsString(alias); + + assertNotNull(xml); + + TimeSeriesAlias returned = XMLv1.buildObjectMapper().readValue(xml, TimeSeriesAlias.class); + + assertEquals(alias, returned); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/catalog/TimeseriesCatalogEntryTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/catalog/TimeseriesCatalogEntryTest.java index 03298ffca8..b729161f48 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/catalog/TimeseriesCatalogEntryTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/catalog/TimeseriesCatalogEntryTest.java @@ -1,10 +1,18 @@ package cwms.cda.data.dto.catalog; +import cwms.cda.formatters.json.JsonV2; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.time.ZonedDateTime; import java.util.ArrayList; import io.restassured.path.json.JsonPath; import io.restassured.path.xml.XmlPath; +import java.util.Arrays; +import java.util.Collection; +import org.apache.commons.io.IOUtils; +import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; import cwms.cda.data.dto.Catalog; @@ -42,6 +50,10 @@ void test_xml_serialization_earliest(){ Object tmp = path.get("catalog.entries.entry.extents"); assertThat(path.getString("catalog.entries.entry.extents.extents.earliest-time"), equalTo("2017-07-27T05:00:00Z")); assertThat(path.getString("catalog.entries.entry.extents.extents.latest-time"), equalTo("2017-11-24T22:30:00Z")); + assertThat(path.getString("catalog.entries.entry.aliases.alias[0].@name"), equalTo("alias1")); + assertThat(path.getString("catalog.entries.entry.aliases.alias[0].value"), equalTo("value1")); + assertThat(path.getString("catalog.entries.entry.aliases.alias[1].@name"), equalTo("alias2")); + assertThat(path.getString("catalog.entries.entry.aliases.alias[1].value"), equalTo("value2")); } @@ -66,6 +78,10 @@ void test_json_serialization_earliest(){ assertThat(path.getString("entries[0].time-zone"), equalTo("US/Central")); assertThat(path.getString("entries[0].extents[0].earliest-time"), equalTo("2017-07-27T05:00:00Z")); assertThat(path.getString("entries[0].extents[0].latest-time"), equalTo("2017-11-24T22:30:00Z")); + assertThat(path.getString("entries[0].aliases[0].name"), equalTo("alias1")); + assertThat(path.getString("entries[0].aliases[0].value"), equalTo("value1")); + assertThat(path.getString("entries[0].aliases[1].name"), equalTo("alias2")); + assertThat(path.getString("entries[0].aliases[1].value"), equalTo("value2")); } @@ -100,6 +116,22 @@ void test_xml_serialization_no_cursor() { assertFalse(xml.contains("cursor")); } + @Test + void test_json_deserialization() throws IOException { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/time-series-catalog-entry.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + TimeseriesCatalogEntry deserialized = JsonV2.buildObjectMapper().readValue(json, TimeseriesCatalogEntry.class); + assertEquals("Pine Flat-Outflow.Stage.Inst.15Minutes.0.one", deserialized.getName()); + assertEquals("SPK", deserialized.getOffice()); + assertEquals("m", deserialized.getUnits()); + assertEquals("15Minutes", deserialized.getInterval()); + Collection aliases = deserialized.getAliases(); + assertEquals(1, aliases.size()); + TimeSeriesAlias alias = aliases.iterator().next(); + assertEquals("Test Category-LessThan3", alias.getName()); + assertEquals("test alias 1", alias.getValue()); + } private TimeseriesCatalogEntry buildEntry() { @@ -113,7 +145,10 @@ private TimeseriesCatalogEntry buildEntry() .withEarliestTime(ZonedDateTime.parse("2017-07-27T05:00:00Z")) .withLatestTime(ZonedDateTime.parse("2017-11-24T22:30:00Z")) .withLastUpdate(ZonedDateTime.parse("2017-11-24T22:30:00Z")) - .build()); + .build()) + .withAliases(Arrays.asList( + new TimeSeriesAlias.Builder().withName("alias1").withValue("value1").build(), + new TimeSeriesAlias.Builder().withName("alias2").withValue("value2").build())); return builder .build(); diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/time-series-alias.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/time-series-alias.json new file mode 100644 index 0000000000..68d3891f3f --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/time-series-alias.json @@ -0,0 +1,4 @@ +{ + "name":"Test Category-LessThan3", + "value":"test alias 1" +} \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/time-series-catalog-entry.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/time-series-catalog-entry.json new file mode 100644 index 0000000000..1bfff729b9 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/time-series-catalog-entry.json @@ -0,0 +1,18 @@ +{ + "office": "SPK", + "name": "Pine Flat-Outflow.Stage.Inst.15Minutes.0.one", + "units": "m", + "interval": "15Minutes", + "interval-offset": 2147483647, + "time-zone": "UTC", + "extents": [ + {}, + {} + ], + "aliases": [ + { + "name": "Test Category-LessThan3", + "value": "test alias 1" + } + ] +} \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/sql/ts_catalog_setup.sql b/cwms-data-api/src/test/resources/cwms/cda/data/sql/ts_catalog_setup.sql index b956e4e586..6b953da329 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/data/sql/ts_catalog_setup.sql +++ b/cwms-data-api/src/test/resources/cwms/cda/data/sql/ts_catalog_setup.sql @@ -135,7 +135,7 @@ begin p_ts_group_id=>'LessThan3', p_ts_id=>'Pine Flat-Outflow.Stage.Inst.15Minutes.0.one', p_ts_attribute=>0, - p_ts_alias_id=>null, + p_ts_alias_id=>'test alias 1', p_ref_ts_id=>NULL, p_db_office_id=>l_office);