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}