001/*
002 * Copyright 2022-2026 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;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.NotThreadSafe;
023import javax.annotation.concurrent.ThreadSafe;
024import java.time.Duration;
025import java.util.Objects;
026import java.util.Optional;
027
028import static java.lang.String.format;
029import static java.util.Objects.requireNonNull;
030
031/**
032 * Immutable details describing why and when a stream terminated.
033 *
034 * @author <a href="https://www.revetkn.com">Mark Allen</a>
035 */
036@ThreadSafe
037public final class StreamTermination {
038        @NonNull
039        private final StreamTerminationReason reason;
040        @NonNull
041        private final Duration duration;
042        @Nullable
043        private final Throwable cause;
044
045        /**
046         * Acquires a builder for {@link StreamTermination} instances.
047         *
048         * @param reason   why the stream terminated
049         * @param duration how long the stream existed
050         * @return the builder
051         */
052        @NonNull
053        public static Builder with(@NonNull StreamTerminationReason reason,
054                                                                                                                 @NonNull Duration duration) {
055                requireNonNull(reason);
056                requireNonNull(duration);
057
058                return new Builder(reason, duration);
059        }
060
061        private StreamTermination(@NonNull Builder builder) {
062                requireNonNull(builder);
063
064                this.reason = builder.reason;
065                this.duration = builder.duration;
066                this.cause = builder.cause;
067        }
068
069        @Override
070        @NonNull
071        public String toString() {
072                return format("%s{reason=%s, duration=%s, cause=%s}", getClass().getSimpleName(),
073                                getReason(), getDuration(), getCause().orElse(null));
074        }
075
076        @Override
077        public boolean equals(@Nullable Object object) {
078                if (this == object)
079                        return true;
080
081                if (!(object instanceof StreamTermination streamTermination))
082                        return false;
083
084                return Objects.equals(getReason(), streamTermination.getReason())
085                                && Objects.equals(getDuration(), streamTermination.getDuration())
086                                && Objects.equals(getCause(), streamTermination.getCause());
087        }
088
089        @Override
090        public int hashCode() {
091                return Objects.hash(getReason(), getDuration(), getCause());
092        }
093
094        /**
095         * Why the stream terminated.
096         *
097         * @return the termination reason
098         */
099        @NonNull
100        public StreamTerminationReason getReason() {
101                return this.reason;
102        }
103
104        /**
105         * How long the stream existed.
106         *
107         * @return the stream duration
108         */
109        @NonNull
110        public Duration getDuration() {
111                return this.duration;
112        }
113
114        /**
115         * The underlying termination cause, if available.
116         *
117         * @return the underlying cause, or {@link Optional#empty()} if unavailable
118         */
119        @NonNull
120        public Optional<Throwable> getCause() {
121                return Optional.ofNullable(this.cause);
122        }
123
124        /**
125         * Vends a mutable copier seeded with this instance's data, suitable for building new instances.
126         *
127         * @return a copier for this instance
128         */
129        @NonNull
130        public Copier copy() {
131                return new Copier(this);
132        }
133
134        /**
135         * Builder used to construct instances of {@link StreamTermination}.
136         */
137        @NotThreadSafe
138        public static class Builder {
139                @NonNull
140                private StreamTerminationReason reason;
141                @NonNull
142                private Duration duration;
143                @Nullable
144                private Throwable cause;
145
146                protected Builder(@NonNull StreamTerminationReason reason,
147                                                                                        @NonNull Duration duration) {
148                        this.reason = requireNonNull(reason);
149                        this.duration = requireNonNull(duration);
150                }
151
152                /**
153                 * Specifies why the stream terminated.
154                 *
155                 * @param reason the termination reason
156                 * @return this builder
157                 */
158                @NonNull
159                public Builder reason(@NonNull StreamTerminationReason reason) {
160                        this.reason = requireNonNull(reason);
161                        return this;
162                }
163
164                /**
165                 * Specifies how long the stream existed.
166                 *
167                 * @param duration the stream duration
168                 * @return this builder
169                 */
170                @NonNull
171                public Builder duration(@NonNull Duration duration) {
172                        this.duration = requireNonNull(duration);
173                        return this;
174                }
175
176                /**
177                 * Specifies the underlying termination cause.
178                 *
179                 * @param cause the cause, or {@code null} if unavailable
180                 * @return this builder
181                 */
182                @NonNull
183                public Builder cause(@Nullable Throwable cause) {
184                        this.cause = cause;
185                        return this;
186                }
187
188                /**
189                 * Builds a {@link StreamTermination} instance.
190                 *
191                 * @return the termination details
192                 */
193                @NonNull
194                public StreamTermination build() {
195                        return new StreamTermination(this);
196                }
197        }
198
199        /**
200         * Mutable copier seeded with an existing {@link StreamTermination}.
201         */
202        @NotThreadSafe
203        public static final class Copier extends Builder {
204                Copier(@NonNull StreamTermination streamTermination) {
205                        super(streamTermination.getReason(), streamTermination.getDuration());
206                        cause(streamTermination.getCause().orElse(null));
207                }
208
209                @Override
210                @NonNull
211                public Copier reason(@NonNull StreamTerminationReason reason) {
212                        super.reason(reason);
213                        return this;
214                }
215
216                @Override
217                @NonNull
218                public Copier duration(@NonNull Duration duration) {
219                        super.duration(duration);
220                        return this;
221                }
222
223                @Override
224                @NonNull
225                public Copier cause(@Nullable Throwable cause) {
226                        super.cause(cause);
227                        return this;
228                }
229        }
230}