/**************************************************************************** | |
* | |
* Copyright (c) 2008-2011, EBM WebSourcing | |
* | |
* This source code is available under agreement available at | |
* http://www.petalslink.com/legal/licenses/petals-studio | |
* | |
* You should have received a copy of the agreement along with this program. | |
* If not, write to EBM WebSourcing (4, rue Amelie - 31200 Toulouse, France). | |
* | |
*****************************************************************************/ | |
package org.eclipse.bpel.common.wsdl.importhelpers; | |
import java.io.ByteArrayInputStream; | |
import java.io.ByteArrayOutputStream; | |
import java.io.File; | |
import java.io.FileOutputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.OutputStream; | |
import java.io.UnsupportedEncodingException; | |
import java.net.MalformedURLException; | |
import java.net.URI; | |
import java.net.URISyntaxException; | |
import java.net.URLConnection; | |
import java.util.ArrayList; | |
import java.util.HashMap; | |
import java.util.HashSet; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Set; | |
import java.util.regex.Matcher; | |
import java.util.regex.Pattern; | |
import javax.xml.parsers.DocumentBuilder; | |
import javax.xml.parsers.DocumentBuilderFactory; | |
import javax.xml.parsers.ParserConfigurationException; | |
import org.eclipse.bpel.common.wsdl.SoaWsdlCommonPlugin; | |
import org.eclipse.bpel.common.wsdl.helpers.UriAndUrlHelper; | |
import org.eclipse.core.runtime.IPath; | |
import org.eclipse.core.runtime.IStatus; | |
import org.eclipse.core.runtime.Path; | |
import org.w3c.dom.Document; | |
import org.w3c.dom.Element; | |
import org.w3c.dom.NodeList; | |
import org.xml.sax.SAXException; | |
/** | |
* A class that can import a WSDL (1.1 and 2.0) and all its dependencies (XSD, XML, WSDL). | |
* @author Vincent Zurczak - EBM WebSourcing | |
*/ | |
public class WsdlImportHelper { | |
public final static String RELOCATED_DIRECTORY = "relocated"; | |
public final static String WSDL20_NAMESPACE = "http://www.w3.org/ns/wsdl"; | |
public final static String WSDL11_NAMESPACE = "http://schemas.xmlsoap.org/wsdl/"; | |
public final static String SCHEMA_NAMESPACE = "http://www.w3.org/2001/XMLSchema"; | |
public final static String XSI_NAMESPACE = "http://http://www.w3.org/2001/XMLSchema-instance"; | |
private final DocumentBuilder documentBuilder; | |
/** | |
* Constructor. | |
* @throws ParserConfigurationException if a parser could not be created | |
*/ | |
public WsdlImportHelper() throws ParserConfigurationException { | |
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); | |
factory.setNamespaceAware( true ); | |
this.documentBuilder = factory.newDocumentBuilder(); | |
} | |
/** | |
* Imports a WSDL or a XSD will all its dependencies. | |
* @param targetDirectory | |
* @param wsdlUris | |
* @return | |
* @throws IOException if a resource could not be read or imported | |
* @throws IllegalArgumentException if an import could not be resolved into an URI | |
* @throws URISyntaxException if an import could not be resolved into an URI | |
* @throws SAXException if an import could not be parsed | |
*/ | |
public Map<String,File> importWsdlOrXsdAndDependencies( File targetDirectory, String... wsdlUris ) | |
throws IllegalArgumentException, IOException, URISyntaxException, SAXException { | |
// Analyze the resources | |
Map<String,WsdlImportBean> resourceUriToWIBean = new HashMap<String,WsdlImportBean> (); | |
analyzeResources( resourceUriToWIBean, null, wsdlUris ); | |
// Now, we have to find the target path and solve conflicts in target files | |
Set<File> targetFiles = new HashSet<File> (); | |
IPath rootPath = new Path( targetDirectory.getAbsolutePath()); | |
for( Map.Entry<String,WsdlImportBean> entry : resourceUriToWIBean.entrySet()) { | |
// Get the real relative file path | |
String relativeFilePath = entry.getValue().getRelativePathToTargetDirectory(); | |
if( relativeFilePath.length() == 0 ) | |
relativeFilePath = UriAndUrlHelper.extractFileName( entry.getKey()); | |
// Imported files which are above the target directory will be put inside a specific directory | |
IPath path = rootPath.append( relativeFilePath ); | |
if( ! rootPath.isPrefixOf( path )) | |
relativeFilePath = RELOCATED_DIRECTORY + "/" + UriAndUrlHelper.extractFileName( entry.getKey()); | |
// Prevent conflicts with existing files and files to be written | |
File targetFile; | |
String newRelativeFilePath = relativeFilePath; | |
int cpt = 0; | |
while(( targetFile = new File( targetDirectory, newRelativeFilePath )).exists() | |
|| targetFiles.contains( targetFile )) { | |
newRelativeFilePath = insertSuffixBeforeFileExtension( relativeFilePath, "_" + cpt ); | |
cpt ++; | |
} | |
entry.getValue().setRelativePathToTargetDirectory( newRelativeFilePath ); | |
targetFiles.add( targetFile ); | |
} | |
// Last steps: | |
// Resolve the final target files | |
// And update the import declarations in the files | |
// And import the file | |
Map<String,File> originUriToTargetFile = new HashMap<String,File> (); | |
for( WsdlImportBean bean : resourceUriToWIBean.values()) { | |
File targetFile = new File( targetDirectory, bean.getRelativePathToTargetDirectory()); | |
URI uri = UriAndUrlHelper.urlToUri( bean.getOriginUri()); | |
StringBuilder fileContent = new StringBuilder( readResourceContent( uri )); | |
for( Map.Entry<String,String> entry : bean.getImportDeclarationToImportFullUri().entrySet()) { | |
WsdlImportBean importBean = resourceUriToWIBean.get( entry.getValue()); | |
File importFile = new File( targetDirectory, importBean.getRelativePathToTargetDirectory()); | |
String relLoc = getRelativeLocationToFile( targetFile, importFile ); | |
// Make the replacement - XSD imports are more complicated | |
String pattern = "(\\s|\")" + Pattern.quote( entry.getKey()) + "(\\s|\")"; | |
Matcher matcher = Pattern.compile( pattern, Pattern.MULTILINE | Pattern.DOTALL ).matcher( fileContent.toString()); | |
while( matcher.find()) { | |
int start = matcher.start() + 1; // Skip the white space or double quote | |
int end = matcher.end() - 1; // Skip the white space or double quote | |
fileContent.replace( start, end, relLoc ); | |
} | |
} | |
if( ! targetFile.getParentFile().exists() | |
&& ! targetFile.getParentFile().mkdirs()) | |
throw new IOException( "Could not create " + targetFile.getParentFile()); | |
OutputStream os = new FileOutputStream( targetFile ); | |
ByteArrayInputStream is; | |
try { | |
is = new ByteArrayInputStream( fileContent.toString().getBytes( "UTF-8" )); | |
} catch( UnsupportedEncodingException e ) { | |
is = new ByteArrayInputStream( fileContent.toString().getBytes()); | |
} | |
copyStream( is, os ); | |
os.close (); | |
originUriToTargetFile.put( bean.getOriginUri(), targetFile ); | |
} | |
return originUriToTargetFile; | |
} | |
/** | |
* Analyzes a set of resources and gets all the import declarations. | |
* @param wsdlUris a set of URIs to WSDL or XML schemas | |
* @param referentUri the URI to use to determine the relative location of the target file | |
* @param resourceUriToWIBean a map (key = resource full URI, value = {@link WsdlImportBean}) | |
* @throws URISyntaxException if an import could not be resolved to an URI | |
* @throws SAXException if an import could not be parsed | |
* @throws IOException if an import could not be read | |
*/ | |
private void analyzeResources( Map<String,WsdlImportBean> resourceUriToWIBean, URI referentUri, String... wsdlUris ) | |
throws URISyntaxException, IOException, SAXException { | |
for( String wsdlUri : wsdlUris ) { | |
// Original URI: add it to the root | |
if( ! resourceUriToWIBean.containsKey( wsdlUri )) { | |
WsdlImportBean bean = new WsdlImportBean(); | |
bean.setOriginUri( wsdlUri ); | |
bean.setRelativePathToTargetDirectory( "" ); | |
resourceUriToWIBean.put( wsdlUri, bean ); | |
} | |
// Now, parse it and get its dependencies | |
WsdlImportBean bean = resourceUriToWIBean.get( wsdlUri ); | |
bean.setAnalyzed( true ); | |
URI uri = UriAndUrlHelper.urlToUri( wsdlUri ); | |
referentUri = referentUri != null ? referentUri : uri; | |
Set<String> brutImports = findImports( uri ); | |
for( String brutImport : brutImports ) { | |
// Absolute URI: we will put them at the same level than the original URI | |
URI importFullUri = null; | |
String importFullUriAsString = null; | |
if( UriAndUrlHelper.isAbsoluteUri( brutImport )) | |
importFullUriAsString = brutImport; | |
else { | |
importFullUri = UriAndUrlHelper.buildNewURI( uri, brutImport ); | |
importFullUriAsString = importFullUri.toString(); | |
} | |
// Keep a trace of the import declaration | |
bean.registerImport( brutImport, importFullUriAsString ); | |
// Remember the import for further processing | |
if( ! resourceUriToWIBean.containsKey( importFullUriAsString )) { | |
WsdlImportBean childBean = new WsdlImportBean(); | |
childBean.setOriginUri( importFullUriAsString ); | |
URI relativeUri = UriAndUrlHelper.getRelativeLocationToUri( referentUri, importFullUri ); | |
String relPath = relativeUri.toString(); | |
if( UriAndUrlHelper.isAbsoluteUri( relPath )) | |
relPath = ""; | |
childBean.setRelativePathToTargetDirectory( relPath ); | |
resourceUriToWIBean.put( importFullUriAsString, childBean ); | |
} | |
// And we need to process recursively all the new imports | |
WsdlImportBean childBean = resourceUriToWIBean.get( importFullUriAsString ); | |
if( ! childBean.wasAnalyzed()) | |
analyzeResources( resourceUriToWIBean, referentUri, importFullUriAsString ); | |
} | |
} | |
} | |
/** | |
* Finds all the import declarations in a given document. | |
* @param referencerUri the URI of the XML file | |
* @return a non-list of import declarations (as found in the document) | |
* @throws IOException if an import could not be read | |
* @throws SAXException if an import could not be parsed | |
*/ | |
private Set<String> findImports( final URI referencerUri ) throws IOException, SAXException { | |
Set<String> result = new HashSet<String> (); | |
// Parse the XML document | |
URLConnection connection = referencerUri.toURL().openConnection(); | |
InputStream is = connection.getInputStream(); | |
Document referencerDocument = this.documentBuilder.parse( is ); | |
// Check the root element is a valid one... | |
Element docRoot = referencerDocument.getDocumentElement(); | |
String rootNs = docRoot.getNamespaceURI(); | |
String rootName = docRoot.getLocalName(); | |
int index = rootName.lastIndexOf( ':' ); | |
if( index != -1 ) | |
rootName = rootName.substring( ++ index ); | |
// ... and if so, get its import declarations | |
if( "definitions".equals( rootName ) && WSDL11_NAMESPACE.equals( rootNs ) | |
|| "description".equals( rootName ) && WSDL20_NAMESPACE.equals( rootNs ) | |
|| "schema".equals( rootName ) && SCHEMA_NAMESPACE.equals( rootNs )) { | |
result.addAll( findImports( docRoot )); | |
} | |
return result; | |
} | |
/** | |
* Gets all the import declarations from a XML document (XSD, WSDL). | |
* @param docRoot the root element | |
* @return a non-null list of import locations, as defined in the document | |
*/ | |
private Set<String> findImports( Element docRoot ) { | |
// Get the imports | |
List<NodeList> importElements = new ArrayList<NodeList> (); | |
importElements.add( docRoot.getElementsByTagNameNS( WSDL11_NAMESPACE, "import" )); | |
importElements.add( docRoot.getElementsByTagNameNS( WSDL20_NAMESPACE, "import" )); | |
importElements.add( docRoot.getElementsByTagNameNS( WSDL20_NAMESPACE, "include" )); | |
importElements.add( docRoot.getElementsByTagNameNS( SCHEMA_NAMESPACE, "import" )); | |
importElements.add( docRoot.getElementsByTagNameNS( SCHEMA_NAMESPACE, "include" )); | |
// Now, get the import locations | |
Set<String> importLocations = new HashSet<String> (); | |
for( NodeList nodeList : importElements ) { | |
for( int i=0; i<nodeList.getLength(); i++ ) { | |
Element importElement = (Element) nodeList.item( i ); | |
String location = importElement.hasAttribute( "location" ) ? importElement.getAttribute( "location" ) : null; | |
if( location == null ) { | |
location = importElement.getAttribute( "schemaLocation" ); | |
int index = location.lastIndexOf( ' ' ); | |
if( index != -1 ) | |
location = location.substring( ++ index ); | |
} | |
if( location != null ) | |
importLocations.add( location ); | |
else | |
SoaWsdlCommonPlugin.log( "An import location could not be found.", IStatus.WARNING ); | |
} | |
} | |
return importLocations; | |
} | |
/** | |
* Return the relative position of <code>file</code> with respect to originFile. | |
* <p> | |
* Legacy and more complete than {@link #getBasicRelativePath(File, File)}. | |
* </p> | |
* | |
* @param originFile the absolute file which acts as the <i>origin</i>. | |
* @param file the file whose relative path must be computed with respect to originFile. | |
* @return the relative path of <code>file</code> with respect to originFile. | |
* @see UriUtils#getRelativeLocationToUri(URI, URI) | |
*/ | |
private static String getRelativeLocationToFile( File originFile, File file ) { | |
String result = file.getAbsolutePath(); | |
try { | |
URI originUri = UriAndUrlHelper.urlToUri( originFile.toURI().toURL()); | |
URI fileUri = UriAndUrlHelper.urlToUri( file.toURI().toURL()); | |
result = UriAndUrlHelper.getRelativeLocationToUri( originUri, fileUri ).toString(); | |
} catch( MalformedURLException e ) { | |
SoaWsdlCommonPlugin.log( e, IStatus.ERROR ); | |
} | |
return result; | |
} | |
/** | |
* Reads the content of a resource and returns it as a string. | |
* <p> | |
* The content is tried to be read with UTF-8 encoding. | |
* If it fails, the default system encoding is used. | |
* </p> | |
* | |
* @param uri the URI of the resource whose content must be loaded | |
* @return the resource's content | |
* @throws IOException if the resource's content could not be read | |
*/ | |
public static String readResourceContent( URI uri ) throws IOException { | |
String result = null; | |
ByteArrayOutputStream os = new ByteArrayOutputStream(); | |
InputStream is = null; | |
try { | |
is = uri.toURL().openStream(); | |
copyStream( is, os ); | |
} finally { | |
if( is != null ) | |
is.close(); | |
} | |
try { | |
result = os.toString( "UTF-8" ); | |
} catch( UnsupportedEncodingException e ) { | |
result = os.toString(); | |
} | |
return result; | |
} | |
/** | |
* Copies the content from in into os. | |
* <p> | |
* Neither <i>in</i> nor <i>os</i> are closed by this method.<br /> | |
* They must be explicitly closed after this method is called. | |
* </p> | |
* | |
* @param in | |
* @param os | |
* @throws IOException | |
*/ | |
private static void copyStream( InputStream in, OutputStream os ) throws IOException { | |
byte[] buf = new byte[ 1024 ]; | |
int len; | |
while((len = in.read( buf )) > 0) { | |
os.write( buf, 0, len ); | |
} | |
} | |
/** | |
* Inserts a suffix before the file extension in a given string. | |
* @param string a string, file name or file path | |
* @param suffix a suffix to insert before the file extension | |
* @return a new string | |
*/ | |
private static String insertSuffixBeforeFileExtension( String string, String suffix ) { | |
int index = string.lastIndexOf( '.' ); | |
String result; | |
if( index == -1 ) | |
result = string + suffix; | |
else | |
result = string.substring( 0, index ) + suffix + string.substring( index ); | |
return result; | |
} | |
} |