001/*
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.
018 */
019package org.apache.wiki.cache;
020
021import net.sf.ehcache.Cache;
022import net.sf.ehcache.CacheManager;
023import net.sf.ehcache.Ehcache;
024import net.sf.ehcache.Element;
025import net.sf.ehcache.event.CacheEventListenerAdapter;
026import org.apache.logging.log4j.LogManager;
027import org.apache.logging.log4j.Logger;
028import org.apache.wiki.api.core.Engine;
029import org.apache.wiki.api.engine.Initializable;
030import org.apache.wiki.api.exceptions.WikiException;
031import org.apache.wiki.util.CheckedSupplier;
032import org.apache.wiki.util.TextUtil;
033
034import java.io.Serializable;
035import java.net.URL;
036import java.util.Collections;
037import java.util.List;
038import java.util.Map;
039import java.util.Properties;
040import java.util.concurrent.ConcurrentHashMap;
041import java.util.concurrent.atomic.AtomicBoolean;
042
043
044/**
045 * Ehcache-based {@link CachingManager}.
046 */
047public class EhcacheCachingManager implements CachingManager, Initializable {
048
049    private static final Logger LOG = LogManager.getLogger( EhcacheCachingManager.class );
050    private static final int DEFAULT_CACHE_SIZE = 1_000;
051    private static final int DEFAULT_CACHE_EXPIRY_PERIOD = 24*60*60;
052
053    final Map< String, Cache > cacheMap = new ConcurrentHashMap<>();
054    final Map< String, CacheInfo > cacheStats = new ConcurrentHashMap<>();
055    CacheManager cacheManager;
056
057    /** {@inheritDoc} */
058    @Override
059    public void shutdown() {
060        if(!cacheMap.isEmpty()) {
061            CacheManager.getInstance().shutdown();
062            cacheMap.clear();
063            cacheStats.clear();
064        }
065    }
066
067    /** {@inheritDoc} */
068    @Override
069    public void initialize( final Engine engine, final Properties props ) throws WikiException {
070        final String cacheEnabled = TextUtil.getStringProperty( props, PROP_CACHE_ENABLE, PROP_USECACHE_DEPRECATED, "true" );
071        final boolean useCache = "true".equalsIgnoreCase( cacheEnabled );
072        final String confLocation = "/" + TextUtil.getStringProperty( props, PROP_CACHE_CONF_FILE, "ehcache-jspwiki.xml" );
073        if( useCache ) {
074            final URL location = this.getClass().getResource( confLocation );
075            LOG.info( "Reading ehcache configuration file from classpath on /{}", location );
076            cacheManager = CacheManager.create( location );
077            registerCache( CACHE_ATTACHMENTS );
078            registerCache( CACHE_ATTACHMENTS_COLLECTION );
079            registerCache( CACHE_ATTACHMENTS_DYNAMIC );
080            registerCache( CACHE_DOCUMENTS );
081            registerCache( CACHE_PAGES );
082            registerCache( CACHE_PAGES_HISTORY );
083            registerCache( CACHE_PAGES_TEXT );
084        }
085    }
086
087    void registerCache( final String cacheName ) {
088        final Cache cache;
089        if( cacheManager.cacheExists( cacheName ) ) {
090            cache = cacheManager.getCache( cacheName );
091        } else {
092            LOG.info( "cache with name {} not found in ehcache configuration file, creating it with defaults.", cacheName );
093            cache = new Cache( cacheName, DEFAULT_CACHE_SIZE, false, false, DEFAULT_CACHE_EXPIRY_PERIOD, DEFAULT_CACHE_EXPIRY_PERIOD );
094            cacheManager.addCache( cache );
095        }
096        cacheMap.put( cacheName, cache );
097        cacheStats.put( cacheName, new CacheInfo( cacheName, cache.getCacheConfiguration().getMaxEntriesLocalHeap() ) );
098    }
099
100    /** {@inheritDoc} */
101    @Override
102    public boolean enabled( final String cacheName ) {
103        return cacheMap.get( cacheName ) != null;
104    }
105
106    /** {@inheritDoc} */
107    @Override
108    public CacheInfo info( final String cacheName ) {
109        if( enabled( cacheName ) ) {
110            return cacheStats.get( cacheName );
111        }
112        return null;
113    }
114
115    /**
116     * {@inheritDoc}
117     */
118    @Override
119    @SuppressWarnings( "unchecked" )
120    public List< String > keys( final String cacheName ) {
121        if( enabled( cacheName ) ) {
122            return cacheMap.get( cacheName ).getKeysWithExpiryCheck();
123        }
124        return Collections.emptyList();
125    }
126
127    /** {@inheritDoc} */
128    @Override
129    @SuppressWarnings( "unchecked" )
130    public < T, E extends Exception > T get( final String cacheName, final Serializable key, final CheckedSupplier< T, E > supplier ) throws E {
131        if( keyAndCacheAreNotNull( cacheName, key ) ) {
132            final Element element = cacheMap.get( cacheName ).get( key );
133            if( element != null ) {
134                cacheStats.get( cacheName ).hit();
135                return ( T )element.getObjectValue();
136            } else {
137                // element doesn't exist in cache, try to retrieve from the cached service instead.
138                final T value = supplier.get();
139                if( value != null ) {
140                    cacheStats.get( cacheName ).miss();
141                    cacheMap.get( cacheName ).put( new Element( key, value ) );
142                }
143                return value;
144            }
145        }
146        return null;
147    }
148
149    /** {@inheritDoc} */
150    @Override
151    public void put( final String cacheName, final Serializable key, final Object val ) {
152        if( keyAndCacheAreNotNull( cacheName, key ) ) {
153            cacheMap.get( cacheName ).put( new Element( key, val ) );
154        }
155    }
156
157    /** {@inheritDoc} */
158    @Override
159    public void remove( final String cacheName, final Serializable key ) {
160        if( keyAndCacheAreNotNull( cacheName, key ) ) {
161            cacheMap.get( cacheName ).remove( key );
162        }
163    }
164
165    /** {@inheritDoc} */
166    @Override
167    public boolean registerListener( final String cacheName, final String listener, final Object... args ) {
168        if( enabled( cacheName ) && "expired".equals( listener ) ) {
169            final AtomicBoolean allRequested = ( AtomicBoolean )args[0];
170            final CacheEventListenerAdapter expiredCacheListenerAdapter = new CacheEventListenerAdapter() {
171                @Override
172                public void notifyElementExpired( final Ehcache cache, final Element element ) {
173                    allRequested.set( false ); // signal that the cache no longer contains all elements...
174                }
175            };
176            return cacheMap.get( cacheName ).getCacheEventNotificationService().registerListener( expiredCacheListenerAdapter );
177        }
178        return false;
179    }
180
181    boolean keyAndCacheAreNotNull( final String cacheName, final Serializable key ) {
182        return enabled( cacheName ) && key != null;
183    }
184
185}