001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.impl;
018
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.IOException;
022import java.nio.file.FileVisitResult;
023import java.nio.file.Files;
024import java.nio.file.Path;
025import java.nio.file.SimpleFileVisitor;
026import java.nio.file.WatchEvent;
027import java.nio.file.WatchKey;
028import java.nio.file.WatchService;
029import java.nio.file.attribute.BasicFileAttributes;
030import java.util.HashMap;
031import java.util.Locale;
032import java.util.Map;
033import java.util.concurrent.ExecutorService;
034import java.util.concurrent.TimeUnit;
035
036import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
037import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
038
039import org.apache.camel.api.management.ManagedAttribute;
040import org.apache.camel.api.management.ManagedResource;
041import org.apache.camel.support.ReloadStrategySupport;
042import org.apache.camel.util.IOHelper;
043import org.apache.camel.util.ObjectHelper;
044
045/**
046 * A file based {@link org.apache.camel.spi.ReloadStrategy} which watches a file folder
047 * for modified files and reload on file changes.
048 * <p/>
049 * This implementation uses the JDK {@link WatchService} to watch for when files are
050 * created or modified. Mac OS X users should be noted the osx JDK does not support
051 * native file system changes and therefore the watch service is much slower than on
052 * linux or windows systems.
053 */
054@ManagedResource(description = "Managed FileWatcherReloadStrategy")
055public class FileWatcherReloadStrategy extends ReloadStrategySupport {
056
057    private String folder;
058    private boolean isRecursive;
059    private WatchService watcher;
060    private ExecutorService executorService;
061    private WatchFileChangesTask task;
062    private Map<WatchKey, Path> folderKeys;
063    private long pollTimeout = 2000;
064
065    public FileWatcherReloadStrategy() {
066        setRecursive(false);
067    }
068
069    public FileWatcherReloadStrategy(String directory) {
070        setFolder(directory);
071        setRecursive(false);
072    }
073    
074    public FileWatcherReloadStrategy(String directory, boolean isRecursive) {
075        setFolder(directory);
076        setRecursive(isRecursive);
077    }
078
079    public void setFolder(String folder) {
080        this.folder = folder;
081    }
082    
083    public void setRecursive(boolean isRecursive) {
084        this.isRecursive = isRecursive;
085    }
086
087    /**
088     * Sets the poll timeout in millis. The default value is 2000.
089     */
090    public void setPollTimeout(long pollTimeout) {
091        this.pollTimeout = pollTimeout;
092    }
093
094    @ManagedAttribute(description = "Folder being watched")
095    public String getFolder() {
096        return folder;
097    }
098    
099    @ManagedAttribute(description = "Whether the reload strategy watches directory recursively")
100    public boolean isRecursive() {
101        return isRecursive;
102    }
103
104    @ManagedAttribute(description = "Whether the watcher is running")
105    public boolean isRunning() {
106        return task != null && task.isRunning();
107    }
108
109    @Override
110    protected void doStart() throws Exception {
111        super.doStart();
112
113        if (folder == null) {
114            // no folder configured
115            return;
116        }
117
118        File dir = new File(folder);
119        if (dir.exists() && dir.isDirectory()) {
120            log.info("Starting ReloadStrategy to watch directory: {}", dir);
121
122            WatchEvent.Modifier modifier = null;
123
124            // if its mac OSX then attempt to apply workaround or warn its slower
125            String os = ObjectHelper.getSystemProperty("os.name", "");
126            if (os.toLowerCase(Locale.US).startsWith("mac")) {
127                // this modifier can speedup the scanner on mac osx (as java on mac has no native file notification integration)
128                Class<WatchEvent.Modifier> clazz = getCamelContext().getClassResolver().resolveClass("com.sun.nio.file.SensitivityWatchEventModifier", WatchEvent.Modifier.class);
129                if (clazz != null) {
130                    WatchEvent.Modifier[] modifiers = clazz.getEnumConstants();
131                    for (WatchEvent.Modifier mod : modifiers) {
132                        if ("HIGH".equals(mod.name())) {
133                            modifier = mod;
134                            break;
135                        }
136                    }
137                }
138                if (modifier != null) {
139                    log.info("On Mac OS X the JDK WatchService is slow by default so enabling SensitivityWatchEventModifier.HIGH as workaround");
140                } else {
141                    log.warn("On Mac OS X the JDK WatchService is slow and it may take up till 10 seconds to notice file changes");
142                }
143            }
144
145            try {
146                Path path = dir.toPath();
147                watcher = path.getFileSystem().newWatchService();
148                // we cannot support deleting files as we don't know which routes that would be
149                if (isRecursive) {
150                    this.folderKeys = new HashMap<>();
151                    registerRecursive(watcher, path, modifier);
152                } else {
153                    registerPathToWatcher(modifier, path, watcher);
154                }                
155
156                task = new WatchFileChangesTask(watcher, path);
157
158                executorService = getCamelContext().getExecutorServiceManager().newSingleThreadExecutor(this, "FileWatcherReloadStrategy");
159                executorService.submit(task);
160            } catch (IOException e) {
161                throw ObjectHelper.wrapRuntimeCamelException(e);
162            }
163        }
164    }
165
166    private WatchKey registerPathToWatcher(WatchEvent.Modifier modifier, Path path, WatchService watcher) throws IOException {
167        WatchKey key;
168        if (modifier != null) {
169            key = path.register(watcher, new WatchEvent.Kind<?>[]{ENTRY_CREATE, ENTRY_MODIFY}, modifier);
170        } else {
171            key = path.register(watcher, ENTRY_CREATE, ENTRY_MODIFY);
172        }
173        return key;
174    }
175    
176    private void registerRecursive(final WatchService watcher, final Path root, final WatchEvent.Modifier modifier) throws IOException {
177        Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
178            @Override
179            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
180                WatchKey key = registerPathToWatcher(modifier, dir, watcher);
181                folderKeys.put(key, dir);
182                return FileVisitResult.CONTINUE;
183            }
184        });
185    }
186
187    @Override
188    protected void doStop() throws Exception {
189        super.doStop();
190
191        if (executorService != null) {
192            getCamelContext().getExecutorServiceManager().shutdownGraceful(executorService);
193            executorService = null;
194        }
195
196        if (watcher != null) {
197            IOHelper.close(watcher);
198        }
199    }
200
201    /**
202     * Background task which watches for file changes
203     */
204    protected class WatchFileChangesTask implements Runnable {
205
206        private final WatchService watcher;
207        private final Path folder;
208        private volatile boolean running;
209
210        public WatchFileChangesTask(WatchService watcher, Path folder) {
211            this.watcher = watcher;
212            this.folder = folder;
213        }
214
215        public boolean isRunning() {
216            return running;
217        }
218
219        public void run() {
220            log.debug("ReloadStrategy is starting watching folder: {}", folder);
221
222            // allow to run while starting Camel
223            while (isStarting() || isRunAllowed()) {
224                running = true;
225
226                WatchKey key;
227                try {
228                    log.trace("ReloadStrategy is polling for file changes in directory: {}", folder);
229                    // wait for a key to be available
230                    key = watcher.poll(pollTimeout, TimeUnit.MILLISECONDS);
231                } catch (InterruptedException ex) {
232                    break;
233                }
234
235                if (key != null) {
236                    Path pathToReload = null;
237                    if (isRecursive) {
238                        pathToReload = folderKeys.get(key);
239                    } else {
240                        pathToReload = folder;
241                    }
242                    
243                    for (WatchEvent<?> event : key.pollEvents()) {
244                        WatchEvent<Path> we = (WatchEvent<Path>) event;
245                        Path path = we.context();
246                        String name = pathToReload.resolve(path).toAbsolutePath().toFile().getAbsolutePath();
247                        log.trace("Modified/Created file: {}", name);
248
249                        // must be an .xml file
250                        if (name.toLowerCase(Locale.US).endsWith(".xml")) {
251                            log.debug("Modified/Created XML file: {}", name);
252                            try {
253                                FileInputStream fis = new FileInputStream(name);
254                                onReloadXml(getCamelContext(), name, fis);
255                                IOHelper.close(fis);
256                            } catch (Exception e) {
257                                log.warn("Error reloading routes from file: " + name + " due " + e.getMessage() + ". This exception is ignored.", e);
258                            }
259                        }
260                    }
261
262                    // the key must be reset after processed
263                    boolean valid = key.reset();
264                    if (!valid) {
265                        break;
266                    }
267                }
268            }
269
270            running = false;
271
272            log.info("ReloadStrategy is stopping watching folder: {}", folder);
273        }
274    }
275
276}