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.net.URI;
025import java.util.ArrayList;
026import java.util.List;
027import java.util.Objects;
028import java.util.Optional;
029
030import static java.util.Objects.requireNonNull;
031import static com.soklet.internal.mcp.protocol.McpApplicationMetadata.requireApplicationMetadata;
032
033/**
034 * Immutable concrete resource descriptor emitted by {@code resources/list}.
035 *
036 * @author <a href="https://www.revetkn.com">Mark Allen</a>
037 */
038@ThreadSafe
039public final class McpResourceDescriptor {
040        @NonNull
041        private final URI uri;
042        @NonNull
043        private final String name;
044        @Nullable
045        private final String title;
046        @Nullable
047        private final String description;
048        @Nullable
049        private final String mimeType;
050        @NonNull
051        private final List<@NonNull McpIcon> icons;
052        @Nullable
053        private final McpContentAnnotations annotations;
054        @Nullable
055        private final Long sizeInBytes;
056        @NonNull
057        private final McpJsonObject metadata;
058
059        /**
060         * Vends a builder primed with the concrete resource URI and name.
061         *
062         * @param uri absolute normalized resource URI in ASCII wire form
063         * @param name nonblank resource name
064         * @return resource-descriptor builder
065         * @throws IllegalArgumentException if the URI is relative, not normalized,
066         * not in ASCII wire form, or the name is blank
067         */
068        @NonNull
069        public static Builder withUriAndName(@NonNull URI uri,
070                        @NonNull String name) {
071                return new Builder(requireExactUri(uri), requireName(name));
072        }
073
074        private McpResourceDescriptor(@NonNull Builder builder) {
075                this.uri = builder.uri;
076                this.name = builder.name;
077                this.title = builder.title;
078                this.description = builder.description;
079                this.mimeType = builder.mimeType;
080                this.icons = List.copyOf(builder.icons);
081                this.annotations = builder.annotations;
082                this.sizeInBytes = builder.sizeInBytes;
083                this.metadata = requireApplicationMetadata(builder.metadata);
084        }
085
086        /** @return absolute normalized resource URI in ASCII wire form */
087        @NonNull
088        public URI getUri() {
089                return this.uri;
090        }
091
092        /** @return nonblank resource name */
093        @NonNull
094        public String getName() {
095                return this.name;
096        }
097
098        /** @return human-readable title, if configured */
099        @NonNull
100        public Optional<@NonNull String> getTitle() {
101                return Optional.ofNullable(this.title);
102        }
103
104        /** @return human-readable description, if configured */
105        @NonNull
106        public Optional<@NonNull String> getDescription() {
107                return Optional.ofNullable(this.description);
108        }
109
110        /** @return resource MIME type, if configured */
111        @NonNull
112        public Optional<@NonNull String> getMimeType() {
113                return Optional.ofNullable(this.mimeType);
114        }
115
116        /** @return immutable icon list in insertion order */
117        @NonNull
118        public List<@NonNull McpIcon> getIcons() {
119                return this.icons;
120        }
121
122        /** @return content annotations, if configured */
123        @NonNull
124        public Optional<@NonNull McpContentAnnotations> getAnnotations() {
125                return Optional.ofNullable(this.annotations);
126        }
127
128        /** @return resource size in bytes, if configured */
129        @NonNull
130        public Optional<@NonNull Long> getSizeInBytes() {
131                return Optional.ofNullable(this.sizeInBytes);
132        }
133
134        /** @return immutable protocol extension metadata */
135        @NonNull
136        public McpJsonObject getMetadata() {
137                return this.metadata;
138        }
139
140        /** @return whether every descriptor property is structurally equal */
141        @Override
142        public boolean equals(@Nullable Object other) {
143                if (this == other)
144                        return true;
145                if (!(other instanceof McpResourceDescriptor descriptor))
146                        return false;
147                return this.uri.equals(descriptor.uri)
148                                && this.name.equals(descriptor.name)
149                                && Objects.equals(this.title, descriptor.title)
150                                && Objects.equals(this.description, descriptor.description)
151                                && Objects.equals(this.mimeType, descriptor.mimeType)
152                                && this.icons.equals(descriptor.icons)
153                                && Objects.equals(this.annotations, descriptor.annotations)
154                                && Objects.equals(this.sizeInBytes, descriptor.sizeInBytes)
155                                && this.metadata.equals(descriptor.metadata);
156        }
157
158        /** @return structural descriptor hash code */
159        @Override
160        public int hashCode() {
161                return Objects.hash(this.uri, this.name, this.title, this.description,
162                                this.mimeType, this.icons, this.annotations, this.sizeInBytes,
163                                this.metadata);
164        }
165
166        @NonNull
167        private static URI requireExactUri(@NonNull URI uri) {
168                return McpResourceValueSupport.requireAbsoluteNormalizedUri(uri);
169        }
170
171        @NonNull
172        private static String requireName(@NonNull String name) {
173                McpContentValueSupport.requireWellFormedString(name);
174                if (name.isBlank())
175                        throw new IllegalArgumentException(
176                                        "MCP resource names must not be blank.");
177                return name;
178        }
179
180        @NonNull
181        private static String requireMimeType(@NonNull String mimeType) {
182                return McpContentValueSupport.requireMimeType(mimeType);
183        }
184
185        /**
186         * Mutable builder for an immutable resource descriptor.
187         *
188         * @author <a href="https://www.revetkn.com">Mark Allen</a>
189         */
190        @NotThreadSafe
191        public static final class Builder {
192                @NonNull
193                private final URI uri;
194                @NonNull
195                private final String name;
196                @Nullable
197                private String title;
198                @Nullable
199                private String description;
200                @Nullable
201                private String mimeType;
202                @NonNull
203                private final List<@NonNull McpIcon> icons = new ArrayList<>();
204                @Nullable
205                private McpContentAnnotations annotations;
206                @Nullable
207                private Long sizeInBytes;
208                @NonNull
209                private McpJsonObject metadata = McpJsonObject.emptyInstance();
210
211                private Builder(@NonNull URI uri, @NonNull String name) {
212                        this.uri = requireNonNull(uri);
213                        this.name = McpContentValueSupport.requireWellFormedString(name);
214                }
215
216                /** @param title human-readable title
217                 * @return this builder */
218                @NonNull
219                public Builder title(@NonNull String title) {
220                        this.title = McpContentValueSupport.requireWellFormedString(title);
221                        return this;
222                }
223
224                /** @param description human-readable description
225                 * @return this builder */
226                @NonNull
227                public Builder description(@NonNull String description) {
228                        this.description = McpContentValueSupport.requireWellFormedString(description);
229                        return this;
230                }
231
232                /**
233                 * Sets the resource MIME type.
234                 *
235                 * @param mimeType syntactically valid media type
236                 * @return this builder
237                 * @throws IllegalArgumentException if {@code mimeType} is not a syntactically valid media type
238                 */
239                @NonNull
240                public Builder mimeType(@NonNull String mimeType) {
241                        this.mimeType = requireMimeType(mimeType);
242                        return this;
243                }
244
245                /**
246                 * Appends one icon descriptor.
247                 *
248                 * @param icon icon descriptor
249                 * @return this builder
250                 */
251                @NonNull
252                public Builder addIcon(@NonNull McpIcon icon) {
253                        this.icons.add(requireNonNull(icon));
254                        return this;
255                }
256
257                /** @param annotations content annotations
258                 * @return this builder */
259                @NonNull
260                public Builder annotations(@NonNull McpContentAnnotations annotations) {
261                        this.annotations = requireNonNull(annotations);
262                        return this;
263                }
264
265                /**
266                 * Sets the resource size in bytes.
267                 *
268                 * @param sizeInBytes nonnegative byte count
269                 * @return this builder
270                 * @throws NullPointerException if {@code sizeInBytes} is null
271                 * @throws IllegalArgumentException if {@code sizeInBytes} is negative
272                 */
273                @NonNull
274                public Builder sizeInBytes(@NonNull Long sizeInBytes) {
275                        requireNonNull(sizeInBytes);
276                        if (sizeInBytes < 0)
277                                throw new IllegalArgumentException(
278                                                "MCP resource sizes must not be negative.");
279                        this.sizeInBytes = sizeInBytes;
280                        return this;
281                }
282
283                /** @param metadata protocol extension metadata
284                 * @return this builder */
285                @NonNull
286                public Builder metadata(@NonNull McpJsonObject metadata) {
287                        this.metadata = requireNonNull(metadata);
288                        return this;
289                }
290
291                /** @return immutable resource descriptor */
292                @NonNull
293                public McpResourceDescriptor build() {
294                        return new McpResourceDescriptor(this);
295                }
296        }
297}