Commons IO
  1. Commons IO
  2. IO-218

Introduce new filter input stream with replacement facilities

    Details

    • Type: Improvement Improvement
    • Status: Open
    • Priority: Major Major
    • Resolution: Unresolved
    • Affects Version/s: 1.4
    • Fix Version/s: None
    • Component/s: Filters
    • Labels:
      None
    • Environment:

      all environments

      Description

      It seems convenient to have a FilterInputStream that allows to apply predefined repalcement rules against the read data.
      For example we may want to configure the following replacements:

      {1, 2} -> {7, 8}
      {1} -> {9}
      {3, 2} -> {}
      

      and apply them to the input like

      {4, 3, 2, 1, 2, 1, 3}
      

      in order to get a result like

      {4, 7, 8, 9, 3}
      

      I created the class that allows to do that and attached it to this ticket. Unit test class at junit4 format is attached as well.

      So, the task is to review the provided classes, consider if it's worth to add them to commons-io distribution and perform the inclusion in the case of possible result.

      1. ReplaceFilterInputStream.java
        26 kB
        Denis Zhdanov
      2. ReplaceFilterInputStreamTest.java
        8 kB
        Denis Zhdanov

        Issue Links

          Activity

          BELUGA BEHR made changes -
          Link This issue is related to IO-419 [ IO-419 ]
          Sebb made changes -
          Link This issue relates to IO-199 [ IO-199 ]
          Hide
          Aaron Digulla added a comment - - edited

          Isn't this a duplicate of issue IO-199?

          Show
          Aaron Digulla added a comment - - edited Isn't this a duplicate of issue IO-199 ?
          Sebb made changes -
          Fix Version/s 2.2 [ 12318448 ]
          Hide
          Denis Zhdanov added a comment -

          No, it's not ready yet.

          Show
          Denis Zhdanov added a comment - No, it's not ready yet.
          Henri Yandell made changes -
          Original Estimate 120h [ 432000 ]
          Remaining Estimate 120h [ 432000 ]
          Fix Version/s 2.2 [ 12318448 ]
          Hide
          Henri Yandell added a comment -

          Cool class, and great to see everyone working together to develop it - is it ready to go in?

          Show
          Henri Yandell added a comment - Cool class, and great to see everyone working together to develop it - is it ready to go in?
          Mark Thomas made changes -
          Workflow jira [ 12475087 ] Default workflow, editable Closed status [ 12601901 ]
          Hide
          Andreas Niedermeier added a comment - - edited

          Thanks.
          I've already tried these but ended up using this one here - I'm not sure why I didn't use Swizzle so I'll check that again.

          edit:
          Now I know why I didn't use it. Their ReplaceStringsInputStream only defines read() but not read(byte[]) and read(byte[], int, int) so calls to the other methods won't replace the contents.
          But the hint did help anyway because I simply subclassed it and overrode these methods. So that it's working now.

          Show
          Andreas Niedermeier added a comment - - edited Thanks. I've already tried these but ended up using this one here - I'm not sure why I didn't use Swizzle so I'll check that again. edit: Now I know why I didn't use it. Their ReplaceStringsInputStream only defines read() but not read(byte[]) and read(byte[], int, int) so calls to the other methods won't replace the contents. But the hint did help anyway because I simply subclassed it and overrode these methods. So that it's working now.
          Hide
          Ondra Žižka added a comment -

          Similar purpose filters are in the Swizzle Stream library:
          http://swizzle.codehaus.org/Swizzle+Stream

          Show
          Ondra Žižka added a comment - Similar purpose filters are in the Swizzle Stream library: http://swizzle.codehaus.org/Swizzle+Stream
          Hide
          Denis Zhdanov added a comment -

          Thanks, I'll check that.

          Show
          Denis Zhdanov added a comment - Thanks, I'll check that.
          Hide
          Andreas Niedermeier added a comment -

          Hi, first of all great work, but while using this class I sometimes experienced some problems.
          While replacing sometimes 0-bytes would appear in my result. I found out that it only seems to happen when replacementTo is larger than replacementFrom. Doing some debugging I realized that in replaceWithExpand() the {{ByteBuffer}}s limit would be set to a very large value (small input and pretty large buffer used) resulting in much more data in the result than expected (which was all filled with zeros).
          When there is plenty of space in the buffer totalUnread will be a large negative number resulting in a negative unreadBufferSize (lines 509 and 510).
          This unreadBufferSize will lead to a very large moveLength on line 528 (int moveLength = data.remaining() - unreadBufferSize;) which is added to data.limit() on line 538 resulting in a buffer pretending to contain far more data than it really does.

          If the size of the array used in the Test (line 218) is increased from 3 to 4 bufferOverflow() will fail. Increasing it to 6 will also break toClashesFrom() and increasing it to 8 will also cause toLongerThanFrom() to fail (for the described reason).
          Reducing the size to smaller than 3 will cause an infinite loop while toClashesFrom().

          I've made the following two changes to ReplaceFilterInputStream:

          • on line 517 I inserted
            if (unreadBufferSize < 0) {
                unreadBufferSize = 0;
            }
            

            so that a negative unreadBufferSize won't increase moveLength

          • changed line 538 to
            data.limit(Math.min(data.limit() + diff, data.capacity()));
            

            because I assumed that the limit is only increasing by the length difference of the replacements and not the remaining data in the buffer

          I'm not sure if it does all it shoud but so far it seems to work for me and the tests succeed with buffer sizes > 2.

          Show
          Andreas Niedermeier added a comment - Hi, first of all great work, but while using this class I sometimes experienced some problems. While replacing sometimes 0 -bytes would appear in my result. I found out that it only seems to happen when replacementTo is larger than replacementFrom . Doing some debugging I realized that in replaceWithExpand() the {{ByteBuffer}}s limit would be set to a very large value (small input and pretty large buffer used) resulting in much more data in the result than expected (which was all filled with zeros). When there is plenty of space in the buffer totalUnread will be a large negative number resulting in a negative unreadBufferSize (lines 509 and 510). This unreadBufferSize will lead to a very large moveLength on line 528 ( int moveLength = data.remaining() - unreadBufferSize; ) which is added to data.limit() on line 538 resulting in a buffer pretending to contain far more data than it really does. If the size of the array used in the Test (line 218) is increased from 3 to 4 bufferOverflow() will fail. Increasing it to 6 will also break toClashesFrom() and increasing it to 8 will also cause toLongerThanFrom() to fail (for the described reason). Reducing the size to smaller than 3 will cause an infinite loop while toClashesFrom() . I've made the following two changes to ReplaceFilterInputStream: on line 517 I inserted if (unreadBufferSize < 0) { unreadBufferSize = 0; } so that a negative unreadBufferSize won't increase moveLength changed line 538 to data.limit( Math .min(data.limit() + diff, data.capacity())); because I assumed that the limit is only increasing by the length difference of the replacements and not the remaining data in the buffer I'm not sure if it does all it shoud but so far it seems to work for me and the tests succeed with buffer sizes > 2.
          Niall Pemberton made changes -
          Fix Version/s 2.1 [ 12315445 ]
          Niall Pemberton made changes -
          Fix Version/s 2.1 [ 12315445 ]
          Fix Version/s 2.0 [ 12312961 ]
          Henri Yandell made changes -
          Fix Version/s 1.4 [ 12312101 ]
          Denis Zhdanov made changes -
          Attachment ReplaceFilterInputStream.java [ 12421203 ]
          Attachment ReplaceFilterInputStreamTest.java [ 12421204 ]
          Hide
          Denis Zhdanov added a comment -

          There was a bug in the case of long 'from' rule that is partially matched by the input data and 'slow' underlying stream.

          It's fixed now and the test suit is populated.

          Show
          Denis Zhdanov added a comment - There was a bug in the case of long 'from' rule that is partially matched by the input data and 'slow' underlying stream. It's fixed now and the test suit is populated.
          Denis Zhdanov made changes -
          Attachment ReplaceFilterInputStreamTest.java [ 12419737 ]
          Denis Zhdanov made changes -
          Attachment ReplaceFilterInputStream.java [ 12419736 ]
          Hide
          Denis Zhdanov added a comment -

          New files are reattached.

          Show
          Denis Zhdanov added a comment - New files are reattached.
          Denis Zhdanov made changes -
          Attachment ReplaceFilterInputStream.java [ 12419736 ]
          Attachment ReplaceFilterInputStreamTest.java [ 12419737 ]
          Hide
          Denis Zhdanov added a comment -

          Buffer overflow error is fixed. Test suit is expanded accordingly.

          Show
          Denis Zhdanov added a comment - Buffer overflow error is fixed. Test suit is expanded accordingly.
          Denis Zhdanov made changes -
          Attachment ReplaceFilterInputStreamTest.java [ 12418758 ]
          Denis Zhdanov made changes -
          Attachment ReplaceFilterInputStream.java [ 12418757 ]
          Hide
          haruhiko nishi added a comment -

          When I started making my version of the search/replace InputStream, the one I posted above, I made it to subclass of FilterInputStream and implemented such as

              public ByteArrayReplaceInputStream(InputStream in,PatternList list) throws IOException {
                  super(in);
                  if(in==null)
                      throw new IllegalArgumentException("Input stream may not be null");
                  this.itr=list.iterator();
                  parse();
              }
              
              private void parse() throws IOException {
                  byte[] srcbuf=new byte[1024];
                  byte[] tmpbuf=new byte[1024];
                  int size;
                  while((size=in.read(tmpbuf))!=-1){
                      int newcount=count+size;
                      if(newcount>srcbuf.length){
                          byte newbuf[]=new byte[Math.max(srcbuf.length<<1,newcount)];
                          System.arraycopy(srcbuf,0,newbuf,0,count);
                          srcbuf=newbuf;
                      }
                      System.arraycopy(tmpbuf,0,srcbuf,count,size);
                      count=newcount;
                  }
                  buf=new byte[count];
                  ByteBuffer byteBuffer=ByteBuffer.wrap(srcbuf,0,count);
                  count=0;
                  itr.readLock();
                  try{
                      match(byteBuffer,itr.next(),0,byteBuffer.limit());
                  }finally{
                      itr.readUnlock();
                  }
              }
          

          but then I though the creation of this buffer at parse() method is so redundant and I decided to change it to be like ByteArrayInputStream.

          Since your method does not buffer up the target byte sequence, I was hoping to get some help from you.

          Show
          haruhiko nishi added a comment - When I started making my version of the search/replace InputStream, the one I posted above, I made it to subclass of FilterInputStream and implemented such as public ByteArrayReplaceInputStream(InputStream in,PatternList list) throws IOException { super (in); if (in== null ) throw new IllegalArgumentException( "Input stream may not be null " ); this .itr=list.iterator(); parse(); } private void parse() throws IOException { byte [] srcbuf= new byte [1024]; byte [] tmpbuf= new byte [1024]; int size; while ((size=in.read(tmpbuf))!=-1){ int newcount=count+size; if (newcount>srcbuf.length){ byte newbuf[]= new byte [ Math .max(srcbuf.length<<1,newcount)]; System .arraycopy(srcbuf,0,newbuf,0,count); srcbuf=newbuf; } System .arraycopy(tmpbuf,0,srcbuf,count,size); count=newcount; } buf= new byte [count]; ByteBuffer byteBuffer=ByteBuffer.wrap(srcbuf,0,count); count=0; itr.readLock(); try { match(byteBuffer,itr.next(),0,byteBuffer.limit()); } finally { itr.readUnlock(); } } but then I though the creation of this buffer at parse() method is so redundant and I decided to change it to be like ByteArrayInputStream. Since your method does not buffer up the target byte sequence, I was hoping to get some help from you.
          Hide
          Denis Zhdanov added a comment -

          Haruhiko,

          I believe that stream-based replacement and replacement on the static data are two different tasks from the user point of view.

          However, you can see that if you have a stream-based replacement you can reuse it with the static data as well (at least my implementation) because java streams widely use Decorator pattern and replacement stream works on any stream that IS-A InputStream. I.e. you can do the following to perform replacements against particular byte array:

          1. Create ByteArrayInputStream for the target array;
          2. Create new ReplaceFilterInputStream that wraps the ByteArrayInputStream created before;
          3. Create new ByteArrayOutputStream;
          4. Filter the data from the ByteArrayInputStream to the ByteArrayOutputStream;
            The only drawback of such approach is that another byte array will be created, i.e. replacements don't occur in-place.

          Regards, Denis.

          Show
          Denis Zhdanov added a comment - Haruhiko, I believe that stream-based replacement and replacement on the static data are two different tasks from the user point of view. However, you can see that if you have a stream-based replacement you can reuse it with the static data as well (at least my implementation) because java streams widely use Decorator pattern and replacement stream works on any stream that IS-A InputStream . I.e. you can do the following to perform replacements against particular byte array: Create ByteArrayInputStream for the target array; Create new ReplaceFilterInputStream that wraps the ByteArrayInputStream created before; Create new ByteArrayOutputStream ; Filter the data from the ByteArrayInputStream to the ByteArrayOutputStream ; The only drawback of such approach is that another byte array will be created, i.e. replacements don't occur in-place. Regards, Denis.
          Hide
          haruhiko nishi added a comment -

          Do you think the search/replace needs to happen on the fly,without buffering up entire page?
          ByteArrayInputStream is not really a stream processing, but it still is part of io package and named InputStream.
          byte[] processing relatively quick and I guess you can have the search target buffered so long as the JVM does not thow OutoutMemory Error.

          Show
          haruhiko nishi added a comment - Do you think the search/replace needs to happen on the fly,without buffering up entire page? ByteArrayInputStream is not really a stream processing, but it still is part of io package and named InputStream. byte[] processing relatively quick and I guess you can have the search target buffered so long as the JVM does not thow OutoutMemory Error.
          Denis Zhdanov made changes -
          Description It seems convenient to have a FilterInputStream that allows to apply predefined repalcement rules against the read data.
          For example we may want to configure the following replacements:
          {noformat}
          {1, } -> {7, 8}
          {1} -> {9}
          {3, 2} -> {}
          {noformat}
          and apply them to the input like
          {noformat}
          {4, 3, 2, 1, 2, 1, 3}
          {noformat}
          in order to get a result like
          {noformat}
          {4, 7, 8, 9, 3}
          {noformat}

          I created the class that allows to do that and attached it to this ticket. Unit test class at junit4 format is attached as well.

          So, the task is to review the provided classes, consider if it's worth to add them to commons-io distribution and perform the inclusion in the case of possible result.
          It seems convenient to have a FilterInputStream that allows to apply predefined repalcement rules against the read data.
          For example we may want to configure the following replacements:
          {noformat}
          {1, 2} -> {7, 8}
          {1} -> {9}
          {3, 2} -> {}
          {noformat}
          and apply them to the input like
          {noformat}
          {4, 3, 2, 1, 2, 1, 3}
          {noformat}
          in order to get a result like
          {noformat}
          {4, 7, 8, 9, 3}
          {noformat}

          I created the class that allows to do that and attached it to this ticket. Unit test class at junit4 format is attached as well.

          So, the task is to review the provided classes, consider if it's worth to add them to commons-io distribution and perform the inclusion in the case of possible result.
          Hide
          Denis Zhdanov added a comment -

          Haruhiko,

          Thanks for the example, I was able to reproduce the problem at the local environment. I'll investigate the problem and provide the fixed stream class and expanded unit test here.

          Regards, Denis

          Show
          Denis Zhdanov added a comment - Haruhiko, Thanks for the example, I was able to reproduce the problem at the local environment. I'll investigate the problem and provide the fixed stream class and expanded unit test here. Regards, Denis
          Hide
          haruhiko nishi added a comment -

          I appreciate your comment.

          About the exception the "test.html" is http://www.yahoo.com is top page copied into a file named test.html.
          It happened intermittently, does not seem to happen always.

          OOPs the Exception has some package name shown....

          The reason we need to use such InputStream is that we need to do search/replace on entire page of web sites.

          Show
          haruhiko nishi added a comment - I appreciate your comment. About the exception the "test.html" is http://www.yahoo.com is top page copied into a file named test.html. It happened intermittently, does not seem to happen always. OOPs the Exception has some package name shown.... The reason we need to use such InputStream is that we need to do search/replace on entire page of web sites.
          Hide
          Denis Zhdanov added a comment -

          Haruhiko,

          Ok, I see now your question. I'll check your code if I have free time (not sure when that can be done however).

          About the 'surround replacements' - no, my class doesn't support that because it's not possible to determine if such a replacement occurs without variable-sized buffering that, in turn, doesn't correlate with streaming processing.

          About the exception - you probably found a bug that evaded from me. Can you send me your input file ("test.html" from your example)?

          Regards, Denis

          Show
          Denis Zhdanov added a comment - Haruhiko, Ok, I see now your question. I'll check your code if I have free time (not sure when that can be done however). About the 'surround replacements' - no, my class doesn't support that because it's not possible to determine if such a replacement occurs without variable-sized buffering that, in turn, doesn't correlate with streaming processing. About the exception - you probably found a bug that evaded from me. Can you send me your input file ( "test.html" from your example)? Regards, Denis
          Hide
          haruhiko nishi added a comment - - edited

          I've tried ReplaceFilterInputStream and got the following Exception.

          all I did was

          public class TestMain {
              public static void main(String[] args)throws Exception{
                  Map<byte[],byte[]> map=new HashMap<byte[],byte[]>();
                  map.put("yahoo".getBytes(),"google".getBytes());
                  ReplaceFilterInputStream in=new ReplaceFilterInputStream(new FileInputStream("test.html"),map);
                  int bytesRead;
                  FileOutputStream fout=new FileOutputStream("test_mod_1.html");
                  byte[] tmp=new byte[4096];
                  while((bytesRead=in.read(tmp,0,tmp.length))!=-1)
                      fout.write(tmp,0,bytesRead);
                  in.close();
                  fout.close();
              }
          }
          

          then I get

          Exception in thread "main" java.io.IOException: Push back buffer is full
          at java.io.PushbackInputStream.unread(PushbackInputStream.java:215)
          at jp.co.alibaba.util.template.reverse.groovy.ReplaceFilterInputStream.replaceWithExpand(ReplaceFilterInputStream.java:467)
          at jp.co.alibaba.util.template.reverse.groovy.ReplaceFilterInputStream.tryToReplace(ReplaceFilterInputStream.java:405)
          at jp.co.alibaba.util.template.reverse.groovy.ReplaceFilterInputStream.doRead(ReplaceFilterInputStream.java:340)
          at jp.co.alibaba.util.template.reverse.groovy.ReplaceFilterInputStream.read(ReplaceFilterInputStream.java:239)
          at TestMain.main(TestMain.java:30)
          at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
          at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
          at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
          at java.lang.reflect.Method.invoke(Method.java:597)
          at com.intellij.rt.execution.application.AppMain.main(AppMain.java:90)

          Show
          haruhiko nishi added a comment - - edited I've tried ReplaceFilterInputStream and got the following Exception. all I did was public class TestMain { public static void main( String [] args) throws Exception{ Map< byte [], byte []> map= new HashMap< byte [], byte []>(); map.put( "yahoo" .getBytes(), "google" .getBytes()); ReplaceFilterInputStream in= new ReplaceFilterInputStream( new FileInputStream( "test.html" ),map); int bytesRead; FileOutputStream fout= new FileOutputStream( "test_mod_1.html" ); byte [] tmp= new byte [4096]; while ((bytesRead=in.read(tmp,0,tmp.length))!=-1) fout.write(tmp,0,bytesRead); in.close(); fout.close(); } } then I get Exception in thread "main" java.io.IOException: Push back buffer is full at java.io.PushbackInputStream.unread(PushbackInputStream.java:215) at jp.co.alibaba.util.template.reverse.groovy.ReplaceFilterInputStream.replaceWithExpand(ReplaceFilterInputStream.java:467) at jp.co.alibaba.util.template.reverse.groovy.ReplaceFilterInputStream.tryToReplace(ReplaceFilterInputStream.java:405) at jp.co.alibaba.util.template.reverse.groovy.ReplaceFilterInputStream.doRead(ReplaceFilterInputStream.java:340) at jp.co.alibaba.util.template.reverse.groovy.ReplaceFilterInputStream.read(ReplaceFilterInputStream.java:239) at TestMain.main(TestMain.java:30) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:90)
          Hide
          haruhiko nishi added a comment - - edited

          There is no problem at all.
          It's just some other way to solve search replace stream. Mine just needs to buffer up everything before the parse occurs.
          I was just wondering if you know how not to buffer everything and filter the stream in my code, I appreciate it very much.
          I'll try using yours. Does yours support surround match?

          P.S.
          I'll just delete the source code.

          Show
          haruhiko nishi added a comment - - edited There is no problem at all. It's just some other way to solve search replace stream. Mine just needs to buffer up everything before the parse occurs. I was just wondering if you know how not to buffer everything and filter the stream in my code, I appreciate it very much. I'll try using yours. Does yours support surround match? P.S. I'll just delete the source code.
          Hide
          Denis Zhdanov added a comment -

          > Hi I'm also working on a Stream that can handle pattern replacement. (It's more like byte[] wrapper rather than actual
          > streaming process. If you can figure out how to do this in real streaming. please help.)

          Haruhiko,

          I'm fraid I don't understand the problem. The class I attached to this ticket (ReplaceFilterInputStream) already does the job, so you can just check the source and use it to perform replacements in streaming mode. Did you mean somthing else? May be some questions on its contract or implementation details?

          Regards, Denis

          P.S. it's better to attach the sources to the ticket instead of plcaing their content directly at the comments since its produces inconvenient comments reading.

          Show
          Denis Zhdanov added a comment - > Hi I'm also working on a Stream that can handle pattern replacement. (It's more like byte[] wrapper rather than actual > streaming process. If you can figure out how to do this in real streaming. please help.) Haruhiko, I'm fraid I don't understand the problem. The class I attached to this ticket (ReplaceFilterInputStream) already does the job, so you can just check the source and use it to perform replacements in streaming mode. Did you mean somthing else? May be some questions on its contract or implementation details? Regards, Denis P.S. it's better to attach the sources to the ticket instead of plcaing their content directly at the comments since its produces inconvenient comments reading.
          Hide
          haruhiko nishi added a comment - - edited

          Hi I'm also working on a Stream that can handle pattern replacement. (It's more like byte[] wrapper rather than actual
          streaming process. If you can figure out how to do this in real streaming. please help.)

          usage: Integer.MAX_VALUE is max occurrence of the pattern that is provided.
          PatternList list=new PatternList();
          list.add("src=\"".getBytes("UTF-8"),"\"".getBytes("UTF-8"),new SrcHrefReplacer("UTF8"),Integer.MAX_VALUE);
          list.add("href=\"".getBytes("UTF-8"),"\"".getBytes("UTF-8"),new SrcHrefReplacer("UTF-8"),Integer.MAX_VALUE);

          ByteArrayReplaceInputStream in=new ByteArrayReplaceInputStream(someByteArray,list);
          int bytesRead;
          byte[] buf=new byte[4096];
          while((bytesRead=in.read(buf,0,buf.length))!=-1)
          out.write(buf,0,bytesRead);
          in.close();

          ByteArrayReplaceInputStream.java
          public class ByteArrayReplaceInputStream extends InputStream {
              private byte[] buf;
              private int count;
              private PatternList.PatternListIterator itr;
              private Map<PatternList.PatternEntry,Integer> counter=new IdentityHashMap<PatternList.PatternEntry,Integer>();
              private int pos;
              private int mark=0;
              
              public ByteArrayReplaceInputStream(byte[] buf,PatternList list) {
                  this.itr=list.iterator();
                  this.buf=new byte[buf.length];
                  ByteBuffer byteBuffer=ByteBuffer.wrap(buf,0,buf.length);
                  itr.readLock();
                  try{
                      match(byteBuffer,itr.next(),0,byteBuffer.limit());
                  }finally{
                      itr.readUnlock();
                  }
              }
          
              private void write(ByteBuffer buffer){
                  byte[] byteArray=new byte[buffer.remaining()];
                  buffer.get(byteArray);
                  write(byteArray);
              }
          
              private void write(byte[] b){
                  int newcount=count+b.length;
                  if(newcount > buf.length){
                      byte newbuf[]=new byte[Math.max(buf.length<<1,newcount)];
                      System.arraycopy(buf,0,newbuf,0,count);
                      buf=newbuf;
                  }
                  System.arraycopy(b,0,buf,count,b.length);
                  count=newcount;
              }
          
              private final void match(ByteBuffer src,PatternList.PatternEntry pattern,int start,int end) {
                  if(pattern.isSingle())
                      single(src,pattern,start,end);
                  else
                      around(src,pattern,start,end);
              }
          
              private int count(PatternList.PatternEntry pattern){
                  Integer count=counter.get(pattern);
                  if(count==null)
                      count=counter.put(pattern,1);
                  else
                      count=counter.put(pattern,count+1);
                  return count==null ? 0 : count;
              }
          
              private boolean isPatternValid(PatternList.PatternEntry pattern){
                  Integer count=counter.get(pattern);
                  if(count==null)
                      return 0<=pattern.getMaxOccurence();
                  else
                      return count<pattern.getMaxOccurence();
              }
          
              private final void around(ByteBuffer src,PatternList.PatternEntry patternEntry,int start,int end){
                  if(start<0 || start>end || end>src.limit())
                      throw new IndexOutOfBoundsException("start:"+start+" end:"+end);
                  int pos=start;
                  int limit_org=end;
                  int mark=0;
                  boolean flag=false;
                  int j=pos;
                  Pattern pattern=patternEntry.getPattern();
                  while(isPatternValid(patternEntry) && j<=limit_org-pattern.length()) {
                      boolean found=true;
                      int cur=j;
                      for(int i=0;i<pattern.length();i++){
                          if(src.get(cur+i)!=pattern.get(i)){
                              found=false;
                              break;
                          }
                      }
                      if(found){
                          if(flag=!flag){
                              j=mark=cur+pattern.length();
                              pattern=pattern.swap();
                          }else{
                              src.position(pos).limit(mark);
                              j=cur+pattern.length();
                              if(itr.hasNext())
                                  match(src,itr.next(),pos,src.limit());
                              else
                                  write(src);
                              pattern=pattern.swap();
                              src.position(mark).limit(cur);
                              if(src.remaining()>0){
                                  ByteBuffer target=src.slice();
                                  int size=target.remaining();
                                  byte[] array=new byte[size];
                                  target.get(array);
                                  array=patternEntry.replace(count(patternEntry),array);
                                  write(array);
                              }
                              pos=src.position(src.limit()).position();
                              src.limit(limit_org);
                          }
                      }
                      if(!found){
                          int k=cur+pattern.length();
                          if(k>=src.limit())
                              break;
                          j +=pattern.skip(src.get(k) & 0xff);
                      }
                  }
                  src.position(pos);
                  if(itr.hasNext())
                      match(src,itr.next(),pos,src.limit());
                  else
                      write(src);
                  itr.previous();
              }
          
              private void single(ByteBuffer src,PatternList.PatternEntry patternEntry,int start,int end){
                  if(start<0 || start>end || end>src.limit())
                      throw new IndexOutOfBoundsException("start:"+start+" end:"+end);
                  int pos=start;
                  int limit_org=end;
                  int j=pos;
          
                  Pattern pattern=patternEntry.getPattern();
                  while(isPatternValid(patternEntry) && j<=limit_org-pattern.length()) {
                      boolean found=true;
                      int cur=j;
                      for(int i=0;i<pattern.length();i++){
                          if(src.get(cur+i)!=pattern.get(i)){
                              found=false;
                              break;
                          }
                      }
                      if(found){
                          src.position(pos).limit(cur);
                          j=cur+pattern.length();
                          if(itr.hasNext())
                              single(src,itr.next(),pos,src.limit());
                          else
                              write(src);
                          src.position(cur).limit(j);
                          if(src.remaining()>0){
                              ByteBuffer target=src.slice();
                              int size=target.remaining();
                              byte[] array=new byte[size];
                              target.get(array);
                              write(patternEntry.replace(count(patternEntry),array));
                          }
                          pos=src.position(src.limit()).position();
                          src.limit(limit_org);
                      }
                      if(!found){
                          int k=cur+pattern.length();
                          if(k>=src.limit())
                              break;
                          j +=pattern.skip(src.get(k) & 0xff);
                      }
                  }
                  src.position(pos);
                  if(itr.hasNext())
                      single(src,itr.next(),pos,src.limit());
                  else
                      write(src);
                  itr.previous();
              }
              
              @Override
              public synchronized int read(){
          	    return (pos < count) ? (buf[pos++] & 0xff) : -1;
              }
          
              @Override
              public synchronized int read(byte b[], int off, int len){
          	    if (b == null)
          	        throw new NullPointerException();
          	    else if(off < 0 || len < 0 || len > b.length - off)
          	        throw new IndexOutOfBoundsException();
          	    if (pos >= count)
          	        return -1;
          	    if(pos + len > count)
                      len=count - pos;
          	    if (len <= 0)
          	        return 0;
          	    System.arraycopy(buf, pos, b, off, len);
          	    pos += len;
          	    return len;
              }
          
              public synchronized long skip(long n){
                  if(pos +n>count){
                      n=count - pos;
                  }
                  if(n<0)
                      return 0;
                  pos +=n;
                  return n;
              }
          
              public synchronized int avaiable(){
                  return count - pos;
              }
          
              public boolean markSupported(){
                  return true;
              }
          
              public synchronized void mark(int readAheadLimit){
                  mark=pos;
              }
          
              public synchronized void reset(){
                  pos=mark;
              }
          
              public void close() throws IOException{
                  
              }
          
          }
          
          Pattern.java
          abstract class Pattern {
              abstract int length();
              abstract int get(int pos);
              abstract int skip(int value);
              abstract Pattern swap();
          }
          
          PatternList.java
          public class PatternList {
              private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
              private Lock readLock=lock.readLock();
              private Lock writeLock=lock.writeLock();
              private PatternEntry head=new PatternEntry(null,-1,null,null,null);
          
              public PatternList(){
                  head.next=head.previous=head;
              }
          
              public void add(byte[] bBegin,byte[] bEnd,PatternReplacer handler, int maxOccurence){
                  add(new BeginPattern(new PatternTable(bBegin),new PatternTable(bEnd)),maxOccurence,handler);
              }
          
              public void add(byte[] p,PatternReplacer handler, int maxOccurence){
                  add(new SinglePattern(new PatternTable(p)),maxOccurence,handler);
              }
          
              PatternListIterator iterator(){
                  return new PatternListIterator(head.previous);
              }
          
              private void add(Pattern pattern,int maxOccurence,PatternReplacer handler){
                  writeLock.lock();
                  try{
                      for(PatternEntry tmp=head;;tmp=tmp.next)
                          if(tmp.pattern==null||tmp.compareTo(pattern)<=0){
                              PatternEntry newEntry=new PatternEntry(handler,maxOccurence,pattern,tmp,tmp.previous);
                              newEntry.previous.next=newEntry;
                              newEntry.next.previous=newEntry;
                              if(tmp==head)
                                  head=newEntry;
                              break;
                          }
                  }finally{
                      writeLock.unlock();
                  }
              }
          
              class PatternListIterator{
                  private PatternEntry copyHead;
                  
                  PatternListIterator(PatternEntry head){
                      this.copyHead=head;
                  }
          
                  public PatternEntry next(){
                      copyHead=copyHead.next;
                      return copyHead;
                  }
          
                  public PatternEntry previous(){
                      copyHead=copyHead.previous;
                      return copyHead;
                  }
          
                  public boolean hasNext(){
                      return copyHead.next.pattern!=null;
                  }
          
                  void readLock(){
                      readLock.lock();
                  }
          
                  void readUnlock(){
                      readLock.unlock();
                  }
          
              }
          
              static class PatternEntry implements Comparable<Pattern>{
                  Pattern pattern;
                  PatternEntry next;
                  PatternEntry previous;
                  PatternReplacer handler;
                  int maxOccurence;
          
                  private PatternEntry(PatternReplacer handler,int maxOccurence,Pattern element,PatternEntry next, PatternEntry previous){
                      this.handler=handler;
                      this.pattern=element;
                      this.next=next;
                      this.previous=previous;
                      this.maxOccurence=maxOccurence;
                  }
          
                  byte[] replace(int pos,byte[] match){
                      return handler.replace(pos,match);
                  }
          
                  int getMaxOccurence() {
                      return maxOccurence;
                  }
          
                  Pattern getPattern(){
                      return pattern;
                  }
                   
                  public int compareTo(Pattern other) {
          
                      if(this.pattern instanceof SinglePattern && other instanceof BeginPattern)
                          return -1;
                      else if(this.pattern instanceof BeginPattern && other instanceof SinglePattern)
                          return 1;
                      else{
                          return ((ComparablePattern)this.pattern).size-((ComparablePattern)other).size;
                      }
                  }
          
                  boolean isSingle(){
                      return pattern instanceof SinglePattern;
                  }
          
          
              }
          
              private static abstract class ComparablePattern extends Pattern{
                  int size;
                  ComparablePattern(int size){
                      this.size=size;
                  }
              }
          
              private static class SinglePattern extends ComparablePattern {
                  PatternTable pattern;
          
                  private SinglePattern(PatternTable pattern){
                      super(pattern.length());
                      this.pattern=pattern;
                  }
          
                  @Override
                  int length() {
                      return pattern.length();
                  }
          
                  @Override
                  int get(int pos) {
                      return pattern.get(pos);
                  }
          
                  @Override
                  int skip(int value) {
                      return pattern.skip(value);
                  }
          
                  @Override
                  protected Pattern swap() {
                      throw new UnsupportedOperationException("single pattern cannot be swapped.");
                  }
              }
          
              private static class EndPattern extends Pattern {
                  PatternTable end;
                  Pattern nextMatcher;
          
                  private EndPattern(PatternTable end,Pattern nextMatcher){
                      this.end=end;
                      this.nextMatcher=nextMatcher;
                  }
          
                  @Override
                  int length() {
                      return end.length();
                  }
          
                  @Override
                  int get(int pos) {
                      return end.get(pos);
                  }
          
                  @Override
                  int skip(int value) {
                      return end.skip(value);
                  }
          
                  @Override
                  protected Pattern swap() {
                      return nextMatcher;
                  }
              }
          
              private static class BeginPattern extends ComparablePattern{
                  PatternTable begin;
                  Pattern nextMatcher;
          
                  private BeginPattern(PatternTable begin,PatternTable end){
                      super(begin.length()+end.length());
                      this.begin=begin;
                      nextMatcher=new EndPattern(end,this);
                  }
          
                  @Override
                  int length() {
                      return begin.length();
                  }
          
                  @Override
                  int get(int pos) {
                      return begin.get(pos);
                  }
          
                  @Override
                  int skip(int value) {
                      return begin.skip(value);
                  }
          
                  @Override
                  protected Pattern swap() {
                      return nextMatcher;
                  }
              }
          
              private static class PatternTable {
                  private final int[] pattern;
                  private int[] skip;
          
                  PatternTable(byte[] target){
                      pattern=new int[target.length];
                      for(int i=0;i<target.length;i++)
                          pattern[i]=target[i] & 0xff;
                      this.skip=getSkipArray(target);
                  }
          
                  int length(){
                      return pattern.length;
                  }
          
                  int get(int i){
                      return pattern[i];
                  }
          
                  int skip(int i){
                      return skip[i];
                  }
          
                  private static int[] getSkipArray(byte[] pattern){
                      int[] skip=new int[256];
                      int i;
                      for(i=0; i<skip.length;i++)
                          skip[i]=pattern.length+1;
                      for(i=0; i<pattern.length;i++)
                          skip[pattern[i] & 0xff]=pattern.length -i;
                      return skip;
                  }
              }
          }
          
          PatternReplacer.java
          public interface PatternReplacer {
              byte[] replace(int pos,final byte[] matched);
              // pos is zero based position of this patern's occurrence in the byte[]
          }
          
          StringPatternReplacer.java
          public abstract class StringPatternReplacer implements PatternReplacer{
              private String charset;
          
              protected StringPatternReplacer(String charset){
                  this.charset=charset;
              }
              
              public final byte[] replace(int pos, byte[] matched) {
                  String replaced;
                  try {
                      replaced = replace(pos,new String(matched,charset));
                      if(replaced!=null)
                          return replaced.getBytes(charset);
                  } catch (UnsupportedEncodingException e) {
          
                  }
                  return null;
              }
          
              protected abstract String replace(int pos,String matched);
          
          }
          
          SrcHrefReplacer.java
          public class SrcHrefReplacer extends StringPatternReplacer {
              
              public SrcHrefReplacer(String charset){
                  super(charset);
              }
          
              public String replace(int pos, String matched) {
                if(matched.endsWith(".jpg")||matched.endsWith(".gif"))
                    return matched;
                 if(matched.contains("somedomain.com"))
                     return matched;
                return matched.replaceAll("somedomain.com","mydomain.com");
          
              }
          }
          
          Show
          haruhiko nishi added a comment - - edited Hi I'm also working on a Stream that can handle pattern replacement. (It's more like byte[] wrapper rather than actual streaming process. If you can figure out how to do this in real streaming. please help.) usage: Integer.MAX_VALUE is max occurrence of the pattern that is provided. PatternList list=new PatternList(); list.add("src=\"".getBytes("UTF-8"),"\"".getBytes("UTF-8"),new SrcHrefReplacer("UTF8"),Integer.MAX_VALUE); list.add("href=\"".getBytes("UTF-8"),"\"".getBytes("UTF-8"),new SrcHrefReplacer("UTF-8"),Integer.MAX_VALUE); ByteArrayReplaceInputStream in=new ByteArrayReplaceInputStream(someByteArray,list); int bytesRead; byte[] buf=new byte [4096] ; while((bytesRead=in.read(buf,0,buf.length))!=-1) out.write(buf,0,bytesRead); in.close(); ByteArrayReplaceInputStream.java public class ByteArrayReplaceInputStream extends InputStream { private byte [] buf; private int count; private PatternList.PatternListIterator itr; private Map<PatternList.PatternEntry, Integer > counter= new IdentityHashMap<PatternList.PatternEntry, Integer >(); private int pos; private int mark=0; public ByteArrayReplaceInputStream( byte [] buf,PatternList list) { this .itr=list.iterator(); this .buf= new byte [buf.length]; ByteBuffer byteBuffer=ByteBuffer.wrap(buf,0,buf.length); itr.readLock(); try { match(byteBuffer,itr.next(),0,byteBuffer.limit()); } finally { itr.readUnlock(); } } private void write(ByteBuffer buffer){ byte [] byteArray= new byte [buffer.remaining()]; buffer.get(byteArray); write(byteArray); } private void write( byte [] b){ int newcount=count+b.length; if (newcount > buf.length){ byte newbuf[]= new byte [ Math .max(buf.length<<1,newcount)]; System .arraycopy(buf,0,newbuf,0,count); buf=newbuf; } System .arraycopy(b,0,buf,count,b.length); count=newcount; } private final void match(ByteBuffer src,PatternList.PatternEntry pattern, int start, int end) { if (pattern.isSingle()) single(src,pattern,start,end); else around(src,pattern,start,end); } private int count(PatternList.PatternEntry pattern){ Integer count=counter.get(pattern); if (count== null ) count=counter.put(pattern,1); else count=counter.put(pattern,count+1); return count== null ? 0 : count; } private boolean isPatternValid(PatternList.PatternEntry pattern){ Integer count=counter.get(pattern); if (count== null ) return 0<=pattern.getMaxOccurence(); else return count<pattern.getMaxOccurence(); } private final void around(ByteBuffer src,PatternList.PatternEntry patternEntry, int start, int end){ if (start<0 || start>end || end>src.limit()) throw new IndexOutOfBoundsException( "start:" +start+ " end:" +end); int pos=start; int limit_org=end; int mark=0; boolean flag= false ; int j=pos; Pattern pattern=patternEntry.getPattern(); while (isPatternValid(patternEntry) && j<=limit_org-pattern.length()) { boolean found= true ; int cur=j; for ( int i=0;i<pattern.length();i++){ if (src.get(cur+i)!=pattern.get(i)){ found= false ; break ; } } if (found){ if (flag=!flag){ j=mark=cur+pattern.length(); pattern=pattern.swap(); } else { src.position(pos).limit(mark); j=cur+pattern.length(); if (itr.hasNext()) match(src,itr.next(),pos,src.limit()); else write(src); pattern=pattern.swap(); src.position(mark).limit(cur); if (src.remaining()>0){ ByteBuffer target=src.slice(); int size=target.remaining(); byte [] array= new byte [size]; target.get(array); array=patternEntry.replace(count(patternEntry),array); write(array); } pos=src.position(src.limit()).position(); src.limit(limit_org); } } if (!found){ int k=cur+pattern.length(); if (k>=src.limit()) break ; j +=pattern.skip(src.get(k) & 0xff); } } src.position(pos); if (itr.hasNext()) match(src,itr.next(),pos,src.limit()); else write(src); itr.previous(); } private void single(ByteBuffer src,PatternList.PatternEntry patternEntry, int start, int end){ if (start<0 || start>end || end>src.limit()) throw new IndexOutOfBoundsException( "start:" +start+ " end:" +end); int pos=start; int limit_org=end; int j=pos; Pattern pattern=patternEntry.getPattern(); while (isPatternValid(patternEntry) && j<=limit_org-pattern.length()) { boolean found= true ; int cur=j; for ( int i=0;i<pattern.length();i++){ if (src.get(cur+i)!=pattern.get(i)){ found= false ; break ; } } if (found){ src.position(pos).limit(cur); j=cur+pattern.length(); if (itr.hasNext()) single(src,itr.next(),pos,src.limit()); else write(src); src.position(cur).limit(j); if (src.remaining()>0){ ByteBuffer target=src.slice(); int size=target.remaining(); byte [] array= new byte [size]; target.get(array); write(patternEntry.replace(count(patternEntry),array)); } pos=src.position(src.limit()).position(); src.limit(limit_org); } if (!found){ int k=cur+pattern.length(); if (k>=src.limit()) break ; j +=pattern.skip(src.get(k) & 0xff); } } src.position(pos); if (itr.hasNext()) single(src,itr.next(),pos,src.limit()); else write(src); itr.previous(); } @Override public synchronized int read(){ return (pos < count) ? (buf[pos++] & 0xff) : -1; } @Override public synchronized int read( byte b[], int off, int len){ if (b == null ) throw new NullPointerException(); else if (off < 0 || len < 0 || len > b.length - off) throw new IndexOutOfBoundsException(); if (pos >= count) return -1; if (pos + len > count) len=count - pos; if (len <= 0) return 0; System .arraycopy(buf, pos, b, off, len); pos += len; return len; } public synchronized long skip( long n){ if (pos +n>count){ n=count - pos; } if (n<0) return 0; pos +=n; return n; } public synchronized int avaiable(){ return count - pos; } public boolean markSupported(){ return true ; } public synchronized void mark( int readAheadLimit){ mark=pos; } public synchronized void reset(){ pos=mark; } public void close() throws IOException{ } } Pattern.java abstract class Pattern { abstract int length(); abstract int get( int pos); abstract int skip( int value); abstract Pattern swap(); } PatternList.java public class PatternList { private ReentrantReadWriteLock lock= new ReentrantReadWriteLock(); private Lock readLock=lock.readLock(); private Lock writeLock=lock.writeLock(); private PatternEntry head= new PatternEntry( null ,-1, null , null , null ); public PatternList(){ head.next=head.previous=head; } public void add( byte [] bBegin, byte [] bEnd,PatternReplacer handler, int maxOccurence){ add( new BeginPattern( new PatternTable(bBegin), new PatternTable(bEnd)),maxOccurence,handler); } public void add( byte [] p,PatternReplacer handler, int maxOccurence){ add( new SinglePattern( new PatternTable(p)),maxOccurence,handler); } PatternListIterator iterator(){ return new PatternListIterator(head.previous); } private void add(Pattern pattern, int maxOccurence,PatternReplacer handler){ writeLock.lock(); try { for (PatternEntry tmp=head;;tmp=tmp.next) if (tmp.pattern== null ||tmp.compareTo(pattern)<=0){ PatternEntry newEntry= new PatternEntry(handler,maxOccurence,pattern,tmp,tmp.previous); newEntry.previous.next=newEntry; newEntry.next.previous=newEntry; if (tmp==head) head=newEntry; break ; } } finally { writeLock.unlock(); } } class PatternListIterator{ private PatternEntry copyHead; PatternListIterator(PatternEntry head){ this .copyHead=head; } public PatternEntry next(){ copyHead=copyHead.next; return copyHead; } public PatternEntry previous(){ copyHead=copyHead.previous; return copyHead; } public boolean hasNext(){ return copyHead.next.pattern!= null ; } void readLock(){ readLock.lock(); } void readUnlock(){ readLock.unlock(); } } static class PatternEntry implements Comparable<Pattern>{ Pattern pattern; PatternEntry next; PatternEntry previous; PatternReplacer handler; int maxOccurence; private PatternEntry(PatternReplacer handler, int maxOccurence,Pattern element,PatternEntry next, PatternEntry previous){ this .handler=handler; this .pattern=element; this .next=next; this .previous=previous; this .maxOccurence=maxOccurence; } byte [] replace( int pos, byte [] match){ return handler.replace(pos,match); } int getMaxOccurence() { return maxOccurence; } Pattern getPattern(){ return pattern; } public int compareTo(Pattern other) { if ( this .pattern instanceof SinglePattern && other instanceof BeginPattern) return -1; else if ( this .pattern instanceof BeginPattern && other instanceof SinglePattern) return 1; else { return ((ComparablePattern) this .pattern).size-((ComparablePattern)other).size; } } boolean isSingle(){ return pattern instanceof SinglePattern; } } private static abstract class ComparablePattern extends Pattern{ int size; ComparablePattern( int size){ this .size=size; } } private static class SinglePattern extends ComparablePattern { PatternTable pattern; private SinglePattern(PatternTable pattern){ super (pattern.length()); this .pattern=pattern; } @Override int length() { return pattern.length(); } @Override int get( int pos) { return pattern.get(pos); } @Override int skip( int value) { return pattern.skip(value); } @Override protected Pattern swap() { throw new UnsupportedOperationException( "single pattern cannot be swapped." ); } } private static class EndPattern extends Pattern { PatternTable end; Pattern nextMatcher; private EndPattern(PatternTable end,Pattern nextMatcher){ this .end=end; this .nextMatcher=nextMatcher; } @Override int length() { return end.length(); } @Override int get( int pos) { return end.get(pos); } @Override int skip( int value) { return end.skip(value); } @Override protected Pattern swap() { return nextMatcher; } } private static class BeginPattern extends ComparablePattern{ PatternTable begin; Pattern nextMatcher; private BeginPattern(PatternTable begin,PatternTable end){ super (begin.length()+end.length()); this .begin=begin; nextMatcher= new EndPattern(end, this ); } @Override int length() { return begin.length(); } @Override int get( int pos) { return begin.get(pos); } @Override int skip( int value) { return begin.skip(value); } @Override protected Pattern swap() { return nextMatcher; } } private static class PatternTable { private final int [] pattern; private int [] skip; PatternTable( byte [] target){ pattern= new int [target.length]; for ( int i=0;i<target.length;i++) pattern[i]=target[i] & 0xff; this .skip=getSkipArray(target); } int length(){ return pattern.length; } int get( int i){ return pattern[i]; } int skip( int i){ return skip[i]; } private static int [] getSkipArray( byte [] pattern){ int [] skip= new int [256]; int i; for (i=0; i<skip.length;i++) skip[i]=pattern.length+1; for (i=0; i<pattern.length;i++) skip[pattern[i] & 0xff]=pattern.length -i; return skip; } } } PatternReplacer.java public interface PatternReplacer { byte [] replace( int pos, final byte [] matched); // pos is zero based position of this patern's occurrence in the byte [] } StringPatternReplacer.java public abstract class StringPatternReplacer implements PatternReplacer{ private String charset; protected StringPatternReplacer( String charset){ this .charset=charset; } public final byte [] replace( int pos, byte [] matched) { String replaced; try { replaced = replace(pos, new String (matched,charset)); if (replaced!= null ) return replaced.getBytes(charset); } catch (UnsupportedEncodingException e) { } return null ; } protected abstract String replace( int pos, String matched); } SrcHrefReplacer.java public class SrcHrefReplacer extends StringPatternReplacer { public SrcHrefReplacer( String charset){ super (charset); } public String replace( int pos, String matched) { if (matched.endsWith( ".jpg" )||matched.endsWith( ".gif" )) return matched; if (matched.contains( "somedomain.com" )) return matched; return matched.replaceAll( "somedomain.com" , "mydomain.com" ); } }
          Denis Zhdanov made changes -
          Attachment ReplaceFilterInputStreamTest.java [ 12418758 ]
          Hide
          Denis Zhdanov added a comment - - edited

          Unit test class is attached

          Show
          Denis Zhdanov added a comment - - edited Unit test class is attached
          Denis Zhdanov made changes -
          Field Original Value New Value
          Attachment ReplaceFilterInputStream.java [ 12418757 ]
          Hide
          Denis Zhdanov added a comment - - edited

          Implementation class is attached

          Show
          Denis Zhdanov added a comment - - edited Implementation class is attached
          Denis Zhdanov created issue -

            People

            • Assignee:
              Unassigned
              Reporter:
              Denis Zhdanov
            • Votes:
              5 Vote for this issue
              Watchers:
              5 Start watching this issue

              Dates

              • Created:
                Updated:

                Development