/* * Maven and Sonar plugin for .Net * Copyright (C) 2010 Jose Chillan and Alexandre Victoor * mailto: jose.chillan@codehaus.org or alexvictoor@codehaus.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 */ package org.apache.maven.dotnet; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.io.OutputStream; import java.io.PrintWriter; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.apache.maven.dotnet.commons.project.DotNetProjectException; import org.apache.maven.dotnet.commons.project.VisualStudioProject; import org.apache.maven.dotnet.commons.project.VisualStudioSolution; import org.apache.maven.dotnet.commons.project.VisualStudioUtils; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.cli.CommandLineException; import org.codehaus.plexus.util.cli.CommandLineUtils; import org.codehaus.plexus.util.cli.Commandline; import org.codehaus.plexus.util.cli.StreamConsumer; /** * A utilitary class to factor some features for the DotNet relative mojos. * * @author Jose CHILLAN Apr 14, 2009 */ public abstract class AbstractDotNetMojo extends AbstractMojo { /** * Name of the file that contains the names of the files to export in a * resource folder */ private final static String CONTENT_FILE_NAME = "content.txt"; /** * A utility empty array. */ private static final String[] EMPTY_STRING_ARRAY = new String[0]; /** * The name of the solution to use in case there are multiple solutions in the * folder AND none has the same name as the artifact with a ".sln" extension. * * @parameter expression="${visual.studio.solution}" */ protected String solutionName; /** * The name of the project to use if it doesn't have the same name as the * artifact with a ".csproj" extension. * * @parameter expression="${visual.studio.project}" */ protected String projectName; /** * A pattern that will help to figure out if a project is a test project from * its name. The '*' and '?' common jokers are accepted. Several patterns may be * specified using ";" as a delimiter. * * @parameter expression="${visual.test.project.pattern}" * default-value="*.Tests" */ protected String testProjectPattern; /** * The build configurations to use for the project or solution, separated by * colons or semi-colons as in "Debug,Release". * Default value is "Debug". If value is "ALL", all the build configurations defined * in the sln file will be used. * * @parameter expression="${msbuild.configurations}" * alias="${buildConfigurations}" default-value="Debug" */ protected String buildConfigurations; /** * Defines if the build should generate debug symbols (typically .pdb files) * * @parameter expression="${maven.compiler.debug}" default-value="true" */ protected boolean generatePdb; /** * Defines if the plugin can use the maven-dotnet-runtime artifact to export * the relevant .Net quality applications instead of using their global path. * The defined path is always taken in priority. * * @parameter expression="${dotnet.use.embedded.runtime}" default-value="true" */ protected boolean useEmbbededRuntime; /** * Location of the output files * * @parameter expression="${project.build.directory}" * @required */ protected File outputDirectory; /** * The maven project. * * @parameter expression="${project}" * @required */ protected MavenProject project; /** * Defines if the launched commands should be appended into a "command.txt" * file. For debug purpose. * * @parameter expression="${trace.commands}" */ protected boolean traceCommands = false; /** * List of the excluded projects, using ',' as delimiter. C# files * of these projects will be ignored. No violation on files * of these projects should be reported. * * @parameter expression="${skippedProjects}" */ protected String skippedProjects; /** * Locations of the assemblies that should be analysed in case default location * should not be used. Actually AOP tools such as PostSharp mess up the cil and * may provoke false positives. * Project names should be used as keys. Values should be the path to the wanted DLLs. * If the assembly of the "Cache" project is located in the "compile" directory instead * of usual "bin/Debug", you can add in the configuration section of the plugin something * like <assemblyDirectories\><Cache>compile</Cache></assemblyDirectories> * @parameter */ protected Map<String, String> assemblyDirectories = new HashMap<String, String>(); /** * Constructs a @link{AbstractDotNetMojo}. */ public AbstractDotNetMojo() { } /** * Launches the MOJO action. <br/> * This method checks whether the pom.xml is for a project or a solution and * launches the adequate method {@link #executeProject(VisualStudioProject)} * or {@link #executeSolution(VisualStudioSolution)}. * * @throws MojoExecutionException * @throws MojoFailureException */ @Override public void execute() throws MojoExecutionException, MojoFailureException { if (!checkExecutionAllowed()) { return; } // Here we should add : if the project is a "sln" packaging, then launch for // the solution if (VisualStudioUtils.SOLUTION_PACKAGINGS.contains(project.getPackaging())) { VisualStudioSolution visualSolution = null; try { visualSolution = VisualStudioUtils.getVisualSolution(project, solutionName); } catch (DotNetProjectException e) { throw new MojoExecutionException( "The solution for project " + project.getArtifactId() + " is not a " + "properly configured Visual Studio solution.\nPlease ensure you have a '.sln' file in " + project.getBasedir(), e); } if (visualSolution == null) { throw new MojoExecutionException( "The solution for project " + project.getArtifactId() + " is not a " + "properly configured Visual Studio solution.\nPlease ensure you have a '.sln' file in " + project.getBasedir()); } if ("ALL".equals(buildConfigurations)) { buildConfigurations = StringUtils.join(visualSolution.getBuildConfigurations(),','); } if (assemblyDirectories != null ) { visualSolution.overrideAssemblyDirectories(assemblyDirectories); } visualSolution.filterProjects(skippedProjects); executeSolution(visualSolution); return; } VisualStudioProject visualProject = getVisualProject(); if (visualProject == null) { getLog().info( "The project " + project.getArtifactId() + " is not a visual studio project"); return; } executeProject(visualProject); } /** * Launches the action in case the project is a VisualStudio project * * @param visualProject */ protected abstract void executeProject(VisualStudioProject visualProject) throws MojoExecutionException, MojoFailureException; /** * Launches the action in case the project is a VisualStudio solution * * @param visualSolution */ protected abstract void executeSolution(VisualStudioSolution visualSolution) throws MojoExecutionException, MojoFailureException; /** * Gets the Visual Studio project associated to the current project. * * @return the project, or <code>null</code> if the project is not a Visual * Studio project * @throws MojoExecutionException * if the current pom.xml correspond to a badly defined project */ protected final VisualStudioProject getVisualProject() throws MojoExecutionException { File projectFile; try { projectFile = VisualStudioUtils.getVisualFile(project, projectName, ".csproj", "project"); } catch (DotNetProjectException except) { throw new MojoExecutionException( "Could not create a VisualStudio project", except); } // No solution defined if (projectFile == null) { getLog().debug( "The project " + project.getName() + " is not a Visual Studio project"); return null; } try { VisualStudioProject visualProject = VisualStudioUtils .getProject(projectFile); assessTestProject(visualProject); return visualProject; } catch (FileNotFoundException e) { throw new MojoExecutionException( "Could not extract the project information for " + projectFile, e); } catch (DotNetProjectException e) { throw new MojoExecutionException( "Could not extract the project information for " + projectFile, e); } } /** * Gets the solution corresponding to the current project if applicable. * * @return the generated solution, or <code>null</code> if the project is not * a solution */ protected final VisualStudioSolution getVisualSolution() throws MojoExecutionException { File basedir = project.getBasedir(); File solutionFile; try { solutionFile = VisualStudioUtils.getVisualFile(project, solutionName, ".sln", "solution"); } catch (DotNetProjectException except) { throw new MojoExecutionException( "Could not create a VisualStudio solution", except); } // No solution defined if (solutionFile == null) { getLog().debug( "The project " + project.getName() + " is not a Visual Studio solution"); return null; } // We define the solution file solutionName = solutionFile.getName(); // We try to build the solution VisualStudioSolution solution; try { solution = VisualStudioUtils.getSolution(basedir, solutionName); } catch (Exception e) { throw new MojoExecutionException( "Could not extract the solution information for " + solutionFile, e); } List<VisualStudioProject> projects = solution.getProjects(); // We define for each project if it is a test project for (VisualStudioProject visualStudioProject : projects) { assessTestProject(visualStudioProject); } return solution; } /** * @param visualStudioProject */ private void assessTestProject(VisualStudioProject visualStudioProject) { VisualStudioUtils.assessTestProject(visualStudioProject, testProjectPattern); } /** * Deletes a set of file before generation * * @param files * the files to delete */ protected void deleteFiles(File... files) { for (File file : files) { if (file.exists()) { file.delete(); } } } /** * Ensures that the current running system is a Windows version * * @throws MojoExecutionException * if the system is not windows */ protected void ensureWindowsSystem() throws MojoExecutionException { String osName = System.getProperty("os.name").toLowerCase(); if (!osName.contains("windows")) { throw new MojoExecutionException( "The task must be launched on a Windows system"); } } /** * Ensure that the java version is at least 1.6 * * @throws MojoExecutionException */ protected void ensureJavaVersion() throws MojoExecutionException { String version = System.getProperty("java.version"); int versionValue = Integer.parseInt(new StringBuilder() .append(version.charAt(0)).append(version.charAt(2)).toString()); if (versionValue < 16) { throw new MojoExecutionException( "The C# reporting plugin requires to be run in at least the JDK 6.0"); } } /** * Launches a command line, redirecting the stream to the maven logs. * @param reportType * a display type for the report * @param acceptedMask * a mask for the exit code of the command that is accepted (put 0 if * you don't know) * @param commandline * the command ready to be executed * * @return the status of the command after execution * @throws MojoExecutionException * if the execution command could not be launched for any reason * @throws MojoFailureException * if the command status after execution is not satisfying */ protected int launchCommand(File executable, List<String> arguments, String reportType, int acceptedMask) throws MojoExecutionException, MojoFailureException { Commandline commandline = generateCommandLine(executable, arguments); return launchCommand(commandline, reportType, acceptedMask, true); } /** * Launches a command line, redirecting the stream to the maven logs * @param commandline * the command ready to be executed * @param reportType * a display type for the report * @param acceptedMask * a mask for the exit code of the command that is accepted (put 0 if * you don't know) * @param throwsFailure * <code>true</code> if the method should throw an exception in case * of failure * * @return the status of the command after execution * @throws MojoExecutionException * if the execution command could not be launched for any reason * @throws MojoFailureException * if the command status after execution is not satisfying and the * throwsFailure parameter is not false */ protected int launchCommand(Commandline commandline, String reportType, int acceptedMask, boolean throwsFailure) throws MojoExecutionException, MojoFailureException { int commandLineResult; Log log = getLog(); String[] commandLineElements = commandline.getCommandline(); CommandStreamConsumer systemOut = new CommandStreamConsumer(log); try { // Execute the commandline log.debug("Executing command: " + commandline); log.debug("Command elements :" + Arrays.toString(commandLineElements)); ProcessBuilder builder = new ProcessBuilder( Arrays.asList(commandLineElements)); builder.redirectErrorStream(true); if (traceCommands) { try { File commandFile = getReportFile("command.txt"); OutputStream stream = new FileOutputStream(commandFile, true); PrintWriter writer = new PrintWriter(stream); writer.println("Mojo : " + reportType + " on " + new Timestamp(System.currentTimeMillis())); writer.println(commandline); writer.println(); writer.close(); } catch (FileNotFoundException e) { // Nothing : commands are not logged log.debug("command.txt not found"); } } commandLineResult = CommandLineUtils.executeCommandLine(commandline, systemOut, systemOut); // Check if nunit-console is not in the path if (systemOut.isCommandNotFound()) { throw new MojoExecutionException("Please add the executable for " + reportType + " to your path"); } else if ((commandLineResult & (~acceptedMask)) != 0) { if (log.isWarnEnabled()) { log.warn("FAILURE !!!"); log.warn("Launched command : " + commandline); log.warn(""); log.warn(systemOut.getContent()); } // We throw the exception only if asked for if (throwsFailure) { throw new MojoFailureException("Failure during the " + reportType + " generation that ended with status=" + commandLineResult); } } } catch (CommandLineException e) { throw new MojoExecutionException("Failure during the " + reportType + " generation, executing commandline:\n" + commandline, e); } return commandLineResult; } /** * Generates a command line with the arguments. * * @param executable * @param arguments * @return */ protected Commandline generateCommandLine(File executable, List<String> arguments) { Commandline commandline = new Java5CommandLine(); // We create the work directory if necessary if (!outputDirectory.exists()) { outputDirectory.mkdirs(); } commandline.setWorkingDirectory(outputDirectory.toString()); commandline.setExecutable(executable.toString()); commandline.addArguments(arguments.toArray(EMPTY_STRING_ARRAY)); return commandline; } /** * Exports a resource folder to a subdirectory of the maven build directory. <br/> * The folder is supposed to contain a "context.txt" file that contains the * list of the files to export, one name per line * * @param resourceDir * the resource directory to export * @param destinationSubFolder * the subfolder to use, that will be created under * ${project.build.dir} * @param application * the exported application * @return the generated folder * @throws MojoExecutionException */ protected File extractFolder(String resourceDir, String destinationSubFolder, String application) throws MojoExecutionException { if (!useEmbbededRuntime) { getLog() .warn( "The use of the embedded runtime package is not enabled. Please add the settings 'dotnet.use.embedded.runtime=true'"); throw new MojoExecutionException( "The use of the embedded runtime package is not enabled for " + application); } getLog().debug("Exporting files for " + application); String contentFile = resourceDir + "/" + CONTENT_FILE_NAME; InputStream contentResource = getClassLoader().getResourceAsStream( contentFile); if (contentResource==null) { throw new MojoExecutionException(application + " binaries were not found"); } LineNumberReader reader = new LineNumberReader(new InputStreamReader( contentResource)); String line = null; List<String> contentFiles = new ArrayList<String>(); try { while ((line = reader.readLine()) != null) { contentFiles.add(line.trim()); } } catch (IOException e) { throw new MojoExecutionException("Could not extract the files for " + application, e); } File reportDirectory = getReportDirectory(); File extractFolder = new File(reportDirectory, destinationSubFolder); extractResources(extractFolder, resourceDir, contentFiles, application); return extractFolder; } /** * Extracts the resources to a specified directory. * * @param destinationDirectory * the directory to which the files will be extracted * @param resourceDirectory * the resource directory from which they are extracted * @param resourceNames * the name of the resource files to extract * @param application * the name of the application for debug purpose * @throws MojoExecutionException * if a file is missing or the folder could not be written to */ protected void extractResources(File destinationDirectory, String resourceDirectory, List<String> resourceNames, String application) throws MojoExecutionException { if (!destinationDirectory.exists()) { destinationDirectory.mkdirs(); } for (String resource : resourceNames) { extractResource(destinationDirectory, resourceDirectory + "/" + resource, resource, application); } } /** * Extracts a resource file from the plugin jar to a given destination folder. * * @param exportDirectory * the destination folder * @param resourcePath * the full path to the extracted resource * @param fileName * the name of the file after export (usually the terminal part of * resourcePath) * @param application * the name of the application for debug purpose * @return the exported file * @throws MojoExecutionException * if the resource is not found or the file could no be written */ protected File extractResource(File exportDirectory, String resourcePath, String fileName, String application) throws MojoExecutionException { File exportedFile = new File(exportDirectory, fileName); // First we create the parent folder if necessary // NOTE here that as the resource may be far in the subtree, // the parent is not necessary the export directory File parentDirectory = exportedFile.getParentFile(); if (!parentDirectory.exists()) { getLog().debug("Creating parent export directory : " + parentDirectory); parentDirectory.mkdirs(); } // We delete the file to replace it if (exportedFile.exists()) { exportedFile.delete(); } // Exports the file try { exportedFile.createNewFile(); InputStream fileStream = getClassLoader().getResourceAsStream( resourcePath); OutputStream out = new FileOutputStream(exportedFile); long length = 0; byte buf[] = new byte[1024]; int len; // We write all the bytes by block while ((len = fileStream.read(buf)) > 0) { out.write(buf, 0, len); length += len; } out.close(); fileStream.close(); getLog().debug( "Exported file " + exportedFile + " : " + length + " bytes"); } catch (Exception e) { // Something went wrong... throw new MojoExecutionException("A problem occurred for " + application + " while extracting file " + fileName + " to " + exportedFile, e); } return exportedFile; } /** * Gets a file for a report whose name is given * * @param fileName * the name of the file * @return the file to generate */ protected File getReportFile(String fileName) { File reportDirectory = getReportDirectory(); return new File(reportDirectory, fileName); } /** * Escapes a file if necessary for command generation. * * @param file * the file to escape * @return the escapes file name * */ protected String toCommandPath(File file) { String absolutePath; try { absolutePath = file.getCanonicalPath(); } catch (IOException e) { // We try another way of processing absolutePath = file.getAbsolutePath(); } return absolutePath; } /** * Gets the current class loader * * @return */ protected ClassLoader getClassLoader() { return Thread.currentThread().getContextClassLoader(); } /** * Gets the report directory to use. * * @return the report directory */ protected File getReportDirectory() { String buildPath = project.getBuild().getDirectory(); File reportDirectory = new File(buildPath); reportDirectory.mkdirs(); return reportDirectory; } /** * Ensures that the execution of the Mojo is allowed. <br/> * The current implementation checks that the execution is launched on a * Windows system and uses at least Java 6.0. This method may be overridden * but should better invoke the super. * * @return <code>true</code> if the execution is allowed * @throws MojoExecutionException */ protected boolean checkExecutionAllowed() throws MojoExecutionException { ensureWindowsSystem(); ensureJavaVersion(); return true; } /** * A consumer for command outputs. * * @author Jose CHILLAN Jul 30, 2009 */ private static final class CommandStreamConsumer implements StreamConsumer { private static final String COMMAND_NOT_FOUND_FRAGMENT = "is not recognized as an internal or external command"; private boolean commandNotFound; private StringBuilder consumedLines = new StringBuilder(); private Log log; private CommandStreamConsumer(Log log) { this.log = log; } /** * Callback each time a line is consumed. * * @param line */ @Override public void consumeLine(String line) { consumedLines.append(line + "\n"); log.info(line); if (line.contains(COMMAND_NOT_FOUND_FRAGMENT)) { commandNotFound = true; } } /** * Checks if the command was not found. * * @return <code>true</code> if the command was not found */ public boolean isCommandNotFound() { return commandNotFound; } /** * Gets the content of the stream. * * @return the stream content */ public CharSequence getContent() { return consumedLines; } } }