0001: /*
0002: * Copyright (c) 2000-2001 Sosnoski Software Solutions, Inc.
0003: *
0004: * Permission is hereby granted, free of charge, to any person obtaining a copy
0005: * of this software and associated documentation files (the "Software"), to deal
0006: * in the Software without restriction, including without limitation the rights
0007: * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
0008: * copies of the Software, and to permit persons to whom the Software is
0009: * furnished to do so, subject to the following conditions:
0010: *
0011: * The above copyright notice and this permission notice shall be included in
0012: * all copies or substantial portions of the Software.
0013: *
0014: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
0015: * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
0016: * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
0017: * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
0018: * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
0019: * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
0020: * IN THE SOFTWARE.
0021: */
0022:
0023: import java.io.*;
0024: import java.util.*;
0025:
0026: /**
0027: * Source configuration processor program. This program processes a set of Java
0028: * source files (or standard input to standard output, if no source files are
0029: * specified), scanning for configuration control lines. A control line is a
0030: * line that starts with "//#token*" (valid only as the first line of a
0031: * file), "//#token{" (beginning an option block), "//#}token{" (inverting an
0032: * option block), or "//#token} (closing an option block). The first two formats
0033: * also allow a '!' immediately before the token in order to invert the token
0034: * state.<p>
0035: *
0036: * The token strings to be processed are specified on the command line as
0037: * either enabled or disabled. See the program documentation for more details
0038: * of the command line options and usage.<p>
0039: *
0040: * Nested option blocks are allowed, but overlapping option blocks are an
0041: * error. In order to ensure proper processing of nested option blocks, the
0042: * user should generally specify <b>every</b> token used for the nested blocks
0043: * as either enabled or disabled if <b>any</b> of them are either enabled or
0044: * disabled. It is an error if an indeterminant beginning of block token (one
0045: * with a token which is not on either list) is immediately contained within
0046: * a block with an enabled token. In other words, the case:
0047: * <pre>
0048: * //#a{
0049: * //#b{
0050: * //#c{
0051: * //#c}
0052: * //#b}
0053: * //#a}
0054: * </pre>
0055: * gives an error if "a" is on the enabled list and "b" is not on either list,
0056: * or if "a" is not on the disabled list, "b" is on the enabled list, and "c"
0057: * is not on either list.
0058: *
0059: * @author Dennis M. Sosnoski
0060: * @version 0.8
0061: */
0062:
0063: public class JEnable {
0064: /** Lead text for option token. */
0065: protected static final String OPTION_LEAD = "//#";
0066: /** Length of lead text for option token. */
0067: protected static final int LEAD_LENGTH = OPTION_LEAD.length();
0068:
0069: /** Size of buffer used to copy file. */
0070: protected static final int COPY_BUFFER_SIZE = 4096;
0071:
0072: /** Return code for not an option line. */
0073: protected static final int NOT_OPTION = 0;
0074: /** Return code for file option line. */
0075: protected static final int FILE_OPTION = 1;
0076: /** Return code for block start option line. */
0077: protected static final int BLOCK_START_OPTION = 2;
0078: /** Return code for block else option line. */
0079: protected static final int BLOCK_ELSE_OPTION = 3;
0080: /** Return code for block end option line. */
0081: protected static final int BLOCK_END_OPTION = 4;
0082: /** Return code for block comment option line. */
0083: protected static final int BLOCK_COMMENT_OPTION = 5;
0084:
0085: /** Error text for file option not on first line. */
0086: protected static final String FILE_OPTION_ERR = "file option token must be first line of file";
0087: /** Error text for token nested within block for same token. */
0088: protected static final String NESTED_SAME_ERR = "block option start cannot be nested within block with same token";
0089: /** Error text for indeterminant token nested in enabled block. */
0090: protected static final String INDETERM_ERR = "block option token within enabled block must be enabled or disabled";
0091: /** Error text for block end token not matching block start token. */
0092: protected static final String UNBALANCED_ERR = "block option end without matching start";
0093: /** Error text for block else token not matching block start token. */
0094: protected static final String BADELSE_ERR = "block option else without matching start";
0095: /** Error text for end of file with open block. */
0096: protected static final String UNCLOSED_ERR = "end of file with open block";
0097: /** Error text for unknown option token type. */
0098: protected static final String UNKNOWN_OPTION_ERR = "unknown option line type";
0099: /** Error text for file option line with unknown file extension. */
0100: protected static final String EXTENSION_ERR = "unknown file option line extension";
0101: /** Error text for unable to rename file to backup directory. */
0102: protected static final String BACKUP_DIR_ERR = "unable to create backup directory";
0103: /** Error text for unable to delete old backup file. */
0104: protected static final String OLD_BACKUP_ERR = "unable to delete old backup file";
0105: /** Error text for unable to rename file within directory. */
0106: protected static final String BACKUP_FILE_ERR = "unable to rename file for backup";
0107: /** Error text for unable to delete original file. */
0108: protected static final String DELETE_ERR = "unable to delete input file";
0109: /** Error text for unable to rename original file. */
0110: protected static final String RENAME_ERR = "unable to rename source file";
0111: /** Error text for unable to rename temp file. */
0112: protected static final String TEMP_RENAME_ERR = "unable to rename temp file";
0113: /** Error text for unable to delete temporary file. */
0114: protected static final String TEMP_DELETE_ERR = "unable to delete temporary output file";
0115: /** Error text for unable to change file modify timestamp. */
0116: protected static final String STAMP_ERR = "unable to change file modify timestamp";
0117: /** Error text for token in both sets. */
0118: protected static final String DUAL_USE_ERR = "Same token cannot be both enabled and disabled";
0119:
0120: /** Preserve timestamp on modified files flag. */
0121: protected boolean m_keepStamp;
0122:
0123: /** Mark backup file with tilde flag. */
0124: protected boolean m_markBackup;
0125:
0126: /** List modified files flag. */
0127: protected boolean m_listModified;
0128:
0129: /** List all files processed flag. */
0130: protected boolean m_listProcessed;
0131:
0132: /** List file summary by path flag. */
0133: protected boolean m_listSummary;
0134:
0135: /** Backup root path (null if not backing up). */
0136: protected File m_backupDir;
0137:
0138: /** Map for enabled tokens (values same as keys). */
0139: protected Hashtable m_enabledTokens;
0140:
0141: /** Map for disabled tokens (values same as keys). */
0142: protected Hashtable m_disabledTokens;
0143:
0144: /** Number of files matched. */
0145: protected int m_matchedCount;
0146:
0147: /** Number of files modified. */
0148: protected int m_modifiedCount;
0149:
0150: /** Current option token, set by check method. */
0151: private String m_token;
0152:
0153: /** Inverted token flag, set by check method. */
0154: private boolean m_invert;
0155:
0156: /** Offset past end of token in line, set by check method. */
0157: private int m_endOffset;
0158:
0159: /**
0160: * Constructor.
0161: *
0162: * @param keep preserve timestamp on modified files flag
0163: * @param mark mark backup files with tilde flag
0164: * @param mods list modified files flag
0165: * @param quiet do not list file summaries by path flag
0166: * @param verbose list all files processed flag
0167: * @param backup root back for backup directory tree (<code>null</code> if
0168: * no backups)
0169: * @param enabled map of enabled tokens
0170: * @param disabled map of disabled tokens
0171: */
0172:
0173: protected JEnable(boolean keep, boolean mark, boolean mods,
0174: boolean quiet, boolean verbose, File backup,
0175: Hashtable enabled, Hashtable disabled) {
0176: m_keepStamp = keep;
0177: m_markBackup = mark;
0178: m_listModified = mods;
0179: m_listProcessed = verbose;
0180: m_listSummary = !quiet;
0181: m_backupDir = backup;
0182: m_enabledTokens = enabled;
0183: m_disabledTokens = disabled;
0184: }
0185:
0186: /**
0187: * Checks for valid first character of token. The first character must be
0188: * an alpha or underscore.
0189: *
0190: * @param chr character to be validated
0191: * @return <code>true</code> if valid first character, <code>false</code>
0192: * if not
0193: */
0194:
0195: protected static boolean isTokenLeadChar(char chr) {
0196: return (chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z')
0197: || chr == '_';
0198: }
0199:
0200: /**
0201: * Checks for valid body character of token. All body characters must be
0202: * an alpha, digits, or underscore.
0203: *
0204: * @param chr character to be validated
0205: * @return <code>true</code> if valid body character, <code>false</code>
0206: * if not
0207: */
0208:
0209: protected static boolean isTokenBodyChar(char chr) {
0210: return (chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z')
0211: || chr == '_' || (chr >= '0' && chr <= '9');
0212: }
0213:
0214: /**
0215: * Convenience method for generating an error report exception.
0216: *
0217: * @param lnum line number within file
0218: * @param line source line to check for option
0219: * @param msg error message text
0220: * @throws IOException wrapping the error information
0221: */
0222:
0223: protected void throwError(int lnum, String line, String msg)
0224: throws IOException {
0225: throw new IOException("Error on input line " + lnum + ", "
0226: + msg + ":\n" + line);
0227: }
0228:
0229: /**
0230: * Check if line is an option. Returns a code for the option line type
0231: * (or "not an option line"), with the actual token from the line stored
0232: * in the instance variable. Ugly technique, but the only easy way to
0233: * return multiple results without using another class.
0234: *
0235: * @param lnum line number within file (used for error reporting)
0236: * @param line source line to check for option
0237: * @return return code for option line type
0238: * @throws IOException on option line error
0239: */
0240:
0241: protected int checkOptionLine(int lnum, String line)
0242: throws IOException {
0243:
0244: // check if line is a candidate for option token
0245: int type = NOT_OPTION;
0246: m_token = null;
0247: m_invert = false;
0248: m_endOffset = 0;
0249: if (line.length() > LEAD_LENGTH && line.startsWith(OPTION_LEAD)) {
0250:
0251: // check for special leading character before token
0252: int offset = LEAD_LENGTH;
0253: char lead = line.charAt(offset);
0254: if (lead == '!' || lead == '}') {
0255: offset++;
0256: } else {
0257: lead = 0;
0258: }
0259:
0260: // make sure a valid token start character follows
0261: int start = offset;
0262: if (isTokenLeadChar(line.charAt(start))) {
0263:
0264: // parse the token characters
0265: int scan = LEAD_LENGTH + 1;
0266: while (scan < line.length()) {
0267: char chr = line.charAt(scan++);
0268: if (!isTokenBodyChar(chr)) {
0269:
0270: // token found, classify and set type
0271: m_token = line.substring(start, scan - 1);
0272: m_endOffset = scan;
0273: switch (chr) {
0274:
0275: case '*':
0276:
0277: // file option, inverted token is only variation
0278: type = FILE_OPTION;
0279: if (lead == '!') {
0280: m_invert = true;
0281: } else if (lead != 0) {
0282: throwError(lnum, line,
0283: UNKNOWN_OPTION_ERR);
0284: }
0285: break;
0286:
0287: case '{':
0288:
0289: // block start option, check variations
0290: if (lead == '}') {
0291: type = BLOCK_ELSE_OPTION;
0292: } else {
0293: if (lead == '!') {
0294: m_invert = true;
0295: }
0296: type = BLOCK_START_OPTION;
0297: }
0298: break;
0299:
0300: case '}':
0301:
0302: // block end option, no variations allowed
0303: type = BLOCK_END_OPTION;
0304: if (lead != 0) {
0305: throwError(lnum, line,
0306: UNKNOWN_OPTION_ERR);
0307: }
0308: break;
0309:
0310: case ':':
0311:
0312: // block comment option, no variations allowed
0313: type = BLOCK_COMMENT_OPTION;
0314: if (lead != 0) {
0315: throwError(lnum, line,
0316: UNKNOWN_OPTION_ERR);
0317: }
0318: break;
0319:
0320: default:
0321:
0322: // no idea what this is supposed to be
0323: throwError(lnum, line, UNKNOWN_OPTION_ERR);
0324: break;
0325:
0326: }
0327: break;
0328: }
0329: }
0330: }
0331: }
0332: return type;
0333: }
0334:
0335: /**
0336: * Processes source options for a text stream. If an error occurs in
0337: * processing, this generates an <code>IOException</code> with the error
0338: * information (including input line number).
0339: *
0340: * @param in input reader for source data
0341: * @param lead first line of file (previously read for checking file enable
0342: * or disable)
0343: * @param out output writer for modified source data
0344: * @return <code>true</code> if source modified, <code>false</code> if not
0345: */
0346:
0347: protected boolean processStream(BufferedReader in, String lead,
0348: BufferedWriter out) throws IOException {
0349:
0350: // initialize state information
0351: Stack enables = new Stack();
0352: Stack nests = new Stack();
0353: String disable = null;
0354: boolean changed = false;
0355:
0356: // basic file line copy loop
0357: String line = lead;
0358: int lnum = 1;
0359: while (line != null) {
0360:
0361: // process based on option type
0362: boolean option = true;
0363: int type = checkOptionLine(lnum, line);
0364: switch (type) {
0365:
0366: case NOT_OPTION:
0367: option = false;
0368: break;
0369:
0370: case FILE_OPTION:
0371:
0372: // file option processed outside, but must be first line
0373: if (lnum > 1) {
0374: throwError(lnum, line, FILE_OPTION_ERR);
0375: }
0376: break;
0377:
0378: case BLOCK_START_OPTION:
0379:
0380: // option block start token, must not be duplicated
0381: if (nests.indexOf(m_token) >= 0) {
0382: throwError(lnum, line, NESTED_SAME_ERR);
0383: } else {
0384:
0385: // push to nesting stack and check if we're handling
0386: nests.push(m_token);
0387: if (disable == null) {
0388:
0389: // see if we know about this token
0390: boolean on = m_enabledTokens
0391: .containsKey(m_token);
0392: boolean off = m_disabledTokens
0393: .containsKey(m_token);
0394:
0395: // swap flags if inverted token
0396: if (m_invert) {
0397: boolean hold = on;
0398: on = off;
0399: off = hold;
0400: }
0401:
0402: // handle start of block
0403: if (off) {
0404:
0405: // set disabling option
0406: disable = m_token;
0407:
0408: } else if (on) {
0409:
0410: // stack enabled option
0411: enables.push(m_token);
0412:
0413: } else if (!enables.empty()) {
0414:
0415: // error if unknown inside enable
0416: throwError(lnum, line, INDETERM_ERR);
0417: }
0418: }
0419: }
0420: break;
0421:
0422: case BLOCK_ELSE_OPTION:
0423:
0424: // option block else, must match top of nesting stack
0425: if (nests.empty() || !nests.peek().equals(m_token)) {
0426: throwError(lnum, line, BADELSE_ERR);
0427: } else {
0428:
0429: // reverse current state, if known
0430: if (disable == null) {
0431:
0432: // enabled state, check if top of stack
0433: if (!enables.empty()) {
0434: if (enables.peek().equals(m_token)) {
0435:
0436: // flip to disable state
0437: enables.pop();
0438: disable = m_token;
0439:
0440: }
0441: }
0442:
0443: } else if (disable.equals(m_token)) {
0444:
0445: // flip to enable state
0446: disable = null;
0447: enables.push(m_token);
0448:
0449: }
0450: }
0451: break;
0452:
0453: case BLOCK_END_OPTION:
0454:
0455: // option block end, must match top of nesting stack
0456: if (nests.empty() || !nests.peek().equals(m_token)) {
0457: throwError(lnum, line, UNBALANCED_ERR);
0458: } else {
0459:
0460: // remove from nesting stack and check state
0461: nests.pop();
0462: if (disable == null) {
0463:
0464: // enabled state, check if top of stack
0465: if (!enables.empty()) {
0466: if (enables.peek().equals(m_token)) {
0467: enables.pop();
0468: }
0469: }
0470:
0471: } else if (disable.equals(m_token)) {
0472: disable = null;
0473: }
0474: }
0475: break;
0476:
0477: case BLOCK_COMMENT_OPTION:
0478:
0479: // disabled line option, check if clearing
0480: if ((disable != null && !disable.equals(m_token))
0481: || (disable == null && enables
0482: .contains(m_token))) {
0483:
0484: // clear disabled line option
0485: line = line.substring(m_endOffset);
0486: option = false;
0487: changed = true;
0488: }
0489: break;
0490:
0491: default:
0492: throwError(lnum, line, UNKNOWN_OPTION_ERR);
0493: }
0494:
0495: // check for disabling lines
0496: if (!option && disable != null) {
0497:
0498: // change line to disabled state
0499: line = OPTION_LEAD + disable + ':' + line;
0500: changed = true;
0501: }
0502:
0503: // write (possibly modified) line to output
0504: out.write(line);
0505: out.newLine();
0506:
0507: // read next line of input
0508: line = in.readLine();
0509: lnum++;
0510:
0511: }
0512:
0513: // check for valid end state
0514: if (nests.size() > 0) {
0515: throwError(lnum, (String) nests.pop(), UNCLOSED_ERR);
0516: }
0517: out.flush();
0518: return changed;
0519: }
0520:
0521: /**
0522: * Processes source options for a file. Starts by checking the first line
0523: * of the file for a file option and processing that. If, after processing
0524: * the file option, the file has a ".java" extension, it is processed for
0525: * other option lines.<p>
0526: *
0527: * This saves the output to a temporary file, then if processing is
0528: * completed successfully first renames or moves the original file (if
0529: * backup has been requested), or deletes it (if backup not requested),
0530: * and then renames the temporary file to the original file name.<p>
0531: *
0532: * Processing errors are printed to <code>System.err</code>, and any
0533: * results are discarded without being saved.
0534: *
0535: * @param file source file to be processed
0536: * @return <code>true</code> if source modified, <code>false</code> if not
0537: */
0538:
0539: protected boolean processFile(File file) {
0540: File temp = null;
0541: try {
0542:
0543: // set up basic information
0544: String name = file.getName();
0545: String dir = file.getParent();
0546: int split = name.lastIndexOf('.');
0547: String ext = (split >= 0) ? name.substring(split + 1) : "";
0548: String toext = ext;
0549: long stamp = m_keepStamp ? file.lastModified() : 0;
0550: File target = file;
0551:
0552: // check first line for file option
0553: BufferedReader in = new BufferedReader(new FileReader(file));
0554: String line = in.readLine();
0555: int type = checkOptionLine(1, line);
0556: if (type == FILE_OPTION) {
0557:
0558: // make sure we have one of the extensions we know about
0559: if (ext.equals("java") || ext.equals("javx")) {
0560:
0561: // set "to" extension based on option setting
0562: if (m_enabledTokens.contains(m_token)) {
0563: toext = m_invert ? "javx" : "java";
0564: } else if (m_disabledTokens.contains(m_token)) {
0565: toext = m_invert ? "java" : "javx";
0566: }
0567:
0568: // generate new target file name if different extension
0569: if (!toext.equals(ext)) {
0570: split = name.indexOf('.');
0571: name = name.substring(0, split) + '.' + toext;
0572: target = new File(dir, name);
0573: }
0574:
0575: } else {
0576: throw new IOException(EXTENSION_ERR);
0577: }
0578:
0579: }
0580:
0581: // check if extension valid for processing
0582: boolean changed = false;
0583: if (!toext.equals("javx")) {
0584:
0585: // set up output to temporary file in same directory
0586: temp = File.createTempFile("sop", null, file
0587: .getParentFile());
0588: BufferedWriter out = new BufferedWriter(new FileWriter(
0589: temp));
0590:
0591: // process the file for changes
0592: changed = processStream(in, line, out);
0593: in.close();
0594: out.close();
0595: if (changed) {
0596:
0597: // handle backup of original file
0598: if (m_backupDir != null) {
0599:
0600: // construct path within backup directory
0601: String extra = file.getCanonicalPath();
0602: int mark = extra.indexOf(File.separatorChar);
0603: if (mark >= 0) {
0604: extra = extra.substring(mark + 1);
0605: }
0606: File backup = new File(m_backupDir, extra);
0607:
0608: // copy file to backup directory
0609: File backdir = backup.getParentFile();
0610: if (!backdir.exists() && !backdir.mkdirs()) {
0611: throw new IOException(BACKUP_DIR_ERR + '\n'
0612: + backdir.getPath());
0613: }
0614: if (backup.exists()) {
0615: if (!backup.delete()) {
0616: throw new IOException(OLD_BACKUP_ERR);
0617: }
0618: }
0619: byte[] buff = new byte[COPY_BUFFER_SIZE];
0620: InputStream is = new FileInputStream(file);
0621: OutputStream os = new FileOutputStream(backup);
0622: int bytes;
0623: while ((bytes = is.read(buff)) >= 0) {
0624: os.write(buff, 0, bytes);
0625: }
0626: is.close();
0627: os.close();
0628: backup.setLastModified(file.lastModified());
0629:
0630: }
0631: if (m_markBackup) {
0632:
0633: // suffix file name with tilde
0634: File backup = new File(dir, name + '~');
0635: if (backup.exists()) {
0636: if (!backup.delete()) {
0637: throw new IOException(OLD_BACKUP_ERR);
0638: }
0639: }
0640: if (!file.renameTo(backup)) {
0641: throw new IOException(BACKUP_FILE_ERR);
0642: }
0643:
0644: } else {
0645:
0646: // just delete the original file
0647: if (!file.delete()) {
0648: throw new IOException(DELETE_ERR);
0649: }
0650: }
0651:
0652: // rename temp to target name
0653: if (temp.renameTo(target)) {
0654: if (m_keepStamp
0655: && !target.setLastModified(stamp)) {
0656: throw new IOException(STAMP_ERR);
0657: }
0658: } else {
0659: throw new IOException(TEMP_RENAME_ERR);
0660: }
0661:
0662: } else {
0663:
0664: // just discard the temporary output file
0665: if (!temp.delete()) {
0666: throw new IOException(TEMP_DELETE_ERR);
0667: }
0668:
0669: // check if file needs to be renamed
0670: if (!toext.equals(ext)) {
0671:
0672: // just rename file for file option result
0673: if (file.renameTo(target)) {
0674: changed = true;
0675: if (m_keepStamp
0676: && !target.setLastModified(stamp)) {
0677: throw new IOException(STAMP_ERR);
0678: }
0679: } else {
0680: throw new IOException(RENAME_ERR);
0681: }
0682: }
0683: }
0684:
0685: } else if (!toext.equals(ext)) {
0686:
0687: // just rename file for file option result
0688: in.close();
0689: if (file.renameTo(target)) {
0690: changed = true;
0691: if (m_keepStamp && !target.setLastModified(stamp)) {
0692: throw new IOException(STAMP_ERR);
0693: }
0694: } else {
0695: throw new IOException(RENAME_ERR);
0696: }
0697:
0698: }
0699:
0700: // check file listing
0701: if (changed && (m_listProcessed || m_listModified)) {
0702: System.out.println(" modified file " + file.getPath());
0703: } else if (m_listProcessed) {
0704: System.out.println(" checked file " + file.getPath());
0705: }
0706: return changed;
0707:
0708: } catch (Exception ex) {
0709:
0710: // report error
0711: System.err.println("Error processing " + file.getPath());
0712: System.err.println(ex.getMessage());
0713:
0714: // discard temporary output file
0715: if (temp != null) {
0716: try {
0717: temp.delete();
0718: } catch (Exception ex2) {
0719: }
0720: }
0721: return false;
0722:
0723: }
0724: }
0725:
0726: /**
0727: * Checks if file or directory name directly matches a pattern. This
0728: * method accepts one or more '*' wildcard characters in the pattern,
0729: * calling itself recursively in order to handle multiple wildcards.
0730: *
0731: * @param name file or directory name
0732: * @param pattern match pattern
0733: * @return <code>true</code> if any pattern matched, <code>false</code>
0734: * if not
0735: */
0736:
0737: protected boolean isPathMatch(String name, String pattern) {
0738:
0739: // check special match cases first
0740: if (pattern.length() == 0) {
0741: return name.length() == 0;
0742: } else if (pattern.charAt(0) == '*') {
0743:
0744: // check if the wildcard is all that's left of pattern
0745: if (pattern.length() == 1) {
0746: return true;
0747: } else {
0748:
0749: // check if another wildcard follows next segment of text
0750: pattern = pattern.substring(1);
0751: int split = pattern.indexOf('*');
0752: if (split > 0) {
0753:
0754: // recurse on each match to text segment
0755: String piece = pattern.substring(0, split);
0756: pattern = pattern.substring(split);
0757: int offset = -1;
0758: while ((offset = name.indexOf(piece, ++offset)) > 0) {
0759: int end = offset + piece.length();
0760: if (isPathMatch(name.substring(end), pattern)) {
0761: return true;
0762: }
0763: }
0764:
0765: } else {
0766:
0767: // no more wildcards, need exact match to end of name
0768: return name.endsWith(pattern);
0769:
0770: }
0771: }
0772: } else {
0773:
0774: // check for leading text before first wildcard
0775: int split = pattern.indexOf('*');
0776: if (split > 0) {
0777:
0778: // match leading text to start of name
0779: String piece = pattern.substring(0, split);
0780: if (name.startsWith(piece)) {
0781: return isPathMatch(name.substring(split), pattern
0782: .substring(split));
0783: } else {
0784: return false;
0785: }
0786:
0787: } else {
0788:
0789: // no wildcards, need exact match
0790: return name.equals(pattern);
0791:
0792: }
0793: }
0794: return false;
0795: }
0796:
0797: /**
0798: * Checks if file name matches a pattern. This works a little differently
0799: * from the general path matching in that if the pattern does not include
0800: * an extension both ".java" and ".javx" file extensions are matched. If
0801: * the pattern includes an extension ending in '*' it is blocked from
0802: * matching with a tilde final character in the file name as a special case.
0803: *
0804: * @param name file or directory name
0805: * @param pattern match pattern
0806: * @return <code>true</code> if any file modified, <code>false</code> if not
0807: */
0808:
0809: protected boolean isNameMatch(String name, String pattern) {
0810:
0811: // check for extension included in pattern
0812: if (pattern.indexOf('.') >= 0) {
0813:
0814: // pattern includes extension, use as is except for tilde endings
0815: if (name.charAt(name.length() - 1) != '~'
0816: || pattern.charAt(pattern.length() - 1) == '~') {
0817: return isPathMatch(name, pattern);
0818: }
0819:
0820: } else {
0821:
0822: // split extension from file name
0823: int split = name.lastIndexOf('.');
0824: if (split >= 0) {
0825:
0826: // check for valid extension with match on name
0827: String ext = name.substring(split + 1);
0828: if (ext.equals("java") || ext.equals("javx")) {
0829: return isPathMatch(name.substring(0, split),
0830: pattern);
0831: }
0832: }
0833:
0834: }
0835: return false;
0836: }
0837:
0838: /**
0839: * Process files matching path segment. This method matches a single step
0840: * (directory specification) in a path for each call, calling itself
0841: * recursively to match the complete path.
0842: *
0843: * @param base base directory for path match
0844: * @param path file path remaining to be processed
0845: */
0846:
0847: protected void matchPathSegment(File base, String path) {
0848:
0849: // find break for leading directory if any in path
0850: File[] files = base.listFiles();
0851: int split = path.indexOf('/');
0852: if (split >= 0) {
0853:
0854: // split off the directory and check it
0855: String dir = path.substring(0, split);
0856: String next = path.substring(split + 1);
0857: if (dir.equals("**")) {
0858:
0859: // match directly against files in this directory
0860: matchPathSegment(base, next);
0861:
0862: // walk all directories in tree under this one
0863: for (int i = 0; i < files.length; i++) {
0864: if (files[i].isDirectory()) {
0865: matchPathSegment(files[i], path);
0866: }
0867: }
0868:
0869: } else {
0870:
0871: // try for concrete match to directory
0872: for (int i = 0; i < files.length; i++) {
0873: if (files[i].isDirectory()) {
0874: if (isPathMatch(files[i].getName(), dir)) {
0875: matchPathSegment(files[i], next);
0876: }
0877: }
0878: }
0879:
0880: }
0881: } else {
0882:
0883: // match directly against files in this directory
0884: for (int i = 0; i < files.length; i++) {
0885: if (!files[i].isDirectory()) {
0886: if (isNameMatch(files[i].getName(), path)) {
0887: m_matchedCount++;
0888: if (processFile(files[i])) {
0889: m_modifiedCount++;
0890: }
0891: }
0892: }
0893: }
0894:
0895: }
0896: }
0897:
0898: /**
0899: * Process all files matching path and print summary. The file path
0900: * format is similar to Ant, supporting arbitrary directory recursion using
0901: * '**' separators between '/' separators. Single '*'s may be used within
0902: * names for wildcarding, but aside from the special case of the directory
0903: * recursion matcher only one '*' may be used per name.<p>
0904: *
0905: * If an extension is
0906: * not specified for the final name in the path both ".java" and ".javx"
0907: * extensions are checked, but after checking for file option lines (which
0908: * may change the file extension) only ".java" extensions are processed for
0909: * other source options.
0910: *
0911: * @param path file path to be processed
0912: */
0913:
0914: protected void processPath(String path) {
0915:
0916: // make sure we have something to process
0917: if (path.length() > 0) {
0918:
0919: // begin matching from root or current directory
0920: if (path.charAt(0) == '/') {
0921: matchPathSegment(new File(File.separator), path
0922: .substring(1));
0923: } else {
0924: matchPathSegment(new File("."), path);
0925: }
0926:
0927: // print summary information for path
0928: if (m_listSummary) {
0929: System.out.println(" matched " + m_matchedCount
0930: + " files and modified " + m_modifiedCount
0931: + " for path: " + path);
0932: m_matchedCount = 0;
0933: m_modifiedCount = 0;
0934: }
0935: }
0936: }
0937:
0938: /**
0939: * Parse comma-separated token list. Parses and validates the tokens,
0940: * adding them to the supplied list. errors are signalled by throwing
0941: * <code>IllegalArgumentException</code>.
0942: *
0943: * @param list comma-separated token list to be parsed
0944: * @param tokens list of tokens to add to
0945: * @throws IllegalArgumentException on error in supplied list
0946: */
0947:
0948: protected static void parseTokenList(String list, Vector tokens) {
0949:
0950: // accumulate comma-delimited tokens from list
0951: while (list.length() > 0) {
0952:
0953: // find end for next token
0954: int mark = list.indexOf(',');
0955: String token;
0956: if (mark == 0) {
0957: throw new IllegalArgumentException("Empty token not "
0958: + "allowed: \"" + list + '"');
0959: } else if (mark > 0) {
0960:
0961: // split token off list
0962: token = list.substring(0, mark);
0963: list = list.substring(mark + 1);
0964:
0965: } else {
0966:
0967: // use rest of list as final token
0968: token = list;
0969: list = "";
0970: }
0971:
0972: // validate the token
0973: for (int i = 0; i < token.length(); i++) {
0974: char chr = token.charAt(i);
0975: if ((i == 0 && !isTokenLeadChar(chr))
0976: || (i > 0 && !isTokenBodyChar(chr))) {
0977: throw new IllegalArgumentException("Illegal "
0978: + "character in token: \"" + token + '"');
0979: }
0980: }
0981:
0982: // add validated token to list
0983: tokens.add(token);
0984: }
0985: }
0986:
0987: /**
0988: * Test driver, just reads the input parameters and executes the source
0989: * checks.
0990: *
0991: * @param argv command line arguments
0992: */
0993:
0994: public static void main(String[] argv) {
0995: if (argv.length > 0) {
0996:
0997: // parse the leading command line parameters
0998: boolean valid = true;
0999: boolean keep = false;
1000: boolean listmod = false;
1001: boolean quiet = false;
1002: boolean tilde = false;
1003: boolean verbose = false;
1004: File backup = null;
1005: Vector enables = new Vector();
1006: Vector disables = new Vector();
1007: int anum = 0;
1008: while (anum < argv.length && argv[anum].charAt(0) == '-') {
1009: String arg = argv[anum++];
1010: int cnum = 1;
1011: while (cnum < arg.length()) {
1012: char option = Character.toLowerCase(arg
1013: .charAt(cnum++));
1014: switch (option) {
1015:
1016: case 'b':
1017: if (anum < argv.length) {
1018: try {
1019: backup = new File(argv[anum++]);
1020: if (!backup.isDirectory()) {
1021: System.err
1022: .println("Backup directory "
1023: + "path must be a directory");
1024: }
1025: } catch (SecurityException ex) {
1026: System.err.println("Unable to access "
1027: + "backup directory");
1028: }
1029: } else {
1030: System.err
1031: .println("Missing directory path "
1032: + "for -b option");
1033: }
1034: break;
1035:
1036: case 'd':
1037: case 'e':
1038: if (anum < argv.length) {
1039:
1040: // accumulate comma-delimited tokens from list
1041: Vector tokens = (option == 'd') ? disables
1042: : enables;
1043: try {
1044: parseTokenList(argv[anum++], tokens);
1045: } catch (IllegalArgumentException ex) {
1046: System.err.println(ex.getMessage());
1047: return;
1048: }
1049: if (option == 'd') {
1050: disables = tokens;
1051: } else {
1052: enables = tokens;
1053: }
1054:
1055: } else {
1056: System.err
1057: .println("Missing token list for -"
1058: + option + " option");
1059: return;
1060: }
1061: break;
1062:
1063: case 'p':
1064: keep = true;
1065: break;
1066:
1067: case 'q':
1068: quiet = true;
1069: break;
1070:
1071: case 'm':
1072: listmod = true;
1073: break;
1074:
1075: case 't':
1076: tilde = true;
1077: break;
1078:
1079: case 'v':
1080: verbose = true;
1081: break;
1082:
1083: default:
1084: System.err.println("Unknown option -" + option);
1085: return;
1086: }
1087: }
1088: }
1089:
1090: // build hashsets of the tokens, checking for overlap
1091: Hashtable enabled = new Hashtable();
1092: for (int i = 0; i < enables.size(); i++) {
1093: Object token = enables.elementAt(i);
1094: enabled.put(token, token);
1095: }
1096: Hashtable disabled = new Hashtable();
1097: for (int i = 0; i < disables.size(); i++) {
1098: Object token = disables.elementAt(i);
1099: disabled.put(token, token);
1100: if (enabled.containsKey(token)) {
1101: System.err.println(DUAL_USE_ERR + ": " + token);
1102: return;
1103: }
1104: }
1105:
1106: // construct an instance of class
1107: JEnable opt = new JEnable(keep, tilde, listmod, quiet,
1108: verbose, backup, enabled, disabled);
1109:
1110: // check if we have file paths
1111: if (anum < argv.length) {
1112:
1113: // process each file path supplied
1114: while (anum < argv.length) {
1115: String arg = argv[anum++];
1116: int split;
1117: while ((split = arg.indexOf(',')) > 0) {
1118: String path = arg.substring(0, split);
1119: opt.processPath(path);
1120: arg = arg.substring(split + 1);
1121: }
1122: opt.processPath(arg);
1123: }
1124:
1125: } else {
1126:
1127: // just process standard input to standard output
1128: BufferedReader in = new BufferedReader(
1129: new InputStreamReader(System.in));
1130: BufferedWriter out = new BufferedWriter(
1131: new OutputStreamWriter(System.out));
1132:
1133: // check first line for disabled token
1134: try {
1135: String line = in.readLine();
1136: int type = opt.checkOptionLine(1, line);
1137: if (type == FILE_OPTION) {
1138: boolean discard = opt.m_invert ? enabled
1139: .contains(opt.m_token) : disabled
1140: .contains(opt.m_token);
1141: if (discard) {
1142: return;
1143: }
1144: }
1145: opt.processStream(in, line, out);
1146: } catch (IOException ex) {
1147: System.err.println(ex.getMessage());
1148: }
1149: }
1150: } else {
1151: System.err
1152: .println("\nJEnable Java source configuration processor version "
1153: + "0.8\nUsage: JEnable [-options] path-list\n"
1154: + "Options are:\n"
1155: + " -b backup directory tree (base directory is next "
1156: + "argument)\n"
1157: + " -d disabled token list (comma-separated token name list"
1158: + " is next argument)\n"
1159: + " -e enabled token list (comma-separated token name list"
1160: + " is next argument)\n"
1161: + " -m list modified files as they're processed\n"
1162: + " -p preserve timestamp on modified files\n"
1163: + " -q quiet mode, do not print file summary by path\n"
1164: + " -t backup modified files in same directory with '~' "
1165: + "suffix\n"
1166: + " -v verbose listing of all files processed (modified or "
1167: + "not)\n"
1168: + "These options may be concatenated together with a single"
1169: + " leading dash.\n\n"
1170: + "Path lists may include '*' wildcards, and may consist of "
1171: + "multiple paths\n"
1172: + "separated by ',' characters. The special directory "
1173: + "pattern '**' matches\n"
1174: + "any number of intervening directories. Any number of path "
1175: + "list parameters may\n"
1176: + "be supplied.\n");
1177: }
1178: }
1179: }
|