Merge branch 'subtree'
authorNiki Roo <niki@nikiroo.be>
Wed, 20 May 2020 15:25:15 +0000 (17:25 +0200)
committerNiki Roo <niki@nikiroo.be>
Wed, 20 May 2020 15:25:15 +0000 (17:25 +0200)
1  2 
src/be/nikiroo/utils/streams/BufferedInputStream.java
src/be/nikiroo/utils/streams/ReplaceInputStream.java
src/be/nikiroo/utils/test_code/ReplaceInputStreamTest.java

index babd2ce88ec929883713af1895a556d6c4178ad9,3cad70d2152fc433c5313b8fbec1292f9c3ff9d2..3cad70d2152fc433c5313b8fbec1292f9c3ff9d2
@@@ -41,6 -41,7 +41,7 @@@ public class BufferedInputStream extend
        private boolean closed;
        private InputStream in;
        private int openCounter;
+       private byte[] singleByteReader = new byte[1];
  
        /** array + offset of pushed-back buffers */
        private List<Entry<byte[], Integer>> backBuffers;
@@@ -48,7 -49,7 +49,7 @@@
        private long bytesRead;
  
        /**
-        * Create a new {@link BufferedInputStream2} that wraps the given
+        * Create a new {@link BufferedInputStream} that wraps the given
         * {@link InputStream}.
         * 
         * @param in
@@@ -64,8 -65,8 +65,8 @@@
        }
  
        /**
-        * Create a new {@link BufferedInputStream2} that wraps the given bytes
-        * array as a data source.
+        * Create a new {@link BufferedInputStream} that wraps the given bytes array
+        * as a data source.
         * 
         * @param in
         *            the array to wrap, cannot be NULL
@@@ -75,8 -76,8 +76,8 @@@
        }
  
        /**
-        * Create a new {@link BufferedInputStream2} that wraps the given bytes
-        * array as a data source.
+        * Create a new {@link BufferedInputStream} that wraps the given bytes array
+        * as a data source.
         * 
         * @param in
         *            the array to wrap, cannot be NULL
        }
  
        /**
-        * Return this very same {@link BufferedInputStream2}, but keep a counter of
+        * Return this very same {@link BufferedInputStream}, but keep a counter of
         * how many streams were open this way. When calling
-        * {@link BufferedInputStream2#close()}, decrease this counter if it is not
+        * {@link BufferedInputStream#close()}, decrease this counter if it is not
         * already zero instead of actually closing the stream.
         * <p>
         * You are now responsible for it &mdash; you <b>must</b> close it.
  
        @Override
        public int read() throws IOException {
-               checkClose();
-               preRead();
-               if (eof) {
+               if (read(singleByteReader) < 0) {
                        return -1;
                }
  
-               return buffer[start++];
+               return singleByteReader[0];
        }
  
        @Override
                }
  
                // Read from the pushed-back buffers if any
+               if (backBuffers.isEmpty()) {
+                       preRead(); // an implementation could pushback in preRead()
+               }
+               
                if (!backBuffers.isEmpty()) {
                        int read = 0;
  
                        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);
         * <p>
         * Including the under-laying {@link InputStream}.
         * <p>
-        * <b>Note:</b> if you called the {@link BufferedInputStream2#open()} method
+        * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
         * prior to this one, it will just decrease the internal count of how many
         * open streams it held and do nothing else. The stream will actually be
-        * closed when you have called {@link BufferedInputStream2#close()} once
-        * more than {@link BufferedInputStream2#open()}.
+        * closed when you have called {@link BufferedInputStream#close()} once more
+        * than {@link BufferedInputStream#open()}.
         * 
         * @exception IOException
         *                in case of I/O error
         * You can call this method multiple times, it will not cause an
         * {@link IOException} for subsequent calls.
         * <p>
-        * <b>Note:</b> if you called the {@link BufferedInputStream2#open()} method
+        * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
         * prior to this one, it will just decrease the internal count of how many
         * open streams it held and do nothing else. The stream will actually be
-        * closed when you have called {@link BufferedInputStream2#close()} once
-        * more than {@link BufferedInputStream2#open()}.
+        * closed when you have called {@link BufferedInputStream#close()} once more
+        * than {@link BufferedInputStream#open()}.
         * 
         * @param includingSubStream
         *            also close the under-laying stream
                boolean hasRead = false;
                if (in != null && !eof && start >= stop) {
                        start = 0;
-                       stop = read(in, buffer, 0, buffer.length);
+                       stop = read(in, buffer);
                        if (stop > 0) {
                                bytesRead += stop;
                        }
                                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 BufferedInputStream2}
-        * @param off
-        *            the offset
-        * @param len
-        *            the length in bytes
+        *            the buffer we use in this {@link BufferedInputStream}
         * 
         * @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);
        }
  
        /**
index ae576e25e7a4833e9ce8f4bea25616ff018eca52,0860f78c35e09b08ffe1f3326a379e1d504e5959..0860f78c35e09b08ffe1f3326a379e1d504e5959
@@@ -2,6 -2,8 +2,8 @@@ package be.nikiroo.utils.streams
  
  import java.io.IOException;
  import java.io.InputStream;
+ import java.util.ArrayList;
+ import java.util.List;
  
  import be.nikiroo.utils.StringUtils;
  
@@@ -23,12 -25,8 +25,8 @@@ public class ReplaceInputStream extend
  
        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 * Math.max(maxToSize, 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 + count,
-                                                               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 + "]";
        }
  }
index d08a91ed779c89502ef8e7d65eca8e47b7fa9ddd,efab8c73d62e5f79779a41b6f8c4c889ad8a8f74..efab8c73d62e5f79779a41b6f8c4c889ad8a8f74
@@@ -83,33 -83,122 +83,122 @@@ class ReplaceInputStreamTest extends Te
  
                                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", "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".getBytes("UTF-8");
+                               byte[] data = "I like red cabage".getBytes("UTF-8");
                                ReplaceInputStream in = new ReplaceInputStream(
                                                new ByteArrayInputStream(data), //
-                                               new String[] {"like", "red"}, //
-                                               new String[] {"dislike", "green"} //
+                                               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("I dislike green", result);
+                               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);