/* * 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.filters; import java.io.IOException; import java.io.Reader; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.taskdefs.condition.Os; import org.apache.tools.ant.types.EnumeratedAttribute; /** * Converts text to local OS formatting conventions, as well as repair text * damaged by misconfigured or misguided editors or file transfer programs. *
* This filter can take the following arguments: *
* 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 can leave untouched any literal TAB characters * embedded within Java string or character constants. *
* Caution: 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 EOLs, eol="asis" * actually means convert to your native O/S EOL convention while 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. * *
* Example: * *
* <<fixcrlf tab="add" eol="crlf" eof="asis"/> ** * Or: * *
* <filterreader classname="org.apache.tools.ant.filters.FixCrLfFilter"> * <param eol="crlf" tab="asis"/> * </filterreader> ** */ public final class FixCrLfFilter extends BaseParamFilterReader implements ChainableReader { private static final int DEFAULT_TAB_LENGTH = 8; private static final int MIN_TAB_LENGTH = 2; private static final int MAX_TAB_LENGTH = 80; private static final char CTRLZ = '\u001A'; private int tabLength = DEFAULT_TAB_LENGTH; private CrLf eol; private AddAsisRemove ctrlz; private AddAsisRemove tabs; private boolean javafiles = false; private boolean fixlast = true; private boolean initialized = false; /** * Constructor for "dummy" instances. * * @see BaseFilterReader#BaseFilterReader() */ public FixCrLfFilter() { super(); } /** * Create a new filtered reader. * * @param in * A Reader object providing the underlying stream. Must not be *
null
.
* @throws IOException on error.
*/
public FixCrLfFilter(final Reader in) throws IOException {
super(in);
}
// Instance initializer: Executes just after the super() call in this
// class's constructor.
{
tabs = AddAsisRemove.ASIS;
if (Os.isFamily("mac") && !Os.isFamily("unix")) {
ctrlz = AddAsisRemove.REMOVE;
setEol(CrLf.MAC);
} else if (Os.isFamily("dos")) {
ctrlz = AddAsisRemove.ASIS;
setEol(CrLf.DOS);
} else {
ctrlz = AddAsisRemove.REMOVE;
setEol(CrLf.UNIX);
}
}
/**
* Create a new FixCrLfFilter using the passed in Reader for instantiation.
*
* @param rdr
* A Reader object providing the underlying stream. Must not be
* null
.
*
* @return a new filter based on this configuration, but filtering the
* specified reader.
*/
public Reader chain(final Reader rdr) {
try {
FixCrLfFilter newFilter = new FixCrLfFilter(rdr);
newFilter.setJavafiles(getJavafiles());
newFilter.setEol(getEol());
newFilter.setTab(getTab());
newFilter.setTablength(getTablength());
newFilter.setEof(getEof());
newFilter.setFixlast(getFixlast());
newFilter.initInternalFilters();
return newFilter;
} catch (IOException e) {
throw new BuildException(e);
}
}
/**
* Get how DOS EOF (control-z) characters are being handled.
*
* @return values:
* * This attribute is only used in association with the "tab" * attribute. Tabs found in Java literals are protected from changes by this * filter. * * @return true if whitespace in Java character and string literals is * ignored. */ public boolean getJavafiles() { return javafiles; } /** * Return how tab characters are being handled. * * @return values: *
* The input stream is already buffered by the copy task so this doesn't * significantly impact performance while it makes writing the individual * fix filters much easier. *
*/ private static class SimpleFilterReader extends Reader { private static final int PREEMPT_BUFFER_LENGTH = 16; private Reader in; private int[] preempt = new int[PREEMPT_BUFFER_LENGTH]; private int preemptIndex = 0; public SimpleFilterReader(Reader in) { this.in = in; } public void push(char c) { push((int) c); } public void push(int c) { try { preempt[preemptIndex++] = c; } catch (ArrayIndexOutOfBoundsException e) { int[] p2 = new int[preempt.length * 2]; System.arraycopy(preempt, 0, p2, 0, preempt.length); preempt = p2; push(c); } } public void push(char[] cs, int start, int length) { for (int i = start + length - 1; i >= start;) { push(cs[i--]); } } public void push(char[] cs) { push(cs, 0, cs.length); } /** * Does this filter want to block edits on the last character returned * by read()? */ public boolean editsBlocked() { return in instanceof SimpleFilterReader && ((SimpleFilterReader) in).editsBlocked(); } public int read() throws java.io.IOException { return preemptIndex > 0 ? preempt[--preemptIndex] : in.read(); } public void close() throws java.io.IOException { in.close(); } public void reset() throws IOException { in.reset(); } public boolean markSupported() { return in.markSupported(); } public boolean ready() throws java.io.IOException { return in.ready(); } public void mark(int i) throws java.io.IOException { in.mark(i); } public long skip(long i) throws java.io.IOException { return in.skip(i); } public int read(char[] buf) throws java.io.IOException { return read(buf, 0, buf.length); } public int read(char[] buf, int start, int length) throws java.io.IOException { int count = 0; int c = 0; // CheckStyle:InnerAssignment OFF - leave alone while (length-- > 0 && (c = this.read()) != -1) { buf[start++] = (char) c; count++; } // if at EOF with no characters in the buffer, return EOF return (count == 0 && c == -1) ? -1 : count; } } private static class MaskJavaTabLiteralsFilter extends SimpleFilterReader { private boolean editsBlocked = false; private static final int JAVA = 1; private static final int IN_CHAR_CONST = 2; private static final int IN_STR_CONST = 3; private static final int IN_SINGLE_COMMENT = 4; private static final int IN_MULTI_COMMENT = 5; private static final int TRANS_TO_COMMENT = 6; private static final int TRANS_FROM_MULTI = 8; private int state; public MaskJavaTabLiteralsFilter(Reader in) { super(in); state = JAVA; } public boolean editsBlocked() { return editsBlocked || super.editsBlocked(); } public int read() throws IOException { int thisChar = super.read(); // Mask, block from being edited, all characters in constants. editsBlocked = (state == IN_CHAR_CONST || state == IN_STR_CONST); switch (state) { case JAVA: // The current character is always emitted. switch (thisChar) { case '\'': state = IN_CHAR_CONST; break; case '"': state = IN_STR_CONST; break; case '/': state = TRANS_TO_COMMENT; break; default: // Fall tru } break; case IN_CHAR_CONST: switch (thisChar) { case '\'': state = JAVA; break; default: // Fall tru } break; case IN_STR_CONST: switch (thisChar) { case '"': state = JAVA; break; default: // Fall tru } break; case IN_SINGLE_COMMENT: // The current character is always emitted. switch (thisChar) { case '\n': case '\r': // EOL state = JAVA; break; default: // Fall tru } break; case IN_MULTI_COMMENT: // The current character is always emitted. switch (thisChar) { case '*': state = TRANS_FROM_MULTI; break; default: // Fall tru } break; case TRANS_TO_COMMENT: // The current character is always emitted. switch (thisChar) { case '*': state = IN_MULTI_COMMENT; break; case '/': state = IN_SINGLE_COMMENT; break; case '\'': state = IN_CHAR_CONST; break; case '"': state = IN_STR_CONST; break; default: state = JAVA; } break; case TRANS_FROM_MULTI: // The current character is always emitted. switch (thisChar) { case '/': state = JAVA; break; default: // Fall tru } break; default: // Fall tru } return thisChar; } } private static class NormalizeEolFilter extends SimpleFilterReader { private boolean previousWasEOL; private boolean fixLast; private int normalizedEOL = 0; private char[] eol = null; public NormalizeEolFilter(Reader in, String eolString, boolean fixLast) { super(in); eol = eolString.toCharArray(); this.fixLast = fixLast; } public int read() throws IOException { int thisChar = super.read(); if (normalizedEOL == 0) { int numEOL = 0; boolean atEnd = false; switch (thisChar) { case CTRLZ: int c = super.read(); if (c == -1) { atEnd = true; if (fixLast && !previousWasEOL) { numEOL = 1; push(thisChar); } } else { push(c); } break; case -1: atEnd = true; if (fixLast && !previousWasEOL) { numEOL = 1; } break; case '\n': // EOL was "\n" numEOL = 1; break; case '\r': numEOL = 1; int c1 = super.read(); int c2 = super.read(); if (c1 == '\r' && c2 == '\n') { // EOL was "\r\r\n" } else if (c1 == '\r') { // EOL was "\r\r" - handle as two consecutive "\r" and // "\r" numEOL = 2; push(c2); } else if (c1 == '\n') { // EOL was "\r\n" push(c2); } else { // EOL was "\r" push(c2); push(c1); } default: // Fall tru } if (numEOL > 0) { while (numEOL-- > 0) { push(eol); normalizedEOL += eol.length; } previousWasEOL = true; thisChar = read(); } else if (!atEnd) { previousWasEOL = false; } } else { normalizedEOL--; } return thisChar; } } private static class AddEofFilter extends SimpleFilterReader { private int lastChar = -1; public AddEofFilter(Reader in) { super(in); } public int read() throws IOException { int thisChar = super.read(); // if source is EOF but last character was NOT ctrl-z, return ctrl-z if (thisChar == -1) { if (lastChar != CTRLZ) { lastChar = CTRLZ; return lastChar; } } else { lastChar = thisChar; } return thisChar; } } private static class RemoveEofFilter extends SimpleFilterReader { private int lookAhead = -1; public RemoveEofFilter(Reader in) { super(in); try { lookAhead = in.read(); } catch (IOException e) { lookAhead = -1; } } public int read() throws IOException { int lookAhead2 = super.read(); // If source at EOF and lookAhead is ctrl-z, return EOF (NOT ctrl-z) if (lookAhead2 == -1 && lookAhead == CTRLZ) { return -1; } // Return current look-ahead int i = lookAhead; lookAhead = lookAhead2; return i; } } private static class AddTabFilter extends SimpleFilterReader { private int columnNumber = 0; private int tabLength = 0; public AddTabFilter(Reader in, int tabLength) { super(in); this.tabLength = tabLength; } public int read() throws IOException { int c = super.read(); switch (c) { case '\r': case '\n': columnNumber = 0; break; case ' ': columnNumber++; if (!editsBlocked()) { int colNextTab = ((columnNumber + tabLength - 1) / tabLength) * tabLength; int countSpaces = 1; int numTabs = 0; scanWhitespace: while ((c = super.read()) != -1) { switch (c) { case ' ': if (++columnNumber == colNextTab) { numTabs++; countSpaces = 0; colNextTab += tabLength; } else { countSpaces++; } break; case '\t': columnNumber = colNextTab; numTabs++; countSpaces = 0; colNextTab += tabLength; break; default: push(c); break scanWhitespace; } } while (countSpaces-- > 0) { push(' '); columnNumber--; } while (numTabs-- > 0) { push('\t'); columnNumber -= tabLength; } c = super.read(); switch (c) { case ' ': columnNumber++; break; case '\t': columnNumber += tabLength; break; default: // Fall tru } } break; case '\t': columnNumber = ((columnNumber + tabLength - 1) / tabLength) * tabLength; break; default: columnNumber++; } return c; } } private static class RemoveTabFilter extends SimpleFilterReader { private int columnNumber = 0; private int tabLength = 0; public RemoveTabFilter(Reader in, int tabLength) { super(in); this.tabLength = tabLength; } public int read() throws IOException { int c = super.read(); switch (c) { case '\r': case '\n': columnNumber = 0; break; case '\t': int width = tabLength - columnNumber % tabLength; if (!editsBlocked()) { for (; width > 1; width--) { push(' '); } c = ' '; } columnNumber += width; break; default: columnNumber++; } return c; } } /** * Enumerated attribute with the values "asis", "add" and "remove". */ public static class AddAsisRemove extends EnumeratedAttribute { private static final AddAsisRemove ASIS = newInstance("asis"); private static final AddAsisRemove ADD = newInstance("add"); private static final AddAsisRemove REMOVE = newInstance("remove"); /** {@inheritDoc}. */ public String[] getValues() { return new String[] {"add", "asis", "remove"}; } /** * Equality depending in the index. * @param other the object to test equality against. * @return true if the object has the same index as this. */ public boolean equals(Object other) { return other instanceof AddAsisRemove && getIndex() == ((AddAsisRemove) other).getIndex(); } /** * Hashcode depending on the index. * @return the index as the hashcode. */ public int hashCode() { return getIndex(); } AddAsisRemove resolve() throws IllegalStateException { if (this.equals(ASIS)) { return ASIS; } if (this.equals(ADD)) { return ADD; } if (this.equals(REMOVE)) { return REMOVE; } throw new IllegalStateException("No replacement for " + this); } // Works like clone() but doesn't show up in the Javadocs private AddAsisRemove newInstance() { return newInstance(getValue()); } /** * Create an instance of this enumerated value based on the string value. * @param value the value to use. * @return an enumerated instance. */ public static AddAsisRemove newInstance(String value) { AddAsisRemove a = new AddAsisRemove(); a.setValue(value); return a; } } /** * Enumerated attribute with the values "asis", "cr", "lf" and "crlf". */ public static class CrLf extends EnumeratedAttribute { private static final CrLf ASIS = newInstance("asis"); private static final CrLf CR = newInstance("cr"); private static final CrLf CRLF = newInstance("crlf"); private static final CrLf DOS = newInstance("dos"); private static final CrLf LF = newInstance("lf"); private static final CrLf MAC = newInstance("mac"); private static final CrLf UNIX = newInstance("unix"); /** * @see EnumeratedAttribute#getValues */ /** {@inheritDoc}. */ public String[] getValues() { return new String[] {"asis", "cr", "lf", "crlf", "mac", "unix", "dos"}; } /** * Equality depending in the index. * @param other the object to test equality against. * @return true if the object has the same index as this. */ public boolean equals(Object other) { return other instanceof CrLf && getIndex() == ((CrLf) other).getIndex(); } /** * Hashcode depending on the index. * @return the index as the hashcode. */ public int hashCode() { return getIndex(); } CrLf resolve() { if (this.equals(ASIS)) { return ASIS; } if (this.equals(CR) || this.equals(MAC)) { return CR; } if (this.equals(CRLF) || this.equals(DOS)) { return CRLF; } if (this.equals(LF) || this.equals(UNIX)) { return LF; } throw new IllegalStateException("No replacement for " + this); } // Works like clone() but doesn't show up in the Javadocs private CrLf newInstance() { return newInstance(getValue()); } /** * Create an instance of this enumerated value based on the string value. * @param value the value to use. * @return an enumerated instance. */ public static CrLf newInstance(String value) { CrLf c = new CrLf(); c.setValue(value); return c; } } }