/*
* Copyright 2010 the original author or authors.
*
* 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.
*/
package org.gradle.api.internal.changedetection.state;
import org.gradle.api.file.FileCollection;
import org.gradle.cache.PersistentIndexedCache;
import org.gradle.internal.id.IdGenerator;
import org.gradle.messaging.serialize.DefaultSerializerRegistry;
import org.gradle.messaging.serialize.LongSerializer;
import org.gradle.messaging.serialize.SerializerRegistry;
import org.gradle.util.ChangeListener;
import org.gradle.util.DiffUtil;
import org.gradle.util.NoOpChangeListener;
import java.io.File;
import java.util.*;
/**
* Takes a snapshot of the output files of a task. 2 parts to the algorithm:
*
* <ul>
* <li>Collect the unique id for each output file and directory. The unique id is generated when we notice that
* a file/directory has been created. The id is regenerated when the file/directory is deleted.</li>
*
* <li>Collect the hash of each output file and each file in each output directory.</li>
* </ul>
*
*/
public class OutputFilesCollectionSnapshotter implements FileCollectionSnapshotter {
private final FileCollectionSnapshotter snapshotter;
private final IdGenerator<Long> idGenerator;
private final TaskArtifactStateCacheAccess cacheAccess;
private final PersistentIndexedCache<String, Long> dirIdentifierCache;
public OutputFilesCollectionSnapshotter(FileCollectionSnapshotter snapshotter, IdGenerator<Long> idGenerator,
TaskArtifactStateCacheAccess cacheAccess) {
this.snapshotter = snapshotter;
this.idGenerator = idGenerator;
this.cacheAccess = cacheAccess;
dirIdentifierCache = cacheAccess.createCache("outputFileStates", String.class, new LongSerializer());
}
public void registerSerializers(SerializerRegistry<FileCollectionSnapshot> registry) {
DefaultSerializerRegistry<FileCollectionSnapshot> nested = new DefaultSerializerRegistry<FileCollectionSnapshot>();
snapshotter.registerSerializers(nested);
registry.register(OutputFilesSnapshot.class, new OutputFilesSnapshotSerializer(nested.build()));
}
public FileCollectionSnapshot emptySnapshot() {
return new OutputFilesSnapshot(new HashMap<String, Long>(), snapshotter.emptySnapshot());
}
public OutputFilesSnapshot snapshot(final FileCollection files) {
final Map<String, Long> snapshotDirIds = new HashMap<String, Long>();
final Set<File> theFiles = files.getFiles();
cacheAccess.useCache("create dir snapshots", new Runnable() {
public void run() {
for (File file : theFiles) {
Long dirId;
if (file.exists()) {
dirId = dirIdentifierCache.get(file.getAbsolutePath());
if (dirId == null) {
dirId = idGenerator.generateId();
dirIdentifierCache.put(file.getAbsolutePath(), dirId);
}
} else {
dirIdentifierCache.remove(file.getAbsolutePath());
dirId = null;
}
snapshotDirIds.put(file.getAbsolutePath(), dirId);
}
}
});
return new OutputFilesSnapshot(snapshotDirIds, snapshotter.snapshot(files));
}
static class OutputFilesSnapshot implements FileCollectionSnapshot {
final Map<String, Long> rootFileIds;
final FileCollectionSnapshot filesSnapshot;
public OutputFilesSnapshot(Map<String, Long> rootFileIds, FileCollectionSnapshot filesSnapshot) {
this.rootFileIds = rootFileIds;
this.filesSnapshot = filesSnapshot;
}
public FileCollection getFiles() {
return filesSnapshot.getFiles();
}
public FilesSnapshotSet getSnapshot() {
return filesSnapshot.getSnapshot();
}
public Diff changesSince(final FileCollectionSnapshot oldSnapshot) {
OutputFilesSnapshot other = (OutputFilesSnapshot) oldSnapshot;
return new OutputFilesDiff(rootFileIds, other.rootFileIds, filesSnapshot.changesSince(other.filesSnapshot));
}
public ChangeIterator<String> iterateChangesSince(FileCollectionSnapshot oldSnapshot) {
final OutputFilesSnapshot other = (OutputFilesSnapshot) oldSnapshot;
final ChangeIterator<String> rootFileIdIterator = iterateRootFileIdChanges(other);
final ChangeIterator<String> fileIterator = filesSnapshot.iterateChangesSince(other.filesSnapshot);
final AddIgnoreChangeListenerAdapter listenerAdapter = new AddIgnoreChangeListenerAdapter();
return new ChangeIterator<String>() {
public boolean next(final ChangeListener<String> listener) {
listenerAdapter.withDelegate(listener);
if (rootFileIdIterator.next(listener)) {
return true;
}
while (fileIterator.next(listenerAdapter)) {
if (!listenerAdapter.wasIgnored) {
return true;
}
}
return false;
}
};
}
private ChangeIterator<String> iterateRootFileIdChanges(final OutputFilesSnapshot other) {
// Inlining DiffUtil.diff makes the inefficiencies here a bit more explicit
Map<String, Long> added = new HashMap<String, Long>(rootFileIds);
added.keySet().removeAll(other.rootFileIds.keySet());
final Iterator<String> addedIterator = added.keySet().iterator();
Map<String, Long> removed = new HashMap<String, Long>(other.rootFileIds);
removed.keySet().removeAll(rootFileIds.keySet());
final Iterator<String> removedIterator = removed.keySet().iterator();
Set<String> changed = new HashSet<String>();
for (Map.Entry<String, Long> current : rootFileIds.entrySet()) {
// Only care about rootIds that used to exist, and have changed or been removed
Long otherValue = other.rootFileIds.get(current.getKey());
if (otherValue != null && !otherValue.equals(current.getValue())) {
changed.add(current.getKey());
}
}
final Iterator<String> changedIterator = changed.iterator();
return new ChangeIterator<String>() {
public boolean next(ChangeListener<String> listener) {
if (addedIterator.hasNext()) {
listener.added(addedIterator.next());
return true;
}
if (removedIterator.hasNext()) {
listener.removed(removedIterator.next());
return true;
}
if (changedIterator.hasNext()) {
listener.changed(changedIterator.next());
return true;
}
return false;
}
};
}
}
/**
* A flyweight wrapper that is used to ignore any added files called.
*/
private static class AddIgnoreChangeListenerAdapter implements ChangeListener<String> {
private ChangeListener<String> delegate;
boolean wasIgnored;
private void withDelegate(ChangeListener<String> delegate) {
this.delegate = delegate;
}
public void added(String element) {
wasIgnored = true;
}
public void removed(String element) {
delegate.removed(element);
wasIgnored = false;
}
public void changed(String element) {
delegate.changed(element);
wasIgnored = false;
}
}
private static class OutputFilesDiff implements FileCollectionSnapshot.Diff {
private final Map<String, Long> newFileIds;
private final Map<String, Long> oldFileIds;
private final FileCollectionSnapshot.Diff filesDiff;
public OutputFilesDiff(Map<String, Long> newFileIds, Map<String, Long> oldFileIds,
FileCollectionSnapshot.Diff filesDiff) {
this.newFileIds = newFileIds;
this.oldFileIds = oldFileIds;
this.filesDiff = filesDiff;
}
public FileCollectionSnapshot applyTo(FileCollectionSnapshot snapshot,
ChangeListener<FileCollectionSnapshot.Merge> listener) {
OutputFilesSnapshot other = (OutputFilesSnapshot) snapshot;
Map<String, Long> dirIds = new HashMap<String, Long>(other.rootFileIds);
DiffUtil.diff(newFileIds, oldFileIds, new MapMergeChangeListener<String, Long>(
new NoOpChangeListener<FileCollectionSnapshot.Merge>(), dirIds));
return new OutputFilesSnapshot(newFileIds, filesDiff.applyTo(other.filesSnapshot, listener));
}
public FileCollectionSnapshot applyTo(FileCollectionSnapshot snapshot) {
return applyTo(snapshot, new NoOpChangeListener<FileCollectionSnapshot.Merge>());
}
}
}