001package com.credibledoc.substitution.reporting.tracking; 002 003import com.credibledoc.substitution.core.context.SubstitutionContext; 004import com.credibledoc.substitution.core.exception.SubstitutionRuntimeException; 005import com.credibledoc.substitution.core.pair.Pair; 006import com.credibledoc.substitution.core.resource.ResourceService; 007import com.credibledoc.substitution.core.resource.TemplateResource; 008import com.credibledoc.substitution.reporting.replacement.ReplacementService; 009import org.slf4j.Logger; 010import org.slf4j.LoggerFactory; 011 012import java.io.File; 013import java.io.IOException; 014import java.nio.file.FileSystems; 015import java.nio.file.FileVisitResult; 016import java.nio.file.Files; 017import java.nio.file.Path; 018import java.nio.file.SimpleFileVisitor; 019import java.nio.file.WatchEvent; 020import java.nio.file.WatchKey; 021import java.nio.file.WatchService; 022import java.nio.file.attribute.BasicFileAttributes; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.Map; 026import java.util.Set; 027 028import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; 029import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; 030import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; 031import static java.nio.file.StandardWatchEventKinds.OVERFLOW; 032 033/** 034 * Watches directories for changes to files and runs template generation in case when some template in the 035 * tracked directory changed. 036 * <p> 037 * This stateful object contains {@link #watchService}, {@link #map}, {@link #substitutionContext} 038 * and {@link #fragmentDependencyMap} instances. 039 * 040 * @author Kyrylo Semenko 041 */ 042public class TrackingService { 043 private static final Logger logger = LoggerFactory.getLogger(TrackingService.class); 044 045 /** 046 * See the {@link WatchService} description. 047 */ 048 private final WatchService watchService; 049 050 /** 051 * Contains {@link Path}s watched by {@link WatchKey}s. 052 */ 053 private final Map<WatchKey, Path> map; 054 055 /** 056 * Contains the application current state. 057 */ 058 private final SubstitutionContext substitutionContext; 059 060 /** 061 * Key is a fragment that can be changed, and Values are dependant files which should be 062 * generated in case of the fragment change. 063 */ 064 private final Map<Path, Set<Path>> fragmentDependencyMap; 065 066 public TrackingService(SubstitutionContext substitutionContext) throws IOException { 067 this.watchService = FileSystems.getDefault().newWatchService(); 068 this.map = new HashMap<>(); 069 this.substitutionContext = substitutionContext; 070 this.fragmentDependencyMap = new HashMap<>(); 071 } 072 073 private void addFromRepository() { 074 try { 075 Set<Path> toRegister = new HashSet<>(); 076 for (Pair<Path, Path> pair : substitutionContext.getTrackableRepository().getPairs()) { 077 Path fragmentPath = pair.getLeft(); 078 Path templatePath = pair.getRight(); 079 if (fragmentDependencyMap.containsKey(fragmentPath)) { 080 fragmentDependencyMap.get(fragmentPath).add(templatePath); 081 } else { 082 HashSet<Path> value = new HashSet<>(); 083 value.add(templatePath); 084 fragmentDependencyMap.put(fragmentPath, value); 085 } 086 toRegister.add(fragmentPath.getParent()); 087 Path parent = fragmentPath.getParent().getParent(); 088 if (parent != null) { 089 toRegister.add(parent); 090 } 091 } 092 for (Path path : toRegister) { 093 register(path); 094 } 095 } catch (Exception e) { 096 throw new SubstitutionRuntimeException(e); 097 } 098 } 099 100 public void track() throws IOException, InterruptedException { 101 addFromRepository(); 102 ResourceService resourceService = ResourceService.getInstance(); 103 String templatesResource = substitutionContext.getConfiguration().getTemplatesResource(); 104 File templatesDir = resourceService.findTemplatesDir(templatesResource); 105 Path path = templatesDir.toPath(); 106 registerAll(path); 107 processEvents(); 108 } 109 110 private void processEvents() throws IOException, InterruptedException { 111 while (true) { 112 // wait for key to be signalled 113 WatchKey key = watchService.take(); 114 115 TrackingResult trackingResult = processWatchKey(key); 116 if (TrackingResult.FAILED == trackingResult) { 117 break; 118 } 119 } 120 } 121 122 private TrackingResult processWatchKey(WatchKey watchKey) throws IOException { 123 Path dir = map.get(watchKey); 124 if (dir == null) { 125 logger.error("WatchKey not recognized."); 126 return TrackingResult.FAILED; 127 } 128 129 for (WatchEvent<?> event : watchKey.pollEvents()) { 130 processEvent(dir, event); 131 } 132 133 // reset key and remove from set if directory no longer accessible 134 boolean valid = watchKey.reset(); 135 if (!valid) { 136 logger.trace("WatchKey will be deleted '{}'", dir); 137 map.remove(watchKey); 138 139 // all directories are inaccessible 140 if (map.isEmpty()) { 141 return TrackingResult.SUCCESSFUL; 142 } 143 } 144 return TrackingResult.SUCCESSFUL; 145 } 146 147 @SuppressWarnings("unchecked") 148 private void processEvent(Path dir, WatchEvent<?> event) throws IOException { 149 WatchEvent.Kind<?> kind = event.kind(); 150 151 if (kind == OVERFLOW) { 152 logger.error("WatchKey OVERFLOW"); 153 return; 154 } 155 156 // Context for the directory entry event is a file name of entry 157 WatchEvent<Path> ev = (WatchEvent<Path>) event; 158 Path name = ev.context(); 159 Path path = dir.resolve(name); 160 161 logger.trace("WatchEvent.Kind: {}, {}", kind, path); 162 163 // if fragment is deleted 164 boolean fragmentDeleted = kind == ENTRY_DELETE && isFragment(path); 165 if (!path.toString().endsWith("~") && fragmentDeleted) { 166 deleteFragment(path); 167 return; 168 } 169 170 // if a directory is deleted 171 if (kind == ENTRY_DELETE && (Files.isDirectory(path) || map.containsValue(path))) { 172 deleteDir(path); 173 } 174 175 // if a directory is created, then register it and all its sub-directories 176 if (kind == ENTRY_CREATE && Files.isDirectory(path)) { 177 createDir(path); 178 } 179 180 // if a file is created or changed, then find placeholder(s) and if some exist, generate content from the template 181 if (!path.toString().endsWith("~") && Files.isRegularFile(path)) { 182 processFile(kind, path); 183 } 184 } 185 186 private boolean isFragment(Path path) { 187 String templatesResource = substitutionContext.getConfiguration().getTemplatesResource(); 188 Path templatesPath = ResourceService.getInstance().findTemplatesDir(templatesResource).toPath(); 189 String templatesPathNormalized = templatesPath.toAbsolutePath().normalize().toString(); 190 String pathNormalized = path.toAbsolutePath().normalize().toString(); 191 return !pathNormalized.startsWith(templatesPathNormalized); 192 } 193 194 private void processFile(WatchEvent.Kind<?> kind, Path path) { 195 try { 196 ReplacementService replacementService = ReplacementService.getInstance(); 197 boolean isFragment = isFragment(path); 198 if (isFragment && fragmentDependencyMap.containsKey(path)) { 199 for (Path templatePath : fragmentDependencyMap.get(path)) { 200 TemplateResource templateResource = new TemplateResource(templatePath); 201 replacementService.insertContentIntoTemplate(templateResource, substitutionContext); 202 } 203 } 204 if (!isFragment) { 205 TemplateResource templateResource = new TemplateResource(path); 206 if (kind == ENTRY_DELETE) { 207 File generatedFile = 208 replacementService.getTargetFile(templateResource, substitutionContext); 209 logger.trace("File will be deleted '{}'", generatedFile.getAbsolutePath()); 210 Files.deleteIfExists(generatedFile.toPath()); 211 } else { 212 replacementService.insertContentIntoTemplate(templateResource, substitutionContext); 213 } 214 } 215 } catch (Exception e) { 216 logger.trace(e.getMessage(), e); 217 } 218 } 219 220 private void deleteFragment(Path path) { 221 try { 222 ReplacementService replacementService = ReplacementService.getInstance(); 223 boolean isFragment = isFragment(path); 224 if (isFragment) { 225 for (Path nextKey : getChildrenFragmentTemplates(path)) { 226 TemplateResource templateResource = new TemplateResource(nextKey); 227 replacementService.insertContentIntoTemplate(templateResource, substitutionContext); 228 } 229 } 230 } catch (Exception e) { 231 logger.trace(e.getMessage(), e); 232 } 233 } 234 235 private Set<Path> getChildrenFragmentTemplates(Path path) { 236 Set<Path> result = new HashSet<>(); 237 String dirName = path.normalize().toString(); 238 for (Map.Entry<Path, Set<Path>> entry : fragmentDependencyMap.entrySet()) { 239 Path mapKey = entry.getKey(); 240 String mapKeyName = mapKey.toString(); 241 if (mapKeyName.startsWith(dirName)) { 242 result.addAll(entry.getValue()); 243 } 244 } 245 return result; 246 } 247 248 private void createDir(Path path) throws IOException { 249 if (!isFragment(path)) { 250 ReplacementService replacementService = ReplacementService.getInstance(); 251 TemplateResource templateResource = new TemplateResource(path); 252 File generatedDir = replacementService.getTargetFile(templateResource, substitutionContext); 253 Files.createDirectories(generatedDir.toPath()); 254 } 255 registerAll(path); 256 } 257 258 private void deleteDir(Path path) throws IOException { 259 ReplacementService replacementService = ReplacementService.getInstance(); 260 TemplateResource templateResource = new TemplateResource(path); 261 File generatedDir = replacementService.getTargetFile(templateResource, substitutionContext); 262 deleteDirRecursively(generatedDir); 263 } 264 265 private void deleteDirRecursively(File directoryToDelete) throws IOException { 266 File[] allContents = directoryToDelete.listFiles(); 267 if (allContents != null) { 268 for (File file : allContents) { 269 deleteDirRecursively(file); 270 } 271 } 272 logger.trace("File will be deleted '{}'", directoryToDelete.getAbsolutePath()); 273 Files.deleteIfExists(directoryToDelete.toPath()); 274 } 275 276 /** 277 * Register the given directory, all its sub-directories and all files, with the {@link #watchService}. 278 * 279 * @param start the root path for watching 280 * @throws IOException in case of read or write exceptions 281 */ 282 private void registerAll(final Path start) throws IOException { 283 // register directory and sub-directories 284 Files.walkFileTree(start, new SimpleFileVisitor<Path>() { 285 @Override 286 public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) throws IOException { 287 register(path); 288 reloadFragments(path); 289 return FileVisitResult.CONTINUE; 290 } 291 }); 292 } 293 294 private void reloadFragments(Path path) { 295 ReplacementService replacementService = ReplacementService.getInstance(); 296 if (isFragment(path)) { 297 File[] files = path.toFile().listFiles(); 298 if (files == null) { 299 return; 300 } 301 for (File file : files) { 302 if (file.isFile() && isFragment(file.toPath()) && fragmentDependencyMap.containsKey(file.toPath())) { 303 for (Path templatePath : fragmentDependencyMap.get(file.toPath())) { 304 TemplateResource templateResource = new TemplateResource(templatePath); 305 replacementService.insertContentIntoTemplate(templateResource, substitutionContext); 306 } 307 } 308 } 309 } 310 } 311 312 /** 313 * Register the given directory with the WatchService 314 * @param path the directory for watching 315 * @throws IOException in case of read or write exceptions 316 */ 317 private void register(Path path) throws IOException { 318 WatchKey key = path.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); 319 map.put(key, path); 320 } 321 322}