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.*;
022import java.util.concurrent.*;
023import java.util.regex.*;
024import org.apache.juneau.terminal.report.*;
025
026/**
027 * A thread-safe, in-memory {@link TerminalViewSource} that a producer appends to directly.
028 *
029 * <p>
030 * Tokens are <c>&lt;epoch&gt;.&lt;n&gt;</c>, where <c>epoch</c> is six characters of <c>[0-9a-z]</c> regenerated by
031 * {@link #reset()} and <c>n</c> is the last <c>seq</c> the client has.  A page returns the events with
032 * <c>seq &gt; n</c>.
033 *
034 * <p>
035 * A <c>done</c> event does not close the log by itself; {@link #close()} is explicit, so an adopter may still append a
036 * late note.  A host that pushes events over its own transport (for example server-sent events) forwards the numbered
037 * event returned by {@link #append(TerminalEvent)}.
038 */
039public final class TerminalViewLog implements TerminalViewSource {
040
041   /** The default capacity. */
042   public static final int DEFAULT_MAX_EVENTS = 200_000;
043
044   private static final Pattern TOKEN = Pattern.compile("^([0-9a-z]{6})\\.(0|[1-9]\\d{0,17})$");
045   private static final String ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
046
047   private final int maxEvents;
048   private final List<TerminalEvent> events = new ArrayList<>();
049   private String epoch = newEpoch(null);
050   private boolean closed;
051
052   private TerminalViewLog(int maxEvents) {
053      this.maxEvents = maxEvents;
054   }
055
056   /**
057    * Creates a log with the default capacity.
058    *
059    * @return A new log.
060    */
061   public static TerminalViewLog create() {
062      return new TerminalViewLog(DEFAULT_MAX_EVENTS);
063   }
064
065   /**
066    * Creates a log with a given capacity.
067    *
068    * @param maxEvents The maximum number of events, at least 1.
069    * @return A new log.
070    */
071   public static TerminalViewLog create(int maxEvents) {
072      if (maxEvents < 1)
073         throw iaex("TerminalViewLog maxEvents must be >= 1; got %s", maxEvents);
074      return new TerminalViewLog(maxEvents);
075   }
076
077   /**
078    * Appends an event, assigning <c>seq = size + 1</c>.
079    *
080    * @param event The event.
081    * @return The numbered copy that was stored.
082    * @throws IllegalStateException If the log is closed or full.
083    * @throws IllegalArgumentException If the event is too large.
084    */
085   public TerminalEvent append(TerminalEvent event) {
086      return appendAll(List.of(event)).get(0);
087   }
088
089   /**
090    * Appends events atomically, numbering them consecutively.
091    *
092    * @param batch The events.
093    * @return The numbered copies, in order.
094    * @throws IllegalStateException If the log is closed or the batch does not fit.
095    * @throws IllegalArgumentException If an event is too large.
096    */
097   public List<TerminalEvent> appendAll(Collection<TerminalEvent> batch) {
098      for (var e : batch)
099         e.validate();
100      synchronized (events) {
101         if (closed)
102            throw new IllegalStateException("TerminalViewLog is closed");
103         if (events.size() + batch.size() > maxEvents)
104            throw new IllegalStateException("TerminalViewLog is full (" + maxEvents + " events)");
105         var out = new ArrayList<TerminalEvent>(batch.size());
106         for (var e : batch) {
107            var numbered = e.withSeq(events.size() + 1L);
108            events.add(numbered);
109            out.add(numbered);
110         }
111         return out;
112      }
113   }
114
115   /**
116    * Marks the log terminal: no further events will be appended.
117    */
118   public void close() {
119      synchronized (events) {
120         closed = true;
121      }
122   }
123
124   /**
125    * Returns whether {@link #close()} has been called.
126    *
127    * @return <jk>true</jk> if the log is closed.
128    */
129   public boolean isTerminal() {
130      synchronized (events) {
131         return closed;
132      }
133   }
134
135   /**
136    * Clears the log, reopens it, and bumps the epoch so that tokens issued before the reset answer <c>410 Gone</c>.
137    */
138   public void reset() {
139      synchronized (events) {
140         events.clear();
141         closed = false;
142         epoch = newEpoch(epoch);
143      }
144   }
145
146   @Override /* TerminalViewSource */
147   public TerminalViewPage page(String after, int max) {
148      var limit = Math.max(1, Math.min(max, TerminalViewPage.MAX_PAGE_EVENTS));
149      synchronized (events) {
150         var from = 0;
151         if (after != null) {
152            var m = TerminalViewChecks.isToken(after) ? TOKEN.matcher(after) : null;
153            if (m == null || ! m.matches() || ! m.group(1).equals(epoch))
154               throw new UnknownTokenException(after);
155            long n = Long.parseLong(m.group(2));
156            if (n > events.size())
157               throw new UnknownTokenException(after);
158            from = (int)n;
159         }
160         var to = Math.min(events.size(), from + limit);
161         var page = new ArrayList<>(events.subList(from, to));
162         var more = to < events.size();
163         return TerminalViewPage.of(page, epoch + "." + to, more, closed && ! more);
164      }
165   }
166
167   private static String newEpoch(String not) {
168      var r = ThreadLocalRandom.current();
169      String s;
170      do {
171         var sb = new StringBuilder(6);
172         for (var i = 0; i < 6; i++)
173            sb.append(ALPHABET.charAt(r.nextInt(ALPHABET.length())));
174         s = sb.toString();
175      } while (s.equals(not));
176      return s;
177   }
178}