From lutecepowers-v8
Rules and patterns for implementing cache in a Lutece 8 plugin. AbstractCacheableService, CDI initialization, cache operations, invalidation via CDI events.
npx claudepluginhub lutece-platform/lutece-dev-plugin-claude --plugin lutecepowers-v8This skill uses the workspace's default tool permissions.
> Before implementing cache, consult `~/.lutece-references/lutece-form-plugin-forms/` — specifically `FormsCacheService.java`.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Before implementing cache, consult
~/.lutece-references/lutece-form-plugin-forms/— specificallyFormsCacheService.java.
AbstractCacheableService<K, V> (lutece-core, JSR-107 JCache)
↑ extends
MyCacheService (@ApplicationScoped, @PostConstruct initCache)
↓ used by
Home / Service (put, get, remove, resetCache)
↓ invalidated by
CDI Events (@Observes)
IMPORTANT: The
put()/get()/remove()methods inherited fromAbstractCacheableServicedelegate directly to_cachewithout null/closed checks. If the cache is disabled in the datastore (default state),_cacheisnulland these methods throwNullPointerException. You MUST override them with defensive guards.
import javax.cache.CacheException;
import fr.paris.lutece.portal.service.cache.AbstractCacheableService;
import fr.paris.lutece.portal.service.util.AppLogService;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class EntityCacheService extends AbstractCacheableService<String, Object>
{
private static final String CACHE_NAME = "myplugin.entityCacheService";
@PostConstruct
public void init( )
{
initCache( CACHE_NAME, String.class, Object.class );
}
@Override
public String getName( )
{
return CACHE_NAME;
}
@Override
public void put( String key, Object value )
{
if ( isCacheEnable( ) && isCacheAvailable( ) )
{
try
{
super.put( key, value );
}
catch( CacheException | IllegalStateException e )
{
AppLogService.error( "EntityCacheService : error putting key {} in cache", key, e );
}
}
}
@Override
public Object get( String key )
{
if ( isCacheEnable( ) && isCacheAvailable( ) )
{
try
{
return super.get( key );
}
catch( CacheException | IllegalStateException e )
{
AppLogService.error( "EntityCacheService : error getting key {} from cache", key, e );
}
}
return null;
}
@Override
public boolean remove( String key )
{
if ( isCacheEnable( ) && isCacheAvailable( ) )
{
try
{
return super.remove( key );
}
catch( CacheException | IllegalStateException e )
{
AppLogService.error( "EntityCacheService : error removing key {} from cache", key, e );
}
}
return false;
}
private boolean isCacheAvailable( )
{
return _cache != null && !_cache.isClosed( );
}
}
Rules:
@ApplicationScoped — singleton CDI bean, one instance per application@PostConstruct calls initCache( name, keyClass, valueClass ) — registers the cache with CacheServicepluginName.entityCacheService<String, Object> is the standard — key is always String, value is the cached objectput/get/remove with isCacheEnable() && isCacheAvailable() guards + try/catch — the core AbstractCacheableService does NOT check for null/closed cache in these methodsDefine static methods for consistent key generation:
@ApplicationScoped
public class EntityCacheService extends AbstractCacheableService<String, Object>
{
private static final String KEY_PREFIX = "myplugin.entity.";
// Key for a single entity
public static String getEntityCacheKey( int nIdEntity )
{
return KEY_PREFIX + nIdEntity;
}
// Key for the full list
public static String getListCacheKey( )
{
return KEY_PREFIX + "list";
}
// Key with parameters (e.g., filtered list)
public static String getFilteredCacheKey( int nIdCategory, int nPage )
{
return KEY_PREFIX + "category." + nIdCategory + ".page." + nPage;
}
// ...
}
@Inject (CDI-managed beans) vs CDI.current().select() (static contexts)| Context | Pattern | Example |
|---|---|---|
CDI-managed bean (@ApplicationScoped, @RequestScoped, etc.) | @Inject | Service class |
| Static context (Home class, static utility) | CDI.current().select() direct field init | Home class |
NEVER provide a getInstance() method on the cache service — it is @Deprecated(since = "8.0", forRemoval = true) in lutece-core.
@Inject in a CDI-managed Service (preferred)@ApplicationScoped
public class EntityService
{
@Inject
private EntityCacheService _cacheService;
public Entity findByPrimaryKey( int nId )
{
String strCacheKey = EntityCacheService.getEntityCacheKey( nId );
Entity entity = (Entity) _cacheService.get( strCacheKey );
if ( entity == null )
{
entity = EntityHome.findByPrimaryKey( nId );
if ( entity != null )
{
_cacheService.put( strCacheKey, entity );
}
}
return entity;
}
public Entity update( Entity entity )
{
EntityHome.update( entity );
_cacheService.remove( EntityCacheService.getEntityCacheKey( entity.getId( ) ) );
_cacheService.remove( EntityCacheService.getListCacheKey( ) );
return entity;
}
public void remove( int nId )
{
EntityHome.remove( nId );
_cacheService.remove( EntityCacheService.getEntityCacheKey( nId ) );
_cacheService.remove( EntityCacheService.getListCacheKey( ) );
}
}
CDI.current().select() in a static Home classUse direct field initialization — no lazy-init getters. This is the pattern used by all Home classes in lutece-core (RoleHome, PageHome, FileHome) and in the Forms plugin (FormHome, StepHome). The CDI container is fully initialized before plugin classes are loaded.
import jakarta.enterprise.inject.spi.CDI;
public class EntityHome
{
private static IEntityDAO _dao = CDI.current( ).select( IEntityDAO.class ).get( );
private static EntityCacheService _cacheService = CDI.current( ).select( EntityCacheService.class ).get( );
private static final Plugin _plugin = PluginService.getPlugin( "pluginname" );
private EntityHome( )
{
}
public static Entity findByPrimaryKey( int nId )
{
String strCacheKey = EntityCacheService.getEntityCacheKey( nId );
Entity entity = (Entity) _cacheService.get( strCacheKey );
if ( entity == null )
{
entity = _dao.load( nId, _plugin );
if ( entity != null )
{
_cacheService.put( strCacheKey, entity );
}
}
return entity;
}
}
Observe domain events to automatically invalidate cache:
import fr.paris.lutece.portal.service.event.ResourceEvent;
import jakarta.enterprise.event.Observes;
@ApplicationScoped
public class EntityCacheService extends AbstractCacheableService<String, Object>
{
// ...
public void onResourceEvent( @Observes ResourceEvent event )
{
if ( isCacheEnable( ) && "MYPLUGIN_ENTITY".equals( event.getResourceType( ) ) )
{
resetCache( );
}
}
}
For finer-grained invalidation (remove specific key instead of full reset):
public void onResourceEvent( @Observes ResourceEvent event )
{
if ( isCacheEnable( ) && "MYPLUGIN_ENTITY".equals( event.getResourceType( ) ) )
{
String strId = event.getIdResource( );
if ( strId != null )
{
remove( EntityCacheService.getEntityCacheKey( Integer.parseInt( strId ) ) );
remove( EntityCacheService.getListCacheKey( ) );
}
else
{
resetCache( );
}
}
}
| Method | Usage |
|---|---|
put( key, value ) | Add or update an entry |
get( key ) | Retrieve (returns null on miss) |
remove( key ) | Delete a single entry |
resetCache( ) | Clear all entries |
enableCache( boolean ) | Toggle cache on/off |
isCacheEnable( ) | Check if cache is active |
getCacheSize( ) | Current entry count |
getKeys( ) | List all keys |
containsKey( key ) | Check key existence |
If your cache should survive when an admin clicks "Reset all caches":
@Override
public boolean isPreventGlobalReset( )
{
return true;
}
Use sparingly — only for caches that are expensive to rebuild (e.g., configuration caches).
Cache behavior can be tuned via caches.properties or datastore:
# Default settings (apply to all caches without specific config)
lutece.cache.default.maxElementsInMemory=1000
lutece.cache.default.eternal=false
lutece.cache.default.timeToIdleSeconds=1000
lutece.cache.default.timeToLiveSeconds=1000
# Per-cache override
core.cache.status.myplugin.entityCacheService.eternal=true
core.cache.status.myplugin.entityCacheService.enabled=1
Cache enabled/disabled state is persisted in the datastore database with key: core.cache.status.{cacheName}.enabled
| File | What to add |
|---|---|
EntityCacheService.java | New class in service/cache/, @ApplicationScoped, extends AbstractCacheableService, key builders |
EntityService.java | @Inject EntityCacheService, cache-through logic (get → miss → load → put) |
beans.xml | Already present (required for CDI) |
No changes needed in plugin.xml or messages.properties — cache is infrastructure, not UI.
| Need | File to consult |
|---|---|
| CDI cache service (v8 pattern) | ~/.lutece-references/lutece-form-plugin-forms/src/java/**/service/FormsCacheService.java |
| Core AbstractCacheableService | ~/.lutece-references/lutece-core/src/java/**/service/cache/AbstractCacheableService.java |
| Core CacheService (static facade) | ~/.lutece-references/lutece-core/src/java/**/service/cache/CacheService.java |
| Cache manager (JSR-107) | ~/.lutece-references/lutece-core/src/java/**/service/cache/Lutece107CacheManager.java |
| Cache configuration | ~/.lutece-references/lutece-core/src/java/**/service/cache/CacheConfigUtil.java |