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}