org.sonar.dotnet.tools.commons.visualstudio.ModelFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.sonar.dotnet.tools.commons.visualstudio.ModelFactory.java

Source

/*
 * .NET tools :: Commons
 * Copyright (C) 2010 Jose Chillan, Alexandre Victoor and SonarSource
 * dev@sonar.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
 */
/*
 * Created on Apr 16, 2009
 */
package org.sonar.dotnet.tools.commons.visualstudio;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.utils.WildcardPattern;
import org.sonar.dotnet.tools.commons.DotNetToolsException;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

/**
 * Utility classes for the parsing of a Visual Studio project
 * 
 * @author Fabrice BELLINGARD
 * @author Jose CHILLAN Aug 14, 2009
 */
public final class ModelFactory {

    private static final Logger LOG = LoggerFactory.getLogger(ModelFactory.class);
    private static final String VERSION_KEY = ", Version=";

    /*
     * Pattern used to define if a project is a test project or not
     */
    private static String testProjectNamePattern = "*.Tests";

    private ModelFactory() {
    }

    /**
     * Sets the pattern used to define if a project is a test project or not
     * 
     * @param testProjectNamePattern
     *          the pattern
     */
    public static void setTestProjectNamePattern(String testProjectNamePattern) {
        ModelFactory.testProjectNamePattern = testProjectNamePattern;
    }

    /**
     * Checks, whether the child directory is a subdirectory of the base directory.
     * 
     * @param base
     *          the base directory.
     * @param child
     *          the suspected child directory.
     * @return true, if the child is a subdirectory of the base directory.
     * @throws IOException
     *           if an IOError occured during the test.
     */
    public static boolean isSubDirectory(File base, File child) {
        try {
            File baseFile = base.getCanonicalFile();
            File childFile = child.getCanonicalFile();
            File parentFile = childFile;

            // Checks recursively if "base" is one of the parent of "child"
            while (parentFile != null) {
                if (baseFile.equals(parentFile)) {
                    return true;
                }
                parentFile = parentFile.getParentFile();
            }
        } catch (IOException ex) {
            // This is false
            if (LOG.isDebugEnabled()) {
                LOG.debug(child + " is not in " + base, ex);
            }
        }
        return false;
    }

    /**
     * @param visualStudioProject
     */
    protected static void assessTestProject(VisualStudioProject visualStudioProject, String testProjectPatterns) {

        String assemblyName = visualStudioProject.getAssemblyName();

        String[] patterns = StringUtils.split(testProjectPatterns, ";");
        boolean testFlag = false;

        for (int i = 0; i < patterns.length; i++) {
            if (WildcardPattern.create(patterns[i]).match(assemblyName)) {
                testFlag = true;
                break;
            }
        }

        if (testFlag) {
            LOG.info("The project '{}' has been qualified as a test project.", visualStudioProject.getName());
        }

        visualStudioProject.setTest(testFlag);
    }

    /**
     * Gets the solution from its folder and name.
     * 
     * @param baseDirectory
     *          the directory containing the solution
     * @param solutionName
     *          the solution name
     * @return the generated solution
     * @throws IOException
     * @throws DotNetToolsException
     */
    public static VisualStudioSolution getSolution(File baseDirectory, String solutionName)
            throws IOException, DotNetToolsException {
        File solutionFile = new File(baseDirectory, solutionName);
        return getSolution(solutionFile);
    }

    /**
     * @param solutionFile
     *          the solution file
     * @return a new visual studio solution
     * @throws IOException
     * @throws DotNetToolsException
     */
    public static VisualStudioSolution getSolution(File solutionFile) throws IOException, DotNetToolsException {

        String solutionContent = FileUtils.readFileToString(solutionFile);
        List<String> buildConfigurations = getBuildConfigurations(solutionContent);

        List<VisualStudioProject> projects = getProjects(solutionFile, solutionContent, buildConfigurations);
        VisualStudioSolution solution = new VisualStudioSolution(solutionFile, projects);
        solution.setBuildConfigurations(buildConfigurations);
        solution.setName(solutionFile.getName());
        return solution;
    }

    private static List<String> getBuildConfigurations(String solutionContent) {
        // A pattern to extract the build configurations from a visual studio solution
        String confExtractExp = "(\tGlobalSection\\(SolutionConfigurationPlatforms\\).*?^\tEndGlobalSection$)";
        Pattern confExtractPattern = Pattern.compile(confExtractExp, Pattern.MULTILINE + Pattern.DOTALL);
        List<String> buildConfigurations = new ArrayList<String>();
        // Extracts all the projects from the solution
        Matcher blockMatcher = confExtractPattern.matcher(solutionContent);
        if (blockMatcher.find()) {
            String buildConfigurationBlock = blockMatcher.group(1);
            String buildConfExtractExp = " = (.*)\\|";
            Pattern buildConfExtractPattern = Pattern.compile(buildConfExtractExp);
            Matcher buildConfMatcher = buildConfExtractPattern.matcher(buildConfigurationBlock);
            while (buildConfMatcher.find()) {
                String buildConfiguration = buildConfMatcher.group(1);
                buildConfigurations.add(buildConfiguration);
            }
        }
        return buildConfigurations;
    }

    /**
     * Gets all the projects in a solution.
     * 
     * @param solutionFile
     *          the solution file
     * @param solutionContent
     *          the text content of the solution file
     * @return a list of projects
     * @throws IOException
     * @throws DotNetToolsException
     */
    private static List<VisualStudioProject> getProjects(File solutionFile, String solutionContent,
            List<String> buildConfigurations) throws IOException, DotNetToolsException {

        File baseDirectory = solutionFile.getParentFile();

        // A pattern to extract the projects from a visual studion solution
        String projectExtractExp = "(Project.*?^EndProject$)";
        Pattern projectExtractPattern = Pattern.compile(projectExtractExp, Pattern.MULTILINE + Pattern.DOTALL);
        List<String> projectDefinitions = new ArrayList<String>();
        // Extracts all the projects from the solution
        Matcher globalMatcher = projectExtractPattern.matcher(solutionContent);
        while (globalMatcher.find()) {
            String projectDefinition = globalMatcher.group(1);
            projectDefinitions.add(projectDefinition);
        }

        // This pattern extracts the projects from a Visual Studio solution
        String normalProjectExp = "\\s*Project\\([^\\)]*\\)\\s*=\\s*\"([^\"]*)\"\\s*,\\s*\"([^\"]*?\\.csproj)\"";
        String webProjectExp = "\\s*Project\\([^\\)]*\\)\\s*=\\s*\"([^\"]*).*?ProjectSection\\(WebsiteProperties\\).*?"
                + "Debug\\.AspNetCompiler\\.PhysicalPath\\s*=\\s*\"([^\"]*)";
        Pattern projectPattern = Pattern.compile(normalProjectExp);
        Pattern webPattern = Pattern.compile(webProjectExp, Pattern.MULTILINE + Pattern.DOTALL);

        List<VisualStudioProject> result = new ArrayList<VisualStudioProject>();
        for (String projectDefinition : projectDefinitions) {
            // Looks for project files
            Matcher matcher = projectPattern.matcher(projectDefinition);
            if (matcher.find()) {
                String projectName = matcher.group(1);
                String projectPath = StringUtils.replace(matcher.group(2), "\\", File.separatorChar + "");

                File projectFile = new File(baseDirectory, projectPath);
                if (!projectFile.exists()) {
                    throw new FileNotFoundException("Could not find the project file: " + projectFile);
                }
                VisualStudioProject project = getProject(projectFile, projectName, buildConfigurations);
                result.add(project);
            } else {
                // Searches the web project
                Matcher webMatcher = webPattern.matcher(projectDefinition);

                if (webMatcher.find()) {
                    String projectName = webMatcher.group(1);
                    String projectPath = webMatcher.group(2);
                    if (projectPath.endsWith("\\")) {
                        projectPath = StringUtils.chop(projectPath);
                    }
                    File projectRoot = new File(baseDirectory, projectPath);
                    VisualStudioProject project = getWebProject(baseDirectory, projectRoot, projectName,
                            projectDefinition);
                    result.add(project);
                }
            }
        }
        return result;
    }

    /**
     * Creates a project from its file
     * 
     * @param projectFile
     *          the project file
     * @return the visual project if possible to define
     * @throws DotNetToolsException
     * @throws FileNotFoundException
     */
    public static VisualStudioProject getProject(File projectFile)
            throws FileNotFoundException, DotNetToolsException {
        String projectName = projectFile.getName();
        return getProject(projectFile, projectName, null);
    }

    /**
     * Generates a list of projects from the path of the visual studio projects files (.csproj)
     * 
     * @param projectFile
     *          the project file
     * @param projectName
     *          the name of the project
     * @throws DotNetToolsException
     * @throws FileNotFoundException
     *           if the file was not found
     */
    public static VisualStudioProject getProject(File projectFile, String projectName,
            List<String> buildConfigurations) throws FileNotFoundException, DotNetToolsException {

        VisualStudioProject project = new VisualStudioProject();
        project.setProjectFile(projectFile);
        project.setName(projectName);
        File projectDir = projectFile.getParentFile();

        XPathFactory factory = XPathFactory.newInstance();
        XPath xpath = factory.newXPath();

        // This is a workaround to avoid Xerces class-loading issues
        ClassLoader savedClassloader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(xpath.getClass().getClassLoader());
        try {
            // We define the namespace prefix for Visual Studio
            xpath.setNamespaceContext(new VisualStudioNamespaceContext());

            if (buildConfigurations != null) {
                Map<String, File> buildConfOutputDirMap = new HashMap<String, File>();
                for (String config : buildConfigurations) {
                    XPathExpression configOutputExpression = xpath.compile(
                            "/vst:Project/vst:PropertyGroup[contains(@Condition,'" + config + "')]/vst:OutputPath");
                    String configOutput = extractProjectProperty(configOutputExpression, projectFile);
                    buildConfOutputDirMap.put(config, new File(projectDir, configOutput));
                }
                project.setBuildConfOutputDirMap(buildConfOutputDirMap);
            }

            XPathExpression projectTypeExpression = xpath.compile("/vst:Project/vst:PropertyGroup/vst:OutputType");
            XPathExpression assemblyNameExpression = xpath
                    .compile("/vst:Project/vst:PropertyGroup/vst:AssemblyName");
            XPathExpression rootNamespaceExpression = xpath
                    .compile("/vst:Project/vst:PropertyGroup/vst:RootNamespace");
            XPathExpression debugOutputExpression = xpath
                    .compile("/vst:Project/vst:PropertyGroup[contains(@Condition,'Debug')]/vst:OutputPath");
            XPathExpression releaseOutputExpression = xpath
                    .compile("/vst:Project/vst:PropertyGroup[contains(@Condition,'Release')]/vst:OutputPath");
            XPathExpression silverlightExpression = xpath
                    .compile("/vst:Project/vst:PropertyGroup/vst:SilverlightApplication");

            // Extracts the properties of a Visual Studio Project
            String typeStr = extractProjectProperty(projectTypeExpression, projectFile);
            String silverlightStr = extractProjectProperty(silverlightExpression, projectFile);
            String assemblyName = extractProjectProperty(assemblyNameExpression, projectFile);
            String rootNamespace = extractProjectProperty(rootNamespaceExpression, projectFile);
            String debugOutput = extractProjectProperty(debugOutputExpression, projectFile);
            String releaseOutput = extractProjectProperty(releaseOutputExpression, projectFile);

            // Assess if the artifact is a library or an executable
            ArtifactType type = ArtifactType.LIBRARY;
            if (StringUtils.containsIgnoreCase(typeStr, "exe")) {
                type = ArtifactType.EXECUTABLE;
            }
            // The project is populated
            project.setProjectFile(projectFile);
            project.setType(type);
            project.setDirectory(projectDir);
            project.setAssemblyName(assemblyName);
            project.setRootNamespace(rootNamespace);
            project.setDebugOutputDir(new File(projectDir, debugOutput));
            project.setReleaseOutputDir(new File(projectDir, releaseOutput));

            if (StringUtils.isNotEmpty(silverlightStr)) {
                project.setSilverlightProject(true);
            }

            project.setBinaryReferences(getBinaryReferences(xpath, projectFile));

            assessTestProject(project, testProjectNamePattern);

            return project;
        } catch (XPathExpressionException xpee) {
            throw new DotNetToolsException("Error while processing the project " + projectFile, xpee);
        } finally {
            // Replaces the class loader after usage
            Thread.currentThread().setContextClassLoader(savedClassloader);
        }
    }

    private static List<BinaryReference> getBinaryReferences(XPath xpath, File projectFile)
            throws DotNetToolsException {
        List<BinaryReference> result = new ArrayList<BinaryReference>();
        try {

            XPathExpression targetFwkIdExpression = xpath
                    .compile("/vst:Project/vst:PropertyGroup/vst:TargetFrameworkIdentifier");
            XPathExpression targetFwkVersionExpression = xpath
                    .compile("/vst:Project/vst:PropertyGroup/vst:TargetFrameworkVersion");

            String fwkId = extractProjectProperty(targetFwkIdExpression, projectFile);
            String fwkversion = extractProjectProperty(targetFwkVersionExpression, projectFile);

            final String systemVersion;
            if (StringUtils.isEmpty(fwkId)) {
                systemVersion = fwkversion;
            } else {
                systemVersion = fwkId + '.' + fwkversion;
            }

            XPathExpression binaryRefExpression = xpath.compile("/vst:Project/vst:ItemGroup/vst:Reference");
            InputSource inputSource = new InputSource(new FileInputStream(projectFile));
            NodeList nodes = (NodeList) binaryRefExpression.evaluate(inputSource, XPathConstants.NODESET);
            int countNodes = nodes.getLength();
            for (int idxNode = 0; idxNode < countNodes; idxNode++) {
                Element includeElement = (Element) nodes.item(idxNode);
                // We filter the files
                String includeAttr = includeElement.getAttribute("Include");
                if (StringUtils.isEmpty(includeAttr)) {
                    LOG.debug("Binary reference ignored, Include attribute missing");
                } else {
                    BinaryReference reference = new BinaryReference();

                    int versionIndex = includeAttr.indexOf(VERSION_KEY);
                    if (versionIndex == -1) {
                        reference.setAssemblyName(includeAttr);
                        reference.setVersion(systemVersion);
                    } else {
                        String assemblyName = includeAttr.substring(0, versionIndex);
                        int versionEndIndex = includeAttr.indexOf(",", versionIndex + 1);
                        if (versionEndIndex < 0) {
                            versionEndIndex = includeAttr.length();
                        }

                        String version = includeAttr.substring(versionIndex + VERSION_KEY.length(),
                                versionEndIndex);
                        reference.setAssemblyName(assemblyName);
                        reference.setVersion(version);
                    }
                    result.add(reference);
                }
            }

        } catch (XPathExpressionException exception) {
            // Should not happen
            LOG.debug("xpath error", exception);
        } catch (FileNotFoundException exception) {
            // Should not happen
            LOG.debug("project file not found", exception);
        }
        return result;
    }

    public static VisualStudioProject getWebProject(File solutionRoot, File projectRoot, String projectName,
            String definition) throws FileNotFoundException {

        // We define the namespace prefix for Visual Studio
        VisualStudioProject project = new VisualStudioWebProject();
        project.setName(projectName);

        // Extracts the properties of a Visual Studio Project
        String assemblyName = projectName;
        String rootNamespace = "";
        String debugOutput = extractSolutionProperty("Debug.AspNetCompiler.TargetPath", definition);
        String releaseOutput = extractSolutionProperty("Release.AspNetCompiler.TargetPath", definition);

        // The project is populated
        project.setDirectory(projectRoot);
        project.setAssemblyName(assemblyName);
        project.setRootNamespace(rootNamespace);
        project.setDebugOutputDir(new File(solutionRoot, debugOutput));
        project.setReleaseOutputDir(new File(solutionRoot, releaseOutput));
        return project;
    }

    /**
     * Reads a property from a project
     * 
     * @param string
     * @param definition
     * @return
     */
    public static String extractSolutionProperty(String name, String definition) {
        String regexp = name + "\\s*=\\s*\"([^\"]*)";
        Pattern pattern = Pattern.compile(regexp);
        Matcher matcher = pattern.matcher(definition);
        if (matcher.find()) {
            return matcher.group(1);
        }
        return null;
    }

    /**
     * Gets the relative paths of all the files in a project, as they are defined in the .csproj file.
     * 
     * @param project
     *          the project file
     * @return a list of the project files
     */
    public static List<String> getFilesPath(File project) {
        List<String> result = new ArrayList<String>();

        XPathFactory factory = XPathFactory.newInstance();
        XPath xpath = factory.newXPath();
        // We define the namespace prefix for Visual Studio
        xpath.setNamespaceContext(new VisualStudioNamespaceContext());
        try {
            XPathExpression filesExpression = xpath.compile("/vst:Project/vst:ItemGroup/vst:Compile");
            InputSource inputSource = new InputSource(new FileInputStream(project));
            NodeList nodes = (NodeList) filesExpression.evaluate(inputSource, XPathConstants.NODESET);
            int countNodes = nodes.getLength();
            for (int idxNode = 0; idxNode < countNodes; idxNode++) {
                Element compileElement = (Element) nodes.item(idxNode);
                // We filter the files
                String filePath = compileElement.getAttribute("Include");
                if ((filePath != null) && filePath.endsWith(".cs")) {

                    // fix tests on unix system
                    // but should not be necessary
                    // on windows build machines
                    filePath = StringUtils.replace(filePath, "\\", File.separatorChar + "");
                    result.add(filePath);
                }
            }

        } catch (XPathExpressionException exception) {
            // Should not happen
            LOG.debug("xpath error", exception);
        } catch (FileNotFoundException exception) {
            // Should not happen
            LOG.debug("project file not found", exception);
        }
        return result;
    }

    /**
     * Extracts a string project data.
     * 
     * @param expression
     * @param projectFile
     * @return
     * @throws DotNetToolsException
     * @throws FileNotFoundException
     */
    private static String extractProjectProperty(XPathExpression expression, File projectFile)
            throws DotNetToolsException {
        try {
            FileInputStream file = new FileInputStream(projectFile);
            InputSource source = new InputSource(file);
            return expression.evaluate(source);
        } catch (Exception e) {
            throw new DotNetToolsException(
                    "Could not evaluate the expression " + expression + " on project " + projectFile, e);
        }
    }

    /**
     * A Namespace context specialized for the handling of csproj files
     * 
     * @author Jose CHILLAN Sep 1, 2009
     */
    private static class VisualStudioNamespaceContext implements NamespaceContext {

        /**
         * Gets the namespace URI.
         * 
         * @param prefix
         * @return
         */
        public String getNamespaceURI(String prefix) {
            if (prefix == null) {
                throw new IllegalStateException("Null prefix");
            }

            final String result;
            if ("vst".equals(prefix)) {
                result = "http://schemas.microsoft.com/developer/msbuild/2003";
            } else if ("xml".equals(prefix)) {
                result = XMLConstants.XML_NS_URI;
            } else {
                result = XMLConstants.NULL_NS_URI;
            }
            return result;
        }

        // This method isn't necessary for XPath processing.
        public String getPrefix(String uri) {
            throw new UnsupportedOperationException();
        }

        // This method isn't necessary for XPath processing either.
        public Iterator<?> getPrefixes(String uri) {
            throw new UnsupportedOperationException();
        }

    }

    /**
     * Checks a file existence in a directory.
     * 
     * @param basedir
     *          the directory containing the file
     * @param fileName
     *          the file name
     * @return <code>null</code> if the file doesn't exist, the file if it is found
     */
    public static File checkFileExistence(File basedir, String fileName) {
        File checkedFile = new File(basedir, fileName);
        if (checkedFile.exists()) {
            return checkedFile;
        }
        return null;
    }
}