/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch 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.elasticsearch.watcher; import org.apache.logging.log4j.Logger; import org.elasticsearch.common.io.FileSystemUtils; import org.elasticsearch.common.logging.Loggers; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.util.Arrays; /** * File resources watcher * * The file watcher checks directory and all its subdirectories for file changes and notifies its listeners accordingly */ public class FileWatcher extends AbstractResourceWatcher<FileChangesListener> { private FileObserver rootFileObserver; private Path file; private static final Logger logger = Loggers.getLogger(FileWatcher.class); /** * Creates new file watcher on the given directory */ public FileWatcher(Path file) { this.file = file; rootFileObserver = new FileObserver(file); } /** * Clears any state with the FileWatcher, making all files show up as new */ public void clearState() { rootFileObserver = new FileObserver(file); try { rootFileObserver.init(false); } catch (IOException e) { // ignore IOException } } @Override protected void doInit() throws IOException { rootFileObserver.init(true); } @Override protected void doCheckAndNotify() throws IOException { rootFileObserver.checkAndNotify(); } private static FileObserver[] EMPTY_DIRECTORY = new FileObserver[0]; private class FileObserver { private Path file; private boolean exists; private long length; private long lastModified; private boolean isDirectory; private FileObserver[] children; FileObserver(Path file) { this.file = file; } public void checkAndNotify() throws IOException { boolean prevExists = exists; boolean prevIsDirectory = isDirectory; long prevLength = length; long prevLastModified = lastModified; exists = Files.exists(file); // TODO we might use the new NIO2 API to get real notification? if (exists) { BasicFileAttributes attributes = Files.readAttributes(file, BasicFileAttributes.class); isDirectory = attributes.isDirectory(); if (isDirectory) { length = 0; lastModified = 0; } else { length = attributes.size(); lastModified = attributes.lastModifiedTime().toMillis(); } } else { isDirectory = false; length = 0; lastModified = 0; } // Perform notifications and update children for the current file if (prevExists) { if (exists) { if (isDirectory) { if (prevIsDirectory) { // Remained a directory updateChildren(); } else { // File replaced by directory onFileDeleted(); onDirectoryCreated(false); } } else { if (prevIsDirectory) { // Directory replaced by file onDirectoryDeleted(); onFileCreated(false); } else { // Remained file if (prevLastModified != lastModified || prevLength != length) { onFileChanged(); } } } } else { // Deleted if (prevIsDirectory) { onDirectoryDeleted(); } else { onFileDeleted(); } } } else { // Created if (exists) { if (isDirectory) { onDirectoryCreated(false); } else { onFileCreated(false); } } } } private void init(boolean initial) throws IOException { exists = Files.exists(file); if (exists) { BasicFileAttributes attributes = Files.readAttributes(file, BasicFileAttributes.class); isDirectory = attributes.isDirectory(); if (isDirectory) { onDirectoryCreated(initial); } else { length = attributes.size(); lastModified = attributes.lastModifiedTime().toMillis(); onFileCreated(initial); } } } private FileObserver createChild(Path file, boolean initial) throws IOException { FileObserver child = new FileObserver(file); child.init(initial); return child; } private Path[] listFiles() throws IOException { final Path[] files = FileSystemUtils.files(file); Arrays.sort(files); return files; } private FileObserver[] listChildren(boolean initial) throws IOException { Path[] files = listFiles(); if (files != null && files.length > 0) { FileObserver[] children = new FileObserver[files.length]; for (int i = 0; i < files.length; i++) { children[i] = createChild(files[i], initial); } return children; } else { return EMPTY_DIRECTORY; } } private void updateChildren() throws IOException { Path[] files = listFiles(); if (files != null && files.length > 0) { FileObserver[] newChildren = new FileObserver[files.length]; int child = 0; int file = 0; while (file < files.length || child < children.length ) { int compare; if (file >= files.length) { compare = -1; } else if (child >= children.length) { compare = 1; } else { compare = children[child].file.compareTo(files[file]); } if (compare == 0) { // Same file copy it and update children[child].checkAndNotify(); newChildren[file] = children[child]; file++; child++; } else { if (compare > 0) { // This child doesn't appear in the old list - init it newChildren[file] = createChild(files[file], false); file++; } else { // The child from the old list is missing in the new list // Delete it deleteChild(child); child++; } } } children = newChildren; } else { // No files - delete all children for (int child = 0; child < children.length; child++) { deleteChild(child); } children = EMPTY_DIRECTORY; } } private void deleteChild(int child) { if (children[child].exists) { if (children[child].isDirectory) { children[child].onDirectoryDeleted(); } else { children[child].onFileDeleted(); } } } private void onFileCreated(boolean initial) { for (FileChangesListener listener : listeners()) { try { if (initial) { listener.onFileInit(file); } else { listener.onFileCreated(file); } } catch (Exception e) { logger.warn("cannot notify file changes listener", e); } } } private void onFileDeleted() { for (FileChangesListener listener : listeners()) { try { listener.onFileDeleted(file); } catch (Exception e) { logger.warn("cannot notify file changes listener", e); } } } private void onFileChanged() { for (FileChangesListener listener : listeners()) { try { listener.onFileChanged(file); } catch (Exception e) { logger.warn("cannot notify file changes listener", e); } } } private void onDirectoryCreated(boolean initial) throws IOException { for (FileChangesListener listener : listeners()) { try { if (initial) { listener.onDirectoryInit(file); } else { listener.onDirectoryCreated(file); } } catch (Exception e) { logger.warn("cannot notify file changes listener", e); } } children = listChildren(initial); } private void onDirectoryDeleted() { // First delete all children for (int child = 0; child < children.length; child++) { deleteChild(child); } for (FileChangesListener listener : listeners()) { try { listener.onDirectoryDeleted(file); } catch (Exception e) { logger.warn("cannot notify file changes listener", e); } } } } }