pull/952/head
ag2s20150909 4 years ago
parent e9c0aac7af
commit c78ebfe58a
  1. 4
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
  2. 327
      epublib/src/main/java/me/ag2s/epublib/util/IOUtil.java
  3. 13
      epublib/src/main/java/me/ag2s/epublib/util/ResourceUtil.java
  4. 24
      epublib/src/main/java/me/ag2s/epublib/util/StringUtil.java
  5. 322
      epublib/src/main/java/me/ag2s/epublib/util/commons/io/BOMInputStream.java
  6. 56
      epublib/src/main/java/me/ag2s/epublib/util/commons/io/ByteOrderMark.java
  7. 329
      epublib/src/main/java/me/ag2s/epublib/util/commons/io/XmlStreamReader.java
  8. 16
      epublib/src/main/java/me/ag2s/epublib/util/commons/io/XmlStreamReaderException.java

@ -118,12 +118,12 @@ object ChapterProvider {
val matcher = AppPattern.imgPattern.matcher(text) val matcher = AppPattern.imgPattern.matcher(text)
if (matcher.find()) { if (matcher.find()) {
matcher.group(1)?.let { src -> matcher.group(1)?.let { src ->
if (!book.isEpub()) { //if (!book.isEpub()) {
durY = setTypeImage( durY = setTypeImage(
book, bookChapter, src, book, bookChapter, src,
durY, textPages, book.getImageStyle() durY, textPages, book.getImageStyle()
) )
} //}
} }
} else { } else {
val isTitle = index == 0 val isTitle = index == 0

@ -1,6 +1,5 @@
package me.ag2s.epublib.util; package me.ag2s.epublib.util;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -12,154 +11,204 @@ import java.io.Writer;
/** /**
* Most of the functions herein are re-implementations of the ones in * Most of the functions herein are re-implementations of the ones in
* apache io IOUtils. * apache io IOUtils.
* * <p>
* The reason for re-implementing this is that the functions are fairly simple * The reason for re-implementing this is that the functions are fairly simple
* and using my own implementation saves the inclusion of a 200Kb jar file. * and using my own implementation saves the inclusion of a 200Kb jar file.
*/ */
public class IOUtil { public class IOUtil {
public static final int IO_COPY_BUFFER_SIZE = 1024 * 4; /**
* Represents the end-of-file (or stream).
/** * @since 2.5 (made public)
* Gets the contents of the Reader as a byte[], with the given character encoding. */
* public static final int EOF = -1;
* @param in g
* @param encoding g public static final int IO_COPY_BUFFER_SIZE = 1024 * 8;
* @return the contents of the Reader as a byte[], with the given character encoding. public static final int DEFAULT_BUFFER_SIZE = 8192;
* @throws IOException g
*/ /**
public static byte[] toByteArray(Reader in, String encoding) * Gets the contents of the Reader as a byte[], with the given character encoding.
throws IOException { *
StringWriter out = new StringWriter(); * @param in g
copy(in, out); * @param encoding g
out.flush(); * @return the contents of the Reader as a byte[], with the given character encoding.
return out.toString().getBytes(encoding); * @throws IOException g
} */
public static byte[] toByteArray(Reader in, String encoding)
/** throws IOException {
* Returns the contents of the InputStream as a byte[] StringWriter out = new StringWriter();
* copy(in, out);
* @param in f out.flush();
* @return the contents of the InputStream as a byte[] return out.toString().getBytes(encoding);
* @throws IOException f
*/
public static byte[] toByteArray(InputStream in) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
copy(in, result);
result.flush();
return result.toByteArray();
}
/**
* Reads data from the InputStream, using the specified buffer size.
*
* This is meant for situations where memory is tight, since
* it prevents buffer expansion.
*
* @param in the stream to read data from
* @param size the size of the array to create
* @return the array, or null
* @throws IOException f
*/
public static byte[] toByteArray(InputStream in, int size)
throws IOException {
try {
ByteArrayOutputStream result;
if (size > 0) {
result = new ByteArrayOutputStream(size);
} else {
result = new ByteArrayOutputStream();
}
copy(in, result);
result.flush();
return result.toByteArray();
} catch (OutOfMemoryError error) {
//Return null so it gets loaded lazily.
return null;
} }
} /**
* Returns the contents of the InputStream as a byte[]
*
/** * @param in f
* if totalNrRead &lt; 0 then totalNrRead is returned, if * @return the contents of the InputStream as a byte[]
* (nrRead + totalNrRead) &lt; Integer.MAX_VALUE then nrRead + totalNrRead * @throws IOException f
* is returned, -1 otherwise. */
* public static byte[] toByteArray(InputStream in) throws IOException {
* @param nrRead f ByteArrayOutputStream result = new ByteArrayOutputStream();
* @param totalNrNread f copy(in, result);
* @return if totalNrRead &lt; 0 then totalNrRead is returned, if result.flush();
* (nrRead + totalNrRead) &lt; Integer.MAX_VALUE then nrRead + totalNrRead return result.toByteArray();
* is returned, -1 otherwise.
*/
protected static int calcNewNrReadSize(int nrRead, int totalNrNread) {
if (totalNrNread < 0) {
return totalNrNread;
} }
if (totalNrNread > (Integer.MAX_VALUE - nrRead)) {
return -1; /**
} else { * Reads data from the InputStream, using the specified buffer size.
return (totalNrNread + nrRead); * <p>
* This is meant for situations where memory is tight, since
* it prevents buffer expansion.
*
* @param in the stream to read data from
* @param size the size of the array to create
* @return the array, or null
* @throws IOException f
*/
public static byte[] toByteArray(InputStream in, int size)
throws IOException {
try {
ByteArrayOutputStream result;
if (size > 0) {
result = new ByteArrayOutputStream(size);
} else {
result = new ByteArrayOutputStream();
}
copy(in, result);
result.flush();
return result.toByteArray();
} catch (OutOfMemoryError error) {
//Return null so it gets loaded lazily.
return null;
}
} }
}
/** /**
* Copies the contents of the InputStream to the OutputStream. * if totalNrRead &lt; 0 then totalNrRead is returned, if
* * (nrRead + totalNrRead) &lt; Integer.MAX_VALUE then nrRead + totalNrRead
* @param in f * is returned, -1 otherwise.
* @param out f *
* @return the nr of bytes read, or -1 if the amount &gt; Integer.MAX_VALUE * @param nrRead f
* @throws IOException f * @param totalNrNread f
*/ * @return if totalNrRead &lt; 0 then totalNrRead is returned, if
public static int copy(InputStream in, OutputStream out) * (nrRead + totalNrRead) &lt; Integer.MAX_VALUE then nrRead + totalNrRead
throws IOException { * is returned, -1 otherwise.
byte[] buffer = new byte[IO_COPY_BUFFER_SIZE]; */
int readSize ; protected static int calcNewNrReadSize(int nrRead, int totalNrNread) {
int result = 0; if (totalNrNread < 0) {
while ((readSize = in.read(buffer)) >= 0) { return totalNrNread;
out.write(buffer, 0, readSize); }
result = calcNewNrReadSize(readSize, result); if (totalNrNread > (Integer.MAX_VALUE - nrRead)) {
return -1;
} else {
return (totalNrNread + nrRead);
}
} }
out.flush();
return result; /**
} * Copies the contents of the InputStream to the OutputStream.
*
/** * @param in f
* Copies the contents of the Reader to the Writer. * @param out f
* * @return the nr of bytes read, or -1 if the amount &gt; Integer.MAX_VALUE
* @param in f * @throws IOException f
* @param out f */
* @return the nr of characters read, or -1 if the amount &gt; Integer.MAX_VALUE public static int copy(InputStream in, OutputStream out)
* @throws IOException f throws IOException {
*/ byte[] buffer = new byte[IO_COPY_BUFFER_SIZE];
public static int copy(Reader in, Writer out) throws IOException { int readSize;
char[] buffer = new char[IO_COPY_BUFFER_SIZE]; int result = 0;
int readSize; while ((readSize = in.read(buffer)) >= 0) {
int result = 0; out.write(buffer, 0, readSize);
while ((readSize = in.read(buffer)) >= 0) { result = calcNewNrReadSize(readSize, result);
out.write(buffer, 0, readSize); }
result = calcNewNrReadSize(readSize, result); out.flush();
return result;
} }
out.flush();
return result; /**
} * Copies the contents of the Reader to the Writer.
*
public static String Stream2String(InputStream inputStream) { * @param in f
String str; * @param out f
try { * @return the nr of characters read, or -1 if the amount &gt; Integer.MAX_VALUE
BufferedInputStream bis = new BufferedInputStream(inputStream); * @throws IOException f
ByteArrayOutputStream buf = new ByteArrayOutputStream(); */
for (int result = bis.read(); result != -1; result = bis.read()) { public static int copy(Reader in, Writer out) throws IOException {
buf.write((byte) result); char[] buffer = new char[IO_COPY_BUFFER_SIZE];
} int readSize;
str=buf.toString("UTF-8"); int result = 0;
}catch (Exception e){ while ((readSize = in.read(buffer)) >= 0) {
str=null; out.write(buffer, 0, readSize);
result = calcNewNrReadSize(readSize, result);
}
out.flush();
return result;
}
/**
* Returns the length of the given array in a null-safe manner.
*
* @param array an array or null
* @return the array length -- or 0 if the given array is null.
* @since 2.7
*/
public static int length(final byte[] array) {
return array == null ? 0 : array.length;
}
/**
* Returns the length of the given array in a null-safe manner.
*
* @param array an array or null
* @return the array length -- or 0 if the given array is null.
* @since 2.7
*/
public static int length(final char[] array) {
return array == null ? 0 : array.length;
}
/**
* Returns the length of the given CharSequence in a null-safe manner.
*
* @param csq a CharSequence or null
* @return the CharSequence length -- or 0 if the given CharSequence is null.
* @since 2.7
*/
public static int length(final CharSequence csq) {
return csq == null ? 0 : csq.length();
}
/**
* Returns the length of the given array in a null-safe manner.
*
* @param array an array or null
* @return the array length -- or 0 if the given array is null.
* @since 2.7
*/
public static int length(final Object[] array) {
return array == null ? 0 : array.length;
}
@SuppressWarnings("unused")
public static String Stream2String(InputStream inputStream) {
ByteArrayOutputStream result = new ByteArrayOutputStream();
try {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int length;
while ((length = inputStream.read(buffer)) != -1) {
result.write(buffer, 0, length);
}
return result.toString();
} catch (Exception e) {
return e.getLocalizedMessage();
}
} }
// StandardCharsets.UTF_8.name() > JDK 7
return str;
}
} }

@ -75,16 +75,15 @@ public class ResourceUtil {
} }
} }
String html = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
html += "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">"; return "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
html += "<head><title>" + title + "</title>" + "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">" +
"<head><title>" + title + "</title>" +
"<link rel=\"stylesheet\" type=\"text/css\" href=\"css/style.css\"/>" + "<link rel=\"stylesheet\" type=\"text/css\" href=\"css/style.css\"/>" +
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/></head>"; "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/></head>" +
html += "<body><h2>" + title + "</h2>" + "<body><h2>" + title + "</h2>" +
body + body +
"</body></html>"; "</body></html>";
return html;
} }
/** /**

@ -273,21 +273,25 @@ public class StringUtil {
} }
return text.substring(cPos + 1); return text.substring(cPos + 1);
} }
// 移除字符串首尾空字符的高效方法(利用ASCII值判断,包括全角空格)
public static String FixTrim(String s) { public static String FixTrim(String s) {
if (s==null){ if (s == null || s.isEmpty()) {
return ""; return "";
} }
Pattern r = Pattern.compile("^[\\s]{1,9}(.*?)[\\s]{1,9}$"); int start = 0;
Matcher m = r.matcher(s); int len = s.length();
if (m.find()) { int end = len - 1;
s= m.group(1); while (start < end && (s.charAt(start) <= 0x20 || s.charAt(start) == ' ')) {
++start;
} }
if(s==null){ while (start < end && (s.charAt(end) <= 0x20 || s.charAt(end) == ' ')) {
return ""; --end;
}
if (end < len) {
++end;
} }
//移除GBK中文全角空格 return (start > 0 || end < len) ? s.substring(start, end) : s;
s = s.replace(" ", "");
return s;
} }
} }

@ -16,62 +16,85 @@
*/ */
package me.ag2s.epublib.util.commons.io; package me.ag2s.epublib.util.commons.io;
import android.os.Build;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator;
import java.util.List; import java.util.List;
import me.ag2s.epublib.util.IOUtil;
import static me.ag2s.epublib.util.IOUtil.EOF;
/** /**
* This class is used to wrap a stream that includes an encoded * This class is used to wrap a stream that includes an encoded {@link ByteOrderMark} as its first bytes.
* {@link ByteOrderMark} as its first bytes.
* *
* This class detects these bytes and, if required, can automatically skip them * This class detects these bytes and, if required, can automatically skip them and return the subsequent byte as the
* and return the subsequent byte as the first byte in the stream. * first byte in the stream.
* *
* The {@link ByteOrderMark} implementation has the following pre-defined BOMs: * The {@link ByteOrderMark} implementation has the following pre-defined BOMs:
* <ul> * <ul>
* <li>UTF-8 - {@link ByteOrderMark#UTF_8}</li> * <li>UTF-8 - {@link ByteOrderMark#UTF_8}</li>
* <li>UTF-16BE - {@link ByteOrderMark#UTF_16LE}</li> * <li>UTF-16BE - {@link ByteOrderMark#UTF_16LE}</li>
* <li>UTF-16LE - {@link ByteOrderMark#UTF_16BE}</li> * <li>UTF-16LE - {@link ByteOrderMark#UTF_16BE}</li>
* <li>UTF-32BE - {@link ByteOrderMark#UTF_32LE}</li>
* <li>UTF-32LE - {@link ByteOrderMark#UTF_32BE}</li>
* </ul> * </ul>
* *
* *
* <h3>Example 1 - Detect and exclude a UTF-8 BOM</h3> * <h2>Example 1 - Detect and exclude a UTF-8 BOM</h2>
*
* <pre> * <pre>
* BOMInputStream bomIn = new BOMInputStream(in); * BOMInputStream bomIn = new BOMInputStream(in);
* if (bomIn.hasBOM()) { * if (bomIn.hasBOM()) {
* // has a UTF-8 BOM * // has a UTF-8 BOM
* } * }
* </pre> * </pre>
* *
* <h3>Example 2 - Detect a UTF-8 BOM (but don't exclude it)</h3> * <h2>Example 2 - Detect a UTF-8 BOM (but don't exclude it)</h2>
*
* <pre> * <pre>
* boolean include = true; * boolean include = true;
* BOMInputStream bomIn = new BOMInputStream(in, include); * BOMInputStream bomIn = new BOMInputStream(in, include);
* if (bomIn.hasBOM()) { * if (bomIn.hasBOM()) {
* // has a UTF-8 BOM * // has a UTF-8 BOM
* } * }
* </pre> * </pre>
* *
* <h3>Example 3 - Detect Multiple BOMs</h3> * <h2>Example 3 - Detect Multiple BOMs</h2>
*
* <pre> * <pre>
* BOMInputStream bomIn = new BOMInputStream(in, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE); * BOMInputStream bomIn = new BOMInputStream(in,
* if (bomIn.hasBOM() == false) { * ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE,
* // No BOM found * ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE
* } else if (bomIn.hasBOM(ByteOrderMark.UTF_16LE)) { * );
* // has a UTF-16LE BOM * if (bomIn.hasBOM() == false) {
* } else if (bomIn.hasBOM(ByteOrderMark.UTF_16BE)) { * // No BOM found
* // has a UTF-16BE BOM * } else if (bomIn.hasBOM(ByteOrderMark.UTF_16LE)) {
* } * // has a UTF-16LE BOM
* } else if (bomIn.hasBOM(ByteOrderMark.UTF_16BE)) {
* // has a UTF-16BE BOM
* } else if (bomIn.hasBOM(ByteOrderMark.UTF_32LE)) {
* // has a UTF-32LE BOM
* } else if (bomIn.hasBOM(ByteOrderMark.UTF_32BE)) {
* // has a UTF-32BE BOM
* }
* </pre> * </pre>
* *
* @see ByteOrderMark * @see ByteOrderMark
* @see <a href="http://en.wikipedia.org/wiki/Byte_order_mark">Wikipedia - Byte Order Mark</a> * @see <a href="http://en.wikipedia.org/wiki/Byte_order_mark">Wikipedia - Byte Order Mark</a>
* @version $Revision: 1052095 $ $Date: 2010-12-22 23:03:20 +0000 (Wed, 22 Dec 2010) $ * @since 2.0
* @since Commons IO 2.0
*/ */
public class BOMInputStream extends ProxyInputStream { public class BOMInputStream extends ProxyInputStream {
private final boolean include; private final boolean include;
/**
* BOMs are sorted from longest to shortest.
*/
private final List<ByteOrderMark> boms; private final List<ByteOrderMark> boms;
private ByteOrderMark byteOrderMark; private ByteOrderMark byteOrderMark;
private int[] firstBytes; private int[] firstBytes;
@ -81,112 +104,138 @@ public class BOMInputStream extends ProxyInputStream {
private boolean markedAtStart; private boolean markedAtStart;
/** /**
* Constructs a new BOM InputStream that excludes * Constructs a new BOM InputStream that excludes a {@link ByteOrderMark#UTF_8} BOM.
* a {@link ByteOrderMark#UTF_8} BOM. *
* @param delegate the InputStream to delegate to * @param delegate
* the InputStream to delegate to
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public BOMInputStream(InputStream delegate) { public BOMInputStream(final InputStream delegate) {
this(delegate, false, ByteOrderMark.UTF_8); this(delegate, false, ByteOrderMark.UTF_8);
} }
/** /**
* Constructs a new BOM InputStream that detects a * Constructs a new BOM InputStream that detects a a {@link ByteOrderMark#UTF_8} and optionally includes it.
* a {@link ByteOrderMark#UTF_8} and optionally includes it. *
* @param delegate the InputStream to delegate to * @param delegate
* @param include true to include the UTF-8 BOM or * the InputStream to delegate to
* false to exclude it * @param include
* true to include the UTF-8 BOM or false to exclude it
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public BOMInputStream(InputStream delegate, boolean include) { public BOMInputStream(final InputStream delegate, final boolean include) {
this(delegate, include, ByteOrderMark.UTF_8); this(delegate, include, ByteOrderMark.UTF_8);
} }
/** /**
* Constructs a new BOM InputStream that excludes * Constructs a new BOM InputStream that excludes the specified BOMs.
* the specified BOMs. *
* @param delegate the InputStream to delegate to * @param delegate
* @param boms The BOMs to detect and exclude * the InputStream to delegate to
* @param boms
* The BOMs to detect and exclude
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public BOMInputStream(InputStream delegate, ByteOrderMark... boms) { public BOMInputStream(final InputStream delegate, final ByteOrderMark... boms) {
this(delegate, false, boms); this(delegate, false, boms);
} }
/** /**
* Constructs a new BOM InputStream that detects the * Compares ByteOrderMark objects in descending length order.
* specified BOMs and optionally includes them.
* @param delegate the InputStream to delegate to
* @param include true to include the specified BOMs or
* false to exclude them
* @param boms The BOMs to detect and optionally exclude
*/ */
@SuppressWarnings("unused") private static final Comparator<ByteOrderMark> ByteOrderMarkLengthComparator = (bom1, bom2) -> {
public BOMInputStream(InputStream delegate, boolean include, ByteOrderMark... boms) { final int len1 = bom1.length();
final int len2 = bom2.length();
return Integer.compare(len2, len1);
};
/**
* Constructs a new BOM InputStream that detects the specified BOMs and optionally includes them.
*
* @param delegate
* the InputStream to delegate to
* @param include
* true to include the specified BOMs or false to exclude them
* @param boms
* The BOMs to detect and optionally exclude
*/
public BOMInputStream(final InputStream delegate, final boolean include, final ByteOrderMark... boms) {
super(delegate); super(delegate);
if (boms == null || boms.length == 0) { if (IOUtil.length(boms) == 0) {
throw new IllegalArgumentException("No BOMs specified"); throw new IllegalArgumentException("No BOMs specified");
} }
this.include = include; this.include = include;
this.boms = Arrays.asList(boms); final List<ByteOrderMark> list = Arrays.asList(boms);
// Sort the BOMs to match the longest BOM first because some BOMs have the same starting two bytes.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
list.sort(ByteOrderMarkLengthComparator);
}
this.boms = list;
} }
/** /**
* Indicates whether the stream contains one of the specified BOMs. * Indicates whether the stream contains one of the specified BOMs.
* *
* @return true if the stream has one of the specified BOMs, otherwise false * @return true if the stream has one of the specified BOMs, otherwise false if it does not
* if it does not * @throws IOException
* @throws IOException if an error reading the first bytes of the stream occurs * if an error reading the first bytes of the stream occurs
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public boolean hasBOM() throws IOException { public boolean hasBOM() throws IOException {
return (getBOM() != null); return getBOM() != null;
} }
/** /**
* Indicates whether the stream contains the specified BOM. * Indicates whether the stream contains the specified BOM.
* *
* @param bom The BOM to check for * @param bom
* @return true if the stream has the specified BOM, otherwise false * The BOM to check for
* if it does not * @return true if the stream has the specified BOM, otherwise false if it does not
* @throws IllegalArgumentException if the BOM is not one the stream * @throws IllegalArgumentException
* is configured to detect * if the BOM is not one the stream is configured to detect
* @throws IOException if an error reading the first bytes of the stream occurs * @throws IOException
* if an error reading the first bytes of the stream occurs
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public boolean hasBOM(ByteOrderMark bom) throws IOException { public boolean hasBOM(final ByteOrderMark bom) throws IOException {
if (!boms.contains(bom)) { if (!boms.contains(bom)) {
throw new IllegalArgumentException("Stream not configure to detect " + bom); throw new IllegalArgumentException("Stream not configure to detect " + bom);
} }
return (byteOrderMark != null && getBOM().equals(bom)); getBOM();
return byteOrderMark != null && byteOrderMark.equals(bom);
} }
/** /**
* Return the BOM (Byte Order Mark). * Return the BOM (Byte Order Mark).
* *
* @return The BOM or null if none * @return The BOM or null if none
* @throws IOException if an error reading the first bytes of the stream occurs * @throws IOException
* if an error reading the first bytes of the stream occurs
*/ */
public ByteOrderMark getBOM() throws IOException { public ByteOrderMark getBOM() throws IOException {
if (firstBytes == null) { if (firstBytes == null) {
int max = 0; fbLength = 0;
for (ByteOrderMark bom : boms) { // BOMs are sorted from longest to shortest
max = Math.max(max, bom.length()); final int maxBomSize = boms.get(0).length();
} firstBytes = new int[maxBomSize];
firstBytes = new int[max]; // Read first maxBomSize bytes
for (int i = 0; i < firstBytes.length; i++) { for (int i = 0; i < firstBytes.length; i++) {
firstBytes[i] = in.read(); firstBytes[i] = in.read();
fbLength++; fbLength++;
if (firstBytes[i] < 0) { if (firstBytes[i] < 0) {
break; break;
} }
}
byteOrderMark = find(); // match BOM in firstBytes
if (byteOrderMark != null) { byteOrderMark = find();
if (!include) { if (byteOrderMark != null) {
if (!include) {
if (byteOrderMark.length() < firstBytes.length) {
fbIndex = byteOrderMark.length();
} else {
fbLength = 0; fbLength = 0;
} }
break;
} }
} }
} }
@ -197,25 +246,27 @@ public class BOMInputStream extends ProxyInputStream {
* Return the BOM charset Name - {@link ByteOrderMark#getCharsetName()}. * Return the BOM charset Name - {@link ByteOrderMark#getCharsetName()}.
* *
* @return The BOM charset Name or null if no BOM found * @return The BOM charset Name or null if no BOM found
* @throws IOException if an error reading the first bytes of the stream occurs * @throws IOException
* if an error reading the first bytes of the stream occurs
* *
*/ */
public String getBOMCharsetName() throws IOException { public String getBOMCharsetName() throws IOException {
getBOM(); getBOM();
return (byteOrderMark == null ? null : byteOrderMark.getCharsetName()); return byteOrderMark == null ? null : byteOrderMark.getCharsetName();
} }
/** /**
* This method reads and either preserves or skips the first bytes in the * This method reads and either preserves or skips the first bytes in the stream. It behaves like the single-byte
* stream. It behaves like the single-byte <code>read()</code> method, * <code>read()</code> method, either returning a valid byte or -1 to indicate that the initial bytes have been
* either returning a valid byte or -1 to indicate that the initial bytes * processed already.
* have been processed already. *
* @return the byte read (excluding BOM) or -1 if the end of stream * @return the byte read (excluding BOM) or -1 if the end of stream
* @throws IOException if an I/O error occurs * @throws IOException
* if an I/O error occurs
*/ */
private int readFirstBytes() throws IOException { private int readFirstBytes() throws IOException {
getBOM(); getBOM();
return (fbIndex < fbLength) ? firstBytes[fbIndex++] : -1; return fbIndex < fbLength ? firstBytes[fbIndex++] : EOF;
} }
/** /**
@ -224,7 +275,7 @@ public class BOMInputStream extends ProxyInputStream {
* @return The matched BOM or null if none matched * @return The matched BOM or null if none matched
*/ */
private ByteOrderMark find() { private ByteOrderMark find() {
for (ByteOrderMark bom : boms) { for (final ByteOrderMark bom : boms) {
if (matches(bom)) { if (matches(bom)) {
return bom; return bom;
} }
@ -235,13 +286,15 @@ public class BOMInputStream extends ProxyInputStream {
/** /**
* Check if the bytes match a BOM. * Check if the bytes match a BOM.
* *
* @param bom The BOM * @param bom
* The BOM
* @return true if the bytes match the bom, otherwise false * @return true if the bytes match the bom, otherwise false
*/ */
private boolean matches(ByteOrderMark bom) { private boolean matches(final ByteOrderMark bom) {
if (bom.length() != fbLength) { // if (bom.length() != fbLength) {
return false; // return false;
} // }
// firstBytes may be bigger than the BOM bytes
for (int i = 0; i < bom.length(); i++) { for (int i = 0; i < bom.length(); i++) {
if (bom.get(i) != firstBytes[i]) { if (bom.get(i) != firstBytes[i]) {
return false; return false;
@ -250,36 +303,41 @@ public class BOMInputStream extends ProxyInputStream {
return true; return true;
} }
//---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Implementation of InputStream // Implementation of InputStream
//---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/** /**
* Invokes the delegate's <code>read()</code> method, detecting and * Invokes the delegate's <code>read()</code> method, detecting and optionally skipping BOM.
* optionally skipping BOM. *
* @return the byte read (excluding BOM) or -1 if the end of stream * @return the byte read (excluding BOM) or -1 if the end of stream
* @throws IOException if an I/O error occurs * @throws IOException
* if an I/O error occurs
*/ */
@Override @Override
public int read() throws IOException { public int read() throws IOException {
int b = readFirstBytes(); final int b = readFirstBytes();
return (b >= 0) ? b : in.read(); return b >= 0 ? b : in.read();
} }
/** /**
* Invokes the delegate's <code>read(byte[], int, int)</code> method, detecting * Invokes the delegate's <code>read(byte[], int, int)</code> method, detecting and optionally skipping BOM.
* and optionally skipping BOM. *
* @param buf the buffer to read the bytes into * @param buf
* @param off The start offset * the buffer to read the bytes into
* @param len The number of bytes to read (excluding BOM) * @param off
* The start offset
* @param len
* The number of bytes to read (excluding BOM)
* @return the number of bytes read or -1 if the end of stream * @return the number of bytes read or -1 if the end of stream
* @throws IOException if an I/O error occurs * @throws IOException
* if an I/O error occurs
*/ */
@Override @Override
public int read(byte[] buf, int off, int len) throws IOException { public int read(final byte[] buf, int off, int len) throws IOException {
int firstCount = 0; int firstCount = 0;
int b = 0; int b = 0;
while ((len > 0) && (b >= 0)) { while (len > 0 && b >= 0) {
b = readFirstBytes(); b = readFirstBytes();
if (b >= 0) { if (b >= 0) {
buf[off++] = (byte) (b & 0xFF); buf[off++] = (byte) (b & 0xFF);
@ -287,37 +345,42 @@ public class BOMInputStream extends ProxyInputStream {
firstCount++; firstCount++;
} }
} }
int secondCount = in.read(buf, off, len); final int secondCount = in.read(buf, off, len);
return (secondCount < 0) ? (firstCount > 0 ? firstCount : -1) : firstCount + secondCount; return secondCount < 0 ? firstCount > 0 ? firstCount : EOF : firstCount + secondCount;
} }
/** /**
* Invokes the delegate's <code>read(byte[])</code> method, detecting and * Invokes the delegate's <code>read(byte[])</code> method, detecting and optionally skipping BOM.
* optionally skipping BOM. *
* @param buf the buffer to read the bytes into * @param buf
* @return the number of bytes read (excluding BOM) * the buffer to read the bytes into
* or -1 if the end of stream * @return the number of bytes read (excluding BOM) or -1 if the end of stream
* @throws IOException if an I/O error occurs * @throws IOException
* if an I/O error occurs
*/ */
@Override @Override
public int read(byte[] buf) throws IOException { public int read(final byte[] buf) throws IOException {
return read(buf, 0, buf.length); return read(buf, 0, buf.length);
} }
/** /**
* Invokes the delegate's <code>mark(int)</code> method. * Invokes the delegate's <code>mark(int)</code> method.
* @param readlimit read ahead limit *
* @param readlimit
* read ahead limit
*/ */
@Override @Override
public synchronized void mark(int readlimit) { public synchronized void mark(final int readlimit) {
markFbIndex = fbIndex; markFbIndex = fbIndex;
markedAtStart = (firstBytes == null); markedAtStart = firstBytes == null;
in.mark(readlimit); in.mark(readlimit);
} }
/** /**
* Invokes the delegate's <code>reset()</code> method. * Invokes the delegate's <code>reset()</code> method.
* @throws IOException if an I/O error occurs *
* @throws IOException
* if an I/O error occurs
*/ */
@Override @Override
public synchronized void reset() throws IOException { public synchronized void reset() throws IOException {
@ -330,17 +393,20 @@ public class BOMInputStream extends ProxyInputStream {
} }
/** /**
* Invokes the delegate's <code>skip(long)</code> method, detecting * Invokes the delegate's <code>skip(long)</code> method, detecting and optionally skipping BOM.
* and optionallyskipping BOM. *
* @param n the number of bytes to skip * @param n
* the number of bytes to skip
* @return the number of bytes to skipped or -1 if the end of stream * @return the number of bytes to skipped or -1 if the end of stream
* @throws IOException if an I/O error occurs * @throws IOException
* if an I/O error occurs
*/ */
@Override @Override
public long skip(long n) throws IOException { public long skip(final long n) throws IOException {
while ((n > 0) && (readFirstBytes() >= 0)) { int skipped = 0;
n--; while ((n > skipped) && (readFirstBytes() >= 0)) {
skipped++;
} }
return in.skip(n); return in.skip(n - skipped) + skipped;
} }
} }

@ -18,15 +18,16 @@ package me.ag2s.epublib.util.commons.io;
*/ */
import java.io.Serializable; import java.io.Serializable;
import java.util.Locale;
/** /**
* Byte Order Mark (BOM) representation - * Byte Order Mark (BOM) representation - see {@link BOMInputStream}.
* see {@link BOMInputStream}.
* *
* @see BOMInputStream * @see BOMInputStream
* @see <a href="http://en.wikipedia.org/wiki/Byte_order_mark">Wikipedia - Byte Order Mark</a> * @see <a href="http://en.wikipedia.org/wiki/Byte_order_mark">Wikipedia: Byte Order Mark</a>
* @version $Id: ByteOrderMark.java 1005099 2010-10-06 16:13:01Z niallp $ * @see <a href="http://www.w3.org/TR/2006/REC-xml-20060816/#sec-guessing">W3C: Autodetection of Character Encodings
* @since Commons IO 2.0 * (Non-Normative)</a>
* @since 2.0
*/ */
public class ByteOrderMark implements Serializable { public class ByteOrderMark implements Serializable {
@ -34,11 +35,32 @@ public class ByteOrderMark implements Serializable {
/** UTF-8 BOM */ /** UTF-8 BOM */
public static final ByteOrderMark UTF_8 = new ByteOrderMark("UTF-8", 0xEF, 0xBB, 0xBF); public static final ByteOrderMark UTF_8 = new ByteOrderMark("UTF-8", 0xEF, 0xBB, 0xBF);
/** UTF-16BE BOM (Big Endian) */
/** UTF-16BE BOM (Big-Endian) */
public static final ByteOrderMark UTF_16BE = new ByteOrderMark("UTF-16BE", 0xFE, 0xFF); public static final ByteOrderMark UTF_16BE = new ByteOrderMark("UTF-16BE", 0xFE, 0xFF);
/** UTF-16LE BOM (Little Endian) */
/** UTF-16LE BOM (Little-Endian) */
public static final ByteOrderMark UTF_16LE = new ByteOrderMark("UTF-16LE", 0xFF, 0xFE); public static final ByteOrderMark UTF_16LE = new ByteOrderMark("UTF-16LE", 0xFF, 0xFE);
/**
* UTF-32BE BOM (Big-Endian)
* @since 2.2
*/
public static final ByteOrderMark UTF_32BE = new ByteOrderMark("UTF-32BE", 0x00, 0x00, 0xFE, 0xFF);
/**
* UTF-32LE BOM (Little-Endian)
* @since 2.2
*/
public static final ByteOrderMark UTF_32LE = new ByteOrderMark("UTF-32LE", 0xFF, 0xFE, 0x00, 0x00);
/**
* Unicode BOM character; external form depends on the encoding.
* @see <a href="http://unicode.org/faq/utf_bom.html#BOM">Byte Order Mark (BOM) FAQ</a>
* @since 2.5
*/
public static final char UTF_BOM = '\uFEFF';
private final String charsetName; private final String charsetName;
private final int[] bytes; private final int[] bytes;
@ -52,8 +74,8 @@ public class ByteOrderMark implements Serializable {
* @throws IllegalArgumentException if the bytes are null or zero * @throws IllegalArgumentException if the bytes are null or zero
* length * length
*/ */
public ByteOrderMark(String charsetName, int... bytes) { public ByteOrderMark(final String charsetName, final int... bytes) {
if (charsetName == null || charsetName.length() == 0) { if (charsetName == null || charsetName.isEmpty()) {
throw new IllegalArgumentException("No charsetName specified"); throw new IllegalArgumentException("No charsetName specified");
} }
if (bytes == null || bytes.length == 0) { if (bytes == null || bytes.length == 0) {
@ -88,7 +110,7 @@ public class ByteOrderMark implements Serializable {
* @param pos The position * @param pos The position
* @return The specified byte * @return The specified byte
*/ */
public int get(int pos) { public int get(final int pos) {
return bytes[pos]; return bytes[pos];
} }
@ -98,7 +120,7 @@ public class ByteOrderMark implements Serializable {
* @return a copy of the BOM's bytes * @return a copy of the BOM's bytes
*/ */
public byte[] getBytes() { public byte[] getBytes() {
byte[] copy = new byte[bytes.length]; final byte[] copy = new byte[bytes.length];
for (int i = 0; i < bytes.length; i++) { for (int i = 0; i < bytes.length; i++) {
copy[i] = (byte)bytes[i]; copy[i] = (byte)bytes[i];
} }
@ -113,11 +135,11 @@ public class ByteOrderMark implements Serializable {
* false * false
*/ */
@Override @Override
public boolean equals(Object obj) { public boolean equals(final Object obj) {
if (!(obj instanceof ByteOrderMark)) { if (!(obj instanceof ByteOrderMark)) {
return false; return false;
} }
ByteOrderMark bom = (ByteOrderMark)obj; final ByteOrderMark bom = (ByteOrderMark)obj;
if (bytes.length != bom.length()) { if (bytes.length != bom.length()) {
return false; return false;
} }
@ -133,12 +155,12 @@ public class ByteOrderMark implements Serializable {
* Return the hashcode for this BOM. * Return the hashcode for this BOM.
* *
* @return the hashcode for this BOM. * @return the hashcode for this BOM.
* @see Object#hashCode() * @see java.lang.Object#hashCode()
*/ */
@Override @Override
public int hashCode() { public int hashCode() {
int hashCode = getClass().hashCode(); int hashCode = getClass().hashCode();
for (int b : bytes) { for (final int b : bytes) {
hashCode += b; hashCode += b;
} }
return hashCode; return hashCode;
@ -152,7 +174,7 @@ public class ByteOrderMark implements Serializable {
@Override @Override
@SuppressWarnings("NullableProblems") @SuppressWarnings("NullableProblems")
public String toString() { public String toString() {
StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();
builder.append(getClass().getSimpleName()); builder.append(getClass().getSimpleName());
builder.append('['); builder.append('[');
builder.append(charsetName); builder.append(charsetName);
@ -162,7 +184,7 @@ public class ByteOrderMark implements Serializable {
builder.append(","); builder.append(",");
} }
builder.append("0x"); builder.append("0x");
builder.append(Integer.toHexString(0xFF & bytes[i]).toUpperCase()); builder.append(Integer.toHexString(0xFF & bytes[i]).toUpperCase(Locale.ROOT));
} }
builder.append(']'); builder.append(']');
return builder.toString(); return builder.toString();

@ -30,42 +30,48 @@ import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import me.ag2s.epublib.util.IOUtil;
/** /**
* Character stream that handles all the necessary Voodo to figure out the * Character stream that handles all the necessary Voodoo to figure out the
* charset encoding of the XML document within the stream. * charset encoding of the XML document within the stream.
* <p> * <p>
* IMPORTANT: This class is not related in any way to the org.xml.sax.XMLReader. * IMPORTANT: This class is not related in any way to the org.xml.sax.XMLReader.
* This one IS a character stream. * This one IS a character stream.
* </p>
* <p> * <p>
* All this has to be done without consuming characters from the stream, if not * All this has to be done without consuming characters from the stream, if not
* the XML parser will not recognized the document as a valid XML. This is not * the XML parser will not recognized the document as a valid XML. This is not
* 100% true, but it's close enough (UTF-8 BOM is not handled by all parsers * 100% true, but it's close enough (UTF-8 BOM is not handled by all parsers
* right now, XmlStreamReader handles it and things work in all parsers). * right now, XmlStreamReader handles it and things work in all parsers).
* </p>
* <p> * <p>
* The XmlStreamReader class handles the charset encoding of XML documents in * The XmlStreamReader class handles the charset encoding of XML documents in
* Files, raw streams and HTTP streams by offering a wide set of constructors. * Files, raw streams and HTTP streams by offering a wide set of constructors.
* </p>
* <p> * <p>
* By default the charset encoding detection is lenient, the constructor with * By default the charset encoding detection is lenient, the constructor with
* the lenient flag can be used for an script (following HTTP MIME and XML * the lenient flag can be used for a script (following HTTP MIME and XML
* specifications). All this is nicely explained by Mark Pilgrim in his blog, <a * specifications). All this is nicely explained by Mark Pilgrim in his blog, <a
* href="http://diveintomark.org/archives/2004/02/13/xml-media-types"> * href="http://diveintomark.org/archives/2004/02/13/xml-media-types">
* Determining the character encoding of a feed</a>. * Determining the character encoding of a feed</a>.
* </p>
* <p> * <p>
* Originally developed for <a href="http://rome.dev.java.net">ROME</a> under * Originally developed for <a href="http://rome.dev.java.net">ROME</a> under
* Apache License 2.0. * Apache License 2.0.
* </p>
* *
* @author Alejandro Abdelnur * //@seerr XmlStreamWriter
* @version $Id: XmlStreamReader.java 1052161 2010-12-23 03:12:09Z niallp $ * @since 2.0
* @see "org.apache.commons.io.output.XmlStreamWriter"
* @since Commons IO 2.0
*/ */
public class XmlStreamReader extends Reader { public class XmlStreamReader extends Reader {
private static final int BUFFER_SIZE = 4096; private static final int BUFFER_SIZE = IOUtil.DEFAULT_BUFFER_SIZE;
private static final String UTF_8 = "UTF-8"; private static final String UTF_8 = "UTF-8";
@ -75,23 +81,36 @@ public class XmlStreamReader extends Reader {
private static final String UTF_16LE = "UTF-16LE"; private static final String UTF_16LE = "UTF-16LE";
private static final String UTF_32BE = "UTF-32BE";
private static final String UTF_32LE = "UTF-32LE";
private static final String UTF_16 = "UTF-16"; private static final String UTF_16 = "UTF-16";
private static final String UTF_32 = "UTF-32";
private static final String EBCDIC = "CP1047"; private static final String EBCDIC = "CP1047";
private static final ByteOrderMark[] BOMS = new ByteOrderMark[] { private static final ByteOrderMark[] BOMS = new ByteOrderMark[] {
ByteOrderMark.UTF_8, ByteOrderMark.UTF_8,
ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_16BE,
ByteOrderMark.UTF_16LE ByteOrderMark.UTF_16LE,
ByteOrderMark.UTF_32BE,
ByteOrderMark.UTF_32LE
}; };
// UTF_16LE and UTF_32LE have the same two starting BOM bytes.
private static final ByteOrderMark[] XML_GUESS_BYTES = new ByteOrderMark[] { private static final ByteOrderMark[] XML_GUESS_BYTES = new ByteOrderMark[] {
new ByteOrderMark(UTF_8, 0x3C, 0x3F, 0x78, 0x6D), new ByteOrderMark(UTF_8, 0x3C, 0x3F, 0x78, 0x6D),
new ByteOrderMark(UTF_16BE, 0x00, 0x3C, 0x00, 0x3F), new ByteOrderMark(UTF_16BE, 0x00, 0x3C, 0x00, 0x3F),
new ByteOrderMark(UTF_16LE, 0x3C, 0x00, 0x3F, 0x00), new ByteOrderMark(UTF_16LE, 0x3C, 0x00, 0x3F, 0x00),
new ByteOrderMark(EBCDIC, 0x4C, 0x6F, 0xA7, 0x94) new ByteOrderMark(UTF_32BE, 0x00, 0x00, 0x00, 0x3C,
0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x6D),
new ByteOrderMark(UTF_32LE, 0x3C, 0x00, 0x00, 0x00,
0x3F, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x6D, 0x00, 0x00, 0x00),
new ByteOrderMark(EBCDIC, 0x4C, 0x6F, 0xA7, 0x94)
}; };
private final Reader reader; private final Reader reader;
private final String encoding; private final String encoding;
@ -106,7 +125,6 @@ public class XmlStreamReader extends Reader {
* *
* @return the default encoding to use. * @return the default encoding to use.
*/ */
@SuppressWarnings("unused")
public String getDefaultEncoding() { public String getDefaultEncoding() {
return defaultEncoding; return defaultEncoding;
} }
@ -124,8 +142,8 @@ public class XmlStreamReader extends Reader {
* @throws IOException thrown if there is a problem reading the file. * @throws IOException thrown if there is a problem reading the file.
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public XmlStreamReader(File file) throws IOException { public XmlStreamReader(final File file) throws IOException {
this(new FileInputStream(file)); this(new FileInputStream(Objects.requireNonNull(file)));
} }
/** /**
@ -136,11 +154,11 @@ public class XmlStreamReader extends Reader {
* It does a lenient charset encoding detection, check the constructor with * It does a lenient charset encoding detection, check the constructor with
* the lenient parameter for details. * the lenient parameter for details.
* *
* @param is InputStream to create a Reader from. * @param inputStream InputStream to create a Reader from.
* @throws IOException thrown if there is a problem reading the stream. * @throws IOException thrown if there is a problem reading the stream.
*/ */
public XmlStreamReader(InputStream is) throws IOException { public XmlStreamReader(final InputStream inputStream) throws IOException {
this(is, true); this(inputStream, true);
} }
/** /**
@ -163,15 +181,15 @@ public class XmlStreamReader extends Reader {
* If lenient detection is indicated an XmlStreamReaderException is never * If lenient detection is indicated an XmlStreamReaderException is never
* thrown. * thrown.
* *
* @param is InputStream to create a Reader from. * @param inputStream InputStream to create a Reader from.
* @param lenient indicates if the charset encoding detection should be * @param lenient indicates if the charset encoding detection should be
* relaxed. * relaxed.
* @throws IOException thrown if there is a problem reading the stream. * @throws IOException thrown if there is a problem reading the stream.
* @throws XmlStreamReaderException thrown if the charset encoding could not * @throws XmlStreamReaderException thrown if the charset encoding could not
* be determined according to the specs. * be determined according to the specs.
*/ */
public XmlStreamReader(InputStream is, boolean lenient) throws IOException { public XmlStreamReader(final InputStream inputStream, final boolean lenient) throws IOException {
this(is, lenient, null); this(inputStream, lenient, null);
} }
/** /**
@ -194,7 +212,7 @@ public class XmlStreamReader extends Reader {
* If lenient detection is indicated an XmlStreamReaderException is never * If lenient detection is indicated an XmlStreamReaderException is never
* thrown. * thrown.
* *
* @param is InputStream to create a Reader from. * @param inputStream InputStream to create a Reader from.
* @param lenient indicates if the charset encoding detection should be * @param lenient indicates if the charset encoding detection should be
* relaxed. * relaxed.
* @param defaultEncoding The default encoding * @param defaultEncoding The default encoding
@ -202,10 +220,12 @@ public class XmlStreamReader extends Reader {
* @throws XmlStreamReaderException thrown if the charset encoding could not * @throws XmlStreamReaderException thrown if the charset encoding could not
* be determined according to the specs. * be determined according to the specs.
*/ */
public XmlStreamReader(InputStream is, boolean lenient, String defaultEncoding) throws IOException { public XmlStreamReader(final InputStream inputStream, final boolean lenient, final String defaultEncoding)
throws IOException {
Objects.requireNonNull(inputStream, "inputStream");
this.defaultEncoding = defaultEncoding; this.defaultEncoding = defaultEncoding;
BOMInputStream bom = new BOMInputStream(new BufferedInputStream(is, BUFFER_SIZE), false, BOMS); final BOMInputStream bom = new BOMInputStream(new BufferedInputStream(inputStream, BUFFER_SIZE), false, BOMS);
BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES); final BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);
this.encoding = doRawStream(bom, pis, lenient); this.encoding = doRawStream(bom, pis, lenient);
this.reader = new InputStreamReader(pis, encoding); this.reader = new InputStreamReader(pis, encoding);
} }
@ -228,8 +248,8 @@ public class XmlStreamReader extends Reader {
* the URL. * the URL.
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public XmlStreamReader(URL url) throws IOException { public XmlStreamReader(final URL url) throws IOException {
this(url.openConnection(), null); this(Objects.requireNonNull(url, "url").openConnection(), null);
} }
/** /**
@ -251,24 +271,24 @@ public class XmlStreamReader extends Reader {
* @throws IOException thrown if there is a problem reading the stream of * @throws IOException thrown if there is a problem reading the stream of
* the URLConnection. * the URLConnection.
*/ */
public XmlStreamReader(URLConnection conn, String defaultEncoding) throws IOException { public XmlStreamReader(final URLConnection conn, final String defaultEncoding) throws IOException {
Objects.requireNonNull(conn, "conm");
this.defaultEncoding = defaultEncoding; this.defaultEncoding = defaultEncoding;
@SuppressWarnings("unused") final boolean lenient = true;
boolean lenient = true; final String contentType = conn.getContentType();
String contentType = conn.getContentType(); final InputStream inputStream = conn.getInputStream();
InputStream is = conn.getInputStream(); final BOMInputStream bom = new BOMInputStream(new BufferedInputStream(inputStream, BUFFER_SIZE), false, BOMS);
BOMInputStream bom = new BOMInputStream(new BufferedInputStream(is, BUFFER_SIZE), false, BOMS); final BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);
BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);
if (conn instanceof HttpURLConnection || contentType != null) { if (conn instanceof HttpURLConnection || contentType != null) {
this.encoding = doHttpStream(bom, pis, contentType, true); this.encoding = processHttpStream(bom, pis, contentType, lenient);
} else { } else {
this.encoding = doRawStream(bom, pis, true); this.encoding = doRawStream(bom, pis, lenient);
} }
this.reader = new InputStreamReader(pis, encoding); this.reader = new InputStreamReader(pis, encoding);
} }
/** /**
* Creates a Reader using an InputStream an the associated content-type * Creates a Reader using an InputStream and the associated content-type
* header. * header.
* <p> * <p>
* First it checks if the stream has BOM. If there is not BOM checks the * First it checks if the stream has BOM. If there is not BOM checks the
@ -279,18 +299,18 @@ public class XmlStreamReader extends Reader {
* It does a lenient charset encoding detection, check the constructor with * It does a lenient charset encoding detection, check the constructor with
* the lenient parameter for details. * the lenient parameter for details.
* *
* @param is InputStream to create the reader from. * @param inputStream InputStream to create the reader from.
* @param httpContentType content-type header to use for the resolution of * @param httpContentType content-type header to use for the resolution of
* the charset encoding. * the charset encoding.
* @throws IOException thrown if there is a problem reading the file. * @throws IOException thrown if there is a problem reading the file.
*/ */
public XmlStreamReader(InputStream is, String httpContentType) public XmlStreamReader(final InputStream inputStream, final String httpContentType)
throws IOException { throws IOException {
this(is, httpContentType, true); this(inputStream, httpContentType, true);
} }
/** /**
* Creates a Reader using an InputStream an the associated content-type * Creates a Reader using an InputStream and the associated content-type
* header. This constructor is lenient regarding the encoding detection. * header. This constructor is lenient regarding the encoding detection.
* <p> * <p>
* First it checks if the stream has BOM. If there is not BOM checks the * First it checks if the stream has BOM. If there is not BOM checks the
@ -313,7 +333,7 @@ public class XmlStreamReader extends Reader {
* If lenient detection is indicated an XmlStreamReaderException is never * If lenient detection is indicated an XmlStreamReaderException is never
* thrown. * thrown.
* *
* @param is InputStream to create the reader from. * @param inputStream InputStream to create the reader from.
* @param httpContentType content-type header to use for the resolution of * @param httpContentType content-type header to use for the resolution of
* the charset encoding. * the charset encoding.
* @param lenient indicates if the charset encoding detection should be * @param lenient indicates if the charset encoding detection should be
@ -323,17 +343,18 @@ public class XmlStreamReader extends Reader {
* @throws XmlStreamReaderException thrown if the charset encoding could not * @throws XmlStreamReaderException thrown if the charset encoding could not
* be determined according to the specs. * be determined according to the specs.
*/ */
public XmlStreamReader(InputStream is, String httpContentType, public XmlStreamReader(final InputStream inputStream, final String httpContentType,
boolean lenient, String defaultEncoding) throws IOException { final boolean lenient, final String defaultEncoding) throws IOException {
Objects.requireNonNull(inputStream, "inputStream");
this.defaultEncoding = defaultEncoding; this.defaultEncoding = defaultEncoding;
BOMInputStream bom = new BOMInputStream(new BufferedInputStream(is, BUFFER_SIZE), false, BOMS); final BOMInputStream bom = new BOMInputStream(new BufferedInputStream(inputStream, BUFFER_SIZE), false, BOMS);
BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES); final BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);
this.encoding = doHttpStream(bom, pis, httpContentType, lenient); this.encoding = processHttpStream(bom, pis, httpContentType, lenient);
this.reader = new InputStreamReader(pis, encoding); this.reader = new InputStreamReader(pis, encoding);
} }
/** /**
* Creates a Reader using an InputStream an the associated content-type * Creates a Reader using an InputStream and the associated content-type
* header. This constructor is lenient regarding the encoding detection. * header. This constructor is lenient regarding the encoding detection.
* <p> * <p>
* First it checks if the stream has BOM. If there is not BOM checks the * First it checks if the stream has BOM. If there is not BOM checks the
@ -356,7 +377,7 @@ public class XmlStreamReader extends Reader {
* If lenient detection is indicated an XmlStreamReaderException is never * If lenient detection is indicated an XmlStreamReaderException is never
* thrown. * thrown.
* *
* @param is InputStream to create the reader from. * @param inputStream InputStream to create the reader from.
* @param httpContentType content-type header to use for the resolution of * @param httpContentType content-type header to use for the resolution of
* the charset encoding. * the charset encoding.
* @param lenient indicates if the charset encoding detection should be * @param lenient indicates if the charset encoding detection should be
@ -365,9 +386,9 @@ public class XmlStreamReader extends Reader {
* @throws XmlStreamReaderException thrown if the charset encoding could not * @throws XmlStreamReaderException thrown if the charset encoding could not
* be determined according to the specs. * be determined according to the specs.
*/ */
public XmlStreamReader(InputStream is, String httpContentType, public XmlStreamReader(final InputStream inputStream, final String httpContentType,
boolean lenient) throws IOException { final boolean lenient) throws IOException {
this(is, httpContentType, lenient, null); this(inputStream, httpContentType, lenient, null);
} }
/** /**
@ -388,7 +409,7 @@ public class XmlStreamReader extends Reader {
* @throws IOException if an I/O error occurs * @throws IOException if an I/O error occurs
*/ */
@Override @Override
public int read(char[] buf, int offset, int len) throws IOException { public int read(final char[] buf, final int offset, final int len) throws IOException {
return reader.read(buf, offset, len); return reader.read(buf, offset, len);
} }
@ -412,19 +433,18 @@ public class XmlStreamReader extends Reader {
* @return the encoding to be used * @return the encoding to be used
* @throws IOException thrown if there is a problem reading the stream. * @throws IOException thrown if there is a problem reading the stream.
*/ */
private String doRawStream(BOMInputStream bom, BOMInputStream pis, boolean lenient) private String doRawStream(final BOMInputStream bom, final BOMInputStream pis, final boolean lenient)
throws IOException { throws IOException {
String bomEnc = bom.getBOMCharsetName(); final String bomEnc = bom.getBOMCharsetName();
String xmlGuessEnc = pis.getBOMCharsetName(); final String xmlGuessEnc = pis.getBOMCharsetName();
String xmlEnc = getXmlProlog(pis, xmlGuessEnc); final String xmlEnc = getXmlProlog(pis, xmlGuessEnc);
try { try {
return calculateRawEncoding(bomEnc, xmlGuessEnc, xmlEnc); return calculateRawEncoding(bomEnc, xmlGuessEnc, xmlEnc);
} catch (XmlStreamReaderException ex) { } catch (final XmlStreamReaderException ex) {
if (lenient) { if (lenient) {
return doLenientDetection(null, ex); return doLenientDetection(null, ex);
} else {
throw ex;
} }
throw ex;
} }
} }
@ -439,20 +459,18 @@ public class XmlStreamReader extends Reader {
* @return the encoding to be used * @return the encoding to be used
* @throws IOException thrown if there is a problem reading the stream. * @throws IOException thrown if there is a problem reading the stream.
*/ */
private String doHttpStream(BOMInputStream bom, BOMInputStream pis, String httpContentType, private String processHttpStream(final BOMInputStream bom, final BOMInputStream pis, final String httpContentType,
boolean lenient) throws IOException { final boolean lenient) throws IOException {
String bomEnc = bom.getBOMCharsetName(); final String bomEnc = bom.getBOMCharsetName();
String xmlGuessEnc = pis.getBOMCharsetName(); final String xmlGuessEnc = pis.getBOMCharsetName();
String xmlEnc = getXmlProlog(pis, xmlGuessEnc); final String xmlEnc = getXmlProlog(pis, xmlGuessEnc);
try { try {
return calculateHttpEncoding(httpContentType, bomEnc, return calculateHttpEncoding(httpContentType, bomEnc, xmlGuessEnc, xmlEnc, lenient);
xmlGuessEnc, xmlEnc, lenient); } catch (final XmlStreamReaderException ex) {
} catch (XmlStreamReaderException ex) {
if (lenient) { if (lenient) {
return doLenientDetection(httpContentType, ex); return doLenientDetection(httpContentType, ex);
} else {
throw ex;
} }
throw ex;
} }
} }
@ -466,14 +484,14 @@ public class XmlStreamReader extends Reader {
* @throws IOException thrown if there is a problem reading the stream. * @throws IOException thrown if there is a problem reading the stream.
*/ */
private String doLenientDetection(String httpContentType, private String doLenientDetection(String httpContentType,
XmlStreamReaderException ex) throws IOException { XmlStreamReaderException ex) throws IOException {
if (httpContentType != null && httpContentType.startsWith("text/html")) { if (httpContentType != null && httpContentType.startsWith("text/html")) {
httpContentType = httpContentType.substring("text/html".length()); httpContentType = httpContentType.substring("text/html".length());
httpContentType = "text/xml" + httpContentType; httpContentType = "text/xml" + httpContentType;
try { try {
return calculateHttpEncoding(httpContentType, ex.getBomEncoding(), return calculateHttpEncoding(httpContentType, ex.getBomEncoding(),
ex.getXmlGuessEncoding(), ex.getXmlEncoding(), true); ex.getXmlGuessEncoding(), ex.getXmlEncoding(), true);
} catch (XmlStreamReaderException ex2) { } catch (final XmlStreamReaderException ex2) {
ex = ex2; ex = ex2;
} }
} }
@ -482,7 +500,7 @@ public class XmlStreamReader extends Reader {
encoding = ex.getContentTypeEncoding(); encoding = ex.getContentTypeEncoding();
} }
if (encoding == null) { if (encoding == null) {
encoding = (defaultEncoding == null) ? UTF_8 : defaultEncoding; encoding = defaultEncoding == null ? UTF_8 : defaultEncoding;
} }
return encoding; return encoding;
} }
@ -496,16 +514,16 @@ public class XmlStreamReader extends Reader {
* @return the raw encoding * @return the raw encoding
* @throws IOException thrown if there is a problem reading the stream. * @throws IOException thrown if there is a problem reading the stream.
*/ */
String calculateRawEncoding(String bomEnc, String xmlGuessEnc, String calculateRawEncoding(final String bomEnc, final String xmlGuessEnc,
String xmlEnc) throws IOException { final String xmlEnc) throws IOException {
// BOM is Null // BOM is Null
if (bomEnc == null) { if (bomEnc == null) {
if (xmlGuessEnc == null || xmlEnc == null) { if (xmlGuessEnc == null || xmlEnc == null) {
return (defaultEncoding == null ? UTF_8 : defaultEncoding); return defaultEncoding == null ? UTF_8 : defaultEncoding;
} }
if (xmlEnc.equals(UTF_16) && if (xmlEnc.equals(UTF_16) &&
(xmlGuessEnc.equals(UTF_16BE) || xmlGuessEnc.equals(UTF_16LE))) { (xmlGuessEnc.equals(UTF_16BE) || xmlGuessEnc.equals(UTF_16LE))) {
return xmlGuessEnc; return xmlGuessEnc;
} }
return xmlEnc; return xmlEnc;
@ -514,11 +532,11 @@ public class XmlStreamReader extends Reader {
// BOM is UTF-8 // BOM is UTF-8
if (bomEnc.equals(UTF_8)) { if (bomEnc.equals(UTF_8)) {
if (xmlGuessEnc != null && !xmlGuessEnc.equals(UTF_8)) { if (xmlGuessEnc != null && !xmlGuessEnc.equals(UTF_8)) {
String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
} }
if (xmlEnc != null && !xmlEnc.equals(UTF_8)) { if (xmlEnc != null && !xmlEnc.equals(UTF_8)) {
String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
} }
return bomEnc; return bomEnc;
@ -527,18 +545,31 @@ public class XmlStreamReader extends Reader {
// BOM is UTF-16BE or UTF-16LE // BOM is UTF-16BE or UTF-16LE
if (bomEnc.equals(UTF_16BE) || bomEnc.equals(UTF_16LE)) { if (bomEnc.equals(UTF_16BE) || bomEnc.equals(UTF_16LE)) {
if (xmlGuessEnc != null && !xmlGuessEnc.equals(bomEnc)) { if (xmlGuessEnc != null && !xmlGuessEnc.equals(bomEnc)) {
String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
} }
if (xmlEnc != null && !xmlEnc.equals(UTF_16) && !xmlEnc.equals(bomEnc)) { if (xmlEnc != null && !xmlEnc.equals(UTF_16) && !xmlEnc.equals(bomEnc)) {
String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
}
return bomEnc;
}
// BOM is UTF-32BE or UTF-32LE
if (bomEnc.equals(UTF_32BE) || bomEnc.equals(UTF_32LE)) {
if (xmlGuessEnc != null && !xmlGuessEnc.equals(bomEnc)) {
final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
}
if (xmlEnc != null && !xmlEnc.equals(UTF_32) && !xmlEnc.equals(bomEnc)) {
final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
} }
return bomEnc; return bomEnc;
} }
// BOM is something else // BOM is something else
String msg = MessageFormat.format(RAW_EX_2, bomEnc, xmlGuessEnc, xmlEnc); final String msg = MessageFormat.format(RAW_EX_2, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
} }
@ -555,9 +586,9 @@ public class XmlStreamReader extends Reader {
* @return the HTTP encoding * @return the HTTP encoding
* @throws IOException thrown if there is a problem reading the stream. * @throws IOException thrown if there is a problem reading the stream.
*/ */
String calculateHttpEncoding(String httpContentType, String calculateHttpEncoding(final String httpContentType,
String bomEnc, String xmlGuessEnc, String xmlEnc, final String bomEnc, final String xmlGuessEnc, final String xmlEnc,
boolean lenient) throws IOException { final boolean lenient) throws IOException {
// Lenient and has XML encoding // Lenient and has XML encoding
if (lenient && xmlEnc != null) { if (lenient && xmlEnc != null) {
@ -565,14 +596,14 @@ public class XmlStreamReader extends Reader {
} }
// Determine mime/encoding content types from HTTP Content Type // Determine mime/encoding content types from HTTP Content Type
String cTMime = getContentTypeMime(httpContentType); final String cTMime = getContentTypeMime(httpContentType);
String cTEnc = getContentTypeEncoding(httpContentType); final String cTEnc = getContentTypeEncoding(httpContentType);
boolean appXml = isAppXml(cTMime); final boolean appXml = isAppXml(cTMime);
boolean textXml = isTextXml(cTMime); final boolean textXml = isTextXml(cTMime);
// Mime type NOT "application/xml" or "text/xml" // Mime type NOT "application/xml" or "text/xml"
if (!appXml && !textXml) { if (!appXml && !textXml) {
String msg = MessageFormat.format(HTTP_EX_3, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); final String msg = MessageFormat.format(HTTP_EX_3, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
} }
@ -580,15 +611,14 @@ public class XmlStreamReader extends Reader {
if (cTEnc == null) { if (cTEnc == null) {
if (appXml) { if (appXml) {
return calculateRawEncoding(bomEnc, xmlGuessEnc, xmlEnc); return calculateRawEncoding(bomEnc, xmlGuessEnc, xmlEnc);
} else {
return (defaultEncoding == null) ? US_ASCII : defaultEncoding;
} }
return defaultEncoding == null ? US_ASCII : defaultEncoding;
} }
// UTF-16BE or UTF-16LE content type encoding // UTF-16BE or UTF-16LE content type encoding
if (cTEnc.equals(UTF_16BE) || cTEnc.equals(UTF_16LE)) { if (cTEnc.equals(UTF_16BE) || cTEnc.equals(UTF_16LE)) {
if (bomEnc != null) { if (bomEnc != null) {
String msg = MessageFormat.format(HTTP_EX_1, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); final String msg = MessageFormat.format(HTTP_EX_1, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
} }
return cTEnc; return cTEnc;
@ -599,7 +629,25 @@ public class XmlStreamReader extends Reader {
if (bomEnc != null && bomEnc.startsWith(UTF_16)) { if (bomEnc != null && bomEnc.startsWith(UTF_16)) {
return bomEnc; return bomEnc;
} }
String msg = MessageFormat.format(HTTP_EX_2, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); final String msg = MessageFormat.format(HTTP_EX_2, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
}
// UTF-32BE or UTF-132E content type encoding
if (cTEnc.equals(UTF_32BE) || cTEnc.equals(UTF_32LE)) {
if (bomEnc != null) {
final String msg = MessageFormat.format(HTTP_EX_1, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
}
return cTEnc;
}
// UTF-32 content type encoding
if (cTEnc.equals(UTF_32)) {
if (bomEnc != null && bomEnc.startsWith(UTF_32)) {
return bomEnc;
}
final String msg = MessageFormat.format(HTTP_EX_2, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
} }
@ -612,10 +660,10 @@ public class XmlStreamReader extends Reader {
* @param httpContentType the HTTP content type * @param httpContentType the HTTP content type
* @return The mime content type * @return The mime content type
*/ */
static String getContentTypeMime(String httpContentType) { static String getContentTypeMime(final String httpContentType) {
String mime = null; String mime = null;
if (httpContentType != null) { if (httpContentType != null) {
int i = httpContentType.indexOf(";"); final int i = httpContentType.indexOf(";");
if (i >= 0) { if (i >= 0) {
mime = httpContentType.substring(0, i); mime = httpContentType.substring(0, i);
} else { } else {
@ -634,22 +682,25 @@ public class XmlStreamReader extends Reader {
* httpContentType is NULL. * httpContentType is NULL.
* *
* @param httpContentType the HTTP content type * @param httpContentType the HTTP content type
* @return The content type encoding * @return The content type encoding (upcased)
*/ */
static String getContentTypeEncoding(String httpContentType) { static String getContentTypeEncoding(final String httpContentType) {
String encoding = null; String encoding = null;
if (httpContentType != null) { if (httpContentType != null) {
int i = httpContentType.indexOf(";"); final int i = httpContentType.indexOf(";");
if (i > -1) { if (i > -1) {
String postMime = httpContentType.substring(i + 1); final String postMime = httpContentType.substring(i + 1);
Matcher m = CHARSET_PATTERN.matcher(postMime); final Matcher m = CHARSET_PATTERN.matcher(postMime);
encoding = (m.find()) ? m.group(1) : null; encoding = m.find() ? m.group(1) : null;
encoding = (encoding != null) ? encoding.toUpperCase() : null; encoding = encoding != null ? encoding.toUpperCase(Locale.ROOT) : null;
} }
} }
return encoding; return encoding;
} }
/**
* Pattern capturing the encoding of the "xml" processing instruction.
*/
public static final Pattern ENCODING_PATTERN = Pattern.compile( public static final Pattern ENCODING_PATTERN = Pattern.compile(
"<\\?xml.*encoding[\\s]*=[\\s]*((?:\".[^\"]*\")|(?:'.[^']*'))", "<\\?xml.*encoding[\\s]*=[\\s]*((?:\".[^\"]*\")|(?:'.[^']*'))",
Pattern.MULTILINE); Pattern.MULTILINE);
@ -657,52 +708,50 @@ public class XmlStreamReader extends Reader {
/** /**
* Returns the encoding declared in the <?xml encoding=...?>, NULL if none. * Returns the encoding declared in the <?xml encoding=...?>, NULL if none.
* *
* @param is InputStream to create the reader from. * @param inputStream InputStream to create the reader from.
* @param guessedEnc guessed encoding * @param guessedEnc guessed encoding
* @return the encoding declared in the <?xml encoding=...?> * @return the encoding declared in the <?xml encoding=...?>
* @throws IOException thrown if there is a problem reading the stream. * @throws IOException thrown if there is a problem reading the stream.
*/ */
private static String getXmlProlog(InputStream is, String guessedEnc) private static String getXmlProlog(final InputStream inputStream, final String guessedEnc)
throws IOException { throws IOException {
String encoding = null; String encoding = null;
if (guessedEnc != null) { if (guessedEnc != null) {
byte[] bytes = new byte[BUFFER_SIZE]; final byte[] bytes = new byte[BUFFER_SIZE];
is.mark(BUFFER_SIZE); inputStream.mark(BUFFER_SIZE);
int offset = 0; int offset = 0;
int max = BUFFER_SIZE; int max = BUFFER_SIZE;
int c = is.read(bytes, offset, max); int c = inputStream.read(bytes, offset, max);
int firstGT = -1; int firstGT = -1;
String xmlProlog = null; String xmlProlog = ""; // avoid possible NPE warning (cannot happen; this just silences the warning)
while (c != -1 && firstGT == -1 && offset < BUFFER_SIZE) { while (c != -1 && firstGT == -1 && offset < BUFFER_SIZE) {
offset += c; offset += c;
max -= c; max -= c;
c = is.read(bytes, offset, max); c = inputStream.read(bytes, offset, max);
xmlProlog = new String(bytes, 0, offset, guessedEnc); xmlProlog = new String(bytes, 0, offset, guessedEnc);
firstGT = xmlProlog.indexOf('>'); firstGT = xmlProlog.indexOf('>');
} }
if (firstGT == -1) { if (firstGT == -1) {
if (c == -1) { if (c == -1) {
throw new IOException("Unexpected end of XML stream"); throw new IOException("Unexpected end of XML stream");
} else {
throw new IOException(
"XML prolog or ROOT element not found on first "
+ offset + " bytes");
} }
throw new IOException(
"XML prolog or ROOT element not found on first "
+ offset + " bytes");
} }
int bytesRead = offset; final int bytesRead = offset;
if (bytesRead > 0) { if (bytesRead > 0) {
is.reset(); inputStream.reset();
BufferedReader bReader = new BufferedReader(new StringReader( final BufferedReader bReader = new BufferedReader(new StringReader(
xmlProlog.substring(0, firstGT + 1))); xmlProlog.substring(0, firstGT + 1)));
StringBuilder prolog = new StringBuilder(); final StringBuffer prolog = new StringBuffer();
String line = bReader.readLine(); String line;
while (line != null) { while ((line = bReader.readLine()) != null) {
prolog.append(line); prolog.append(line);
line = bReader.readLine();
} }
Matcher m = ENCODING_PATTERN.matcher(prolog); final Matcher m = ENCODING_PATTERN.matcher(prolog);
if (m.find()) { if (m.find()) {
encoding = Objects.requireNonNull(m.group(1)).toUpperCase(); encoding = Objects.requireNonNull(m.group(1)).toUpperCase(Locale.ROOT);
encoding = encoding.substring(1, encoding.length() - 1); encoding = encoding.substring(1, encoding.length() - 1);
} }
} }
@ -717,12 +766,12 @@ public class XmlStreamReader extends Reader {
* @return true if the mime type belongs to the APPLICATION XML family, * @return true if the mime type belongs to the APPLICATION XML family,
* otherwise false * otherwise false
*/ */
static boolean isAppXml(String mime) { static boolean isAppXml(final String mime) {
return mime != null && return mime != null &&
(mime.equals("application/xml") || (mime.equals("application/xml") ||
mime.equals("application/xml-dtd") || mime.equals("application/xml-dtd") ||
mime.equals("application/xml-external-parsed-entity") || mime.equals("application/xml-external-parsed-entity") ||
(mime.startsWith("application/") && mime.endsWith("+xml"))); mime.startsWith("application/") && mime.endsWith("+xml"));
} }
/** /**
@ -732,26 +781,26 @@ public class XmlStreamReader extends Reader {
* @return true if the mime type belongs to the TEXT XML family, * @return true if the mime type belongs to the TEXT XML family,
* otherwise false * otherwise false
*/ */
static boolean isTextXml(String mime) { static boolean isTextXml(final String mime) {
return mime != null && return mime != null &&
(mime.equals("text/xml") || (mime.equals("text/xml") ||
mime.equals("text/xml-external-parsed-entity") || mime.equals("text/xml-external-parsed-entity") ||
(mime.startsWith("text/") && mime.endsWith("+xml"))); mime.startsWith("text/") && mime.endsWith("+xml"));
} }
private static final String RAW_EX_1 = private static final String RAW_EX_1 =
"Invalid encoding, BOM [{0}] XML guess [{1}] XML prolog [{2}] encoding mismatch"; "Invalid encoding, BOM [{0}] XML guess [{1}] XML prolog [{2}] encoding mismatch";
private static final String RAW_EX_2 = private static final String RAW_EX_2 =
"Invalid encoding, BOM [{0}] XML guess [{1}] XML prolog [{2}] unknown BOM"; "Invalid encoding, BOM [{0}] XML guess [{1}] XML prolog [{2}] unknown BOM";
private static final String HTTP_EX_1 = private static final String HTTP_EX_1 =
"Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], BOM must be NULL"; "Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], BOM must be NULL";
private static final String HTTP_EX_2 = private static final String HTTP_EX_2 =
"Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], encoding mismatch"; "Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], encoding mismatch";
private static final String HTTP_EX_3 = private static final String HTTP_EX_3 =
"Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], Invalid MIME"; "Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], Invalid MIME";
} }

@ -28,10 +28,9 @@ import java.io.IOException;
* do an alternate processing with the stream. Note that the original * do an alternate processing with the stream. Note that the original
* InputStream given to the XmlStreamReader cannot be used as that one has been * InputStream given to the XmlStreamReader cannot be used as that one has been
* already read. * already read.
* </p>
* *
* @author Alejandro Abdelnur * @since 2.0
* @version $Id: XmlStreamReaderException.java 1004112 2010-10-04 04:48:25Z niallp $
* @since Commons IO 2.0
*/ */
public class XmlStreamReaderException extends IOException { public class XmlStreamReaderException extends IOException {
@ -52,14 +51,15 @@ public class XmlStreamReaderException extends IOException {
* determined. * determined.
* <p> * <p>
* Instances of this exception are thrown by the XmlStreamReader. * Instances of this exception are thrown by the XmlStreamReader.
* </p>
* *
* @param msg message describing the reason for the exception. * @param msg message describing the reason for the exception.
* @param bomEnc BOM encoding. * @param bomEnc BOM encoding.
* @param xmlGuessEnc XML guess encoding. * @param xmlGuessEnc XML guess encoding.
* @param xmlEnc XML prolog encoding. * @param xmlEnc XML prolog encoding.
*/ */
public XmlStreamReaderException(String msg, String bomEnc, public XmlStreamReaderException(final String msg, final String bomEnc,
String xmlGuessEnc, String xmlEnc) { final String xmlGuessEnc, final String xmlEnc) {
this(msg, null, null, bomEnc, xmlGuessEnc, xmlEnc); this(msg, null, null, bomEnc, xmlGuessEnc, xmlEnc);
} }
@ -68,6 +68,7 @@ public class XmlStreamReaderException extends IOException {
* determined. * determined.
* <p> * <p>
* Instances of this exception are thrown by the XmlStreamReader. * Instances of this exception are thrown by the XmlStreamReader.
* </p>
* *
* @param msg message describing the reason for the exception. * @param msg message describing the reason for the exception.
* @param ctMime MIME type in the content-type. * @param ctMime MIME type in the content-type.
@ -76,8 +77,8 @@ public class XmlStreamReaderException extends IOException {
* @param xmlGuessEnc XML guess encoding. * @param xmlGuessEnc XML guess encoding.
* @param xmlEnc XML prolog encoding. * @param xmlEnc XML prolog encoding.
*/ */
public XmlStreamReaderException(String msg, String ctMime, String ctEnc, public XmlStreamReaderException(final String msg, final String ctMime, final String ctEnc,
String bomEnc, String xmlGuessEnc, String xmlEnc) { final String bomEnc, final String xmlGuessEnc, final String xmlEnc) {
super(msg); super(msg);
contentTypeMime = ctMime; contentTypeMime = ctMime;
contentTypeEncoding = ctEnc; contentTypeEncoding = ctEnc;
@ -120,7 +121,6 @@ public class XmlStreamReaderException extends IOException {
* @return the MIME type in the content-type, null if there was not * @return the MIME type in the content-type, null if there was not
* content-type or the encoding detection did not involve HTTP. * content-type or the encoding detection did not involve HTTP.
*/ */
@SuppressWarnings("unused")
public String getContentTypeMime() { public String getContentTypeMime() {
return contentTypeMime; return contentTypeMime;
} }

Loading…
Cancel
Save