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

}

@@ -5350,16 +5350,34 @@
     if (
+filter != null && 
 !filter.

Resolve possible NPE for filter