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&amp;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}