15
15
16
16
package software .amazon .smithy .protocoltests .traits ;
17
17
18
+ import java .io .IOException ;
19
+ import java .io .StringReader ;
18
20
import java .util .ArrayList ;
21
+ import java .util .Collections ;
19
22
import java .util .List ;
20
23
import java .util .Optional ;
21
- import java .util .stream .Collectors ;
22
- import java .util .stream .Stream ;
24
+ import javax .xml .parsers .DocumentBuilder ;
25
+ import javax .xml .parsers .DocumentBuilderFactory ;
26
+ import javax .xml .parsers .ParserConfigurationException ;
27
+ import org .xml .sax .InputSource ;
28
+ import org .xml .sax .SAXException ;
23
29
import software .amazon .smithy .model .Model ;
24
30
import software .amazon .smithy .model .knowledge .OperationIndex ;
31
+ import software .amazon .smithy .model .loader .ModelSyntaxException ;
25
32
import software .amazon .smithy .model .node .Node ;
26
33
import software .amazon .smithy .model .node .ObjectNode ;
27
34
import software .amazon .smithy .model .shapes .OperationShape ;
33
40
import software .amazon .smithy .model .validation .NodeValidationVisitor ;
34
41
import software .amazon .smithy .model .validation .ValidationEvent ;
35
42
import software .amazon .smithy .model .validation .node .TimestampValidationStrategy ;
43
+ import software .amazon .smithy .utils .MediaType ;
36
44
45
+ /**
46
+ * Validates the following:
47
+ *
48
+ * <ul>
49
+ * <li>XML and JSON bodyMediaTypes contain valid content.</li>
50
+ * <li>vendorParamsShape is a valid shape.</li>
51
+ * <li>Vendor params are compatible with any referenced shape.</li>
52
+ * <li>Params for a test case are valid for the model.</li>
53
+ * </ul>
54
+ *
55
+ * @param <T> Type of test case to validate.
56
+ */
37
57
abstract class ProtocolTestCaseValidator <T extends Trait > extends AbstractValidator {
38
58
39
59
private final Class <T > traitClass ;
40
60
private final ShapeId traitId ;
41
61
private final String descriptor ;
62
+ private final DocumentBuilderFactory documentBuilderFactory ;
42
63
43
64
ProtocolTestCaseValidator (ShapeId traitId , Class <T > traitClass , String descriptor ) {
44
65
this .traitId = traitId ;
45
66
this .traitClass = traitClass ;
46
67
this .descriptor = descriptor ;
68
+ documentBuilderFactory = DocumentBuilderFactory .newInstance ();
47
69
}
48
70
49
71
@ Override
50
72
public List <ValidationEvent > validate (Model model ) {
51
73
OperationIndex operationIndex = OperationIndex .of (model );
52
74
53
- return Stream . concat ( model . shapes ( OperationShape . class ), model . shapes ( StructureShape . class ))
54
- . filter ( shape -> shape . hasTrait (traitClass ))
55
- . flatMap ( shape -> {
56
- return validateOperation ( model , operationIndex , shape , shape . expectTrait ( traitClass )). stream ();
57
- })
58
- . collect ( Collectors . toList ()) ;
75
+ List < ValidationEvent > events = new ArrayList <>();
76
+ for ( Shape shape : model . getShapesWithTrait (traitClass )) {
77
+ events . addAll ( validateShape ( model , operationIndex , shape , shape . expectTrait ( traitClass )));
78
+ }
79
+
80
+ return events ;
59
81
}
60
82
61
83
abstract StructureShape getStructure (Shape shape , OperationIndex operationIndex );
@@ -66,7 +88,7 @@ boolean isValidatedBy(Shape shape) {
66
88
return shape instanceof OperationShape ;
67
89
}
68
90
69
- private List <ValidationEvent > validateOperation (
91
+ private List <ValidationEvent > validateShape (
70
92
Model model ,
71
93
OperationIndex operationIndex ,
72
94
Shape shape ,
@@ -78,6 +100,9 @@ private List<ValidationEvent> validateOperation(
78
100
for (int i = 0 ; i < testCases .size (); i ++) {
79
101
HttpMessageTestCase testCase = testCases .get (i );
80
102
103
+ // Validate the syntax of known media types like XML and JSON.
104
+ events .addAll (validateMediaType (shape , trait , testCase ));
105
+
81
106
// Validate the vendorParams for the test case if we have a shape defined.
82
107
Optional <ShapeId > vendorParamsShapeOptional = testCase .getVendorParamsShape ();
83
108
ObjectNode vendorParams = testCase .getVendorParams ();
@@ -127,4 +152,59 @@ private NodeValidationVisitor createVisitor(
127
152
.allowBoxedNull (true )
128
153
.build ();
129
154
}
155
+
156
+ private List <ValidationEvent > validateMediaType (Shape shape , Trait trait , HttpMessageTestCase test ) {
157
+ // Only validate the body if it's a non-empty string. Some protocols
158
+ // require a content-type header even with no payload.
159
+ if (!test .getBody ().filter (s -> !s .isEmpty ()).isPresent ()) {
160
+ return Collections .emptyList ();
161
+ }
162
+
163
+ String rawMediaType = test .getBodyMediaType ().orElse ("application/octet-stream" );
164
+ MediaType mediaType = MediaType .from (rawMediaType );
165
+ List <ValidationEvent > events = new ArrayList <>();
166
+ if (isXml (mediaType )) {
167
+ validateXml (shape , trait , test ).ifPresent (events ::add );
168
+ } else if (isJson (mediaType )) {
169
+ validateJson (shape , trait , test ).ifPresent (events ::add );
170
+ }
171
+
172
+ return events ;
173
+ }
174
+
175
+ private boolean isXml (MediaType mediaType ) {
176
+ return mediaType .getSubtype ().equals ("xml" ) || mediaType .getSuffix ().orElse ("" ).equals ("xml" );
177
+ }
178
+
179
+ private boolean isJson (MediaType mediaType ) {
180
+ return mediaType .getSubtype ().equals ("json" ) || mediaType .getSuffix ().orElse ("" ).equals ("json" );
181
+ }
182
+
183
+ private Optional <ValidationEvent > validateXml (Shape shape , Trait trait , HttpMessageTestCase test ) {
184
+ try {
185
+ DocumentBuilder builder = documentBuilderFactory .newDocumentBuilder ();
186
+ builder .parse (new InputSource (new StringReader (test .getBody ().orElse ("" ))));
187
+ return Optional .empty ();
188
+ } catch (ParserConfigurationException | SAXException | IOException e ) {
189
+ return Optional .of (emitMediaTypeError (shape , trait , test , e ));
190
+ }
191
+ }
192
+
193
+ private Optional <ValidationEvent > validateJson (Shape shape , Trait trait , HttpMessageTestCase test ) {
194
+ try {
195
+ Node .parse (test .getBody ().orElse ("" ));
196
+ return Optional .empty ();
197
+ } catch (ModelSyntaxException e ) {
198
+ return Optional .of (emitMediaTypeError (shape , trait , test , e ));
199
+ }
200
+ }
201
+
202
+ private ValidationEvent emitMediaTypeError (Shape shape , Trait trait , HttpMessageTestCase test , Throwable e ) {
203
+ return danger (shape , trait , String .format (
204
+ "Invalid %s content in `%s` protocol test case `%s`: %s" ,
205
+ test .getBodyMediaType ().orElse ("" ),
206
+ trait .toShapeId (),
207
+ test .getId (),
208
+ e .getMessage ()));
209
+ }
130
210
}
0 commit comments