com.buildml.main.CliUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.buildml.main.CliUtils.java

Source

/*******************************************************************************
 * 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:
 *    "Peter Smith <psmith@arapiki.com>" - initial API and 
 *        implementation and/or initial documentation
 *******************************************************************************/

package com.buildml.main;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;

import org.apache.commons.io.IOUtils;

import com.buildml.model.IActionMgr;
import com.buildml.model.IActionMgr.OperationType;
import com.buildml.model.IBuildStore;
import com.buildml.model.IFileMgr;
import com.buildml.model.IPackageMemberMgr;
import com.buildml.model.IPackageMemberMgr.PackageDesc;
import com.buildml.model.IPackageMgr;
import com.buildml.model.IPackageRootMgr;
import com.buildml.model.types.FileSet;
import com.buildml.model.types.ActionSet;
import com.buildml.utils.errors.ErrorCode;
import com.buildml.utils.print.PrintUtils;
import com.buildml.utils.string.ShellCommandUtils;

/**
 * A collection of utility methods that can be used by any CLI Command code. This
 * includes error reporting, command argument parsing, printing a FileSet and printing
 * a ActionSet. These methods are all static, so no object is required for them to be
 * invoked.
 * 
 * Note: A number of these methods are used for command-line validation, and could 
 * potentially abort the program without returning. They should therefore only be
 * used for command-line-based applications.
 * 
 * @author "Peter Smith <psmith@arapiki.com>"
 */
public class CliUtils {

    /*=====================================================================================*
     * FIELDS/TYPES
     *=====================================================================================*/

    /** Enumeration for specifying how a action's command string should be displayed. */
    public enum DisplayWidth {

        /** 
         * As much as possible of the action's command line should be displayed on one line 
         * (truncate the remainder of the line if it's too long).
         */
        ONE_LINE,

        /** 
         * If the command line is too long, wrap it onto multiple lines, using our custom-set
         * line width when splitting lines. Try to be intelligent about breaking lines at spaces,
         * rather than in the middle of words.
         */
        WRAPPED,

        /** 
         * Don't do any command-line wrapping, and just let the terminal wrap the line 
         * if it's too long. The whole string will be shown, across multiple lines.
         */
        NOT_WRAPPED
    }

    /** 
     * The number of columns (characters) per output line. This is used when wrapping text.
     */
    private static int columnWidth = 80;

    /** 
     * When validating command line arguments, this value is used to represent an unlimited
     * number of arguments.
     */
    public static final int ARGS_INFINITE = -1;

    /**
     * The number of characters to allow when printing the package details.
     */
    public static final int PACKAGE_NAME_WIDTH = 25;

    /*=====================================================================================*
     * Public methods
     *=====================================================================================*/

    /**
     * Given a user-specified string (from the command line), parse the specification into
     * a FileSet data structure containing all the relevant files. The string specification
     * is a colon-separated list of:
     *    <ol>
     *      <li> An absolute path name (starting with /), either a directory name or a file name. 
     *       If the path is a directory, add all files and directories below that point in the tree.</li>
     *    <li>A path name starting with a "@root" - the same rules apply as for #1.</li>
     *    <li>A single file name, with one or more wildcard (*) characters. All files that match
      *       the name are added, no matter what their directory.</li>
      *    <li>A package spec, starting with %pkg, or the complement of a package, starting 
      *       with %not-pkg.</li>
      *    </ol>
      *    
     * @param fileMgr The FileMgr object that manages the files.
     * @param pathSpecs A String of ":"-separated path specs (files, directories, or regular expressions).
     * @return A FileSet containing all the files that were selected by the command-line arguments.
     */
    public static FileSet getCmdLineFileSet(IFileMgr fileMgr, String pathSpecs) {

        String pathSpecList[] = pathSpecs.split(":");

        /* else populate a new FileSet */
        FileSet result = new FileSet(fileMgr);
        if (result.populateWithPaths(pathSpecList) != ErrorCode.OK) {
            CliUtils.reportErrorAndExit("Invalid path filter provided");
        }

        return result;
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Given a user-supplied set of command line arguments, parse those arguments and create
     * a suitable ActionSet containing all the relevant actions that match the specification. The
     * specification string is a colon-separated list of:
     * <ol>
     *     <li>A specific action number, which will be added to the ActionSet.</li>
     *   <li>The action number followed by [/depth] to indicate that all actions in the sub tree,
     *      starting at the specified action and moving down the action tree "depth" level, should
     *      be added.</li>
     *   <li>If 'depth' is omitted (only the '/' is provided), all actions is the subtree are added
     *      (regardless of their depth).</li>
     *   <li>If the action number is prefixed by '-', the actions are removed from the ActionSet, rather
     *      than being added. The "/depth" and "/" suffix can be used to remove subactions as well.
     *   <li>The special syntax "%pkg/foo" means all actions in the package "foo".</li>
     *   <li>The special syntax "%not-pkg/foo" means all actions outside the package "foo".</li>
     * </ol>
     * @param actionMgr The ActionMgr object to query.
     * @param actionSpecs The command line argument providing the action specification string.
     * @return The ActionSet, as described by the input action specification.
     */
    public static ActionSet getCmdLineActionSet(IActionMgr actionMgr, String actionSpecs) {

        String actionSpecList[] = actionSpecs.split(":");

        ActionSet result = new ActionSet(actionMgr);
        if (result.populateWithActions(actionSpecList) != ErrorCode.OK) {
            System.err.println("Error: Invalid action filter provided.");
            System.exit(1);
        }

        return result;
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Given a FileSet, display the files in that set in a pretty-printed format. This is 
     * used primarily for displaying the result of reports.
     * 
     * @param outStream The PrintStream on which the output should be displayed.
     * @param buildStore The BuildStore object containing the files to be listed.
     * @param resultFileSet The set of files to be displayed (if null, show them all).
     * @param filterFileSet If not-null, used to filter which paths from resultFileSet should be
     *         displayed (set to null to display everything).
     * @param showRoots Indicates whether path roots should be displayed.
     * @param showPkgs Indicates whether the package names should be displayed.
     */
    public static void printFileSet(PrintStream outStream, IBuildStore buildStore, FileSet resultFileSet,
            FileSet filterFileSet, boolean showRoots, boolean showPkgs) {

        IPackageRootMgr pkgRootMgr = buildStore.getPackageRootMgr();

        /*
         * This method uses recursion to traverse the VFS from the root to the leaves 
         * of the tree. It maintains a StringBuilder with the path encountered so far. 
         * That is, if the StringBuilder contains "/a/b/c" and the path "d" is encountered, 
         * then append "/d" to the StringBuilder to get "/a/b/c/d". Once we've finished 
         * traversing directory "d", we pop it off the StringBuilder and return to 
         * "/a/b/c/". This allows us to do a depth-first traversal of the VFS tree 
         * without doing more database access than we need to.
         * 
         * The resultFileSet and filterFileSet work together to determine which paths are
         * to be displayed. resultFileSet contains all the files from the relevant database
         * query. On the other hand, filterFileSet is the list of files that have been
         * selected by the user's command line argument (e.g. selecting a subdirectory, or
         * selecting files that match a pattern, such as *.c).
         */
        StringBuffer sb = new StringBuffer();

        /* call the helper function to display each of our children */
        printFileSetHelper(outStream, sb, buildStore, pkgRootMgr.getRootPath("root"), resultFileSet, filterFileSet,
                showRoots, showPkgs);
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Given a ActionSet, display the actions in that set in a pretty-printed format. To ensure
     * that all actions are displayed, you should first call ActionSet.populateWithParents().
     * 
     * @param outStream The PrintStream on which to display the output.
     * @param buildStore The database containing the action information.
     * @param resultActionSet The set of actions to be displayed (the results of some previous query).
     * @param filterActionSet The set of actions to actually be displayed (for post-filtering the query results).
     * @param outputFormat Mode for formatting the command strings.
     * @param showPkgs Set to true if the package names should be shown.
     */
    public static void printActionSet(PrintStream outStream, IBuildStore buildStore, ActionSet resultActionSet,
            ActionSet filterActionSet, DisplayWidth outputFormat, boolean showPkgs) {

        IActionMgr actionMgr = buildStore.getActionMgr();

        /* 
         * We always start at the top root, even though we may only display a subset
         * of the paths underneath that root.
         */
        int topRoot = actionMgr.getRootAction("");

        /* call the helper function to display each of our children */
        Integer children[] = actionMgr.getChildren(topRoot);
        for (int i = 0; i < children.length; i++) {
            printActionSetHelper(outStream, buildStore, children[i], resultActionSet, filterActionSet, outputFormat,
                    showPkgs, 1);
        }
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Return the number of columns (characters) of output to be printed per report line.
     * @return The number of columns.
     */
    public static int getColumnWidth() {
        return columnWidth;
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Set the number of columns (characters) of output to be printed per report line. The
     * minimum width is 40 characters. Any attempt to set a narrower width will revert to 40.
     * @param newWidth The new column width to set.
     */
    public static void setColumnWidth(int newWidth) {
        if (newWidth < 40) {
            newWidth = 40;
        }
        columnWidth = newWidth;
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Parse the user-supplied package/scope string, and return the corresponding
     * package ID and scope ID. The string should be in the format "package" or 
     * "package/scope". If "scope" is not provided (and scopeAllowed is true),
     * "private" is assumed. If the input is invalid, display a meaningful error message
     * and exit the program.
     * 
     * This method may abort the whole program (never returning) if the input string
     * is invalid.
     * 
     * @param buildStore The IBuildStore containing the package information.
     * @param pkgString The user-supplied input string (could be anything).
     * @param scopeAllowed True if the input string is allowed to provide a scope name. 
     * @return An array of two integers. The first is the package's ID number,
     * and the second is the scope's ID number.
     */
    public static int[] parsePackageAndScope(IBuildStore buildStore, String pkgString, boolean scopeAllowed) {

        IPackageMgr pkgMgr = buildStore.getPackageMgr();
        IPackageMemberMgr pkgMemberMgr = buildStore.getPackageMemberMgr();
        String pkgName = null;
        String scopeName = null;

        /* check if there's a '/' in the string, to separate "package" from "scope" */
        int slashIndex = pkgString.indexOf('/');
        if (slashIndex != -1) {
            pkgName = pkgString.substring(0, slashIndex);
            scopeName = pkgString.substring(slashIndex + 1);
            if (!scopeAllowed) {
                CliUtils.reportErrorAndExit("Invalid syntax - '/" + scopeName + "' not allowed.");
            }
        }

        /* else, there's no /, assume 'private' for the scope */
        else {
            pkgName = pkgString;
            scopeName = "private";
        }

        /* compute the IDs */
        int pkgId = pkgMgr.getId(pkgName);
        int scopeId = pkgMemberMgr.getScopeId(scopeName);

        if (pkgId == ErrorCode.NOT_FOUND) {
            CliUtils.reportErrorAndExit("Unknown package: " + pkgName);
        }
        if (scopeId == ErrorCode.NOT_FOUND) {
            CliUtils.reportErrorAndExit("Unknown scope name: " + scopeName);
        }

        return new int[] { pkgId, scopeId };
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Validation function to ensure that the number of arguments provided to a command
     * is in range between minArgs and maxArgs.
     * 
     * This method may abort the whole program (never returning) if the number of input
     * arguments is invalid.
     * 
     * @param cmdName The name of the command being executed.
     * @param cmdArgs The array of input arguments.
     * @param minArgs The minimum number of arguments required (0 or higher).
     * @param maxArgs The maximum number of arguments required (0 or higher - possibly ARGS_INFINITE).
     * @param message An error message to display if an invalid number of arguments is included.
     */
    public static void validateArgs(String cmdName, String[] cmdArgs, int minArgs, int maxArgs, String message) {

        int actualArgs = cmdArgs.length;

        /* too few arguments? */
        if (actualArgs < minArgs) {
            reportErrorAndExit("Too few arguments to " + cmdName + " - " + message);
        }

        /* too many arguments? */
        else if ((maxArgs != ARGS_INFINITE) && (actualArgs > maxArgs)) {
            reportErrorAndExit("Too many arguments to " + cmdName + " - " + message);
        }
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Display an error message in a standard format, then exit the program with a non-zero
     * error code. This method call never returns.
     * 
     * @param message The message to be display. If null, just exit without displaying.
     */
    public static void reportErrorAndExit(String message) {
        if (message != null) {
            System.err.println("Error: " + message);
            System.err.println("       Use bml -h for more help.");
        }
        System.exit(1);
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Given that a command-line user may have specified the --read and --write command line 
     * options, return the appropriate OperationType value that can be used for querying the database.
     * 
     * @param optionRead Set if the user provided the --read flag.
     * @param optionWrite Set if the user provided the --write flag.
     * @param optionModify Set if the user provided the --modify flag.
     * @param optionDelete Set if the user provided the --delete flag.
     * @return Either OP_UNSPECIFIED (search for either), OP_READ, or OP_WRITE
     */
    public static OperationType getOperationType(boolean optionRead, boolean optionWrite, boolean optionModify,
            boolean optionDelete) {

        OperationType opType = OperationType.OP_UNSPECIFIED;
        int optionsProvided = 0;
        if (optionRead) {
            opType = OperationType.OP_READ;
            optionsProvided++;
        }
        if (optionWrite) {
            opType = OperationType.OP_WRITE;
            optionsProvided++;
        }
        if (optionModify) {
            opType = OperationType.OP_MODIFIED;
            optionsProvided++;
        }
        if (optionDelete) {
            opType = OperationType.OP_DELETE;
            optionsProvided++;
        }

        /* can't have more than one option provided at one time. */
        if (optionsProvided > 1) {
            System.err.println("Error: can't specify more than one of --read, --write, --modify or --delete.");
            System.exit(-1);
        }

        return opType;
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Generate a localized help message, for use when displaying online help.
     * The provided message string will most likely contain a single line with
     * an "#include" directive, which has the effect of pulling in another text
     * file containing the main body of the message. This text file may also pull
     * in other text files for inclusion.
     * 
     * When searching for an included text file, the "messages/&lt;lang%gt;/" directory
     * is searched, where %lt;lang%gt; is a language specifier, such as "en".
     * 
     * @param message Message to display, which most likely contains a #include directive.
     * @return The full message string, which may be hundreds of lines long.
     */
    public static String genLocalizedMessage(String message) {

        /* 
         * We use recursion to pull all the messages (and possibly nested files)
         * into the final string.
         */
        StringBuffer sb = new StringBuffer();
        genLocalizedMessageHelper("en", message, sb);
        return sb.toString();
    }

    /*=====================================================================================*
     * Private methods
     *=====================================================================================*/

    /**
     * The CliUtils class can not be instantiated. Use the static methods only.
     */
    private CliUtils() {
        /* empty */
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Helper method for displaying a path and all it's children, called exclusively by
     * printFileSet().
     * 
     * @param outStream The PrintStream on which to display paths.
     * @param pathSoFar This path's parent path as a string, complete with trailing "/".
     * @param buildStore The BuildStore in which these paths belong.
     * @param thisPathId The path to display (assuming it's in the filesToShow FileSet).
     * @param resultFileSet The set of files to be displayed (if null, show them all).
     * @param filterFileSet If not-null, used to filter which paths from resultFileSet
     *          should be displayed (set to null to display everything).
     * @param showRoots Whether to show path roots.
     * @param showPkgs Whether to show the package names.
     */
    private static void printFileSetHelper(PrintStream outStream, StringBuffer pathSoFar, IBuildStore buildStore,
            int thisPathId, FileSet resultFileSet, FileSet filterFileSet, boolean showRoots, boolean showPkgs) {

        IFileMgr fileMgr = buildStore.getFileMgr();
        IPackageMgr pkgMgr = buildStore.getPackageMgr();
        IPackageMemberMgr pkgMemberMgr = buildStore.getPackageMemberMgr();
        IPackageRootMgr pkgRootMgr = buildStore.getPackageRootMgr();

        /* StringBuilders for forming the package name and the root names */
        StringBuilder pkgString = null;
        StringBuilder rootString = null;

        /* should this path be displayed? */
        if (!shouldBeDisplayed(thisPathId, resultFileSet, filterFileSet)) {
            return;
        }

        /* fetch this path's name */
        String baseName = fileMgr.getBaseName(thisPathId);

        /* get this path's list of children */
        Integer children[] = fileMgr.getChildPaths(thisPathId);

        /*
         * Figure out whether this path has attached roots.
         */
        String rootNames[] = null;
        if (showRoots) {
            rootNames = pkgRootMgr.getRootsAtPath(thisPathId);
        }

        /* 
         * If we've been asked to display file packages, prepare the string to be printed.
         */
        if (showPkgs) {

            pkgString = new StringBuilder();

            /* fetch the file's package and scope */
            PackageDesc pkgAndScopeId = pkgMemberMgr.getPackageOfMember(IPackageMemberMgr.TYPE_FILE, thisPathId);
            if (pkgAndScopeId == null) {
                pkgString.append("Invalid file");
            }

            /* if valid, fetch the human-readable names */
            else {
                String pkgName = pkgMgr.getName(pkgAndScopeId.pkgId);
                String scopeName = pkgMemberMgr.getScopeName(pkgAndScopeId.pkgScopeId);

                /* if we can't fetch the text name of the package or scope... */
                if (pkgName == null || scopeName == null) {
                    pkgString.append("Invalid package");
                }

                /* else, both names are valid, append them to the string */
                else {
                    pkgString.append(pkgName);
                    pkgString.append(" - ");
                    pkgString.append(scopeName);
                }
            }
        }

        /* 
         * Does this path have a root (and we were asked to show roots)?
         * If so, prepare the string to be printed.
         */
        if ((rootNames != null) && (rootNames.length > 0)) {
            rootString = new StringBuilder();

            /* display a root name, or comma-separated root names */
            rootString.append(" (");
            for (int i = 0; i < rootNames.length; i++) {
                if (i != 0) {
                    rootString.append(' ');
                }
                rootString.append('@');
                rootString.append(rootNames[i]);
            }
            rootString.append(')');
        }

        /* show packages, if requested. Truncate to a fixed column width. */
        if (pkgString != null) {
            if (pkgString.length() > PACKAGE_NAME_WIDTH - 1) {
                pkgString.setLength(PACKAGE_NAME_WIDTH - 1);
            }
            outStream.print(pkgString);
            PrintUtils.indent(outStream, PACKAGE_NAME_WIDTH - pkgString.length());
        }

        /* Display this path, prefixed by the absolute pathSoFar */
        outStream.print(pathSoFar);
        outStream.print(baseName);

        /* show roots, if requested */
        if (rootString != null) {
            outStream.print(rootString);
        }
        outStream.println();

        /* if there are children, call ourselves recursively to display them */
        if (children.length != 0) {

            /* append this path onto the pathSoFar, since it'll become the pathSoFar for each child */
            int pathSoFarLen = pathSoFar.length();
            pathSoFar.append(baseName);
            if (baseName.charAt(0) != '/') {
                pathSoFar.append('/');
            }

            /* display each of the children */
            for (int i = 0; i < children.length; i++) {
                printFileSetHelper(outStream, pathSoFar, buildStore, children[i], resultFileSet, filterFileSet,
                        showRoots, showPkgs);
            }

            /* remove our base name from the pathSoFar, so our caller sees the correct value again */
            pathSoFar.setLength(pathSoFarLen);
        }
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * A helper method, called exclusively by printActionSet(). This method calls itself recursively
     * as it traverses the ActionSet's tree structure.
     * 
     * @param outStream The PrintStream on which to display the output.
     * @param buildStore The database containing file, action and package information.
     * @param actionId The ID of the action we're currently displaying (at this level of recursion).
     * @param resultActionSet The full set of actions to be displayed (the result of some previous query).
     * @param filterActionSet The set of actions to actually be displayed (for post-filtering the query results).
     * @param outputFormat The way in which the actions should be formatted.
     * @param showPkgs Set to true if we should display package names.
     * @param indentLevel The number of spaces to indent this action by (at this recursion level).
     */
    private static void printActionSetHelper(PrintStream outStream, IBuildStore buildStore, int actionId,
            ActionSet resultActionSet, ActionSet filterActionSet, DisplayWidth outputFormat, boolean showPkgs,
            int indentLevel) {

        IActionMgr actionMgr = buildStore.getActionMgr();
        IFileMgr fileMgr = buildStore.getFileMgr();
        IPackageMgr pkgMgr = buildStore.getPackageMgr();
        IPackageMemberMgr pkgMemberMgr = buildStore.getPackageMemberMgr();

        /* 
         * Display the current action, at the appropriate indentation level. The format is:
         * 
         * - Action 1 (/home/psmith/t/cvs-1.11.23)
          *     if test ! -f config.h; then rm -f stamp-h1; emake  stamp-h1; else :;
          *     
          * -- Action 2 (/home/psmith/t/cvs-1.11.23)
          *      failcom='exit 1'; for f in x $MAKEFLAGS; do case $f in *=* | --[!k]*);; \
          *
          * Where Action 1 is the parent of Action 2.
          */

        /* is this action in the ActionSet to be printed? If not, terminate recursion */
        if (!(((resultActionSet == null) || (resultActionSet.isMember(actionId)))
                && ((filterActionSet == null) || (filterActionSet.isMember(actionId))))) {
            return;
        }

        /* 
         * Fetch the action's command string (if there is one). It can either be
         * in short format (on a single line), or a full string (possibly multiple lines)
         */
        String command = (String) actionMgr.getSlotValue(actionId, IActionMgr.COMMAND_SLOT_ID);
        if (command == null) {
            command = "<unknown command>";
        } else if (outputFormat == DisplayWidth.ONE_LINE) {
            command = ShellCommandUtils.getCommandSummary(command, getColumnWidth() - indentLevel - 3);
        }

        /* fetch the name of the directory the action was executed in */
        int actionDirId = (Integer) actionMgr.getSlotValue(actionId, IActionMgr.DIRECTORY_SLOT_ID);
        String actionDirName = fileMgr.getPathName(actionDirId);

        /* display the correct number of "-" characters */
        for (int i = 0; i != indentLevel; i++) {
            outStream.append('-');
        }
        outStream.print(" Action " + actionId + " (" + actionDirName);

        /* if requested, display the action's package name */
        if (showPkgs) {
            PackageDesc pkg = pkgMemberMgr.getPackageOfMember(IPackageMemberMgr.TYPE_ACTION, actionId);
            if (pkg == null) {
                outStream.print(" - Invalid action");
            } else {
                String pkgName = pkgMgr.getName(pkg.pkgId);
                if (pkgName == null) {
                    outStream.print(" - Invalid package");
                } else {
                    outStream.print(" - " + pkgName);
                }
            }
        }
        outStream.println(")");

        /* display the action's command string. Each line must be indented appropriately */
        if (outputFormat != DisplayWidth.NOT_WRAPPED) {
            PrintUtils.indentAndWrap(outStream, command, indentLevel + 3, getColumnWidth());
            outStream.println();
        } else {
            outStream.println(command);
        }

        /* recursively call ourselves to display each of our children */
        Integer children[] = actionMgr.getChildren(actionId);
        for (int i = 0; i < children.length; i++) {
            printActionSetHelper(outStream, buildStore, children[i], resultActionSet, filterActionSet, outputFormat,
                    showPkgs, indentLevel + 1);
        }

    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Determine whether this path is in the set of paths to be displayed. That is, it's
     * in the resultFileSet as well as being part of filterFileSet.
     * 
     * @param thisPathId The ID of the path we might want to display.
     * @param resultFileSet The set of paths in the result set.
     * @param filterFileSet The set of paths in the filter set.
     * @return Whether or not the path should be displayed.
     */
    private static boolean shouldBeDisplayed(int thisPathId, FileSet resultFileSet, FileSet filterFileSet) {

        return ((resultFileSet == null) || (resultFileSet.isMember(thisPathId)))
                && ((filterFileSet == null) || (filterFileSet.isMember(thisPathId)));
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * A helper function for genLocalizedMessage, used for recursion.
     * 
     * @param lang The language to localize into (e.g. "en" or "fr").
     * @param message The message to be displayed (possibly including #include).
     * @param sb The StringBuffer we'll use to build up the final string.
     */
    private static void genLocalizedMessageHelper(String lang, String message, StringBuffer sb) {

        /* tokenize the string, and handle each line separately */
        String[] lines = message.split("\n");
        for (int i = 0; i < lines.length; i++) {
            String thisLine = lines[i];

            /* 
             * If this line contains an #include directive, read the file content, then
             * call ourselves recursively to process the content.
             */
            if (thisLine.matches("#include .*")) {
                String includeLine[] = thisLine.split(" ");

                /* try to open the file (resource) as a stream */
                String fileName = "messages/" + lang + "/" + includeLine[1];
                InputStream inStream = ClassLoader.getSystemResourceAsStream(fileName);
                if (inStream == null) {
                    sb.append("<missing include: " + fileName + ">\n");
                }

                /* read the stream into a string, then recursively process it */
                else {
                    try {
                        String content = IOUtils.toString(inStream, "UTF-8");
                        genLocalizedMessageHelper(lang, content, sb);

                    } catch (IOException e1) {
                        sb.append("<invalid include: " + fileName + ">\n");
                    }

                    try {
                        inStream.close();
                    } catch (IOException e) {
                        /* nothing */
                    }
                }
            }

            /* no #include directive, so just include the line verbatim */
            else {
                sb.append(lines[i]);
                sb.append('\n');
            }
        }

    }

    /*-------------------------------------------------------------------------------------*/
}