001/*
002 * Copyright 2017-2022 Product Mog LLC, 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.lokalized;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.Immutable;
023import javax.annotation.concurrent.NotThreadSafe;
024import java.util.Objects;
025
026import static com.lokalized.Diagnostics.format;
027
028/**
029 * Immutable safety limits for translation construction and evaluation.
030 * <p>
031 * The defaults are deliberately generous enough for normal localized strings while bounding work that is proportional to
032 * untrusted localized strings data or runtime values. Builders may customize a limit through the library's hard ceiling.
033 * Localized strings loaders validate expressions against hard ceilings because an application's runtime policy is not yet
034 * available; {@link Strings} construction then enforces its configured limits. A single instance is safe to share
035 * between {@link Strings} instances and threads.
036 * <p>
037 * Caller-supplied {@link CharSequence} values are length-checked before Lokalized scans, copies, bidi-isolates, or
038 * materializes them for phonetic resolution. The interpolated-output limit also bounds one phonetic input. For other
039 * placeholder object types, Lokalized must invoke application-defined {@link Object#toString()} code before it can
040 * measure the result; these limits bound Lokalized's subsequent work but cannot bound work performed inside that
041 * application method. Use a pre-bounded {@code String} or {@code CharSequence} when runtime values originate from
042 * untrusted input.
043 * <p>
044 * Expression source, token, and nesting limits apply equally to whole-message alternative predicates and
045 * {@link LocalizedString.ExpressionAlternative expression-fragment predicates}. Generated-placeholder depth and
046 * cumulative expansion limits are shared by {@link LocalizedString.LanguageFormTranslation language-form fragments}
047 * and {@link LocalizedString.ExpressionTranslation expression-selected fragments}, including dependencies that cross
048 * between the two kinds.
049 *
050 * @author <a href="https://revetkn.com">Mark Allen</a>
051 * @since 3.0.0
052 */
053@Immutable
054public final class TranslationRuntimeLimits {
055        /** Default limit for decimal precision: 1,024. */
056        @NonNull public static final Integer DEFAULT_MAXIMUM_NUMBER_PRECISION = 1_024;
057        /** Default limit for the absolute value of a decimal scale: 1,024. */
058        @NonNull public static final Integer DEFAULT_MAXIMUM_ABSOLUTE_NUMBER_SCALE = 1_024;
059        /** Default limit for explicitly visible decimal places: 1,024. */
060        @NonNull public static final Integer DEFAULT_MAXIMUM_VISIBLE_DECIMAL_PLACES = 1_024;
061        /** Default limit for compact-decimal exponents: 64. */
062        @NonNull public static final Integer DEFAULT_MAXIMUM_COMPACT_EXPONENT = 64;
063        /** Default limit for characters in one whole-message or generated-fragment expression: 2,048. */
064        @NonNull public static final Integer DEFAULT_MAXIMUM_EXPRESSION_CHARACTERS = 2_048;
065        /** Default limit for tokens in one whole-message or generated-fragment expression: 256. */
066        @NonNull public static final Integer DEFAULT_MAXIMUM_EXPRESSION_TOKENS = 256;
067        /** Default limit for nested groups in one whole-message or generated-fragment expression: 32. */
068        @NonNull public static final Integer DEFAULT_MAXIMUM_EXPRESSION_NESTING_DEPTH = 32;
069        /** Default limit for nested generated placeholders across both definition kinds: 32. */
070        @NonNull public static final Integer DEFAULT_MAXIMUM_GENERATED_PLACEHOLDER_DEPTH = 32;
071        /** Default limit for one interpolated message, generated fragment, or phonetic input: 262,144 UTF-16 code units. */
072        @NonNull public static final Integer DEFAULT_MAXIMUM_INTERPOLATED_OUTPUT_CHARACTERS = 256 * 1_024;
073        /**
074         * Default cumulative expansion limit across both generated-placeholder kinds: 1,048,576 UTF-16 code units per
075         * locale fallback attempt.
076         */
077        @NonNull public static final Integer DEFAULT_MAXIMUM_GENERATED_EXPANSION_CHARACTERS = 1_024 * 1_024;
078
079        /** Hard ceiling for decimal precision: 4,096. */
080        @NonNull public static final Integer MAXIMUM_NUMBER_PRECISION = 4_096;
081        /** Hard ceiling for the absolute value of a decimal scale: 4,096. */
082        @NonNull public static final Integer MAXIMUM_ABSOLUTE_NUMBER_SCALE = 4_096;
083        /** Hard ceiling for explicitly visible decimal places: 4,096. */
084        @NonNull public static final Integer MAXIMUM_VISIBLE_DECIMAL_PLACES = 4_096;
085        /** Hard ceiling for compact-decimal exponents: 4,096. */
086        @NonNull public static final Integer MAXIMUM_COMPACT_EXPONENT = 4_096;
087        /** Hard ceiling for characters in one whole-message or generated-fragment expression: 4,096. */
088        @NonNull public static final Integer MAXIMUM_EXPRESSION_CHARACTERS = 4_096;
089        /** Hard ceiling for tokens in one whole-message or generated-fragment expression: 512. */
090        @NonNull public static final Integer MAXIMUM_EXPRESSION_TOKENS = 512;
091        /** Hard ceiling for nested groups in one whole-message or generated-fragment expression: 64. */
092        @NonNull public static final Integer MAXIMUM_EXPRESSION_NESTING_DEPTH = 64;
093        /** Hard ceiling for nested generated placeholders across both definition kinds: 64. */
094        @NonNull public static final Integer MAXIMUM_GENERATED_PLACEHOLDER_DEPTH = 64;
095        /** Hard ceiling for one interpolated message, generated fragment, or phonetic input: 1,048,576 UTF-16 code units. */
096        @NonNull public static final Integer MAXIMUM_INTERPOLATED_OUTPUT_CHARACTERS = 1_024 * 1_024;
097        /**
098         * Hard ceiling for cumulative expansion across both generated-placeholder kinds: 8,388,608 UTF-16 code units per
099         * locale fallback attempt.
100         */
101        @NonNull public static final Integer MAXIMUM_GENERATED_EXPANSION_CHARACTERS = 8 * 1_024 * 1_024;
102
103        @NonNull
104        private static final TranslationRuntimeLimits DEFAULTS = new Builder().build();
105        @NonNull
106        private static final TranslationRuntimeLimits HARD_CEILINGS = new Builder()
107                        .maximumNumberPrecision(MAXIMUM_NUMBER_PRECISION)
108                        .maximumAbsoluteNumberScale(MAXIMUM_ABSOLUTE_NUMBER_SCALE)
109                        .maximumVisibleDecimalPlaces(MAXIMUM_VISIBLE_DECIMAL_PLACES)
110                        .maximumCompactExponent(MAXIMUM_COMPACT_EXPONENT)
111                        .maximumExpressionCharacters(MAXIMUM_EXPRESSION_CHARACTERS)
112                        .maximumExpressionTokens(MAXIMUM_EXPRESSION_TOKENS)
113                        .maximumExpressionNestingDepth(MAXIMUM_EXPRESSION_NESTING_DEPTH)
114                        .maximumGeneratedPlaceholderDepth(MAXIMUM_GENERATED_PLACEHOLDER_DEPTH)
115                        .maximumInterpolatedOutputCharacters(MAXIMUM_INTERPOLATED_OUTPUT_CHARACTERS)
116                        .maximumGeneratedExpansionCharacters(MAXIMUM_GENERATED_EXPANSION_CHARACTERS)
117                        .build();
118
119        @NonNull private final Integer maximumNumberPrecision;
120        @NonNull private final Integer maximumAbsoluteNumberScale;
121        @NonNull private final Integer maximumVisibleDecimalPlaces;
122        @NonNull private final Integer maximumCompactExponent;
123        @NonNull private final Integer maximumExpressionCharacters;
124        @NonNull private final Integer maximumExpressionTokens;
125        @NonNull private final Integer maximumExpressionNestingDepth;
126        @NonNull private final Integer maximumGeneratedPlaceholderDepth;
127        @NonNull private final Integer maximumInterpolatedOutputCharacters;
128        @NonNull private final Integer maximumGeneratedExpansionCharacters;
129
130        private TranslationRuntimeLimits(@NonNull Builder builder) {
131                this.maximumNumberPrecision = builder.maximumNumberPrecision;
132                this.maximumAbsoluteNumberScale = builder.maximumAbsoluteNumberScale;
133                this.maximumVisibleDecimalPlaces = builder.maximumVisibleDecimalPlaces;
134                this.maximumCompactExponent = builder.maximumCompactExponent;
135                this.maximumExpressionCharacters = builder.maximumExpressionCharacters;
136                this.maximumExpressionTokens = builder.maximumExpressionTokens;
137                this.maximumExpressionNestingDepth = builder.maximumExpressionNestingDepth;
138                this.maximumGeneratedPlaceholderDepth = builder.maximumGeneratedPlaceholderDepth;
139                this.maximumInterpolatedOutputCharacters = builder.maximumInterpolatedOutputCharacters;
140                this.maximumGeneratedExpansionCharacters = builder.maximumGeneratedExpansionCharacters;
141        }
142
143        /**
144         * Gets the library-default limits.
145         *
146         * @return the library-default limits, not null
147         */
148        @NonNull public static TranslationRuntimeLimits defaults() { return DEFAULTS; }
149
150        /**
151         * Gets the library hard ceilings.
152         *
153         * @return the library hard ceilings, not null
154         */
155        @NonNull static TranslationRuntimeLimits hardCeilings() { return HARD_CEILINGS; }
156
157        /**
158         * Creates a builder initialized to the library defaults.
159         *
160         * @return a builder initialized to the library defaults, not null
161         */
162        @NonNull public static Builder builder() { return new Builder(); }
163
164        /**
165         * Creates a builder initialized from this instance.
166         *
167         * @return a builder initialized from this instance, not null
168         */
169        @NonNull public Builder toBuilder() { return new Builder(this); }
170
171        /**
172         * Gets the maximum decimal precision.
173         *
174         * @return maximum decimal precision, not null
175         */
176        @NonNull public Integer getMaximumNumberPrecision() { return maximumNumberPrecision; }
177        /**
178         * Gets the maximum absolute decimal scale.
179         *
180         * @return maximum absolute decimal scale, not null
181         */
182        @NonNull public Integer getMaximumAbsoluteNumberScale() { return maximumAbsoluteNumberScale; }
183        /**
184         * Gets the maximum number of explicitly visible decimal places.
185         *
186         * @return maximum explicitly visible decimal places, not null
187         */
188        @NonNull public Integer getMaximumVisibleDecimalPlaces() { return maximumVisibleDecimalPlaces; }
189        /**
190         * Gets the maximum compact-decimal exponent.
191         *
192         * @return maximum compact-decimal exponent, not null
193         */
194        @NonNull public Integer getMaximumCompactExponent() { return maximumCompactExponent; }
195        /**
196         * Gets the maximum characters in one whole-message or generated-fragment expression.
197         *
198         * @return maximum characters in one whole-message or generated-fragment expression, not null
199         */
200        @NonNull public Integer getMaximumExpressionCharacters() { return maximumExpressionCharacters; }
201        /**
202         * Gets the maximum tokens in one whole-message or generated-fragment expression.
203         *
204         * @return maximum tokens in one whole-message or generated-fragment expression, not null
205         */
206        @NonNull public Integer getMaximumExpressionTokens() { return maximumExpressionTokens; }
207        /**
208         * Gets the maximum nested groups in one whole-message or generated-fragment expression.
209         *
210         * @return maximum nested groups in one whole-message or generated-fragment expression, not null
211         */
212        @NonNull public Integer getMaximumExpressionNestingDepth() { return maximumExpressionNestingDepth; }
213        /**
214         * Gets the maximum generated-placeholder nesting depth across both definition kinds.
215         *
216         * @return maximum generated-placeholder nesting depth across both definition kinds, not null
217         */
218        @NonNull public Integer getMaximumGeneratedPlaceholderDepth() { return maximumGeneratedPlaceholderDepth; }
219        /**
220         * Gets the maximum UTF-16 code units in one interpolated message, generated fragment, or caller-supplied phonetic
221         * input.
222         *
223         * @return maximum UTF-16 code units in one interpolated message, generated fragment, or phonetic input, not null
224         */
225        @NonNull public Integer getMaximumInterpolatedOutputCharacters() { return maximumInterpolatedOutputCharacters; }
226        /**
227         * Gets the maximum cumulative expansion across both generated-placeholder kinds per locale fallback attempt.
228         *
229         * @return maximum cumulative expansion across both generated-placeholder kinds in UTF-16 code units per
230         * locale fallback attempt, not null
231         */
232        @NonNull public Integer getMaximumGeneratedExpansionCharacters() { return maximumGeneratedExpansionCharacters; }
233
234        @Override
235        public boolean equals(@Nullable Object object) {
236                if (this == object)
237                        return true;
238                if (!(object instanceof TranslationRuntimeLimits))
239                        return false;
240                TranslationRuntimeLimits that = (TranslationRuntimeLimits) object;
241                return Objects.equals(maximumNumberPrecision, that.maximumNumberPrecision)
242                                && Objects.equals(maximumAbsoluteNumberScale, that.maximumAbsoluteNumberScale)
243                                && Objects.equals(maximumVisibleDecimalPlaces, that.maximumVisibleDecimalPlaces)
244                                && Objects.equals(maximumCompactExponent, that.maximumCompactExponent)
245                                && Objects.equals(maximumExpressionCharacters, that.maximumExpressionCharacters)
246                                && Objects.equals(maximumExpressionTokens, that.maximumExpressionTokens)
247                                && Objects.equals(maximumExpressionNestingDepth, that.maximumExpressionNestingDepth)
248                                && Objects.equals(maximumGeneratedPlaceholderDepth, that.maximumGeneratedPlaceholderDepth)
249                                && Objects.equals(maximumInterpolatedOutputCharacters, that.maximumInterpolatedOutputCharacters)
250                                && Objects.equals(maximumGeneratedExpansionCharacters, that.maximumGeneratedExpansionCharacters);
251        }
252
253        @Override
254        public int hashCode() {
255                return Objects.hash(maximumNumberPrecision, maximumAbsoluteNumberScale, maximumVisibleDecimalPlaces,
256                                maximumCompactExponent, maximumExpressionCharacters, maximumExpressionTokens,
257                                maximumExpressionNestingDepth, maximumGeneratedPlaceholderDepth,
258                                maximumInterpolatedOutputCharacters, maximumGeneratedExpansionCharacters);
259        }
260
261        @Override
262        @NonNull
263        public String toString() {
264                return format("%s{maximumNumberPrecision=%d, maximumAbsoluteNumberScale=%d, " +
265                                "maximumVisibleDecimalPlaces=%d, maximumCompactExponent=%d, maximumExpressionCharacters=%d, " +
266                                "maximumExpressionTokens=%d, maximumExpressionNestingDepth=%d, " +
267                                "maximumGeneratedPlaceholderDepth=%d, maximumInterpolatedOutputCharacters=%d, " +
268                                "maximumGeneratedExpansionCharacters=%d}", getClass().getSimpleName(), maximumNumberPrecision,
269                                maximumAbsoluteNumberScale, maximumVisibleDecimalPlaces, maximumCompactExponent,
270                                maximumExpressionCharacters, maximumExpressionTokens, maximumExpressionNestingDepth,
271                                maximumGeneratedPlaceholderDepth, maximumInterpolatedOutputCharacters,
272                                maximumGeneratedExpansionCharacters);
273        }
274
275        /**
276         * Builder for {@link TranslationRuntimeLimits}.
277         *
278         * @since 3.0.0
279         */
280        @NotThreadSafe
281        public static final class Builder {
282                @NonNull private Integer maximumNumberPrecision = DEFAULT_MAXIMUM_NUMBER_PRECISION;
283                @NonNull private Integer maximumAbsoluteNumberScale = DEFAULT_MAXIMUM_ABSOLUTE_NUMBER_SCALE;
284                @NonNull private Integer maximumVisibleDecimalPlaces = DEFAULT_MAXIMUM_VISIBLE_DECIMAL_PLACES;
285                @NonNull private Integer maximumCompactExponent = DEFAULT_MAXIMUM_COMPACT_EXPONENT;
286                @NonNull private Integer maximumExpressionCharacters = DEFAULT_MAXIMUM_EXPRESSION_CHARACTERS;
287                @NonNull private Integer maximumExpressionTokens = DEFAULT_MAXIMUM_EXPRESSION_TOKENS;
288                @NonNull private Integer maximumExpressionNestingDepth = DEFAULT_MAXIMUM_EXPRESSION_NESTING_DEPTH;
289                @NonNull private Integer maximumGeneratedPlaceholderDepth = DEFAULT_MAXIMUM_GENERATED_PLACEHOLDER_DEPTH;
290                @NonNull private Integer maximumInterpolatedOutputCharacters = DEFAULT_MAXIMUM_INTERPOLATED_OUTPUT_CHARACTERS;
291                @NonNull private Integer maximumGeneratedExpansionCharacters = DEFAULT_MAXIMUM_GENERATED_EXPANSION_CHARACTERS;
292
293                private Builder() {}
294
295                private Builder(@NonNull TranslationRuntimeLimits limits) {
296                        this.maximumNumberPrecision = limits.maximumNumberPrecision;
297                        this.maximumAbsoluteNumberScale = limits.maximumAbsoluteNumberScale;
298                        this.maximumVisibleDecimalPlaces = limits.maximumVisibleDecimalPlaces;
299                        this.maximumCompactExponent = limits.maximumCompactExponent;
300                        this.maximumExpressionCharacters = limits.maximumExpressionCharacters;
301                        this.maximumExpressionTokens = limits.maximumExpressionTokens;
302                        this.maximumExpressionNestingDepth = limits.maximumExpressionNestingDepth;
303                        this.maximumGeneratedPlaceholderDepth = limits.maximumGeneratedPlaceholderDepth;
304                        this.maximumInterpolatedOutputCharacters = limits.maximumInterpolatedOutputCharacters;
305                        this.maximumGeneratedExpansionCharacters = limits.maximumGeneratedExpansionCharacters;
306                }
307
308                /**
309                 * Sets the decimal-precision limit.
310                 *
311                 * @param value value from 1 through {@link #MAXIMUM_NUMBER_PRECISION}, or null to restore the default
312                 * @return this builder, not null
313                 */
314                @NonNull public Builder maximumNumberPrecision(@Nullable Integer value) {
315                        this.maximumNumberPrecision = value == null ? DEFAULT_MAXIMUM_NUMBER_PRECISION : value;
316                        return this;
317                }
318                /**
319                 * Sets the absolute decimal-scale limit.
320                 *
321                 * @param value value from 0 through {@link #MAXIMUM_ABSOLUTE_NUMBER_SCALE}, or null to restore the default
322                 * @return this builder, not null
323                 */
324                @NonNull public Builder maximumAbsoluteNumberScale(@Nullable Integer value) {
325                        this.maximumAbsoluteNumberScale = value == null ? DEFAULT_MAXIMUM_ABSOLUTE_NUMBER_SCALE : value;
326                        return this;
327                }
328                /**
329                 * Sets the explicitly visible-decimal-place limit.
330                 *
331                 * @param value value from 0 through {@link #MAXIMUM_VISIBLE_DECIMAL_PLACES}, or null to restore the default
332                 * @return this builder, not null
333                 */
334                @NonNull public Builder maximumVisibleDecimalPlaces(@Nullable Integer value) {
335                        this.maximumVisibleDecimalPlaces = value == null ? DEFAULT_MAXIMUM_VISIBLE_DECIMAL_PLACES : value;
336                        return this;
337                }
338                /**
339                 * Sets the compact-decimal-exponent limit.
340                 *
341                 * @param value value from 0 through {@link #MAXIMUM_COMPACT_EXPONENT}, or null to restore the default
342                 * @return this builder, not null
343                 */
344                @NonNull public Builder maximumCompactExponent(@Nullable Integer value) {
345                        this.maximumCompactExponent = value == null ? DEFAULT_MAXIMUM_COMPACT_EXPONENT : value;
346                        return this;
347                }
348                /**
349                 * Sets the source-length limit for each whole-message or generated-fragment expression.
350                 *
351                 * @param value value from 1 through {@link #MAXIMUM_EXPRESSION_CHARACTERS}, or null to restore the default
352                 * @return this builder, not null
353                 */
354                @NonNull public Builder maximumExpressionCharacters(@Nullable Integer value) {
355                        this.maximumExpressionCharacters = value == null ? DEFAULT_MAXIMUM_EXPRESSION_CHARACTERS : value;
356                        return this;
357                }
358                /**
359                 * Sets the token-count limit for each whole-message or generated-fragment expression.
360                 *
361                 * @param value value from 1 through {@link #MAXIMUM_EXPRESSION_TOKENS}, or null to restore the default
362                 * @return this builder, not null
363                 */
364                @NonNull public Builder maximumExpressionTokens(@Nullable Integer value) {
365                        this.maximumExpressionTokens = value == null ? DEFAULT_MAXIMUM_EXPRESSION_TOKENS : value;
366                        return this;
367                }
368                /**
369                 * Sets the nested-group limit for each whole-message or generated-fragment expression.
370                 *
371                 * @param value value from 0 through {@link #MAXIMUM_EXPRESSION_NESTING_DEPTH}, or null to restore the default
372                 * @return this builder, not null
373                 */
374                @NonNull public Builder maximumExpressionNestingDepth(@Nullable Integer value) {
375                        this.maximumExpressionNestingDepth = value == null ? DEFAULT_MAXIMUM_EXPRESSION_NESTING_DEPTH : value;
376                        return this;
377                }
378                /**
379                 * Sets the generated-placeholder nesting limit shared by language-form and expression-selected fragments.
380                 *
381                 * @param value value from 0 through {@link #MAXIMUM_GENERATED_PLACEHOLDER_DEPTH}, or null to restore the default
382                 * @return this builder, not null
383                 */
384                @NonNull public Builder maximumGeneratedPlaceholderDepth(@Nullable Integer value) {
385                        this.maximumGeneratedPlaceholderDepth = value == null ? DEFAULT_MAXIMUM_GENERATED_PLACEHOLDER_DEPTH : value;
386                        return this;
387                }
388                /**
389                 * Sets the maximum UTF-16 code-unit count for one interpolated message, generated fragment, or caller-supplied
390                 * phonetic input.
391                 *
392                 * @param value value from 1 through {@link #MAXIMUM_INTERPOLATED_OUTPUT_CHARACTERS}, or null to restore the default
393                 * @return this builder, not null
394                 */
395                @NonNull public Builder maximumInterpolatedOutputCharacters(@Nullable Integer value) {
396                        this.maximumInterpolatedOutputCharacters = value == null ? DEFAULT_MAXIMUM_INTERPOLATED_OUTPUT_CHARACTERS : value;
397                        return this;
398                }
399                /**
400                 * Sets the cumulative UTF-16 code-unit budget shared by language-form and expression-selected generated fragments
401                 * in one locale fallback attempt. Locale fallback starts a fresh budget for each candidate.
402                 *
403                 * @param value value from 0 through {@link #MAXIMUM_GENERATED_EXPANSION_CHARACTERS}, or null to restore the default
404                 * @return this builder, not null
405                 */
406                @NonNull public Builder maximumGeneratedExpansionCharacters(@Nullable Integer value) {
407                        this.maximumGeneratedExpansionCharacters = value == null ? DEFAULT_MAXIMUM_GENERATED_EXPANSION_CHARACTERS : value;
408                        return this;
409                }
410
411                /**
412                 * Builds immutable runtime limits.
413                 *
414                 * @return immutable runtime limits, not null
415                 * @throws IllegalArgumentException if a configured value is outside its documented range
416                 */
417                @NonNull
418                public TranslationRuntimeLimits build() {
419                        validatePositive("maximumNumberPrecision", maximumNumberPrecision, MAXIMUM_NUMBER_PRECISION);
420                        validateNonNegative("maximumAbsoluteNumberScale", maximumAbsoluteNumberScale, MAXIMUM_ABSOLUTE_NUMBER_SCALE);
421                        validateNonNegative("maximumVisibleDecimalPlaces", maximumVisibleDecimalPlaces, MAXIMUM_VISIBLE_DECIMAL_PLACES);
422                        validateNonNegative("maximumCompactExponent", maximumCompactExponent, MAXIMUM_COMPACT_EXPONENT);
423                        validatePositive("maximumExpressionCharacters", maximumExpressionCharacters, MAXIMUM_EXPRESSION_CHARACTERS);
424                        validatePositive("maximumExpressionTokens", maximumExpressionTokens, MAXIMUM_EXPRESSION_TOKENS);
425                        validateNonNegative("maximumExpressionNestingDepth", maximumExpressionNestingDepth, MAXIMUM_EXPRESSION_NESTING_DEPTH);
426                        validateNonNegative("maximumGeneratedPlaceholderDepth", maximumGeneratedPlaceholderDepth, MAXIMUM_GENERATED_PLACEHOLDER_DEPTH);
427                        validatePositive("maximumInterpolatedOutputCharacters", maximumInterpolatedOutputCharacters, MAXIMUM_INTERPOLATED_OUTPUT_CHARACTERS);
428                        validateNonNegative("maximumGeneratedExpansionCharacters", maximumGeneratedExpansionCharacters, MAXIMUM_GENERATED_EXPANSION_CHARACTERS);
429                        return new TranslationRuntimeLimits(this);
430                }
431
432                private static void validatePositive(@NonNull String name, @NonNull Integer value, @NonNull Integer ceiling) {
433                        if (value <= 0)
434                                throw new IllegalArgumentException(format("%s must be positive, but was %d", name, value));
435                        validateCeiling(name, value, ceiling);
436                }
437
438                private static void validateNonNegative(@NonNull String name, @NonNull Integer value, @NonNull Integer ceiling) {
439                        if (value < 0)
440                                throw new IllegalArgumentException(format("%s must be non-negative, but was %d", name, value));
441                        validateCeiling(name, value, ceiling);
442                }
443
444                private static void validateCeiling(@NonNull String name, @NonNull Integer value, @NonNull Integer ceiling) {
445                        if (value > ceiling)
446                                throw new IllegalArgumentException(format("%s %d exceeds the hard ceiling of %d", name, value, ceiling));
447                }
448        }
449}