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}