Skip to content

Commit a594b55

Browse files
authored
REGI-470: Added lightweight catalog endpoint for location + location kind (#1230)
Adds support for lightweight data retrieval on locations getAll endpoint. Returns location ID, office ID, and location kind ID. To be used by REGI in AtLocationManager::retrieveAllLocationTemplatesWithKinds method.
1 parent 41aa582 commit a594b55

10 files changed

Lines changed: 436 additions & 6 deletions

File tree

cwms-data-api/src/main/java/cwms/cda/ApiServlet.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
import cwms.cda.api.project.UpdateLockRevokerRights;
139139
import cwms.cda.api.rating.ReverseRateTimeSeriesController;
140140
import cwms.cda.api.rating.ReverseRateValuesController;
141+
import cwms.cda.api.LocationKindController;
141142
import cwms.cda.api.watersupply.AccountingCatalogController;
142143
import cwms.cda.api.watersupply.AccountingCreateController;
143144
import cwms.cda.api.timeseriesprofile.TimeSeriesProfileCatalogController;
@@ -532,6 +533,7 @@ protected void configureRoutes() {
532533
new LocationCategoryController(metrics), requiredRoles, 5, TimeUnit.MINUTES);
533534
cdaCrudCache("/location/group/{group-id}",
534535
new LocationGroupController(metrics), requiredRoles, 5, TimeUnit.MINUTES);
536+
get("/locations/with-kinds/", new LocationKindController(metrics));
535537
cdaCrudCache("/locations/{location-id}",
536538
new LocationController(metrics), requiredRoles, 5, TimeUnit.MINUTES);
537539
cdaCrudCache("/states/{state}",
@@ -703,7 +705,6 @@ protected void configureRoutes() {
703705
addProjectLocksHandlers("/project-locks/{name}", requiredRoles);
704706
addProjectLockRightsHandlers("/project-lock-rights/{project-id}", requiredRoles);
705707

706-
707708
addUserManagementHandlers();
708709
}
709710

cwms-data-api/src/main/java/cwms/cda/api/Controllers.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ public final class Controllers {
156156
public static final String AGENCY = "agency";
157157
public static final String QUALITY = "quality";
158158
public static final String NAMES = "names";
159-
public static final String EXCLUDE_KINDS = "exclude-kinds";
160159
public static final String FILTER_BASE_LOCATIONS = "filter-base-locations";
161160

162161
public static final String GROUP_ID = "group-id";
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
*
3+
* MIT License
4+
*
5+
* Copyright (c) 2025 Hydrologic Engineering Center
6+
*
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy
8+
* of this software and associated documentation files (the "Software"), to deal
9+
* in the Software without restriction, including without limitation the rights
10+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the Software is
12+
* furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in all
15+
* copies or substantial portions of the Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23+
* DEALINGS IN THE
24+
* SOFTWARE.
25+
*/
26+
27+
package cwms.cda.api;
28+
29+
import static com.codahale.metrics.MetricRegistry.name;
30+
import static cwms.cda.api.Controllers.LOCATION_KIND_LIKE;
31+
import static cwms.cda.api.Controllers.NAMES;
32+
import static cwms.cda.api.Controllers.OFFICE;
33+
import static cwms.cda.api.Controllers.RESULTS;
34+
import static cwms.cda.api.Controllers.SIZE;
35+
import static cwms.cda.api.Controllers.STATUS_200;
36+
import static cwms.cda.api.LocationController.getLocationsDao;
37+
import static cwms.cda.data.dao.JooqDao.getDslContext;
38+
39+
import com.codahale.metrics.Histogram;
40+
import com.codahale.metrics.MetricRegistry;
41+
import cwms.cda.data.dao.LocationsDao;
42+
import cwms.cda.data.dto.CwmsIdLocationKind;
43+
import cwms.cda.data.dto.Location;
44+
import cwms.cda.formatters.ContentType;
45+
import cwms.cda.formatters.Formats;
46+
import io.javalin.core.util.Header;
47+
import io.javalin.http.Context;
48+
import io.javalin.http.Handler;
49+
import io.javalin.plugin.openapi.annotations.HttpMethod;
50+
import io.javalin.plugin.openapi.annotations.OpenApi;
51+
import io.javalin.plugin.openapi.annotations.OpenApiContent;
52+
import io.javalin.plugin.openapi.annotations.OpenApiParam;
53+
import io.javalin.plugin.openapi.annotations.OpenApiResponse;
54+
import java.util.List;
55+
import javax.servlet.http.HttpServletResponse;
56+
import org.jetbrains.annotations.NotNull;
57+
import org.jooq.DSLContext;
58+
59+
public class LocationKindController implements Handler {
60+
public static final String TAG = "REGI";
61+
62+
private final Histogram requestResultSize;
63+
64+
public LocationKindController(MetricRegistry metrics) {
65+
MetricRegistry controllerMetrics = metrics;
66+
String className = this.getClass().getName();
67+
requestResultSize = controllerMetrics.histogram((name(className, RESULTS, SIZE)));
68+
}
69+
70+
@OpenApi(
71+
queryParams = {
72+
@OpenApiParam(name = NAMES, description = "Specifies the name(s) of the "
73+
+ "location(s) whose data is to be included in the response. This parameter is a "
74+
+ "Posix <a href=\"regexp.html\">regular expression</a> matching against the id"),
75+
@OpenApiParam(name = LOCATION_KIND_LIKE, description = "Specifies the location kind(s) "
76+
+ "whose data is to be included in the response. This parameter is a "
77+
+ "Posix <a href=\"regexp.html\">regular expression</a> matching against the location kind. "
78+
+ "If this field is not specified, all location kinds shall be returned."),
79+
@OpenApiParam(name = OFFICE, description = "Specifies the owning office of "
80+
+ "the location level(s) whose data is to be included in the response"
81+
+ ". If this field is not specified, matching location level "
82+
+ "information from all offices shall be returned."),
83+
},
84+
responses = {
85+
@OpenApiResponse(status = STATUS_200,
86+
content = {
87+
@OpenApiContent(isArray = true, type = Formats.JSONV2, from = Location.class),
88+
})
89+
},
90+
description = "Returns CWMS Location Data. The Catalog end-point is also capable of "
91+
+ "retrieving lists of locations and can filter on additional fields.",
92+
method = HttpMethod.GET,
93+
path = "/locations/with-kind",
94+
tags = {TAG}
95+
)
96+
@Override
97+
public void handle(@NotNull Context ctx) {
98+
DSLContext dsl = getDslContext(ctx);
99+
100+
LocationsDao locationsDao = getLocationsDao(dsl);
101+
102+
String names = ctx.queryParam(NAMES);
103+
String kindRegexMask = ctx.queryParam(LOCATION_KIND_LIKE);
104+
String office = ctx.queryParam(OFFICE);
105+
106+
String formatParm = ctx.queryParamAsClass(Formats.JSONV2, String.class).getOrDefault("");
107+
String formatHeader = ctx.header(Header.ACCEPT);
108+
ContentType contentType = Formats.parseHeaderAndQueryParm(formatHeader, formatParm, Location.class);
109+
110+
String results;
111+
112+
List<CwmsIdLocationKind> locationKinds = locationsDao.getLocationKinds(names, kindRegexMask, office);
113+
results = Formats.format(contentType, locationKinds, CwmsIdLocationKind.class);
114+
ctx.result(results);
115+
requestResultSize.update(results.length());
116+
117+
ctx.contentType(contentType.toString());
118+
ctx.status(HttpServletResponse.SC_OK);
119+
}
120+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
package cwms.cda.data.dao;
2626

2727
import cwms.cda.data.dto.Catalog;
28+
import cwms.cda.data.dto.CwmsIdLocationKind;
2829
import cwms.cda.data.dto.Location;
2930
import java.io.IOException;
3031
import java.util.List;
@@ -35,6 +36,8 @@ public interface LocationsDao {
3536

3637
List<Location> getLocations(String names, String units, String datum, String officeId);
3738

39+
List<CwmsIdLocationKind> getLocationKinds(String idRegexMask, String kindRegexMask, String officeId);
40+
3841
Location getLocation(String locationName, String unitSystem, String officeId) throws IOException;
3942

4043
void deleteLocation(String locationName, String officeId);

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,13 @@
4747
import cwms.cda.api.errors.NotFoundException;
4848
import cwms.cda.data.dao.location.kind.LocationUtil;
4949
import cwms.cda.data.dto.Catalog;
50+
import cwms.cda.data.dto.CwmsId;
51+
import cwms.cda.data.dto.CwmsIdLocationKind;
5052
import cwms.cda.data.dto.Location;
5153
import cwms.cda.data.dto.catalog.CatalogEntry;
5254
import cwms.cda.data.dto.catalog.LocationAlias;
5355
import cwms.cda.data.dto.catalog.LocationCatalogEntry;
56+
import cwms.cda.helpers.ZoneIdHelper;
5457
import java.io.IOException;
5558
import java.math.BigDecimal;
5659
import java.time.ZoneId;
@@ -64,7 +67,6 @@
6467
import java.util.Set;
6568
import java.util.logging.Level;
6669
import java.util.logging.Logger;
67-
import cwms.cda.helpers.ZoneIdHelper;
6870
import org.geojson.Feature;
6971
import org.geojson.FeatureCollection;
7072
import org.geojson.Point;
@@ -131,6 +133,24 @@ public List<Location> getLocations(String nameRegex, String unitSystem, String d
131133
.fetch(this::buildLocation);
132134
}
133135

136+
@Override
137+
public List<CwmsIdLocationKind> getLocationKinds(String idRegexMask, String kindRegexMask, String officeId) {
138+
Condition whereCondition = JooqDao.caseInsensitiveLikeRegexNullTrue(AV_LOC.LOCATION_ID, idRegexMask);
139+
140+
whereCondition = whereCondition
141+
.and(JooqDao.caseInsensitiveLikeRegexNullTrue(AV_LOC.LOCATION_KIND_ID, kindRegexMask));
142+
143+
if (officeId != null) {
144+
whereCondition = whereCondition.and(AV_LOC.DB_OFFICE_ID.equalIgnoreCase(officeId));
145+
}
146+
147+
return dsl.selectDistinct(AV_LOC.LOCATION_ID, AV_LOC.DB_OFFICE_ID, AV_LOC.LOCATION_KIND_ID)
148+
.from(AV_LOC)
149+
.where(whereCondition)
150+
.fetchSize(DEFAULT_SMALL_FETCH_SIZE)
151+
.fetch(this::buildLocationKind);
152+
}
153+
134154
@Override
135155
public Location getLocation(String locationName, String unitSystem, String officeId) {
136156
Record loc = dsl.select(AV_LOC.asterisk())
@@ -146,6 +166,13 @@ public Location getLocation(String locationName, String unitSystem, String offic
146166
return buildLocation(loc);
147167
}
148168

169+
private CwmsIdLocationKind buildLocationKind(Record loc) {
170+
CwmsIdLocationKind.Builder builder = new CwmsIdLocationKind.Builder();
171+
builder.withLocationKindId(loc.get(AV_LOC.LOCATION_KIND_ID));
172+
builder.withLocationId(CwmsId.buildCwmsId(loc.get(AV_LOC.DB_OFFICE_ID), loc.get(AV_LOC.LOCATION_ID)));
173+
return builder.build();
174+
}
175+
149176
private Location buildLocation(Record loc) {
150177
String timeZoneName = loc.get(AV_LOC.TIME_ZONE_NAME); // may be null...
151178
ZoneId zone = null;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
*
3+
* MIT License
4+
*
5+
* Copyright (c) 2025 Hydrologic Engineering Center
6+
*
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy
8+
* of this software and associated documentation files (the "Software"), to deal
9+
* in the Software without restriction, including without limitation the rights
10+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the Software is
12+
* furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in all
15+
* copies or substantial portions of the Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23+
* DEALINGS IN THE
24+
* SOFTWARE.
25+
*/
26+
27+
package cwms.cda.data.dto;
28+
29+
import com.fasterxml.jackson.annotation.JsonInclude;
30+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
31+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
32+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
33+
import cwms.cda.formatters.Formats;
34+
import cwms.cda.formatters.annotations.FormattableWith;
35+
import cwms.cda.formatters.json.JsonV2;
36+
37+
@FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class,
38+
aliases = {Formats.JSON, Formats.DEFAULT})
39+
@JsonInclude(JsonInclude.Include.NON_NULL)
40+
@JsonDeserialize(builder = CwmsIdLocationKind.Builder.class)
41+
@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class)
42+
public final class CwmsIdLocationKind extends CwmsDTOBase {
43+
private final CwmsId location;
44+
private final String locationKindId;
45+
46+
public CwmsIdLocationKind(Builder builder) {
47+
this.location = builder.id;
48+
this.locationKindId = builder.locationKindId;
49+
}
50+
51+
public CwmsId getLocationId() {
52+
return location;
53+
}
54+
55+
public String getLocationKindId() {
56+
return locationKindId;
57+
}
58+
59+
public static final class Builder {
60+
private CwmsId id;
61+
private String locationKindId;
62+
63+
public Builder withLocationId(CwmsId id) {
64+
this.id = id;
65+
return this;
66+
}
67+
68+
public Builder withLocationKindId(String locationKindId) {
69+
this.locationKindId = locationKindId;
70+
return this;
71+
}
72+
73+
public CwmsIdLocationKind build() {
74+
return new CwmsIdLocationKind(this);
75+
}
76+
}
77+
}

cwms-data-api/src/main/java/cwms/cda/formatters/Formats.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,7 @@ public class Formats {
103103
private Formats() {
104104
}
105105

106-
public static String getLegacyTypeFromContentType(ContentType contentType)
107-
{
106+
public static String getLegacyTypeFromContentType(ContentType contentType) {
108107
return typeMap.entrySet()
109108
.stream()
110109
.filter(e -> e.getValue().equals(contentType.getType()))
@@ -287,7 +286,7 @@ public static ContentType parseHeaderAndQueryParm(String header, String queryPar
287286
/**
288287
* For endpoints that still allow either for transition, favors the query parameter as that's the likely user
289288
* expectation since machine systems wouldn't said both.
290-
* @param header content type from a header
289+
* @param headerParam content type from a header
291290
* @param queryParam content type from a query parameter
292291
* @param klass DTO to find a matching formatter for.
293292
* @return ContentType appropriate to the given selection.

0 commit comments

Comments
 (0)