001/*
002 * Copyright 2022-2025 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet.core;
018
019import javax.annotation.Nonnull;
020import javax.annotation.Nullable;
021import javax.annotation.concurrent.NotThreadSafe;
022import javax.annotation.concurrent.ThreadSafe;
023import java.time.Duration;
024import java.util.ArrayList;
025import java.util.List;
026import java.util.Optional;
027import java.util.stream.Collectors;
028
029import static java.lang.String.format;
030import static java.util.Objects.requireNonNull;
031
032/**
033 * Encapsulates a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events">Server-Sent Event</a> payload that can be sent across the wire to a client.
034 * <p>
035 * For example:
036 * <pre>{@code  ServerSentEvent serverSentEvent = ServerSentEvent.withEvent("example")
037 *   .data("""
038 *     {
039 *       "testing": 123,
040 *       "value": "abc"
041 *     }
042 *     """)
043 *   .id(UUID.randomUUID().toString())
044 *   .retry(Duration.ofSeconds(5))
045 *   .build();}</pre>
046 * <p>
047 * To acquire an event suitable for heartbeats, use the {@link #forHeartbeat()} factory method.
048 * <p>
049 * See <a href="https://www.soklet.com/docs/server-sent-events">https://www.soklet.com/docs/server-sent-events</a> for detailed documentation.
050 * <p>
051 * Formal specification is available at <a href="https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events">https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events</a>.
052 *
053 * @author <a href="https://www.revetkn.com">Mark Allen</a>
054 */
055@ThreadSafe
056public class ServerSentEvent {
057        @Nonnull
058        private static final ServerSentEvent HEARTBEAT;
059
060        @Nullable
061        private final String id;
062        @Nullable
063        private final String event;
064        @Nullable
065        private final String data;
066        @Nullable
067        private final Duration retry;
068
069        static {
070                // This would be an event like ":\n\n"
071                HEARTBEAT = new ServerSentEvent(new ServerSentEvent.Builder());
072        }
073
074        /**
075         * Acquires a reference to the shared "heartbeat" event.
076         *
077         * @return the shared "heartbeat" event
078         */
079        @Nonnull
080        public static ServerSentEvent forHeartbeat() {
081                return HEARTBEAT;
082        }
083
084        /**
085         * Acquires a builder for {@link ServerSentEvent} instances, seeded with an {@code event} value.
086         *
087         * @param event the {@code event} value for the instance
088         * @return the builder
089         */
090        @Nonnull
091        public static ServerSentEvent.Builder withEvent(@Nullable String event) {
092                return new Builder().event(event);
093        }
094
095        /**
096         * Acquires a builder for {@link ServerSentEvent} instances, seeded with a {@code data} value.
097         *
098         * @param data the {@code data} value for the instance
099         * @return the builder
100         */
101        @Nonnull
102        public static ServerSentEvent.Builder withData(@Nullable String data) {
103                return new Builder().data(data);
104        }
105
106        /**
107         * Acquires an "empty" builder for {@link ServerSentEvent} instances.
108         *
109         * @return the builder
110         */
111        @Nonnull
112        public static ServerSentEvent.Builder builder() {
113                return new Builder();
114        }
115
116        protected ServerSentEvent(@Nonnull ServerSentEvent.Builder builder) {
117                requireNonNull(builder);
118
119                this.id = builder.id;
120                this.event = builder.event;
121                this.data = builder.data;
122                this.retry = builder.retry;
123        }
124
125        /**
126         * Builder used to construct instances of {@link ServerSentEvent} via {@link ServerSentEvent#withEvent(String)} or {@link ServerSentEvent#withData(String)}.
127         * <p>
128         * This class is intended for use by a single thread.
129         *
130         * @author <a href="https://www.revetkn.com">Mark Allen</a>
131         */
132        @NotThreadSafe
133        public static class Builder {
134                @Nullable
135                private String id;
136                @Nullable
137                private String event;
138                @Nullable
139                private String data;
140                @Nullable
141                private Duration retry;
142
143                protected Builder() {
144                        // Nothing to do
145                }
146
147                @Nonnull
148                public Builder id(@Nullable String id) {
149                        this.id = id;
150                        return this;
151                }
152
153                @Nonnull
154                public Builder event(@Nullable String event) {
155                        this.event = event;
156                        return this;
157                }
158
159                @Nonnull
160                public Builder data(@Nullable String data) {
161                        this.data = data;
162                        return this;
163                }
164
165                @Nonnull
166                public Builder retry(@Nullable Duration retry) {
167                        this.retry = retry;
168                        return this;
169                }
170
171                @Nonnull
172                public ServerSentEvent build() {
173                        return new ServerSentEvent(this);
174                }
175        }
176
177        @Override
178        @Nonnull
179        public String toString() {
180                List<String> components = new ArrayList<>(4);
181
182                if (this.event != null)
183                        components.add(format("event=%s", this.event));
184                if (this.id != null)
185                        components.add(format("id=%s", this.id));
186                if (this.retry != null)
187                        components.add(format("retry=%s", this.retry));
188                if (this.data != null)
189                        components.add(format("data=%s", this.data.trim()));
190
191                return format("%s{%s}", getClass().getSimpleName(), components.stream().collect(Collectors.joining(", ")));
192        }
193
194        /**
195         * Is this instance the shared "heartbeat" event (as provided via {@link ServerSentEvent#forHeartbeat()})?
196         *
197         * @return {@code true} if this instance is the shared "heartbeat" event, {@code false} otherwise
198         */
199        @Nonnull
200        public Boolean isHeartbeat() {
201                return this == HEARTBEAT;
202        }
203
204        /**
205         * The {@code id} for this Server-Sent Event, used by clients to populate the {@code Last-Event-ID} request header should a reconnect occur.
206         * <p>
207         * Formal specification is available at <a href="https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events">https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events</a>.
208         *
209         * @return the optional {@code id} for this Server-Sent Event
210         */
211        @Nonnull
212        public Optional<String> getId() {
213                return Optional.ofNullable(this.id);
214        }
215
216        /**
217         * The {@code event} value for this Server-Sent Event.
218         * <p>
219         * Formal specification is available at <a href="https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events">https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events</a>.
220         *
221         * @return the optional {@code event} value for this Server-Sent Event
222         */
223        @Nonnull
224        public Optional<String> getEvent() {
225                return Optional.ofNullable(this.event);
226        }
227
228        /**
229         * The {@code data} payload for this Server-Sent Event.
230         * <p>
231         * Formal specification is available at <a href="https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events">https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events</a>.
232         *
233         * @return the optional {@code data} payload for this Server-Sent Event
234         */
235        @Nonnull
236        public Optional<String> getData() {
237                return Optional.ofNullable(this.data);
238        }
239
240        /**
241         * The {@code retry} duration for this Server-Sent Event.
242         * <p>
243         * Formal specification is available at <a href="https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events">https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events</a>.
244         *
245         * @return the optional {@code retry} duration for this Server-Sent Event
246         */
247        @Nonnull
248        public Optional<Duration> getRetry() {
249                return Optional.ofNullable(this.retry);
250        }
251}