/*
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.services.jcr.impl.storage.fs;

import org.exoplatform.services.jcr.JcrImplBaseTest;
import org.exoplatform.services.jcr.core.value.EditableBinaryValue;
import org.exoplatform.services.jcr.core.value.ReadableBinaryValue;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Calendar;
import java.util.Random;

import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.Session;

/**
 * Created by The eXo Platform SAS
 * 
 * @author <a href="mailto:peter.nedonosko@exoplatform.com.ua">Peter Nedonosko</a>
 * @version $Id: TestRandomValueIO.java 13062 2008-04-10 12:51:39Z pnedonosko $
 */
public class TestRandomValueIO extends JcrImplBaseTest
{

   private Node testRoot;

   private File testFile;

   @Override
   public void setUp() throws Exception
   {

      super.setUp();

      testRoot = root.addNode("binValueTest");
      session.save();

      if (testFile == null)
      {
         testFile = createBLOBTempFile(this.getClass().getSimpleName() + "_", 2 * 1024); // 2M
         testFile.deleteOnExit();
      }

      root.getNodes();
   }

   @Override
   protected void tearDown() throws Exception
   {
      testRoot.remove();
      session.save();

      super.tearDown();
   }

   public void testUpdateNew() throws Exception
   {

      // create property
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new FileInputStream(testFile));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();

      String update1String = "update#1";

      long pos = 1024 * 1024;

      // update
      exv.update(new ByteArrayInputStream(update1String.getBytes()), update1String.length(), pos);

      // transient, before the save
      try
      {

         // the value obtained by getXXX must be same as on setProperty()
         compareStream(new FileInputStream(testFile), testRoot.getProperty(pname).getStream());

         // apply edited ExtendetValue to the Property
         p.setValue(exv);

         compareStream(new ByteArrayInputStream(update1String.getBytes()), testRoot.getProperty(pname).getStream(), 0,
            pos, update1String.length());

         testRoot.save();

         // persisted, after the save
         compareStream(new ByteArrayInputStream(update1String.getBytes()), testRoot.getProperty(pname).getStream(), 0,
            pos, update1String.length());

      }
      catch (CompareStreamException e)
      {
         e.printStackTrace();
         fail(e.getMessage());
      }
   }

   public void testUpdateExisted() throws Exception
   {

      // create property
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new FileInputStream(testFile));

      testRoot.save();

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();

      String update1String = "update#1";

      long pos = 1024 * 1024;

      // update
      exv.update(new ByteArrayInputStream(update1String.getBytes()), update1String.length(), pos);

      // transient, before the save
      try
      {

         // the value obtained by getXXX must be same as on setProperty()
         compareStream(new FileInputStream(testFile), testRoot.getProperty(pname).getStream());

         // apply edited ExtendetValue to the Property
         p.setValue(exv);

         compareStream(new ByteArrayInputStream(update1String.getBytes()), testRoot.getProperty(pname).getStream(), 0,
            pos, update1String.length());

         testRoot.save();

         // persisted, after the save
         compareStream(new ByteArrayInputStream(update1String.getBytes()), testRoot.getProperty(pname).getStream(), 0,
            pos, update1String.length());

      }
      catch (CompareStreamException e)
      {
         e.printStackTrace();
         fail(e.getMessage());
      }
   }

   public void testNodeSetPropertyExisted() throws Exception
   {

      // create property
      String pname = "file@" + testFile.getName();
      testRoot.setProperty(pname, new FileInputStream(testFile));

      testRoot.save();

      // get the property value
      EditableBinaryValue exv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();

      String update1String = "update#1";

      long pos = 1024 * 1024;

      // update length bytes from the stream stating from the position in existed Value data
      exv.update(new ByteArrayInputStream(update1String.getBytes()), update1String.length(), pos);

      // transient, before the save
      try
      {

         // the value obtained by getXXX must be same as on setProperty()
         compareStream(new FileInputStream(testFile), testRoot.getProperty(pname).getStream());

         // apply edited ExtendetValue to the Property
         testRoot.setProperty(pname, exv);

         compareStream(new ByteArrayInputStream(update1String.getBytes()), testRoot.getProperty(pname).getStream(), 0,
            pos, update1String.length());

         // save
         testRoot.save();

         // persisted, after the save
         compareStream(new ByteArrayInputStream(update1String.getBytes()), testRoot.getProperty(pname).getStream(), 0,
            pos, update1String.length());

      }
      catch (CompareStreamException e)
      {
         e.printStackTrace();
         fail(e.getMessage());
      }
   }

   public void testUpdate_SameObject() throws Exception
   {

      // create property
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new FileInputStream(testFile));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();

      String update1String = "update#1";

      long pos1 = 1024 * 1024;

      // update 1
      exv.update(new ByteArrayInputStream(update1String.getBytes()), update1String.length(), pos1);

      // apply to the Property and save
      p.setValue(exv);
      testRoot.save();

      String update2String = "UPDATE#2";

      long pos2 = (1024 * 1024) + 5;

      // update 2
      exv.update(new ByteArrayInputStream(update2String.getBytes()), update2String.length(), pos2);
      // apply to the Property
      p.setValue(exv);

      // check the content from the first updated char to the last char of second
      // update

      String updateString = update1String.substring(0, 5) + update2String;

      // transient, before the save
      try
      {
         compareStream(new ByteArrayInputStream(updateString.getBytes()), testRoot.getProperty(pname).getStream(), 0,
            pos1, updateString.length());

         testRoot.save();

         // persisted, after the save
         compareStream(new ByteArrayInputStream(updateString.getBytes()), testRoot.getProperty(pname).getStream(), 0,
            pos1, updateString.length());

      }
      catch (CompareStreamException e)
      {
         fail(e.getMessage());
      }
   }

   public void _testUpdate_SameObjectAcrossSessions() throws Exception
   {

      // create property
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new FileInputStream(testFile));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();

      String update1String = "update#1";

      long pos1 = 1024 * 1024;

      // update 1
      exv.update(new ByteArrayInputStream(update1String.getBytes()), update1String.length(), pos1);

      // apply to the Property in another session and save
      Session s1 = repository.login(credentials);

      try
      {
         Node troot = s1.getRootNode().getNode(testRoot.getName());

         byte[] s1Content = "__string_stream__".getBytes();
         p = troot.setProperty(pname, new ByteArrayInputStream(s1Content));

         BufferedInputStream exvStream = new BufferedInputStream(exv.getStream());
         // fill the buffer from exv
         exvStream.mark((int)exv.getLength() + 1);
         while ((exvStream.read(new byte[2048])) >= 0)
         {
         }
         exvStream.reset();

         p.setValue(exv);
         troot.save();

         // check if we has the exv value
         compareStream(exvStream, troot.getProperty(pname).getStream(), 0, pos1, update1String.length());

         String update2String = "UPDATE#2";

         long pos2 = (1024 * 1024) + 5;

         // update 2
         exv.update(new ByteArrayInputStream(update2String.getBytes()), update2String.length(), pos2);
         // apply to the Property
         p.setValue(exv);

         // check the content from the first updated char to the last char of
         // second update

         String updateString = update1String.substring(0, 5) + update2String;

         // transient, before the save
         compareStream(new ByteArrayInputStream(updateString.getBytes()), troot.getProperty(pname).getStream(), 0,
            pos1, updateString.length());

         troot.save();

         // persisted, after the save
         compareStream(new ByteArrayInputStream(updateString.getBytes()), troot.getProperty(pname).getStream(), 0,
            pos1, updateString.length());

         // from first session
         compareStream(new ByteArrayInputStream(updateString.getBytes()), testRoot.getProperty(pname).getStream(), 0,
            pos1, updateString.length());

      }
      catch (CompareStreamException e)
      {
         e.printStackTrace();
         fail(e.getMessage());
      }
      finally
      {
         s1.logout();
      }
   }

   public void testRollback() throws Exception
   {

      // create property
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new FileInputStream(testFile));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();

      String update1String = "update#1";

      long pos = 1024 * 1024;

      // update
      exv.update(new ByteArrayInputStream(update1String.getBytes()), update1String.length(), pos);
      // apply to the Property and save
      p.setValue(exv);
      testRoot.save();

      // test if the rollbacked value isn't saved

      String update2String = "UPDATE#2";

      long pos2 = (1024 * 1024) + 5;

      // update 2
      exv.update(new ByteArrayInputStream(update2String.getBytes()), update2String.length(), pos2);
      // apply to the Property
      p.setValue(exv);

      testRoot.refresh(false); // rollback

      // check the content
      // transient, before the save

      try
      {
         compareStream(new ByteArrayInputStream(update1String.getBytes()), testRoot.getProperty(pname).getStream(), 0,
            pos, update1String.length());

         testRoot.save();

         // persisted, after the save
         compareStream(new ByteArrayInputStream(update1String.getBytes()), testRoot.getProperty(pname).getStream(), 0,
            pos, update1String.length());

      }
      catch (CompareStreamException e)
      {
         fail(e.getMessage());
      }
   }

   public void testUpdate_BigToBigValue() throws Exception
   {

      // create property
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new FileInputStream(testFile));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();
      String update1String = "update#1";
      long pos = 3 * 1024 * 1024;

      // update
      try
      {

         // exv.setLength(pos + 1);
         // assertEquals("Value data length must be increased ", pos + 1, exv.getLength());

         exv.update(new ByteArrayInputStream(update1String.getBytes()), update1String.length(), pos);

         // apply to the Property and save
         p.setValue(exv);
         testRoot.save();

         // test after save
         // first 2M of stream data must be same as on setProperty()
         compareStream(new FileInputStream(testFile), testRoot.getProperty(pname).getStream(), 0, 0, testFile.length());

         compareStream(new ByteArrayInputStream(update1String.getBytes()), testRoot.getProperty(pname).getStream(), 0,
            pos, update1String.length());
      }
      catch (CompareStreamException e)
      {
         fail(e.getMessage());
      }
   }

   /**
    * Case of out=of-memory during port bytes to file
    * */
   public void testUpdate_SmallToBigValue() throws Exception
   {

      // create property
      String pname = "file@" + testFile.getName();
      String content = "short message";
      Property p = testRoot.setProperty(pname, new ByteArrayInputStream(content.getBytes()));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();
      long pos = exv.getLength() + 1024 * 1024 * 5;

      long fmem = Runtime.getRuntime().freeMemory();

      exv.update(new FileInputStream(testFile), testFile.length(), pos);

      long fmemAfter = Runtime.getRuntime().freeMemory();

      if ((fmemAfter - fmem) >= (pos + testFile.length()))
         log.warn("Free memory must not be increased on value of the new Value size but does. Was " + fmem
            + " current " + fmemAfter);

      assertEquals("Value data length must be increased ", pos + testFile.length(), exv.getLength());

      // apply to the Property and save
      p.setValue(exv);
      testRoot.save();

      EditableBinaryValue newexv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();

      assertEquals("Value data length must be increased ", pos + testFile.length(), newexv.getLength());

      // update inside the big data
      int npos = 5;
      newexv.update(new FileInputStream(testFile), testFile.length(), npos);

      // apply to the Property and save
      p.setValue(newexv);
      testRoot.save();

      byte[] buff = new byte[npos];
      testRoot.getProperty(pname).getStream().read(buff);

      assertEquals("Value content is wrong ", content.substring(0, npos), new String(buff));

      compareStream(new FileInputStream(testFile), testRoot.getProperty(pname).getStream(), 0, npos, testFile.length());
   }

   public void testAddLength_BigValue() throws Exception
   {

      // create property
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new FileInputStream(testFile));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();
      long pos = 3 * 1024 * 1024;

      exv.setLength(pos);

      assertEquals("Value data length must be increased ", pos, exv.getLength());

      // apply to the Property and save
      p.setValue(exv);
      testRoot.save();

      EditableBinaryValue newexv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();
      assertEquals("Value data length must be increased ", pos, newexv.getLength());
   }

   public void testTruncateLength_BigValue() throws Exception
   {

      // create property
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new FileInputStream(testFile));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();
      long pos = 1024 * 1024;

      exv.setLength(pos);

      assertEquals("Value data length must be decreased ", pos, exv.getLength());

      // apply to the Property and save
      p.setValue(exv);
      testRoot.save();

      EditableBinaryValue newexv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();
      assertEquals("Value data length must be decreased ", pos, newexv.getLength());
   }

   public void testAddLength_SmallValue() throws Exception
   {

      // create property
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new ByteArrayInputStream("short message".getBytes()));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();
      long pos = exv.getLength() + 20;

      exv.setLength(pos);

      assertEquals("Value data length must be increased ", pos, exv.getLength());

      // apply to the Property and save
      p.setValue(exv);
      testRoot.save();

      EditableBinaryValue newexv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();
      assertEquals("Value data length must be increased ", pos, newexv.getLength());
   }

   public void testAddLength_SmallToBigValue() throws Exception
   {

      // create property
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new ByteArrayInputStream("short message".getBytes()));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();
      long pos = exv.getLength() + 1024 * 1024 * 5;

      long fmem = Runtime.getRuntime().freeMemory();

      exv.setLength(pos);

      long fmemAfter = Runtime.getRuntime().freeMemory();

      if ((fmemAfter - fmem) >= pos)
         log.warn("Free memory must not be increased on value of the new Value size but does. Was " + fmem
            + " current " + fmemAfter);

      assertEquals("Value data length must be increased ", pos, exv.getLength());

      // apply to the Property and save
      p.setValue(exv);
      testRoot.save();

      EditableBinaryValue newexv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();
      assertEquals("Value data length must be increased ", pos, newexv.getLength());
   }

   public void testAddLength_SmallToBigValue_Persistent() throws Exception
   {

      // create property
      String pname = "file@" + testFile.getName();
      testRoot.setProperty(pname, new ByteArrayInputStream(new byte[]{}));

      testRoot.save();

      // change after save
      EditableBinaryValue newexv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();
      long pos = newexv.getLength() + 1024 * 1024 * 5;

      long tmem = Runtime.getRuntime().totalMemory();

      newexv.setLength(pos);

      // apply to the Property and save
      testRoot.getProperty(pname).setValue(newexv);

      long tmemAfter = Runtime.getRuntime().totalMemory();

      if ((tmemAfter - tmem) >= pos)
         log.warn("JVM total memory should not be increased on size of the new Value but does. Was " + tmem
            + " current " + tmemAfter);

      assertEquals("Value data length must be increased ", pos, newexv.getLength());

      newexv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();
      assertEquals("Value data length must be increased ", pos, newexv.getLength());

      // save new size
      testRoot.save();
      newexv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();
      assertEquals("Value data length must be increased ", pos, newexv.getLength());
   }

   public void testTruncateLength_BigToSmallValue() throws Exception
   {

      // create property
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new FileInputStream(testFile));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();
      long pos = exv.getLength() - (testFile.length() - 20);

      exv.setLength(pos);

      assertEquals("Value data length must be decreased ", pos, exv.getLength());

      // apply to the Property and save
      p.setValue(exv);
      testRoot.save();

      EditableBinaryValue newexv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();
      assertEquals("Value data length must be decreased ", pos, newexv.getLength());
   }

   public void testTruncateLength_SmallValue() throws Exception
   {

      // create property
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new ByteArrayInputStream("short message".getBytes()));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();
      long pos = exv.getLength() - 5;

      exv.setLength(pos);

      assertEquals("Value data length must be decreased ", pos, exv.getLength());

      // apply to the Property and save
      p.setValue(exv);
      testRoot.save();

      EditableBinaryValue newexv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();
      assertEquals("Value data length must be decreased ", pos, newexv.getLength());
   }

   /**
    * Test the case when the property has small (zero) initial length and it will be updated with
    * large size content, i.e. the property must be spooled.
    * 
    * The case differs from testAddLength_SmallToBigValue_Persistent in Workspace cache usage, the
    * property will be obtained from persistent storage here.
    * 
    * @throws Exception
    */
   public void testAddLength_SmallToBigValue_NTFile() throws Exception
   {
      // create property

      String type = "nt:file";
      String name = "foo.txt";

      Node createdNodeRef = testRoot.addNode(name, type);
      Node dataNode = createdNodeRef.addNode("jcr:content", "nt:resource");

      dataNode.setProperty("jcr:mimeType", "text/plain");
      dataNode.setProperty("jcr:lastModified", Calendar.getInstance());
      dataNode.setProperty("jcr:data", new ByteArrayInputStream(new byte[]{}));

      testRoot.save();

      EditableBinaryValue exv =
         (EditableBinaryValue)testRoot.getNode(name).getNode("jcr:content").getProperty("jcr:data").getValue();
      long pos = 1024 * 1024 * 5;

      exv.setLength(pos);

      assertEquals("Value data length must be increased ", pos, exv.getLength());

      // apply to the Property and save
      testRoot.getNode(name).getNode("jcr:content").getProperty("jcr:data").setValue(exv);
      testRoot.save();
   }

   // ----- read operation ------

   public void testRead() throws Exception
   {

      // create property, prepare the data to be readed
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new FileInputStream(testFile));
      testRoot.save();

      // read partial
      ReadableBinaryValue exv = (ReadableBinaryValue)testRoot.getProperty(pname).getValue();

      ByteArrayOutputStream baos = new ByteArrayOutputStream();

      int len = 200 * 1024;
      int pos = 900 * 1024;
      long res = exv.read(baos, len, pos);

      assertEquals("Wrong bytes count have been read", len, res);

      compareStream(new FileInputStream(testFile), new ByteArrayInputStream(baos.toByteArray()), pos, 0, len);
   }

   public void testReadBigValue() throws Exception
   {

      // create property, prepare the data to be readed
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new FileInputStream(testFile));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();

      String update1String = "update#1";

      long pos = 1024 * 1024;

      exv.update(new ByteArrayInputStream(update1String.getBytes()), update1String.length(), pos);

      p.setValue(exv);
      testRoot.save();

      // read partial
      exv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();

      ByteArrayOutputStream baos = new ByteArrayOutputStream();

      long res = exv.read(baos, 5, pos + 2); // read 'date#' bytes

      String expected = update1String.substring(2, 7);

      assertEquals(expected.length() + " bytes must be read", expected.length(), res);

      assertEquals("Readed content not equals to expected", expected, new String(baos.toByteArray()));

      // next read
      baos = new ByteArrayOutputStream();
      res = exv.read(baos, 3, pos + 1); // read 'pda' bytes

      expected = update1String.substring(1, 4);

      assertEquals(expected.length() + " bytes must be read", expected.length(), res);

      assertEquals("Readed content not equals to expected", expected, new String(baos.toByteArray()));
   }

   public void testReadBigValueEOF() throws Exception
   {

      // create property, prepare the data to be readed
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new FileInputStream(testFile));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();

      String update1String = "update#1";

      long pos = testFile.length();

      exv.update(new ByteArrayInputStream(update1String.getBytes()), update1String.length(), pos);

      p.setValue(exv);
      testRoot.save();

      // read partial greater the value size
      exv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();

      ByteArrayOutputStream baos = new ByteArrayOutputStream();

      long res = exv.read(baos, 1024, pos + 2); // read 'date#1' bytes

      String expected = update1String.substring(2);

      assertEquals(expected.length() + " bytes must be read", expected.length(), res);

      assertEquals("Readed content not equals to expected", expected, new String(baos.toByteArray()));
   }

   public void testReadSmallValue() throws Exception
   {

      // create property, prepare the data to be readed
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new ByteArrayInputStream("short message".getBytes()));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();

      String update1String = "update#1";

      long pos = 6;

      exv.update(new ByteArrayInputStream(update1String.getBytes()), update1String.length(), pos);

      p.setValue(exv);
      testRoot.save();

      // read partial
      exv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();

      ByteArrayOutputStream baos = new ByteArrayOutputStream();

      long res = exv.read(baos, 5, pos + 2); // read 'date#' bytes

      String expected = update1String.substring(2, 7);

      assertEquals(expected.length() + " bytes must be read", expected.length(), res);

      assertEquals("Readed content not equals to expected", expected, new String(baos.toByteArray()));
   }

   public void testReadSmallValueEOF() throws Exception
   {

      // create property, prepare the data to be readed
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new ByteArrayInputStream("short message".getBytes()));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();

      String update1String = "update#1";

      long pos = exv.getLength() + 1;

      exv.update(new ByteArrayInputStream(update1String.getBytes()), update1String.length(), pos);

      p.setValue(exv);
      testRoot.save();

      // read partial greater the value size
      exv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();

      ByteArrayOutputStream baos = new ByteArrayOutputStream();

      long res = exv.read(baos, 1024, pos + 2); // read 'date#1' bytes

      String expected = update1String.substring(2);

      assertEquals(expected.length() + " bytes must be read", expected.length(), res);

      assertEquals("Readed content not equals to expected", expected, new String(baos.toByteArray()));
   }

   public void testReadZeroLengthSmallValue() throws Exception
   {

      // create property, prepare the data to be readed
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new ByteArrayInputStream("short message".getBytes()));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();

      // read zero bytes
      exv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();

      ByteArrayOutputStream baos = new ByteArrayOutputStream();

      // from begin
      long res = exv.read(baos, 0, 0);

      assertEquals("Zero bytes must be read", 0, res);

      assertEquals("Zero bytes must be read", 0, baos.size());

      // from middle
      baos = new ByteArrayOutputStream();

      res = exv.read(baos, 0, 5);

      assertEquals("Zero bytes must be read", 0, res);

      assertEquals("Zero bytes must be read", 0, baos.size());

      // out of end
      try
      {
         exv.read(baos, 0, exv.getLength() + 10);
         fail("The out-of-range exception should be thrown");
      }
      catch (IOException e)
      {
         // ok
      }
   }

   public void testReadZeroLengthBigValue() throws Exception
   {

      // create property, prepare the data to be readed
      String pname = "file@" + testFile.getName();
      Property p = testRoot.setProperty(pname, new FileInputStream(testFile));

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();

      // read zero bytes
      exv = (EditableBinaryValue)testRoot.getProperty(pname).getValue();

      ByteArrayOutputStream baos = new ByteArrayOutputStream();

      // from begin
      long res = exv.read(baos, 0, 0);

      assertEquals("Zero bytes must be read", 0, res);

      assertEquals("Zero bytes must be read", 0, baos.size());

      // from middle
      baos = new ByteArrayOutputStream();

      res = exv.read(baos, 0, 1024 * 1024);

      assertEquals("Zero bytes must be read", 0, res);

      assertEquals("Zero bytes must be read", 0, baos.size());

      // out of end
      try
      {
         exv.read(baos, 0, exv.getLength() + 1024);
         fail("The out-of-range exception should be thrown");
      }
      catch (IOException e)
      {
         // ok
      }
   }

   /**
    * There is a test of next situation: non truncated zero value updated by data (size in
    * maxIOBuffSize byte buffer size bounds)
    */
   public void testUpdateSmallValues() throws Exception
   {
      // create property
      String pname = "jcr:data";

      Property p = testRoot.setProperty(pname, new ByteArrayInputStream(new byte[]{}));

      testRoot.save();

      EditableBinaryValue exv = (EditableBinaryValue)p.getValue();

      long pos = 0;

      int size = 61440;

      byte[] data = new byte[size];
      Random random = new Random();

      random.nextBytes(data);

      // update
      exv.update(new ByteArrayInputStream(data), size, pos);

      // transient, before the save
      try
      {

         // the value obtained by getXXX must be same as on setProperty()
         compareStream(new ByteArrayInputStream(new byte[]{}), testRoot.getProperty(pname).getStream());
         p.setValue(exv);

         compareStream(new ByteArrayInputStream(data), testRoot.getProperty(pname).getStream(), 0, pos, size);

         testRoot.save();

         // persisted, after the save
         compareStream(new ByteArrayInputStream(data), testRoot.getProperty(pname).getStream(), 0, pos, size);

      }
      catch (CompareStreamException e)
      {
         e.printStackTrace();
         fail(e.getMessage());
      }
   }

}
