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}