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.util.Locale;
025import java.util.Optional;
026
027import static java.util.Objects.requireNonNull;
028
029/**
030 * Immutable, node-local localization context for one admitted
031 * localization-capable MCP operation.
032 * <p>
033 * This is not a distributed session. It has no ID, is never serialized or
034 * recovered on another node, and requires no routing affinity. The context
035 * captures one locale and one immutable translation snapshot. A later request
036 * on another node creates a new context from portable request or continuation
037 * facts.
038 * <p>
039 * The context is not closeable and must not own resources whose correctness
040 * depends on an exact close callback. Its lookup must be safe for concurrent
041 * calls. Locale and revision remain equal for its full lifetime; equal
042 * localizable-text inputs must produce equal results independently of lookup
043 * order, including under concurrent calls.
044 * <p>
045 * This is a borrowed invocation-scoped feature. An application handler or
046 * interceptor must not retain it or its feature carrier after invocation
047 * termination. The lookup should capture only the minimum immutable
048 * snapshot and must not capture the request or application object graph. Every
049 * localization lookup must use that already-loaded snapshot and perform
050 * bounded, in-memory, nonblocking work without remote TMS/network I/O, lazy
051 * unbounded loading, or unbounded-executor dispatch.
052 *
053 * @author <a href="https://www.revetkn.com">Mark Allen</a>
054 */
055@ThreadSafe
056public final class McpLocalizationContext {
057        @NonNull
058        private final Locale locale;
059        @Nullable
060        private final McpLocalizationRevision revision;
061        @NonNull
062        private final McpLocalizationLookup localizationLookup;
063
064        /**
065         * Vends a context builder primed with its required construction values.
066         *
067         * @param locale canonical selected catalog locale
068         * @param localizationLookup request-local immutable-snapshot lookup
069         * @return localization-context builder
070         * @throws NullPointerException if an argument is null
071         * @throws IllegalArgumentException if {@code locale} is not a canonical,
072         * non-root BCP 47 locale of at most 255 ASCII bytes
073         */
074        @NonNull
075        public static Builder withLocale(@NonNull Locale locale,
076                        @NonNull McpLocalizationLookup localizationLookup) {
077                return new Builder(locale, localizationLookup);
078        }
079
080        private McpLocalizationContext(@NonNull Builder builder) {
081                this.locale = builder.locale;
082                this.revision = builder.revision;
083                this.localizationLookup = builder.localizationLookup;
084        }
085
086        /** @return canonical selected catalog locale */
087        @NonNull
088        public Locale getLocale() {
089                return this.locale;
090        }
091
092        /**
093         * Returns optional non-secret identity for the captured catalog snapshot.
094         * Revisions are not MCP wire values or distributed-session identifiers.
095         *
096         * @return immutable snapshot revision, or empty
097         */
098        @NonNull
099        public Optional<@NonNull McpLocalizationRevision> getRevision() {
100                return Optional.ofNullable(this.revision);
101        }
102
103        /**
104         * Localizes one framework-owned source-text field against this context's
105         * captured snapshot. The lookup must be bounded, in-memory, and nonblocking;
106         * it must not perform remote I/O or unbounded loading.
107         * <p>
108         * The localization lookup reports an unexpected failure with
109         * {@link McpLocalizationResult#failure()}; it must not throw to report an
110         * operational lookup failure. If an unchecked contract violation nevertheless
111         * escapes while Soklet invokes this callback, Soklet treats it as untrusted
112         * localization data and does not forward it through framework-owned
113         * lifecycle, simulation, logging, response-throwable, or cause surfaces.
114         * Direct application invocation is application-owned and has normal
115         * application failure semantics.
116         *
117         * @param text structured coordinate and canonical source text
118         * @return non-null localization result
119         * @throws NullPointerException if {@code text} is null or the configured
120         * lookup returns null
121         */
122        @NonNull
123        public McpLocalizationResult localize(@NonNull McpLocalizableText text) {
124                return requireNonNull(
125                                this.localizationLookup.localize(requireNonNull(text, "text")),
126                                "The MCP localization lookup returned null.");
127        }
128
129        /** @return redacted diagnostic rendering */
130        @Override
131        @NonNull
132        public String toString() {
133                return "McpLocalizationContext{locale=<redacted>, "
134                                + "revision=<redacted>, localizationLookup=<redacted>}";
135        }
136
137        /**
138         * Single-threaded builder for an immutable request-local context.
139         *
140         * @author <a href="https://www.revetkn.com">Mark Allen</a>
141         */
142        @NotThreadSafe
143        public static final class Builder {
144                @NonNull
145                private final Locale locale;
146                @Nullable
147                private McpLocalizationRevision revision;
148                @NonNull
149                private final McpLocalizationLookup localizationLookup;
150
151                private Builder(@NonNull Locale locale,
152                                @NonNull McpLocalizationLookup localizationLookup) {
153                        this.locale = McpLocaleSupport.requireCanonicalCatalogLocale(
154                                        locale, "locale");
155                        this.localizationLookup = requireNonNull(localizationLookup);
156                }
157
158                /**
159                 * Sets the non-secret identity for the captured catalog snapshot.
160                 *
161                 * @param revision immutable snapshot revision, or null to clear it
162                 * @return this builder
163                 */
164                @NonNull
165                public Builder revision(@Nullable McpLocalizationRevision revision) {
166                        this.revision = revision;
167                        return this;
168                }
169
170                /**
171                 * Builds an immutable localization context.
172                 *
173                 * @return immutable request-local context
174                 */
175                @NonNull
176                public McpLocalizationContext build() {
177                        return new McpLocalizationContext(this);
178                }
179        }
180}