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 static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023import org.apache.juneau.marshall.json.*;
024import org.apache.juneau.terminal.report.*;
025
026/**
027 * One page of a terminal-view event stream: the envelope the events endpoint returns and the client polls.
028 *
029 * <p>
030 * The wire form is <c>{contractVersion, events, next, more, terminal}</c>.  A page with no events still carries
031 * <c>next</c>.
032 */
033public final class TerminalViewPage {
034
035   /** The contract version carried by every page. */
036   public static final String CONTRACT_VERSION = "1";
037
038   /** The maximum number of events in one page. */
039   public static final int MAX_PAGE_EVENTS = 10_000;
040
041   /** The maximum serialized size of the events in one page. */
042   public static final int MAX_PAGE_CHARS = 8_388_608;
043
044   private final List<TerminalEvent> events;
045   private final String next;
046   private final boolean more;
047   private final boolean terminal;
048
049   private TerminalViewPage(List<TerminalEvent> events, String next, boolean more, boolean terminal) {
050      this.events = events;
051      this.next = next;
052      this.more = more;
053      this.terminal = terminal;
054   }
055
056   /**
057    * Creates a page.
058    *
059    * @param events The events, in ascending <c>seq</c>.  Copied defensively.
060    * @param next The continuation token.
061    * @param more Whether the source holds further events right now.
062    * @param terminal Whether the source will never produce another event.
063    * @return A new page.  Call {@link #validate()} before sending it.
064    */
065   public static TerminalViewPage of(List<TerminalEvent> events, String next, boolean more, boolean terminal) {
066      return new TerminalViewPage(List.copyOf(events == null ? List.of() : events), next, more, terminal);
067   }
068
069   /**
070    * Returns the events.
071    *
072    * @return An unmodifiable list.
073    */
074   public List<TerminalEvent> events() {
075      return events;
076   }
077
078   /**
079    * Returns the continuation token.
080    *
081    * @return The token.
082    */
083   public String next() {
084      return next;
085   }
086
087   /**
088    * Returns whether the source holds further events right now.
089    *
090    * @return <jk>true</jk> if so.
091    */
092   public boolean more() {
093      return more;
094   }
095
096   /**
097    * Returns whether the source will never produce another event.
098    *
099    * @return <jk>true</jk> if so.
100    */
101   public boolean terminal() {
102      return terminal;
103   }
104
105   /**
106    * Validates the page: ascending numbered events, the token grammar, and the size caps.
107    *
108    * @return This object.
109    * @throws IllegalArgumentException If the page is not valid.
110    */
111   public TerminalViewPage validate() {
112      if (! TerminalViewChecks.isToken(next))
113         throw iaex("TerminalViewPage next must match ^[A-Za-z0-9._~-]{1,128}$; got '%s'", TerminalViewChecks.clip(next));
114      if (events.size() > MAX_PAGE_EVENTS)
115         throw iaex("TerminalViewPage events must hold at most %s events; got %s", MAX_PAGE_EVENTS, events.size());
116      if (more && events.isEmpty())
117         throw iaex("TerminalViewPage must not set more with no events");
118      var last = 0L;
119      long chars = 0;
120      for (var e : events) {
121         if (e.seq() < 1)
122            throw iaex("TerminalViewPage events must carry a seq >= 1; got %s on a '%s' event", e.seq(), e.kind().name().toLowerCase(Locale.ROOT));
123         if (e.seq() <= last)
124            throw iaex("TerminalViewPage events must be in strictly ascending seq order; got %s after %s", e.seq(), last);
125         last = e.seq();
126         chars += JsonSerializer.DEFAULT.toString(e.toContractMap()).length();
127      }
128      if (chars > MAX_PAGE_CHARS)
129         throw iaex("TerminalViewPage events must total at most %s chars; got %s", MAX_PAGE_CHARS, chars);
130      return this;
131   }
132
133   /**
134    * Returns the page in its wire form.
135    *
136    * @return An ordered map: <c>contractVersion</c>, <c>events</c>, <c>next</c>, <c>more</c>, <c>terminal</c>.
137    */
138   public Map<String,Object> toContractMap() {
139      var m = new LinkedHashMap<String,Object>();
140      m.put("contractVersion", CONTRACT_VERSION);
141      var list = events.stream().map(TerminalEvent::toContractMap).toList();
142      m.put("events", list);
143      m.put("next", next);
144      m.put("more", more);
145      m.put("terminal", terminal);
146      return m;
147   }
148}