Skip to content
This repository has been archived by the owner on Aug 8, 2020. It is now read-only.

Commit

Permalink
Merge pull request #102 from furfurylic/timidity
Browse files Browse the repository at this point in the history
Add "timid" attribute to Output sink to avoid unnecessary touching
  • Loading branch information
furfurylic committed Jul 29, 2016
2 parents 62d5291 + e7dc294 commit ce40f07
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 9 deletions.
2 changes: 2 additions & 0 deletions UsersGuide.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,8 @@ The XPath expression can include names which belong some namespaces only when th
|mkdirs|Whether this sink creates parent directories of the destination file if needed.| No; defaults to +yes+

|force|Whether this sink creates output files even if existing files seem new enough.| No; defaults to +no+

|timid|Whether this sink avoids overwriting existing files which already have identical contents to be written.| No; defaults to +no+
|=================

==== Nested elements
Expand Down
7 changes: 3 additions & 4 deletions src/net/furfurylic/chionographis/CachingResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ final class CachingResolver implements EntityResolver, URIResolver {
private static final NetResourceCache<byte[]> BYTES = new NetResourceCache<>();
private static final NetResourceCache<Source> TREES = new NetResourceCache<>();

private static final Pool<byte[]> BUFFER = new Pool<>(() -> new byte[4096]);

private Consumer<URI> listenStored_;
private Consumer<URI> listenHit_;

Expand Down Expand Up @@ -76,7 +74,7 @@ public InputSource resolveEntity(String publicId, String systemId)
return Files.readAllBytes(Paths.get(u));
} else {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
byte[] buffer = BUFFER.get();
byte[] buffer = Pool.BYTES.get();
try {
try (InputStream in = u.toURL().openStream()) {
int length;
Expand All @@ -85,8 +83,9 @@ public InputSource resolveEntity(String publicId, String systemId)
}
}
} finally {
BUFFER.release(buffer);
Pool.BYTES.release(buffer);
}
// TODO: Can make more efficient (by avoiding copy)
return bytes.toByteArray();
}

Expand Down
56 changes: 51 additions & 5 deletions src/net/furfurylic/chionographis/Output.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@
* An <i>Output</i> {@linkplain Sink sink} writes each source document into an filesystem file.
*/
public final class Output extends Sink {
private static final Pool<OutputStream> BUFFER =
private static final Pool<ExposingByteArrayOutputStream> BUFFER =
new Pool<>(() -> new ExposingByteArrayOutputStream());

private Path destDir_ = null;
private Path dest_ = null;
private boolean mkDirs_ = true;
private String referent_ = null;
private boolean force_ = false;
private boolean timid_ = false;
private FileNameMapper mapper_ = null;

private Logger logger_;
Expand Down Expand Up @@ -137,7 +138,7 @@ public void setRefer(String refer) {
* if necessary. Defaulted to "no".
*
* @param mkDirs
* {@code true} if make parent directories; {@code false} otherwise.
* {@code true} if makes parent directories; {@code false} otherwise.
*/
public void setMkDirs(boolean mkDirs) {
mkDirs_ = mkDirs;
Expand All @@ -154,6 +155,19 @@ public void setForce(boolean force) {
force_ = force;
}

/**
* Sets whether this sink should compare existing destination files with the contents
* about to be written and avoid overwriting them if not necessary.
*
* @param timid
* {@code true} if avoids unnecessary overwriting; {@code false} otherwise.
*
* @since 1.1
*/
public void setTimid(boolean timid) {
timid_ = timid;
}

/**
* Installs a file mapper.
* The file mapper maps a source file name to an destination file name.
Expand Down Expand Up @@ -352,14 +366,27 @@ void finishOne(Result result) {
Files.createDirectories(parent);
}
}

Path absolute = mapped.toAbsolutePath();

if (timid_) {
File file = absolute.toFile();
if (file.exists() && (file.length() == out.size())
&& hasIdenticalContent(file, out.buffer())) {
logger_.log(this, "No need to overwrite the output file: " + absolute,
Logger.Level.FINE);
continue;
}
}

logger_.log(this, "Creating " + absolute, Logger.Level.FINE);
// We take advantage of FileChannel for its capability to be interrupted
try {
try (FileChannel channel = FileChannel.open(
absolute, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
channel.write(ByteBuffer.wrap(out.buffer(), 0, out.size()));
}
countInBundle_.incrementAndGet();
} catch (IOException e) {
logger_.log(this, "Failed to create " + absolute, Logger.Level.WARN);
logger_.log(this, e, " Cause: ", Logger.Level.INFO, Logger.Level.VERBOSE);
Expand All @@ -371,16 +398,35 @@ void finishOne(Result result) {
} finally {
placeBackBuffer(out);
}
}

countInBundle_.incrementAndGet();
private boolean hasIdenticalContent(File file, byte[] content) throws IOException {
byte[] bytes = Pool.BYTES.get();
try (FileChannel in = FileChannel.open(file.toPath(), StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.wrap(bytes);
int length;
int head = 0;
while ((length = in.read(buffer)) > -1) {
buffer.limit(length);
buffer.rewind();
if (!buffer.equals(ByteBuffer.wrap(content, head, length))) {
return false;
}
head += length;
}
return true;
} finally {
Pool.BYTES.release(bytes);
}
}

@Override
void abortOne(Result result) {
placeBackBuffer((ByteArrayOutputStream) ((OutputStreamResult) result).getOutputStream());
placeBackBuffer(
(ExposingByteArrayOutputStream) ((OutputStreamResult) result).getOutputStream());
}

private void placeBackBuffer(ByteArrayOutputStream buffer) {
private void placeBackBuffer(ExposingByteArrayOutputStream buffer) {
buffer.reset();
BUFFER.release(buffer);
}
Expand Down
2 changes: 2 additions & 0 deletions src/net/furfurylic/chionographis/Pool.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
*/
final class Pool<T> {

public static final Pool<byte[]> BYTES = new Pool<>(() -> new byte[4096]);

private final ReentrantLock lock_ = new ReentrantLock();
private Supplier<? extends T> create_;
private SoftReference<Queue<T>> pool_;
Expand Down
1 change: 1 addition & 0 deletions test/crossing/input-timid/input.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<X><Y><Z message="abc"/></Y></X>
1 change: 1 addition & 0 deletions test/crossing/input-timid/placeholder1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[X:[Y:[Z(message=abc):]]]
1 change: 1 addition & 0 deletions test/crossing/input-timid/placeholder2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[X:[Y:[Z(message=abc):]]x
1 change: 1 addition & 0 deletions test/crossing/input-timid/placeholder3.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[X:[Y:[Z(message=abc):]]
50 changes: 50 additions & 0 deletions test/test.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<antcall target="snip-ns"/>
<antcall target="crossing-cache"/>
<antcall target="crossing-cache-refer-content"/>
<antcall target="crossing-timid"/>
<antcall target="crossing-parallel-sinks"/>
<antcall target="crossing-abort-sources"/>
<antcall target="crossing-abort-sinks-dom"/>
Expand Down Expand Up @@ -1111,6 +1112,55 @@
<delete dir="${dir.output}"/>
</target>

<target name="crossing-timid">
<property name="test.prefix" value="crossing"/>
<property name="test.title" value="timid"/>
<property name="test.name" value="${test.prefix}-${test.title}"/>
<property name="dir.input" location="${test.prefix}/input-${test.title}"/>
<property name="dir.output" location="${test.prefix}/output-${test.title}"/>

<delete dir="${dir.output}"/>
<mkdir dir="${dir.output}"/>

<copy file="${dir.input}/input.xml" tofile="${dir.output}/input1.xml"/>
<copy file="${dir.input}/input.xml" tofile="${dir.output}/input2.xml"/>
<copy file="${dir.input}/input.xml" tofile="${dir.output}/input3.xml"/>
<copy file="${dir.input}/input.xml" tofile="${dir.output}/input4.xml"/>
<copy todir="${dir.output}">
<fileset dir="${dir.input}" includes="placeholder*.txt"/>
<mapper type="glob" from="placeholder*.txt" to="actual*.txt"/>
</copy>
<touch file="${dir.output}/input1.xml" datetime="01/01/2001 00:01:10 AM"/> <!-- newer -->
<touch file="${dir.output}/input2.xml" datetime="01/01/2001 00:01:10 AM"/> <!-- newer -->
<touch file="${dir.output}/input3.xml" datetime="01/01/2001 00:01:10 AM"/> <!-- newer -->
<touch file="${dir.output}/actual1.txt" datetime="01/01/2001 00:01:00 AM"/> <!-- older -->
<touch file="${dir.output}/actual2.txt" datetime="01/01/2001 00:01:00 AM"/> <!-- older -->
<touch file="${dir.output}/actual3.txt" datetime="01/01/2001 00:01:00 AM"/> <!-- older -->

<chionographis srcdir="${dir.output}" includes="input*.xml" cache="no">
<transform style="flatten.xsl" cache="no">
<output destdir="${dir.output}" timid="yes">
<globmapper from="input*.xml" to="actual*.txt"/>
</output>
</transform>
</chionographis>

<!-- 1: identical: not touched -->
<assertfilelastmodified name="${test.name} - 1"
file="${dir.output}/actual1.txt" datetime="01/01/2001 00:01:00 AM"/>
<!-- 2: equal length and different content: touched -->
<assertfileeq name="${test.name} - 2"
expected="${dir.input}/placeholder1.txt" actual="${dir.output}/actual2.txt"/>
<!-- 3: different length: touched -->
<assertfileeq name="${test.name} - 3"
expected="${dir.input}/placeholder1.txt" actual="${dir.output}/actual3.txt"/>
<!-- 4: missing in destination: touched -->
<assertfileeq name="${test.name} - 4"
expected="${dir.input}/placeholder1.txt" actual="${dir.output}/actual4.txt"/>

<delete dir="${dir.output}"/>
</target>

<target name="crossing-parallel-sinks">
<property name="test.prefix" value="crossing"/>
<property name="test.title" value="parallel-sinks"/>
Expand Down

0 comments on commit ce40f07

Please sign in to comment.