import java.io.IOException;
import java.io.InputStream;
-import java.util.Arrays;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
import be.nikiroo.utils.StringUtils;
public class BufferedInputStream extends InputStream {
/**
* The size of the internal buffer (can be different if you pass your own
- * buffer, of course).
+ * buffer, of course, and can also expand to search for longer "startsWith"
+ * data).
* <p>
- * A second buffer of twice the size can sometimes be created as needed for
- * the {@link BufferedInputStream#startsWith(byte[])} search operation.
+ * Note that special "push-back" buffers can also be created during the life
+ * of this stream.
*/
static private final int BUFFER_SIZE = 4096;
private boolean closed;
private InputStream in;
private int openCounter;
+ private byte[] singleByteReader = new byte[1];
- // special use, prefetched next buffer
- private byte[] buffer2;
- private int pos2;
- private int len2;
- private byte[] originalBuffer;
+ /** array + offset of pushed-back buffers */
+ private List<Entry<byte[], Integer>> backBuffers;
private long bytesRead;
this.in = in;
this.buffer = new byte[BUFFER_SIZE];
- this.originalBuffer = this.buffer;
this.start = 0;
this.stop = 0;
+ this.backBuffers = new ArrayList<Entry<byte[], Integer>>();
}
/**
this.in = null;
this.buffer = in;
- this.originalBuffer = this.buffer;
this.start = offset;
this.stop = length;
- }
-
- /**
- * The internal buffer size (can be useful to know for search methods).
- *
- * @return the size of the internal buffer, in bytes.
- */
- public int getInternalBufferSize() {
- return originalBuffer.length;
+ this.backBuffers = new ArrayList<Entry<byte[], Integer>>();
}
/**
*/
public boolean is(byte[] search) throws IOException {
if (startsWith(search)) {
- return (stop - start) == search.length;
+ return available() == search.length;
}
return false;
* greater than the internal buffer
*/
public boolean startsWith(byte[] search) throws IOException {
- if (search.length > originalBuffer.length) {
- throw new IOException(
- "This stream does not support searching for more than "
- + buffer.length + " bytes");
- }
-
checkClose();
- if (available() < search.length) {
+ while (consolidatePushBack(search.length) < search.length) {
preRead();
- }
-
- if (available() >= search.length) {
- // Easy path
- return StreamUtils.startsWith(search, buffer, start, stop);
- } else if (in != null && !eof) {
- // Harder path
- if (buffer2 == null && buffer.length == originalBuffer.length) {
- buffer2 = Arrays.copyOf(buffer, buffer.length * 2);
-
- pos2 = buffer.length;
- len2 = read(in, buffer2, pos2, buffer.length);
- if (len2 > 0) {
- bytesRead += len2;
- }
-
- // Note: here, len/len2 = INDEX of last good byte
- len2 += pos2;
+ if (start >= stop) {
+ // Not enough data left to start with that
+ return false;
}
- return StreamUtils.startsWith(search, buffer2, pos2, len2);
+ byte[] newBuffer = new byte[stop - start];
+ System.arraycopy(buffer, start, newBuffer, 0, stop - start);
+ pushback(newBuffer, 0);
+ start = stop;
}
- return false;
+ Entry<byte[], Integer> bb = backBuffers.get(backBuffers.size() - 1);
+ byte[] bbBuffer = bb.getKey();
+ int bbOffset = bb.getValue();
+
+ return StreamUtils.startsWith(search, bbBuffer, bbOffset,
+ bbBuffer.length);
}
/**
}
/**
- * Check if this stream is spent (no more data to read or to
- * process).
+ * Check if this stream is spent (no more data to read or to process).
*
* @return TRUE if it is
*
@Override
public int read() throws IOException {
- checkClose();
-
- preRead();
- if (eof) {
+ if (read(singleByteReader) < 0) {
return -1;
}
- return buffer[start++];
+ return singleByteReader[0];
}
@Override
return 0;
}
+ // Read from the pushed-back buffers if any
+ if (backBuffers.isEmpty()) {
+ preRead(); // an implementation could pushback in preRead()
+ }
+
+ if (!backBuffers.isEmpty()) {
+ int read = 0;
+
+ Entry<byte[], Integer> bb = backBuffers
+ .remove(backBuffers.size() - 1);
+ byte[] bbBuffer = bb.getKey();
+ int bbOffset = bb.getValue();
+ int bbSize = bbBuffer.length - bbOffset;
+
+ if (bbSize > blen) {
+ read = blen;
+ System.arraycopy(bbBuffer, bbOffset, b, boff, read);
+ pushback(bbBuffer, bbOffset + read);
+ } else {
+ read = bbSize;
+ System.arraycopy(bbBuffer, bbOffset, b, boff, read);
+ }
+
+ return read;
+ }
+
int done = 0;
while (hasMoreData() && done < blen) {
preRead();
}
long skipped = 0;
+ while (!backBuffers.isEmpty() && n > 0) {
+ Entry<byte[], Integer> bb = backBuffers
+ .remove(backBuffers.size() - 1);
+ byte[] bbBuffer = bb.getKey();
+ int bbOffset = bb.getValue();
+ int bbSize = bbBuffer.length - bbOffset;
+
+ int localSkip = 0;
+ localSkip = (int) Math.min(n, bbSize);
+
+ n -= localSkip;
+ bbSize -= localSkip;
+
+ if (bbSize > 0) {
+ pushback(bbBuffer, bbOffset + localSkip);
+ }
+ }
while (hasMoreData() && n > 0) {
preRead();
return 0;
}
- return Math.max(0, stop - start);
+ int avail = 0;
+ for (Entry<byte[], Integer> entry : backBuffers) {
+ avail += entry.getKey().length - entry.getValue();
+ }
+
+ return avail + Math.max(0, stop - start);
}
/**
}
}
+ /**
+ * Consolidate the push-back buffers so the last one is at least the given
+ * size, if possible.
+ * <p>
+ * If there is not enough data in the push-back buffers, they will all be
+ * consolidated.
+ *
+ * @param size
+ * the minimum size of the consolidated buffer, or -1 to force
+ * the consolidation of all push-back buffers
+ *
+ * @return the size of the last, consolidated buffer; can be less than the
+ * requested size if not enough data
+ */
+ protected int consolidatePushBack(int size) {
+ int bbIndex = -1;
+ int bbUpToSize = 0;
+ for (Entry<byte[], Integer> entry : backBuffers) {
+ bbIndex++;
+ bbUpToSize += entry.getKey().length - entry.getValue();
+
+ if (size >= 0 && bbUpToSize >= size) {
+ break;
+ }
+ }
+
+ // Index 0 means "the last buffer is already big enough"
+ if (bbIndex > 0) {
+ byte[] consolidatedBuffer = new byte[bbUpToSize];
+ int consolidatedPos = 0;
+ for (int i = 0; i <= bbIndex; i++) {
+ Entry<byte[], Integer> bb = backBuffers
+ .remove(backBuffers.size() - 1);
+ byte[] bbBuffer = bb.getKey();
+ int bbOffset = bb.getValue();
+ int bbSize = bbBuffer.length - bbOffset;
+ System.arraycopy(bbBuffer, bbOffset, consolidatedBuffer,
+ consolidatedPos, bbSize);
+ }
+
+ pushback(consolidatedBuffer, 0);
+ }
+
+ return bbUpToSize;
+ }
+
/**
* Check if we still have some data in the buffer and, if not, fetch some.
*
boolean hasRead = false;
if (in != null && !eof && start >= stop) {
start = 0;
- if (buffer2 != null) {
- buffer = buffer2;
- start = pos2;
- stop = len2;
-
- buffer2 = null;
- pos2 = 0;
- len2 = 0;
- } else {
- buffer = originalBuffer;
-
- stop = read(in, buffer, 0, buffer.length);
- if (stop > 0) {
- bytesRead += stop;
- }
+ stop = read(in, buffer);
+ if (stop > 0) {
+ bytesRead += stop;
}
hasRead = true;
}
/**
- * Read the under-laying stream into the local buffer.
+ * Push back some data that will be read again at the next read call.
+ *
+ * @param buffer
+ * the buffer to push back
+ * @param offset
+ * the offset at which to start reading in the buffer
+ */
+ protected void pushback(byte[] buffer, int offset) {
+ backBuffers.add(
+ new AbstractMap.SimpleEntry<byte[], Integer>(buffer, offset));
+ }
+
+ /**
+ * Push back some data that will be read again at the next read call.
+ *
+ * @param buffer
+ * the buffer to push back
+ * @param offset
+ * the offset at which to start reading in the buffer
+ * @param len
+ * the length to copy
+ */
+ protected void pushback(byte[] buffer, int offset, int len) {
+ // TODO: not efficient!
+ if (buffer.length != len) {
+ byte[] lenNotSupportedYet = new byte[len];
+ System.arraycopy(buffer, offset, lenNotSupportedYet, 0, len);
+ buffer = lenNotSupportedYet;
+ offset = 0;
+ }
+
+ pushback(buffer, offset);
+ }
+
+ /**
+ * Read the under-laying stream into the given local buffer.
*
* @param in
* the under-laying {@link InputStream}
* @param buffer
* the buffer we use in this {@link BufferedInputStream}
- * @param off
- * the offset
- * @param len
- * the length in bytes
*
* @return the number of bytes read
*
* @throws IOException
* in case of I/O error
*/
- protected int read(InputStream in, byte[] buffer, int off, int len)
- throws IOException {
- return in.read(buffer, off, len);
+ protected int read(InputStream in, byte[] buffer) throws IOException {
+ return in.read(buffer, 0, buffer.length);
}
/**
return false;
}
- return (start < stop) || !eof;
+ return !backBuffers.isEmpty() || (start < stop) || !eof;
}
/**
import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
import be.nikiroo.utils.StringUtils;
private byte[][] froms;
private byte[][] tos;
+ private int bufferSize;
private int maxFromSize;
- private int maxToSize;
-
- private byte[] source;
- private int spos;
- private int slen;
/**
* Create a {@link ReplaceInputStream} that will replace <tt>from</tt> with
maxFromSize = Math.max(maxFromSize, froms[i].length);
}
- maxToSize = 0;
+ int maxToSize = 0;
for (int i = 0; i < tos.length; i++) {
maxToSize = Math.max(maxToSize, tos[i].length);
}
// We need at least maxFromSize so we can iterate and replace
- source = new byte[Math.max(2 * maxFromSize, MIN_BUFFER_SIZE)];
- spos = 0;
- slen = 0;
+ bufferSize = Math.max(4 * Math.max(maxToSize, maxFromSize),
+ MIN_BUFFER_SIZE);
}
@Override
- protected int read(InputStream in, byte[] buffer, int off, int len)
- throws IOException {
- if (len < maxToSize || source.length < maxToSize * 2) {
- throw new IOException(
- "An underlaying buffer is too small for these replace values");
- }
+ protected boolean preRead() throws IOException {
+ boolean rep = super.preRead();
+ start = stop;
+ return rep;
+ }
- // We need at least one byte of data to process
- if (available() < Math.max(maxFromSize, 1) && !eof) {
- spos = 0;
- slen = in.read(source);
+ @Override
+ protected int read(InputStream in, byte[] buffer) throws IOException {
+ buffer = null; // do not use the buffer.
+
+ byte[] newBuffer = new byte[bufferSize];
+ int read = 0;
+ while (read < bufferSize / 2) {
+ int thisTime = in.read(newBuffer, read, bufferSize / 2 - read);
+ if (thisTime <= 0) {
+ break;
+ }
+ read += thisTime;
}
- // Note: very simple, not efficient implementation; sorry.
- int count = 0;
- while (spos < slen && count < len - maxToSize) {
- boolean replaced = false;
- for (int i = 0; i < froms.length; i++) {
- if (froms[i] != null && froms[i].length > 0
- && StreamUtils.startsWith(froms[i], source, spos, slen)) {
- if (tos[i] != null && tos[i].length > 0) {
- System.arraycopy(tos[i], 0, buffer, off + spos,
- tos[i].length);
- count += tos[i].length;
+ List<byte[]> bbBuffers = new ArrayList<byte[]>();
+ List<Integer> bbOffsets = new ArrayList<Integer>();
+ List<Integer> bbLengths = new ArrayList<Integer>();
+
+ int offset = 0;
+ for (int i = 0; i < read; i++) {
+ for (int fromIndex = 0; fromIndex < froms.length; fromIndex++) {
+ byte[] from = froms[fromIndex];
+ byte[] to = tos[fromIndex];
+
+ if (from.length > 0
+ && StreamUtils.startsWith(from, newBuffer, i, read)) {
+ if (i - offset > 0) {
+ bbBuffers.add(newBuffer);
+ bbOffsets.add(offset);
+ bbLengths.add(i - offset);
}
- spos += froms[i].length;
- replaced = true;
- break;
+ if (to.length > 0) {
+ bbBuffers.add(to);
+ bbOffsets.add(0);
+ bbLengths.add(to.length);
+ }
+
+ i += from.length;
+ offset = i;
}
}
+ }
+
+ if (offset < read) {
+ bbBuffers.add(newBuffer);
+ bbOffsets.add(offset);
+ bbLengths.add(read - offset);
+ }
- if (!replaced) {
- buffer[off + count++] = source[spos++];
+ for (int i = bbBuffers.size() - 1; i >= 0; i--) {
+ // DEBUG("pushback", bbBuffers.get(i), bbOffsets.get(i),
+ // bbLengths.get(i));
+ pushback(bbBuffers.get(i), bbOffsets.get(i), bbLengths.get(i));
+ }
+
+ return read;
+ }
+
+ // static public void DEBUG(String title, byte[] b, int off, int len) {
+ // String str = new String(b,off,len);
+ // if(str.length()>20) {
+ // str=str.substring(0,10)+" ...
+ // "+str.substring(str.length()-10,str.length());
+ // }
+ // }
+
+ @Override
+ public String toString() {
+ StringBuilder rep = new StringBuilder();
+ rep.append(getClass().getSimpleName()).append("\n");
+
+ for (int i = 0; i < froms.length; i++) {
+ byte[] from = froms[i];
+ byte[] to = tos[i];
+
+ rep.append("\t");
+ rep.append("bytes[").append(from.length).append("]");
+ if (from.length <= 20) {
+ rep.append(" (").append(new String(from)).append(")");
+ }
+ rep.append(" -> ");
+ rep.append("bytes[").append(to.length).append("]");
+ if (to.length <= 20) {
+ rep.append(" (").append(new String(to)).append(")");
}
+ rep.append("\n");
}
- return count;
+ return "[" + rep + "]";
}
}
package be.nikiroo.utils.test_code;
import java.io.ByteArrayInputStream;
-import java.io.IOException;
import be.nikiroo.utils.IOUtils;
import be.nikiroo.utils.streams.NextableInputStream;
11 }));
// too big
- try {
- in.startsWith(new byte[] { 42, 12, 0, 127, 12, 51, 11, 12,
- 0 });
- fail("Searching a prefix bigger than the array should throw an IOException");
- } catch (IOException e) {
- }
+ assertEquals(
+ "A search term bigger than the whole data cannot be found in the data",
+ false, in.startsWith(new byte[] { 42, 12, 0, 127, 12,
+ 51, 11, 12, 0 }));
in.close();
}
in.startsWith("Toto"));
assertEquals("It actually does not start with that", false,
in.startsWith("Fanfan et Toto vont à la mee"));
-
+
// too big
- try {
- in.startsWith("Fanfan et Toto vont à la mer.");
- fail("Searching a prefix bigger than the array should throw an IOException");
- } catch (IOException e) {
- }
+ assertEquals(
+ "A search term bigger than the whole data cannot be found in the data",
+ false, in.startsWith("Fanfan et Toto vont à la mer."));
in.close();
}
}
});
- addTest(new TestCase("Lnger replace") {
+ addTest(new TestCase("Longer replace") {
@Override
public void test() throws Exception {
byte[] data = new byte[] { 42, 12, 0, 127 };
byte[] data = "I like red".getBytes("UTF-8");
ReplaceInputStream in = new ReplaceInputStream(
new ByteArrayInputStream(data),
- "red".getBytes("UTF-8"), "blue".getBytes("UTF-8"));
+ "red", "blue");
checkArrays(this, "FIRST", in, "I like blue".getBytes("UTF-8"));
- data = "I like blue".getBytes("UTF-8");
+ data = "I like blue hammers".getBytes("UTF-8");
in = new ReplaceInputStream(new ByteArrayInputStream(data),
- "blue".getBytes("UTF-8"), "red".getBytes("UTF-8"));
+ "blue", "red");
- checkArrays(this, "FIRST", in, "I like red".getBytes("UTF-8"));
+ checkArrays(this, "SECOND", in, "I like red hammers".getBytes("UTF-8"));
}
});
+
+ addTest(new TestCase("Multiple replaces") {
+ @Override
+ public void test() throws Exception {
+ byte[] data = "I like red cabage".getBytes("UTF-8");
+ ReplaceInputStream in = new ReplaceInputStream(
+ new ByteArrayInputStream(data), //
+ new String[] { "red", "like" }, //
+ new String[] { "green", "very very much like" } //
+ );
+
+ String result = new String(IOUtils.toByteArray(in), "UTF-8");
+ assertEquals("I very very much like green cabage", result);
+ }
+ });
+
+ addTest(new TestCase("Multiple replaces") {
+ @Override
+ public void test() throws Exception {
+ String str= ("" //
+ + "<!DOCTYPE html>\n" //
+ + "<html>\n" //
+ + "<head>\n" //
+ + "<!--\n" //
+ + "\tCopyright 2020 David ROULET\n" //
+ + "\t\n" //
+ + "\tThis file is part of fanfix.\n" //
+ + "\t\n" //
+ + "\tfanfix is free software: you can redistribute it and/or modify\n" //
+ + "\tit under the terms of the GNU Affero General Public License as published by\n" //
+ + "\tthe Free Software Foundation, either version 3 of the License, or\n" //
+ + "\t(at your option) any later version.\n" //
+ + "\t\n" //
+ + "\tfanfix is distributed in the hope that it will be useful,\n" //
+ + "\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n" //
+ + "\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" //
+ + "\tGNU Affero General Public License for more details.\n" //
+ + "\t\n" //
+ + "\tYou should have received a copy of the GNU Affero General Public License\n" //
+ + "\talong with fanfix. If not, see <https://www.gnu.org/licenses/>.\n" //
+ + "\t___________________________________________________________________________\n" //
+ + "\n" //
+ + " This website was coded by:\n" //
+ + " \t\tA kangaroo.\n" //
+ + " _ _\n" //
+ + " (\\\\( \\\n" //
+ + " `.\\-.)\n" //
+ + " _...._ _,-' `-.\n" //
+ + "\\ ,' `-._.- -.,-' . \\\n" //
+ + " \\`. ,' `.\n" //
+ + " \\ `-...__ / . .: y\n" //
+ + " `._ ``-...__ / ,'```-._/\n" //
+ + " `-._ ```-' | /_ //\n" //
+ + " `.._ _ ; <_ \\ //\n" //
+ + " ``-.___ `. `-._ \\ \\ //\n" //
+ + " `- < `. (\\ _/)/ `.\\/ //\n" //
+ + " \\ \\ ` ^^^^^^^^^\n" //
+ + "\t___________________________________________________________________________\n" //
+ + "\t\n" //
+ + "-->\n" //
+ + "\t<meta http-equiv='content-type' content='text/html; charset=UTF-8'>\n" //
+ + "\t<meta name='viewport' content='width=device-width, initial-scale=1.0'>\n" //
+ + "\t<title>${title}</title>\n" //
+ + "\t<link rel='stylesheet' type='text/css' href='/style.css' />\n" //
+ + "\t<link rel='icon' type='image/x-icon' href='/${favicon}' />\n" //
+ + "</head>\n" //
+ + "<body>\n" //
+ + "\t<div class='main'>\n" //
+ + "${banner}${content}\t</div>\n" //
+ + "</body>\n" //
+ + "" //
+ );
+ byte[] data = str.getBytes("UTF-8");
+
+ String title = "Fanfix";
+ String banner = "<div class='banner'>Super banner v3</div>";
+ String content = "";
+
+ InputStream in = new ReplaceInputStream(
+ new ByteArrayInputStream(data), //
+ new String[] { "${title}", "${banner}", "${content}" }, //
+ new String[] { title, banner, content } //
+ );
+
+ String result = new String(IOUtils.toByteArray(in), "UTF-8");
+ assertEquals(str //
+ .replace("${title}", title) //
+ .replace("${banner}", banner) //
+ .replace("${content}", content) //
+ , result);
+ }
+ });
+
+
}
static void checkArrays(TestCase test, String prefix, InputStream in,
byte[] expected) throws Exception {
byte[] actual = IOUtils.toByteArray(in);
+
+// System.out.println("\nActual:");
+// for(byte byt : actual) {
+// System.out.print(byt+" ");
+// }
+// System.out.println("\nExpected:");
+// for(byte byt : expected) {
+// System.out.print(byt+" ");
+// }
+
test.assertEquals("The " + prefix
+ " resulting array has not the correct number of items",
expected.length, actual.length);
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
-import be.nikiroo.fanfix.Instance;
import be.nikiroo.utils.Version;
import be.nikiroo.utils.VersionCheck;
try {
Desktop.getDesktop().browse(e.getURL().toURI());
} catch (IOException ee) {
- Instance.getInstance().getTraceHandler().error(ee);
+ ee.printStackTrace();
} catch (URISyntaxException ee) {
- Instance.getInstance().getTraceHandler().error(ee);
+ ee.printStackTrace();
}
}
});