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}