diff --git a/src/main/java/org/codehaus/plexus/util/io/CachingOutputStream.java b/src/main/java/org/codehaus/plexus/util/io/CachingOutputStream.java
new file mode 100644
index 00000000..521d5373
--- /dev/null
+++ b/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;
+    }
+}
diff --git a/src/main/java/org/codehaus/plexus/util/io/CachingWriter.java b/src/main/java/org/codehaus/plexus/util/io/CachingWriter.java
new file mode 100644
index 00000000..23cc4411
--- /dev/null
+++ b/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();
+    }
+}
diff --git a/src/test/java/org/codehaus/plexus/util/io/CachingOutputStreamTest.java b/src/test/java/org/codehaus/plexus/util/io/CachingOutputStreamTest.java
new file mode 100644
index 00000000..3c329ea9
--- /dev/null
+++ b/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;
+    }
+
+}
diff --git a/src/test/java/org/codehaus/plexus/util/io/CachingWriterTest.java b/src/test/java/org/codehaus/plexus/util/io/CachingWriterTest.java
new file mode 100644
index 00000000..f5b95903
--- /dev/null
+++ b/src/test/java/org/codehaus/plexus/util/io/CachingWriterTest.java
@@ -0,0 +1,143 @@
+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.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 CachingWriterTest
+{
+
+    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
+    {
+        String data = "Hello world!";
+        Path path = tempDir.resolve( "file.txt" );
+        assertFalse( Files.exists( path ) );
+
+        try ( CachingWriter cos = new CachingWriter( path, StandardCharsets.UTF_8, 4 ) )
+        {
+            cos.write( data );
+        }
+        assertTrue( Files.exists( path ) );
+        String read = new String( Files.readAllBytes( path ), StandardCharsets.UTF_8 );
+        assertEquals( data, read );
+        FileTime modified = Files.getLastModifiedTime( path );
+
+        waitLastModified();
+
+        try ( CachingWriter cos = new CachingWriter( path, StandardCharsets.UTF_8, 4 ) )
+        {
+            cos.write( data );
+        }
+        assertTrue( Files.exists( path ) );
+        read = new String( Files.readAllBytes( path ), StandardCharsets.UTF_8 );
+        assertEquals( data, read );
+        FileTime newModified = Files.getLastModifiedTime( path );
+        assertEquals( modified, newModified );
+        modified = newModified;
+
+        waitLastModified();
+
+        // write longer data
+        data = "Good morning!";
+        try ( CachingWriter cos = new CachingWriter( path, StandardCharsets.UTF_8, 4 ) )
+        {
+            cos.write( data );
+        }
+        assertTrue( Files.exists( path ) );
+        read = new String( Files.readAllBytes( path ), StandardCharsets.UTF_8 );
+        assertEquals( data, read );
+        newModified = Files.getLastModifiedTime( path );
+        assertNotEquals( modified, newModified );
+        modified = newModified;
+
+        waitLastModified();
+
+        // different data same size
+        data = "Good mornong!";
+        try ( CachingWriter cos = new CachingWriter( path, StandardCharsets.UTF_8, 4 ) )
+        {
+            cos.write( data );
+        }
+        assertTrue( Files.exists( path ) );
+        read = new String( Files.readAllBytes( path ), StandardCharsets.UTF_8 );
+        assertEquals( data, read );
+        newModified = Files.getLastModifiedTime( path );
+        assertNotEquals( modified, newModified );
+        modified = newModified;
+
+        waitLastModified();
+
+        // same data but shorter
+        data = "Good mornon";
+        try ( CachingWriter cos = new CachingWriter( path, StandardCharsets.UTF_8, 4 ) )
+        {
+            cos.write( data );
+        }
+        assertTrue( Files.exists( path ) );
+        read = new String( Files.readAllBytes( path ), StandardCharsets.UTF_8 );
+        assertEquals( data, read );
+        newModified = Files.getLastModifiedTime( path );
+        assertNotEquals( modified, newModified );
+        modified = newModified;
+    }
+}