Skip to content

Commit 4ef37e4

Browse files
refactor: Upgrade ehcache from 2.x to 3.x (#15)
--------- Co-authored-by: Michael J. Simons <michael@simons.ac>
1 parent 0b341f3 commit 4ef37e4

File tree

6 files changed

+327
-64
lines changed

6 files changed

+327
-64
lines changed

pom.xml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
<checkstyle.version>12.3.1</checkstyle.version>
7373
<commons-beanutils.version>1.11.0</commons-beanutils.version>
7474
<commons-logging.version>1.3.5</commons-logging.version>
75-
<ehcache.version>2.10.9.2</ehcache.version>
75+
<ehcache.version>3.11.1</ehcache.version>
7676
<httpclient.version>4.5.14</httpclient.version>
7777
<httpcore.version>4.4.16</httpcore.version>
7878
<jackson.version>2.20.1</jackson.version>
@@ -181,16 +181,22 @@
181181
<artifactId>jakarta.xml.bind-api</artifactId>
182182
<version>${jakarta.xml.bind-api.version}</version>
183183
</dependency>
184-
<dependency>
185-
<groupId>net.sf.ehcache</groupId>
186-
<artifactId>ehcache</artifactId>
187-
<version>${ehcache.version}</version>
188-
</dependency>
189184
<dependency>
190185
<groupId>org.apache.httpcomponents</groupId>
191186
<artifactId>httpclient</artifactId>
192187
<version>${httpclient.version}</version>
193188
</dependency>
189+
<dependency>
190+
<groupId>org.ehcache</groupId>
191+
<artifactId>ehcache</artifactId>
192+
<version>${ehcache.version}</version>
193+
<exclusions>
194+
<exclusion>
195+
<groupId>org.glassfish.jaxb</groupId>
196+
<artifactId>jaxb-runtime</artifactId>
197+
</exclusion>
198+
</exclusions>
199+
</dependency>
194200
<dependency>
195201
<groupId>org.jsoup</groupId>
196202
<artifactId>jsoup</artifactId>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Created by Michael Simons, michael-simons.eu
3+
* and released under The BSD License
4+
* http://www.opensource.org/licenses/bsd-license.php
5+
*
6+
* Copyright (c) 2010-2026, Michael Simons
7+
* All rights reserved.
8+
*
9+
* Redistribution and use in source and binary forms, with or without
10+
* modification, are permitted provided that the following conditions are met:
11+
*
12+
* * Redistributions of source code must retain the above copyright notice,
13+
* this list of conditions and the following disclaimer.
14+
*
15+
* * Redistributions in binary form must reproduce the above copyright notice,
16+
* this list of conditions and the following disclaimer in the documentation
17+
* and/or other materials provided with the distribution.
18+
*
19+
* * Neither the name of michael-simons.eu nor the names of its contributors
20+
* may be used to endorse or promote products derived from this software
21+
* without specific prior written permission.
22+
*
23+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33+
*/
34+
package ac.simons.oembed;
35+
36+
import java.time.Duration;
37+
import java.util.function.Supplier;
38+
39+
import org.ehcache.expiry.ExpiryPolicy;
40+
41+
/**
42+
* A custom {@link ExpiryPolicy} for Ehcache 3.x that extracts the TTL from an
43+
* {@link OembedResponse}, enabling per-entry expiration times.
44+
*
45+
* @author Oliver Lockwood
46+
* @author Michael J. Simons
47+
* @since 2026-01-17
48+
*/
49+
final class OembedResponseExpiryPolicy implements ExpiryPolicy<String, OembedResponseWrapper> {
50+
51+
/**
52+
* Time in seconds responses are cached. Used if the response has no cache_age.
53+
*/
54+
private long defaultCacheAge = 3600;
55+
56+
@Override
57+
public Duration getExpiryForCreation(String key, OembedResponseWrapper value) {
58+
return getExpiryOf(value);
59+
}
60+
61+
@Override
62+
public Duration getExpiryForAccess(String key, Supplier<? extends OembedResponseWrapper> value) {
63+
return null;
64+
}
65+
66+
@Override
67+
public Duration getExpiryForUpdate(String key, Supplier<? extends OembedResponseWrapper> oldValue,
68+
OembedResponseWrapper newValue) {
69+
return getExpiryOf(newValue);
70+
}
71+
72+
Duration getExpiryOf(OembedResponseWrapper wrapper) {
73+
long cacheAge;
74+
if (wrapper.value() != null && wrapper.value().getCacheAge() != null) {
75+
// Cache at least 60 seconds
76+
cacheAge = Math.max(60, wrapper.value().getCacheAge());
77+
}
78+
else {
79+
cacheAge = this.defaultCacheAge;
80+
}
81+
return Duration.ofSeconds(Math.min(cacheAge, Integer.MAX_VALUE));
82+
}
83+
84+
long getDefaultCacheAge() {
85+
return this.defaultCacheAge;
86+
}
87+
88+
void setDefaultCacheAge(long defaultCacheAge) {
89+
this.defaultCacheAge = defaultCacheAge;
90+
}
91+
92+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Created by Michael Simons, michael-simons.eu
3+
* and released under The BSD License
4+
* http://www.opensource.org/licenses/bsd-license.php
5+
*
6+
* Copyright (c) 2010-2026, Michael Simons
7+
* All rights reserved.
8+
*
9+
* Redistribution and use in source and binary forms, with or without
10+
* modification, are permitted provided that the following conditions are met:
11+
*
12+
* * Redistributions of source code must retain the above copyright notice,
13+
* this list of conditions and the following disclaimer.
14+
*
15+
* * Redistributions in binary form must reproduce the above copyright notice,
16+
* this list of conditions and the following disclaimer in the documentation
17+
* and/or other materials provided with the distribution.
18+
*
19+
* * Neither the name of michael-simons.eu nor the names of its contributors
20+
* may be used to endorse or promote products derived from this software
21+
* without specific prior written permission.
22+
*
23+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33+
*/
34+
package ac.simons.oembed;
35+
36+
/**
37+
* EHCache 3 is unhappy with {@literal null} values, so we wrap them.
38+
*
39+
* @param value actual OEmbed response, might be {@literal null}
40+
* @author Oliver Lockwood
41+
* @author Michael J. Simons
42+
* @since 2026-01-17
43+
*/
44+
record OembedResponseWrapper(OembedResponse value) {
45+
}

src/main/java/ac/simons/oembed/OembedService.java

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,16 @@
5050
import java.util.stream.Collectors;
5151

5252
import ac.simons.oembed.OembedResponse.Format;
53-
import net.sf.ehcache.CacheManager;
54-
import net.sf.ehcache.Ehcache;
5553
import org.apache.commons.beanutils.BeanUtils;
5654
import org.apache.http.HttpResponse;
5755
import org.apache.http.HttpStatus;
5856
import org.apache.http.client.HttpClient;
5957
import org.apache.http.client.methods.HttpGet;
6058
import org.apache.http.util.EntityUtils;
59+
import org.ehcache.Cache;
60+
import org.ehcache.CacheManager;
61+
import org.ehcache.config.builders.CacheConfigurationBuilder;
62+
import org.ehcache.config.builders.ResourcePoolsBuilder;
6163
import org.jsoup.Jsoup;
6264
import org.jsoup.nodes.Document;
6365
import org.jsoup.nodes.Element;
@@ -84,7 +86,7 @@ public class OembedService {
8486
/**
8587
* An optional cache manager used for caching oembed responses.
8688
*/
87-
private final Optional<CacheManager> cacheManager;
89+
private final CacheManager cacheManager;
8890

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

126-
/**
127-
* Time in seconds responses are cached. Used if the response has no cache_age.
128-
*/
129-
private long defaultCacheAge = 3600;
130-
131128
/**
132129
* Used for auto-discovered endpoints.
133130
*/
@@ -138,6 +135,8 @@ public class OembedService {
138135
*/
139136
private final OembedResponseRenderer defaultRenderer = new DefaultOembedResponseRenderer();
140137

138+
private final OembedResponseExpiryPolicy expiryPolicy = new OembedResponseExpiryPolicy();
139+
141140
/**
142141
* Creates a new {@code OembedService}. This service depends on a {@link HttpClient}
143142
* and can use a {@link CacheManager} for caching requests.
@@ -149,7 +148,7 @@ public class OembedService {
149148
public OembedService(final HttpClient httpClient, final CacheManager cacheManager,
150149
final List<OembedEndpoint> endpoints, final String applicationName) {
151150
this.httpClient = httpClient;
152-
this.cacheManager = Optional.ofNullable(cacheManager);
151+
this.cacheManager = cacheManager;
153152
final Properties version = new Properties();
154153
try {
155154
version.load(OembedService.class.getResourceAsStream("/oembed.properties"));
@@ -236,8 +235,9 @@ public String getCacheName() {
236235
* @param cacheName the new cache name
237236
*/
238237
public void setCacheName(final String cacheName) {
239-
if (this.cacheManager.isPresent() && this.cacheManager.get().cacheExists(this.cacheName)) {
240-
this.cacheManager.get().removeCache(this.cacheName);
238+
if (this.cacheManager != null
239+
&& this.cacheManager.getCache(this.cacheName, String.class, OembedResponseWrapper.class) != null) {
240+
this.cacheManager.removeCache(this.cacheName);
241241
}
242242
this.cacheName = cacheName;
243243
}
@@ -246,15 +246,15 @@ public void setCacheName(final String cacheName) {
246246
* {@return the default time in seconds responses are cached}
247247
*/
248248
public long getDefaultCacheAge() {
249-
return this.defaultCacheAge;
249+
return this.expiryPolicy.getDefaultCacheAge();
250250
}
251251

252252
/**
253253
* Changes the default cache age.
254254
* @param defaultCacheAge new default cache age in seconds
255255
*/
256256
public void setDefaultCacheAge(final long defaultCacheAge) {
257-
this.defaultCacheAge = defaultCacheAge;
257+
this.expiryPolicy.setDefaultCacheAge(defaultCacheAge);
258258
}
259259

260260
/**
@@ -331,6 +331,26 @@ final InputStream executeRequest(final HttpGet request) {
331331
return rv;
332332
}
333333

334+
/**
335+
* Gets or creates the cache for oembed responses. In Ehcache 3.x, caches need to be
336+
* explicitly created with a configuration.
337+
* @return the cache instance or null if there is no cache manager
338+
*/
339+
private Cache<String, OembedResponseWrapper> getOrCreateCache() {
340+
if (this.cacheManager == null) {
341+
return null;
342+
}
343+
var cache = this.cacheManager.getCache(this.cacheName, String.class, OembedResponseWrapper.class);
344+
if (cache != null) {
345+
return cache;
346+
}
347+
348+
return this.cacheManager.createCache(this.cacheName, CacheConfigurationBuilder
349+
.newCacheConfigurationBuilder(String.class, OembedResponseWrapper.class, ResourcePoolsBuilder.heap(1000))
350+
.withExpiry(new OembedResponseExpiryPolicy())
351+
.build());
352+
}
353+
334354
/**
335355
* Tries to find an {@link OembedResponse} for the URL {@code url}. If a cache manager
336356
* is present, it tries that first. If an {@code OembedResponse} can be discovered and
@@ -345,8 +365,9 @@ public Optional<OembedResponse> getOembedResponseFor(final String url) {
345365
return Optional.empty();
346366
}
347367

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

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

385403
return rv;

0 commit comments

Comments
 (0)