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.commons.jcs3.jcache;
020
021import static org.apache.commons.jcs3.jcache.Asserts.assertNotNull;
022
023import java.io.ByteArrayInputStream;
024import java.io.IOException;
025import java.io.InputStream;
026import java.net.URI;
027import java.net.URL;
028import java.nio.charset.StandardCharsets;
029import java.util.Enumeration;
030import java.util.Map;
031import java.util.Properties;
032import java.util.concurrent.ConcurrentHashMap;
033import java.util.concurrent.ConcurrentMap;
034
035import javax.cache.Cache;
036import javax.cache.CacheManager;
037import javax.cache.configuration.Configuration;
038import javax.cache.spi.CachingProvider;
039
040import org.apache.commons.jcs3.engine.behavior.ICompositeCacheAttributes;
041import org.apache.commons.jcs3.engine.behavior.IElementAttributes;
042import org.apache.commons.jcs3.engine.control.CompositeCache;
043import org.apache.commons.jcs3.engine.control.CompositeCacheConfigurator;
044import org.apache.commons.jcs3.engine.control.CompositeCacheManager;
045import org.apache.commons.jcs3.jcache.lang.Subsitutor;
046import org.apache.commons.jcs3.jcache.proxy.ClassLoaderAwareCache;
047
048public class JCSCachingManager implements CacheManager
049{
050    private static final Subsitutor SUBSTITUTOR = Subsitutor.Helper.INSTANCE;
051    private static final String DEFAULT_CONFIG =
052        "jcs.default=DC\n" +
053        "jcs.default.cacheattributes=org.apache.commons.jcs3.engine.CompositeCacheAttributes\n" +
054        "jcs.default.cacheattributes.MaxObjects=200001\n" +
055        "jcs.default.cacheattributes.MemoryCacheName=org.apache.commons.jcs3.engine.memory.lru.LRUMemoryCache\n" +
056        "jcs.default.cacheattributes.UseMemoryShrinker=true\n" +
057        "jcs.default.cacheattributes.MaxMemoryIdleTimeSeconds=3600\n" +
058        "jcs.default.cacheattributes.ShrinkerIntervalSeconds=60\n" +
059        "jcs.default.elementattributes=org.apache.commons.jcs3.engine.ElementAttributes\n" +
060        "jcs.default.elementattributes.IsEternal=false\n" +
061        "jcs.default.elementattributes.MaxLife=700\n" +
062        "jcs.default.elementattributes.IdleTime=1800\n" +
063        "jcs.default.elementattributes.IsSpool=true\n" +
064        "jcs.default.elementattributes.IsRemote=true\n" +
065        "jcs.default.elementattributes.IsLateral=true\n";
066
067    private static class InternalManager extends CompositeCacheManager
068    {
069        protected static InternalManager create()
070        {
071            return new InternalManager();
072        }
073
074        @Override
075        protected CompositeCacheConfigurator newConfigurator()
076        {
077            return new CompositeCacheConfigurator()
078            {
079                @Override
080                protected <K, V> CompositeCache<K, V> newCache(
081                        final ICompositeCacheAttributes cca, final IElementAttributes ea)
082                {
083                    return new ExpiryAwareCache<>( cca, ea );
084                }
085            };
086        }
087
088        @Override // needed to call it from JCSCachingManager
089        protected void initialize() {
090            super.initialize();
091        }
092    }
093
094    private final CachingProvider provider;
095    private final URI uri;
096    private final ClassLoader loader;
097    private final Properties properties;
098    private final ConcurrentMap<String, Cache<?, ?>> caches = new ConcurrentHashMap<>();
099    private final Properties configProperties;
100    private volatile boolean closed;
101    private final InternalManager delegate = InternalManager.create();
102
103    public JCSCachingManager(final CachingProvider provider, final URI uri, final ClassLoader loader, final Properties properties)
104    {
105        this.provider = provider;
106        this.uri = uri;
107        this.loader = loader;
108        this.properties = readConfig(uri, loader, properties);
109        this.configProperties = properties;
110
111        delegate.setJmxName(CompositeCacheManager.JMX_OBJECT_NAME
112                + ",provider=" + provider.hashCode()
113                + ",uri=" + uri.toString().replaceAll(",|:|=|\n", ".")
114                + ",classloader=" + loader.hashCode()
115                + ",properties=" + this.properties.hashCode());
116        delegate.initialize();
117        delegate.configure(this.properties);
118    }
119
120    private static Properties readConfig(final URI uri, final ClassLoader loader, final Properties properties) {
121        final Properties props = new Properties();
122        try {
123            if (JCSCachingProvider.DEFAULT_URI.toString().equals(uri.toString()) || uri.toURL().getProtocol().equals("jcs"))
124            {
125
126                final Enumeration<URL> resources = loader.getResources(uri.getPath());
127                if (!resources.hasMoreElements()) // default
128                {
129                    props.load(new ByteArrayInputStream(DEFAULT_CONFIG.getBytes(StandardCharsets.UTF_8)));
130                }
131                else
132                {
133                    do
134                    {
135                        addProperties(resources.nextElement(), props);
136                    }
137                    while (resources.hasMoreElements());
138                }
139            }
140            else
141            {
142                props.load(uri.toURL().openStream());
143            }
144        } catch (final IOException e) {
145            throw new IllegalStateException(e);
146        }
147
148        if (properties != null)
149        {
150            props.putAll(properties);
151        }
152
153        for (final Map.Entry<Object, Object> entry : props.entrySet()) {
154            if (entry.getValue() == null)
155            {
156                continue;
157            }
158            final String substitute = SUBSTITUTOR.substitute(entry.getValue().toString());
159            if (!substitute.equals(entry.getValue()))
160            {
161                entry.setValue(substitute);
162            }
163        }
164        return props;
165    }
166
167    private static void addProperties(final URL url, final Properties aggregator)
168    {
169        try (InputStream inStream = url.openStream()) {
170            aggregator.load(inStream);
171        } catch (final IOException e) {
172            throw new IllegalArgumentException(e);
173        }
174    }
175
176    private void assertNotClosed()
177    {
178        if (isClosed())
179        {
180            throw new IllegalStateException("cache manager closed");
181        }
182    }
183
184    @Override
185    // TODO: use configuration + handle not serializable key/values
186    public <K, V, C extends Configuration<K, V>> Cache<K, V> createCache(final String cacheName, final C configuration)
187            throws IllegalArgumentException
188    {
189        assertNotClosed();
190        assertNotNull(cacheName, "cacheName");
191        assertNotNull(configuration, "configuration");
192        final Class<K> keyType = configuration.getKeyType();
193        final Class<V> valueType = configuration.getValueType();
194        if (caches.containsKey(cacheName)) {
195            throw new javax.cache.CacheException("cache " + cacheName + " already exists");
196        }
197        @SuppressWarnings("unchecked")
198        final Cache<K, V> cache = ClassLoaderAwareCache.wrap(loader,
199                new JCSCache<>(
200                        loader, this, cacheName,
201                        new JCSConfiguration<K, V>(configuration, keyType, valueType),
202                        properties,
203                        ExpiryAwareCache.class.cast(delegate.getCache(cacheName))));
204        caches.putIfAbsent(cacheName, cache);
205        return getCache(cacheName, keyType, valueType);
206    }
207
208    @Override
209    public void destroyCache(final String cacheName)
210    {
211        assertNotClosed();
212        assertNotNull(cacheName, "cacheName");
213        final Cache<?, ?> cache = caches.remove(cacheName);
214        if (cache != null && !cache.isClosed())
215        {
216            cache.clear();
217            cache.close();
218        }
219    }
220
221    @Override
222    public void enableManagement(final String cacheName, final boolean enabled)
223    {
224        assertNotClosed();
225        assertNotNull(cacheName, "cacheName");
226        final JCSCache<?, ?> cache = getJCSCache(cacheName);
227        if (cache != null)
228        {
229            if (enabled)
230            {
231                cache.enableManagement();
232            }
233            else
234            {
235                cache.disableManagement();
236            }
237        }
238    }
239
240    private JCSCache<?, ?> getJCSCache(final String cacheName)
241    {
242        final Cache<?, ?> cache = caches.get(cacheName);
243        return JCSCache.class.cast(ClassLoaderAwareCache.getDelegate(cache));
244    }
245
246    @Override
247    public void enableStatistics(final String cacheName, final boolean enabled)
248    {
249        assertNotClosed();
250        assertNotNull(cacheName, "cacheName");
251        final JCSCache<?, ?> cache = getJCSCache(cacheName);
252        if (cache != null)
253        {
254            if (enabled)
255            {
256                cache.enableStatistics();
257            }
258            else
259            {
260                cache.disableStatistics();
261            }
262        }
263    }
264
265    @Override
266    public synchronized void close()
267    {
268        if (isClosed())
269        {
270            return;
271        }
272
273        assertNotClosed();
274        for (final Cache<?, ?> c : caches.values())
275        {
276            c.close();
277        }
278        caches.clear();
279        closed = true;
280        if (JCSCachingProvider.class.isInstance(provider))
281        {
282            JCSCachingProvider.class.cast(provider).remove(this);
283        }
284        delegate.shutDown();
285    }
286
287    @Override
288    public <T> T unwrap(final Class<T> clazz)
289    {
290        if (clazz.isInstance(this))
291        {
292            return clazz.cast(this);
293        }
294        throw new IllegalArgumentException(clazz.getName() + " not supported in unwrap");
295    }
296
297    @Override
298    public boolean isClosed()
299    {
300        return closed;
301    }
302
303    @Override
304    public <K, V> Cache<K, V> getCache(final String cacheName)
305    {
306        assertNotClosed();
307        assertNotNull(cacheName, "cacheName");
308        return (Cache<K, V>) doGetCache(cacheName, Object.class, Object.class);
309    }
310
311    @Override
312    public Iterable<String> getCacheNames()
313    {
314        return new ImmutableIterable<>(caches.keySet());
315    }
316
317    @Override
318    public <K, V> Cache<K, V> getCache(final String cacheName, final Class<K> keyType, final Class<V> valueType)
319    {
320        assertNotClosed();
321        assertNotNull(cacheName, "cacheName");
322        assertNotNull(keyType, "keyType");
323        assertNotNull(valueType, "valueType");
324        try
325        {
326            return doGetCache(cacheName, keyType, valueType);
327        }
328        catch (final IllegalArgumentException iae)
329        {
330            throw new ClassCastException(iae.getMessage());
331        }
332    }
333
334    private <K, V> Cache<K, V> doGetCache(final String cacheName, final Class<K> keyType, final Class<V> valueType)
335    {
336        @SuppressWarnings("unchecked") // common map for all caches
337        final Cache<K, V> cache = (Cache<K, V>) caches.get(cacheName);
338        if (cache == null)
339        {
340            return null;
341        }
342
343        @SuppressWarnings("unchecked") // don't know how to solve this
344        final Configuration<K, V> config = cache.getConfiguration(Configuration.class);
345        if (keyType != null && !config.getKeyType().isAssignableFrom(keyType) ||
346            valueType != null && !config.getValueType().isAssignableFrom(valueType))
347        {
348            throw new IllegalArgumentException("this cache is <" + config.getKeyType().getName() + ", " + config.getValueType().getName()
349                    + "> " + " and not <" + keyType.getName() + ", " + valueType.getName() + ">");
350        }
351        return cache;
352    }
353
354    @Override
355    public CachingProvider getCachingProvider()
356    {
357        return provider;
358    }
359
360    @Override
361    public URI getURI()
362    {
363        return uri;
364    }
365
366    @Override
367    public ClassLoader getClassLoader()
368    {
369        return loader;
370    }
371
372    @Override
373    public Properties getProperties()
374    {
375        return configProperties;
376    }
377
378    public void release(final String name) {
379        caches.remove(name);
380    }
381}