package com.psddev.cms.tool.file; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.psddev.dari.db.Query; import com.psddev.dari.db.Record; import com.psddev.dari.util.AggregateException; import com.psddev.dari.util.ImageMetadataMap; import com.psddev.dari.util.StorageItem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.ParametersAreNonnullByDefault; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.Map; import java.util.UUID; /** * Cache that stores storage item metadata in the database in order to minimize * the number of data downloads. */ public class StorageItemMetadataCache extends Record { private static final Logger LOGGER = LoggerFactory.getLogger(StorageItemMetadataCache.class); private static final LoadingCache<UUID, Deleter> DELETERS = CacheBuilder.newBuilder() .build(new CacheLoader<UUID, Deleter>() { @Override @ParametersAreNonnullByDefault public Deleter load(UUID cacheId) { Deleter deleter = new Deleter(cacheId); Thread deleterThread = new Thread(deleter); deleterThread.setDaemon(true); deleterThread.start(); return deleter; } }); @Indexed(unique = true) @Required private String key; private Map<String, Object> metadata; /** * Updates the given {@code item}'s metadata. * * @param item Can't be {@code null}. */ public static void update(StorageItem item) { String path = item.getPath(); if (path == null) { return; } String key = item.getStorage() + ":" + path; Map<String, Object> itemMetadata = item.getMetadata(); StorageItemMetadataCache cache = Query.from(StorageItemMetadataCache.class) .where("key = ?", key) .first(); if (cache == null) { cache = new StorageItemMetadataCache(); cache.key = key; if (!itemMetadata.containsKey("width") && !itemMetadata.containsKey("height")) { String contentType = item.getContentType(); if (contentType != null && contentType.startsWith("image/")) { try (InputStream imageInput = item.getData()) { ImageMetadataMap imageMetadata = new ImageMetadataMap(imageInput); List<Throwable> errors = imageMetadata.getErrors(); if (errors.isEmpty()) { cache.metadata = imageMetadata; } else { LOGGER.info("Can't read image metadata!", new AggregateException(errors)); } } catch (IOException error) { LOGGER.info("Can't read image!", error); } } } cache.saveImmediately(); } DELETERS.getUnchecked(cache.getId()).extend(); if (cache.metadata != null) { itemMetadata.putAll(cache.metadata); } } private static class Deleter implements Runnable { private final UUID cacheId; private volatile long triggerTime; public Deleter(UUID cacheId) { this.cacheId = cacheId; extend(); } public void extend() { triggerTime = System.currentTimeMillis() + 10000; } @Override public void run() { while (triggerTime > System.currentTimeMillis()) { try { Thread.sleep(1000); } catch (InterruptedException error) { break; } } Query.fromAll() .where("_id = ?", cacheId) .deleteAll(); } } }