/*
* Copyright (C) 2007, 2008, 2010, 2020 XStream Committers.
* All rights reserved.
*
* The software in this package is published under the terms of the BSD
* style license a copy of which has been included with this distribution in
* the LICENSE.txt file.
*
* Created on 13. September 2007 by Joerg Schaible.
*/
package com.thoughtworks.xstream.core.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PushbackInputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
/**
* A {@link Reader} that evaluates the XML header. It selects its encoding based on the encoding read with the XML
* header of the provided {@link InputStream}. The default encoding is <em>UTF-8</em> and the version is 1.0 if the
* stream does not contain an XML header or the attributes are not set within the header.
*
* @author Jörg Schaible
* @since 1.3
*/
public final class XmlHeaderAwareReader extends Reader {
private final InputStreamReader reader;
private final double version;
private static final String KEY_ENCODING = "encoding";
private static final String KEY_VERSION = "version";
private static final String XML_TOKEN = "?xml";
private static final int STATE_BOM = 0;
private static final int STATE_START = 1;
private static final int STATE_AWAIT_XML_HEADER = 2;
private static final int STATE_ATTR_NAME = 3;
private static final int STATE_ATTR_VALUE = 4;
/**
* Constructs an XmlHeaderAwareReader.
*
* @param in the {@link InputStream}
* @throws UnsupportedEncodingException if the encoding is not supported
* @throws IOException occurred while reading the XML header
* @since 1.3
*/
public XmlHeaderAwareReader(final InputStream in) throws UnsupportedEncodingException, IOException {
final PushbackInputStream[] pin = new PushbackInputStream[]{in instanceof PushbackInputStream
? (PushbackInputStream)in
: new PushbackInputStream(in, 64)};
final Map header = getHeader(pin);
version = Double.parseDouble((String)header.get(KEY_VERSION));
reader = new InputStreamReader(pin[0], (String)header.get(KEY_ENCODING));
}
private Map getHeader(final PushbackInputStream[] in) throws IOException {
final Map header = new HashMap();
header.put(KEY_ENCODING, "UTF-8");
header.put(KEY_VERSION, "1.0");
int state = STATE_BOM;
final ByteArrayOutputStream out = new ByteArrayOutputStream(64);
int i = 0;
char ch = 0;
char valueEnd = 0;
final StringBuffer name = new StringBuffer();
final StringBuffer value = new StringBuffer();
boolean escape = false;
while (i != -1 && (i = in[0].read()) != -1) {
out.write(i);
ch = (char)i;
switch (state) {
case STATE_BOM:
if ((ch == 0xEF && out.size() == 1)
|| (ch == 0xBB && out.size() == 2)
|| (ch == 0xBF && out.size() == 3)) {
if (ch == 0xBF) {
out.reset();
state = STATE_START;
}
break;
} else if (out.size() > 1) {
i = -1;
break;
} else {
state = STATE_START;
}
// fall through
case STATE_START:
if (!Character.isWhitespace(ch)) {
if (ch == '<') {
state = STATE_AWAIT_XML_HEADER;
} else {
i = -1;
}
}
break;
case STATE_AWAIT_XML_HEADER:
if (!Character.isWhitespace(ch)) {
name.append(Character.toLowerCase(ch));
if (!XML_TOKEN.startsWith(name.substring(0))) {
i = -1;
}
} else {
if (name.toString().equals(XML_TOKEN)) {
state = STATE_ATTR_NAME;
name.setLength(0);
} else {
i = -1;
}
}
break;
case STATE_ATTR_NAME:
if (!Character.isWhitespace(ch)) {
if (ch == '=') {
state = STATE_ATTR_VALUE;
} else {
ch = Character.toLowerCase(ch);
if (Character.isLetter(ch)) {
name.append(ch);
} else {
i = -1;
}
}
} else if (name.length() > 0) {
i = -1;
}
break;
case STATE_ATTR_VALUE:
if (valueEnd == 0) {
if (ch == '"' || ch == '\'') {
valueEnd = ch;
} else {
i = -1;
}
} else {
if (ch == '\\' && !escape) {
escape = true;
break;
}
if (ch == valueEnd && !escape) {
valueEnd = 0;
state = STATE_ATTR_NAME;
header.put(name.toString(), value.toString());
name.setLength(0);
value.setLength(0);
} else {
escape = false;
if (ch != '\n') {
value.append(ch);
} else {
i = -1;
}
}
}
break;
}
}
byte[] pushbackData = out.toByteArray();
for (i = pushbackData.length; i-- > 0;) {
final byte b = pushbackData[i];
try {
in[0].unread(b);
} catch (IOException ex) {
in[0] = new PushbackInputStream(in[0], ++i);
}
}
return header;
}
/**
* @see InputStreamReader#getEncoding()
* @since 1.3
*/
public String getEncoding() {
return reader.getEncoding();
}
/**
* @see InputStreamReader#getEncoding()
* @since 1.3
*/
public double getVersion() {
return version;
}
/**
* @see java.io.Reader#mark(int)
*/
public void mark(final int readAheadLimit) throws IOException {
reader.mark(readAheadLimit);
}
/**
* @see java.io.Reader#markSupported()
*/
public boolean markSupported() {
return reader.markSupported();
}
/**
* @see java.io.Reader#read()
*/
public int read() throws IOException {
return reader.read();
}
/**
* @see java.io.Reader#read(char[], int, int)
*/
public int read(final char[] cbuf, final int offset, final int length) throws IOException {
return reader.read(cbuf, offset, length);
}
/**
* @see java.io.Reader#read(char[])
*/
public int read(final char[] cbuf) throws IOException {
return reader.read(cbuf);
}
// TODO: This is JDK 1.5
// public int read(final CharBuffer target) throws IOException {
// return reader.read(target);
// }
/**
* @see java.io.Reader#ready()
*/
public boolean ready() throws IOException {
return reader.ready();
}
/**
* @see java.io.Reader#reset()
*/
public void reset() throws IOException {
reader.reset();
}
/**
* @see java.io.Reader#skip(long)
*/
public long skip(final long n) throws IOException {
return reader.skip(n);
}
/**
* @see java.io.Reader#close()
*/
public void close() throws IOException {
reader.close();
}
/**
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(final Object obj) {
return reader.equals(obj);
}
/**
* @see java.lang.Object#hashCode()
*/
public int hashCode() {
return reader.hashCode();
}
/**
* @see java.lang.Object#toString()
*/
public String toString() {
return reader.toString();
}
}