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><epoch>.<n></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 > 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}