/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.io; import java.io.FilterWriter; import java.io.IOException; import java.io.Writer; import org.geotools.resources.XArray; /** * Writes characters to a stream while replacing various EOL by a unique string. This class * catches all occurrences of {@code "\r"}, {@code "\n"} and {@code "\r\n"}, and replaces them * by the platform depend EOL string ({@code "\r\n"} on Windows, {@code "\n"} on Unix), or any * other EOL explicitly set at construction time. This writer also remove trailing blanks before * end of lines, but this behavior can be changed by overriding {@link #isWhitespace}. * * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) * * @since 2.0 */ public class LineWriter extends FilterWriter { /** * The line separator for End Of Line (EOL). */ private String lineSeparator; /** * Tells if the next '\n' character must be ignored. This field * is used in order to avoid writing two EOL in place of "\r\n". */ private boolean skipCR; /** * Temporary buffer containing blanks to write. Whitespaces are put * in this buffer before to be written. If whitespaces are followed * by a character, they are written to the underlying stream before * the character. Otherwise, if whitespaces are followed by a line * separator, then they are discarted. The buffer capacity will be * expanded as needed. */ private char[] buffer = new char[64]; /** * Number of valid characters in {@link #buffer}. */ private int count = 0; /** * Constructs a {@code LineWriter} object that will use the platform dependent line separator. * * @param out A writer object to provide the underlying stream. * @throws IllegalArgumentException if {@code out} is {@code null}. */ public LineWriter(final Writer out) { this(out, System.getProperty("line.separator", "\n")); } /** * Constructs a {@code LineWriter} object that will use the specified line separator. * * @param out A writer object to provide the underlying stream. * @param lineSeparator String to use as line separator. * @throws IllegalArgumentException if {@code out} or {@code lineSeparator} is {@code null}. */ public LineWriter(final Writer out, final String lineSeparator) { super(out); this.lineSeparator = lineSeparator; if (out==null || lineSeparator==null) { throw new IllegalArgumentException(); } } /** * Returns the current line separator. * * @return The current line separator. */ public String getLineSeparator() { return lineSeparator; } /** * Changes the line separator. This is the string to insert in place of every occurences of * {@code "\r"}, {@code "\n"} or {@code "\r\n"}. * * @param lineSeparator The new line separator. * @throws IllegalArgumentException If {@code lineSeparator} is {@code null}. */ public void setLineSeparator(final String lineSeparator) { if (lineSeparator == null) { throw new IllegalArgumentException(); } synchronized (lock) { this.lineSeparator = lineSeparator; } } /** * Writes a line separator. * * @throws IOException If an I/O error occurs. */ private void writeEOL() throws IOException { assert count == 0 : count; // Do NOT call super.write(String). out.write(lineSeparator); } /** * Returns {@code true} if {@link #buffer} contains only white spaces. It should * always be the case. This method is used for assertions only. */ private boolean bufferBlank() throws IOException { for (int i=count; --i>=0;) { if (!isWhitespace(buffer[i])) { return false; } } return true; } /** * Flushs the content of {@link #buffer} to the underlying stream. * * @throws IOException If an I/O error occurs. */ private void flushBuffer() throws IOException { assert bufferBlank(); if (count != 0) { out.write(buffer, 0, count); count = 0; } } /** * Writes a portion of an array of characters. This portion must * <strong>not</strong> contains any line separator. */ private void writeLine(final char[] cbuf, final int lower, int upper) throws IOException { while (upper != lower) { final char c = cbuf[upper-1]; assert (c!='\r' && c!='\n'); if (isWhitespace(c)) { upper--; continue; } flushBuffer(); out.write(cbuf, lower, upper-lower); return; } assert bufferBlank(); count = 0; } /** * Writes a portion of an array of characters. This portion must * <strong>not</strong> contains any line separator. */ private void writeLine(final String str, final int lower, int upper) throws IOException { while (upper != lower) { final char c = str.charAt(upper-1); assert (c!='\r' && c!='\n'); if (isWhitespace(c)) { upper--; continue; } flushBuffer(); out.write(str, lower, upper-lower); return; } assert bufferBlank(); count = 0; } /** * Writes a single character. * * @throws IOException If an I/O error occurs. */ @Override public void write(final int c) throws IOException { synchronized (lock) { switch (c) { case '\r': { assert bufferBlank(); count = 0; // Discard whitespaces writeEOL(); skipCR = true; break; } case '\n': { if (!skipCR) { assert bufferBlank(); count = 0; // Discard whitespaces writeEOL(); } skipCR = false; break; } default: { if (c>=Character.MIN_VALUE && c<=Character.MAX_VALUE && isWhitespace((char)c)) { if (count >= buffer.length) { buffer = XArray.resize(buffer, count + Math.min(8192, count)); } buffer[count++] = (char)c; } else { flushBuffer(); out.write(c); } skipCR = false; break; } } } } /** * Writes a portion of an array of characters. * * @param cbuf Buffer of characters to be written. * @param offset Offset from which to start reading characters. * @param length Number of characters to be written. * @throws IOException If an I/O error occurs. */ @Override public void write(final char cbuf[], int offset, int length) throws IOException { if (offset<0 || length<0 || (offset+length)>cbuf.length) { throw new IndexOutOfBoundsException(); } if (length == 0) { return; } synchronized (lock) { if (skipCR && cbuf[offset]=='\n') { offset++; length--; } int upper = offset; for (; length!=0; length--) { switch (cbuf[upper++]) { case '\r': { writeLine(cbuf, offset, upper-1); writeEOL(); if (length > 1 && cbuf[upper] == '\n') { upper++; length--; } offset = upper; break; } case '\n': { writeLine(cbuf, offset, upper-1); writeEOL(); offset = upper; break; } } } skipCR = (cbuf[upper-1] == '\r'); /* * Write the remainding characters and * put trailing blanks into the buffer. */ for (int i=upper; --i>=offset;) { if (!isWhitespace(cbuf[i])) { writeLine(cbuf, offset, offset=i+1); break; } } length = upper - offset; final int newCount = count + length; if (newCount > buffer.length) { buffer = XArray.resize(buffer, newCount); } System.arraycopy(cbuf, offset, buffer, count, length); count = newCount; } } /** * Writes a portion of an array of a string. * * @param string String to be written. * @param offset Offset from which to start reading characters. * @param length Number of characters to be written. * @throws IOException If an I/O error occurs. */ @Override public void write(final String string, int offset, int length) throws IOException { if (offset<0 || length<0 || (offset+length)>string.length()) { throw new IndexOutOfBoundsException(); } if (length == 0) { return; } synchronized (lock) { if (skipCR && string.charAt(offset)=='\n') { offset++; length--; } int upper = offset; for (; length!=0; length--) { switch (string.charAt(upper++)) { case '\r': { writeLine(string, offset, upper-1); writeEOL(); if (length > 1 && string.charAt(upper) == '\n') { upper++; length--; } offset=upper; break; } case '\n': { writeLine(string, offset, upper-1); writeEOL(); offset=upper; break; } } } skipCR = (string.charAt(upper-1) == '\r'); /* * Write the remainding characters and * put trailing blanks into the buffer. */ for (int i=upper; --i>=offset;) { if (!isWhitespace(string.charAt(i))) { writeLine(string, offset, offset=i+1); break; } } length = upper - offset; final int newCount = count+length; if (newCount > buffer.length) { buffer = XArray.resize(buffer, newCount); } while (--length>=0) { buffer[count++] = string.charAt(offset++); } assert count == newCount : newCount; } } /** * Flushs the stream's content to the underlying stream. This method flush completly * all internal buffers, including any whitespace characters that should have been * skipped if the next non-blank character is a line separator. * * @throws IOException If an I/O error occurs. */ @Override public void flush() throws IOException { synchronized (lock) { flushBuffer(); super.flush(); } } /** * Returns {@code true} if the specified character is a white space that can be ignored * on end of line. The default implementation returns {@link Character#isSpaceChar(char)}. * Subclasses can override this method in order to change the criterion. * * @param c The character to test. * @return {@code true} if {@code c} is a character that can be ignored on end of line. * @throws IOException if this method can not determine if the character is ignoreable. * * @since 2.5 */ protected boolean isWhitespace(final char c) throws IOException { return Character.isSpaceChar(c); } }