001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.petstore.console.dev; 018 019import static java.nio.charset.StandardCharsets.*; 020 021import java.io.*; 022import java.time.*; 023import java.time.format.*; 024import java.time.temporal.*; 025import java.util.*; 026import java.util.stream.*; 027 028import org.apache.juneau.http.*; 029import org.apache.juneau.http.response.*; 030import org.apache.juneau.petstore.console.*; 031import org.apache.juneau.petstore.console.pages.*; 032import org.apache.juneau.rest.server.*; 033import org.apache.juneau.console.data.*; 034import org.apache.juneau.rest.server.view.*; 035 036/** 037 * Data sources: a demo page built with {@link DevPageBuilders#data()} only (it has no template twin). 038 * 039 * <p> 040 * {@code /orders} streams one CSV row per UTC hour in {@code [start, end)} with {@link SeriesSource}. The browser 041 * requests it once per UTC day chunk and caches closed days in IndexedDB. 042 * 043 * <h5 class='section'>Example:</h5> 044 * <p class='bcode'> 045 * GET /console/dev/data 046 * GET /console/dev/data/orders?start=2026-10-01T00:00:00Z&end=2026-10-02T00:00:00Z 047 * </p> 048 */ 049@Rest(path="/data", title="Data sources") 050@SuppressWarnings({ 051 "java:S110", // Inheritance depth comes from the BasicRestServlet hierarchy, not this page. 052 "resource" // The response writer is owned by the servlet container; Eclipse JDT @Owning warning is by design. 053}) 054public class DataRest extends PetstoreConsolePage { 055 056 private static final long serialVersionUID = 1L; 057 058 /** The longest window {@code /orders} serves. */ 059 static final Duration MAX_WINDOW = Duration.ofDays(31); 060 061 /** 062 * Renders the Data sources page. 063 * 064 * @param req The request. 065 * @return The page. 066 */ 067 @RestGet(path="/") 068 public View page(RestRequest req) { 069 return twin(req, DevPageBuilders.data()); 070 } 071 072 /** 073 * Hourly order counts in the window. 074 * 075 * <p> 076 * Writes one row for each UTC hour whose start falls in {@code [floor(start), end)}, where {@code floor(start)} is 077 * {@code start} rounded down to the hour. So {@code start=00:30&end=02:30} writes the 00:00, 01:00 and 02:00 rows. 078 * A window with {@code end} before {@code start}, or longer than 31 days, is a 400. 079 * 080 * @param start The window start, ISO-8601. 081 * @param end The window end, ISO-8601, exclusive. 082 * @param res The response. 083 * @throws IOException If the response can't be written. 084 */ 085 @RestGet(path="/orders") 086 public void orders(@Query("start") String start, @Query("end") String end, RestResponse res) throws IOException { 087 var from = instant("start", start); 088 var to = instant("end", end); 089 if (to.isBefore(from)) 090 throw new BadRequest("Window end '%s' is before start '%s'", end, start); 091 if (Duration.between(from, to).compareTo(MAX_WINDOW) > 0) 092 throw new BadRequest("Window '%s' to '%s' is longer than %s days", start, end, MAX_WINDOW.toDays()); 093 var first = from.truncatedTo(ChronoUnit.HOURS); 094 SeriesSource.csv(res).columns("t", "placed").write( 095 Stream.iterate(first, t -> t.isBefore(to), t -> t.plus(1, ChronoUnit.HOURS)) 096 .map(t -> SeriesSource.Row.of(Map.of("t", t, "placed", placed(t))))); 097 } 098 099 /** 100 * Serves the page-local script. 101 * 102 * @param res The response. 103 * @throws IOException If the bundled script can't be read. 104 */ 105 @RestGet(path="/data.js") 106 public void script(RestResponse res) throws IOException { 107 try (var in = DataRest.class.getResourceAsStream("data.js")) { 108 if (in == null) 109 throw new IllegalStateException( 110 "Missing classpath resource 'data.js' beside " + DataRest.class.getName()); 111 res.setContentType("text/javascript;charset=utf-8"); 112 res.getWriter().write(new String(in.readAllBytes(), UTF_8)); 113 } 114 } 115 116 static long placed(Instant t) { 117 return (t.getEpochSecond() / 3600 * 7) % 10 + 1; 118 } 119 120 private static Instant instant(String name, String value) { 121 if (value == null) 122 throw new BadRequest("Missing query parameter '%s'", name); 123 try { 124 return Instant.parse(value); 125 } catch (DateTimeParseException x) { 126 throw new BadRequest("Query parameter '%s' is not an ISO-8601 instant", name); 127 } 128 } 129}