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