Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@
import cwms.cda.api.StreamController;
import cwms.cda.api.StreamLocationController;
import cwms.cda.api.StreamReachController;
import cwms.cda.api.TextTimeSeriesController;
import cwms.cda.api.TextTimeSeriesValueController;
import cwms.cda.api.texttimeseries.TextTimeSeriesControllerV1;
import cwms.cda.api.texttimeseries.TextTimeSeriesControllerV2;
import cwms.cda.api.texttimeseries.TextTimeSeriesValueController;
import cwms.cda.api.TimeSeriesCategoryController;
import cwms.cda.api.TimeSeriesController;
import cwms.cda.api.TimeSeriesFilteredController;
Expand Down Expand Up @@ -237,11 +238,17 @@ public static void configureRoutes(MetricRegistry metrics, RouteRole[] requiredR
cdaCrudCache(format("/standard-text-id/{%s}", Controllers.STANDARD_TEXT_ID),
new StandardTextController(metrics), requiredRoles,1, TimeUnit.DAYS);

String textTsPath = format("/timeseries/text/{%s}", NAME);
cdaCrudCache(textTsPath, new TextTimeSeriesController(metrics), requiredRoles,5, TimeUnit.MINUTES);
String textTsPathTemplate = "/timeseries/text/{%s}";
String textTsPath = format(textTsPathTemplate, NAME);
cdaCrudCache(textTsPath, new TextTimeSeriesControllerV1(metrics), requiredRoles,5, TimeUnit.MINUTES);
cdaCrudCache(formatV2(textTsPathTemplate, NAME),
new TextTimeSeriesControllerV2(metrics), requiredRoles, 5, TimeUnit.MINUTES);
String textValuePath = textTsPath + "/value";
get(textValuePath, new TextTimeSeriesValueController(metrics));
addCacheControl(textValuePath, 1, TimeUnit.DAYS);
String textValuePathV2 = formatV2(textTsPathTemplate, NAME) + "/value";
get(textValuePathV2, new TextTimeSeriesValueController(metrics));
addCacheControl(textValuePathV2, 1, TimeUnit.DAYS);

String binTsPath = format("/timeseries/binary/{%s}", NAME);
cdaCrudCache(binTsPath, new BinaryTimeSeriesController(metrics), requiredRoles,5, TimeUnit.MINUTES);
Expand Down
1 change: 1 addition & 0 deletions cwms-data-api/src/main/java/cwms/cda/api/Controllers.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ public final class Controllers {
public static final String FAIL_IF_EXISTS = "fail-if-exists";
public static final String CREATE_POOL_NAME = "create-pool-name";
public static final String IGNORE_NULLS = "ignore-nulls";
public static final String COLLECTION_MERGE_STRATEGY = "collection-merge-strategy";
public static final String EFFECTIVE_DATE = "effective-date";
public static final String DATE = "date";
public static final String LEVEL_ID = "level-id";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package cwms.cda.api.enums;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(
name = "Collection Patch Strategy",
description = CollectionPatchStrategy.DESCRIPTION
)
public enum CollectionPatchStrategy {

OVERWRITE,
MERGE;

public static final String DESCRIPTION = "Controls how a PATCH request body's collection "
+ "fields are applied. OVERWRITE: the collection becomes exactly what "
+ "the body contains -- anything within the request's time window that isn't named "
+ "in the body is removed. MERGE: items named in the body are matched to "
+ "existing items by their identity field(s) -- the field(s) marked @Identifier, "
+ "or, when the collection's element type has none of those, whichever field(s) are "
+ "marked @JsonProperty(required = true) -- and updated in place, preserving that "
+ "item's own omitted fields; a null or absent identity field on the incoming item "
+ "never matches anything, so an unmatched identity is added as new, and every other "
+ "existing item is left untouched.";

public static CollectionPatchStrategy strategyFor(String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("Cannot determine collection-patch strategy for null or empty");
}
String normalized = value.trim().toUpperCase().replace('-', '_');
for (CollectionPatchStrategy strategy : values()) {
if (strategy.name().equals(normalized)) {
return strategy;
}
}
throw new UnsupportedOperationException("Unsupported collection-patch strategy: " + value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* MIT License
*
* Copyright (c) 2026 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.texttimeseries;

import static cwms.cda.api.Controllers.DATE;
import static cwms.cda.api.Controllers.GET_ALL;
import static cwms.cda.api.Controllers.NAME;
import static cwms.cda.api.Controllers.OFFICE;
import static cwms.cda.api.Controllers.VERSION_DATE;
import static cwms.cda.api.Controllers.requiredInstant;
import static cwms.cda.api.Controllers.requiredParam;
import static cwms.cda.data.dao.JooqDao.getDslContext;

import com.codahale.metrics.Timer;
import com.google.common.flogger.FluentLogger;
import cwms.cda.api.BaseCrudHandler;
import cwms.cda.api.Controllers;
import cwms.cda.api.errors.CdaError;
import cwms.cda.api.errors.ExceptionTraceSupport;
import cwms.cda.data.dao.texttimeseries.TimeSeriesTextDao;
import cwms.cda.data.dto.texttimeseries.TextTimeSeries;
import cwms.cda.formatters.ContentType;
import cwms.cda.formatters.Formats;
import cwms.cda.helpers.ReplaceUtils;
import io.javalin.core.util.Header;
import io.javalin.http.Context;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.time.Instant;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.client.utils.URIBuilder;
import org.jetbrains.annotations.NotNull;
import org.jooq.DSLContext;

public abstract class TextTimeSeriesController extends BaseCrudHandler {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static final String TAG = "Text-TimeSeries";

public static final String REPLACE_ALL = "replace-all";

public static final boolean DEFAULT_CREATE_REPLACE_ALL = false;
public static final boolean DEFAULT_UPDATE_REPLACE_ALL = true;

protected TextTimeSeriesController(com.codahale.metrics.MetricRegistry metrics) {
super(metrics);
}

@NotNull
protected TimeSeriesTextDao getDao(DSLContext dsl) {
return new TimeSeriesTextDao(dsl);
}

protected abstract String getOffice(@NotNull Context ctx);

@Override
public void getAll(@NotNull Context ctx) {

String office = getOffice(ctx);
String tsId = requiredParam(ctx, NAME);
Instant begin = requiredInstant(ctx, Controllers.BEGIN);
Instant end = requiredInstant(ctx, Controllers.END);
Instant version = Controllers.queryParamAsInstant(ctx, VERSION_DATE);
int kiloByteLimit = Integer.parseInt(System.getProperty("cda.api.ts.text.max.length.kB", "64"));
String formatHeader = ctx.header(Header.ACCEPT);
ContentType contentType = Formats.parseHeader(formatHeader, TextTimeSeries.class);
try (Timer.Context ignored = markAndTime(GET_ALL)) {
DSLContext dsl = getDslContext(ctx);
TimeSeriesTextDao dao = getDao(dsl);

String textMask = "*";

String dateToken = "{date_token}";
String path = ctx.path();
if (!path.endsWith("/")) {
path += "/";
}
path += tsId + "/value";
String url = new URIBuilder(ctx.fullUrl())
.setPath(path)
.clearParameters()
.addParameter(OFFICE, office)
.addParameter(VERSION_DATE, ctx.queryParam(VERSION_DATE))
.addParameter(DATE, dateToken)
.build()
.toString();
ReplaceUtils.OperatorBuilder urlBuilder = new ReplaceUtils.OperatorBuilder()
.withTemplate(url)
.withOperatorKey(URLEncoder.encode(dateToken, "UTF-8"));
TextTimeSeries textTimeSeries = dao.retrieveFromDao(office, tsId, textMask,
begin, end, version, kiloByteLimit, urlBuilder);

ctx.contentType(contentType.toString());

String result = Formats.format(contentType, textTimeSeries);

ctx.status(HttpServletResponse.SC_OK);

byte[] bytes = result.getBytes();
ctx.header(Header.CONTENT_LENGTH, String.valueOf(bytes.length));
ctx.res.getOutputStream().write(bytes);
} catch (URISyntaxException | IOException ex) {
CdaError re = ExceptionTraceSupport.buildError(ctx,
"Failed to process request: " + ex.getLocalizedMessage(), ex);
logger.atSevere().withCause(ex).log("%s", re);
ctx.status(HttpServletResponse.SC_INTERNAL_SERVER_ERROR).json(re);
}

}

@Override
public void getOne(@NotNull Context ctx, @NotNull String templateId) {
ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented());
}

@Override
public void create(@NotNull Context ctx) {
try (Timer.Context ignored = markAndTime(Controllers.CREATE)) {
DSLContext dsl = getDslContext(ctx);

String formatHeader = ctx.req.getContentType();

ContentType contentType = Formats.parseHeader(formatHeader, TextTimeSeries.class);
TextTimeSeries tts = Formats.parseContent(contentType, ctx.bodyAsInputStream(), TextTimeSeries.class);
TimeSeriesTextDao dao = getDao(dsl);

boolean replaceAll = ctx.queryParamAsClass(REPLACE_ALL, Boolean.class)
.getOrDefault(DEFAULT_CREATE_REPLACE_ALL);
dao.create(tts, replaceAll);
ctx.status(HttpServletResponse.SC_CREATED);
}
}

@Override
public void delete(@NotNull Context ctx, @NotNull String textTimeSeriesId) {
try (Timer.Context ignored = markAndTime(Controllers.DELETE)) {
DSLContext dsl = getDslContext(ctx);
String office = getOffice(ctx);
String mask = requiredParam(ctx, Controllers.TEXT_MASK);


Instant begin = requiredInstant(ctx, Controllers.BEGIN);
Instant end = requiredInstant(ctx, Controllers.END);
Instant version = Controllers.queryParamAsInstant(ctx, VERSION_DATE);

TimeSeriesTextDao dao2 = getDao(dsl);

dao2.delete(office, textTimeSeriesId, mask, begin, end, version);

ctx.status(HttpServletResponse.SC_NO_CONTENT);
}
}
}
Loading
Loading