/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.tools.ant.taskdefs; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.nio.file.Files; import java.util.Enumeration; import java.util.NoSuchElementException; import java.util.Vector; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.Project; import org.apache.tools.ant.filters.ChainableReader; import org.apache.tools.ant.filters.FixCrLfFilter; import org.apache.tools.ant.types.EnumeratedAttribute; import org.apache.tools.ant.types.FilterChain; import org.apache.tools.ant.util.FileUtils; /** * Converts text source files to local OS formatting conventions, as * well as repair text files damaged by misconfigured or misguided editors or * file transfer programs. * <p> * This task can take the following arguments: * <ul> * <li>srcdir * <li>destdir * <li>include * <li>exclude * <li>cr * <li>eol * <li>tab * <li>eof * <li>encoding * <li>targetencoding * </ul> * Of these arguments, only <b>sourcedir</b> is required. * <p> * When this task executes, it will scan the srcdir based on the include * and exclude properties. * <p> * This version generalises the handling of EOL characters, and allows * for CR-only line endings (the standard on Mac systems prior to OS X). * Tab handling has also been generalised to accommodate any tabwidth * from 2 to 80, inclusive. Importantly, it will leave untouched any * literal TAB characters embedded within string or character constants. * <p> * <em>Warning:</em> do not run on binary files. * <em>Caution:</em> run with care on carefully formatted files. * This may sound obvious, but if you don't specify asis, presume that * your files are going to be modified. If "tabs" is "add" or "remove", * whitespace characters may be added or removed as necessary. Similarly, * for CR's - in fact "eol"="crlf" or cr="add" can result in cr * characters being removed in one special case accommodated, i.e., * CRCRLF is regarded as a single EOL to handle cases where other * programs have converted CRLF into CRCRLF. * * @since Ant 1.1 * * @ant.task category="filesystem" */ public class FixCRLF extends MatchingTask implements ChainableReader { private static final String FIXCRLF_ERROR = "<fixcrlf> error: "; /** error string for using srcdir and file */ public static final String ERROR_FILE_AND_SRCDIR = FIXCRLF_ERROR + "srcdir and file are mutually exclusive"; private static final FileUtils FILE_UTILS = FileUtils.getFileUtils(); private boolean preserveLastModified = false; private File srcDir; private File destDir = null; private File file; private FixCrLfFilter filter = new FixCrLfFilter(); private Vector<FilterChain> fcv = null; /** * Encoding to assume for the files */ private String encoding = null; /** * Encoding to use for output files */ private String outputEncoding = null; /** * Chain this task as a reader. * @param rdr Reader to chain. * @return a Reader. * @since Ant 1.7? */ @Override public final Reader chain(final Reader rdr) { return filter.chain(rdr); } /** * Set the source dir to find the source text files. * @param srcDir the source directory. */ public void setSrcdir(File srcDir) { this.srcDir = srcDir; } /** * Set the destination where the fixed files should be placed. * Default is to replace the original file. * @param destDir the destination directory. */ public void setDestdir(File destDir) { this.destDir = destDir; } /** * Set to true if modifying Java source files. * @param javafiles whether modifying Java files. */ public void setJavafiles(boolean javafiles) { filter.setJavafiles(javafiles); } /** * Set a single file to convert. * @since Ant 1.6.3 * @param file the file to convert. */ public void setFile(File file) { this.file = file; } /** * Specify how EndOfLine characters are to be handled. * * @param attr valid values: * <ul> * <li>asis: leave line endings alone * <li>cr: convert line endings to CR * <li>lf: convert line endings to LF * <li>crlf: convert line endings to CRLF * </ul> */ public void setEol(CrLf attr) { filter.setEol(FixCrLfFilter.CrLf.newInstance(attr.getValue())); } /** * Specify how carriage return (CR) characters are to be handled. * * @param attr valid values: * <ul> * <li>add: ensure that there is a CR before every LF * <li>asis: leave CR characters alone * <li>remove: remove all CR characters * </ul> * * @deprecated since 1.4.x. * Use {@link #setEol setEol} instead. */ @Deprecated public void setCr(AddAsisRemove attr) { log("DEPRECATED: The cr attribute has been deprecated,", Project.MSG_WARN); log("Please use the eol attribute instead", Project.MSG_WARN); String option = attr.getValue(); CrLf c = new CrLf(); if ("remove".equals(option)) { c.setValue("lf"); } else if ("asis".equals(option)) { c.setValue("asis"); } else { // must be "add" c.setValue("crlf"); } setEol(c); } /** * Specify how tab characters are to be handled. * * @param attr valid values: * <ul> * <li>add: convert sequences of spaces which span a tab stop to tabs * <li>asis: leave tab and space characters alone * <li>remove: convert tabs to spaces * </ul> */ public void setTab(AddAsisRemove attr) { filter.setTab(FixCrLfFilter.AddAsisRemove.newInstance(attr.getValue())); } /** * Specify tab length in characters. * * @param tlength specify the length of tab in spaces. * @throws BuildException on error. */ public void setTablength(int tlength) throws BuildException { try { filter.setTablength(tlength); } catch (IOException e) { // filter.setTablength throws IOException that would better be // a BuildException throw new BuildException(e.getMessage(), e); } } /** * Specify how DOS EOF (control-z) characters are to be handled. * * @param attr valid values: * <ul> * <li>add: ensure that there is an eof at the end of the file * <li>asis: leave eof characters alone * <li>remove: remove any eof character found at the end * </ul> */ public void setEof(AddAsisRemove attr) { filter.setEof(FixCrLfFilter.AddAsisRemove.newInstance(attr.getValue())); } /** * Specifies the encoding Ant expects the files to be * in--defaults to the platforms default encoding. * @param encoding String encoding name. */ public void setEncoding(String encoding) { this.encoding = encoding; } /** * Specifies the encoding that the files are * to be written in--same as input encoding by default. * @param outputEncoding String outputEncoding name. */ public void setOutputEncoding(String outputEncoding) { this.outputEncoding = outputEncoding; } /** * Specify whether a missing EOL will be added * to the final line of a file. * @param fixlast whether to fix the last line. */ public void setFixlast(boolean fixlast) { filter.setFixlast(fixlast); } /** * Set whether to preserve the last modified time as the original files. * @param preserve true if timestamps should be preserved. * @since Ant 1.6.3 */ public void setPreserveLastModified(boolean preserve) { preserveLastModified = preserve; } /** * Executes the task. * @throws BuildException on error. */ @Override public void execute() throws BuildException { // first off, make sure that we've got a srcdir and destdir validate(); // log options used String enc = encoding == null ? "default" : encoding; log("options:" + " eol=" + filter.getEol().getValue() + " tab=" + filter.getTab().getValue() + " eof=" + filter.getEof().getValue() + " tablength=" + filter.getTablength() + " encoding=" + enc + " outputencoding=" + (outputEncoding == null ? enc : outputEncoding), Project.MSG_VERBOSE); DirectoryScanner ds = super.getDirectoryScanner(srcDir); String[] files = ds.getIncludedFiles(); for (int i = 0; i < files.length; i++) { processFile(files[i]); } } private void validate() throws BuildException { if (file != null) { if (srcDir != null) { throw new BuildException(ERROR_FILE_AND_SRCDIR); } //patch file into the fileset fileset.setFile(file); //set our parent dir srcDir = file.getParentFile(); } if (srcDir == null) { throw new BuildException( FIXCRLF_ERROR + "srcdir attribute must be set!"); } if (!srcDir.exists()) { throw new BuildException( FIXCRLF_ERROR + "srcdir does not exist: '%s'", srcDir); } if (!srcDir.isDirectory()) { throw new BuildException( FIXCRLF_ERROR + "srcdir is not a directory: '%s'", srcDir); } if (destDir != null) { if (!destDir.exists()) { throw new BuildException( FIXCRLF_ERROR + "destdir does not exist: '%s'", destDir); } if (!destDir.isDirectory()) { throw new BuildException( FIXCRLF_ERROR + "destdir is not a directory: '%s'", destDir); } } } private void processFile(String file) throws BuildException { File srcFile = new File(srcDir, file); long lastModified = srcFile.lastModified(); File destD = destDir == null ? srcDir : destDir; if (fcv == null) { FilterChain fc = new FilterChain(); fc.add(filter); fcv = new Vector<>(1); fcv.add(fc); } File tmpFile = FILE_UTILS.createTempFile("fixcrlf", "", null, true, true); try { FILE_UTILS.copyFile(srcFile, tmpFile, null, fcv, true, false, encoding, outputEncoding == null ? encoding : outputEncoding, getProject()); File destFile = new File(destD, file); boolean destIsWrong = true; if (destFile.exists()) { // Compare the destination with the temp file log("destFile " + destFile + " exists", Project.MSG_DEBUG); destIsWrong = !FILE_UTILS.contentEquals(destFile, tmpFile); log(destFile + (destIsWrong ? " is being written" : " is not written, as the contents are identical"), Project.MSG_DEBUG); } if (destIsWrong) { FILE_UTILS.rename(tmpFile, destFile); if (preserveLastModified) { log("preserved lastModified for " + destFile, Project.MSG_DEBUG); FILE_UTILS.setFileLastModified(destFile, lastModified); } } } catch (IOException e) { throw new BuildException("error running fixcrlf on file " + srcFile, e); } finally { if (tmpFile != null && tmpFile.exists()) { FILE_UTILS.tryHardToDelete(tmpFile); } } } /** * Deprecated, the functionality has been moved to filters.FixCrLfFilter. * @deprecated since 1.7.0. */ @Deprecated protected class OneLiner implements Enumeration<Object> { private static final int UNDEF = -1; private static final int NOTJAVA = 0; private static final int LOOKING = 1; private static final int INBUFLEN = 8192; private static final int LINEBUFLEN = 200; private static final char CTRLZ = '\u001A'; private int state = filter.getJavafiles() ? LOOKING : NOTJAVA; private StringBuffer eolStr = new StringBuffer(LINEBUFLEN); private StringBuffer eofStr = new StringBuffer(); private BufferedReader reader; private StringBuffer line = new StringBuffer(); private boolean reachedEof = false; private File srcFile; /** * Constructor. * @param srcFile the file to read. * @throws BuildException if there is an error. */ public OneLiner(File srcFile) throws BuildException { this.srcFile = srcFile; try { reader = new BufferedReader( ((encoding == null) ? new FileReader(srcFile) : new InputStreamReader( Files.newInputStream(srcFile.toPath()), encoding)), INBUFLEN); nextLine(); } catch (IOException e) { throw new BuildException(srcFile + ": " + e.getMessage(), e, getLocation()); } } /** * Move to the next line. * @throws BuildException if there is an error. */ protected void nextLine() throws BuildException { int ch = -1; int eolcount = 0; eolStr = new StringBuffer(); line = new StringBuffer(); try { ch = reader.read(); while (ch != -1 && ch != '\r' && ch != '\n') { line.append((char) ch); ch = reader.read(); } if (ch == -1 && line.length() == 0) { // Eof has been reached reachedEof = true; return; } switch ((char) ch) { case '\r': // Check for \r, \r\n and \r\r\n // Regard \r\r not followed by \n as two lines ++eolcount; eolStr.append('\r'); reader.mark(2); ch = reader.read(); switch (ch) { case '\r': ch = reader.read(); if ((char) (ch) == '\n') { eolcount += 2; eolStr.append("\r\n"); } else { reader.reset(); } break; case '\n': ++eolcount; eolStr.append('\n'); break; case -1: // don't reposition when we've reached the end // of the stream break; default: reader.reset(); break; } // end of switch ((char)(ch = reader.read())) break; case '\n': ++eolcount; eolStr.append('\n'); break; default: // Fall tru } // end of switch ((char) ch) // if at eolcount == 0 and trailing characters of string // are CTRL-Zs, set eofStr if (eolcount == 0) { int i = line.length(); while (--i >= 0 && line.charAt(i) == CTRLZ) { // keep searching for the first ^Z } if (i < line.length() - 1) { // Trailing characters are ^Zs // Construct new line and eofStr eofStr.append(line.toString().substring(i + 1)); if (i < 0) { line.setLength(0); reachedEof = true; } else { line.setLength(i + 1); } } } // end of if (eolcount == 0) } catch (IOException e) { throw new BuildException(srcFile + ": " + e.getMessage(), e, getLocation()); } } /** * get the eof string. * @return the eof string. */ public String getEofStr() { return eofStr.substring(0); } /** * get the state. * @return the state. */ public int getState() { return state; } /** * Set the state. * @param state the value to use. */ public void setState(int state) { this.state = state; } /** * @return true if there is more elements. */ @Override public boolean hasMoreElements() { return !reachedEof; } /** * get the next element. * @return the next element. * @throws NoSuchElementException if there is no more. */ @Override public Object nextElement() throws NoSuchElementException { if (!hasMoreElements()) { throw new NoSuchElementException("OneLiner"); } BufferLine tmpLine = new BufferLine(line.toString(), eolStr.substring(0)); nextLine(); return tmpLine; } /** * Close the reader. * @throws IOException if there is an error. */ public void close() throws IOException { if (reader != null) { reader.close(); } } class BufferLine { private int next = 0; private int column = 0; private int lookahead = UNDEF; private String line; private String eolStr; public BufferLine(String line, String eolStr) throws BuildException { next = 0; column = 0; this.line = line; this.eolStr = eolStr; } public int getNext() { return next; } public void setNext(int next) { this.next = next; } public int getLookahead() { return lookahead; } public void setLookahead(int lookahead) { this.lookahead = lookahead; } public char getChar(int i) { return line.charAt(i); } public char getNextChar() { return getChar(next); } public char getNextCharInc() { return getChar(next++); } public int getColumn() { return column; } public void setColumn(int col) { column = col; } public int incColumn() { return column++; } public int length() { return line.length(); } public int getEolLength() { return eolStr.length(); } public String getLineString() { return line; } public String getEol() { return eolStr; } public String substring(int begin) { return line.substring(begin); } public String substring(int begin, int end) { return line.substring(begin, end); } public void setState(int state) { OneLiner.this.setState(state); } public int getState() { return OneLiner.this.getState(); } } } /** * Enumerated attribute with the values "asis", "add" and "remove". */ public static class AddAsisRemove extends EnumeratedAttribute { /** {@inheritDoc}. */ @Override public String[] getValues() { return new String[] { "add", "asis", "remove" }; } } /** * Enumerated attribute with the values "asis", "cr", "lf", "crlf", "mac", "unix" and "dos. */ public static class CrLf extends EnumeratedAttribute { /** * @see EnumeratedAttribute#getValues */ /** {@inheritDoc}. */ @Override public String[] getValues() { return new String[] { "asis", "cr", "lf", "crlf", "mac", "unix", "dos" }; } } }