package org.oddjob.io; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.io.OutputStream; import java.io.PrintStream; import java.util.Collection; import java.util.concurrent.Callable; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @oddjob.description Search files or an input stream for lines containing * a text value or matches for a regular expression. * * @oddjob.example * * Search a buffer of text for the word red. In this example the * search is case insensitive and the results a written to the console * with the line number. * * {@oddjob.xml.resource org/oddjob/io/GrepJobExample.xml} * * * * @author rob * */ public class GrepJob implements Callable<Integer> { /** * @oddjob.property * @oddjob.description A display name for the job. * @oddjob.required No. */ private String name; /** * @oddjob.property * @oddjob.description The files to search. * @oddjob.required No, not if an in is provided. */ private File[] files; /** * @oddjob.property * @oddjob.description The input to search. * @oddjob.required No, not if files are provided. */ private InputStream in; /** * @oddjob.property * @oddjob.description Text to search for. This is a regular expression * if the regexp property is set to true. * @oddjob.required Yes, not if a regexp is provided. */ private String text; /** * @oddjob.property * @oddjob.description Treat the text to match as a regular expression. * @oddjob.required No, Text is treated as plain text. */ private boolean regexp; /** * @oddjob.property * @oddjob.description Where to write output to. * @oddjob.required No. If not provided no output will be written */ private OutputStream out; /** * @oddjob.property * @oddjob.description A count of the number of matched lines. * @oddjob.required Read Only. */ private int matchedLineCount; /** * @oddjob.property * @oddjob.description A collection for {@link GrepLineResult} beans * to be written to. * @oddjob.required No. */ private Collection<? super GrepLineResult> results; /** * @oddjob.property * @oddjob.description Prefix output with line numbers. If true * then the number of the match in the file or input will be prepended * to each line of output. * @oddjob.required No. Default to false. */ private boolean lineNumbers; /** * @oddjob.property * @oddjob.description Remove the path from the file name. If true * and a file name is prefixed to each line of output, then the path * is removed. * @oddjob.required No. Default to false. */ private boolean noPath; /** * @oddjob.property * @oddjob.description Don't prefix output with a file name. If true * then no file name will be prefixed to each line of output. * @oddjob.required No. Default to false. */ private boolean noFilename; /** * @oddjob.property * @oddjob.description Prefix output with a file name. If true * then the file name will be prefixed to each line of output. By * default the file name is not prefixed to a single file, only when * there are multiple files being searched. This property will prefix * the file name when only a single file is being searched. * @oddjob.required No. Default to false. */ private boolean withFilename; /** * @oddjob.property * @oddjob.description Ignore case. If true, the search will be case * insensitive. * @oddjob.required No. Default to false. */ private boolean ignoreCase; /** * @oddjob.property * @oddjob.description Invert the search. If true, then only lines that * don't contain a match will be output. * @oddjob.required No. Default to false. */ private boolean invert; public Integer call() throws IOException { matchedLineCount = 0; int flags = 0; if (ignoreCase) { flags = Pattern.CASE_INSENSITIVE; } if (text == null) { throw new NullPointerException("Nothing to search for."); } Pattern grep; if (regexp) { grep = Pattern.compile(text, flags); } else { grep = Pattern.compile(Pattern.quote(text), flags); } PrintStream resultStream; if (out == null) { resultStream = null; } else { resultStream = new PrintStream(out); } GrepHandler grepHandler; if (files != null) { grepHandler = new FilesGrepHandler(resultStream); } else if (in != null){ grepHandler = new StreamGrepHandler(resultStream); } else { throw new NullPointerException("No Input."); } try { while (true) { LineNumberReader reader = grepHandler.nextReader(); if (reader == null) { break; } try { while (true) { String line = reader.readLine(); if (line == null) { break; } Matcher matcher = grep.matcher(line); if (matcher.find() ^ invert) { matchedLineCount++; int lineNumber = reader.getLineNumber(); String match = null; if (!invert) { match = matcher.group(); } grepHandler.processResult(lineNumber, line, match); } } } finally { reader.close(); } } } finally { if (resultStream != null) { resultStream.close(); } } return 0; } /** * Allow for different handling when reading files and streams. * */ private interface GrepHandler { LineNumberReader nextReader() throws FileNotFoundException; void processResult(int lineNumber, String line, String match); } private class FilesGrepHandler implements GrepHandler { private final PrintStream resultStream; private int fileIndex = -1; public FilesGrepHandler(PrintStream resultStream) { this.resultStream = resultStream; } @Override public LineNumberReader nextReader() throws FileNotFoundException { if (++fileIndex >= files.length) { return null; } return new LineNumberReader(new FileReader(files[fileIndex])); } @Override public void processResult(int lineNumber, String line, String match) { if (resultStream != null) { File file = null; if (files.length > 1 || withFilename) { file = files[fileIndex]; } ResultLine resultLine = new ResultLine( file, lineNumber, line); resultStream.println(resultLine.toString()); } if (results != null) { results.add(new GrepLineResult(files[fileIndex], lineNumber, line, match)); } } } private class StreamGrepHandler implements GrepHandler { private final PrintStream resultStream; boolean done; public StreamGrepHandler(PrintStream resultStream) { this.resultStream = resultStream; } @Override public LineNumberReader nextReader() { if (done) { return null; } else { done = true; return new LineNumberReader(new InputStreamReader(in)); } } @Override public void processResult(int lineNumber, String line, String match) { if (resultStream != null) { ResultLine resultLine = new ResultLine( lineNumber, line); resultStream.println(resultLine.toString()); } if (results != null) { results.add(new GrepLineResult(lineNumber, line, match)); } } } /** * Helper to build the text line that is written out for a match. */ class ResultLine { private final File file; private final int lineNumber; private final String line; public ResultLine(int lineNumber, String line) { this(null, lineNumber, line); } public ResultLine(File file, int lineNumber, String line) { this.file = file; this.lineNumber = lineNumber; this.line = line; } @Override public String toString() { StringBuilder builder = new StringBuilder(); if (file != null && !noFilename) { if (noPath) { builder.append(file.getName()); } else { builder.append(file.getPath()); } builder.append(':'); } if (lineNumbers) { builder.append(lineNumber); builder.append(':'); } builder.append(line); return builder.toString(); } } public String getName() { return name; } public void setName(String name) { this.name = name; } public File[] getFiles() { return files; } public void setFiles(File[] files) { this.files = files; } public InputStream getIn() { return in; } public void setIn(InputStream in) { this.in = in; } public String getText() { return text; } public void setText(String text) { this.text = text; } public boolean isRegexp() { return regexp; } public void setRegexp(boolean regexp) { this.regexp = regexp; } public OutputStream getOut() { return out; } public void setOut(OutputStream out) { this.out = out; } public Collection<? super GrepLineResult> getResults() { return results; } public void setResults(Collection<? super GrepLineResult> results) { this.results = results; } public int getMatchedLineCount() { return matchedLineCount; } public boolean isLineNumbers() { return lineNumbers; } public void setLineNumbers(boolean lineNumbers) { this.lineNumbers = lineNumbers; } public boolean isNoPath() { return noPath; } public void setNoPath(boolean noPath) { this.noPath = noPath; } public boolean isNoFilename() { return noFilename; } public void setNoFilename(boolean noFileName) { this.noFilename = noFileName; } public boolean isWithFilename() { return withFilename; } public void setWithFilename(boolean withFilename) { this.withFilename = withFilename; } public boolean isIgnoreCase() { return ignoreCase; } public void setIgnoreCase(boolean ignoreCase) { this.ignoreCase = ignoreCase; } public boolean isInvert() { return invert; } public void setInvert(boolean invert) { this.invert = invert; } @Override public String toString() { if (name == null) { return getClass().getSimpleName(); } else { return name; } } }