package com.psddev.cms.rte; import com.psddev.cms.db.ExternalContent; import com.psddev.cms.db.ExternalContentProvider; import com.psddev.dari.db.Database; import com.psddev.dari.db.Query; import com.psddev.dari.db.Record; import com.psddev.dari.util.ClassFinder; import com.psddev.dari.util.IoUtils; import com.psddev.dari.util.ObjectUtils; import com.psddev.dari.util.StringUtils; import com.psddev.dari.util.TypeDefinition; import com.psddev.dari.util.TypeReference; import org.jsoup.Jsoup; import org.jsoup.nodes.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * Cache that stores external content responses in the database in order to * minimize the number of API calls. * * @see <a href="http://oembed.com/">oEmbed Specification</a> */ public class ExternalContentCache extends Record { private static final Logger LOGGER = LoggerFactory.getLogger(ExternalContentCache.class); private long created; private Map<String, Object> response; /** * Returns a response that can be used to render the given {@code url} and * should fit within the given {@code maximumWidth} and * {@code maximumHeight}. * * @param url Nullable. * @param maximumWidth Nullable. * @param maximumHeight Nullable. * @return Nullable. */ public static Map<String, Object> get(String url, Integer maximumWidth, Integer maximumHeight) { if (url == null) { return null; } // Cache key based on all the arguments. String key = url + ":" + maximumWidth + ":" + maximumHeight; UUID id = UUID.nameUUIDFromBytes(key.getBytes(StandardCharsets.UTF_8)); // Already cached? ExternalContentCache cache = Query.from(ExternalContentCache.class).where("_id = ?", id).first(); long now = Database.Static.getDefault().now(); if (cache != null) { Map<String, Object> response = cache.response; Long cacheAge = null; // Response contains suggested cache lifetime in seconds? if (response != null) { cacheAge = ObjectUtils.to(Long.class, response.get("cache_age")); } // Nope, default to 1 hour. if (cacheAge == null) { cacheAge = TimeUnit.HOURS.toSeconds(1); } // Not expired yet. if (cache.created + TimeUnit.SECONDS.toMillis(cacheAge) > now) { return response; } } // See if there's ExternalContentProvider set up to handle the URL. Map<String, Object> response = null; ExternalContent content = new ExternalContent(); content.setUrl(url); content.setMaximumWidth(maximumWidth); content.setMaximumHeight(maximumHeight); for (Class<? extends ExternalContentProvider> providerClass : ClassFinder.findConcreteClasses(ExternalContentProvider.class)) { ExternalContentProvider provider = TypeDefinition.getInstance(providerClass).newInstance(); response = provider.createResponse(content); if (response != null) { break; } } // If not, try looking for oEmbed information. if (response == null) { try { for (Element link : Jsoup.connect(url).get().select("link[type=application/json+oembed]")) { String oEmbedUrl = link.attr("href"); if (!ObjectUtils.isBlank(oEmbedUrl)) { response = ObjectUtils.to( new TypeReference<Map<String, Object>>() { }, ObjectUtils.fromJson(IoUtils.toString(new URL( StringUtils.addQueryParameters(oEmbedUrl, "maxwidth", maximumWidth, "maxheight", maximumHeight)), 5000))); if (response != null) { break; } } } } catch (IOException error) { LOGGER.warn( String.format("Can't download from [%s] to get the oEmbed URL!", url), error); } } // Legacy ExternalContent support. if (response != null) { response.put("_url", url); response.put("_maximumWidth", maximumWidth); response.put("_maximumHeight", maximumHeight); } // Save the cache. cache = new ExternalContentCache(); cache.getState().setId(id); cache.created = now; cache.response = response; cache.saveImmediately(); return response; } }