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.List;
026import java.util.Objects;
027import java.util.Optional;
028
029import static com.soklet.McpContentValueSupport.resourceContentsEqual;
030import static com.soklet.McpContentValueSupport.resourceContentsHashCode;
031import static java.util.Objects.requireNonNull;
032
033/**
034 * Immutable output of one completed MCP resource-read request.
035 *
036 * <p>The matching resource registration owns the fixed cache scope and
037 * default time to live. An output may override only the time to live for this
038 * response. A localization-enabled server conservatively publishes a private,
039 * zero-time-to-live policy regardless of an output override.
040 *
041 * @author <a href="https://www.revetkn.com">Mark Allen</a>
042 */
043@ThreadSafe
044public final class McpResourceOutput implements McpCompletePayload {
045        @NonNull
046        private final List<@NonNull McpResourceContents> contents;
047        @Nullable
048        private final Duration cacheTimeToLiveOverride;
049
050        /**
051         * Vends a builder containing its required first resource value.
052         *
053         * @param resourceContents resource contents
054         * @return resource-output builder
055         * @throws NullPointerException if {@code resourceContents} is null
056         */
057        @NonNull
058        public static Builder withContent(
059                        @NonNull McpResourceContents resourceContents) {
060                return withContents(List.of(requireNonNull(resourceContents)));
061        }
062
063        /**
064         * Creates output containing exactly one resource value.
065         *
066         * @param resourceContents resource contents
067         * @return immutable resource output
068         * @throws NullPointerException if {@code resourceContents} is null
069         */
070        @NonNull
071        public static McpResourceOutput fromContent(
072                        @NonNull McpResourceContents resourceContents) {
073                return withContent(resourceContents).build();
074        }
075
076        /**
077         * Vends a builder containing an ordered snapshot of resource contents.
078         *
079         * @param resourceContents nonempty resource contents in response order
080         * @return resource-output builder
081         * @throws NullPointerException if the list or any item is null
082         * @throws IllegalArgumentException if the list is empty
083         */
084        @NonNull
085        public static Builder withContents(
086                        @NonNull List<? extends @NonNull McpResourceContents> resourceContents) {
087                return new Builder(resourceContents);
088        }
089
090        /**
091         * Creates output from an ordered snapshot of resource contents.
092         *
093         * @param resourceContents nonempty resource contents in response order
094         * @return immutable resource output
095         * @throws NullPointerException if the list or any item is null
096         * @throws IllegalArgumentException if the list is empty
097         */
098        @NonNull
099        public static McpResourceOutput fromContents(
100                        @NonNull List<? extends @NonNull McpResourceContents> resourceContents) {
101                return withContents(resourceContents).build();
102        }
103
104        private McpResourceOutput(@NonNull Builder builder) {
105                this.contents = requireContents(builder.contents);
106                this.cacheTimeToLiveOverride = builder.cacheTimeToLiveOverride;
107        }
108
109        @NonNull
110        private static List<@NonNull McpResourceContents> requireContents(
111                        @NonNull List<? extends @NonNull McpResourceContents> resourceContents) {
112                List<McpResourceContents> copiedContents =
113                                List.copyOf(requireNonNull(resourceContents));
114                if (copiedContents.isEmpty())
115                        throw new IllegalArgumentException(
116                                        "At least one MCP resource-content value is required.");
117                return copiedContents;
118        }
119
120        /** @return immutable resource contents in insertion order */
121        @NonNull
122        public List<@NonNull McpResourceContents> getContents() {
123                return this.contents;
124        }
125
126        /**
127         * Returns this response's time-to-live override.
128         *
129         * @return override, or empty to use the registration default
130         */
131        @NonNull
132        public Optional<@NonNull Duration> getCacheTimeToLiveOverride() {
133                return Optional.ofNullable(this.cacheTimeToLiveOverride);
134        }
135
136        /** @return whether contents and cache override are structurally equal */
137        @Override
138        public boolean equals(@Nullable Object other) {
139                if (this == other)
140                        return true;
141                if (!(other instanceof McpResourceOutput output))
142                        return false;
143                return contentsEqual(this.contents, output.contents)
144                                && Objects.equals(this.cacheTimeToLiveOverride,
145                                        output.cacheTimeToLiveOverride);
146        }
147
148        /** @return structural resource-output hash code */
149        @Override
150        public int hashCode() {
151                int contentsHashCode = 1;
152                for (McpResourceContents resourceContents : this.contents)
153                        contentsHashCode = 31 * contentsHashCode
154                                        + resourceContentsHashCode(resourceContents);
155                return Objects.hash(contentsHashCode, this.cacheTimeToLiveOverride);
156        }
157
158        private static boolean contentsEqual(
159                        @NonNull List<@NonNull McpResourceContents> first,
160                        @NonNull List<@NonNull McpResourceContents> second) {
161                if (first.size() != second.size())
162                        return false;
163                for (int index = 0; index < first.size(); ++index) {
164                        if (!resourceContentsEqual(first.get(index), second.get(index)))
165                                return false;
166                }
167                return true;
168        }
169
170        /**
171         * Mutable builder for immutable {@link McpResourceOutput}.
172         *
173         * @author <a href="https://www.revetkn.com">Mark Allen</a>
174         */
175        @NotThreadSafe
176        public static final class Builder {
177                @NonNull
178                private List<@NonNull McpResourceContents> contents;
179                @Nullable
180                private Duration cacheTimeToLiveOverride;
181
182                private Builder(
183                                @NonNull List<? extends @NonNull McpResourceContents> resourceContents) {
184                        this.contents = requireContents(resourceContents);
185                }
186
187                /**
188                 * Replaces all resource contents with an ordered immutable snapshot.
189                 * A failed replacement leaves the previous contents unchanged.
190                 *
191                 * @param resourceContents nonempty resource contents in response order
192                 * @return this builder
193                 * @throws NullPointerException if the list or any item is null
194                 * @throws IllegalArgumentException if the list is empty
195                 */
196                @NonNull
197                public Builder contents(
198                                @NonNull List<? extends @NonNull McpResourceContents> resourceContents) {
199                        this.contents = requireContents(resourceContents);
200                        return this;
201                }
202
203                /**
204                 * Overrides only the registration's default time to live.
205                 *
206                 * @param cacheTimeToLiveOverride nonnegative whole-millisecond duration
207                 * @return this builder
208                 * @throws IllegalArgumentException if the duration is negative or has
209                 * sub-millisecond precision
210                 */
211                @NonNull
212                public Builder cacheTimeToLiveOverride(
213                                @NonNull Duration cacheTimeToLiveOverride) {
214                        this.cacheTimeToLiveOverride =
215                                        McpCachePolicy.requireTimeToLive(cacheTimeToLiveOverride);
216                        return this;
217                }
218
219                /** @return immutable resource output */
220                @NonNull
221                public McpResourceOutput build() {
222                        return new McpResourceOutput(this);
223                }
224        }
225}