Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
<checkstyle.version>12.3.1</checkstyle.version>
<commons-beanutils.version>1.11.0</commons-beanutils.version>
<commons-logging.version>1.3.5</commons-logging.version>
<ehcache.version>2.10.9.2</ehcache.version>
<ehcache.version>3.11.1</ehcache.version>
<httpclient.version>4.5.14</httpclient.version>
<httpcore.version>4.4.16</httpcore.version>
<jackson.version>2.20.1</jackson.version>
Expand Down Expand Up @@ -181,16 +181,22 @@
<artifactId>jakarta.xml.bind-api</artifactId>
<version>${jakarta.xml.bind-api.version}</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>${ehcache.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient.version}</version>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>${ehcache.version}</version>
<exclusions>
<exclusion>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
Expand Down
92 changes: 92 additions & 0 deletions src/main/java/ac/simons/oembed/OembedResponseExpiryPolicy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Created by Michael Simons, michael-simons.eu
* and released under The BSD License
* http://www.opensource.org/licenses/bsd-license.php
*
* Copyright (c) 2010-2026, Michael Simons
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of michael-simons.eu nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package ac.simons.oembed;

import java.time.Duration;
import java.util.function.Supplier;

import org.ehcache.expiry.ExpiryPolicy;

/**
* A custom {@link ExpiryPolicy} for Ehcache 3.x that extracts the TTL from an
* {@link OembedResponse}, enabling per-entry expiration times.
*
* @author Oliver Lockwood
* @author Michael J. Simons
* @since 2026-01-17
*/
final class OembedResponseExpiryPolicy implements ExpiryPolicy<String, OembedResponseWrapper> {

/**
* Time in seconds responses are cached. Used if the response has no cache_age.
*/
private long defaultCacheAge = 3600;

@Override
public Duration getExpiryForCreation(String key, OembedResponseWrapper value) {
return getExpiryOf(value);
}

@Override
public Duration getExpiryForAccess(String key, Supplier<? extends OembedResponseWrapper> value) {
return null;
}

@Override
public Duration getExpiryForUpdate(String key, Supplier<? extends OembedResponseWrapper> oldValue,
OembedResponseWrapper newValue) {
return getExpiryOf(newValue);
}

Duration getExpiryOf(OembedResponseWrapper wrapper) {
long cacheAge;
if (wrapper.value() != null && wrapper.value().getCacheAge() != null) {
// Cache at least 60 seconds
cacheAge = Math.max(60, wrapper.value().getCacheAge());
}
else {
cacheAge = this.defaultCacheAge;
}
return Duration.ofSeconds(Math.min(cacheAge, Integer.MAX_VALUE));
}

long getDefaultCacheAge() {
return this.defaultCacheAge;
}

void setDefaultCacheAge(long defaultCacheAge) {
this.defaultCacheAge = defaultCacheAge;
}

}
45 changes: 45 additions & 0 deletions src/main/java/ac/simons/oembed/OembedResponseWrapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Created by Michael Simons, michael-simons.eu
* and released under The BSD License
* http://www.opensource.org/licenses/bsd-license.php
*
* Copyright (c) 2010-2026, Michael Simons
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of michael-simons.eu nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package ac.simons.oembed;

/**
* EHCache 3 is unhappy with {@literal null} values, so we wrap them.
*
* @param value actual OEmbed response, might be {@literal null}
* @author Oliver Lockwood
* @author Michael J. Simons
* @since 2026-01-17
*/
record OembedResponseWrapper(OembedResponse value) {
}
62 changes: 40 additions & 22 deletions src/main/java/ac/simons/oembed/OembedService.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,16 @@
import java.util.stream.Collectors;

import ac.simons.oembed.OembedResponse.Format;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
Expand All @@ -84,7 +86,7 @@ public class OembedService {
/**
* An optional cache manager used for caching oembed responses.
*/
private final Optional<CacheManager> cacheManager;
private final CacheManager cacheManager;

/**
* The user agent to use. We want to be a goot net citizen and provide some info about
Expand Down Expand Up @@ -123,11 +125,6 @@ public class OembedService {
*/
private String cacheName = OembedService.class.getName();

/**
* Time in seconds responses are cached. Used if the response has no cache_age.
*/
private long defaultCacheAge = 3600;

/**
* Used for auto-discovered endpoints.
*/
Expand All @@ -138,6 +135,8 @@ public class OembedService {
*/
private final OembedResponseRenderer defaultRenderer = new DefaultOembedResponseRenderer();

private final OembedResponseExpiryPolicy expiryPolicy = new OembedResponseExpiryPolicy();

/**
* Creates a new {@code OembedService}. This service depends on a {@link HttpClient}
* and can use a {@link CacheManager} for caching requests.
Expand All @@ -149,7 +148,7 @@ public class OembedService {
public OembedService(final HttpClient httpClient, final CacheManager cacheManager,
final List<OembedEndpoint> endpoints, final String applicationName) {
this.httpClient = httpClient;
this.cacheManager = Optional.ofNullable(cacheManager);
this.cacheManager = cacheManager;
final Properties version = new Properties();
try {
version.load(OembedService.class.getResourceAsStream("/oembed.properties"));
Expand Down Expand Up @@ -236,8 +235,9 @@ public String getCacheName() {
* @param cacheName the new cache name
*/
public void setCacheName(final String cacheName) {
if (this.cacheManager.isPresent() && this.cacheManager.get().cacheExists(this.cacheName)) {
this.cacheManager.get().removeCache(this.cacheName);
if (this.cacheManager != null
&& this.cacheManager.getCache(this.cacheName, String.class, OembedResponseWrapper.class) != null) {
this.cacheManager.removeCache(this.cacheName);
}
this.cacheName = cacheName;
}
Expand All @@ -246,15 +246,15 @@ public void setCacheName(final String cacheName) {
* {@return the default time in seconds responses are cached}
*/
public long getDefaultCacheAge() {
return this.defaultCacheAge;
return this.expiryPolicy.getDefaultCacheAge();
}

/**
* Changes the default cache age.
* @param defaultCacheAge new default cache age in seconds
*/
public void setDefaultCacheAge(final long defaultCacheAge) {
this.defaultCacheAge = defaultCacheAge;
this.expiryPolicy.setDefaultCacheAge(defaultCacheAge);
}

/**
Expand Down Expand Up @@ -331,6 +331,26 @@ final InputStream executeRequest(final HttpGet request) {
return rv;
}

/**
* Gets or creates the cache for oembed responses. In Ehcache 3.x, caches need to be
* explicitly created with a configuration.
* @return the cache instance or null if there is no cache manager
*/
private Cache<String, OembedResponseWrapper> getOrCreateCache() {
if (this.cacheManager == null) {
return null;
}
var cache = this.cacheManager.getCache(this.cacheName, String.class, OembedResponseWrapper.class);
if (cache != null) {
return cache;
}

return this.cacheManager.createCache(this.cacheName, CacheConfigurationBuilder
.newCacheConfigurationBuilder(String.class, OembedResponseWrapper.class, ResourcePoolsBuilder.heap(1000))
.withExpiry(new OembedResponseExpiryPolicy())
.build());
}

/**
* Tries to find an {@link OembedResponse} for the URL {@code url}. If a cache manager
* is present, it tries that first. If an {@code OembedResponse} can be discovered and
Expand All @@ -345,8 +365,9 @@ public Optional<OembedResponse> getOembedResponseFor(final String url) {
return Optional.empty();
}

var rv = this.cacheManager.map(cm -> cm.addCacheIfAbsent(this.cacheName).get(trimmedUrl))
.map(element -> (OembedResponse) element.getObjectValue());
var rv = Optional.ofNullable(this.getOrCreateCache())
.map(cache -> cache.get(trimmedUrl))
.map(OembedResponseWrapper::value);
// If there's already an oembed response cached, use that
if (rv.isPresent()) {
LOGGER.debug("Using OembedResponse from cache for '{}'...", trimmedUrl);
Expand All @@ -371,15 +392,12 @@ public Optional<OembedResponse> getOembedResponseFor(final String url) {
return oembedResponse;
});

if (this.cacheManager.isPresent()) {
final Ehcache cache = this.cacheManager.get().addCacheIfAbsent(this.cacheName);
// Cache at least 60 seconds
final int cacheAge = (int) Math.min(
Math.max(60L, rv.map(OembedResponse::getCacheAge).orElse(this.defaultCacheAge)), Integer.MAX_VALUE);
if (this.cacheManager != null) {
var cache = getOrCreateCache();
// We're adding failed urls to the cache as well to prevent them
// from being tried again over and over (at least for some seconds)
cache.put(new net.sf.ehcache.Element(trimmedUrl, rv.orElse(null), cacheAge, cacheAge));
LOGGER.debug("Cached {} for {} seconds for url '{}'...", rv, cacheAge, trimmedUrl);
cache.put(trimmedUrl, new OembedResponseWrapper(rv.orElse(null)));
LOGGER.debug("Cached response {} from url '{}'...", rv, trimmedUrl);
}

return rv;
Expand Down
Loading