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.ThreadSafe; 023 024import static java.util.Objects.requireNonNull; 025 026/** 027 * Immutable, non-secret application identity for one localization catalog 028 * snapshot. 029 * <p> 030 * A revision supports deployment comparison and cache/invalidation 031 * bookkeeping. It is not an MCP wire value, metric label, log field, 032 * historical-catalog request, or distributed-session identifier. 033 * 034 * @author <a href="https://www.revetkn.com">Mark Allen</a> 035 */ 036@ThreadSafe 037public final class McpLocalizationRevision { 038 private static final int MAXIMUM_VALUE_BYTES = 128; 039 @NonNull 040 private final String value; 041 042 /** 043 * Creates a revision from its bounded application value. 044 * 045 * @param value 1-128 bytes of visible ASCII 046 * @return immutable revision 047 * @throws NullPointerException if {@code value} is null 048 * @throws IllegalArgumentException if {@code value} is empty, longer than 049 * 128 bytes, or contains a character outside visible ASCII 050 */ 051 @NonNull 052 public static McpLocalizationRevision fromValue(@NonNull String value) { 053 return new McpLocalizationRevision(value); 054 } 055 056 private McpLocalizationRevision(@NonNull String value) { 057 this.value = requireNonNull(value); 058 if (value.isEmpty() || value.length() > MAXIMUM_VALUE_BYTES) 059 throw new IllegalArgumentException( 060 "MCP localization revisions must contain 1 through 128 visible ASCII bytes."); 061 for (int index = 0; index < value.length(); ++index) { 062 char character = value.charAt(index); 063 if (character < 0x21 || character > 0x7E) 064 throw new IllegalArgumentException( 065 "MCP localization revisions must contain 1 through 128 visible ASCII bytes."); 066 } 067 } 068 069 /** @return application-defined revision value */ 070 @NonNull 071 public String getValue() { 072 return this.value; 073 } 074 075 /** @return whether this value has the same application revision */ 076 @Override 077 public boolean equals(@Nullable Object other) { 078 if (this == other) 079 return true; 080 if (!(other instanceof McpLocalizationRevision revision)) 081 return false; 082 return this.value.equals(revision.value); 083 } 084 085 /** @return value-based hash code */ 086 @Override 087 public int hashCode() { 088 return this.value.hashCode(); 089 } 090 091 /** @return redacted diagnostic rendering */ 092 @Override 093 @NonNull 094 public String toString() { 095 return "McpLocalizationRevision{value=<redacted>}"; 096 } 097}