Java tutorial
/** * Copyright (c) Codice Foundation * * <p>This is free software: you can redistribute it and/or modify it under the terms of the GNU * Lesser General Public License as published by the Free Software Foundation, either version 3 of * the License, or any later version. * * <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public * License is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package org.codice.ddf.catalog.content.monitor; import com.google.common.annotations.VisibleForTesting; import java.io.File; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicLong; import javax.annotation.Nullable; import org.apache.commons.io.FileUtils; import org.codice.ddf.catalog.content.monitor.synchronizations.CompletionSynchronization; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Based on {@link org.apache.commons.io.monitor.FileAlterationObserver}, except modified to only * update the observer state on successful async request * * <p>This implementation only works with one AsyncFileAlterationListener. * * <p>Every time the AsyncFileAlterationObserver is polled by calling {@code checkAndNotify()}, if * there are no files currently being processed, the observer will check all the {@link File}'s and * {@link AsyncFileEntry}'s that are under the directory being monitored, and call the {@link * AsyncFileAlterationListener}'s corresponding methods * * <p>if there are files being processed or a thread already inside {@code checkAndNotify()}, check * and notify will immediately return false * * <p>Known Limitations: * * <ul> * <li>if a file becomes a directory then it's contents will not be deleted from the catalog * <li>if a directory becomes a file, then it's contents will not be created in the catalog * </ul> * * @see AsyncFileAlterationListener */ public class AsyncFileAlterationObserver { private static final Logger LOGGER = LoggerFactory.getLogger(AsyncFileAlterationObserver.class); private final AsyncFileEntry rootFile; private AsyncFileAlterationListener listener = null; private final AtomicLong processing = new AtomicLong(0); private final Object listenerLock = new Object(); private final ObjectPersistentStore serializer; private final Object processingLock = new Object(); private boolean isProcessing = false; public AsyncFileAlterationObserver(File fileToObserve, ObjectPersistentStore serializer) { if (fileToObserve == null || serializer == null) { throw new IllegalArgumentException("Arguments can not be null"); } this.serializer = serializer; rootFile = new AsyncFileEntry(fileToObserve); } private AsyncFileAlterationObserver(AsyncFileEntry entry, ObjectPersistentStore serializer) { if (entry == null) { throw new IllegalArgumentException("Arguments can not be null"); } rootFile = entry; rootFile.initialize(); this.serializer = serializer; } /** * @param observedFile * @param store * @return returns a AsyncFileAlterationObserver if there was one serialized by an {@link * ObjectPersistentStore} Otherwise returns {@code null} */ public static @Nullable AsyncFileAlterationObserver load(File observedFile, ObjectPersistentStore store) { if (observedFile == null || store == null) { throw new IllegalArgumentException("Arguments can not be null"); } AsyncFileEntry temp = store.load(observedFile.getName(), AsyncFileEntry.class); if (temp == null) { return null; } return new AsyncFileAlterationObserver(temp, store); } /** * Initializes the object state of the Observer. * * @throws IllegalStateException when the observer fails to initialize and initialization should * be retried */ public void initialize() throws IllegalStateException { initChildEntries(rootFile); serializer.store(rootFile.getName(), rootFile); } public void destroy() { rootFile.destroy(); } public void setListener(final AsyncFileAlterationListener listener) { synchronized (listenerLock) { this.listener = listener; } } public void removeListener() { synchronized (listenerLock) { this.listener = null; } } /** * Called when the observer should compare the snapshot state to the actual state of the directory * being monitored. */ public boolean checkAndNotify() { AsyncFileAlterationListener listenerCopy; synchronized (processingLock) { if (processing.get() != 0) { LOGGER.debug("{} files are still processing. Waiting until the list is empty", processing.get()); return false; } else if (isProcessing) { LOGGER.debug("Another thread is currently running, returning until next poll"); return false; } isProcessing = true; // You cannot change listeners in the middle of executions. synchronized (listenerLock) { if (listener == null) { isProcessing = false; return false; } listenerCopy = listener; } } /* fire directory/file events */ if (rootFile.checkNetwork()) { checkAndNotify(rootFile, rootFile.getChildren(), listFiles(rootFile.getFile()), listenerCopy); } else { // If we can't connect to the network then the file doesn't exist to us now. LOGGER.debug("The monitored file [{}] does not exist. No file fileLocks will be done through the CDM", rootFile.getName()); } synchronized (processingLock) { isProcessing = false; } return true; } @VisibleForTesting AsyncFileEntry getRootFile() { return rootFile; } /** * Fire directory/file created events to the registered listeners. * * @param entry The file entry */ private void doCreate(AsyncFileEntry entry, final AsyncFileAlterationListener listenerCopy) { processing.incrementAndGet(); if (!entry.getFile().isDirectory()) { LOGGER.trace("Sending create Request for {}", entry.getName()); listenerCopy.onFileCreate(entry.getFile(), new CompletionSynchronization(entry, this::commitCreate)); } else { // Directories are always committed and added to the parent IF they // don't already exist File[] children = listFiles(entry.getFile()); for (File child : children) { doCreate(new AsyncFileEntry(entry, child), listenerCopy); } commitCreate(entry, true); } } /** * Callback to allow successful ContentFiles to commit themselves * * @param entry The AsyncFileEntry wrapping the file being listened to. * @param success Boolean that shows if the task failed or completed successfully */ private void commitCreate(AsyncFileEntry entry, boolean success) { LOGGER.debug("commitCreate({},{}): Starting...", entry.getName(), success); if (success) { entry.commit(); entry.getParent().ifPresent(e -> e.addChild(entry)); } onFinish(); } /** * Fire directory/file change events to the registered listeners. * * @param entry The previous file system entry */ private void doMatch(AsyncFileEntry entry, final AsyncFileAlterationListener listenerCopy) { if (!entry.hasChanged()) { return; } processing.incrementAndGet(); LOGGER.trace("{} has changed", entry.getName()); if (!entry.getFile().isDirectory()) { LOGGER.trace("Sending Match Request for {}...", entry.getName()); listenerCopy.onFileChange(entry.getFile(), new CompletionSynchronization(entry, this::commitMatch)); } else { commitMatch(entry, true); } } /** * Callback to allow successful {@link AsyncFileEntry}'s to commit their changes * * @param entry The AsyncFileEntry wrapping the file being listened to. * @param success Boolean that shows if the task failed or completed successfully */ private void commitMatch(AsyncFileEntry entry, boolean success) { LOGGER.debug("commitMatch({},{}): Starting...", entry.getName(), success); if (success) { entry.commit(); } onFinish(); } /** * Fire directory/file delete events to the registered listeners. * * @param entry The file entry */ private void doDelete(AsyncFileEntry entry, final AsyncFileAlterationListener listenerCopy) { if (!entry.isDirectory()) { processing.incrementAndGet(); LOGGER.trace("Sending Delete Request for {}...", entry.getName()); listenerCopy.onFileDelete(entry.getFile(), new CompletionSynchronization(entry, this::commitDelete)); } // Once there are no more children we can delete directories. // Check that there are no children, and that no locked files have it as it's parent. else if (!entry.hasChildren()) { processing.incrementAndGet(); commitDelete(entry, true); } // If there are still children, we're going to keep it within the tree until all the // children are successfully deleted } /** * Callback to allow successful children to remove themselves from their parent directory and put * themselves in a state where they have never been committed. * * @param entry The AsyncFileEntry wrapping the file being listened to. * @param success Boolean that shows if the task failed or completed successfully */ private void commitDelete(AsyncFileEntry entry, boolean success) { LOGGER.debug("commitDelete({},{}): Starting...", entry.getName(), success); if (success) { entry.getParent().ifPresent(e -> e.removeChild(entry)); entry.destroy(); } onFinish(); } /** * Steps file by file comparing the snapshot state to the current state of the directory being * monitored. * * @param parent The parent directory (Wrapped in a AsyncFileEntry) * @param previous The list of all children of the parent directory (In sorted order) * @param files The list of current files (in sorted order) */ private void checkAndNotify(final AsyncFileEntry parent, final List<AsyncFileEntry> previous, @Nullable final File[] files, final AsyncFileAlterationListener listenerCopy) { // If there was an IO error then just stop. if (files == null) { return; } int c = 0; for (final AsyncFileEntry entry : previous) { while (c < files.length && entry.compareToFile(files[c]) > 0) { doCreate(new AsyncFileEntry(parent, files[c]), listenerCopy); c++; } if (c < files.length && entry.compareToFile(files[c]) == 0) { doMatch(entry, listenerCopy); checkAndNotify(entry, entry.getChildren(), listFiles(files[c]), listenerCopy); c++; } else { // Do Delete if (!entry.checkNetwork()) { // The file may still exist but it's the network that's down. return; } checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY, listenerCopy); doDelete(entry, listenerCopy); } } for (; c < files.length; c++) { doCreate(new AsyncFileEntry(parent, files[c]), listenerCopy); } } /** * Note: returns a new Array to avoid sync access exceptions * * @param file file to retrieve files from. * @return A new sorted File Array if {@code file} is a directory, an empty Array if the file is * not a directory, and null if there is an error retrieving the children files. */ private File[] listFiles(File file) { if (file.isDirectory()) { File[] temp = file.listFiles(); if (temp != null) { Arrays.sort(temp); return temp; } LOGGER.info("There was a problem reading the files contained within [{}]", file.getName()); return null; } return FileUtils.EMPTY_FILE_ARRAY; } private void initChildEntries(AsyncFileEntry parent) throws IllegalStateException { File[] children = listFiles(parent.getFile()); if (children == null) { LOGGER.debug("Error while initializing children for [{}]", parent.getName()); throw new IllegalStateException("Failed to initialize the FileObserver"); } for (File child : children) { AsyncFileEntry childEntry = new AsyncFileEntry(parent, child); parent.addChild(childEntry); initChildEntries(childEntry); } } private void onFinish() { synchronized (processingLock) { if (processing.decrementAndGet() == 0) { serializer.store(rootFile.getName(), rootFile); isProcessing = false; } } } }