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
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       	// just check for correct message element, other elements are avoided (should create an error)
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                //XmlObject obj = paths[0].copy().changeType( type );
282                //obj.validate( new XmlOptions().setErrorListener( errors ));
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 		// need to create new body element of correct type from xml text
294 		// since we want to retain line-numbers
295 		XmlObject obj = XmlObject.Factory.parse( msg.copy().changeType( type ).xmlText( new XmlOptions().setSaveOuter()), new XmlOptions().setLoadLineNumbers() );
296 		obj = obj.changeType( type );
297 		
298 		// create internal error list
299 		List list = new ArrayList();
300 		obj.validate( new XmlOptions().setErrorListener( list ));
301 		
302 		// transfer errors for "real" line numbers
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       // get root element
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                   //XmlObject obj = children[0].copy().changeType( type );
361                   //obj.validate( new XmlOptions().setErrorListener( errors ));
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 }