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
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022
023import org.apache.juneau.commons.collections.*;
024import org.apache.juneau.marshall.serializer.*;
025import org.apache.juneau.marshall.stream.*;
026
027/**
028 * Serializes {@link SseEvent} objects to the <c>text/event-stream</c> wire format defined by the
029 * <a class="doclink" href="https://html.spec.whatwg.org/multipage/server-sent-events.html">WHATWG Server-Sent Events spec</a>.
030 *
031 * <h5 class='topic'>Media types</h5>
032 * <p>
033 * Produces <c>Content-Type</c> types: <bc>text/event-stream</bc>.
034 *
035 * <h5 class='topic'>Description</h5>
036 * <p>
037 * Accepts any of the following as input and emits one wire event per element followed by a blank
038 * line (the spec's "dispatch" boundary):
039 * <ul class='spaced-list'>
040 *    <li>A single {@link SseEvent}.
041 *    <li>{@link Iterable Iterable&lt;SseEvent&gt;}.
042 *    <li>An array {@code SseEvent[]}.
043 *    <li>{@link java.util.stream.Stream Stream&lt;SseEvent&gt;}.
044 * </ul>
045 *
046 * <p>
047 * After each event is written, the session calls {@link Writer#flush() Writer.flush()} so that downstream
048 * REST infrastructure flushes the underlying TCP socket and the event is delivered immediately to the
049 * client.
050 *
051 * <h5 class='section'>Notes:</h5><ul>
052 *    <li class='note'>This class is thread safe and reusable.
053 * </ul>
054 *
055 * <h5 class='section'>See Also:</h5><ul>
056 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Sse">SSE</a>
057 * </ul>
058 */
059@SuppressWarnings({
060   "java:S110", // Inheritance depth acceptable for serializer hierarchy
061   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
062   "java:S9149", // Per-format static factories intentionally shadow the parent's.
063   "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design.
064})
065public class SseSerializer extends WriterSerializer implements RecordWritable {
066
067   /**
068    * Wire-format media type produced by this serializer: <c>text/event-stream</c>.
069    */
070   public static final String MEDIA_TYPE = "text/event-stream";
071
072   /** Default serializer, all default settings. */
073   public static final SseSerializer DEFAULT = new SseSerializer(create());
074
075   /**
076    * Creates a new builder for this object.
077    *
078    * @return A new builder.
079    */
080   public static Builder create() {
081      return new Builder();
082   }
083
084   /**
085    * Builder class.
086    */
087   public static class Builder extends WriterSerializer.Builder<Builder> {
088
089      private static final Cache<HashKey,SseSerializer> CACHE = Cache.of(HashKey.class, SseSerializer.class).build();
090
091      /**
092       * Constructor, default settings.
093       */
094      protected Builder() {
095         produces(MEDIA_TYPE);
096         type(SseSerializer.class);
097      }
098
099      /**
100       * Copy constructor.
101       *
102       * @param copyFrom The builder to copy from. Must not be <jk>null</jk>.
103       */
104      protected Builder(Builder copyFrom) {
105         super(reqnn("copyFrom", copyFrom));
106      }
107
108      /**
109       * Copy constructor.
110       *
111       * @param copyFrom The serializer to copy from. Must not be <jk>null</jk>.
112       */
113      protected Builder(SseSerializer copyFrom) {
114         super(reqnn("copyFrom", copyFrom));
115      }
116
117      @Override /* Overridden from Context.Builder<?> */
118      public HashKey hashKey() {
119         return HashKey.of(super.hashKey());
120      }
121
122      @Override /* Overridden from Context.Builder<?> */
123      public SseSerializer build() {
124         return cache(CACHE).build(SseSerializer.class);
125      }
126
127      @Override /* Overridden from Context.Builder<?> */
128      public Builder copy() {
129         return new Builder(this);
130      }
131   }
132
133   /**
134    * Constructor.
135    *
136    * @param builder The builder for this object.
137    */
138   public SseSerializer(Builder builder) {
139      super(builder);
140   }
141
142   @Override /* Overridden from Context */
143   public Builder copy() {
144      return new Builder(this);
145   }
146
147   @Override /* Overridden from Context */
148   public SseSerializerSession.Builder createSession() {
149      return SseSerializerSession.create(this);
150   }
151
152   @Override /* Overridden from Context */
153   public SseSerializerSession getSession() {
154      return createSession().build();
155   }
156
157   /**
158    * Writes a comment line (heartbeat) to the supplied writer in the SSE wire format and flushes it.
159    *
160    * @param w The writer. Must not be <jk>null</jk>.
161    * @param comment The comment text. {@code null} is treated as an empty string.
162    * @throws IOException If a problem occurred writing to the underlying stream.
163    */
164   @SuppressWarnings({
165      "java:S2095" // Writer ownership is the caller's; we only write/flush.
166   })
167   public static void writeComment(Writer w, String comment) throws IOException {
168      reqnn("w", w);
169      var c = comment == null ? "" : comment;
170      for (var line : c.split("\n", -1))
171         w.write(": " + line + "\n");
172      w.write("\n");
173      w.flush();
174   }
175
176   /**
177    * Convenience delegator that opens a {@link RecordWriter} over the output using
178    * <b>default session arguments</b> (mirrors {@link #write(Object)}).
179    *
180    * <p>
181    * The real implementation lives on {@link SseSerializerSession#writeRecords(Object)}.
182    * Callers that need request-derived configuration should call {@link #createSession()} and
183    * invoke {@link SseSerializerSession#writeRecords(Object)} on the built session instead.
184    *
185    * @param output The output.
186    * @return A new {@link RecordWriter}.
187    * @throws IOException If a problem occurred opening the underlying output.
188    */
189   @Override /* RecordWritable */
190   public RecordWriter writeRecords(Object output) throws IOException {
191      return getSession().writeRecords(output);
192   }
193
194   @Override /* RecordWritable */
195   public boolean isRecordStreaming() {
196      return false;
197   }
198}