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}