/**
* Copyright 2012 Tobias Gierke <tobias.gierke@code-sourcery.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codesourcery.jasm16.ide;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
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.lang.StringUtils;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import de.codesourcery.jasm16.emulator.EmulationOptions;
import de.codesourcery.jasm16.emulator.IEmulationOptionsProvider;
import de.codesourcery.jasm16.utils.Misc;
/**
* XML file describing a JASM16 project's information like
* source folders,output folder, project name etc.
*
* @author tobias.gierke@code-sourcery.de
*/
public class ProjectConfiguration implements IEmulationOptionsProvider
{
public static final Set<String> DEFAULT_SOURCEFILENAME_PATTERNS = new HashSet<>(
Arrays.asList(".*?\\.dasm",".*?\\.dasm16",".*?\\.asm")
);
public static final String DEFAULT_OUTPUT_FOLDER = "bin";
public static final String DEFAULT_SOURCE_FOLDER = "src";
private static final Logger LOG = Logger.getLogger(ProjectConfiguration.class);
public static final String PROJECT_CONFIG_FILE = "jasm_project.xml";
public static final String DEFAULT_EXECUTABLE_NAME = "a.out";
private final File baseDir;
private final List<String> sourceFolders = new ArrayList<String>();
private String outputFolder;
private String projectName;
private String executableName;
private String compilationRoot; // path relative to project basedir, points to source file that should be compiled first (and that will
// include all other files that need to be compiled)
private final Set<String> sourceFilenamePatterns = new HashSet<>();
private final List<Pattern> sourceFilenameRegexPatterns = new ArrayList<>();
private BuildOptions buildOptions = new BuildOptions();
private EmulationOptions emulationOptions=new EmulationOptions();
private final DebuggerOptions debuggerOptions = new DebuggerOptions();
public void populateFrom(ProjectConfiguration other) {
if ( ! this.baseDir.getAbsolutePath().equals( other.baseDir.getAbsolutePath() ) ) {
throw new IllegalArgumentException("Project base directories do not match.");
}
this.sourceFolders.clear();
this.sourceFolders.addAll(other.sourceFolders);
this.outputFolder = other.outputFolder;
this.projectName = other.projectName;
this.executableName = other.executableName;
this.compilationRoot = other.compilationRoot;
this.setSourceFilenamePatterns( other.sourceFilenamePatterns );
setBuildOptions( other.getBuildOptions() );
setEmulationOptions( other.getEmulationOptions() );
}
/**
* Create instance.
*
* @param baseDir the top-level directory of this project.
* @throws IOException
*/
public ProjectConfiguration(File baseDir) throws IOException
{
this.baseDir = baseDir;
setSourceFilenamePatterns( DEFAULT_SOURCEFILENAME_PATTERNS );
}
public EmulationOptions getEmulationOptions() {
return new EmulationOptions(emulationOptions);
}
public void setEmulationOptions(EmulationOptions emulationOptions)
{
if (emulationOptions == null) {
throw new IllegalArgumentException(
"emulationOptions must not be null");
}
this.emulationOptions = new EmulationOptions( emulationOptions );
}
public String getExecutableName() {
return executableName;
}
public void setExecutableName(String executableName) {
if (StringUtils.isBlank(executableName)) {
throw new IllegalArgumentException(
"executableName must not be NULL/blank");
}
this.executableName = executableName;
}
/**
* (Re-)populates this project configuration instance from
* it's XML file.
*
* @throws IOException
*/
public void load() throws IOException
{
final File xmlFile = resolveRelativePath( PROJECT_CONFIG_FILE );
if ( ! xmlFile.exists() ) {
LOG.error("load(): File "+xmlFile.getAbsolutePath()+" does not exist?");
throw new IOException("File "+xmlFile.getAbsolutePath()+" does not exist?");
}
LOG.info("load(): Loading project configuration from "+xmlFile.getAbsolutePath());
final Document doc;
try {
doc = loadXML(xmlFile);
}
catch (Exception e)
{
LOG.error("Failed to load project description from "+
xmlFile.getAbsolutePath() );
throw new IOException("Failed to load project description from "+
xmlFile.getAbsolutePath(),e);
}
try {
parseXML( doc );
}
catch (Exception e)
{
throw new IOException("Failed to load project configuration",e);
}
}
/**
* Stores this project's configuration as an XML file.
*
* @throws IOException
*/
public void save() throws IOException {
if ( ! baseDir.exists() ) {
LOG.error("save(): Project base directory "+baseDir.getAbsolutePath()+" does not exist ?");
throw new IOException("Project base directory "+baseDir.getAbsolutePath()+" does not exist ?");
}
Document document;
try {
document = createDocumentBuilder().newDocument();
} catch (ParserConfigurationException e) {
LOG.error("Failed to save project configuration",e);
throw new IOException("Failed to save project configuration",e);
}
final Element root = document.createElement("project");
document.appendChild( root );
root.appendChild( createElement("name" , projectName , document ) );
root.appendChild( createElement("outputFolder" , outputFolder , document ) );
root.appendChild( createElement("executableName" , executableName , document ) );
// compilation root
if ( compilationRoot != null ) {
root.appendChild( createElement("compilationRoot" , compilationRoot , document ) );
}
// source filename patterns
final Element srcFilePatterns = createElement("sourceFilenamePatterns",document);
root.appendChild( srcFilePatterns );
for ( String pat : sourceFilenamePatterns ) {
srcFilePatterns.appendChild( createElement("sourceFilenamePattern" , pat , document ) );
}
// build options
final Element buildOptions = document.createElement("buildOptions");
root.appendChild( buildOptions );
this.buildOptions.saveBuildOptions( buildOptions , document );
// debugger options
final Element debugOptions = document.createElement("debuggerOptions");
root.appendChild( debugOptions );
this.debuggerOptions.saveDebuggerOptions(debugOptions,document);
// emulation options
final Element options = document.createElement("emulationOptions");
this.emulationOptions.saveEmulationOptions( options , document );
root.appendChild( options );
final Element srcFolderNode = createElement("sourceFolders",document);
root.appendChild( srcFolderNode );
for ( String folder : sourceFolders ) {
srcFolderNode.appendChild( createElement("sourceFolder" , folder , document ) );
}
try {
writeXML( document , resolveRelativePath( PROJECT_CONFIG_FILE ) );
} catch (Exception e) {
LOG.error("Failed to save project configuration",e);
throw new IOException("Failed to save project configuration",e);
}
}
private void writeXML(Document doc,File file) throws TransformerFactoryConfigurationError, TransformerException
{
final Source source = new DOMSource(doc);
final Result result = new StreamResult(file);
final Transformer xformer = TransformerFactory.newInstance().newTransformer();
xformer.transform(source, result);
}
private Element createElement(String tagName,String value,Document doc)
{
Element result = createElement( tagName , doc );
result.appendChild( doc.createTextNode( value ) );
return result;
}
private Element createElement(String tagName,Document doc) {
return doc.createElement( tagName );
}
/*
* <project>
* <name>myProject</name>
* <sourceFolders>
* <sourceFolder>src</sourceFolder>
* </sourceFolders>
* <outputFolder>bin</outputFolder>
* <executableName>a.out</executableName>
* </project>
*/
private void parseXML(Document doc) throws XPathExpressionException
{
final XPathFactory factory = XPathFactory.newInstance();
final XPath xpath = factory.newXPath();
final XPathExpression nameExpr = xpath.compile("/project/name");
final XPathExpression outputFolderExpr = xpath.compile("/project/outputFolder");
final XPathExpression executableNameExpr = xpath.compile("/project/executableName");
final XPathExpression srcFoldersExpr = xpath.compile("/project/sourceFolders/sourceFolder");
final XPathExpression emulationOptionsExpr = xpath.compile("/project/emulationOptions");
final XPathExpression buildOptionsExpr = xpath.compile("/project/buildOptions");
final XPathExpression srcFilePatternsExpr = xpath.compile("/project/sourceFilenamePatterns/sourceFilenamePattern");
final XPathExpression compilationRootExpr = xpath.compile("/project/compilationRoot");
final XPathExpression debuggerOptionsExpr = xpath.compile("/project/debuggerOptions");
this.outputFolder = getValue( outputFolderExpr , doc );
this.projectName = getValue( nameExpr , doc );
this.executableName = getValue( executableNameExpr , doc );
this.sourceFolders.clear();
this.sourceFolders.addAll( getValues( srcFoldersExpr , doc ) );
// compilation root
final List<String> roots = getValues( compilationRootExpr , doc );
if ( ! roots.isEmpty() )
{
if ( roots.size() > 1 ) {
throw new RuntimeException("Parse error, more than one compilation root in project config XML ?");
}
setCompilationRoot( new File( roots.get(0) ) );
}
// parse srcfile name patterns
final List<String> patterns = getValues( srcFilePatternsExpr , doc );
if ( ! patterns.isEmpty() ) {
setSourceFilenamePatterns( new HashSet<>(patterns) );
}
// parse emulation options
Element element = getElement(emulationOptionsExpr,doc);
if ( element == null ) {
this.emulationOptions = new EmulationOptions();
} else {
this.emulationOptions = EmulationOptions.loadEmulationOptions( element );
}
// parse build options
element = getElement(buildOptionsExpr,doc);
if ( element == null ) {
this.buildOptions = new BuildOptions();
} else {
this.buildOptions = BuildOptions.loadBuildOptions( element );
}
// parse build options
element = getElement(debuggerOptionsExpr,doc);
if ( element == null ) {
this.debuggerOptions.reset();
} else {
this.debuggerOptions.loadDebuggerOptions( element );
}
}
private String getValue(XPathExpression expr, Document doc) throws XPathExpressionException
{
List<String> values = getValues( expr , doc );
if ( values.isEmpty() ) {
throw new XPathExpressionException("Project XML lacks node matching "+expr);
}
if ( values.size() != 1 ) {
throw new XPathExpressionException("Project XML contains more than one node matching "+expr);
}
return values.get(0);
}
private Element getElement(XPathExpression expr,Document doc) throws XPathExpressionException
{
return (Element) expr.evaluate(doc, XPathConstants.NODE);
}
private List<String> getValues(XPathExpression expr, Document doc) throws XPathExpressionException {
final NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
final List<String> result = new ArrayList<String>();
for ( int i = 0 ; i < nodes.getLength() ; i++ ) {
final Node node = nodes.item( i );
final String value = node.getTextContent();
if ( value == null || StringUtils.isBlank( value ) ) {
LOG.error("getValues(): Invalid project XML - blank/empty value");
throw new XPathExpressionException("Invalid project XML - blank/empty value for "+
" expression "+expr);
}
result.add( value.trim() );
}
return result;
}
protected DocumentBuilder createDocumentBuilder() throws ParserConfigurationException {
final DocumentBuilderFactory fac = DocumentBuilderFactory.newInstance();
return fac.newDocumentBuilder();
}
protected Document loadXML(File xmlFile) throws ParserConfigurationException, SAXException, IOException
{
return createDocumentBuilder().parse( xmlFile );
}
/**
* Creates this project's base folder along with all
* source folders and output folder and saves the configuration
* to XML file {@link #PROJECT_CONFIG_FILE}.
*
* <p>
* If this project config has no source and/or output folder
* set, the source folder will be set to 'src' and the output
* folder will be set to 'bin'.
* </p>
* @throws IOException
*/
public void create() throws IOException
{
if ( StringUtils.isBlank( this.projectName ) ) {
LOG.error("create(): Cannot create project without a name");
throw new IllegalStateException("Cannot create project without a name");
}
if ( sourceFolders.isEmpty() )
{
sourceFolders.add( DEFAULT_SOURCE_FOLDER );
}
if ( outputFolder == null ) {
outputFolder = DEFAULT_OUTPUT_FOLDER;
}
if ( executableName == null ) {
executableName = DEFAULT_EXECUTABLE_NAME;
}
// used to keep track of created folders so we're
// able to remove them if something goes wrong on the way...
final List<File> createdFolders = new ArrayList<File>();
// create / check base directory
if ( Misc.checkFileExistsAndIsDirectory( baseDir , true ) ) {
createdFolders.add( baseDir );
}
try
{
// create source folders
for ( String src : sourceFolders )
{
final File absPath = resolveRelativePath( src );
if ( Misc.checkFileExistsAndIsDirectory( absPath , true ) )
{
createdFolders.add( absPath );
}
}
// create output folder
final File absOutputFolder = resolveRelativePath( outputFolder ); // note: creates missing directory
if ( Misc.checkFileExistsAndIsDirectory( absOutputFolder , true ) ) {
createdFolders.add( absOutputFolder );
}
save();
}
catch(IOException e)
{
for ( File folder : createdFolders )
{
Misc.deleteRecursively( folder );
}
throw e;
}
}
private File resolveRelativePath(String path) {
return new File( baseDir , path );
}
/**
* Returns absolute locations of source folders of this project.
*
* @return
*/
public List<File> getSourceFolders() {
final List<File> result = new ArrayList<File>();
for ( String srcFolder : sourceFolders ) {
result.add( resolveRelativePath( srcFolder ) );
}
return result;
}
/**
* Adds a source folder.
*
* @param file
*/
public void addSourceFolder(File file)
{
if (file == null) {
throw new IllegalArgumentException("file must not be NULL");
}
if ( ! sourceFolders.contains( file.getName() ) ) {
sourceFolders.add( file.getName() );
}
}
/**
* Set's this project's name.
*
* @param projectName
*/
public void setProjectName(String projectName) {
if (StringUtils.isBlank(projectName)) {
throw new IllegalArgumentException(
"projectName must not be NULL/blank");
}
this.projectName = projectName;
}
/**
* Returns this project's name.
*
* @return
*/
public String getProjectName() {
return projectName;
}
/**
* Sets this project's binary output folder.
*
* @param outputFolder
*/
public void setOutputFolder(File outputFolder) {
if (outputFolder == null) {
throw new IllegalArgumentException("outputFolder must not be NULL");
}
this.outputFolder = outputFolder.getName();
}
/**
* Returns this project's binary output folder.
*
* @return
*/
public File getOutputFolder() {
return resolveRelativePath( outputFolder );
}
public File getBaseDirectory() {
return baseDir;
}
public DebuggerOptions getDebuggerOptions() {
return this.debuggerOptions;
}
public BuildOptions getBuildOptions()
{
return new BuildOptions( buildOptions );
}
public void setBuildOptions(BuildOptions buildOptions)
{
this.buildOptions = new BuildOptions( buildOptions );
}
public static boolean isProjectConfigurationFile(File file) {
return file.isFile() && ProjectConfiguration.PROJECT_CONFIG_FILE.equals( file.getName() );
}
public Set<String> getSourceFilenamePatterns()
{
return sourceFilenamePatterns;
}
public void setSourceFilenamePatterns(Set<String> patterns)
{
final List<Pattern> newPatterns = new ArrayList<>();
for ( String pat : patterns ) {
newPatterns.add( Pattern.compile( pat ) );
}
this.sourceFilenamePatterns.clear();
this.sourceFilenameRegexPatterns.clear();
this.sourceFilenamePatterns.addAll( patterns );
this.sourceFilenameRegexPatterns.addAll( newPatterns );
}
public boolean isSourceFile(File file)
{
if ( ! file.isFile() ) {
return false;
}
final String fileName = file.getName() ;
for ( Pattern p : sourceFilenameRegexPatterns ) {
if ( p.matcher( fileName ).matches() ) {
return true;
}
}
return false;
}
/**
* Returns this project's compilation root.
* @return
*/
public File getCompilationRoot()
{
return ( compilationRoot == null ) ? null : new File(getBaseDirectory() , compilationRoot );
}
private static boolean isRelativePath(File f) {
return ! f.getPath().startsWith( File.separator );
}
public void setCompilationRoot(File compilationRoot)
{
if ( compilationRoot == null )
{
this.compilationRoot = null;
return;
}
if ( ! isRelativePath( compilationRoot ) && ! compilationRoot.getAbsolutePath().startsWith( getBaseDirectory().getAbsolutePath() ) )
{
throw new IllegalArgumentException("File "+compilationRoot.getAbsolutePath()+" is not inside this project's folder "+getBaseDirectory().getAbsolutePath());
}
// convert to path relative to base dir
String stripped = compilationRoot.getPath();
if ( ! isRelativePath( compilationRoot ) ) {
stripped =compilationRoot.getAbsolutePath().substring( getBaseDirectory().getAbsolutePath().length() );
while ( stripped.startsWith(File.separator ) ) {
stripped = stripped.substring(1);
}
}
this.compilationRoot = stripped;
}
}