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.marshall.sse; 018 019 020import java.io.*; 021import java.util.*; 022import static org.apache.juneau.commons.utils.Shorts.*; 023 024/** 025 * Line-driven {@link Iterator} over the events in a <c>text/event-stream</c>. 026 * 027 * <p> 028 * Wraps a {@link Reader} the caller owns and dispatches one {@link SseEvent} at a time using the 029 * WHATWG parse state machine (<a class="doclink" href="https://html.spec.whatwg.org/multipage/server-sent-events.html">HTML §9.2</a>). 030 * Designed for long-lived SSE streams that outlive any reasonable per-call buffer. 031 * 032 * <h5 class='section'>Why a separate class:</h5> 033 * <p> 034 * No other Juneau marshaller ships a separate iterator-style helper. SSE warrants the deviation 035 * because SSE responses are open until the client disconnects or the server pushes its last event; 036 * forcing callers through the eager {@code List<SseEvent>} path would defeat the entire point of 037 * using SSE. 038 * 039 * <h5 class='figure'>Example:</h5> 040 * <p class='bjava'> 041 * <jk>try</jk> (SseEventReader <jv>r</jv> = <jk>new</jk> SseEventReader(<jv>responseReader</jv>)) { 042 * <jk>while</jk> (<jv>r</jv>.hasNext()) { 043 * SseEvent <jv>e</jv> = <jv>r</jv>.next(); 044 * <jc>// Dispatch...</jc> 045 * } 046 * } 047 * </p> 048 * 049 * <h5 class='section'>Notes:</h5><ul> 050 * <li class='warn'>This class is not thread safe. 051 * </ul> 052 * 053 * <h5 class='section'>See Also:</h5><ul> 054 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Sse">SSE</a> 055 * </ul> 056 */ 057@SuppressWarnings({ 058 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 059 "java:S6541", // Brain method acceptable for readLine / nextEvent — they encode the SSE lexical state machine 060 "resource" // in is the caller-owned Reader (see class Javadoc); this iterator reads from it but does not own its lifecycle. 061}) 062public class SseEventReader implements Iterator<SseEvent>, Closeable { 063 064 /** UTF-8 BOM character that must be stripped exactly once from the start of an SSE stream. */ 065 private static final int BOM = 0xFEFF; 066 067 private final Reader in; 068 069 private boolean bomChecked; 070 private boolean eof; 071 private SseEvent peeked; 072 073 private final StringBuilder dataBuf = new StringBuilder(); 074 private String eventName; 075 private String lastEventId; 076 private Long retryValue; 077 private boolean haveAnyField; 078 079 private int pendingChar = -1; 080 081 /** 082 * Constructor. 083 * 084 * @param in The reader to consume events from. Must not be <jk>null</jk>. Caller retains ownership; 085 * {@link #close()} closes this reader. 086 */ 087 @SuppressWarnings({ 088 "java:S2095" // Reader ownership is transferred to this Closeable and released by close(). 089 }) 090 public SseEventReader(Reader in) { 091 this.in = reqnn("in", in); 092 } 093 094 @Override /* Overridden from Iterator */ 095 public boolean hasNext() { 096 if (peeked != null) 097 return true; 098 if (eof) 099 return false; 100 try { 101 peeked = nextEvent(); 102 } catch (IOException e) { 103 throw new UncheckedIOException(e); 104 } 105 return peeked != null; 106 } 107 108 @Override /* Overridden from Iterator */ 109 public SseEvent next() { 110 if (!hasNext()) 111 throw new NoSuchElementException(); 112 var out = peeked; 113 peeked = null; 114 return out; 115 } 116 117 @Override /* Overridden from Closeable */ 118 public void close() throws IOException { 119 in.close(); 120 } 121 122 /** 123 * Drains the remaining events into a list, then closes this reader. 124 * 125 * @return The list of events in order. 126 * @throws IOException If an I/O error occurs while reading. 127 */ 128 public List<SseEvent> toList() throws IOException { 129 var out = new ArrayList<SseEvent>(); 130 try { 131 while (hasNext()) 132 out.add(next()); 133 } catch (UncheckedIOException e) { 134 throw e.getCause(); 135 } finally { 136 close(); 137 } 138 return out; 139 } 140 141 private SseEvent nextEvent() throws IOException { 142 while (true) { 143 var line = readLine(); 144 if (line == null) { 145 eof = true; 146 if (haveAnyField) 147 return dispatch(); 148 return null; 149 } 150 if (line.isEmpty()) { 151 if (haveAnyField) 152 return dispatch(); 153 continue; 154 } 155 processField(line); 156 } 157 } 158 159 private SseEvent dispatch() { 160 var out = new SseEvent(); 161 out.setEvent(eventName); 162 if (!dataBuf.isEmpty()) { 163 if (dataBuf.charAt(dataBuf.length() - 1) == '\n') 164 dataBuf.deleteCharAt(dataBuf.length() - 1); 165 out.setData(dataBuf.toString()); 166 } 167 out.setId(lastEventId); 168 out.setRetry(retryValue); 169 resetEventBuffers(); 170 return out; 171 } 172 173 private void resetEventBuffers() { 174 dataBuf.setLength(0); 175 eventName = null; 176 lastEventId = null; 177 retryValue = null; 178 haveAnyField = false; 179 } 180 181 private void processField(String line) { 182 if (line.charAt(0) == ':') { 183 haveAnyField = true; 184 return; 185 } 186 String fieldName; 187 String fieldValue; 188 var colon = line.indexOf(':'); 189 if (colon < 0) { 190 fieldName = line; 191 fieldValue = ""; 192 } else { 193 fieldName = line.substring(0, colon); 194 fieldValue = line.substring(colon + 1); 195 if (!fieldValue.isEmpty() && fieldValue.charAt(0) == ' ') 196 fieldValue = fieldValue.substring(1); 197 } 198 haveAnyField = true; 199 applyField(fieldName, fieldValue); 200 } 201 202 private void applyField(String fieldName, String fieldValue) { 203 switch (fieldName) { 204 case "event": 205 eventName = fieldValue; 206 break; 207 case "data": 208 dataBuf.append(fieldValue).append('\n'); 209 break; 210 case "id": 211 if (fieldValue.indexOf('\u0000') < 0) 212 lastEventId = fieldValue; 213 break; 214 case "retry": 215 retryValue = parseRetry(fieldValue); 216 break; 217 default: 218 break; 219 } 220 } 221 222 private static Long parseRetry(String s) { 223 if (s.isEmpty()) 224 return null; 225 for (var i = 0; i < s.length(); i++) { 226 var c = s.charAt(i); 227 if (c < '0' || c > '9') 228 return null; 229 } 230 try { 231 return Long.valueOf(s); 232 } catch (@SuppressWarnings("unused") NumberFormatException e) { 233 return null; 234 } 235 } 236 237 @SuppressWarnings({ 238 "java:S3776" // SSE line parsing is a deliberate lexical state machine. 239 }) 240 private String readLine() throws IOException { 241 var sb = new StringBuilder(); 242 int c; 243 var sawAny = false; 244 while (true) { 245 if (pendingChar != -1) { 246 c = pendingChar; 247 pendingChar = -1; 248 } else { 249 c = in.read(); 250 } 251 if (c == -1) { 252 if (!sawAny) 253 return null; 254 return sb.toString(); 255 } 256 if (!bomChecked) { 257 bomChecked = true; 258 if (c == BOM) 259 continue; 260 } 261 sawAny = true; 262 if (c == '\n') 263 return sb.toString(); 264 if (c == '\r') { 265 var next = in.read(); 266 if (next != -1 && next != '\n') 267 pendingChar = next; 268 return sb.toString(); 269 } 270 sb.append((char) c); 271 } 272 } 273}