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}