| /* |
| * 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; |
| } |
| } |
| |
| } |