blob: a680b489efcc248906690f1f2a8887ca60e0fc86 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.felix.fileinstall.internal;
import java.io.Closeable;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
/**
* A Scanner object is able to detect and report new, modified
* and deleted files.
*
* The scanner use an internal checksum to identify the signature
* of a file or directory. The checksum will change if the file
* or any of the directory's child is modified.
*
* In addition, if the scanner detects a change on a given file, it
* will wait until the checksum does not change anymore before reporting
* the change on this file. This allows to not report the change until
* a big copy if complete for example.
*/
public class Scanner implements Closeable {
public final static String SUBDIR_MODE_JAR = "jar";
public final static String SUBDIR_MODE_SKIP = "skip";
public final static String SUBDIR_MODE_RECURSE = "recurse";
final File directory;
final FilenameFilter filter;
final boolean jarSubdir;
final boolean skipSubdir;
final boolean recurseSubdir;
// Store checksums of files or directories
Map<File, Long> lastChecksums = new HashMap<File, Long>();
Map<File, Long> storedChecksums = new HashMap<File, Long>();
/**
* Create a scanner for the specified directory
*
* @param directory the directory to scan
*/
public Scanner(File directory)
{
this(directory, null, null);
}
/**
* Create a scanner for the specified directory and file filter
*
* @param directory the directory to scan
* @param filterString a filter for file names
* @param subdirMode to use when scanning
*/
public Scanner(File directory, final String filterString, String subdirMode)
{
this.directory = canon(directory);
if (filterString != null && filterString.length() > 0)
{
this.filter = new FilenameFilter()
{
Pattern pattern = Pattern.compile(filterString);
public boolean accept(File dir, String name)
{
return pattern.matcher(name).matches();
}
};
}
else
{
this.filter = null;
}
this.jarSubdir = subdirMode == null || SUBDIR_MODE_JAR.equals(subdirMode);
this.skipSubdir = SUBDIR_MODE_SKIP.equals(subdirMode);
this.recurseSubdir = SUBDIR_MODE_RECURSE.equals(subdirMode);
}
/**
* Initialize the list of known files.
* This should be called before the first scan to initialize
* the list of known files. The purpose is to be able to detect
* files that have been deleted while the scanner was inactive.
*
* @param checksums a map of checksums
*/
public void initialize(Map<File, Long> checksums)
{
storedChecksums.putAll(checksums);
}
/**
* Report a set of new, modified or deleted files.
* Modifications are checked against a computed checksum on some file
* attributes to detect any modification.
* Upon restart, such checksums are not known so that all files will
* be reported as modified.
*
* @param reportImmediately report all files immediately without waiting for the checksum to be stable
* @return a list of changes on the files included in the directory
*/
public Set<File> scan(boolean reportImmediately)
{
File[] list = directory.listFiles();
Set<File> files = processFiles(reportImmediately, list);
return new TreeSet<>(files);
}
private Set<File> processFiles(boolean reportImmediately, File[] list)
{
if (list == null)
{
return new HashSet<>();
}
Set<File> files = new HashSet<File>();
Set<File> removed = new HashSet<File>(storedChecksums.keySet());
for (File file : list)
{
if (file.isDirectory())
{
if (skipSubdir)
{
continue;
}
else if (recurseSubdir)
{
files.addAll(processFiles(reportImmediately, file.listFiles()));
continue;
}
}
else {
if (!filter.accept(file.getParentFile(),file.getName())){
continue;
}
}
long lastChecksum = lastChecksums.get(file) != null ? (Long) lastChecksums.get(file) : 0;
long storedChecksum = storedChecksums.get(file) != null ? (Long) storedChecksums.get(file) : 0;
long newChecksum = checksum(file);
lastChecksums.put(file, newChecksum);
// Only handle file when it does not change anymore and it has changed
// since last reported
if ((newChecksum == lastChecksum || reportImmediately) && newChecksum != storedChecksum)
{
storedChecksums.put(file, newChecksum);
files.add(file);
}
removed.remove(file);
}
// Make sure we'll handle a file that has been deleted
files.addAll(removed);
for (File file : removed)
{
// Remove no longer used checksums
lastChecksums.remove(file);
storedChecksums.remove(file);
}
return files;
}
public void close() throws IOException {
}
private static File canon(File file)
{
try
{
return file.getCanonicalFile();
}
catch (IOException e)
{
return file;
}
}
/**
* Retrieve the previously computed checksum for a give file.
*
* @param file the file to retrieve the checksum
* @return the checksum
*/
public long getChecksum(File file)
{
Long c = storedChecksums.get(file);
return c != null ? c : 0;
}
/**
* Update the checksum of a file if that file is already known locally.
*/
public void updateChecksum(File file)
{
if (file != null && storedChecksums.containsKey(file))
{
long newChecksum = checksum(file);
storedChecksums.put(file, newChecksum);
}
}
/**
* Compute a cheksum for the file or directory that consists of the name, length and the last modified date
* for a file and its children in case of a directory
*
* @param file the file or directory
* @return a checksum identifying any change
*/
static long checksum(File file)
{
CRC32 crc = new CRC32();
checksum(file, crc);
return crc.getValue();
}
private static void checksum(File file, CRC32 crc)
{
crc.update(file.getName().getBytes());
if (file.isFile())
{
checksum(file.lastModified(), crc);
checksum(file.length(), crc);
}
else if (file.isDirectory())
{
File[] children = file.listFiles();
if (children != null)
{
for (File aChildren : children)
{
checksum(aChildren, crc);
}
}
}
}
private static void checksum(long l, CRC32 crc)
{
for (int i = 0; i < 8; i++)
{
crc.update((int) (l & 0x000000ff));
l >>= 8;
}
}
}