View Javadoc

1   /*
2    *  soapui, copyright (C) 2005 Ole Matzura / eviware.com 
3    *
4    *  SoapUI is free software; you can redistribute it and/or modify it under the 
5    *  terms of the GNU Lesser General Public License as published by the Free Software Foundation; 
6    *  either version 2.1 of the License, or (at your option) any later version.
7    *
8    *  SoapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
9    *  even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
10   *  See the GNU Lesser General Public License for more details at gnu.org.
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       	// just check for correct message element, other elements are avoided (should create an error)
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                //XmlObject obj = paths[0].copy().changeType( type );
283                //obj.validate( new XmlOptions().setErrorListener( errors ));
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 		// need to create new body element of correct type from xml text
295 		// since we want to retain line-numbers
296 		XmlObject obj = XmlObject.Factory.parse( msg.copy().changeType( type ).xmlText( new XmlOptions().setSaveOuter()), new XmlOptions().setLoadLineNumbers() );
297 		obj = obj.changeType( type );
298 		
299 		// create internal error list
300 		List list = new ArrayList();
301 		obj.validate( new XmlOptions().setErrorListener( list ));
302 		
303 		// transfer errors for "real" line numbers
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       // get root element
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          // this is not allowed by Basic Profile.. remove?
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 }