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}