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.Path;
023import java.nio.file.WatchEvent;
024import java.nio.file.WatchKey;
025import java.nio.file.WatchService;
026import java.util.Locale;
027import java.util.concurrent.ExecutorService;
028import java.util.concurrent.TimeUnit;
029
030import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
031import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
032
033import org.apache.camel.api.management.ManagedAttribute;
034import org.apache.camel.api.management.ManagedResource;
035import org.apache.camel.support.ReloadStrategySupport;
036import org.apache.camel.util.IOHelper;
037import org.apache.camel.util.ObjectHelper;
038
039/**
040 * A file based {@link org.apache.camel.spi.ReloadStrategy} which watches a file folder
041 * for modified files and reload on file changes.
042 * <p/>
043 * This implementation uses the JDK {@link WatchService} to watch for when files are
044 * created or modified. Mac OS X users should be noted the osx JDK does not support
045 * native file system changes and therefore the watch service is much slower than on
046 * linux or windows systems.
047 */
048@ManagedResource(description = "Managed FileWatcherReloadStrategy")
049public class FileWatcherReloadStrategy extends ReloadStrategySupport {
050
051    private String folder;
052    private ExecutorService executorService;
053    private WatchFileChangesTask task;
054
055    public FileWatcherReloadStrategy() {
056    }
057
058    public FileWatcherReloadStrategy(String directory) {
059        setFolder(directory);
060    }
061
062    public void setFolder(String folder) {
063        this.folder = folder;
064    }
065
066    @ManagedAttribute(description = "Folder being watched")
067    public String getFolder() {
068        return folder;
069    }
070
071    @ManagedAttribute(description = "Whether the watcher is running")
072    public boolean isRunning() {
073        return task != null && task.isRunning();
074    }
075
076    @Override
077    protected void doStart() throws Exception {
078        super.doStart();
079
080        if (folder == null) {
081            // no folder configured
082            return;
083        }
084
085        File dir = new File(folder);
086        if (dir.exists() && dir.isDirectory()) {
087            log.info("Starting ReloadStrategy to watch directory: {}", dir);
088
089            WatchEvent.Modifier modifier = null;
090
091            // if its mac OSX then attempt to apply workaround or warn its slower
092            String os = ObjectHelper.getSystemProperty("os.name", "");
093            if (os.toLowerCase(Locale.US).startsWith("mac")) {
094                // this modifier can speedup the scanner on mac osx (as java on mac has no native file notification integration)
095                Class<WatchEvent.Modifier> clazz = getCamelContext().getClassResolver().resolveClass("com.sun.nio.file.SensitivityWatchEventModifier", WatchEvent.Modifier.class);
096                if (clazz != null) {
097                    WatchEvent.Modifier[] modifiers = clazz.getEnumConstants();
098                    for (WatchEvent.Modifier mod : modifiers) {
099                        if ("HIGH".equals(mod.name())) {
100                            modifier = mod;
101                            break;
102                        }
103                    }
104                }
105                if (modifier != null) {
106                    log.info("On Mac OS X the JDK WatchService is slow by default so enabling SensitivityWatchEventModifier.HIGH as workaround");
107                } else {
108                    log.warn("On Mac OS X the JDK WatchService is slow and it may take up till 10 seconds to notice file changes");
109                }
110            }
111
112            try {
113                Path path = dir.toPath();
114                WatchService watcher = path.getFileSystem().newWatchService();
115                // we cannot support deleting files as we don't know which routes that would be
116                if (modifier != null) {
117                    path.register(watcher, new WatchEvent.Kind<?>[]{ENTRY_CREATE, ENTRY_MODIFY}, modifier);
118                } else {
119                    path.register(watcher, ENTRY_CREATE, ENTRY_MODIFY);
120                }
121
122                task = new WatchFileChangesTask(watcher, path);
123
124                executorService = getCamelContext().getExecutorServiceManager().newSingleThreadExecutor(this, "FileWatcherReloadStrategy");
125                executorService.submit(task);
126            } catch (IOException e) {
127                throw ObjectHelper.wrapRuntimeCamelException(e);
128            }
129        }
130    }
131
132    @Override
133    protected void doStop() throws Exception {
134        super.doStop();
135
136        if (executorService != null) {
137            getCamelContext().getExecutorServiceManager().shutdownGraceful(executorService);
138            executorService = null;
139        }
140    }
141
142    /**
143     * Background task which watches for file changes
144     */
145    protected class WatchFileChangesTask implements Runnable {
146
147        private final WatchService watcher;
148        private final Path folder;
149        private volatile boolean running;
150
151        public WatchFileChangesTask(WatchService watcher, Path folder) {
152            this.watcher = watcher;
153            this.folder = folder;
154        }
155
156        public boolean isRunning() {
157            return running;
158        }
159
160        public void run() {
161            log.debug("ReloadStrategy is starting watching folder: {}", folder);
162
163            // allow to run while starting Camel
164            while (isStarting() || isRunAllowed()) {
165                running = true;
166
167                WatchKey key;
168                try {
169                    log.trace("ReloadStrategy is polling for file changes in directory: {}", folder);
170                    // wait for a key to be available
171                    key = watcher.poll(2, TimeUnit.SECONDS);
172                } catch (InterruptedException ex) {
173                    break;
174                }
175
176                if (key != null) {
177                    for (WatchEvent<?> event : key.pollEvents()) {
178                        WatchEvent<Path> we = (WatchEvent<Path>) event;
179                        Path path = we.context();
180                        String name = folder.resolve(path).toAbsolutePath().toFile().getAbsolutePath();
181                        log.trace("Modified/Created file: {}", name);
182
183                        // must be an .xml file
184                        if (name.toLowerCase(Locale.US).endsWith(".xml")) {
185                            log.debug("Modified/Created XML file: {}", name);
186                            try {
187                                FileInputStream fis = new FileInputStream(name);
188                                onReloadXml(getCamelContext(), name, fis);
189                                IOHelper.close(fis);
190                            } catch (Exception e) {
191                                log.warn("Error reloading routes from file: " + name + " due " + e.getMessage() + ". This exception is ignored.", e);
192                            }
193                        }
194                    }
195
196                    // the key must be reset after processed
197                    boolean valid = key.reset();
198                    if (!valid) {
199                        break;
200                    }
201                }
202            }
203
204            running = false;
205
206            log.info("ReloadStrategy is stopping watching folder: {}", folder);
207        }
208    }
209
210}