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}