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}