1
2
3
4
5
6
7
8
9
10
11
12
13 package com.eviware.soapui.impl.wsdl.support;
14
15 import java.util.ArrayList;
16 import java.util.Iterator;
17 import java.util.List;
18 import java.util.Map;
19
20 import javax.wsdl.Binding;
21 import javax.wsdl.BindingFault;
22 import javax.wsdl.BindingOperation;
23 import javax.wsdl.Part;
24 import javax.wsdl.Port;
25 import javax.wsdl.Service;
26 import javax.xml.namespace.QName;
27
28 import org.apache.log4j.Logger;
29 import org.apache.xmlbeans.SchemaGlobalElement;
30 import org.apache.xmlbeans.SchemaType;
31 import org.apache.xmlbeans.SchemaTypeLoader;
32 import org.apache.xmlbeans.XmlBeans;
33 import org.apache.xmlbeans.XmlError;
34 import org.apache.xmlbeans.XmlException;
35 import org.apache.xmlbeans.XmlLineNumber;
36 import org.apache.xmlbeans.XmlObject;
37 import org.apache.xmlbeans.XmlOptions;
38
39 import com.eviware.soapui.SoapUI;
40 import com.eviware.soapui.impl.wsdl.teststeps.assertions.AssertionError;
41
42 /***
43 * Class for validating SOAP requests/responses against their definition and schema, requires that
44 * the messages follow basic-profile requirements
45 *
46 * @author Ole.Matzura
47 */
48
49 public class WsdlValidator
50 {
51 private final WsdlContext wsdlContext;
52 private final static Logger log = Logger.getLogger( WsdlValidator.class );
53
54 public WsdlValidator( WsdlContext wsdlContext )
55 {
56 this.wsdlContext = wsdlContext;
57 }
58
59 public AssertionError [] assertRequest( String request, String operationName )
60 {
61 List<XmlError> errors = new ArrayList<XmlError>();
62 try
63 {
64 validateXml(request, errors);
65
66 if (errors.isEmpty())
67 {
68 validateSoapEnvelope(request, errors);
69
70 BindingOperation bindingOperation = findBindingOperation(operationName);
71 if (bindingOperation == null)
72 {
73 errors.add(XmlError.forMessage("Missing operation ["
74 + operationName + "] in wsdl definition"));
75 }
76 else
77 validateMessage(request, bindingOperation, WsdlUtils
78 .getInputParts(bindingOperation), errors);
79 }
80 }
81 catch( XmlException e )
82 {
83 errors.addAll( e.getErrors() );
84 }
85 catch (Exception e)
86 {
87 errors.add( XmlError.forMessage( e.getMessage() ));
88 }
89
90 return convertErrors( errors );
91 }
92
93 public void validateXml(String request, List<XmlError> errors )
94 {
95 try
96 {
97 XmlOptions xmlOptions = new XmlOptions();
98 xmlOptions.setLoadLineNumbers();
99 xmlOptions.setErrorListener(errors);
100 XmlObject xml = XmlObject.Factory.parse(request, xmlOptions);
101 }
102 catch( XmlException e )
103 {
104 errors.addAll( e.getErrors() );
105 }
106 catch (Exception e)
107 {
108 errors.add( XmlError.forMessage( e.getMessage() ));
109 }
110 }
111
112 private AssertionError[] convertErrors(List<XmlError> errors)
113 {
114 if( errors.size() > 0 )
115 {
116 List<AssertionError> response = new ArrayList<AssertionError>();
117 for (Iterator<XmlError> i = errors.iterator(); i.hasNext();)
118 {
119 AssertionError assertionError = new AssertionError(i.next());
120 if( !response.contains( assertionError ))
121 response.add( assertionError );
122 }
123
124 return response.toArray( new AssertionError[response.size()] );
125 }
126
127 return new AssertionError[0];
128 }
129
130 public void validateMessage( String request, BindingOperation bindingOperation, Part [] parts, List<XmlError> errors )
131 {
132 try
133 {
134 if( !wsdlContext.hasSchemaTypes() )
135 {
136 errors.add( XmlError.forMessage( "Missing schema types for message"));
137 }
138 else
139 {
140 if( !WsdlUtils.isOutputSoapEncoded( bindingOperation))
141 {
142 XmlOptions xmlOptions = new XmlOptions();
143 xmlOptions.setLoadLineNumbers();
144 XmlObject xml = XmlObject.Factory.parse( request, xmlOptions );
145
146 XmlObject[] paths = xml.selectPath( "declare namespace env='http://schemas.xmlsoap.org/soap/envelope/';" +
147 "$this/env:Envelope/env:Body/env:Fault");
148
149 if( paths.length > 0 )
150 {
151 validateSoapFault( bindingOperation, paths[0], errors );
152 }
153 else if( WsdlUtils.isRpc( wsdlContext.getDefinition(), bindingOperation ))
154 {
155 validateRpcLiteral( bindingOperation, parts, xml, errors );
156 }
157 else
158 {
159 validateDocLiteral( bindingOperation, parts, xml, errors );
160 }
161 }
162 else errors.add( XmlError.forMessage( "Validation of SOAP-Encoded messages not supported"));
163 }
164 }
165 catch ( XmlException e )
166 {
167 errors.addAll( e.getErrors() );
168 }
169 catch (Exception e)
170 {
171 errors.add( XmlError.forMessage( e.getMessage() ));
172 }
173 }
174
175 private BindingOperation findBindingOperation(String operationName) throws Exception
176 {
177 Map services = wsdlContext.getDefinition().getServices();
178 Iterator i = services.keySet().iterator();
179 while( i.hasNext() )
180 {
181 Service service = (Service) wsdlContext.getDefinition().getService( (QName) i.next());
182 Map ports = service.getPorts();
183
184 Iterator iterator = ports.keySet().iterator();
185 while( iterator.hasNext() )
186 {
187 Port port = (Port) service.getPort( (String) iterator.next() );
188 BindingOperation bindingOperation = port.getBinding().getBindingOperation( operationName, null, null );
189 if( bindingOperation != null ) return bindingOperation;
190 }
191 }
192
193 Map bindings = wsdlContext.getDefinition().getBindings();
194 i = bindings.keySet().iterator();
195 while( i.hasNext() )
196 {
197 Binding binding = (Binding) bindings.get( i.next() );
198 BindingOperation bindingOperation = binding.getBindingOperation( operationName, null, null );
199 if( bindingOperation != null ) return bindingOperation;
200 }
201
202 return null;
203 }
204
205 public AssertionError [] assertResponse( String response, String operationName )
206 {
207 List<XmlError> errors = new ArrayList<XmlError>();
208 try
209 {
210 validateXml(response, errors);
211
212 if (errors.isEmpty())
213 {
214 validateSoapEnvelope(response, errors);
215
216 BindingOperation bindingOperation = findBindingOperation(operationName);
217 if (bindingOperation == null)
218 {
219 errors.add(XmlError.forMessage("Missing operation ["
220 + operationName + "] in wsdl definition"));
221 }
222 else
223 validateMessage(response, bindingOperation, WsdlUtils
224 .getOutputParts(bindingOperation), errors);
225 }
226 }
227 catch ( XmlException e )
228 {
229 errors.addAll( e.getErrors() );
230 }
231 catch (Exception e)
232 {
233 errors.add( XmlError.forMessage( e.getMessage() ));
234 }
235
236
237 return convertErrors( errors );
238 }
239
240 private void validateDocLiteral(BindingOperation bindingOperation, Part[] outputParts, XmlObject msgXml, List<XmlError> errors) throws Exception
241 {
242 if( outputParts.length != 1 )
243 {
244 errors.add( XmlError.forMessage("DocLiteral message must contain 1 part definition" ));
245 return;
246 }
247
248 Part part = outputParts[0];
249 QName elementName = part.getElementName();
250 if( elementName != null )
251 {
252
253 XmlObject[] paths = msgXml.selectPath( "declare namespace env='http://schemas.xmlsoap.org/soap/envelope/';" +
254 "declare namespace ns='" + elementName.getNamespaceURI() + "';" +
255 "$this/env:Envelope/env:Body/ns:" + elementName.getLocalPart() );
256
257 if( paths.length == 1 )
258 {
259 SchemaGlobalElement elm = wsdlContext.getSchemaTypes().findElement( elementName );
260 if( elm != null )
261 {
262 validateMessageBody(errors, elm.getType(), paths[0]);
263 }
264 else errors.add( XmlError.forMessage("Missing part type in associated schema") );
265 }
266 else errors.add( XmlError.forMessage("Missing message part with name [" + elementName + "]" ));
267 }
268 else if( part.getTypeName() != null )
269 {
270 QName typeName = part.getTypeName();
271
272 XmlObject[] paths = msgXml.selectPath( "declare namespace env='http://schemas.xmlsoap.org/soap/envelope/';" +
273 "declare namespace ns='" + typeName.getNamespaceURI() + "';" +
274 "$this/env:Envelope/env:Body/ns:" + part.getName() );
275
276 if( paths.length == 1 )
277 {
278 SchemaType type = wsdlContext.getSchemaTypes().findType( typeName );
279 if( type != null )
280 {
281 validateMessageBody( errors, type, paths[0] );
282
283
284 }
285 else errors.add(XmlError.forMessage( "Missing part type in associated schema") );
286 }
287 else errors.add( XmlError.forMessage("Missing message part with name:type [" +
288 part.getName() + ":" + typeName + "]" ));
289 }
290 }
291
292 private void validateMessageBody(List<XmlError> errors, SchemaType type, XmlObject msg) throws XmlException
293 {
294
295
296 XmlObject obj = XmlObject.Factory.parse( msg.copy().changeType( type ).xmlText( new XmlOptions().setSaveOuter()), new XmlOptions().setLoadLineNumbers() );
297 obj = obj.changeType( type );
298
299
300 List list = new ArrayList();
301 obj.validate( new XmlOptions().setErrorListener( list ));
302
303
304 for( int c = 0; c < list.size(); c++ )
305 {
306 XmlError error = (XmlError) list.get( c );
307 errors.add( XmlError.forLocation( error.getMessage(), error.getSourceName(),
308 getLine( msg ) + error.getLine()-1, error.getColumn(), error.getOffset() ));
309 }
310 }
311
312 private int getLine(XmlObject object)
313 {
314 List list = new ArrayList();
315 object.newCursor().getAllBookmarkRefs( list );
316 for( int c = 0; c < list.size(); c++ )
317 {
318 if( list.get( c ) instanceof XmlLineNumber )
319 {
320 return ((XmlLineNumber)list.get(c)).getLine();
321 }
322 }
323
324 return -1;
325 }
326
327 private void validateRpcLiteral(BindingOperation bindingOperation, Part[] outputParts, XmlObject msgXml, List<XmlError> errors ) throws Exception
328 {
329 if( outputParts.length == 0 )
330 return;
331
332
333 XmlObject[] paths = msgXml.selectPath( "declare namespace env='http://schemas.xmlsoap.org/soap/envelope/';" +
334 "declare namespace ns='" + wsdlContext.getDefinition().getTargetNamespace() + "';" +
335 "$this/env:Envelope/env:Body/ns:" + bindingOperation.getName() );
336
337 if( paths.length != 1 )
338 {
339 errors.add( XmlError.forMessage("Missing message wrapper element [" +
340 wsdlContext.getDefinition().getTargetNamespace() + "@" + bindingOperation.getName() ));
341 }
342 else
343 {
344 XmlObject wrapper = paths[0];
345
346 for (int i = 0; i < outputParts.length; i++)
347 {
348 Part part = outputParts[i];
349 XmlObject[] children = wrapper.selectChildren( new QName( wsdlContext.getDefinition().getTargetNamespace(), part.getName() ));
350 if( children.length != 1 )
351 {
352 errors.add( XmlError.forMessage("Missing message part [" + part.getName() + "]" ));
353 }
354 else
355 {
356 QName typeName = part.getTypeName();
357 SchemaType type = wsdlContext.getSchemaTypes().findType( typeName );
358 if( type != null )
359 {
360 validateMessageBody( errors, type, children[0]);
361 }
362 else errors.add( XmlError.forMessage("Missing type in associated schema for part [" + part.getName() + "]" ));
363 }
364 }
365 }
366 }
367
368 private void validateSoapFault(BindingOperation bindingOperation, XmlObject msgXml, List<XmlError> errors) throws Exception
369 {
370 Map faults = bindingOperation.getBindingFaults();
371 Iterator<BindingFault> i = faults.values().iterator();
372
373 while( i.hasNext() )
374 {
375 BindingFault bindingFault = i.next();
376 String faultName = bindingFault.getName();
377
378 Part[] faultParts = WsdlUtils.getFaultParts( bindingOperation, faultName );
379 if( faultParts.length != 1 )
380 {
381 log.info( "Missing fault parts in wsdl for fault [" + faultName + "]" );
382 continue;
383 }
384
385 Part part = faultParts[0];
386 QName elementName = part.getElementName();
387 if( elementName != null )
388 {
389 XmlObject[] paths = msgXml.selectPath( "declare namespace env='http://schemas.xmlsoap.org/soap/envelope/';" +
390 "declare namespace ns='" + elementName.getNamespaceURI() + "';" +
391 "//env:Envelope/env:Body/env:Fault/detail/ns:" + elementName.getLocalPart() );
392
393 if( paths.length == 1 )
394 {
395 SchemaGlobalElement elm = wsdlContext.getSchemaTypes().findElement( elementName );
396 if( elm != null )
397 {
398 validateMessageBody( errors, elm.getType(), paths[0]);
399 break;
400 }
401 else errors.add( XmlError.forMessage("Missing fault part type in associated schema") );
402 }
403 else log.info("Missing fault part in message with name [" + elementName + "]");
404 }
405
406 else if( part.getTypeName() != null )
407 {
408 QName typeName = part.getTypeName();
409
410 XmlObject[] paths = msgXml.selectPath( "declare namespace env='http://schemas.xmlsoap.org/soap/envelope/';" +
411 "declare namespace ns='" + typeName.getNamespaceURI() + "';" +
412 "$this/env:Envelope/env:Fault/detail/ns:" + part.getName() );
413
414 if( paths.length == 1 )
415 {
416 SchemaType type = wsdlContext.getSchemaTypes().findType( typeName );
417 if( type != null )
418 {
419 validateMessageBody( errors, type, paths[0]);
420 }
421 else errors.add( XmlError.forMessage( "Missing part type in associated schema" ) );
422 }
423 else log.info("Missing message part with name:type [" + part.getName() + ":" + typeName + "]");
424 }
425 }
426 }
427
428 public void validateSoapEnvelope(String soapMessage, List<XmlError> errors)
429 {
430 try
431 {
432 SchemaTypeLoader schema = XmlBeans
433 .loadXsd(new XmlObject[] { XmlObject.Factory
434 .parse(SoapUI.class.getResource("/soapEnvelope.xsd")) });
435
436 SchemaType envelopeType = schema.findDocumentType(new QName(
437 "http://schemas.xmlsoap.org/soap/envelope/", "Envelope"));
438
439 XmlOptions xmlOptions = new XmlOptions();
440 xmlOptions.setLoadLineNumbers();
441 XmlObject xmlObject = schema.parse(soapMessage, envelopeType,
442 xmlOptions);
443 xmlOptions.setErrorListener(errors);
444 xmlObject.validate(xmlOptions);
445 }
446 catch ( XmlException e )
447 {
448 errors.addAll( e.getErrors() );
449 }
450 catch (Exception e)
451 {
452 errors.add( XmlError.forMessage( e.getMessage() ));
453 }
454 }
455 }