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