Skip to content

Commit

Permalink
Provides a CachingOuptutStream and a CachingWriter (#184)
Browse files Browse the repository at this point in the history
Provides a CachingOuptutStream and a CachingWriter
  • Loading branch information
gnodet committed Apr 26, 2022
1 parent 0808fe3 commit 49773f1
Show file tree
Hide file tree
Showing 4 changed files with 525 additions and 0 deletions.
175 changes: 175 additions & 0 deletions src/main/java/org/codehaus/plexus/util/io/CachingOutputStream.java
@@ -0,0 +1,175 @@
package org.codehaus.plexus.util.io;

/*
* Copyright The Codehaus Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.Objects;

/**
* Caching OutputStream to avoid overwriting a file with
* the same content.
*/
public class CachingOutputStream extends OutputStream
{
private final Path path;
private FileChannel channel;
private ByteBuffer readBuffer;
private ByteBuffer writeBuffer;
private boolean modified;

public CachingOutputStream( File path ) throws IOException
{
this( Objects.requireNonNull( path ).toPath() );
}

public CachingOutputStream( Path path ) throws IOException
{
this( path, 32 * 1024 );
}

public CachingOutputStream( Path path, int bufferSize ) throws IOException
{
this.path = Objects.requireNonNull( path );
this.channel = FileChannel.open( path,
StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE );
this.readBuffer = ByteBuffer.allocate( bufferSize );
this.writeBuffer = ByteBuffer.allocate( bufferSize );
}

@Override
public void write( int b ) throws IOException
{
if ( writeBuffer.remaining() < 1 )
{
( ( Buffer ) writeBuffer ).flip();
flushBuffer( writeBuffer );
( ( Buffer ) writeBuffer ).clear();
}
writeBuffer.put( ( byte ) b );
}

@Override
public void write( byte[] b ) throws IOException
{
write( b, 0, b.length );
}

@Override
public void write( byte[] b, int off, int len ) throws IOException
{
if ( writeBuffer.remaining() < len )
{
( ( Buffer ) writeBuffer ).flip();
flushBuffer( writeBuffer );
( ( Buffer ) writeBuffer ).clear();
}
int capacity = writeBuffer.capacity();
while ( len >= capacity )
{
flushBuffer( ByteBuffer.wrap( b, off, capacity ) );
off += capacity;
len -= capacity;
}
if ( len > 0 )
{
writeBuffer.put( b, off, len );
}
}

@Override
public void flush() throws IOException
{
( ( Buffer ) writeBuffer ).flip();
flushBuffer( writeBuffer );
( ( Buffer ) writeBuffer ).clear();
super.flush();
}

private void flushBuffer( ByteBuffer writeBuffer ) throws IOException
{
if ( modified )
{
channel.write( writeBuffer );
}
else
{
int len = writeBuffer.remaining();
ByteBuffer readBuffer;
if ( this.readBuffer.capacity() >= len )
{
readBuffer = this.readBuffer;
( ( Buffer ) readBuffer ).clear();
}
else
{
readBuffer = ByteBuffer.allocate( len );
}
while ( len > 0 )
{
int read = channel.read( readBuffer );
if ( read <= 0 )
{
modified = true;
channel.position( channel.position() - readBuffer.position() );
channel.write( writeBuffer );
return;
}
len -= read;
}
( ( Buffer ) readBuffer ).flip();
if ( readBuffer.compareTo( writeBuffer ) != 0 )
{
modified = true;
channel.position( channel.position() - readBuffer.remaining() );
channel.write( writeBuffer );
}
}
}

@Override
public void close() throws IOException
{
flush();
long position = channel.position();
if ( position != channel.size() )
{
if ( !modified )
{
FileTime now = FileTime.from( Instant.now() );
Files.setLastModifiedTime( path, now );
modified = true;
}
channel.truncate( position );
}
channel.close();
}

public boolean isModified()
{
return modified;
}
}
62 changes: 62 additions & 0 deletions src/main/java/org/codehaus/plexus/util/io/CachingWriter.java
@@ -0,0 +1,62 @@
package org.codehaus.plexus.util.io;

/*
* Copyright The Codehaus Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Objects;

/**
* Caching Writer to avoid overwriting a file with
* the same content.
*/
public class CachingWriter extends OutputStreamWriter
{
private final CachingOutputStream cos;

public CachingWriter( File path, Charset charset ) throws IOException
{
this( Objects.requireNonNull( path ).toPath(), charset );
}

public CachingWriter( Path path, Charset charset ) throws IOException
{
this( path, charset, 32 * 1024 );
}

public CachingWriter( Path path, Charset charset, int bufferSize ) throws IOException
{
this( new CachingOutputStream( path, bufferSize ), charset );
}

private CachingWriter( CachingOutputStream outputStream, Charset charset ) throws IOException
{
super( outputStream, charset );
this.cos = outputStream;
}

public boolean isModified()
{
return cos.isModified();
}
}
145 changes: 145 additions & 0 deletions src/test/java/org/codehaus/plexus/util/io/CachingOutputStreamTest.java
@@ -0,0 +1,145 @@
package org.codehaus.plexus.util.io;

/*
* Copyright The Codehaus Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.util.Objects;

import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;

public class CachingOutputStreamTest
{

Path tempDir;
Path checkLastModified;
FileTime lm;

@Before
public void setup() throws IOException
{
Path dir = Paths.get( "target/io" );
Files.createDirectories( dir );
tempDir = Files.createTempDirectory( dir, "temp-" );
checkLastModified = tempDir.resolve( ".check" );
Files.newOutputStream( checkLastModified ).close();
lm = Files.getLastModifiedTime( checkLastModified );
}

private void waitLastModified() throws IOException, InterruptedException
{
while ( true )
{
Files.newOutputStream( checkLastModified ).close();
FileTime nlm = Files.getLastModifiedTime( checkLastModified );
if ( !Objects.equals( nlm, lm ) )
{
lm = nlm;
break;
}
Thread.sleep( 10 );
}
}

@Test
public void testWriteNoExistingFile() throws IOException, InterruptedException
{
byte[] data = "Hello world!".getBytes( StandardCharsets.UTF_8 );
Path path = tempDir.resolve( "file.txt" );
assertFalse( Files.exists( path ) );

try ( CachingOutputStream cos = new CachingOutputStream( path, 4 ) )
{
cos.write( data );
}
assertTrue( Files.exists( path ) );
byte[] read = Files.readAllBytes( path );
assertArrayEquals( data, read );
FileTime modified = Files.getLastModifiedTime( path );

waitLastModified();

try ( CachingOutputStream cos = new CachingOutputStream( path, 4 ) )
{
cos.write( data );
}
assertTrue( Files.exists( path ) );
read = Files.readAllBytes( path );
assertArrayEquals( data, read );
FileTime newModified = Files.getLastModifiedTime( path );
assertEquals( modified, newModified );
modified = newModified;

waitLastModified();

// write longer data
data = "Good morning!".getBytes( StandardCharsets.UTF_8 );
try ( CachingOutputStream cos = new CachingOutputStream( path, 4 ) )
{
cos.write( data );
}
assertTrue( Files.exists( path ) );
read = Files.readAllBytes( path );
assertArrayEquals( data, read );
newModified = Files.getLastModifiedTime( path );
assertNotEquals( modified, newModified );
modified = newModified;

waitLastModified();

// different data same size
data = "Good mornong!".getBytes( StandardCharsets.UTF_8 );
try ( CachingOutputStream cos = new CachingOutputStream( path, 4 ) )
{
cos.write( data );
}
assertTrue( Files.exists( path ) );
read = Files.readAllBytes( path );
assertArrayEquals( data, read );
newModified = Files.getLastModifiedTime( path );
assertNotEquals( modified, newModified );
modified = newModified;

waitLastModified();

// same data but shorter
data = "Good mornon".getBytes( StandardCharsets.UTF_8 );
try ( CachingOutputStream cos = new CachingOutputStream( path, 4 ) )
{
cos.write( data );
}
assertTrue( Files.exists( path ) );
read = Files.readAllBytes( path );
assertArrayEquals( data, read );
newModified = Files.getLastModifiedTime( path );
assertNotEquals( modified, newModified );
modified = newModified;
}

}

0 comments on commit 49773f1

Please sign in to comment.