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 ExecutorService executorService;
060    private WatchFileChangesTask task;
061    private Map<WatchKey, Path> folderKeys;
062    private long pollTimeout = 2000;
063
064    public FileWatcherReloadStrategy() {
065        setRecursive(false);
066    }
067
068    public FileWatcherReloadStrategy(String directory) {
069        setFolder(directory);
070        setRecursive(false);
071    }
072    
073    public FileWatcherReloadStrategy(String directory, boolean isRecursive) {
074        setFolder(directory);
075        setRecursive(isRecursive);
076    }
077
078    public void setFolder(String folder) {
079        this.folder = folder;
080    }
081    
082    public void setRecursive(boolean isRecursive) {
083        this.isRecursive = isRecursive;
084    }
085
086    /**
087     * Sets the poll timeout in millis. The default value is 2000.
088     */
089    public void setPollTimeout(long pollTimeout) {
090        this.pollTimeout = pollTimeout;
091    }
092
093    @ManagedAttribute(description = "Folder being watched")
094    public String getFolder() {
095        return folder;
096    }
097    
098    @ManagedAttribute(description = "Whether the reload strategy watches directory recursively")
099    public boolean isRecursive() {
100        return isRecursive;
101    }
102
103    @ManagedAttribute(description = "Whether the watcher is running")
104    public boolean isRunning() {
105        return task != null && task.isRunning();
106    }
107
108    @Override
109    protected void doStart() throws Exception {
110        super.doStart();
111
112        if (folder == null) {
113            // no folder configured
114            return;
115        }
116
117        File dir = new File(folder);
118        if (dir.exists() && dir.isDirectory()) {
119            log.info("Starting ReloadStrategy to watch directory: {}", dir);
120
121            WatchEvent.Modifier modifier = null;
122
123            // if its mac OSX then attempt to apply workaround or warn its slower
124            String os = ObjectHelper.getSystemProperty("os.name", "");
125            if (os.toLowerCase(Locale.US).startsWith("mac")) {
126                // this modifier can speedup the scanner on mac osx (as java on mac has no native file notification integration)
127                Class<WatchEvent.Modifier> clazz = getCamelContext().getClassResolver().resolveClass("com.sun.nio.file.SensitivityWatchEventModifier", WatchEvent.Modifier.class);
128                if (clazz != null) {
129                    WatchEvent.Modifier[] modifiers = clazz.getEnumConstants();
130                    for (WatchEvent.Modifier mod : modifiers) {
131                        if ("HIGH".equals(mod.name())) {
132                            modifier = mod;
133                            break;
134                        }
135                    }
136                }
137                if (modifier != null) {
138                    log.info("On Mac OS X the JDK WatchService is slow by default so enabling SensitivityWatchEventModifier.HIGH as workaround");
139                } else {
140                    log.warn("On Mac OS X the JDK WatchService is slow and it may take up till 10 seconds to notice file changes");
141                }
142            }
143
144            try {
145                Path path = dir.toPath();
146                WatchService watcher = path.getFileSystem().newWatchService();
147                // we cannot support deleting files as we don't know which routes that would be
148                if (isRecursive) {
149                    this.folderKeys = new HashMap<WatchKey, Path>();
150                    registerRecursive(watcher, path, modifier);
151                } else {
152                    registerPathToWatcher(modifier, path, watcher);
153                }                
154
155                task = new WatchFileChangesTask(watcher, path);
156
157                executorService = getCamelContext().getExecutorServiceManager().newSingleThreadExecutor(this, "FileWatcherReloadStrategy");
158                executorService.submit(task);
159            } catch (IOException e) {
160                throw ObjectHelper.wrapRuntimeCamelException(e);
161            }
162        }
163    }
164
165    private WatchKey registerPathToWatcher(WatchEvent.Modifier modifier, Path path, WatchService watcher) throws IOException {
166        WatchKey key;
167        if (modifier != null) {
168            key = path.register(watcher, new WatchEvent.Kind<?>[]{ENTRY_CREATE, ENTRY_MODIFY}, modifier);
169        } else {
170            key = path.register(watcher, ENTRY_CREATE, ENTRY_MODIFY);
171        }
172        return key;
173    }
174    
175    private void registerRecursive(final WatchService watcher, final Path root, final WatchEvent.Modifier modifier) throws IOException {
176        Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
177            @Override
178            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
179                WatchKey key = registerPathToWatcher(modifier, dir, watcher);
180                folderKeys.put(key, dir);
181                return FileVisitResult.CONTINUE;
182            }
183        });
184    }
185
186    @Override
187    protected void doStop() throws Exception {
188        super.doStop();
189
190        if (executorService != null) {
191            getCamelContext().getExecutorServiceManager().shutdownGraceful(executorService);
192            executorService = null;
193        }
194    }
195
196    /**
197     * Background task which watches for file changes
198     */
199    protected class WatchFileChangesTask implements Runnable {
200
201        private final WatchService watcher;
202        private final Path folder;
203        private volatile boolean running;
204
205        public WatchFileChangesTask(WatchService watcher, Path folder) {
206            this.watcher = watcher;
207            this.folder = folder;
208        }
209
210        public boolean isRunning() {
211            return running;
212        }
213
214        public void run() {
215            log.debug("ReloadStrategy is starting watching folder: {}", folder);
216
217            // allow to run while starting Camel
218            while (isStarting() || isRunAllowed()) {
219                running = true;
220
221                WatchKey key;
222                try {
223                    log.trace("ReloadStrategy is polling for file changes in directory: {}", folder);
224                    // wait for a key to be available
225                    key = watcher.poll(pollTimeout, TimeUnit.MILLISECONDS);
226                } catch (InterruptedException ex) {
227                    break;
228                }
229
230                if (key != null) {
231                    Path pathToReload = null;
232                    if (isRecursive) {
233                        pathToReload = folderKeys.get(key);
234                    } else {
235                        pathToReload = folder;
236                    }
237                    
238                    for (WatchEvent<?> event : key.pollEvents()) {
239                        WatchEvent<Path> we = (WatchEvent<Path>) event;
240                        Path path = we.context();
241                        String name = pathToReload.resolve(path).toAbsolutePath().toFile().getAbsolutePath();
242                        log.trace("Modified/Created file: {}", name);
243
244                        // must be an .xml file
245                        if (name.toLowerCase(Locale.US).endsWith(".xml")) {
246                            log.debug("Modified/Created XML file: {}", name);
247                            try {
248                                FileInputStream fis = new FileInputStream(name);
249                                onReloadXml(getCamelContext(), name, fis);
250                                IOHelper.close(fis);
251                            } catch (Exception e) {
252                                log.warn("Error reloading routes from file: " + name + " due " + e.getMessage() + ". This exception is ignored.", e);
253                            }
254                        }
255                    }
256
257                    // the key must be reset after processed
258                    boolean valid = key.reset();
259                    if (!valid) {
260                        break;
261                    }
262                }
263            }
264
265            running = false;
266
267            log.info("ReloadStrategy is stopping watching folder: {}", folder);
268        }
269    }
270
271}