-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathTimeSeriesController.java
More file actions
682 lines (615 loc) · 35.9 KB
/
TimeSeriesController.java
File metadata and controls
682 lines (615 loc) · 35.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
package cwms.cda.api;
import static com.codahale.metrics.MetricRegistry.name;
import static cwms.cda.api.Controllers.BEGIN;
import static cwms.cda.api.Controllers.CREATE;
import static cwms.cda.api.Controllers.CREATE_AS_LRTS;
import static cwms.cda.api.Controllers.CURSOR;
import static cwms.cda.api.Controllers.DATUM;
import static cwms.cda.api.Controllers.DELETE;
import static cwms.cda.api.Controllers.END;
import static cwms.cda.api.Controllers.END_TIME_INCLUSIVE;
import static cwms.cda.api.Controllers.FORMAT;
import static cwms.cda.api.Controllers.GET_ALL;
import static cwms.cda.api.Controllers.GET_ONE;
import static cwms.cda.api.Controllers.INCLUDE_ENTRY_DATE;
import static cwms.cda.api.Controllers.MAX_VERSION;
import static cwms.cda.api.Controllers.NAME;
import static cwms.cda.api.Controllers.NOT_SUPPORTED_YET;
import static cwms.cda.api.Controllers.OFFICE;
import static cwms.cda.api.Controllers.OVERRIDE_PROTECTION;
import static cwms.cda.api.Controllers.PAGE;
import static cwms.cda.api.Controllers.PAGE_SIZE;
import static cwms.cda.api.Controllers.RESULTS;
import static cwms.cda.api.Controllers.SIZE;
import static cwms.cda.api.Controllers.START_TIME_INCLUSIVE;
import static cwms.cda.api.Controllers.STATUS_200;
import static cwms.cda.api.Controllers.STATUS_400;
import static cwms.cda.api.Controllers.STATUS_404;
import static cwms.cda.api.Controllers.STATUS_501;
import static cwms.cda.api.Controllers.STORE_RULE;
import static cwms.cda.api.Controllers.TIMESERIES;
import static cwms.cda.api.Controllers.TIMEZONE;
import static cwms.cda.api.Controllers.TIME_FORMAT_DESC;
import static cwms.cda.api.Controllers.UNIT;
import static cwms.cda.api.Controllers.UNITS;
import static cwms.cda.api.Controllers.UPDATE;
import static cwms.cda.api.Controllers.VERSION;
import static cwms.cda.api.Controllers.VERSION_DATE;
import static cwms.cda.api.Controllers.addDeprecatedContentTypeWarning;
import static cwms.cda.api.Controllers.queryParamAsClass;
import static cwms.cda.api.Controllers.queryParamAsZdt;
import static cwms.cda.api.Controllers.requiredParam;
import static cwms.cda.api.Controllers.requiredZdt;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import cwms.cda.api.enums.UnitSystem;
import cwms.cda.api.errors.CdaError;
import cwms.cda.api.errors.ErrorTraceSupport;
import cwms.cda.api.errors.NotFoundException;
import cwms.cda.data.dao.JooqDao;
import cwms.cda.data.dao.StoreRule;
import cwms.cda.data.dao.TimeSeriesDao;
import cwms.cda.data.dao.TimeSeriesDaoImpl;
import cwms.cda.data.dao.TimeSeriesDeleteOptions;
import cwms.cda.data.dao.TimeSeriesRequestParameters;
import cwms.cda.data.dao.TimeSeriesVerticalDatumConverter;
import cwms.cda.data.dao.VerticalDatum;
import cwms.cda.data.dto.TimeSeries;
import cwms.cda.formatters.ContentType;
import cwms.cda.formatters.Formats;
import cwms.cda.helpers.DateUtils;
import io.javalin.apibuilder.CrudHandler;
import io.javalin.core.util.Header;
import io.javalin.core.validation.JavalinValidation;
import io.javalin.core.validation.Validator;
import io.javalin.http.Context;
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.OpenApiRequestBody;
import io.javalin.plugin.openapi.annotations.OpenApiResponse;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import com.google.common.flogger.FluentLogger;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.client.utils.URLEncodedUtils;
import org.jetbrains.annotations.NotNull;
import org.jooq.DSLContext;
import org.jooq.exception.DataAccessException;
public class TimeSeriesController implements CrudHandler {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static final String TAG = "TimeSeries";
public static final String STORE_RULE_DESC = "The business rule to use "
+ "when merging the incoming with existing data\n"
+ "<table border=\"1\" summary=\"\">\n"
+ "<tr><td colspan=2>Store Rules</td></tr>\n"
+ "<tr>\n"
+ " <td>Delete Insert</td>\n"
+ " <td>All existing data in the time window will be deleted and "
+ "then replaced with the new dataset.</td>\n"
+ "</tr>\n"
+ "<tr>\n"
+ " <td>Replace All</td>\n"
+ " <td>\n"
+ " <ul>\n"
+ " <li>When the new dataset's date/time exactly matches the date/time of "
+ "an existing data value, the new data value will replace the existing data.</li>\n"
+ " <li>When the new dataset's data/time does not match an existing data/time "
+ "(i.e., a new data/time - data value pair) then an insert to the database "
+ "will occur.</li>\n"
+ " <li>When there's an existing \"data/time - data value pair\" without "
+ "a corresponding date/time value pair, no change will happen to the existing "
+ "date/time value pair.</li>\n"
+ " </ul>\n"
+ " </td>\n"
+ "</tr>\n"
+ "<tr>\n"
+ " <td>Replace With Non Missing</td>\n"
+ " <td>\n"
+ " <ul>\n"
+ " <li>New data is always inserted, i.e., an existing date/time-value "
+ "pair does not already exist for the record.</li>\n"
+ " <li>If date/time-value pair does exist, then only non-missing value "
+ "will replace the existing data value*.</li>\n"
+ " </ul>\n"
+ " </td>\n"
+ "<tr>\n"
+ " <td>Replace Missing Values Only</td>\n"
+ " <td>\n"
+ " <ul>\n"
+ " <li>New data is always inserted, i.e., an existing date/time-value "
+ "pair does not already exist for the record.</li>\n"
+ " <li>If date/time-value pair does exist, then only replace an existing "
+ "data/time-value pair whose missing flag was set.</li>\n"
+ " </ul>\n"
+ " </td>\n"
+ "<tr>\n"
+ " <td>Do Not Replace</td>\n"
+ " <td>\n"
+ " Only inserts new data values if an existing date/time-value pair does not "
+ "already exist.\n"
+ " Note: an existing date/time-value pair whose missing value quality bit is "
+ "set will NOT be overwritten.\n"
+ " </td>\n"
+ "</tr>\n"
+ "</table>";
private final MetricRegistry metrics;
private final Histogram requestResultSize;
private static final int DEFAULT_PAGE_SIZE = 500;
public TimeSeriesController(MetricRegistry metrics) {
this.metrics = metrics;
String className = this.getClass().getName();
requestResultSize = this.metrics.histogram((name(className, RESULTS, SIZE)));
}
static {
JavalinValidation.register(StoreRule.class, StoreRule::getStoreRule);
JavalinValidation.register(VerticalDatum.class, VerticalDatum::getVerticalDatum);
}
private Timer.Context markAndTime(String subject) {
return Controllers.markAndTime(metrics, getClass().getName(), subject);
}
@OpenApi(
description = "Used to create and save time-series data. Data to be stored must have "
+ "time stamps in UTC represented as epoch milliseconds. If data entry date is included in the "
+ "request, it will be dropped. ",
requestBody = @OpenApiRequestBody(
content = {
@OpenApiContent(from = TimeSeries.class, type = Formats.JSONV2),
@OpenApiContent(from = TimeSeries.class, type = Formats.XMLV2)
},
required = true
),
queryParams = {
@OpenApiParam(name = CREATE_AS_LRTS, type = Boolean.class, description = "Flag indicating if "
+ "timeseries should be created as Local Regular Time Series. "
+ "'True' or 'False', default is 'False'"),
@OpenApiParam(name = STORE_RULE, type = StoreRule.class, description = STORE_RULE_DESC),
@OpenApiParam(name = OVERRIDE_PROTECTION, type = Boolean.class, description = "A flag "
+ "to ignore the protected data quality when storing data. 'True' or 'False'"
+ ", default is " + TimeSeriesDaoImpl.OVERRIDE_PROTECTION),
@OpenApiParam(name = DATUM, type = VerticalDatum.class, description = "If the provided "
+ "time-series includes an explicit vertical-datum-info attribute "
+ "then it is assumed that the data is in the datum specified by the vertical-datum-info. "
+ "If the input timeseries does not include vertical-datum-info and "
+ "this parameter is not provided it is assumed that the data is in the as-stored "
+ "datum and no conversion is necessary. "
+ "If the input timeseries does not include vertical-datum-info and "
+ "this parameter is provided it is assumed that the data is in the Datum named by the argument "
+ "and should be converted to the as-stored datum before being saved.")
},
method = HttpMethod.POST,
path = "/timeseries",
tags = TAG
)
@Override
public void create(@NotNull Context ctx) {
boolean createAsLrts = ctx.queryParamAsClass(CREATE_AS_LRTS, Boolean.class)
.getOrDefault(false);
StoreRule storeRule = ctx.queryParamAsClass(STORE_RULE, StoreRule.class)
.getOrDefault(StoreRule.REPLACE_ALL);
boolean overrideProtection = ctx.queryParamAsClass(OVERRIDE_PROTECTION, Boolean.class)
.getOrDefault(TimeSeriesDaoImpl.OVERRIDE_PROTECTION);
VerticalDatum vd = ctx.queryParamAsClass(DATUM, VerticalDatum.class)
.getOrDefault(null);
try (final Timer.Context ignored = markAndTime(CREATE)) {
DSLContext dsl = getDslContext(ctx);
TimeSeriesDao dao = getTimeSeriesDao(dsl);
TimeSeries timeSeries = deserializeTimeSeries(ctx);
vd = TimeSeriesVerticalDatumConverter.getVerticalDatum(timeSeries).orElse(vd);
dao.create(timeSeries, createAsLrts, storeRule, overrideProtection, vd);
ctx.status(HttpServletResponse.SC_OK);
} catch (DataAccessException | IOException ex) {
CdaError re = ErrorTraceSupport.buildError(ctx, "Internal Error", ex);
logger.atSevere().withCause(ex).log("%s", re.toString());
ctx.status(HttpServletResponse.SC_INTERNAL_SERVER_ERROR).json(re);
}
}
protected DSLContext getDslContext(Context ctx) {
return JooqDao.getDslContext(ctx);
}
@NotNull
protected TimeSeriesDao getTimeSeriesDao(DSLContext dsl) {
return new TimeSeriesDaoImpl(dsl, metrics);
}
@OpenApi(
pathParams = {
@OpenApiParam(name = TIMESERIES, required = true, description = "The timeseries-id of "
+ "the timeseries values to be deleted. "),
},
queryParams = {
@OpenApiParam(name = OFFICE, required = true, description = "Specifies the office of "
+ "the timeseries to be deleted."),
@OpenApiParam(name = BEGIN, required = true, description = "The start of the time "
+ "window to delete. " + TIME_FORMAT_DESC),
@OpenApiParam(name = END, required = true, description = "The end of the time "
+ "window to delete. " + TIME_FORMAT_DESC),
@OpenApiParam(name = TIMEZONE, description = "This field specifies a default timezone "
+ "to be used if the format of the " + BEGIN + ", " + END + ", or "
+ VERSION_DATE + " parameters do not include offset or time zone information. "
+ "Defaults to UTC."),
@OpenApiParam(name = VERSION_DATE, description = "The version date/time of the time "
+ "series in the specified or default time zone. If NULL, the earliest or "
+ "latest version date will be used depending on p_max_version."),
@OpenApiParam(name = START_TIME_INCLUSIVE, type = Boolean.class, description = "A flag "
+ "specifying whether any data at the start time should be deleted ('True') "
+ "or only data <b><em>after</em></b> the start time ('False'). "
+ "Default value is True"),
@OpenApiParam(name = END_TIME_INCLUSIVE, type = Boolean.class, description = "A flag "
+ "('True'/'False') specifying whether any data at the end time should be "
+ "deleted ('True') or only data <b><em>before</em></b> the end time ('False'). "
+ "Default value is False"),
@OpenApiParam(name = MAX_VERSION, type = Boolean.class, description = "A flag "
+ "('True'/'False') specifying whether to use the earliest ('False') or "
+ "latest ('True') version date for each time if p_version_date is NULL. "
+ "Default is 'True'"),
@OpenApiParam(name = OVERRIDE_PROTECTION, type = Boolean.class, description = "A flag "
+ "('True'/'False') specifying whether to delete protected data. "
+ "Default is False")
},
method = HttpMethod.DELETE,
path = "/timeseries/{timeseries}",
tags = TAG
)
@Override
public void delete(@NotNull Context ctx, @NotNull String timeseries) {
String office = requiredParam(ctx, OFFICE);
try (final Timer.Context ignored = markAndTime(DELETE)) {
DSLContext dsl = getDslContext(ctx);
TimeSeriesDao dao = getTimeSeriesDao(dsl);
Timestamp startTimeDate = Timestamp.from(requiredZdt(ctx, BEGIN).toInstant());
Timestamp endTimeDate = Timestamp.from(requiredZdt(ctx, END).toInstant());
Timestamp versionDate = null;
ZonedDateTime versionZdt = queryParamAsZdt(ctx, VERSION_DATE);
if (versionZdt != null) {
versionDate = Timestamp.from(versionZdt.toInstant());
}
// FYI queryParamAsClass with Boolean.class returns a case-insensitive comparison to "true".
boolean startTimeInclusive = ctx.queryParamAsClass(START_TIME_INCLUSIVE, Boolean.class)
.getOrDefault(true);
boolean endTimeInclusive = ctx.queryParamAsClass(END_TIME_INCLUSIVE, Boolean.class)
.getOrDefault(false);
boolean maxVersion = ctx.queryParamAsClass(MAX_VERSION, Boolean.class)
.getOrDefault(true);
boolean opArg = ctx.queryParamAsClass(OVERRIDE_PROTECTION, Boolean.class)
.getOrDefault(false);
TimeSeriesDaoImpl.OverrideProtection op;
if (opArg) {
op = TimeSeriesDaoImpl.OverrideProtection.True;
} else {
op = TimeSeriesDaoImpl.OverrideProtection.False;
}
TimeSeriesDeleteOptions options = new TimeSeriesDaoImpl.DeleteOptions.Builder()
.withStartTime(startTimeDate)
.withEndTime(endTimeDate)
.withVersionDate(versionDate)
.withStartTimeInclusive(startTimeInclusive)
.withEndTimeInclusive(endTimeInclusive)
.withMaxVersion(maxVersion)
.withOverrideProtection(op.toString())
.build();
dao.delete(office, timeseries, options);
}
}
@OpenApi(
queryParams = {
@OpenApiParam(name = NAME, required = true, description = "Specifies the "
+ "name of the time series whose data is to be included in the "
+ "response. A case insensitive comparison is used to match names."),
@OpenApiParam(name = OFFICE, description = "Specifies the"
+ " owning office of the time series(s) whose data is to be included "
+ "in the response. "
+ "Required for:" + Formats.JSONV2 + " and " + Formats.XMLV2 + ". "
+ "For other formats, if this field is not specified, matching location "
+ "level information from all offices shall be returned."),
@OpenApiParam(name = UNIT, deprecated = true, description = "Specifies the "
+ "unit or unit system of the response. Valid values for the unit "
+ "field are: "
+ "\n* `EN` (default) Specifies English unit system. "
+ "Location level values will be in the default English units for "
+ "their parameters."
+ "\n* `SI` Specifies the SI unit system. "
+ "Location level values will be in the default SI units for their "
+ "parameters."
+ "\n* `Other` Any unit returned in the response to the units URI "
+ "request that is appropriate for the requested parameters."),
@OpenApiParam(name = UNITS, description = "Specifies the "
+ "units or unit system of the response. Valid values for the units "
+ "field are: "
+ "\n* `EN` (default) Specifies English unit system. "
+ "Location level values will be in the default English units for "
+ "their parameters."
+ "\n* `SI` Specifies the SI unit system. "
+ "Location level values will be in the default SI units for their "
+ "parameters."
+ "\n* `Other` Any units returned in the response to the units URI "
+ "request that is appropriate for the requested parameters."),
@OpenApiParam(name = VERSION_DATE, description = "Specifies the version date of a "
+ "time series trace to be selected. " + TIME_FORMAT_DESC
+ "If field is empty, query will return a max aggregate for the timeseries. "
+ "Only supported for:" + Formats.JSONV2 + " and " + Formats.XMLV2),
@OpenApiParam(name = DATUM, description = "Specifies the "
+ "elevation datum of the response. This field affects only elevation"
+ " location levels. Valid values for this field are:"
+ "\n* `NAVD88` The elevation values will in the specified or default units above "
+ "the NAVD-88 datum."
+ "\n* `NGVD29` The elevation values will be in "
+ "the specified or default units above the NGVD-29 datum. "
+ "This parameter is not supported for:" + Formats.JSONV2 + " or " + Formats.XMLV2),
@OpenApiParam(name = BEGIN, description = "Specifies the "
+ "start of the time window for data to be included in the response. "
+ "If this field is not specified, any required time window begins 24"
+ " hours prior to the specified or default end time. "
+ TIME_FORMAT_DESC),
@OpenApiParam(name = END, description = "Specifies the "
+ "end of the time window for data to be included in the response. If"
+ " this field is not specified, any required time window ends at the"
+ " current time. "
+ TIME_FORMAT_DESC),
@OpenApiParam(name = TIMEZONE, description = "Specifies "
+ "the time zone of the values of the begin and end fields (unless "
+ "otherwise specified). "
+ "For " + Formats.JSONV2 + " and " + Formats.XMLV2
+ " the results are returned in UTC. For other formats this parameter "
+ "affects the time zone of times in the "
+ "response. If this field is not specified, the default time zone "
+ "of UTC shall be used.\r\nIgnored if begin was specified with "
+ "offset and timezone."),
@OpenApiParam(name = Controllers.TRIM, type = Boolean.class, description = "Specifies "
+ "whether to trim missing values from the beginning and end of the "
+ "retrieved values. "
+ "Only supported for:" + Formats.JSONV2 + " and " + Formats.XMLV2 + ". "
+ "Default is true."),
@OpenApiParam(name = FORMAT, description = "Specifies the"
+ " encoding format of the response. Valid values for the format "
+ "field for this URI are:"
+ "\n* `tab`"
+ "\n* `csv`"
+ "\n* `xml`"
+ "\n* `wml2` (only if name field is specified)"
+ "\n* `json` (default)"
+ "\n\nSee <a href=\"legacy-format/\">this page</a> for more "
+ "information about accept header usage."),
@OpenApiParam(name = INCLUDE_ENTRY_DATE, type = Boolean.class, description = "Specifies "
+ "whether to include the data entry date of each value in the response. Including the data entry "
+ "date will increase the size of the array containing each data value from three to four, "
+ "changing the format of the response. Default is false."),
@OpenApiParam(name = PAGE, description = "This end point can return large amounts "
+ "of data as a series of pages. This parameter is used to describes the "
+ "current location in the response stream. This is an opaque "
+ "value, and can be obtained from the 'next-page' value in the response."),
@OpenApiParam(name = CURSOR, deprecated = true,
description = "This end point can return a lot of data, this "
+ "identifies where in the request you are. This is an opaque"
+ " value, and can be obtained from the 'next-page' value in "
+ "the response. Deprecated, use " + PAGE + " instead."),
@OpenApiParam(name = PAGE_SIZE,
type = Integer.class,
description = "How many entries per page returned. "
+ "Default " + DEFAULT_PAGE_SIZE + ".")
},
responses = {
@OpenApiResponse(status = STATUS_200,
description = "A list of elements of the data set you've selected.",
content = {
@OpenApiContent(from = TimeSeries.class, type = Formats.JSONV2),
@OpenApiContent(from = TimeSeries.class, type = Formats.XMLV2),
@OpenApiContent(from = TimeSeries.class, type = Formats.XML),
@OpenApiContent(from = TimeSeries.class, type = Formats.JSON),
@OpenApiContent(from = TimeSeries.class, type = ""),}),
@OpenApiResponse(status = STATUS_400, description = "Invalid parameter combination"),
@OpenApiResponse(status = STATUS_404, description = "The provided combination of "
+ "parameters did not find a timeseries."),
@OpenApiResponse(status = STATUS_501, description = "Requested format is not "
+ "implemented")
},
method = HttpMethod.GET,
path = "/timeseries",
tags = TAG
)
@Override
public void getAll(@NotNull Context ctx) {
try (final Timer.Context ignored = markAndTime(GET_ALL)) {
DSLContext dsl = getDslContext(ctx);
TimeSeriesDao dao = getTimeSeriesDao(dsl);
String format = ctx.queryParamAsClass(FORMAT, String.class).getOrDefault("");
String names = requiredParam(ctx, NAME);
//try 'unit' if 'units' is not provided as that was the original parameter name
String units = ctx.queryParamAsClass(UNITS, String.class)
.getOrDefault(ctx.queryParamAsClass(UNIT, String.class)
.getOrDefault(UnitSystem.EN.getValue()));
String datum = ctx.queryParam(DATUM);
String begin = ctx.queryParam(BEGIN);
String end = ctx.queryParam(END);
String timezone = ctx.queryParamAsClass(TIMEZONE, String.class)
.getOrDefault("UTC");
Validator<Boolean> trim = ctx.queryParamAsClass(Controllers.TRIM, Boolean.class);
ZonedDateTime versionDate = queryParamAsZdt(ctx, VERSION_DATE);
boolean includeEntryDate = ctx.queryParamAsClass(INCLUDE_ENTRY_DATE, Boolean.class)
.getOrDefault(false);
// The following parameters are only used for jsonv2 and xmlv2
String cursor = queryParamAsClass(ctx, new String[]{PAGE, CURSOR},
String.class, "", metrics, name(TimeSeriesController.class.getName(),
GET_ALL));
int pageSize = queryParamAsClass(ctx, new String[]{PAGE_SIZE },
Integer.class, DEFAULT_PAGE_SIZE, metrics,
name(TimeSeriesController.class.getName(), GET_ALL));
String acceptHeader = ctx.header(Header.ACCEPT);
ContentType contentType = Formats.parseHeaderAndQueryParm(acceptHeader, format, TimeSeries.class);
String results;
String version = contentType.getParameters().get(VERSION);
ZoneId tz = ZoneId.of(timezone, ZoneId.SHORT_IDS);
begin = begin != null ? begin : "PT-24H";
ZonedDateTime beginZdt = DateUtils.parseUserDate(begin, timezone);
ZonedDateTime endZdt = end != null
? DateUtils.parseUserDate(end, timezone)
: ZonedDateTime.now(tz);
if (version != null && version.equals("2")) {
String office = requiredParam(ctx, OFFICE);
TimeSeriesRequestParameters requestParameters = new TimeSeriesRequestParameters.Builder()
.withNames(names)
.withOffice(office)
.withUnits(units)
.withBeginTime(beginZdt)
.withEndTime(endZdt)
.withVersionDate(versionDate)
.withShouldTrim(trim.getOrDefault(true))
.withIncludeEntryDate(includeEntryDate)
.build();
TimeSeries ts = dao.getTimeseries(cursor, pageSize, requestParameters);
if(datum != null) { //this will be null for non-elevation ts
// user has requested a specific vertical datum
VerticalDatum vd = VerticalDatum.valueOf(datum); // the users request
ts = TimeSeriesVerticalDatumConverter.convertToVerticalDatum(ts, vd);
}
results = Formats.format(contentType, ts);
ctx.status(HttpServletResponse.SC_OK);
addLinkHeader(ctx, ts, contentType);
ctx.result(results).contentType(contentType.toString());
} else {
if (versionDate != null) {
throw new IllegalArgumentException(String.format("Version date is only supported for:%s and %s",
Formats.JSONV2, Formats.XMLV2));
}
if (trim.hasValue()) {
throw new IllegalArgumentException(String.format("Trim is only supported for:%s and %s",
Formats.JSONV2, Formats.XMLV2));
}
if (format == null || format.isEmpty()) {
format = "json";
}
String office = ctx.queryParam(OFFICE);
results = dao.getTimeseries(format, names, office, units, datum, beginZdt, endZdt, tz);
ctx.status(HttpServletResponse.SC_OK);
ctx.result(results);
}
addDeprecatedContentTypeWarning(ctx, contentType);
requestResultSize.update(results.length());
} catch (NotFoundException e) {
CdaError re = new CdaError("Not found.");
logger.atSevere().withCause(e).log("%s", re.toString());
ctx.status(HttpServletResponse.SC_NOT_FOUND);
ctx.json(re);
} catch (IllegalArgumentException ex) {
CdaError re = new CdaError("Invalid arguments supplied");
logger.atSevere().withCause(ex).log("%s", re.toString());
ctx.status(HttpServletResponse.SC_BAD_REQUEST);
ctx.json(re);
}
}
private void addLinkHeader(@NotNull Context ctx, TimeSeries ts, ContentType contentType) {
try {
// Send back the link to the next page in the response header
StringBuilder linkValue = new StringBuilder(600);
String pageUrl = buildRequestUrl(ctx, ts, ts.getPage());
linkValue.append(String.format("<%s>; rel=self; type=\"%s\"",
pageUrl, contentType));
if (ts.getNextPage() != null) {
linkValue.append(",");
String nextPageUrl = buildRequestUrl(ctx, ts, ts.getNextPage());
linkValue.append(String.format("<%s>; rel=next; type=\"%s\"",
nextPageUrl,
contentType));
}
ctx.header("Link", linkValue.toString());
} catch (URISyntaxException ex) {
logger.atWarning().withCause(ex).log("Failed to build Link header");
}
}
@OpenApi(ignore = true)
@Override
public void getOne(@NotNull Context ctx, @NotNull String id) {
try (final Timer.Context ignored = markAndTime(GET_ONE)) {
ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented());
}
}
@OpenApi(
description = "Update a TimeSeries with provided values",
pathParams = {
@OpenApiParam(name = TIMESERIES, description = "Full CWMS Timeseries name")
},
requestBody = @OpenApiRequestBody(
content = {
@OpenApiContent(from = TimeSeries.class, type = Formats.JSONV2),
@OpenApiContent(from = TimeSeries.class, type = Formats.XMLV2)
},
required = true),
queryParams = {
@OpenApiParam(name = CREATE_AS_LRTS, type = Boolean.class, description = ""),
@OpenApiParam(name = STORE_RULE, type = StoreRule.class, description = STORE_RULE_DESC),
@OpenApiParam(name = OVERRIDE_PROTECTION, type = Boolean.class, description =
"A flag to ignore the protected data quality when storing data. \"'true' or 'false'\""),
@OpenApiParam(name = DATUM, type = VerticalDatum.class, description = "If the provided "
+ "time-series includes an explicit vertical-datum-info attribute "
+ "then it is assumed that the data is in the datum specified by the vertical-datum-info. "
+ "If the input timeseries does not include vertical-datum-info and "
+ "this parameter is not provided it is assumed that the data is in the as-stored "
+ "datum and no conversion is necessary. "
+ "If the input timeseries does not include vertical-datum-info and "
+ "this parameter is provided it is assumed that the data is in the Datum named by the argument "
+ "and should be converted to the as-stored datum before being saved.")
},
method = HttpMethod.PATCH,
path = "/timeseries/{timeseries}",
tags = TAG
)
@Override
public void update(@NotNull Context ctx, @NotNull String id) {
try (final Timer.Context ignored = markAndTime(UPDATE)) {
DSLContext dsl = getDslContext(ctx);
TimeSeriesDao dao = getTimeSeriesDao(dsl);
TimeSeries timeSeries = deserializeTimeSeries(ctx);
boolean createAsLrts = ctx.queryParamAsClass(CREATE_AS_LRTS, Boolean.class)
.getOrDefault(false);
StoreRule storeRule = ctx.queryParamAsClass(STORE_RULE, StoreRule.class)
.getOrDefault(StoreRule.REPLACE_ALL);
boolean overrideProtection = ctx.queryParamAsClass(OVERRIDE_PROTECTION, Boolean.class)
.getOrDefault(TimeSeriesDaoImpl.OVERRIDE_PROTECTION);
VerticalDatum vd = ctx.queryParamAsClass(DATUM, VerticalDatum.class)
.getOrDefault(null);
vd = TimeSeriesVerticalDatumConverter.getVerticalDatum(timeSeries).orElse(vd);
dao.store(timeSeries, createAsLrts, storeRule, overrideProtection, vd);
ctx.status(HttpServletResponse.SC_OK);
} catch (DataAccessException | IOException ex) {
CdaError re = ErrorTraceSupport.buildError(ctx, "Internal Error", ex);
logger.atSevere().withCause(ex).log("%s", re.toString());
ctx.status(HttpServletResponse.SC_INTERNAL_SERVER_ERROR).json(re);
}
}
private TimeSeries deserializeTimeSeries(Context ctx) throws IOException {
String contentTypeHeader = ctx.req.getContentType();
StringWriter writer = new StringWriter();
IOUtils.copy(ctx.bodyAsInputStream(), writer, StandardCharsets.UTF_8);
if (writer.toString().contains("data-entry-date")) {
throw new IllegalArgumentException("Data entry date is not allowed in the request");
}
ContentType contentType = Formats.parseHeader(contentTypeHeader, TimeSeries.class);
return Formats.parseContent(contentType, writer.toString(), TimeSeries.class);
}
/**
* Builds a URL that references a specific "page" of the result.
*
* @param ctx the context of the request
* @param ts the TimeSeries object that was used to generate the result
* @return a URL that references the same query, but with a different "page" parameter
*/
public String buildRequestUrl(Context ctx, TimeSeries ts, String cursor) throws URISyntaxException {
URIBuilder builder = new URIBuilder(ctx.req.getRequestURL().toString()); // requestURL stops just before '?'
// Instead of adding specific parameters and risk forgetting to add one to this method
// Lets add all the previous parameters and then (cont.)
builder.setParameters(URLEncodedUtils.parse(ctx.req.getQueryString(), StandardCharsets.UTF_8));
// (cont.) override or add the page parameter with the new cursor value
if (cursor != null && !cursor.isEmpty()) {
builder.setParameter("page", cursor);
}
return builder.build().toString();
}
}