/******************************************************************************* * Copyright (c) 2012 Arapiki Solutions Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * psmith - initial API and * implementation and/or initial documentation *******************************************************************************/ package com.buildml.config; import java.io.File; import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.Set; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; import com.buildml.model.IBuildStore; import com.buildml.model.IPackageMgr; import com.buildml.model.IPackageRootMgr; import com.buildml.utils.errors.ErrorCode; /** * Objects of this class manage the per-tree configuration data that's associated with * each checked-out source tree. Alongside the standard build.bml file, there's a * ".bmlconfig" file that holds per-tree aliases, native root paths, etc. All reading, * writing and management of this configuration file is handled by this class. * * @author Peter Smith <psmith@arapiki.com> */ public class PerTreeConfigFile { /*=====================================================================================* * FIELDS/TYPES *=====================================================================================*/ /** version of the XML file schema */ public static final int SCHEMA_VERSION = 1; /** The native path to the configuration file */ private File configFile = null; /** A mapping from alias names to the list of packages the alias represents */ private HashMap<String, String[]> aliasMap; /** A mapping from root names to native file system paths */ private HashMap<String, String> rootMap; /** The BuildStore that this config file augments */ private IBuildStore buildStore; /*=====================================================================================* * CONSTRUCTORS *=====================================================================================*/ /** * Create a new {@link PerTreeConfigFile}. * * @param buildStore The IBuildStore that this configuration is associated with. * @param configFile The path on the native file system to the configuration file. * This file will be read from disk when the object is instantiated, * and written back to disk when a save() operation is invoked. * @throws IOException An error occurred while opening/parsing the file. * */ public PerTreeConfigFile(IBuildStore buildStore, File configFile) throws IOException { this.buildStore = buildStore; this.configFile = configFile; /* create empty data structures - to be populated from the file, or programmatically */ aliasMap = new HashMap<String, String[]>(); rootMap = new HashMap<String, String>(); /* parse the content of the file into memory, if it exists, else create it. */ if (configFile.exists()) { try { load(configFile); } catch (SAXException e) { /* translate SAXException into IOException */ throw new IOException(e.getMessage()); } } else { save(); } } /*=====================================================================================* * PUBLIC METHODS *=====================================================================================*/ /** * Write the content of the configuration to the disk file. The whole set of in-memory * data structures are written to the file, overwriting any previous content in the file. * * @throws IOException A problem occurred while writing to the file. */ public void save() throws IOException { PrintWriter out = new PrintWriter(new FileWriter(configFile)); out.println("<bmlconfig version=\"" + SCHEMA_VERSION + "\">"); /* write out alias information */ for (Iterator<String> iter = aliasMap.keySet().iterator(); iter.hasNext();) { String aliasName = (String) iter.next(); out.println(" <alias name=\"" + aliasName + "\">"); String pkgs[] = getAlias(aliasName); for (int i = 0; i < pkgs.length; i++) { out.println(" <package name=\"" + pkgs[i] + "\"/>"); } out.println(" </alias>"); } /* write out root mapping information */ for (Iterator<String> iter = rootMap.keySet().iterator(); iter.hasNext();) { String rootName = (String) iter.next(); String nativePath = getNativeRootMapping(rootName); out.println(" <rootmap name=\"" + rootName + "\" path=\"" + nativePath + "\"/>"); } out.println("</bmlconfig>"); out.close(); } /*-------------------------------------------------------------------------------------*/ /** * Add or update a build alias in the configuration. An alias can be used as a short-cut * for specifying a list of packages to be built. * * @param aliasName The alias name. * @param packages An array of package names to be built when the alias is built. * @return ErrorCode.OK on success, ErrorCode.INVALID_NAME if the alias name is not * legal, ErrorCode.BAD_VALUE if one of the package names is invalid. */ public int addAlias(String aliasName, String packages[]) { /* valid the alias's name */ if (!isValidAliasName(aliasName)) { return ErrorCode.INVALID_NAME; } /* valid the list of packages */ if ((packages == null) || (packages.length == 0)) { return ErrorCode.BAD_VALUE; } for (int i = 0; i < packages.length; i++) { if (!isValidPackage(packages[i])) { return ErrorCode.BAD_VALUE; } } /* make a copy of the input array, and make sure it's sorted */ String sortedPackages[] = Arrays.copyOf(packages, packages.length); Arrays.sort(sortedPackages); aliasMap.put(aliasName, sortedPackages); return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /** * Remove an existing build alias from the configuration. * * @param aliasName The name of the alias to be removed. * @return ErrorCode.OK on success, or ErrorCode.NOT_FOUND if the alias is not defined. */ public int removeAlias(String aliasName) { Object previous = aliasMap.remove(aliasName); if (previous == null) { return ErrorCode.NOT_FOUND; } return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /** * Return the array of packages that are associated with the specified alias. * * @param aliasName Name of the alias to expand. * @return An array of package names to be built, or null if the alias is undefined. */ public String[] getAlias(String aliasName) { return (String[])aliasMap.get(aliasName); } /*-------------------------------------------------------------------------------------*/ /** * @return The configure file's list of build aliases, in alphabetic order. */ public String[] getAliases() { Set<String> keySet = aliasMap.keySet(); String result[] = keySet.toArray(new String[keySet.size()]); Arrays.sort(result); return result; } /*-------------------------------------------------------------------------------------*/ /** * Add a mapping between a package root (such as "pkg_src" or "pkg_gen") to a native * file system path. This is used when specifying where on the native file system the * package's file can actually be found. * * @param rootName Name of the root to be mapped (e.g. "pkg_src"). * @param nativePath The native file system path to map to the root. * @return ErrorCode.OK on success, ErrorCode.NOT_FOUND if the root name is invalid, * or ErrorCode.BAD_PATH if the native path is not a valid directory. */ public int addNativeRootMapping(String rootName, String nativePath) { int type = getRootType(rootName); int pkgId = getRootPathId(rootName); if ((type == ErrorCode.INVALID_NAME) || (pkgId == ErrorCode.NOT_FOUND)) { return ErrorCode.NOT_FOUND; } /* validate that the native path is a valid directory */ File nativeFile = new File(nativePath); if (!nativeFile.exists() || !nativeFile.isDirectory()) { return ErrorCode.BAD_PATH; } /* * Now write the native root information into our internal data structure, so * the information will be persisted to disk when a save() is invoked. */ rootMap.put(rootName, nativePath); return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /** * Remove a previously added mapping between a package root and a native file system * directory. This is the opposite of addNativeRootMapping(). * * @param rootName Name of the root to be mapped (e.g. "pkg_src"). * @return ErrorCode.OK on success, or ErrorCode.NOT_FOUND if the root name is invalid. */ public int clearNativeRootMapping(String rootName) { /* validate the root name */ int type = getRootType(rootName); int pkgId = getRootPathId(rootName); if ((type == ErrorCode.INVALID_NAME) || (pkgId == ErrorCode.NOT_FOUND)) { return ErrorCode.NOT_FOUND; } /* remove it from our internal data structure */ rootMap.remove(rootName); return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /** * Return the native path associated with a specified package root. * * @param rootName Name of the root to be queried. * @return The native path that this root is mapped to, or null if there's no mapping * or the package name is invalid. */ public String getNativeRootMapping(String rootName) { return rootMap.get(rootName); } /*=====================================================================================* * PRIVATE METHODS *=====================================================================================*/ /** * Read the content of the configuration file into in-memory data structures. * * @param configFile The native file containing the configuration data (in XML). * @throws SAXException An error occurred while parsing the XML. * @throws IOException An error occurred while opening/reading the file. */ private void load(File configFile) throws SAXException, IOException { /* Open the file for input, and report progress every two seconds */ FileInputStream in = new FileInputStream(configFile); /* * Create a new XMLReader to parse this file, then set the ContentHandler * to our own SAX handler class. */ XMLReader parser = XMLReaderFactory.createXMLReader(); ContentHandler contentHandler = new PerTreeConfigSAXHandler(this); parser.setContentHandler(contentHandler); try { parser.parse(new InputSource(in)); } finally { in.close(); } } /*-------------------------------------------------------------------------------------*/ /** * Determine whether a user-supplied alias name is valid. * * @param aliasName The alias name. * @return True if the name is valid, else false. */ private boolean isValidAliasName(String aliasName) { if ((aliasName == null) || (aliasName.length() == 0)) { return false; } for (int i = 0; i < aliasName.length(); i++) { char ch = aliasName.charAt(i); if ((ch != '_') && (!Character.isAlphabetic(ch))) { return false; } } return true; } /*-------------------------------------------------------------------------------------*/ /** * Determine whether the specified package is valid (in the BuildStore). * * @param pkgName The package's name. * @return True if the package is valid, else false. */ private boolean isValidPackage(String pkgName) { IPackageMgr pkgMgr = buildStore.getPackageMgr(); return pkgMgr.getId(pkgName) != ErrorCode.NOT_FOUND; } /*-------------------------------------------------------------------------------------*/ /** * Helper function for determining the package ID associated with the provided root name. * * @param rootName The name of the root. * @return The package ID, or ErrorCode.NOT_FOUND if the package name is invalid. */ private int getRootPathId(String rootName) { if ((rootName == null) || (rootName.length() < "_src".length())) { return ErrorCode.NOT_FOUND; } IPackageMgr pkgMgr = buildStore.getPackageMgr(); String pkgName = rootName.substring(0, rootName.length() - "_src".length()); return pkgMgr.getId(pkgName); } /*-------------------------------------------------------------------------------------*/ /** * Helper function for determining the type (SOURCE_ROOT or GENERATED_ROOT) of a root * name. This method exits with an error message if the root name is invalid. * * @param rootName The name of the root. * @return Either SOURCE_ROOT or GENERATED_ROOT, or ErrorCode.INVALID_NAME if * the root name doesn't end with _src or _gen. */ private int getRootType(String rootName) { if (rootName == null) { return ErrorCode.INVALID_NAME; } int type = 0; if (rootName.endsWith("_src")) { type = IPackageRootMgr.SOURCE_ROOT; } else if (rootName.endsWith("_gen")) { type = IPackageRootMgr.GENERATED_ROOT; } else { type = ErrorCode.INVALID_NAME; } return type; } /*-------------------------------------------------------------------------------------*/ }