/*
* .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;
}
}