Skip to content

Commit 1b0e9a4

Browse files
authored
Merge pull request #2 from graphql-java/better_locale_support
Better locale support while its missing in graphql-java
2 parents e1aa48e + 966d2ce commit 1b0e9a4

File tree

9 files changed

+340
-33
lines changed

9 files changed

+340
-33
lines changed

readme.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,20 @@ You can use Java EL syntax in the message templates to format even more powerful
120120
If you use directive arguments like `message : String = "graphql.validation.Size.message"` then the `ResourceBundleMessageInterpolator` class
121121
will use that as a resource bundle lookup key. This too is inspired by the javax.validation annotations and how they work.
122122
123-
Like javax.validation, this library ships with some default error message templates but you can override them.
123+
Like javax.validation, this library ships with some default error message templates but you can override them.
124+
125+
# I18n Locale Support
126+
127+
The validation library aims to offer Internationalisation (18N) of the error messages. When the validation rules
128+
run they are passed in a `java.util.Locale`. A `ResourceBundleMessageInterpolator` can then be used to build up messages
129+
that come from I18N bundles.
130+
131+
A `Locale` should be created per graphql execution. However at the time of writing graphql-java does not
132+
pass in a `Locale` per request `ExecutionInput` . A PR exists to fix this and it will be released in v14.0. This
133+
library will then be updated to to take advantage of this.
134+
135+
In the mean time you can work around this by having the `context`, `source` or `root` implement `graphql.validation.locale.LocaleProvider` and
136+
the library will extract a `Locale` from that.
124137
125138
# Schema Directive Wiring
126139
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package graphql.validation.locale;
2+
3+
import java.util.Locale;
4+
5+
/**
6+
* An object that can give back a locale
7+
*/
8+
public interface LocaleProvider {
9+
10+
/**
11+
* @return a locale to be used by validation rules
12+
*/
13+
Locale getLocale();
14+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package graphql.validation.locale;
2+
3+
import graphql.Internal;
4+
import graphql.schema.DataFetchingEnvironment;
5+
6+
import java.lang.reflect.InvocationTargetException;
7+
import java.lang.reflect.Method;
8+
import java.util.Locale;
9+
import java.util.Map;
10+
import java.util.concurrent.ConcurrentHashMap;
11+
12+
public class LocaleUtil {
13+
14+
/**
15+
* This will try to determine the Locale from the data fetching env in a number of ways, searching
16+
* via the context and source objects and the data fetching environment itself. This plugs a gap while
17+
* graphql-java does not have a getLocale on ExecutionInput / DataFetchingEnvironment
18+
*
19+
* @param environment the fetching env
20+
* @param defaultLocale the default to use
21+
*
22+
* @return a Locale
23+
*/
24+
public static Locale determineLocale(DataFetchingEnvironment environment, Locale defaultLocale) {
25+
//
26+
// in a future version of graphql java the DFE will have the Locale but in the mean time
27+
Locale locale;
28+
locale = extractLocale(environment);
29+
if (locale == null) {
30+
locale = extractLocale(environment.getContext());
31+
if (locale == null) {
32+
locale = extractLocale(environment.getSource());
33+
if (locale == null) {
34+
locale = extractLocale(environment.getRoot());
35+
if (locale == null) {
36+
locale = defaultLocale;
37+
}
38+
}
39+
}
40+
}
41+
return locale;
42+
}
43+
44+
private static Locale extractLocale(Object object) {
45+
if (object != null) {
46+
if (object instanceof LocaleProvider) {
47+
return ((LocaleProvider) object).getLocale();
48+
}
49+
return reflectGetLocale(object);
50+
}
51+
return null;
52+
}
53+
54+
private static final Map<Class, Method> METHOD_CACHE = new ConcurrentHashMap<>();
55+
private static final Map<Class, Class> FAILED_CLASS_CACHE = new ConcurrentHashMap<>();
56+
57+
@Internal
58+
public static void clearMethodCaches() {
59+
METHOD_CACHE.clear();
60+
FAILED_CLASS_CACHE.clear();
61+
}
62+
63+
private static Locale reflectGetLocale(Object object) {
64+
Class<?> clazz = object.getClass();
65+
if (FAILED_CLASS_CACHE.containsKey(clazz)) {
66+
return null;
67+
}
68+
try {
69+
Method getLocaleMethod = METHOD_CACHE.get(clazz);
70+
if (getLocaleMethod == null) {
71+
getLocaleMethod = clazz.getMethod("getLocale");
72+
if (Locale.class.equals(getLocaleMethod.getReturnType())) {
73+
METHOD_CACHE.put(clazz, getLocaleMethod);
74+
} else {
75+
getLocaleMethod = null; // wat - very tricksy hobbit??
76+
}
77+
}
78+
if (getLocaleMethod != null) {
79+
return (Locale) getLocaleMethod.invoke(object);
80+
}
81+
82+
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ignored) {
83+
}
84+
FAILED_CLASS_CACHE.put(clazz, clazz);
85+
return null;
86+
}
87+
88+
89+
}

src/main/java/graphql/validation/rules/TargetedValidationRules.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import graphql.schema.GraphQLTypeUtil;
1818
import graphql.util.FpKit;
1919
import graphql.validation.interpolation.MessageInterpolator;
20+
import graphql.validation.locale.LocaleUtil;
2021
import graphql.validation.util.Util;
2122

2223
import java.util.ArrayList;
@@ -56,12 +57,15 @@ public boolean isEmpty() {
5657
/**
5758
* Runs the contained rules that match the currently executing field named by the {@link graphql.schema.DataFetchingEnvironment}
5859
*
59-
* @param env the field being executed
60-
* @param interpolator the message interpolator to use
61-
* @param locale the locale in play
60+
* @param env the field being executed
61+
* @param interpolator the message interpolator to use
62+
* @param defaultLocale the default locale in play
63+
*
6264
* @return a list of zero or more input data validation errors
6365
*/
64-
public List<GraphQLError> runValidationRules(DataFetchingEnvironment env, MessageInterpolator interpolator, Locale locale) {
66+
public List<GraphQLError> runValidationRules(DataFetchingEnvironment env, MessageInterpolator interpolator, Locale defaultLocale) {
67+
68+
defaultLocale = LocaleUtil.determineLocale(env, defaultLocale);
6569

6670
List<GraphQLError> errors = new ArrayList<>();
6771

@@ -76,6 +80,7 @@ public List<GraphQLError> runValidationRules(DataFetchingEnvironment env, Messag
7680
ValidationEnvironment ruleEnvironment = ValidationEnvironment.newValidationEnvironment()
7781
.dataFetchingEnvironment(env)
7882
.messageInterpolator(interpolator)
83+
.locale(defaultLocale)
7984
.validatedElement(FIELD)
8085
.validatedPath(fieldPath)
8186
.build();
@@ -109,7 +114,7 @@ public List<GraphQLError> runValidationRules(DataFetchingEnvironment env, Messag
109114
.validatedPath(fieldPath.segment(fieldArg.getName()))
110115
.directives(fieldArg.getDirectives())
111116
.messageInterpolator(interpolator)
112-
.locale(locale)
117+
.locale(defaultLocale)
113118
.build();
114119

115120
for (ValidationRule rule : rules) {

src/main/java/graphql/validation/rules/ValidationRules.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,19 +93,17 @@ public List<ValidationRule> getRulesFor(GraphQLFieldDefinition fieldDefinition,
9393
* This helper method will run the validation rules that apply to the provided {@link graphql.schema.DataFetchingEnvironment}
9494
*
9595
* @param env the data fetching environment
96+
*
9697
* @return a list of zero or more input data validation errors
9798
*/
9899
public List<GraphQLError> runValidationRules(DataFetchingEnvironment env) {
99100
GraphQLFieldsContainer fieldsContainer = env.getExecutionStepInfo().getFieldContainer();
100101
GraphQLFieldDefinition fieldDefinition = env.getFieldDefinition();
101102

102-
//
103-
// a future version of graphql-java will have the locale within the DataFetchingEnvironment
104-
Locale locale = this.getLocale();
105103
MessageInterpolator messageInterpolator = this.getMessageInterpolator();
106104

107105
TargetedValidationRules rules = this.buildRulesFor(fieldDefinition, fieldsContainer);
108-
return rules.runValidationRules(env, messageInterpolator, locale);
106+
return rules.runValidationRules(env, messageInterpolator, this.getLocale());
109107
}
110108

111109
/**
@@ -163,6 +161,7 @@ public Builder messageInterpolator(MessageInterpolator messageInterpolator) {
163161
* will not be as useful.
164162
*
165163
* @param locale the locale to use for message interpolation
164+
*
166165
* @return this builder
167166
*/
168167
public Builder locale(Locale locale) {

src/main/java/graphql/validation/schemawiring/ValidationSchemaWiring.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
import graphql.schema.idl.SchemaDirectiveWiringEnvironment;
1010
import graphql.validation.interpolation.MessageInterpolator;
1111
import graphql.validation.rules.OnValidationErrorStrategy;
12-
import graphql.validation.rules.ValidationRules;
1312
import graphql.validation.rules.TargetedValidationRules;
13+
import graphql.validation.rules.ValidationRules;
1414
import graphql.validation.util.Util;
1515

1616
import java.util.List;
@@ -55,12 +55,10 @@ public GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment<GraphQLFi
5555
return fieldDefinition;
5656
}
5757

58-
private DataFetcher buildValidatingDataFetcher(TargetedValidationRules rules, OnValidationErrorStrategy errorStrategy, MessageInterpolator messageInterpolator, DataFetcher currentDF, Locale locale) {
58+
private DataFetcher buildValidatingDataFetcher(TargetedValidationRules rules, OnValidationErrorStrategy errorStrategy, MessageInterpolator messageInterpolator, DataFetcher currentDF, final Locale defaultLocale) {
5959
// ok we have some rules that need to be applied to this field and its arguments
6060
return environment -> {
61-
// TODO - get the Locale from the DFE instead of statically - this needs to go into graphql-java however
62-
63-
List<GraphQLError> errors = rules.runValidationRules(environment, messageInterpolator, locale);
61+
List<GraphQLError> errors = rules.runValidationRules(environment, messageInterpolator, defaultLocale);
6462
if (!errors.isEmpty()) {
6563
// should we continue?
6664
if (!errorStrategy.shouldContinue(errors, environment)) {
@@ -75,4 +73,5 @@ private DataFetcher buildValidatingDataFetcher(TargetedValidationRules rules, On
7573
return Util.mkDFRFromFetchedResult(errors, returnValue);
7674
};
7775
}
76+
7877
}

0 commit comments

Comments
 (0)