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.Objects;
026import java.util.Optional;
027
028import static com.soklet.internal.mcp.protocol.McpApplicationMetadata.requireApplicationMetadata;
029import static java.util.Objects.requireNonNull;
030
031/**
032 * Immutable UTF-16 Java text contents for one MCP resource.
033 *
034 * <p>Construction rejects unpaired UTF-16 surrogates with
035 * {@link IllegalArgumentException}. Well-formed text is retained exactly,
036 * without normalization or replacement. Text must fit the independent JSON
037 * string/token and complete-response limits described by {@link McpJsonString};
038 * construction does not prevalidate its eventual serialized size.</p>
039 *
040 * @author <a href="https://www.revetkn.com">Mark Allen</a>
041 */
042@ThreadSafe
043public final class McpTextResourceContents implements McpResourceContents {
044        @NonNull
045        private final URI uri;
046        @NonNull
047        private final String text;
048        @Nullable
049        private final String mimeType;
050        @Nullable
051        private final McpAppResourceMetadata appResourceMetadata;
052        @NonNull
053        private final McpJsonObject metadata;
054
055        /**
056         * Vends a builder primed with the resource URI and text.
057         *
058         * @param uri absolute normalized resource URI in ASCII wire form
059         * @param text resource text
060         * @return resource-content builder
061         * @throws IllegalArgumentException if the URI is relative, not normalized,
062         * or not in ASCII wire form, or the text contains an unpaired surrogate
063         */
064        @NonNull
065        public static Builder withUriAndText(@NonNull URI uri,
066                        @NonNull String text) {
067                return new Builder(uri, text);
068        }
069
070        private McpTextResourceContents(@NonNull Builder builder) {
071                this.uri = builder.uri;
072                this.text = builder.text;
073                this.mimeType = builder.mimeType;
074                this.appResourceMetadata = builder.appResourceMetadata;
075                this.metadata = requireApplicationMetadata(builder.metadata);
076        }
077
078        /**
079         * Returns the URI that identifies this resource.
080         *
081         * @return absolute normalized resource URI in ASCII wire form
082         */
083        @Override
084        @NonNull
085        public URI getUri() {
086                return this.uri;
087        }
088
089        /** @return resource text */
090        @NonNull
091        public String getText() {
092                return this.text;
093        }
094
095        /**
096         * Returns the resource MIME type, if one was supplied.
097         *
098         * @return resource MIME type, if available
099         */
100        @Override
101        @NonNull
102        public Optional<@NonNull String> getMimeType() {
103                return Optional.ofNullable(this.mimeType);
104        }
105
106        /**
107         * Returns separately configured Apps policy and presentation hints.
108         *
109         * @return typed Apps resource metadata, if supplied
110         */
111        @Override
112        @NonNull
113        public Optional<@NonNull McpAppResourceMetadata> getAppResourceMetadata() {
114                return Optional.ofNullable(this.appResourceMetadata);
115        }
116
117        /**
118         * Returns protocol extension metadata associated with this resource.
119         *
120         * @return immutable metadata object
121         */
122        @Override
123        @NonNull
124        public McpJsonObject getMetadata() {
125                return this.metadata;
126        }
127
128        /** @return whether every resource-content property is structurally equal */
129        @Override
130        public boolean equals(@Nullable Object other) {
131                if (this == other)
132                        return true;
133                if (!(other instanceof McpTextResourceContents contents))
134                        return false;
135                return this.uri.equals(contents.uri)
136                                && this.text.equals(contents.text)
137                                && Objects.equals(this.mimeType, contents.mimeType)
138                                && Objects.equals(this.appResourceMetadata, contents.appResourceMetadata)
139                                && this.metadata.equals(contents.metadata);
140        }
141
142        /** @return structural resource-content hash code */
143        @Override
144        public int hashCode() {
145                return Objects.hash(McpTextResourceContents.class, this.uri, this.text,
146                                this.mimeType, this.appResourceMetadata, this.metadata);
147        }
148
149        /**
150         * Mutable builder for immutable {@link McpTextResourceContents}.
151         *
152         * @author <a href="https://www.revetkn.com">Mark Allen</a>
153         */
154        @NotThreadSafe
155        public static final class Builder {
156                @NonNull
157                private final URI uri;
158                @NonNull
159                private final String text;
160                @Nullable
161                private String mimeType;
162                @Nullable
163                private McpAppResourceMetadata appResourceMetadata;
164                @NonNull
165                private McpJsonObject metadata = McpJsonObject.emptyInstance();
166
167                private Builder(@NonNull URI uri, @NonNull String text) {
168                        this.uri = McpResourceValueSupport
169                                        .requireAbsoluteNormalizedUri(uri);
170                        this.text = McpContentValueSupport.requireWellFormedString(text);
171                }
172
173                /**
174                 * Sets the resource MIME type.
175                 *
176                 * @param mimeType syntactically valid media type
177                 * @return this builder
178                 * @throws IllegalArgumentException if {@code mimeType} is not a syntactically valid media type
179                 */
180                @NonNull
181                public Builder mimeType(@NonNull String mimeType) {
182                        this.mimeType = McpContentValueSupport.requireMimeType(mimeType);
183                        return this;
184                }
185
186                /**
187                 * Sets Apps policy and presentation hints for a resource read.
188                 *
189                 * <p>The built contents must use a normalized ASCII {@code ui://} URI
190                 * and the {@code text/html;profile=mcp-app} MIME type. Typed Apps
191                 * metadata is kept separate from raw extension metadata; declaring
192                 * typed-owned fields in both is rejected regardless of setter order.
193                 *
194                 * @param appResourceMetadata immutable Apps resource metadata
195                 * @return this builder
196                 * @throws IllegalArgumentException if raw metadata declares a typed-owned field
197                 */
198                @NonNull
199                public Builder appResourceMetadata(@NonNull McpAppResourceMetadata appResourceMetadata) {
200                        McpAppMetadataSupport.effectiveResourceMetadata(this.metadata,
201                                        requireNonNull(appResourceMetadata));
202                        this.appResourceMetadata = appResourceMetadata;
203                        return this;
204                }
205
206                /**
207                 * Sets protocol extension metadata.
208                 *
209                 * @param metadata immutable metadata object
210                 * @return this builder
211                 */
212                @NonNull
213                public Builder metadata(@NonNull McpJsonObject metadata) {
214                        McpAppMetadataSupport.effectiveResourceMetadata(requireNonNull(metadata),
215                                        this.appResourceMetadata);
216                        this.metadata = metadata;
217                        return this;
218                }
219
220                /** @return immutable text resource contents */
221                @NonNull
222                public McpTextResourceContents build() {
223                        McpAppMetadataSupport.requireResourceContents(this.uri, this.mimeType,
224                                        this.metadata, this.appResourceMetadata);
225                        return new McpTextResourceContents(this);
226                }
227        }
228}