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