/****************************************************************************** * Copyright (c) 2010-2013, Linagora * * 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: * Linagora - initial API and implementation *******************************************************************************/ package com.ebmwebsourcing.petals.common.internal.provisional.utils; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.PrintWriter; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IncrementalProjectBuilder; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.jdt.core.IAnnotation; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IMemberValuePair; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.search.IJavaSearchConstants; import org.eclipse.jdt.core.search.SearchEngine; import org.eclipse.jdt.core.search.SearchMatch; import org.eclipse.jdt.core.search.SearchParticipant; import org.eclipse.jdt.core.search.SearchPattern; import org.eclipse.jdt.core.search.SearchRequestor; import org.eclipse.jdt.launching.IVMInstall; import org.eclipse.jdt.launching.IVMInstallType; import org.eclipse.jdt.launching.JavaRuntime; import com.ebmwebsourcing.petals.common.internal.PetalsCommonPlugin; import com.ebmwebsourcing.petals.common.internal.provisional.preferences.PreferencesManager; /** * Utility methods related to JAX-WS. * @author Vincent Zurczak - EBM WebSourcing */ public final class JaxWsUtils { /** * A file to store the generated classes. */ private static final File TEMP_FILE = new File( System.getProperty( "java.io.tmpdir" ), "Petals-JAX-WS" ); /** * The unique instance of this class. */ public static final JaxWsUtils INSTANCE = new JaxWsUtils(); /** * Constructor. */ private JaxWsUtils() { // nothing } /** * Gets a JAX-WS executable, that comes with a JDK 6. * <p> * This method can be used to check that there is an available JDK 6 * that will be used by the tooling. * </p> * * @param wsGen true to get WsGen, false to get WsImport * @return the required executable file * @throws IOException if the executable was not found */ public static File getJavaExecutable( boolean wsGen ) throws IOException { final String winExecName; final String linuxExecName; if( wsGen ) { winExecName = "bin/wsgen.exe"; linuxExecName = "bin/wsgen"; } else { winExecName = "bin/wsimport.exe"; linuxExecName = "bin/wsimport"; } File result = null; List<String> locations = new ArrayList<String> (); // Add all the locations where to search // Use the environment variables String javaHome = System.getProperty( "java.home" ); if( javaHome != null ) locations.add( javaHome ); javaHome = System.getenv( "JAVA_HOME" ); if( javaHome != null ) locations.add( javaHome ); // Use the registered VM IVMInstallType[] vmInstallTypes = JavaRuntime.getVMInstallTypes(); for( IVMInstallType vmInstallType : vmInstallTypes ) { IVMInstall[] vmInstalls = vmInstallType.getVMInstalls(); for( IVMInstall vmInstall : vmInstalls ) locations.add( vmInstall.getInstallLocation().getAbsolutePath()); } // Search the executables in these locations for( String location : locations ) { File executable; if(( executable = new File( location, winExecName )).exists()) result = executable; else if(( executable = new File( location, linuxExecName )).exists()) result = executable; if( result != null ) break; } if( result == null ) { if( wsGen ) throw new IOException( "Wsgen could not be found. Make sure you have a JDK 6 or higher." ); else throw new IOException( "Wsimport could not be found. Make sure you have a JDK 6 or higher." ); } return result; } /** * Generates a WSDL from a JAX-WS annotated class. * <p> * If an error occurs during the execution, it will appear in the returned * string builder. * </p> * * @param className the name of the annotated class * @param targetDirectory the target directory * @param jp the Java project that contains this class * @return a string builder containing the execution details * @throws IOException if WSgen was not found, or if the process could not be started * @throws InterruptedException * @throws JaxWsException if the generation seems to have failed */ public StringBuilder generateWsdl( String className, File targetDirectory, IJavaProject jp ) throws IOException, InterruptedException, JaxWsException { // Create the temporary file synchronized( TEMP_FILE ) { if( ! TEMP_FILE.exists() && ! TEMP_FILE.mkdir()) throw new IOException( "The class directories could not be created." ); } final StringBuilder outputBuffer = new StringBuilder(); int exitValue = 0; try { File executable = getJavaExecutable( true ); // Build the class path StringBuilder classpath = new StringBuilder(); String separator = System.getProperty( "path.separator" ); for( String entry : JavaUtils.getClasspath( jp, true, true )) classpath.append( entry + separator ); IPath binPath = null; try { binPath = jp.getOutputLocation(); } catch( JavaModelException e ) { PetalsCommonPlugin.log( e, IStatus.ERROR ); } if( binPath != null ) { binPath = ResourcesPlugin.getWorkspace().getRoot().getLocation().append( binPath ); File binFile = binPath.toFile(); if( ! binFile.exists() && ! binFile.mkdir()) PetalsCommonPlugin.log( "The 'bin' directory could not be created.", IStatus.WARNING ); } // Build the command line List<String> cmd = new ArrayList<String>(); cmd.add( executable.getAbsolutePath()); cmd.add( "-verbose" ); cmd.add( "-wsdl" ); cmd.add( "-classpath" ); cmd.add( classpath.toString()); cmd.add( "-r" ); cmd.add( targetDirectory.getAbsolutePath()); cmd.add( "-d" ); cmd.add( TEMP_FILE.getAbsolutePath()); cmd.add( "-s" ); cmd.add( TEMP_FILE.getAbsolutePath()); cmd.add( className ); // Create the process ProcessBuilder pb = new ProcessBuilder( cmd ); pb = pb.redirectErrorStream( true ); final Process process = pb.start(); // Monitor the execution Thread loggingThread = new Thread() { @Override public void run() { try { BufferedReader reader = new BufferedReader( new InputStreamReader( process.getInputStream())); String line = ""; try { while((line = reader.readLine()) != null ) outputBuffer.append( line + "\n" ); } finally { reader.close(); } } catch( IOException ioe ) { PetalsCommonPlugin.log( ioe, IStatus.ERROR ); } } }; loggingThread.start(); loggingThread.join(); exitValue = process.waitFor(); } finally { // Free the temporary directory synchronized( TEMP_FILE ) { IoUtils.deleteFilesRecursively( TEMP_FILE.listFiles()); } // Keep a trace of the generation JaxWsException loggingException; if( exitValue != 0 ) { loggingException = new JaxWsException( "The JAX-WS process did not return 0. An error may have occurred." ) { private static final long serialVersionUID = -8585870927871804985L; @Override public void printStackTrace( PrintStream s ) { s.print( outputBuffer.toString()); } @Override public void printStackTrace( PrintWriter s ) { s.print( outputBuffer.toString()); } }; throw loggingException; } else if( PreferencesManager.logAllJaxWsTraces()) { loggingException = new JaxWsException( "Logging the WSDL generation trace (" + jp.getProject().getName() + ")." ) { private static final long serialVersionUID = -5344307338271840633L; @Override public void printStackTrace( PrintStream s ) { s.print( outputBuffer.toString()); } @Override public void printStackTrace( PrintWriter s ) { s.print( outputBuffer.toString()); } }; PetalsCommonPlugin.log( loggingException, IStatus.INFO ); } } return outputBuffer; } /** * Generates a JAX-WS annotated client from a WSDL file. * <p> * If an error occurs during the execution, it will appear in the returned * string builder. * </p> * * @param wsdlUri the WSDL's URI * @param targetDirectory the target directory * @return a string builder containing the execution details * @throws IOException if WSgen was not found, or if the process could not be started * @throws InterruptedException * @throws JaxWsException if the generation seems to have failed */ public StringBuilder generateWsClient( URI wsdlUri, File targetDirectory ) throws IOException, InterruptedException, JaxWsException { // Create the temporary file synchronized( TEMP_FILE ) { if( ! TEMP_FILE.exists() && ! TEMP_FILE.mkdir()) throw new IOException( "The class directories could not be created." ); } final StringBuilder outputBuffer = new StringBuilder(); int exitValue = 0; try { File executable = getJavaExecutable( false ); // Build the command line List<String> cmd = new ArrayList<String>(); cmd.add( executable.getAbsolutePath()); cmd.add( "-verbose" ); cmd.add( "-s" ); cmd.add( targetDirectory.getAbsolutePath()); cmd.add( "-d" ); cmd.add( TEMP_FILE.getAbsolutePath()); cmd.add( wsdlUri.toString()); // Create the process ProcessBuilder pb = new ProcessBuilder( cmd ); pb = pb.redirectErrorStream( true ); final Process process = pb.start(); // Monitor the execution Thread loggingThread = new Thread() { @Override public void run() { try { BufferedReader reader = new BufferedReader( new InputStreamReader( process.getInputStream())); String line = ""; try { while((line = reader.readLine()) != null ) outputBuffer.append( line + "\n" ); } finally { reader.close(); } } catch( IOException ioe ) { PetalsCommonPlugin.log( ioe, IStatus.ERROR ); } } }; loggingThread.start(); loggingThread.join(); exitValue = process.waitFor(); } finally { // Free the temporary directory synchronized( TEMP_FILE ) { IoUtils.deleteFilesRecursively( TEMP_FILE.listFiles()); } // Keep a trace of the generation JaxWsException loggingException; if( exitValue != 0 ) { loggingException = new JaxWsException( "The JAX-WS process did not return 0. An error may have occurred." ) { private static final long serialVersionUID = -8585870927871804985L; @Override public void printStackTrace( PrintStream s ) { s.print( outputBuffer.toString()); } @Override public void printStackTrace( PrintWriter s ) { s.print( outputBuffer.toString()); } }; throw loggingException; } else if( PreferencesManager.logAllJaxWsTraces()) { loggingException = new JaxWsException( "Logging the WSDL import trace (" + wsdlUri + ")." ) { private static final long serialVersionUID = -5344307338271840633L; @Override public void printStackTrace( PrintStream s ) { s.print( outputBuffer.toString()); } @Override public void printStackTrace( PrintWriter s ) { s.print( outputBuffer.toString()); } }; PetalsCommonPlugin.log( loggingException, IStatus.INFO ); } } return outputBuffer; } /** * The @WebService annotation (javax.jws.WebService"). */ private static final String WEB_WS_ANNOTATION = "javax.jws.WebService"; /** * The shorten @WebService annotation (WebService). */ private static final String SHORT_WEB_WS_ANNOTATION = "WebService"; /** * The @WebServiceClient annotation (javax.jws.WebServiceClient"). */ private static final String WEB_WS_CLIENT_ANNOTATION = "javax.xml.ws.WebServiceClient"; /** * Searches all the Java types from a Java project that are annotated with @WebService. * * @param jp the containing Java project * @param monitor the progress monitor * @param includeClasses true to include classes in the search results * @param includeInterfaces true to include the interfaces in the search results * @return a Map * <p> * Key = the qualified name of the annotated type<br /> * Value = the associated service name (in the target WSDL) * </p> * * @throws CoreException if the search could not be launched */ public static Map<String,String> getJaxAnnotatedJavaTypes( IJavaProject jp, IProgressMonitor monitor, final boolean includeClasses, final boolean includeInterfaces ) throws CoreException { jp.getProject().refreshLocal( IResource.DEPTH_INFINITE, monitor ); jp.getProject().build( IncrementalProjectBuilder.FULL_BUILD, monitor ); final Map<String,String> classNameToServiceName = new HashMap<String,String> (); SearchPattern pattern = SearchPattern.createPattern( WEB_WS_ANNOTATION, IJavaSearchConstants.ANNOTATION_TYPE, IJavaSearchConstants.ANNOTATION_TYPE_REFERENCE, SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE ); // This is what we do when we find a match SearchRequestor requestor = new SearchRequestor() { @Override public void acceptSearchMatch( SearchMatch match ) throws CoreException { // We get the Java type that is annotated with @WebService if( match.getElement() instanceof IType ) { // Find the annotation IType type = (IType) match.getElement(); if(type.isInterface() && includeInterfaces || type.isClass() && includeClasses) { IAnnotation ann = type.getAnnotation( WEB_WS_ANNOTATION ); if( ! ann.exists()) ann = type.getAnnotation( SHORT_WEB_WS_ANNOTATION ); // Get the service name and store it if( ann.exists()) { String serviceName = null; for( IMemberValuePair pair : ann.getMemberValuePairs()) { if( "serviceName".equalsIgnoreCase( pair.getMemberName())) { serviceName = (String) pair.getValue(); break; } } if( serviceName == null ) serviceName = type.getElementName() + "Service"; classNameToServiceName.put( type.getFullyQualifiedName(), serviceName ); } } } } }; new SearchEngine().search( pattern, new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant()}, SearchEngine.createJavaSearchScope( new IJavaElement[] { jp }, false ), requestor, monitor ); return classNameToServiceName; } /** * Deletes all the classes annotated with @WebServiceClient. * * @param jp the containing Java project * @param monitor the progress monitor * @throws CoreException if the classes annotated with @WebServiceClient could not be listed */ public static void removeWebServiceClient( IJavaProject jp, final IProgressMonitor monitor ) throws CoreException { SearchPattern pattern = SearchPattern.createPattern( WEB_WS_CLIENT_ANNOTATION, IJavaSearchConstants.ANNOTATION_TYPE, IJavaSearchConstants.ANNOTATION_TYPE_REFERENCE, SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE ); // This is what we do when we find a match SearchRequestor requestor = new SearchRequestor() { @Override public void acceptSearchMatch( SearchMatch match ) throws CoreException { // We get the Java type that is annotated with @WebService if( match.getElement() instanceof IType ) { IType type = (IType) match.getElement(); ICompilationUnit cu = type.getCompilationUnit(); if( cu != null && cu.getCorrespondingResource() != null ) cu.getCorrespondingResource().delete( true, monitor ); else PetalsCommonPlugin.log( "A WS client could not be deleted (no compilation unit).", IStatus.ERROR ); } } }; new SearchEngine().search( pattern, new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant()}, SearchEngine.createJavaSearchScope( new IJavaElement[] { jp }, false ), requestor, monitor ); } /** * Creates JAX-WS implementations from annotated interfaces contained in a Java project. * <p> * For very interface annotated with @WebService, a default implementing class * is created and replaces the interface. * </p> * * @param jp the containing Java project * @param monitor the progress monitor * @return a map associating a WSDL service name with a JAX-WS class * <p> * Key = a WSDL service name<br /> * Value = the JAX-WS class * </p> * * @throws CoreException if the annotated interfaces could not be listed */ public static Map<String,String> createJaxWsImplementation( IJavaProject jp, final IProgressMonitor monitor ) throws CoreException { Map<String,String> serviceNameToImplName = new HashMap<String,String> (); Map<String,String> classNameToServiceName = getJaxAnnotatedJavaTypes( jp, monitor, false, true ); for( Map.Entry<String,String> entry : classNameToServiceName.entrySet()) { // Get the class source String className = entry.getKey(); IType type = jp.findType( className ); if( type == null || type.getCompilationUnit() == null ) continue; // Get the interface's simple name int index = className.lastIndexOf( '.' ); String simpleName; if( index == -1 ) simpleName = className; else simpleName = className.substring( index + 1 ); String implName = simpleName.replaceFirst( "PortType$", "" ); if( implName.equals( simpleName )) implName = simpleName + "Impl"; serviceNameToImplName.put( entry.getValue(), className.replaceFirst( simpleName + "$", implName )); StringBuffer sourceBuffer = replaceInterfaceDeclarationByImpl( type.getCompilationUnit().getSource(), simpleName, implName ); String source = replaceInterfaceMethodsByImpl( sourceBuffer ); // Create the class file and set its content ICompilationUnit cu = type.getCompilationUnit(); if( cu != null && cu.getCorrespondingResource() != null ) { IFile implFile = cu.getCorrespondingResource().getParent().getFile( new Path( implName + ".java" )); // Normally, the file should exist and be the interface's one if( ! implFile.exists()) implFile.create( new ByteArrayInputStream( source.getBytes()), true, monitor ); else implFile.setContents( new ByteArrayInputStream( source.getBytes()), true, true, monitor ); } else PetalsCommonPlugin.log( "The JAX-WS implementation could not be created (no compilation unit).", IStatus.ERROR ); } return serviceNameToImplName; } /** * Replaces the interface declaration by the declaration of an implementation. * @param interfaceDecl * @param simpleName * @param implName * @return */ static StringBuffer replaceInterfaceDeclarationByImpl( String interfaceDecl, String simpleName, String implName ) { StringBuffer source = new StringBuffer( interfaceDecl ); Pattern interfacePattern = Pattern.compile( " interface\\s+\\w+\\s*\\{", Pattern.MULTILINE | Pattern.DOTALL ); Matcher matcher = interfacePattern.matcher( source ); if( matcher.find()) { source = source.replace( matcher.start(), matcher.end(), " class " + implName + " implements " + simpleName + " {" ); } return source; } /** * Replaces interface methods by their implementations. * <p> * Made public for test purposes. * Not really useful otherwise. * </p> * * @param source * @return the modified source */ public static String replaceInterfaceMethodsByImpl( StringBuffer source ) { // Get the methods' default implementations Pattern methodPattern = Pattern.compile( "public\\s+(\\w+)\\s+\\w+\\s*\\([^;]*\\)\\s*(throws\\s+\\w+)?\\s*;", Pattern.MULTILINE | Pattern.DOTALL ); Matcher matcher = methodPattern.matcher( source ); while( matcher.find()) { String javaType = matcher.group( 1 ); String defaultImpl = getDefaultImplementation( javaType ); String methodDecl = matcher.group( 0 ); methodDecl = methodDecl.substring( 0, methodDecl.length() - 1 ).trim(); // Remove the semicolon String methodRepl = methodDecl + " {\n\t\t" + defaultImpl + "\n\t}"; source = source.replace( matcher.start(), matcher.end(), methodRepl ); // We progressively modify the searched string. // We have to reset the matcher to make sure it does not stop to // an offset that has now been pushed by our additions. matcher.reset(); } return source.toString(); } /** * Gets the default implementation text for a given return type. * @param javaType the Java type (void, primitive type or class name) * @return the default implementation (never null) */ static String getDefaultImplementation( String javaType ) { String result; if( "void".equals( javaType )) result = "// nothing"; else if( "int".equals( javaType )) result = "return 0;"; else if( "boolean".equals( javaType )) result = "return false;"; else if( "long".equals( javaType )) result = "return 0;"; else if( "double".equals( javaType )) result = "return 0;"; else if( "float".equals( javaType )) result = "return 0;"; else if( "byte".equals( javaType )) result = "return 0;"; else if( "short".equals( javaType )) result = "return 0;"; else if( "char".equals( javaType )) result = "return ' ';"; else result = "return null;"; return result; } /** * An exception related to this utility class. */ public static class JaxWsException extends Exception { /** * The serial ID. */ private static final long serialVersionUID = -358390664739689755L; /** * Constructor. */ public JaxWsException() { super(); } /** * Constructor. * @param message * @param cause */ public JaxWsException( String message, Throwable cause ) { super( message, cause ); } /** * Constructor. * @param message */ public JaxWsException( String message ) { super( message ); } /** * Constructor. * @param cause */ public JaxWsException( Throwable cause ) { super( cause ); } } }