blob: 39013f4e51042749d8afc5ed6bc9e935fc7cbdea [file] [log] [blame]
/*
* 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.sis.io;
import java.util.Objects;
import java.io.IOException;
import java.io.CharConversionException;
import static org.apache.sis.util.Characters.isLineOrParagraphSeparator;
/**
* Base class for writing filtered characters to another {@link Appendable}.
* This base class performs a work similar to the {@link java.io.FilterWriter} work,
* except for the following:
*
* <ul>
* <li>The filtered output is sent to an arbitrary {@link Appendable} instead of
* to the {@link java.io.Writer} sub-type.</li>
* <li>No synchronization is performed.</li>
* </ul>
*
* If needed, this {@code Appender} can be viewed as a synchronized
* {@link java.io.Writer} by invoking the {@link IO#asWriter(Appendable)} method.
*
* <h2>Flushing and closing the stream</h2>
* Subclasses implement the {@link java.io.Flushable} interface only if they
* hold data in an internal buffer before to send them to the wrapped {@code Appendable}.
* This is the case of {@link TableAppender} and {@link LineAppender} for instance.
* For unconditionally flushing or closing an {@code Appendable} and its underlying stream,
* see {@link IO#flush(Appendable)} and {@link IO#close(Appendable)}.
*
* @author Martin Desruisseaux (Geomatys)
*
* @see java.io.FilterWriter
*/
abstract class Appender implements Appendable {
/**
* The underlying character output stream or buffer.
*/
protected final Appendable out;
/**
* If the last character given to {@link #toCodePoint(char)} if it was a high surrogate,
* or 0 otherwise.
*/
private char highSurrogate;
/**
* Creates a new appender which will send its output to the given stream or buffer.
*
* @param out the underlying character output stream or buffer.
*/
protected Appender(final Appendable out) {
this.out = Objects.requireNonNull(out);
}
/**
* Finds the line separator used in the given character sequence portion, or returns
* {@code null} if unknown. This method is designed for invocation at the beginning
* of {@code append(CharSequence, ...)}, before the characters are effectively read.
*/
final String lineSeparator(final CharSequence sequence, int start, final int end) {
if (isHighSurrogate()) {
start++; // Skip invalid character.
}
while (start < end) {
final int c = Character.codePointAt(sequence, start);
final int b = start;
start += Character.charCount(c);
if (isLineOrParagraphSeparator(c)) {
if (c == '\r' && (start < end) && sequence.charAt(start) == '\n') {
start++;
}
return sequence.subSequence(b, start).toString();
}
}
return null;
}
/**
* Returns the code point for the given character, or -1 if we need to wait for the next
* character. This method computes the code point from the given character and the character
* given to the previous call of this method. This works only if this method is consistently
* invoked for every characters.
*/
final int toCodePoint(final char c) {
final char h = highSurrogate;
if (h != 0) {
highSurrogate = 0;
if (Character.isLowSurrogate(c)) {
return Character.toCodePoint(h, c);
}
/*
* If we reach this point, we have unpaired surrogate. This is usually an error,
* but this not the fault of this class since we are processing data supplied by
* the user. Be lenient.
*/
}
if (Character.isHighSurrogate(c)) {
highSurrogate = c;
return -1;
}
return c;
}
/**
* Returns {@code true} if the last character given to {@link #toCodePoint(char)}
* is a {@linkplain Character#isHighSurrogate(char) high surrogate}.
*/
final boolean isHighSurrogate() {
return highSurrogate != 0;
}
/**
* If the given sequence begins with a low surrogate completing a previous high surrogate,
* delegates to {@link #append(char)} and returns {@code start+1}. The intent is to avoid
* processing a character sequence which starts by an invalid code point.
*
* @param sequence the character sequence to write.
* @param start index of the first character to write by this method or by the caller.
* @param end index after the last character to be written by the caller.
* @return index of the first character which need to be written by the caller.
*/
final int appendSurrogate(final CharSequence sequence, int start, final int end) throws IOException {
if (start != end && highSurrogate != 0) {
final char c = sequence.charAt(start);
if (Character.isLowSurrogate(c)) {
append(c);
start++;
} else {
throw new CharConversionException();
}
}
return start;
}
/**
* Appends the given code point to the underlying {@link #out} stream or buffer.
*
* @param c the code point to append.
* @throws IOException if an error occurred while appending the code point.
*/
final void appendCodePoint(final int c) throws IOException {
if (Character.isBmpCodePoint(c)) {
out.append((char) c);
} else if (Character.isSupplementaryCodePoint(c)) {
out.append(Character.highSurrogate(c))
.append(Character. lowSurrogate(c));
} else {
throw new CharConversionException();
}
}
/**
* Appends the specified character sequence.
* The default implementation delegates to {@link #append(CharSequence, int, int)}.
*
* @param sequence the character sequence to append, or {@code null}.
* @return a reference to this {@code Appendable}.
* @throws IOException if an I/O error occurred.
*/
@Override
public Appendable append(CharSequence sequence) throws IOException {
if (sequence == null) {
sequence = "null";
}
return append(sequence, 0, sequence.length());
}
/**
* Returns the content of this {@code Appendable} as a string if possible,
* or the localized <q>Unavailable content</q> string otherwise.
*
* @return the content of this {@code Appendable}, or a localized message for unavailable content.
*
* @see IO#content(Appendable)
*/
@Override
public String toString() {
return IO.toString(this);
}
}