Skip to content

Commit

Permalink
Merge pull request USACE#747 from rma-rripken/feature/cto-137-project…
Browse files Browse the repository at this point in the history
…-sub-location

Query Project children of a kind.
  • Loading branch information
rma-rripken authored Aug 8, 2024
2 parents 158c16a + fac0bf2 commit 6922bcc
Show file tree
Hide file tree
Showing 16 changed files with 1,560 additions and 229 deletions.
2 changes: 2 additions & 0 deletions cwms-data-api/src/main/java/cwms/cda/ApiServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
import cwms.cda.api.errors.JsonFieldsException;
import cwms.cda.api.errors.NotFoundException;
import cwms.cda.api.errors.RequiredQueryParameterException;
import cwms.cda.api.project.ProjectChildLocationHandler;
import cwms.cda.api.location.kind.VirtualOutletCreateController;
import cwms.cda.data.dao.JooqDao;
import cwms.cda.formatters.Formats;
Expand Down Expand Up @@ -519,6 +520,7 @@ protected void configureRoutes() {
cdaCrudCache(virtualOutletPath, new VirtualOutletController(metrics), requiredRoles, 1, TimeUnit.DAYS);
post(virtualOutletCreatePath, new VirtualOutletCreateController(metrics));

get("/projects/locations/", new ProjectChildLocationHandler(metrics));
cdaCrudCache(format("/projects/{%s}", Controllers.NAME),
new ProjectController(metrics), requiredRoles,5, TimeUnit.MINUTES);
cdaCrudCache(format("/properties/{%s}", Controllers.NAME),
Expand Down
6 changes: 2 additions & 4 deletions cwms-data-api/src/main/java/cwms/cda/api/Controllers.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,6 @@ public final class Controllers {
public static final String LOCATION_CATEGORY_LIKE = "location-category-like";
public static final String LOCATION_GROUP_LIKE = "location-group-like";




public static final String TIMESERIES_GROUP_LIKE = "timeseries-group-like";
public static final String ACCEPT = "Accept";
public static final String CLOB_ID = "clob-id";
Expand Down Expand Up @@ -145,7 +142,6 @@ public final class Controllers {
public static final String ANY_MASK = "*";
public static final String OFFICE_MASK = "office-mask";
public static final String ID_MASK = "id-mask";
public static final String LOCATION_MASK = "location-mask";
public static final String NAME_MASK = "name-mask";
public static final String BOTTOM_MASK = "bottom-mask";
public static final String TOP_MASK = "top-mask";
Expand All @@ -164,6 +160,7 @@ public final class Controllers {
public static final String STATUS_501 = "501";
public static final String STATUS_400 = "400";
public static final String TEXT_MASK = "text-mask";
public static final String DELETE_MODE = "delete-mode";
public static final String MIN_ATTRIBUTE = "min-attribute";
public static final String MAX_ATTRIBUTE = "max-attribute";
public static final String STANDARD_TEXT_ID_MASK = "standard-text-id-mask";
Expand All @@ -188,6 +185,7 @@ public final class Controllers {
public static final String DEFAULT_VALUE = "default-value";
public static final String CATEGORY = "category";
public static final String PREFIX = "prefix";
public static final String PROJECT_LIKE = "project-like";

private static final String DEPRECATED_HEADER = "CWMS-DATA-Format-Deprecated";
private static final String DEPRECATED_TAB = "2024-11-01 TAB is not used often.";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* MIT License
*
* Copyright (c) 2024 Hydrologic Engineering Center
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package cwms.cda.api.project;

import static com.codahale.metrics.MetricRegistry.name;
import static cwms.cda.api.Controllers.GET_ALL;
import static cwms.cda.api.Controllers.LOCATION_KIND_LIKE;
import static cwms.cda.api.Controllers.OFFICE;
import static cwms.cda.api.Controllers.PROJECT_LIKE;
import static cwms.cda.api.Controllers.STATUS_200;
import static cwms.cda.api.Controllers.requiredParam;

import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import cwms.cda.api.Controllers;
import cwms.cda.data.dao.JooqDao;
import cwms.cda.data.dao.project.ProjectChildLocationDao;
import cwms.cda.data.dto.project.ProjectChildLocations;
import cwms.cda.formatters.ContentType;
import cwms.cda.formatters.Formats;
import io.javalin.core.util.Header;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import io.javalin.plugin.openapi.annotations.HttpMethod;
import io.javalin.plugin.openapi.annotations.OpenApi;
import io.javalin.plugin.openapi.annotations.OpenApiContent;
import io.javalin.plugin.openapi.annotations.OpenApiParam;
import io.javalin.plugin.openapi.annotations.OpenApiResponse;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;


public class ProjectChildLocationHandler implements Handler {
public static final String TAGS = "Projects";
public static final String PATH = "/projects/child-locations/";
private final MetricRegistry metrics;
private final Histogram requestResultSize;

private Timer.Context markAndTime(String subject) {
return Controllers.markAndTime(metrics, getClass().getName(), subject);
}

public ProjectChildLocationHandler(MetricRegistry metrics) {
this.metrics = metrics;
requestResultSize = this.metrics.histogram((name(ProjectChildLocationHandler.class, Controllers.RESULTS,
Controllers.SIZE)));
}


@OpenApi(
description = "Get a list of project child locations",
queryParams = {
@OpenApiParam(name = OFFICE, required = true),
@OpenApiParam(name = PROJECT_LIKE, description = "Posix <a href=\"regexp.html\">regular expression</a> matching "
+ "against the project_id."),
@OpenApiParam(name = LOCATION_KIND_LIKE, description = "Posix <a "
+ "href=\"regexp.html\">regular expression</a> matching against the "
+ "location kind. The pattern will be matched against "
+ "the valid location-kinds for Project child locations:"
+ "{\"EMBANKMENT\", \"TURBINE\", \"OUTLET\", \"LOCK\", \"GATE\"}. "
+ "Multiple kinds can be matched by using Regular Expression "
+ "OR clauses. For example: \"(TURBINE|OUTLET)\"")
},
responses = {
@OpenApiResponse(status = STATUS_200, content = {
@OpenApiContent(type = Formats.JSON, from = ProjectChildLocations.class, isArray = true)})
},
tags = {TAGS},
path = PATH,
method = HttpMethod.GET
)
@Override
public void handle(@NotNull Context ctx) throws Exception {
String office = requiredParam(ctx, OFFICE);
try (Timer.Context ignored = markAndTime(GET_ALL)) {
ProjectChildLocationDao lockDao = new ProjectChildLocationDao(JooqDao.getDslContext(ctx));
String projLike = ctx.queryParamAsClass(PROJECT_LIKE, String.class).getOrDefault(null);
String kindLike = ctx.queryParamAsClass(LOCATION_KIND_LIKE, String.class).getOrDefault(null);

List<ProjectChildLocations> childLocations = lockDao.retrieveProjectChildLocations(office, projLike, kindLike);
String formatHeader = ctx.header(Header.ACCEPT);
ContentType contentType = Formats.parseHeader(formatHeader, ProjectChildLocations.class);
String result = Formats.format(contentType, childLocations, ProjectChildLocations.class);
ctx.result(result).contentType(contentType.toString());
requestResultSize.update(result.length());
ctx.status(HttpServletResponse.SC_OK);
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
* MIT License
*
* Copyright (c) 2024 Hydrologic Engineering Center
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package cwms.cda.data.dao.project;

import cwms.cda.data.dao.JooqDao;
import cwms.cda.data.dto.CwmsId;
import cwms.cda.data.dto.project.ProjectChildLocations;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
import org.jooq.DSLContext;
import usace.cwms.db.jooq.codegen.tables.AV_EMBANKMENT;
import usace.cwms.db.jooq.codegen.tables.AV_LOCK;
import usace.cwms.db.jooq.codegen.tables.AV_OUTLET;
import usace.cwms.db.jooq.codegen.tables.AV_TURBINE;

public class ProjectChildLocationDao extends JooqDao<ProjectChildLocations> {

public ProjectChildLocationDao(DSLContext dsl) {
super(dsl);
}


public List<ProjectChildLocations> retrieveProjectChildLocations(String office, String projLike, String kindRegex) {
return retrieveProjectChildLocations(office, projLike, ProjectKind.getMatchingKinds(kindRegex));
}

private List<ProjectChildLocations> retrieveProjectChildLocations(
String office, String projLike, Set<ProjectKind> kinds) {

Map<String, ProjectChildLocations.Builder> builderMap = new LinkedHashMap<>(); // proj-id->

for (ProjectKind kind : kinds) {
Map<String, List<CwmsId>> locsOfKind = getChildLocationsOfKind(office, projLike, kind);
if (locsOfKind != null) {
for (Map.Entry<String, List<CwmsId>> entry : locsOfKind.entrySet()) {
String projId = entry.getKey();
List<CwmsId> locs = entry.getValue();
ProjectChildLocations.Builder builder = builderMap.computeIfAbsent(projId, k ->
new ProjectChildLocations.Builder()
.withProject(new CwmsId.Builder()
.withOfficeId(office)
.withName(projId)
.build()));
builder.withLocations(kind, locs);
}
}
}

return builderMap.values().stream()
.map(ProjectChildLocations.Builder::build)
.collect(Collectors.toList());
}

@Nullable
private Map<String, List<CwmsId>> getChildLocationsOfKind(String office, String projRegex, ProjectKind kind) {
switch (kind) {
case EMBANKMENT:
return getEmbankmentChildLocations(office, projRegex);
case TURBINE:
return getTurbineChildLocations(office, projRegex);
case OUTLET:
return getOutletChildLocations(office, projRegex);
case LOCK:
return getLockChildLocations(office, projRegex);
case GATE:
return getGateChildLocations(office, projRegex);
default:
return null;
}

}

private Map<String, List<CwmsId>> getGateChildLocations(String office, @Nullable String projRegex) {
Map<String, List<CwmsId>> retval = new LinkedHashMap<>();
// AV_GATE is apparently not used.
AV_OUTLET view = AV_OUTLET.AV_OUTLET;
dsl.selectDistinct(view.OFFICE_ID, view.PROJECT_ID, view.OUTLET_ID)
.from(view)
.where(view.OFFICE_ID.eq(office)
.and(view.OPENING_UNIT_EN.isNotNull().or(view.OPENING_UNIT_SI.isNotNull()))
)
.and(caseInsensitiveLikeRegexNullTrue(view.PROJECT_ID, projRegex))
.orderBy(view.OFFICE_ID, view.PROJECT_ID, view.OUTLET_ID)
.forEach(row -> {
String projId = row.get(view.PROJECT_ID);
CwmsId gate = new CwmsId.Builder()
.withOfficeId(row.get(view.OFFICE_ID))
.withName(row.get(view.OUTLET_ID))
.build();
retval.computeIfAbsent(projId, k -> new ArrayList<>()).add(gate);
});

return retval;
}

private Map<String, List<CwmsId>> getLockChildLocations(String office, @Nullable String projRegex) {
Map<String, List<CwmsId>> retval = new LinkedHashMap<>();
AV_LOCK view = AV_LOCK.AV_LOCK;
dsl.selectDistinct(view.DB_OFFICE_ID, view.LOCK_ID, view.PROJECT_ID)
.from(view)
.where(view.DB_OFFICE_ID.eq(office))
.and(caseInsensitiveLikeRegexNullTrue(view.PROJECT_ID, projRegex))
.orderBy(view.DB_OFFICE_ID, view.PROJECT_ID, view.LOCK_ID)
.forEach(row -> {
String projId = row.get(view.PROJECT_ID);
CwmsId lock = new CwmsId.Builder()
.withOfficeId(row.get(view.DB_OFFICE_ID))
.withName(row.get(view.LOCK_ID))
.build();
retval.computeIfAbsent(projId, k -> new ArrayList<>()).add(lock);
});

return retval;
}

private Map<String, List<CwmsId>> getOutletChildLocations(String office, @Nullable String projRegex) {
Map<String, List<CwmsId>> retval = new LinkedHashMap<>();
AV_OUTLET view = AV_OUTLET.AV_OUTLET;
dsl.selectDistinct(view.OFFICE_ID, view.OUTLET_ID, view.PROJECT_ID)
.from(view)
.where(view.OFFICE_ID.eq(office))
.and(caseInsensitiveLikeRegexNullTrue(view.PROJECT_ID, projRegex))
.orderBy(view.OFFICE_ID, view.PROJECT_ID, view.OUTLET_ID)
.forEach(row -> {
String projId = row.get(view.PROJECT_ID);
CwmsId outlet = new CwmsId.Builder()
.withOfficeId(row.get(view.OFFICE_ID))
.withName(row.get(view.OUTLET_ID))
.build();
retval.computeIfAbsent(projId, k -> new ArrayList<>()).add(outlet);
});

return retval;
}

private Map<String, List<CwmsId>> getTurbineChildLocations(String office, @Nullable String projRegex) {
Map<String, List<CwmsId>> retval = new LinkedHashMap<>();
AV_TURBINE view = AV_TURBINE.AV_TURBINE;
dsl.selectDistinct(view.OFFICE_ID, view.TURBINE_ID, view.PROJECT_ID)
.from(view)
.where(view.OFFICE_ID.eq(office))
.and(caseInsensitiveLikeRegexNullTrue(view.PROJECT_ID, projRegex))
.orderBy(view.OFFICE_ID, view.PROJECT_ID, view.TURBINE_ID)
.forEach(row -> {
String projId = row.get(view.PROJECT_ID);
CwmsId turbine = new CwmsId.Builder()
.withOfficeId(row.get(view.OFFICE_ID))
.withName(row.get(view.TURBINE_ID))
.build();
retval.computeIfAbsent(projId, k -> new ArrayList<>()).add(turbine);
});

return retval;
}

private Map<String, List<CwmsId>> getEmbankmentChildLocations(String office, @Nullable String projRegex) {
Map<String, List<CwmsId>> retval = new LinkedHashMap<>();
AV_EMBANKMENT view = AV_EMBANKMENT.AV_EMBANKMENT;
dsl.selectDistinct(view.OFFICE_ID, view.EMBANKMENT_LOCATION_ID, view.PROJECT_ID)
.from(view)
.where(view.OFFICE_ID.eq(office)
.and(view.UNIT_SYSTEM.eq("SI")))
.and(caseInsensitiveLikeRegexNullTrue(view.PROJECT_ID, projRegex))
.orderBy(view.OFFICE_ID, view.PROJECT_ID, view.EMBANKMENT_LOCATION_ID)
.forEach(row -> {
String projId = row.get(view.PROJECT_ID);
CwmsId embankment = new CwmsId.Builder()
.withOfficeId(row.get(view.OFFICE_ID))
.withName(row.get(view.EMBANKMENT_LOCATION_ID))
.build();
retval.computeIfAbsent(projId, k -> new ArrayList<>()).add(embankment);
});

return retval;
}

}
Loading

0 comments on commit 6922bcc

Please sign in to comment.