/** * Copyright [2015] [Christian Loehnert] * <p> * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.ks.gallery; import com.google.common.net.MediaType; import de.ks.activity.executor.ActivityExecutor; import de.ks.executor.JavaFXExecutorService; import de.ks.option.Options; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.File; import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Supplier; public class GalleryResource { private static final Logger log = LoggerFactory.getLogger(GalleryResource.class); private final Set<File> files = new LinkedHashSet<>(); private final ConcurrentHashMap<File, GalleryItem> items = new ConcurrentHashMap<>(); private Consumer<List<GalleryItem>> callback; protected Supplier<GallerySettings> settingsSupplier = () -> Options.get(GallerySettings.class); protected final Set<File> parents = new HashSet<>(); protected final Map<WatchKey, File> key2Dir = new ConcurrentHashMap<>(); protected final Set<File> knownDeleted = Collections.synchronizedSet(new HashSet<>()); @Inject ActivityExecutor executor; @Inject JavaFXExecutorService javaFXExecutorService; private WatchService watchService; private Thread watchThread; private final AtomicReference<String> currentLoad = new AtomicReference<>(); protected GalleryResource() { reset(); } protected GalleryItem createItem(File file, int thumbNailSize) { try { String contentType = Files.probeContentType(file.toPath()); if (contentType == null) { return null; } MediaType parse = MediaType.parse(contentType); if (!parse.is(MediaType.ANY_IMAGE_TYPE)) { log.debug("File {} is no image", file); return null; } } catch (IOException e) { log.error("Could not probe content type of {}", file, e); return null; } try { GalleryItem descriptor = new GalleryItem(file, thumbNailSize, executor); log.debug("Created gallery item for {}", file); return descriptor; } catch (Exception e) { log.info("Could not get image descriptor for {}", file, e); throw new RuntimeException(e); } } public void setFolder(File folder, boolean recurse) { if (!folder.isDirectory()) { throw new IllegalArgumentException("Given file " + folder + " is no folder"); } ArrayList<File> files = new ArrayList<>(); SimpleFileVisitor<Path> visitor = new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { files.add(file.toFile()); return super.visitFile(file, attrs); } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { if (dir.toFile().equals(folder)) { return super.preVisitDirectory(dir, attrs); } else if (recurse) { return FileVisitResult.CONTINUE; } else { return FileVisitResult.SKIP_SUBTREE; } } }; try { Files.walkFileTree(folder.toPath(), visitor); } catch (IOException e) { log.error("Could not walk filetree {}", folder); } setFiles(files); } public synchronized void setFiles(Collection<File> files) { ArrayList<File> sorted = new ArrayList<>(files); sorted.removeAll(this.files); Collections.sort(sorted); this.files.retainAll(files); this.files.addAll(files); this.items.keySet().retainAll(files); final String currentLoadIdentifier = UUID.randomUUID().toString(); currentLoad.set(currentLoadIdentifier); int thumbNailSize = getThumbnailSize(); CompletableFuture<Void> combined = null; for (File file : sorted) { parents.add(file.getParentFile()); CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> { if (currentLoad.get().equals(currentLoadIdentifier)) { return createItem(file, thumbNailSize); } else { return null; } }, executor).thenAccept(item -> { if (item != null) { items.put(item.getFile(), item); } }); if (combined == null) { combined = future; } else { combined = CompletableFuture.allOf(combined, future); } } if (combined != null) { combined.thenApply(bla -> getItemsSorted(currentLoadIdentifier)).thenApplyAsync((List<GalleryItem> all) -> { if (callback != null) { callback.accept(all); } return all; }, javaFXExecutorService).exceptionally(t -> { log.error("Could not add items", t); return null; }); } recreateWatchService(); parents.forEach(p -> { try { WatchKey register = p.toPath().register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); key2Dir.put(register, p); } catch (IOException e) { log.error("Could not register {} at watchservice", p, e); } }); } protected List<GalleryItem> getItemsSorted(String currentLoadIdentifier) { List<GalleryItem> values = new ArrayList<>(items.values()); if (currentLoad.get().equals(currentLoadIdentifier)) { Collections.sort(values); log.info("Got all"); return values; } else { return Collections.<GalleryItem>emptyList(); } } public synchronized void reset() { this.files.clear(); key2Dir.clear(); knownDeleted.clear(); recreateWatchService(); } protected void recreateWatchService() { if (watchService != null) { try { watchService.close(); } catch (IOException e) { log.error("Could not close watchService", e); } } watchService = null; try { watchService = FileSystems.getDefault().newWatchService(); watchThread = new Thread(this::pollService); watchThread.setName("WatchService-Poll-" + getClass().getSimpleName()); watchThread.setDaemon(true); watchThread.start(); } catch (IOException e) { log.error("Could not open watchService", e); } } protected void pollService() { try { while (true) { WatchKey key = watchService.poll(); if (key != null && key.isValid()) { File parentDir = key2Dir.get(key); List<WatchEvent<?>> watchEvents = key.pollEvents(); for (WatchEvent<?> watchEvent : watchEvents) { final WatchEvent<Path> wePath = (WatchEvent<Path>) watchEvent; Path path = wePath.context(); File file = new File(parentDir, path.toFile().getName()); log.trace("Got watchevent {} with file {} and kind {}", watchEvent, file.getAbsolutePath(), watchEvent.kind()); if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_DELETE) { handleItemDeleted(file); } else if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_MODIFY) { handleItemModified(file); } else if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_CREATE) { handleItemCreated(file); } } key.reset(); } } } catch (ClosedWatchServiceException e) { log.debug("Closed watchservice normally"); } catch (Exception e) { log.error("Exception while polling on watchservice ", e); } } private void handleItemCreated(File file) { if (knownDeleted.contains(file)) { log.debug("Got previously deleted file back again {}", file); int thumbNailSize = getThumbnailSize(); GalleryItem item = createItem(file, thumbNailSize); submitItem(item); } } private void submitItem(GalleryItem item) { items.put(item.getFile(), item); List<GalleryItem> values = new ArrayList<>(items.values()); Collections.sort(values); javaFXExecutorService.submit(() -> { if (callback != null) { callback.accept(values); } }); } private void handleItemModified(File file) { int thumbNailSize = getThumbnailSize(); handleItemDeleted(file); GalleryItem item = createItem(file, thumbNailSize); submitItem(item); } public int getThumbnailSize() { return settingsSupplier.get().getThumbNailSize(); } private void handleItemDeleted(File file) { if (items.containsKey(file)) { knownDeleted.add(file); log.debug("File {} was deleted, removing it.", file); items.remove(file); List<GalleryItem> itemsSorted = getItemsSorted(currentLoad.get()); javaFXExecutorService.submit(() -> { if (callback != null) { callback.accept(itemsSorted); } }); } } public void setCallback(Consumer<List<GalleryItem>> callback) { this.callback = callback; } public Consumer<List<GalleryItem>> getCallback() { return callback; } public Collection<GalleryItem> getItems() { return items.values(); } }