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.wicket.core.util.watch;
018
019import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
020import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
021import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
022
023import java.io.File;
024import java.io.IOException;
025import java.nio.file.FileSystems;
026import java.nio.file.FileVisitResult;
027import java.nio.file.Files;
028import java.nio.file.Path;
029import java.nio.file.Paths;
030import java.nio.file.SimpleFileVisitor;
031import java.nio.file.WatchEvent;
032import java.nio.file.WatchKey;
033import java.nio.file.WatchService;
034import java.nio.file.attribute.BasicFileAttributes;
035import java.util.List;
036
037import org.apache.wicket.Application;
038import org.apache.wicket.ThreadContext;
039import org.apache.wicket.WicketRuntimeException;
040import org.apache.wicket.util.io.IOUtils;
041import org.apache.wicket.util.string.Strings;
042import org.apache.wicket.util.thread.ICode;
043import org.apache.wicket.util.thread.Task;
044import java.time.Duration;
045import org.apache.wicket.util.watch.ModificationWatcher;
046import org.slf4j.Logger;
047import org.slf4j.LoggerFactory;
048
049/**
050 * An extension of ModificationWatcher that removes the NotFound entries from
051 * the MarkupCache for newly created files.
052 *
053 * By default MarkupCache registers Markup.NO_MARKUP value for each requested but
054 * not found markup file. Later when the user creates the markup file the MarkupCache
055 * should be notified.
056 *
057 * @since 7.0.0
058 */
059public class Nio2ModificationWatcher extends ModificationWatcher
060{
061        private static final Logger LOG = LoggerFactory.getLogger(Nio2ModificationWatcher.class);
062
063        private final WatchService watchService;
064        private final Application application;
065
066        /** the <code>Task</code> to run */
067        private Task task;
068
069        /**
070         * Constructor.
071         *
072         * @param application
073         *              The application that manages the caches
074         * @param pollFrequency
075         *              How often to check on <code>IModifiable</code>s
076         */
077        public Nio2ModificationWatcher(final Application application, Duration pollFrequency)
078        {
079                try
080                {
081                        this.application = application;
082
083                        this.watchService = FileSystems.getDefault().newWatchService();
084                        registerWatchables(watchService);
085
086                        start(pollFrequency);
087
088                } catch (IOException iox)
089                {
090                        throw new WicketRuntimeException("Cannot get the watch service", iox);
091                }
092        }
093
094        @Override
095        public void start(final Duration pollFrequency)
096        {
097                // Construct task with the given polling frequency
098                task = new Task("Wicket-ModificationWatcher-NIO2");
099
100                task.run(pollFrequency, new ICode() {
101                        @Override
102                        public void run(final Logger log)
103                        {
104                                checkCreated(log);
105                                checkModified();
106                        }
107                });
108        }
109
110        /**
111         * Checks for newly created files and folders.
112         * New folders are registered to be watched.
113         * New files are removed from the MarkupCache because there could be
114         * {@link org.apache.wicket.markup.Markup#NO_MARKUP} (Not Found) entries for them already.
115         * @param log
116         *              a logger that can be used to log the events
117         */
118        protected void checkCreated(Logger log)
119        {
120                WatchKey watchKey = watchService.poll();
121                if (watchKey != null)
122                {
123                        List<WatchEvent<?>> events = watchKey.pollEvents();
124                        for (WatchEvent<?> event : events)
125                        {
126                                WatchEvent.Kind<?> eventKind = event.kind();
127                                Path eventPath = (Path) event.context();
128
129                                if (eventKind == ENTRY_CREATE)
130                                {
131                                        entryCreated(eventPath, log);
132                                }
133                                else if (eventKind == ENTRY_DELETE)
134                                {
135                                        entryDeleted(eventPath, log);
136                                }
137                                else if (eventKind == ENTRY_MODIFY)
138                                {
139                                        entryModified(eventPath, log);
140                                }
141                        }
142
143                        watchKey.reset();
144                }
145        }
146
147        /**
148         * A callback method called when a new Path entry is modified
149         *
150         * @param path
151         *              the modified path
152         * @param log
153         *              a logger that can be used to log the events
154         */
155        protected void entryModified(Path path, Logger log)
156        {
157        }
158
159        /**
160         * A callback method called when a new Path entry is deleted
161         *
162         * @param path
163         *              the deleted path
164         * @param log
165         *              a logger that can be used to log the events
166         */
167        protected void entryDeleted(Path path, Logger log)
168        {
169        }
170
171        /**
172         * A callback method called when a new Path entry is created
173         *
174         * @param path
175         *              the new path entry
176         * @param log
177         *              a logger that can be used to log the events
178         */
179        protected void entryCreated(Path path, Logger log)
180        {
181                if (Files.isDirectory(path))
182                {
183                        try
184                        {
185                                // a directory is created. register it for notifications
186                                register(path, watchService);
187                        } catch (IOException iox)
188                        {
189                                log.warn("Cannot register folder '" + path + "' to be watched.", iox);
190                        }
191                }
192                else
193                {
194                        // A new file is created. We need to clear the NOT_FOUND entry that may have been added earlier.
195                        // MarkupCache keys are fully qualified URIs
196                        String absolutePath = path.toAbsolutePath().toFile().toURI().toString();
197
198                        try
199                        {
200                                ThreadContext.setApplication(application);
201                                application.getMarkupSettings()
202                                                .getMarkupFactory().getMarkupCache().removeMarkup(absolutePath);
203                        } finally {
204                                ThreadContext.setApplication(null);
205                        }
206                }
207        }
208
209        @Override
210        public void destroy()
211        {
212                try
213                {
214                        super.destroy();
215
216                        if (task != null)
217                        {
218                                task.interrupt();
219                        }
220                } finally
221                {
222                        IOUtils.closeQuietly(watchService);
223                }
224        }
225
226        /**
227         * Registers all classpath folder entries and their subfolders in the {@code #watchService}.
228         * 
229         * @param watchService
230         *      the watch service that will send the notifications
231         * @throws IOException
232         */
233        private void registerWatchables(final WatchService watchService) throws IOException
234        {
235                String classpath = System.getProperty("java.class.path");
236
237                String[] classPathEntries = Strings.split(classpath, File.pathSeparatorChar);
238                for (String classPathEntry : classPathEntries)
239                {
240                        if (classPathEntry.endsWith(".jar") == false)
241                        {
242                                Path folder = Paths.get(classPathEntry);
243                                if (Files.isDirectory(folder))
244                                {
245                                        register(folder, watchService);
246
247                                        Files.walkFileTree(folder, new SimpleFileVisitor<Path>() {
248                                                @Override
249                                                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
250                                                {
251                                                        register(dir, watchService);
252                                                        return FileVisitResult.CONTINUE;
253                                                }
254                                        });
255                                }
256                        }
257                }
258        }
259        
260        private void register(final Path folder, final WatchService watchService) throws IOException
261        {
262                WatchEvent.Kind[] watchedKinds = getWatchedKinds(folder);
263                LOG.debug("Registering folder '{}' to the watching service with kinds: {}", folder, watchedKinds);
264                folder.register(watchService, watchedKinds);
265        }
266
267        /**
268         * @param folder
269         *          the folder that will be watched
270         * @return an array of watch event kinds to use for the watching of the given folder
271         */
272        protected WatchEvent.Kind[] getWatchedKinds(Path folder)
273        {
274                return new WatchEvent.Kind[] {ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY};
275        }
276}