Skip to content

Commit 49773f1

Browse files
authored
Provides a CachingOuptutStream and a CachingWriter (#184)
Provides a CachingOuptutStream and a CachingWriter
1 parent 0808fe3 commit 49773f1

File tree

4 files changed

+525
-0
lines changed

4 files changed

+525
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package org.codehaus.plexus.util.io;
2+
3+
/*
4+
* Copyright The Codehaus Foundation.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.io.OutputStream;
22+
import java.nio.Buffer;
23+
import java.nio.ByteBuffer;
24+
import java.nio.channels.FileChannel;
25+
import java.nio.file.Files;
26+
import java.nio.file.Path;
27+
import java.nio.file.StandardOpenOption;
28+
import java.nio.file.attribute.FileTime;
29+
import java.time.Instant;
30+
import java.util.Objects;
31+
32+
/**
33+
* Caching OutputStream to avoid overwriting a file with
34+
* the same content.
35+
*/
36+
public class CachingOutputStream extends OutputStream
37+
{
38+
private final Path path;
39+
private FileChannel channel;
40+
private ByteBuffer readBuffer;
41+
private ByteBuffer writeBuffer;
42+
private boolean modified;
43+
44+
public CachingOutputStream( File path ) throws IOException
45+
{
46+
this( Objects.requireNonNull( path ).toPath() );
47+
}
48+
49+
public CachingOutputStream( Path path ) throws IOException
50+
{
51+
this( path, 32 * 1024 );
52+
}
53+
54+
public CachingOutputStream( Path path, int bufferSize ) throws IOException
55+
{
56+
this.path = Objects.requireNonNull( path );
57+
this.channel = FileChannel.open( path,
58+
StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE );
59+
this.readBuffer = ByteBuffer.allocate( bufferSize );
60+
this.writeBuffer = ByteBuffer.allocate( bufferSize );
61+
}
62+
63+
@Override
64+
public void write( int b ) throws IOException
65+
{
66+
if ( writeBuffer.remaining() < 1 )
67+
{
68+
( ( Buffer ) writeBuffer ).flip();
69+
flushBuffer( writeBuffer );
70+
( ( Buffer ) writeBuffer ).clear();
71+
}
72+
writeBuffer.put( ( byte ) b );
73+
}
74+
75+
@Override
76+
public void write( byte[] b ) throws IOException
77+
{
78+
write( b, 0, b.length );
79+
}
80+
81+
@Override
82+
public void write( byte[] b, int off, int len ) throws IOException
83+
{
84+
if ( writeBuffer.remaining() < len )
85+
{
86+
( ( Buffer ) writeBuffer ).flip();
87+
flushBuffer( writeBuffer );
88+
( ( Buffer ) writeBuffer ).clear();
89+
}
90+
int capacity = writeBuffer.capacity();
91+
while ( len >= capacity )
92+
{
93+
flushBuffer( ByteBuffer.wrap( b, off, capacity ) );
94+
off += capacity;
95+
len -= capacity;
96+
}
97+
if ( len > 0 )
98+
{
99+
writeBuffer.put( b, off, len );
100+
}
101+
}
102+
103+
@Override
104+
public void flush() throws IOException
105+
{
106+
( ( Buffer ) writeBuffer ).flip();
107+
flushBuffer( writeBuffer );
108+
( ( Buffer ) writeBuffer ).clear();
109+
super.flush();
110+
}
111+
112+
private void flushBuffer( ByteBuffer writeBuffer ) throws IOException
113+
{
114+
if ( modified )
115+
{
116+
channel.write( writeBuffer );
117+
}
118+
else
119+
{
120+
int len = writeBuffer.remaining();
121+
ByteBuffer readBuffer;
122+
if ( this.readBuffer.capacity() >= len )
123+
{
124+
readBuffer = this.readBuffer;
125+
( ( Buffer ) readBuffer ).clear();
126+
}
127+
else
128+
{
129+
readBuffer = ByteBuffer.allocate( len );
130+
}
131+
while ( len > 0 )
132+
{
133+
int read = channel.read( readBuffer );
134+
if ( read <= 0 )
135+
{
136+
modified = true;
137+
channel.position( channel.position() - readBuffer.position() );
138+
channel.write( writeBuffer );
139+
return;
140+
}
141+
len -= read;
142+
}
143+
( ( Buffer ) readBuffer ).flip();
144+
if ( readBuffer.compareTo( writeBuffer ) != 0 )
145+
{
146+
modified = true;
147+
channel.position( channel.position() - readBuffer.remaining() );
148+
channel.write( writeBuffer );
149+
}
150+
}
151+
}
152+
153+
@Override
154+
public void close() throws IOException
155+
{
156+
flush();
157+
long position = channel.position();
158+
if ( position != channel.size() )
159+
{
160+
if ( !modified )
161+
{
162+
FileTime now = FileTime.from( Instant.now() );
163+
Files.setLastModifiedTime( path, now );
164+
modified = true;
165+
}
166+
channel.truncate( position );
167+
}
168+
channel.close();
169+
}
170+
171+
public boolean isModified()
172+
{
173+
return modified;
174+
}
175+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.codehaus.plexus.util.io;
2+
3+
/*
4+
* Copyright The Codehaus Foundation.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.io.OutputStreamWriter;
22+
import java.io.StringWriter;
23+
import java.nio.charset.Charset;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.util.Arrays;
27+
import java.util.Objects;
28+
29+
/**
30+
* Caching Writer to avoid overwriting a file with
31+
* the same content.
32+
*/
33+
public class CachingWriter extends OutputStreamWriter
34+
{
35+
private final CachingOutputStream cos;
36+
37+
public CachingWriter( File path, Charset charset ) throws IOException
38+
{
39+
this( Objects.requireNonNull( path ).toPath(), charset );
40+
}
41+
42+
public CachingWriter( Path path, Charset charset ) throws IOException
43+
{
44+
this( path, charset, 32 * 1024 );
45+
}
46+
47+
public CachingWriter( Path path, Charset charset, int bufferSize ) throws IOException
48+
{
49+
this( new CachingOutputStream( path, bufferSize ), charset );
50+
}
51+
52+
private CachingWriter( CachingOutputStream outputStream, Charset charset ) throws IOException
53+
{
54+
super( outputStream, charset );
55+
this.cos = outputStream;
56+
}
57+
58+
public boolean isModified()
59+
{
60+
return cos.isModified();
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package org.codehaus.plexus.util.io;
2+
3+
/*
4+
* Copyright The Codehaus Foundation.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import java.io.IOException;
20+
import java.io.OutputStream;
21+
import java.nio.charset.StandardCharsets;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.nio.file.Paths;
25+
import java.nio.file.attribute.FileTime;
26+
import java.util.Objects;
27+
28+
import org.junit.Before;
29+
import org.junit.Test;
30+
31+
import static org.junit.Assert.assertArrayEquals;
32+
import static org.junit.Assert.assertEquals;
33+
import static org.junit.Assert.assertFalse;
34+
import static org.junit.Assert.assertNotEquals;
35+
import static org.junit.Assert.assertTrue;
36+
37+
public class CachingOutputStreamTest
38+
{
39+
40+
Path tempDir;
41+
Path checkLastModified;
42+
FileTime lm;
43+
44+
@Before
45+
public void setup() throws IOException
46+
{
47+
Path dir = Paths.get( "target/io" );
48+
Files.createDirectories( dir );
49+
tempDir = Files.createTempDirectory( dir, "temp-" );
50+
checkLastModified = tempDir.resolve( ".check" );
51+
Files.newOutputStream( checkLastModified ).close();
52+
lm = Files.getLastModifiedTime( checkLastModified );
53+
}
54+
55+
private void waitLastModified() throws IOException, InterruptedException
56+
{
57+
while ( true )
58+
{
59+
Files.newOutputStream( checkLastModified ).close();
60+
FileTime nlm = Files.getLastModifiedTime( checkLastModified );
61+
if ( !Objects.equals( nlm, lm ) )
62+
{
63+
lm = nlm;
64+
break;
65+
}
66+
Thread.sleep( 10 );
67+
}
68+
}
69+
70+
@Test
71+
public void testWriteNoExistingFile() throws IOException, InterruptedException
72+
{
73+
byte[] data = "Hello world!".getBytes( StandardCharsets.UTF_8 );
74+
Path path = tempDir.resolve( "file.txt" );
75+
assertFalse( Files.exists( path ) );
76+
77+
try ( CachingOutputStream cos = new CachingOutputStream( path, 4 ) )
78+
{
79+
cos.write( data );
80+
}
81+
assertTrue( Files.exists( path ) );
82+
byte[] read = Files.readAllBytes( path );
83+
assertArrayEquals( data, read );
84+
FileTime modified = Files.getLastModifiedTime( path );
85+
86+
waitLastModified();
87+
88+
try ( CachingOutputStream cos = new CachingOutputStream( path, 4 ) )
89+
{
90+
cos.write( data );
91+
}
92+
assertTrue( Files.exists( path ) );
93+
read = Files.readAllBytes( path );
94+
assertArrayEquals( data, read );
95+
FileTime newModified = Files.getLastModifiedTime( path );
96+
assertEquals( modified, newModified );
97+
modified = newModified;
98+
99+
waitLastModified();
100+
101+
// write longer data
102+
data = "Good morning!".getBytes( StandardCharsets.UTF_8 );
103+
try ( CachingOutputStream cos = new CachingOutputStream( path, 4 ) )
104+
{
105+
cos.write( data );
106+
}
107+
assertTrue( Files.exists( path ) );
108+
read = Files.readAllBytes( path );
109+
assertArrayEquals( data, read );
110+
newModified = Files.getLastModifiedTime( path );
111+
assertNotEquals( modified, newModified );
112+
modified = newModified;
113+
114+
waitLastModified();
115+
116+
// different data same size
117+
data = "Good mornong!".getBytes( StandardCharsets.UTF_8 );
118+
try ( CachingOutputStream cos = new CachingOutputStream( path, 4 ) )
119+
{
120+
cos.write( data );
121+
}
122+
assertTrue( Files.exists( path ) );
123+
read = Files.readAllBytes( path );
124+
assertArrayEquals( data, read );
125+
newModified = Files.getLastModifiedTime( path );
126+
assertNotEquals( modified, newModified );
127+
modified = newModified;
128+
129+
waitLastModified();
130+
131+
// same data but shorter
132+
data = "Good mornon".getBytes( StandardCharsets.UTF_8 );
133+
try ( CachingOutputStream cos = new CachingOutputStream( path, 4 ) )
134+
{
135+
cos.write( data );
136+
}
137+
assertTrue( Files.exists( path ) );
138+
read = Files.readAllBytes( path );
139+
assertArrayEquals( data, read );
140+
newModified = Files.getLastModifiedTime( path );
141+
assertNotEquals( modified, newModified );
142+
modified = newModified;
143+
}
144+
145+
}

0 commit comments

Comments
 (0)