1+ /* Copyright 2012 predic8 GmbH, www.predic8.com
2+
3+ Licensed under the Apache License, Version 2.0 (the "License");
4+ you may not use this file except in compliance with the License.
5+ You may obtain a copy of the License at
6+
7+ http://www.apache.org/licenses/LICENSE-2.0
8+
9+ Unless required by applicable law or agreed to in writing, software
10+ distributed under the License is distributed on an "AS IS" BASIS,
11+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ See the License for the specific language governing permissions and
13+ limitations under the License. */
14+
15+ package com .predic8 .membrane .core .interceptor .schemavalidation .json ;
16+
17+ import com .github .fge .jsonschema .*;
18+ import com .networknt .schema .*;
19+ import com .predic8 .membrane .core .exchange .*;
20+ import com .predic8 .membrane .core .http .*;
21+ import com .predic8 .membrane .core .interceptor .Interceptor .*;
22+ import com .predic8 .membrane .core .interceptor .*;
23+ import com .predic8 .membrane .core .interceptor .schemavalidation .*;
24+ import com .predic8 .membrane .core .interceptor .schemavalidation .ValidatorInterceptor .*;
25+ import com .predic8 .membrane .core .resolver .*;
26+ import org .jetbrains .annotations .*;
27+ import org .slf4j .*;
28+
29+ import java .nio .charset .*;
30+ import java .util .*;
31+ import java .util .concurrent .atomic .*;
32+
33+ import static com .networknt .schema .InputFormat .JSON ;
34+ import static com .predic8 .membrane .core .exceptions .ProblemDetails .*;
35+ import static com .predic8 .membrane .core .interceptor .Outcome .*;
36+ import static java .nio .charset .StandardCharsets .*;
37+
38+ public class JSONYAMLSchemaValidator extends AbstractMessageValidator {
39+
40+ private static final Logger log = LoggerFactory .getLogger (JSONYAMLSchemaValidator .class );
41+
42+ private final Resolver resolver ;
43+ private final String jsonSchema ;
44+ private final FailureHandler failureHandler ;
45+
46+ private final AtomicLong valid = new AtomicLong ();
47+ private final AtomicLong invalid = new AtomicLong ();
48+ private final SpecVersion .VersionFlag schemaId ;
49+
50+ /**
51+ * JsonSchemaFactory instances are thread-safe provided its configuration is not modified.
52+ */
53+ JsonSchemaFactory jsonSchemaFactory ;
54+
55+ SchemaValidatorsConfig config ;
56+
57+ /**
58+ * JsonSchema instances are thread-safe provided its configuration is not modified.
59+ */
60+ JsonSchema schema ;
61+
62+ public JSONYAMLSchemaValidator (Resolver resolver , String jsonSchema , FailureHandler failureHandler , String schemaVersion ) {
63+ this .resolver = resolver ;
64+ this .jsonSchema = jsonSchema ;
65+ this .failureHandler = failureHandler ;
66+ this .schemaId = JSONSchemaVersionParser .parse ( schemaVersion );
67+ }
68+
69+ public JSONYAMLSchemaValidator (Resolver resolver , String jsonSchema , FailureHandler failureHandler ) {
70+ this (resolver , jsonSchema , failureHandler , "2020-12" );
71+ }
72+
73+ @ Override
74+ public String getName () {
75+ return "JSON Schema Validator" ;
76+ }
77+
78+ @ Override
79+ public void init () {
80+ super .init ();
81+
82+ jsonSchemaFactory = JsonSchemaFactory .getInstance (schemaId , builder ->
83+ builder .schemaLoaders (loaders -> loaders .add (new MembraneSchemaLoader (resolver )))
84+ // builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.example.org/", "classpath:/"))
85+ );
86+
87+ SchemaValidatorsConfig .Builder builder = SchemaValidatorsConfig .builder ();
88+ // By default the JDK regular expression implementation which is not ECMA 262 compliant is used
89+ // Note that setting this requires including optional dependencies
90+ // builder.regularExpressionFactory(GraalJSRegularExpressionFactory.getInstance());
91+ // builder.regularExpressionFactory(JoniRegularExpressionFactory.getInstance());
92+ config = builder .build ();
93+
94+ // If the schema data does not specify an $id the absolute IRI of the schema location will be used as the $id.
95+ schema = jsonSchemaFactory .getSchema (SchemaLocation .of ( jsonSchema ), config );
96+ schema .initializeValidators ();
97+
98+ }
99+
100+ public Outcome validateMessage (Exchange exc , Flow flow ) throws Exception {
101+ return validateMessage (exc , flow , UTF_8 );
102+ }
103+
104+ public Outcome validateMessage (Exchange exc , Flow flow , Charset ignored ) throws Exception {
105+
106+ Set <ValidationMessage > assertions = schema .validate (exc .getMessage (flow ).getBodyAsStringDecoded (), JSON );
107+
108+ if (assertions .isEmpty ()) {
109+ valid .incrementAndGet ();
110+ return CONTINUE ;
111+ }
112+ invalid .incrementAndGet ();
113+
114+ log .debug ("Validation failed: {}" , assertions );
115+
116+ user (false , getName ())
117+ .title (getErrorTitle ())
118+ .addSubType ("validation" )
119+ .component (getName ())
120+ .internal ("flow" , flow .name ())
121+ .internal ("errors" , getMapForProblemDetails (assertions ))
122+ .buildAndSetResponse (exc );
123+
124+ return ABORT ;
125+ }
126+
127+ private @ NotNull List <Map <String , Object >> getMapForProblemDetails (Set <ValidationMessage > assertions ) {
128+ return assertions .stream ().map (this ::validationMessageToProblemDetailsMap ).toList ();
129+ }
130+
131+ private @ NotNull Map <String , Object > validationMessageToProblemDetailsMap (ValidationMessage vm ) {
132+ Map <String , Object > m = new LinkedHashMap <>();
133+ m .put ("message" , vm .getMessage ());
134+ m .put ("code" , vm .getCode ());
135+ m .put ("key" , vm .getMessageKey ());
136+ if (vm .getDetails () != null )
137+ m .put ("details" , vm .getDetails ());
138+ m .put ("type" , vm .getType ());
139+ m .put ("error" , vm .getError ());
140+ m .put ("pointer" , getPointer (vm .getEvaluationPath ()));
141+ m .put ("node" , vm .getInstanceNode ());
142+ return m ;
143+ }
144+
145+ private String getPointer (JsonNodePath evaluationPath ) {
146+ if (evaluationPath == null || evaluationPath .getNameCount () == 0 ) {
147+ return "" ;
148+ }
149+
150+ StringBuilder sb = new StringBuilder ();
151+ for (int i = 0 ; i < evaluationPath .getNameCount (); i ++) {
152+ sb .append ('/' );
153+ String part = evaluationPath .getName (i );
154+
155+ // escape according to RFC 6901
156+ part = part .replace ("~" , "~0" ).replace ("/" , "~1" );
157+
158+ sb .append (part );
159+ }
160+ return sb .toString ();
161+ }
162+
163+ @ Override
164+ public long getValid () {
165+ return valid .get ();
166+ }
167+
168+ @ Override
169+ public long getInvalid () {
170+ return invalid .get ();
171+ }
172+
173+ @ Override
174+ public String getErrorTitle () {
175+ return "JSON validation failed" ;
176+ }
177+ }
0 commit comments