-
Notifications
You must be signed in to change notification settings - Fork 565
/
ResourceBundleMessageInterpolator.java
372 lines (327 loc) · 12.7 KB
/
ResourceBundleMessageInterpolator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
/*
* JBoss, Home of Professional Open Source
* Copyright 2010, Red Hat, Inc. and/or its affiliates, and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.hibernate.validator.messageinterpolation;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import javax.validation.MessageInterpolator;
import javax.xml.bind.ValidationException;
import org.hibernate.validator.internal.engine.messageinterpolation.InterpolationTerm;
import org.hibernate.validator.internal.engine.messageinterpolation.InterpolationTermType;
import org.hibernate.validator.internal.engine.messageinterpolation.LocalizedMessage;
import org.hibernate.validator.internal.engine.messageinterpolation.parser.MessageDescriptorFormatException;
import org.hibernate.validator.internal.engine.messageinterpolation.parser.Token;
import org.hibernate.validator.internal.engine.messageinterpolation.parser.TokenCollector;
import org.hibernate.validator.internal.engine.messageinterpolation.parser.TokenIterator;
import org.hibernate.validator.internal.util.ConcurrentReferenceHashMap;
import org.hibernate.validator.internal.util.logging.Log;
import org.hibernate.validator.internal.util.logging.LoggerFactory;
import org.hibernate.validator.resourceloading.PlatformResourceBundleLocator;
import org.hibernate.validator.spi.resourceloading.ResourceBundleLocator;
import static org.hibernate.validator.internal.util.ConcurrentReferenceHashMap.ReferenceType.SOFT;
/**
* Resource bundle backed message interpolator.
*
* @author Emmanuel Bernard
* @author Hardy Ferentschik
* @author Gunnar Morling
* @author Kevin Pollet <kevin.pollet@serli.com> (C) 2011 SERLI
*/
public class ResourceBundleMessageInterpolator implements MessageInterpolator {
private static final Log log = LoggerFactory.make();
/**
* The default initial capacity for this cache.
*/
private static final int DEFAULT_INITIAL_CAPACITY = 100;
/**
* The default load factor for this cache.
*/
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The default concurrency level for this cache.
*/
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
/**
* The name of the default message bundle.
*/
private static final String DEFAULT_VALIDATION_MESSAGES = "org.hibernate.validator.ValidationMessages";
/**
* The name of the user-provided message bundle as defined in the specification.
*/
public static final String USER_VALIDATION_MESSAGES = "ValidationMessages";
/**
* The default locale in the current JVM.
*/
private final Locale defaultLocale;
/**
* Loads user-specified resource bundles.
*/
private final ResourceBundleLocator userResourceBundleLocator;
/**
* Loads built-in resource bundles.
*/
private final ResourceBundleLocator defaultResourceBundleLocator;
/**
* Step 1-3 of message interpolation can be cached. We do this in this map.
*/
private final ConcurrentReferenceHashMap<LocalizedMessage, String> resolvedMessages;
/**
* Step 4 of message interpolation replaces message parameters. The token list for message parameters is cached in this map.
*/
private final ConcurrentReferenceHashMap<String, List<Token>> tokenizedParameterMessages;
/**
* Step 5 of message interpolation replaces EL expressions. The token list for EL expressions is cached in this map.
*/
private final ConcurrentReferenceHashMap<String, List<Token>> tokenizedELMessages;
/**
* Flag indicating whether this interpolator should cache some of the interpolation steps.
*/
private final boolean cachingEnabled;
public ResourceBundleMessageInterpolator() {
this( null );
}
public ResourceBundleMessageInterpolator(ResourceBundleLocator userResourceBundleLocator) {
this( userResourceBundleLocator, true );
}
public ResourceBundleMessageInterpolator(ResourceBundleLocator userResourceBundleLocator, boolean cacheMessages) {
defaultLocale = Locale.getDefault();
if ( userResourceBundleLocator == null ) {
this.userResourceBundleLocator = new PlatformResourceBundleLocator( USER_VALIDATION_MESSAGES );
}
else {
this.userResourceBundleLocator = userResourceBundleLocator;
}
this.defaultResourceBundleLocator = new PlatformResourceBundleLocator( DEFAULT_VALIDATION_MESSAGES );
this.cachingEnabled = cacheMessages;
if ( cachingEnabled ) {
this.resolvedMessages = new ConcurrentReferenceHashMap<LocalizedMessage, String>(
DEFAULT_INITIAL_CAPACITY,
DEFAULT_LOAD_FACTOR,
DEFAULT_CONCURRENCY_LEVEL,
SOFT,
SOFT,
EnumSet.noneOf( ConcurrentReferenceHashMap.Option.class )
);
this.tokenizedParameterMessages = new ConcurrentReferenceHashMap<String, List<Token>>(
DEFAULT_INITIAL_CAPACITY,
DEFAULT_LOAD_FACTOR,
DEFAULT_CONCURRENCY_LEVEL,
SOFT,
SOFT,
EnumSet.noneOf( ConcurrentReferenceHashMap.Option.class )
);
this.tokenizedELMessages = new ConcurrentReferenceHashMap<String, List<Token>>(
DEFAULT_INITIAL_CAPACITY,
DEFAULT_LOAD_FACTOR,
DEFAULT_CONCURRENCY_LEVEL,
SOFT,
SOFT,
EnumSet.noneOf( ConcurrentReferenceHashMap.Option.class )
);
}
else {
resolvedMessages = null;
tokenizedParameterMessages = null;
tokenizedELMessages = null;
}
// HV-793 - To fail eagerly in case we have no EL dependencies on the classpath we try to load the expression
// factory
try {
ResourceBundleMessageInterpolator.class.getClassLoader().loadClass( "javax.el.ExpressionFactory" );
}
catch ( ClassNotFoundException e ) {
throw log.getMissingELDependenciesException();
}
}
@Override
public String interpolate(String message, Context context) {
// probably no need for caching, but it could be done by parameters since the map
// is immutable and uniquely built per Validation definition, the comparison has to be based on == and not equals though
String interpolatedMessage = message;
try {
interpolatedMessage = interpolateMessage( message, context, defaultLocale );
}
catch ( MessageDescriptorFormatException e ) {
log.warn( e.getMessage() );
}
return interpolatedMessage;
}
@Override
public String interpolate(String message, Context context, Locale locale) {
String interpolatedMessage = message;
try {
interpolatedMessage = interpolateMessage( message, context, locale );
}
catch ( ValidationException e ) {
log.warn( e.getMessage() );
}
return interpolatedMessage;
}
/**
* Runs the message interpolation according to algorithm specified in the Bean Validation specification.
* <br/>
* Note:
* <br/>
* Look-ups in user bundles is recursive whereas look-ups in default bundle are not!
*
* @param message the message to interpolate
* @param context the context for this interpolation
* @param locale the {@code Locale} to use for the resource bundle.
*
* @return the interpolated message.
*/
private String interpolateMessage(String message, Context context, Locale locale)
throws MessageDescriptorFormatException {
LocalizedMessage localisedMessage = new LocalizedMessage( message, locale );
String resolvedMessage = null;
if ( cachingEnabled ) {
resolvedMessage = resolvedMessages.get( localisedMessage );
}
// if the message is not already in the cache we have to run step 1-3 of the message resolution
if ( resolvedMessage == null ) {
ResourceBundle userResourceBundle = userResourceBundleLocator
.getResourceBundle( locale );
ResourceBundle defaultResourceBundle = defaultResourceBundleLocator
.getResourceBundle( locale );
String userBundleResolvedMessage;
resolvedMessage = message;
boolean evaluatedDefaultBundleOnce = false;
do {
// search the user bundle recursive (step1)
userBundleResolvedMessage = interpolateBundleMessage(
resolvedMessage, userResourceBundle, locale, true
);
// exit condition - we have at least tried to validate against the default bundle and there was no
// further replacements
if ( evaluatedDefaultBundleOnce
&& !hasReplacementTakenPlace( userBundleResolvedMessage, resolvedMessage ) ) {
break;
}
// search the default bundle non recursive (step2)
resolvedMessage = interpolateBundleMessage(
userBundleResolvedMessage,
defaultResourceBundle,
locale,
false
);
evaluatedDefaultBundleOnce = true;
} while ( true );
}
// cache resolved message
if ( cachingEnabled ) {
String cachedResolvedMessage = resolvedMessages.putIfAbsent( localisedMessage, resolvedMessage );
if ( cachedResolvedMessage != null ) {
resolvedMessage = cachedResolvedMessage;
}
}
// resolve parameter expressions (step 4)
List<Token> tokens = null;
if ( cachingEnabled ) {
tokens = tokenizedParameterMessages.get( resolvedMessage );
}
if ( tokens == null ) {
TokenCollector tokenCollector = new TokenCollector( resolvedMessage, InterpolationTermType.PARAMETER );
tokens = tokenCollector.getTokenList();
if ( cachingEnabled ) {
tokenizedParameterMessages.putIfAbsent( resolvedMessage, tokens );
}
}
resolvedMessage = interpolateExpression(
new TokenIterator( tokens ),
context,
locale
);
// resolve EL expressions (step 5)
tokens = null;
if ( cachingEnabled ) {
tokens = tokenizedELMessages.get( resolvedMessage );
}
if ( tokens == null ) {
TokenCollector tokenCollector = new TokenCollector( resolvedMessage, InterpolationTermType.EL );
tokens = tokenCollector.getTokenList();
if ( cachingEnabled ) {
tokenizedELMessages.putIfAbsent( resolvedMessage, tokens );
}
}
resolvedMessage = interpolateExpression(
new TokenIterator( tokens ),
context,
locale
);
// last but not least we have to take care of escaped literals
resolvedMessage = replaceEscapedLiterals( resolvedMessage );
return resolvedMessage;
}
private String replaceEscapedLiterals(String resolvedMessage) {
resolvedMessage = resolvedMessage.replace( "\\{", "{" );
resolvedMessage = resolvedMessage.replace( "\\}", "}" );
resolvedMessage = resolvedMessage.replace( "\\\\", "\\" );
resolvedMessage = resolvedMessage.replace( "\\$", "$" );
return resolvedMessage;
}
private boolean hasReplacementTakenPlace(String origMessage, String newMessage) {
return !origMessage.equals( newMessage );
}
private String interpolateBundleMessage(String message, ResourceBundle bundle, Locale locale, boolean recursive)
throws MessageDescriptorFormatException {
TokenCollector tokenCollector = new TokenCollector( message, InterpolationTermType.PARAMETER );
TokenIterator tokenIterator = new TokenIterator( tokenCollector.getTokenList() );
while ( tokenIterator.hasMoreInterpolationTerms() ) {
String term = tokenIterator.nextInterpolationTerm();
String resolvedParameterValue = resolveParameter(
term, bundle, locale, recursive
);
tokenIterator.replaceCurrentInterpolationTerm( resolvedParameterValue );
}
return tokenIterator.getInterpolatedMessage();
}
private String interpolateExpression(TokenIterator tokenIterator, Context context, Locale locale)
throws MessageDescriptorFormatException {
while ( tokenIterator.hasMoreInterpolationTerms() ) {
String term = tokenIterator.nextInterpolationTerm();
InterpolationTerm expression = new InterpolationTerm( term, locale );
String resolvedExpression = expression.interpolate( context );
tokenIterator.replaceCurrentInterpolationTerm( resolvedExpression );
}
return tokenIterator.getInterpolatedMessage();
}
private String resolveParameter(String parameterName, ResourceBundle bundle, Locale locale, boolean recursive)
throws MessageDescriptorFormatException {
String parameterValue;
try {
if ( bundle != null ) {
parameterValue = bundle.getString( removeCurlyBraces( parameterName ) );
if ( recursive ) {
parameterValue = interpolateBundleMessage( parameterValue, bundle, locale, recursive );
}
}
else {
parameterValue = parameterName;
}
}
catch ( MissingResourceException e ) {
// return parameter itself
parameterValue = parameterName;
}
return parameterValue;
}
private String removeCurlyBraces(String parameter) {
return parameter.substring( 1, parameter.length() - 1 );
}
}