de.codesourcery.jasm16.ide.ProjectConfiguration.java Source code

Java tutorial

Introduction

Here is the source code for de.codesourcery.jasm16.ide.ProjectConfiguration.java

Source

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