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.console.views;
018
019import java.io.*;
020import java.util.*;
021
022import org.apache.juneau.http.response.BadRequest;
023import org.apache.juneau.http.response.Gone;
024import org.apache.juneau.http.response.NotFound;
025import org.apache.juneau.marshall.json.*;
026import org.apache.juneau.rest.server.*;
027import org.apache.juneau.terminal.report.*;
028
029/**
030 * Static helpers that implement the terminal-view events endpoint.
031 *
032 * <p>
033 * {@link TerminalViewMixin} delegates to these; an application that wants its own path writes one {@code @RestGet}
034 * method that calls {@link #events(Optional, RestRequest, RestResponse)} and gets the same validation, cap and
035 * headers.
036 */
037public final class TerminalViewEndpoints {
038
039   /** The page size cap of the bundled endpoint. */
040   public static final int MAX_ENDPOINT_PAGE_EVENTS = 2000;
041
042   private TerminalViewEndpoints() {}
043
044   /**
045    * Checks a mixin {@code runId} against {@code ^[A-Za-z0-9_-]{1,128}$}.
046    *
047    * @param runId The run id from the URL path.
048    * @return The same id.
049    * @throws BadRequest If the id does not match.
050    */
051   public static String checkRunId(String runId) {
052      if (! TerminalViewChecks.isRunId(runId))
053         throw new BadRequest("runId must match [A-Za-z0-9_-]{1,128}");
054      return runId;
055   }
056
057   /**
058    * Serves one page of the events endpoint ({@code ?after=}) as JSON.
059    *
060    * <p>
061    * Answers {@code 404} for an empty source, {@code 400} for a malformed {@code after}, {@code 410 Gone} for an
062    * unknown or stale token, and otherwise {@code 200} with {@code Cache-Control: no-store}.
063    *
064    * @param source The resolved source; empty answers {@code 404}.
065    * @param req The request.
066    * @param res The response.
067    * @throws IOException If the response could not be written.
068    */
069   public static void events(Optional<TerminalViewSource> source, RestRequest req, RestResponse res) throws IOException {
070      var src = source.orElseThrow(NotFound::new);
071      var after = req.getQueryParam("after").asString().orElse(null);
072      if (after != null && ! TerminalViewChecks.isToken(after))
073         throw new BadRequest("after is not a valid token");
074      TerminalViewPage page;
075      try {
076         page = src.page(after, MAX_ENDPOINT_PAGE_EVENTS);
077      } catch (TerminalViewSource.UnknownTokenException e) {
078         throw new Gone("unknown or stale terminal-view token");
079      }
080      page.validate();
081      res.setHeader("Cache-Control", "no-store");
082      try (var w = res.getDirectWriter("application/json")) {
083         JsonSerializer.DEFAULT.write(page.toContractMap(), w);
084      }
085   }
086}