EscapeControlCharsReader.java
/*
** Module : EscapeControlCharsReader.java
** Public : A wrapper which automatically escapes ISO control characters, except whitespace.
**
** Copyright (c) 2021-2022, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA 20210709 Created initial version.
** TJD 20220504 Java 11 compatibility minor changes
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.xml;
import java.io.*;
import java.util.LinkedList;
/**
* Escape all ISO control characters from this reader, to be valid in XML 1.1. Any whitespace character is
* not escaped.
*/
public class EscapeControlCharsReader
extends FilterReader
{
/**
* Create a new instance for the specified reader.
* @param in
*/
public EscapeControlCharsReader(Reader in)
{
super(in);
}
/** A buffer which holds the characters temporarily, as ISO control characters are escaped. */
private LinkedList<Character> buffer = new LinkedList<>();
/**
* Read the next character from the stream.
* <p>
* If the next character is a non-whitespace ISO control character, the escape sequence will be added to
* the {@link #buffer} and characters will be polled from it until is emptied.
*
* @return The next character in the reader.
*/
@Override
public int read()
throws IOException
{
if (!buffer.isEmpty())
{
return buffer.poll();
}
int ch = super.read();
if (ch == -1)
{
return -1;
}
if (Character.isISOControl((char) ch) && !Character.isWhitespace((char) ch))
{
// escape the character
String escaped = "&#" + Integer.toString(ch) + ";";
escaped.toString().chars().forEach(i -> buffer.add((char) i));
ch = buffer.poll();
}
return ch;
}
/**
* Read a bunch of characters from the stream.
* <p>
* Any non-whitespace ISO control character which was read will be escaped and {@link #buffer} will be
* populated with the entire chunk of characters, including the escape sequences. The buffer will be used
* until is emptied.
*
* @param cbuf
* The array where to hold the read characters.
* @param off
* The offset in the array.
* @param len
* The maximum number of characters to be read.
*/
@Override
public int read(char[] cbuf, int off, int len)
throws IOException
{
int idx = off;
int firstBufLen = 0;
while (!buffer.isEmpty() && idx < cbuf.length && firstBufLen < len)
{
// fill the cbuf with the buffer
cbuf[idx] = buffer.poll();
idx = idx + 1;
firstBufLen = firstBufLen + 1;
}
if (idx == cbuf.length || firstBufLen == len)
{
// we read the required amount of characters or it does not fit in the passed buffer
return firstBufLen;
}
// start from the next index to which to add characters
off = idx;
// more characters can be read, ensure we do not read more than the required length
int num = super.read(cbuf, off, len - firstBufLen);
if (num == -1)
{
// EOF
return -1;
}
// process the characters
boolean bufferChanged = false;
LinkedList<Character> newBuffer = new LinkedList<>();
for (int j = 0; j < num; j++)
{
char ch = cbuf[off + j];
if (Character.isISOControl((char) ch) && !Character.isWhitespace((char) ch))
{
// escape the character and add it to the buffer
String escaped = "&#" + Integer.toString(ch) + ";";
escaped.toString().chars().forEach(i -> newBuffer.add((char) (i & 0xFFFF)));
bufferChanged = true;
}
else
{
newBuffer.add(ch);
}
}
if (!bufferChanged)
{
// no character was escaped
return num + firstBufLen;
}
buffer.addAll(newBuffer);
// if the buffer has changed, then we need to adjust the array parameter; return the exact
// number of chars as reported by the reader
idx = off;
int nextBufLen = 0;
while (nextBufLen < num)
{
// fill the cbuf with the buffer
cbuf[idx] = buffer.poll();
idx = idx + 1;
nextBufLen = nextBufLen + 1;
}
return num + firstBufLen;
}
}