/*******************************************************************************
 * Copyright (c) 2007, 2009 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * IBM Corporation - initial API and implementation
 * yyyymmdd bug      Email and other contact information
 * -------- -------- -----------------------------------------------------------
 * 20070125   171071 makandre@ca.ibm.com - Andrew Mak, Create public utility method for copying WSDL files
 * 20070409   181635 makandre@ca.ibm.com - Andrew Mak, WSDLCopier utility should create target folder
 * 20071205   211262 ericdp@ca.ibm.com - Eric Peters, CopyWSDLTreeCommand fails to copy ?wsdl
 * 20080324   215552 makandre@ca.ibm.com - Andrew Mak, WSDLCopier expects not encoded URI
 * 20080501   229728 makandre@ca.ibm.com - Andrew Mak, uppercase .WSDL cannot be found by the Web Service Client wizard
 * 20090128   262639 ericdp@ca.ibm.com - Eric D. Peters, WSDLCopier gives NPE in projects with spaces
 * 20090204   262913 ericdp@ca.ibm.com - Eric D. Peters, Top-down WS fails when .wsdl imports .xsd or .wsdl which resides in folder containing URI encoded chars like " ", ")"
 *******************************************************************************/

package org.eclipse.wst.ws.internal.util;

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.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.wsdl.Definition;
import javax.wsdl.Import;
import javax.wsdl.Types;
import javax.wsdl.WSDLException;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.extensions.UnknownExtensibilityElement;
import javax.wsdl.factory.WSDLFactory;
import javax.wsdl.xml.WSDLWriter;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.osgi.util.NLS;
import org.eclipse.wst.command.internal.env.core.common.StatusUtils;
import org.eclipse.wst.common.environment.EnvironmentService;
import org.eclipse.wst.common.environment.uri.IURIScheme;
import org.eclipse.wst.common.environment.uri.SimpleURIFactory;
import org.eclipse.wst.common.environment.uri.URIException;
import org.eclipse.wst.common.uriresolver.internal.util.URIEncoder;
import org.eclipse.wst.ws.internal.WstWSPluginMessages;
import org.eclipse.wst.ws.internal.parser.discovery.NetUtils;
import org.eclipse.wst.ws.internal.parser.wsil.WWWAuthenticationException;
import org.eclipse.wst.ws.internal.parser.wsil.WebServicesParser;
import org.eclipse.wst.wsdl.XSDSchemaExtensibilityElement;
import org.eclipse.wst.wsdl.internal.impl.wsdl4j.WSDLFactoryImpl;
import org.eclipse.xsd.XSDSchema;
import org.eclipse.xsd.XSDSchemaDirective;
import org.eclipse.xsd.impl.XSDSchemaImpl;
import org.eclipse.xsd.util.XSDParser;
import org.w3c.dom.Element;

/**
 * <p>This is a utility class that can be used to copy a WSDL file and its relative imports
 * (both wsdl:import and xsd:import) to a folder in the filesystem.  The code will analyze
 * the directory structure and does the following:
 * <ol>
 *    <li>find all files that needs to be copied and tag them as such.</li>
 *    <li>find the common parent directory of all the files that are tagged; this is the 
 *    root of the directory structure.</li>
 * </ol>
 * During the copy, the root of the directory structure is be mapped to the 
 * specified target folder and all tagged files will be duplicated under it.</p>
 * 
 * <p>If the WSDL source and the target location are the same, this utility only guarantees
 * that the starting WSDL file is "re-copied" onto itself (since the WSDL definition could 
 * have been altered).  No guarantee is made for the other files that the WSDL file imports.</p>
 * 
 * <p>Note that WSDLCopier does not do a direct file copy.  The source files are read into
 * a model and then written out to the target folder, hence the output file might not be
 * syntactically identical to the source file but they will be structurally identical.</p>
 * 
 * <p>Since this utility implements IWorkspaceRunnable, it can be run by the workspace as
 * an atomic workspace operation.</p>
 */
public class WSDLCopier implements IWorkspaceRunnable {

	private static final String XSD = ".xsd";
	
	/**
	 * Helper class holding information about the files we need to copy
	 */
	private class XMLObjectInfo {
	  	
		public IPath  path;
	  	public Object content;
	  	  	
	  	/**
	  	 * Constructor.
	  	 * 
	  	 * @param path The path of the source file.
	  	 * @param content A representation of the source file (either wsdl or xsd)
	  	 */
	  	public XMLObjectInfo(IPath path, Object content) {
	  		this.path    = path.setDevice(null); // we want a device independent path  		
	  		this.content = content;
	  	}
	}	  
	
	private WebServicesParser parser	= null;					// parser to parse the wsdl and xsd files
	
	private URI sourceURI				= URI.create("");		// uri of the starting wsdl
	private Definition definition		= null;					// representation of the starting wsdl
	
	private URI targetFolderURI			= null;					// uri of the folder to copy to
	private String targetFilename		= null;					// optional new filename for the starting wsdl
	
	private IPath pathPrefix			= null;  				// the shortest common path of all files we need to copy
	private IPath wsdlRelPath			= null;					// relative path of the starting wsdl to the target folder
		
	private Map xmlObjectInfos			= new LinkedHashMap();	// table to store info about the files we need to copy
	
	private SimpleURIFactory uriFactory = null;
	
	/**
	 * Default constructor.  A new WebServicesParser is constructed for use.
	 */
	public WSDLCopier() {
		this(null);
	}
	
	/**
	 * Constructor.  The given WebServicesParser will be used.
	 * 
	 * @param parser A WebServicesParser for WSDLCopier to use.  If null, a new one will be constructed.
	 */
	public WSDLCopier(WebServicesParser parser) {
		if (parser == null)
			this.parser = new WebServicesParser();
		else
			this.parser = parser;
		
		IURIScheme eclipseScheme = EnvironmentService.getEclipseScheme();
	    IURIScheme fileScheme    = EnvironmentService.getFileScheme();
	    uriFactory = new SimpleURIFactory();
	    uriFactory.registerScheme("platform", eclipseScheme);
	    uriFactory.registerScheme("file", fileScheme);
	}

	/**
	 * Sets the URI of the starting wsdl document.  The URI must be an absolute URI
	 * with a valid protocol.  For local files, both file:/ and platform:/resource 
	 * protocol are acceptable. 
	 * 
	 * @param uri The URI of the starting wsdl document.
	 * 
	 * @deprecated
	 */
	public void setSourceURI(String uri) {
		setSourceURI(uri, null);
	}
	
	/**
	 * Sets the URI of the starting wsdl document.  The URI must be an absolute URI
	 * with a valid protocol.  For local files, both file:/ and platform:/resource 
	 * protocol are acceptable. 
	 * 
	 * @param uri The URI of the starting wsdl document. 
	 */
	public void setSourceURI(URI uri) {
		setSourceURI(uri, null);
	}
	
	/**
	 * Same as setSourceURI(String uri) version, except that an already parsed
	 * representation of the wsdl document can be passed to this method to avoid
	 * having to parse it a second time.
	 * 
	 * @param uri The URI of the starting wsdl document.
	 * @param definition A parsed representation of the starting wsdl document.
	 * 
	 * @deprecated
	 */
	public void setSourceURI(String uri, Definition definition) {
		if (uri != null)
			uri = uri.replace('\\', '/');
		URI uriObj = null;		
		try {
			try {
				uriObj = new URI(uri);
			}
			catch (URISyntaxException e) {				
				uriObj = URI.create(URIEncoder.encode(uri, "UTF-8"));
			}
			setSourceURI(uriObj, definition);		
		}
		catch (UnsupportedEncodingException e) {
			// ignore, default URI("") is used, operation will be a no-op
		}		
	}
	
	/**
	 * Same as setSourceURI(String uri) version, except that an already parsed
	 * representation of the wsdl document can be passed to this method to avoid
	 * having to parse it a second time.
	 * 
	 * @param uri The URI of the starting wsdl document.
	 * @param definition A parsed representation of the starting wsdl document. 
	 */
	public void setSourceURI(URI uri, Definition definition) {
		sourceURI = uri;
		this.definition = definition;
	}
	
	/**
	 * Specify the target folder URI for WSDLCopier to copy the wsdl to.  The URI must be
	 * an absolute URI with a valid protocol.  Both file:/ and platform:/resource protocol
	 * are acceptable.  The entire directory structure with all the files that the wsdl 
	 * imports will be duplicated under this folder.  If the target folder does not exist, 
	 * it will be created. 
	 * 
	 * @param uri The target folder URI where the wsdl structure is copied to.
	 * 
	 * @deprecated
	 */
	public void setTargetFolderURI(String uri) {
		if (uri != null)
			uri = uri.replace('\\', '/');
		URI uriObj = null;		
		try {
			try {
				uriObj = new URI(uri);
			}
			catch (URISyntaxException e) {				
				uriObj = URI.create(URIEncoder.encode(uri, "UTF-8"));
			}
			setTargetFolderURI(uriObj);		
		}
		catch (UnsupportedEncodingException e) {
			// ignore, targetFolderURI will be checked for null later
		}		
	}
	
	/**
	 * Specify the target folder URI for WSDLCopier to copy the wsdl to.  The URI must be
	 * an absolute URI with a valid protocol.  Both file:/ and platform:/resource protocol
	 * are acceptable.  The entire directory structure with all the files that the wsdl 
	 * imports will be duplicated under this folder.  If the target folder does not exist, 
	 * it will be created. 
	 * 
	 * @param uri The target folder URI where the wsdl structure is copied to. 
	 */
	public void setTargetFolderURI(URI uri) {		
		targetFolderURI = uri;
	}
	
	/**
	 * Optionally set a new filename for the starting wsdl document to use after it is
	 * copied.  If not specified, the original name of the wsdl is used.
	 * 
	 * @param filename A filename.
	 */
	public void setTargetFilename(String filename) {
		targetFilename = filename;
	} 
	
	/*
	 * Compare the path prefix with the path in the info object,
	 * modify the path prefix accordingly.
	 */
	private void updatePathPrefix(XMLObjectInfo info) {
	  	if (pathPrefix == null)
	        pathPrefix = info.path.removeLastSegments(1);
	    else {
	    	int matching = pathPrefix.matchingFirstSegments(info.path);
	    	
	    	if (matching < pathPrefix.segmentCount())
	    		pathPrefix = pathPrefix.uptoSegment(matching);
	    }
	}    	
	
	/*
	 * Determine if the given URI is a relative URI. 
	 */
	private boolean isRelative(String uri) {
		return uri.indexOf(':') == -1;
	}

	/*
	 * Analyze the wsdl document at the given URI, and traverse any relative files that
	 * it imports. Can optionally pass in a parsed Definition if one's available so
	 * we don't have to parse the wsdl again (otherwise just pass in null). 
	 */
	private void analyzeWSDL(URI uri, Definition definition) throws 
		MalformedURLException, IOException, WSDLException, WWWAuthenticationException {
		
		uri = uri.normalize();
		
		// already seen this wsdl, skip
		if (xmlObjectInfos.containsKey(uri.toString()))
			return;
				
		// need to parse the wsdl ourselves
		if (definition == null)
			definition = parser.getWSDLDefinitionVerbose(uri.toString());
		
		// save a reference to the starting wsdl
		if (this.definition == null)
			this.definition = definition;
		
		IPath path = new Path(uri.getPath());
		
		// a target filename was given, so we need to modify the path with the new name
		if (definition == this.definition && targetFilename != null)
			path = path.removeLastSegments(1).append(targetFilename);
		 		
	    XMLObjectInfo info = new XMLObjectInfo(path, definition);	    
	    xmlObjectInfos.put(uri.toString(), info);
	    updatePathPrefix(info);
		
	    // now look at wsdl imports
	    
	    for (Iterator it = definition.getImports().values().iterator(); it.hasNext();) {

	    	List list = (List) it.next();
	      
	    	for (Iterator listIt = list.iterator(); listIt.hasNext();) {
	    		
		        Import wsdlImport = (Import) listIt.next();
		        String wsdlImportLocation = wsdlImport.getLocationURI();

		        // analyze any relative imports we find
		        if (wsdlImportLocation != null && isRelative(wsdlImportLocation)) {

		        	// bad form, importing xsd with wsdl:import, but need to handle
		        	if (wsdlImportLocation.toLowerCase().endsWith(XSD))
		        		analyzeXSD(uri.resolve(uriCreate(wsdlImportLocation)));
		        	else	
		        		analyzeWSDL(uri.resolve(uriCreate(wsdlImportLocation)), null);
		        }
	    	}
	    }
	    
	    // now look at xsd imports

	    Types types = definition.getTypes();
	    
	    // there's no wsdl:types, we're done
	    if (types == null)
	    	return;
	        	
    	for (Iterator it = types.getExtensibilityElements().iterator(); it.hasNext();) {			
			
    		ExtensibilityElement extElement = (ExtensibilityElement) it.next();    		
    		Element element;
    		
    		// we'll try to parse any UnknownExtensibilityElements and
    		// XSDSchemaExtensibilityElements into an XSD schema    		
    		
    		if (extElement instanceof UnknownExtensibilityElement)
    			element = ((UnknownExtensibilityElement) extElement).getElement();
   			else if (extElement instanceof XSDSchemaExtensibilityElement)
   				element = ((XSDSchemaExtensibilityElement) extElement).getElement();
   			else
   				continue;
    			    			
			try {
				XSDSchema xsdSchema = XSDSchemaImpl.createSchema(element);
				
				// analyze the inlined schema at the current uri
				analyzeXSD(uri, xsdSchema);
			}
			catch (Exception t) {
				// ignore any extensibility elements that cannot be parsed into a
				// XSDSchema instance
			}
        }
	}
	
	/*
	 * Analyze the schema at the given URI, traverse its imports and includes.
	 * The schema information is not stored in XMLObjectInfos because it's
	 * either not neccessary (schema inlined in a wsdl) or it has already
	 * been stored.
	 */
	private void analyzeXSD(URI uri, XSDSchema schema) {
		
		if (schema == null)
			return;
		
		// look at the imports and includes
	    
		for (Iterator it = schema.getContents().iterator(); it.hasNext();) {
	        
			Object content = it.next();
	        
			if (!(content instanceof XSDSchemaDirective))
				continue;
					        
			XSDSchemaDirective xsdSchemaDirective = (XSDSchemaDirective) content;
			String xsdSchemaDirectiveLocation = xsdSchemaDirective.getSchemaLocation();
	          
			// analyze any relative imports and includes we find
			if (xsdSchemaDirectiveLocation != null && isRelative(xsdSchemaDirectiveLocation))
				try {
					analyzeXSD(uri.resolve(uriCreate(xsdSchemaDirectiveLocation)));
				} catch (Exception e) {
					// ignore any xsd's we cannot resolve
				}
		}
	}
	
	/*
	 * Analyze the schema at the given URI, the schema is parsed and stored in
	 * XMLObjectInfos.
	 */
	private void analyzeXSD(URI uri) {
		
		uri = uri.normalize();
		
		// already seen this xsd, skip it
		if (xmlObjectInfos.containsKey(uri.toString()))
			return;
		
		XSDSchema xsdSchema = XSDSchemaImpl.getSchemaForSchema(uri.toString());

		// if schema is not cached, parse it
	    if (xsdSchema == null) {
	    	XSDParser p = new XSDParser(null);
	    	InputStream is = NetUtils.getURLInputStream(uri.toString());
	      
	    	if (is != null) {
	    		p.parse(is);
	    		xsdSchema = p.getSchema();
	    	}
	    }
	    
	    if (xsdSchema != null) {
	        
	    	XMLObjectInfo info = new XMLObjectInfo(new Path(uri.getPath()), xsdSchema);	    	
	    	xmlObjectInfos.put(uri.toString(), info);
	    	updatePathPrefix(info);
	      
	    	// analyze its imports and includes
	    	analyzeXSD(uri, xsdSchema);
	    }
	}
		
	/*
	 * Appends a path to a URI and returns the new combined URI in an array of 2 elements,
	 * where the first element is in decoded form and the second element is in encoded form.
	 */
	private String[] appendPathToURI(URI uriObj, IPath path) {
		
		String schemeSpecificPart = uriObj.getSchemeSpecificPart(); 
		
		if (schemeSpecificPart.endsWith("/"))
			path = path.makeRelative();
		else
			path = path.makeAbsolute();
		
		return new String[] {
				uriObj.getScheme() + ':' + schemeSpecificPart + path,
				uriObj.getScheme() + ':' + uriObj.getRawSchemeSpecificPart() + path
		};
	}
	
	/*
	 * Writes the wsdl definition to the file at the given path, relative to the target folder.
	 */
	private String writeXMLObj(IPath path, Definition definition, IProgressMonitor monitor) throws
		WSDLException, URIException, IOException, CoreException {				
			  
	    WSDLFactory wsdlFactory = new WSDLFactoryImpl();	    
	    WSDLWriter wsdlWriter = wsdlFactory.newWSDLWriter();
	    
	    String[] targetURI = appendPathToURI(targetFolderURI, path);
	    
	    OutputStream os = uriFactory.newURI(targetURI[0]).getOutputStream();	    
	    wsdlWriter.writeWSDL(definition, os);
	    os.close();
	    
	    return targetURI[1];
	}
  
	/*
	 * Writes the xsd schema to the file at the given path, relative to the target folder.
	 */
	private String writeXMLObj(IPath path, XSDSchema xsdSchema, IProgressMonitor monitor) throws
		TransformerConfigurationException, TransformerException, URIException, IOException, CoreException {			
		
		Transformer serializer = TransformerFactory.newInstance().newTransformer();
		serializer.setOutputProperty(OutputKeys.INDENT, "yes");
		serializer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
		serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
		
		Element e = xsdSchema.getElement();
		DOMSource domSource = new DOMSource(e);		
		
		String[] targetURI = appendPathToURI(targetFolderURI, path);
		
		OutputStream os = uriFactory.newURI(targetURI[0]).getOutputStream();
		serializer.transform(domSource, new StreamResult(os));
		os.close();
		
		return targetURI[1];
	}  

	/*
	 * Compares the 2 uris and see if they point to the same file. 
	 * We need to convert both uris to file: protocal uris in order to compare.
	 */	
	private boolean isSameLocation(String uri1, String uri2) {

		// if either uri is null, we cannot make any meaningful comparison
		if (uri1 == null || uri2 == null)
			return false;
		try {
			//we have an encoded URI and want to compare if they resolve to the same file on the file system
			uri1 = UniversalPathTransformer.uriToLocation(uri1);
			uri2 = UniversalPathTransformer.uriToLocation(uri2);
			if (uri1 == null || uri2 == null || uri1.length() == 0 || uri2.length() == 0)
				return false;
			return uri1.equals(uri2);
		} catch (Exception e) {
			return false;
		}
	}
	
	/**
	 * Executes the copying action.
	 * 
	 * @param monitor An optional progress monitor.
	 * @throws CoreException Thrown if the copying is unsuccessful.  Possible causes include:
	 * target folder URI was not specified; source URI has incorrect format; problem parsing wsdl 
	 * or xsd documents; problem writing to filesystem.
	 */
	public void run(IProgressMonitor monitor) throws CoreException {
		
		xmlObjectInfos.clear();

		// target folder must be set
		if (targetFolderURI == null)
			throw new CoreException(StatusUtils.errorStatus(WstWSPluginMessages.MSG_ERROR_TARGET_FOLDER_NOT_SPECIFIED));		
				
  		try {
  			analyzeWSDL(sourceURI, definition);  			 
  			
  			// begin writing out files
  			
  			Iterator iter = xmlObjectInfos.values().iterator(); 			  			
  			
  			while (iter.hasNext()) {
  				XMLObjectInfo info = (XMLObjectInfo) iter.next();  				
  				IPath relPath = info.path.removeFirstSegments(pathPrefix.segmentCount());
  				
  				if (info.content instanceof Definition) {
  					Definition definition = (Definition) info.content;
  					
  					// if this is the starting wsdl, remember its path relative to the target folder
  					if (definition == this.definition)
  						wsdlRelPath = relPath;

  					String targetURI = writeXMLObj(relPath, definition, monitor);
  					
  					if (definition == this.definition && isSameLocation(sourceURI.toString(), targetURI))
  						return;
  				}
  				else
  					writeXMLObj(relPath, (XSDSchema) info.content, monitor);  					
  			}  			
  		} catch (Exception t) {
  			throw new CoreException(StatusUtils.errorStatus(
  					NLS.bind(WstWSPluginMessages.MSG_ERROR_COPY_WSDL, new String[] {sourceURI.toString(), targetFolderURI.toString()}), t));
  		}
	}
	
	/**
	 * Returns the path of the starting wsdl document, relative to the target folder after
	 * copying completes.
	 * 
	 * @return The relative path of the starting wsdl document.
	 */
	public IPath getRelativePath() {
		return wsdlRelPath;
	}

	private static java.net.URI uriCreate(String uri) {
		if (uri != null)
			uri = uri.replace('\\', '/');
		try {
			try {
				return java.net.URI.create(uri);
			}
			catch (IllegalArgumentException e) {				
				return java.net.URI.create(URIEncoder.encode(uri, "UTF-8"));
			}
		}
		catch (UnsupportedEncodingException e) {
			return java.net.URI.create("");
		}	
	}
}
