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.ThreadSafe;
023import java.util.Collections;
024import java.util.EnumMap;
025import java.util.EnumSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Optional;
029import java.util.Set;
030
031import static java.util.Objects.requireNonNull;
032
033/**
034 * Immutable result of one Soklet lifecycle attempt.
035 * <p>
036 * A result is complete when every configured lifecycle component has affirmative
037 * termination proof and no framework-tracked activity remains. Throwable
038 * instances retain their exact identities and may contain application-sensitive
039 * data; callers own any logging or disclosure decision.
040 */
041@ThreadSafe
042public final class ShutdownResult {
043        @NonNull
044        private final ShutdownDisposition shutdownDisposition;
045        @NonNull
046        private final StartupDisposition startupDisposition;
047        @NonNull
048        private final List<@NonNull ShutdownComponentResult>
049                        shutdownComponentResults;
050        @NonNull
051        private final Map<@NonNull ShutdownComponentType,
052                        @NonNull ShutdownComponentResult>
053                        shutdownComponentResultsByType;
054        @Nullable
055        private final Throwable startupFailureCause;
056        @Nullable
057        private final UnexpectedShutdownComponentTermination
058                        unexpectedShutdownComponentTermination;
059        @NonNull
060        private final InternalShutdownResult internalResult;
061
062        private ShutdownResult(@NonNull InternalShutdownResult internalResult,
063                        @Nullable Throwable startupFailureCause,
064                        @Nullable ShutdownComponentType unexpectedShutdownComponentType,
065                        @Nullable Throwable unexpectedCause) {
066                this.internalResult = requireNonNull(internalResult);
067                this.shutdownDisposition = ShutdownDisposition.valueOf(
068                                internalResult.disposition().name());
069                this.startupDisposition = StartupDisposition.valueOf(
070                                internalResult.startupDisposition().name());
071                String residualSummary = internalResult.retentionSummary()
072                                .map(LifecycleRetentionSummary::summary).orElse("");
073                this.shutdownComponentResults = internalResult.participantResults()
074                                .stream()
075                                .map(result -> fromInternal(result, residualSummary)).toList();
076                EnumMap<ShutdownComponentType, ShutdownComponentResult> indexed =
077                                new EnumMap<>(ShutdownComponentType.class);
078                for (ShutdownComponentResult result
079                                : this.shutdownComponentResults)
080                        indexed.put(result.getShutdownComponentType(), result);
081                this.shutdownComponentResultsByType = Collections.unmodifiableMap(
082                                indexed);
083                this.startupFailureCause = startupFailureCause;
084                this.unexpectedShutdownComponentTermination =
085                                unexpectedShutdownComponentType == null ? null
086                                : new UnexpectedShutdownComponentTermination(unexpectedShutdownComponentType,
087                                                unexpectedCause);
088        }
089
090        @NonNull
091        static ShutdownResult fromInternal(
092                        @NonNull InternalShutdownResult internalResult) {
093                return fromInternal(internalResult, null, null, null);
094        }
095
096        @NonNull
097        static ShutdownResult fromInternal(
098                        @NonNull InternalShutdownResult internalResult,
099                        @Nullable Throwable startupFailureCause,
100                        @Nullable ShutdownComponentType unexpectedShutdownComponentType,
101                        @Nullable Throwable unexpectedCause) {
102                return new ShutdownResult(internalResult, startupFailureCause,
103                                unexpectedShutdownComponentType, unexpectedCause);
104        }
105
106        @NonNull
107        private static ShutdownComponentResult fromInternal(
108                        @NonNull InternalLifecycleComponentShutdownResult internalResult,
109                        @NonNull String residualSummary) {
110                Set<ResidualActivityType> residualActivityTypes = internalResult
111                                .residualActivity().stream()
112                                .map(kind -> ResidualActivityType.valueOf(kind.name()))
113                                .collect(() -> EnumSet.noneOf(ResidualActivityType.class),
114                                                EnumSet::add, EnumSet::addAll);
115                ResidualActivityEvidence evidence = residualActivityTypes.isEmpty() ? null
116                                : new ResidualActivityEvidence(residualActivityTypes,
117                                                residualSummary.isEmpty()
118                                                                ? "Residual activity remains: "
119                                                                                + residualActivityTypes
120                                                                : residualSummary);
121                return new ShutdownComponentResult(
122                                ShutdownComponentType.valueOf(internalResult.kind().name()),
123                                ShutdownComponentDisposition.valueOf(
124                                                internalResult.disposition().name()),
125                                internalResult.failures(), evidence);
126        }
127
128        /** @return aggregate shutdown disposition */
129        @NonNull
130        public ShutdownDisposition getShutdownDisposition() {
131                return this.shutdownDisposition;
132        }
133
134        /** @return startup disposition for the same lifecycle attempt */
135        @NonNull
136        public StartupDisposition getStartupDisposition() {
137                return this.startupDisposition;
138        }
139
140        /**
141         * @return immutable shutdown component results in
142         * {@link ShutdownComponentType} enum order
143         */
144        @NonNull
145        public List<@NonNull ShutdownComponentResult> getShutdownComponentResults() {
146                return this.shutdownComponentResults;
147        }
148
149        /**
150         * Looks up terminal evidence for one configured shutdown component type.
151         *
152         * @param shutdownComponentType shutdown component type
153         * @return shutdown component result, otherwise empty when that type
154         * was not configured
155         * @throws NullPointerException if {@code shutdownComponentType} is null
156         */
157        @NonNull
158        public Optional<@NonNull ShutdownComponentResult> getShutdownComponentResult(
159                        @NonNull ShutdownComponentType shutdownComponentType) {
160                return Optional.ofNullable(this.shutdownComponentResultsByType.get(
161                                requireNonNull(shutdownComponentType)));
162        }
163
164        /**
165         * @return the cause associated with a failed, timed-out, or canceled startup
166         * attempt, otherwise empty
167         */
168        @NonNull
169        public Optional<@NonNull Throwable> getStartupFailureCause() {
170                return Optional.ofNullable(this.startupFailureCause);
171        }
172
173        /** @return the first premature shutdown component termination, otherwise empty */
174        @NonNull
175        public Optional<@NonNull UnexpectedShutdownComponentTermination>
176        getUnexpectedShutdownComponentTermination() {
177                return Optional.ofNullable(this.unexpectedShutdownComponentTermination);
178        }
179
180        /**
181         * @return {@code true} exactly when the disposition is not
182         * {@link ShutdownDisposition#INCOMPLETE}
183         */
184        @NonNull
185        public Boolean isComplete() {
186                return getShutdownDisposition() != ShutdownDisposition.INCOMPLETE;
187        }
188
189        /**
190         * Returns a bounded, single-line diagnostic with dispositions, component
191         * results, failure presence and residual-component counts when available.
192         * Does not render Throwable instances, residual-summary text or retained
193         * runtime objects. Inspect the typed accessors for full evidence.
194         *
195         * @return compact lifecycle diagnostic; not a serialization format
196         */
197        @Override
198        @NonNull
199        public String toString() {
200                StringBuilder diagnostic = new StringBuilder("ShutdownResult{shutdownDisposition=")
201                                .append(this.shutdownDisposition.name())
202                                .append(", startupDisposition=").append(this.startupDisposition.name())
203                                .append(", shutdownComponentResults=").append(this.shutdownComponentResults)
204                                .append(", startupFailurePresent=").append(this.startupFailureCause != null)
205                                .append(", unexpectedShutdownComponentType=")
206                                .append(this.unexpectedShutdownComponentTermination == null ? "none"
207                                                : this.unexpectedShutdownComponentTermination.getShutdownComponentType().name());
208                this.internalResult.retentionSummary().ifPresent(retention -> diagnostic
209                                .append(", residualComponentCounts=").append(retention.counts()));
210                return diagnostic.append('}').toString();
211        }
212
213        @NonNull
214        InternalShutdownResult internalResult() {
215                return this.internalResult;
216        }
217}