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 */
019
020package org.apache.fulcrum.cache;
021
022import static org.junit.jupiter.api.Assertions.assertEquals;
023import static org.junit.jupiter.api.Assertions.assertNotNull;
024import static org.junit.jupiter.api.Assertions.assertNull;
025import static org.junit.jupiter.api.Assertions.assertSame;
026import static org.junit.jupiter.api.Assertions.assertTrue;
027import static org.junit.jupiter.api.Assertions.fail;
028
029import java.util.ConcurrentModificationException;
030import java.util.Date;
031import java.util.List;
032
033import org.apache.avalon.framework.component.ComponentException;
034import org.apache.fulcrum.cache.impl.DefaultGlobalCacheService;
035import org.apache.fulcrum.testcontainer.BaseUnit5Test;
036import org.apache.logging.log4j.LogManager;
037import org.apache.logging.log4j.Logger;
038import org.junit.jupiter.api.AfterEach;
039import org.junit.jupiter.api.BeforeEach;
040import org.junit.jupiter.api.Tag;
041import org.junit.jupiter.api.Test;
042
043/**
044 * CacheTest
045 *
046 * @author <a href="paulsp@apache.org">Paul Spencer</a>
047 * @author <a href="epugh@upstate.com">Eric Pugh</a>
048 * @author <a href="mailto:peter@courefreshableCachedObjectux.biz">Peter CourefreshableCachedObjectux</a>
049 * @version $Id$
050 */
051public class CacheTest extends BaseUnit5Test
052{
053
054    protected GlobalCacheService globalCache = null;
055
056    protected static final String cacheKey = "CacheKey";
057
058    protected static final String cacheKey_2 = "CacheKey_2";
059
060    protected static final Logger log = LogManager.getLogger( CacheTest.class );
061
062    static {
063        String logSystem = System.getProperty("jcs.logSystem", null);
064        if (logSystem == null) {
065            System.setProperty("jcs.logSystem", "log4j2" );
066            log.info( "Setting jcs.logSystem to: log4j2");
067            logSystem = System.getProperty("jcs.logSystem", null);
068        }
069        log.warn( "What is the value of the jcs.logSystem: "+ logSystem);
070        
071    }
072
073    /**
074     * Method to configure the role name of the service used
075     *
076     * @return the role name of the service to lookup
077     */
078    protected String getCacheRoleName()
079    {
080        return GlobalCacheService.ROLE;
081    }
082
083    @BeforeEach
084    protected void setUp() throws Exception
085    {
086        System.out.println( "Testing service: "+ getClass().getName() + "for "+ getCacheRoleName());
087        //if (globalCache == null) {
088            try
089            {
090                globalCache = (GlobalCacheService) this
091                        .lookup(getCacheRoleName());
092            }
093            catch (ComponentException e)
094            {
095                e.printStackTrace();
096                fail(e.getMessage());
097            }
098        //}
099    }
100    
101    @AfterEach
102    protected void cleanup() {
103        this.globalCache.removeObject(cacheKey);
104    }
105
106    /**
107     * Simple test that verify an object can be created and deleted.
108     *
109     * @throws Exception if unable to add object
110     */
111    @Test
112    public void testSimpleAddGetCacheObject() throws Exception
113    {
114        String testString = "This is a test";
115        Object retrievedObject = null;
116        CachedObject<String> cacheObject1 = null;
117        // Create object
118        cacheObject1 = new CachedObject<String>(testString);
119        assertNotNull(cacheObject1, "Failed to create a cachable object 1" );
120        // Add object to cache
121        this.globalCache.addObject(cacheKey, cacheObject1);
122        // Get object from cache
123        retrievedObject = this.globalCache.getObject(cacheKey);
124        assertNotNull( retrievedObject, "Did not retrieve a cached object 1");
125        assertSame(
126                retrievedObject, cacheObject1,
127                "Did not retrieve a correct, expected cached object 1");
128        // Remove object from cache
129        this.globalCache.removeObject(cacheKey);
130        // Verify object removed from cache
131        retrievedObject = null;
132        cacheObject1 = null;
133        try
134        {
135            retrievedObject = this.globalCache.getObject(cacheKey);
136            assertNull(
137                    retrievedObject,
138                    "Retrieved the deleted cached object 1 and did not get expected ObjectExpiredException"
139                    );
140            assertNotNull(
141                    retrievedObject,
142                    "Did not get expected ObjectExpiredException retrieving a deleted object"
143                    );
144        }
145        catch (ObjectExpiredException e)
146        {
147            assertNull(
148                    retrievedObject,
149                    "Retrieved the deleted cached object 1, but caught expected ObjectExpiredException exception"
150                    );
151        }
152        // Remove object from cache that does NOT exist in the cache
153        this.globalCache.removeObject(cacheKey);
154    }
155
156    /**
157     * Simple test that adds, retrieves, and deletes 2 object.
158     *
159     * @throws Exception if unable to add and retrieve objects
160     */
161    @Test
162    public void test2ObjectAddGetCachedObject() throws Exception
163    {
164        String testString = "This is a test";
165        Object retrievedObject = null;
166        CachedObject<String> cacheObject1 = null;
167        CachedObject<String> cacheObject2 = null;
168        // Create and add Object #1
169        cacheObject1 = new CachedObject<String>(testString);
170        assertNotNull(cacheObject1, "Failed to create a cachable object 1" );
171        this.globalCache.addObject(cacheKey, cacheObject1);
172        retrievedObject = this.globalCache.getObject(cacheKey);
173        assertNotNull( retrievedObject, "Did not retrieve a cached object 1");
174        assertEquals(cacheObject1,
175                retrievedObject, "Did not retrieve correct cached object");
176        // Create and add Object #2
177        cacheObject2 = new CachedObject<String>(testString);
178        assertNotNull(cacheObject2, "Failed to create a cachable object 2");
179        this.globalCache.addObject(cacheKey_2, cacheObject2);
180        retrievedObject = this.globalCache.getObject(cacheKey_2);
181        assertNotNull(retrievedObject, "Did not retrieve a cached object 2");
182        assertEquals( cacheObject2,
183                retrievedObject,
184                "Did not retrieve correct cached object 2");
185        // Get object #1
186        retrievedObject = this.globalCache.getObject(cacheKey);
187        assertNotNull(
188                retrievedObject,
189                "Did not retrieve a cached object 1. Attempt #2");
190        assertEquals(
191                cacheObject1, retrievedObject,
192                "Did not retrieve correct cached object 1. Attempt #2");
193        // Get object #1
194        retrievedObject = this.globalCache.getObject(cacheKey);
195        assertNotNull(
196                retrievedObject,
197                "Did not retrieve a cached object 1. Attempt #3");
198        assertEquals(
199                cacheObject1, retrievedObject,
200                "Did not retrieve correct cached object 1. Attempt #3");
201        // Get object #2
202        retrievedObject = this.globalCache.getObject(cacheKey_2);
203        assertNotNull(
204                retrievedObject,
205                "Did not retrieve a cached object 2. Attempt #2");
206        assertEquals(
207                cacheObject2, retrievedObject,
208                "Did not retrieve correct cached object 2 Attempt #2");
209        // Remove objects
210        this.globalCache.removeObject(cacheKey);
211        this.globalCache.removeObject(cacheKey_2);
212    }
213
214    /**
215     * Verify that an object will throw the ObjectExpiredException when it now
216     * longer exists in cache.
217     *
218     * @throws Exception if object is not expired
219     */
220    @Test
221    public void testObjectExpiration() throws Exception
222    {
223        String testString = "This is a test";
224        Object retrievedObject = null;
225        CachedObject<String> cacheObject = null;
226        // Create and add Object that expires in 1000 millis (1 second)
227        cacheObject = new CachedObject<String>(testString, 1000);
228        assertNotNull(cacheObject, "Failed to create a cachable object");
229        long addTime = System.currentTimeMillis();
230        this.globalCache.addObject(cacheKey, cacheObject);
231        // Try to get un-expired object
232        try
233        {
234            retrievedObject = this.globalCache.getObject(cacheKey);
235            assertNotNull( retrievedObject, "Did not retrieve a cached object");
236            assertEquals(
237                    cacheObject, retrievedObject,
238                    "Did not retrieve correct cached object");
239        }
240        catch (ObjectExpiredException e)
241        {
242            assertTrue(false, "Object expired early ( "
243                    + (System.currentTimeMillis() - addTime) + " millis)");
244        }
245        // Sleep 1500 Millis (1.5 seconds)
246        Thread.sleep(1500);
247        // Try to get expired object
248        try
249        {
250            retrievedObject = null;
251            retrievedObject = this.globalCache.getObject(cacheKey);
252            assertNull(retrievedObject,
253                    "Retrieved the expired cached object  and did not get expected ObjectExpiredException"
254                    );
255            assertNotNull(retrievedObject,
256                    "Did not get expected ObjectExpiredException retrieving an expired object"
257                    );
258        }
259        catch (ObjectExpiredException e)
260        {
261            assertNull(retrievedObject,
262                    "Retrieved the expired cached object, but caught expected ObjectExpiredException exception"
263                    );
264        }
265        // Remove objects
266        this.globalCache.removeObject(cacheKey);
267    }
268
269    /**
270     * Verify the all object will be flushed from the cache.
271     *
272     * This test can take server minutes.
273     *
274     * @throws Exception if flushing the cache fails
275     */
276    @Test
277    public void testCacheFlush() throws Exception
278    {
279        String testString = "This is a test";
280        CachedObject<String> cacheObject = null;
281        // Create and add Object that expires in 1 turbine Refresh + 1 millis
282        cacheObject = new CachedObject<String>(testString, (getCacheRefresh() * 5) + 1);
283        assertNotNull( cacheObject, "Failed to create a cachable object");
284        this.globalCache.addObject(cacheKey, cacheObject);
285        // 1 Refresh
286        Thread.sleep(getCacheRefresh() + 1);
287        assertTrue( (0 < this.globalCache
288                .getNumberOfObjects()),
289                "No object in cache before flush");
290        // Flush Cache
291        this.globalCache.flushCache();
292        // Wait 15 seconds, 3 Refresh
293        // if using disk cache, you might have to wait longer  
294        Thread.sleep((getCacheRefresh() * 2) + 1);
295        assertEquals( 0, this.globalCache.getNumberOfObjects(),
296                "After refresh");
297        // Remove objects
298        this.globalCache.removeObject(cacheKey);
299    }
300
301    /**
302     * Verify the Cache count is correct.
303     *
304     * @throws Exception if the cache count does not match expected value
305     */
306    @Test
307    public void testObjectCount() throws Exception
308    {
309        assertNotNull(this.globalCache, "Could not retrieve cache service.");
310
311        long cacheRefresh = getCacheRefresh();
312
313        // Create and add Object that expires in 1.5 turbine Refresh
314        long expireTime = cacheRefresh + cacheRefresh / 2;
315        log.info( "set expireTime in ms: {}", expireTime );
316
317        CachedObject<String> cacheObject = new CachedObject<String>("This is a test",
318                expireTime);
319        assertNotNull( cacheObject, "Failed to create a cachable object");
320
321        this.globalCache.addObject(cacheKey, cacheObject);
322        assertEquals( 1, this.globalCache
323                .getNumberOfObjects(),
324                "After adding 1 Object");
325
326        // Wait until we're passed 1 refresh, but not half way.
327        Thread.sleep(cacheRefresh + cacheRefresh / 3);
328        assertEquals( 1, this.globalCache
329                .getNumberOfObjects(),
330                "After one refresh");
331
332        // Wait until we're passed 2 more refreshes
333        Thread.sleep((cacheRefresh * 2) + cacheRefresh / 3);
334        
335        assertEquals(0, this.globalCache
336                .getNumberOfObjects(),
337                "After three refreshes");
338    }
339
340    /**
341     * Verify a refreshable object will refreshed in the following cases: o The
342     * object is retrieved via getObject an it is stale. o The object is
343     * determined to be stale during a cache refresh
344     *
345     * This test can take several minutes.
346     *
347     * @throws Exception if object is not a refreshable object
348     */
349    @Tag("LongRunning")
350    @Test
351    public void testRefreshableObject() throws Exception
352    {
353        CachedObject<RefreshableObject> retrievedObject = null;
354        RefreshableCachedObject<RefreshableObject> cacheObject = null;
355        // Create and add Object that expires in TEST_EXPIRETIME millis.
356        cacheObject = new RefreshableCachedObject<RefreshableObject>(new RefreshableObject(),
357                getTestExpireTime());
358        assertNotNull( cacheObject, "Failed to create a cachable object");
359        long addTime = System.currentTimeMillis();
360        log.info( "Adding refreshable object to cache: {}", cacheObject );
361        this.globalCache.addObject(cacheKey, cacheObject);
362        // Try to get un-expired object
363        try
364        {
365            retrievedObject = this.globalCache.getObject(cacheKey);
366            assertNotNull(retrievedObject, "Did not retrieve a cached object");
367            assertEquals(
368                    cacheObject, retrievedObject,
369                    "Did not retrieve correct cached object");
370        }
371        catch (ObjectExpiredException e)
372        {
373            assertTrue(false, "Object expired early ( "
374                    + (System.currentTimeMillis() - addTime) + " millis)"
375                    );
376        }
377        // Wait 1 Turbine cache refresh + 1 second.
378        Thread.sleep(getTestExpireTime() + 1000);
379        // Try to get expired object
380        try
381        {
382            retrievedObject = null;
383            retrievedObject = this.globalCache.getObject(cacheKey);
384            assertNotNull(
385                    retrievedObject, "Did not retrieve a cached object, after sleep");
386            assertNotNull(
387                    ((RefreshableCachedObject<?>) retrievedObject).getContents(),
388                    "Cached object has no contents, after sleep.");
389            assertTrue(
390                   
391                    (((RefreshableCachedObject<RefreshableObject>) retrievedObject)
392                            .getContents().getRefreshCount() > 0),
393                    "Object did not refresh.");
394        }
395        catch (ObjectExpiredException e)
396        {
397            assertTrue(false, "Received unexpected ObjectExpiredException exception "
398                    + "when retrieving refreshable object after ( "
399                    + (System.currentTimeMillis() - addTime) + " millis)"
400                    );
401        }
402        // See if object will expires (testing every second for 100 seconds. It
403        // should not!
404        for (int i = 0; i < 100; i++)
405        {
406            Thread.sleep(1000); // Sleep 0.5 seconds
407            // Try to get expired object
408            try
409            {
410                retrievedObject = null;
411                retrievedObject = this.globalCache.getObject(cacheKey);
412                assertNotNull(retrievedObject,
413                        "Did not retrieve a cached object, after sleep");
414                assertNotNull(
415                        ((RefreshableCachedObject<?>) retrievedObject)
416                                .getContents(),
417                                "Cached object has no contents, after sleep.");
418                assertTrue(
419                        
420                        (((RefreshableCachedObject<RefreshableObject>) retrievedObject)
421                                .getContents().getRefreshCount() > 0),
422                        "Object did not refresh.");
423            }
424            catch (ObjectExpiredException e)
425            {
426                assertTrue(false,
427                        "Received unexpected ObjectExpiredException exception "
428                                + "when retrieving refreshable object after ( "
429                                + (System.currentTimeMillis() - addTime)
430                                + " millis)");
431            }
432        }
433        // Remove objects
434        this.globalCache.removeObject(cacheKey);
435    }
436
437    /**
438     * Verify a cached object will be delete after it has been untouched beyond
439     * it's TimeToLive.
440     *
441     * This test can take several minutes.
442     *
443     * @throws Exception if object is not deleted
444     */
445    @Tag("LongRunning")
446    @Test
447    public void testRefreshableTimeToLive() throws Exception
448    {
449        CachedObject<RefreshableObject> retrievedObject = null;
450        RefreshableCachedObject<RefreshableObject>  cacheObject = null;
451        // Create and add Object that expires in TEST_EXPIRETIME millis.
452        cacheObject = new RefreshableCachedObject<RefreshableObject>(new RefreshableObject(),
453                getTestExpireTime());
454        assertNotNull(cacheObject, "Failed to create a cachable object");
455        cacheObject.setTTL(getTestExpireTime());
456        // Verify TimeToLive was set
457        assertEquals(getTestExpireTime(), cacheObject
458                .getTTL(),
459                "Returned TimeToLive");
460        // Add object to Cache        
461        log.info( "Adding refreshable object to cache: {}", cacheObject );
462        this.globalCache.addObject(cacheKey, cacheObject);
463        long addTime = System.currentTimeMillis();
464        // Try to get un-expired object
465        try
466        {
467            retrievedObject = this.globalCache.getObject(cacheKey);
468            assertNotNull(retrievedObject, "Did not retrieve a cached object");
469            assertEquals(
470                    cacheObject, retrievedObject,
471                    "Did not retrieve correct cached object");
472        }
473        catch (ObjectExpiredException e)
474        {
475            fail("Object expired early ( "
476                    + (System.currentTimeMillis() - addTime) + " millis)");
477        }
478        // Wait long enough to allow object to expire, but do not exceed TTL
479        long timeout = getTestExpireTime() - 0000;
480        Thread.sleep(timeout);
481        // Try to get expired object
482        try
483        {
484            retrievedObject = null;
485            retrievedObject = this.globalCache.getObject(cacheKey);
486            assertNotNull(retrievedObject,
487                    "Did not retrieve a cached object, after sleep");
488            assertNotNull(
489                    ((RefreshableCachedObject<?>) retrievedObject).getContents(),
490                    "Cached object has no contents, after sleep.");
491            /*
492             * @todo this is not working for some reason
493             *
494             * assertTrue( "Object did not refresh.", (((RefreshableObject)
495             * ((RefreshableCachedObject)
496             * retrievedObject).getContents()).getRefreshCount() > 0));
497             */
498        }
499        catch (ObjectExpiredException e)
500        {
501            assertTrue(false, "Received unexpected ObjectExpiredException exception "
502                    + "when retrieving refreshable object after ( "
503                    + (System.currentTimeMillis() - addTime) + " millis)");
504        }
505        // Wait long enough to allow object to expire and exceed TTL
506        Thread.sleep(getTestExpireTime() + 5000);
507        // Try to get expired object
508        try
509        {
510            retrievedObject = null;
511            retrievedObject = this.globalCache.getObject(cacheKey);
512            assertNull(retrievedObject, "Retrieved a cached object, after exceeding TimeToLive"
513                    );
514        }
515        catch (ObjectExpiredException e)
516        {
517            assertNull(retrievedObject,
518                    "Retrieved the expired cached object, but caught expected ObjectExpiredException exception");
519        }
520        // Remove objects
521        this.globalCache.removeObject(cacheKey);
522    }
523
524    /**
525     * Test that we can get a list of the keys in the cache
526     *
527     */
528    @Test
529    public void testCacheGetKeyList()
530    {
531        this.globalCache.flushCache();
532        this.globalCache.addObject("date1", new CachedObject<Date>(new Date()));
533        this.globalCache.addObject("date2", new CachedObject<Date>(new Date()));
534        this.globalCache.addObject("date3", new CachedObject<Date>(new Date()));
535        assertTrue(
536                (this.globalCache.getKeys() != null),
537                "Did not get key list back.");
538        List<String> keys = this.globalCache.getKeys();
539        for (String key : keys)
540        {
541            assertTrue(
542                    (key instanceof String),
543                    "Key was not an instance of String.");
544        }
545
546    }
547
548    /**
549     * Test that we can get a list of the keys in the cache
550     */
551    @Test
552    public void testCacheGetCachedObjects()
553    {
554        this.globalCache.flushCache();
555        this.globalCache.addObject("date1", new CachedObject<Date>(new Date()));
556        this.globalCache.addObject("date2", new CachedObject<Date>(new Date()));
557        this.globalCache.addObject("date3", new CachedObject<Date>(new Date()));
558        assertTrue((this.globalCache
559                .getCachedObjects() != null),
560                "Did not get object list back.");
561        List<CachedObject<?>> objects = this.globalCache.getCachedObjects();
562        for (CachedObject<?> obj : objects)
563        {
564            assertNotNull(obj, "Object was null.");
565            assertTrue(
566                    (obj instanceof CachedObject),
567                    "Object was not an instance of CachedObject");
568        }
569
570    }
571
572    /**
573     * Test that the retrieved list is safe from
574     * ConcurrentModificationException's being thrown if the cache is updated
575     * while we are iterating over the List.
576     *
577     */
578    @Test
579    public void testCacheModification()
580    {
581        this.globalCache.flushCache();
582        this.globalCache.addObject("date1", new CachedObject<Date>(new Date()));
583        this.globalCache.addObject("date2", new CachedObject<Date>(new Date()));
584        this.globalCache.addObject("date3", new CachedObject<Date>(new Date()));
585        assertTrue(
586                (this.globalCache.getKeys() != null),
587                "Did not get key list back.");
588        List<String> keys = this.globalCache.getKeys();
589        try
590        {
591            for (@SuppressWarnings("unused") String key : keys)
592            {
593                this.globalCache.addObject("date4",
594                        new CachedObject<Date>(new Date()));
595            }
596        }
597        catch (ConcurrentModificationException cme)
598        {
599            fail("Caught ConcurrentModificationException adding to cache.");
600        }
601        List<CachedObject<?>> objects = this.globalCache.getCachedObjects();
602        try
603        {
604            for (@SuppressWarnings("unused") CachedObject<?> obj : objects)
605            {
606                this.globalCache.addObject("date4",
607                        new CachedObject<Date>(new Date()));
608            }
609        }
610        catch (ConcurrentModificationException cme)
611        {
612            fail("Caught ConcurrentModificationException adding to cache.");
613        }
614    }
615
616    /**
617     * Down cast the interface to the concrete object in order to grab the
618     * cache check frequency.
619     *
620     * @return the refresh frequency in milliseconds
621     */
622    private long getCacheRefresh()
623    {
624        try
625        {
626            DefaultGlobalCacheService cache =
627                (DefaultGlobalCacheService)this.lookup(GlobalCacheService.ROLE);
628            return cache.getCacheCheckFrequency();
629        }
630        catch (ComponentException e)
631        {
632            return 5000;
633        }
634    }
635
636    /**
637     * How long until it expires
638     *
639     * @return the cache refresh plus 1000 millisec.
640     */
641    private long getTestExpireTime()
642    {
643        return getCacheRefresh() + 1000;
644    }
645
646    /**
647     * Simple object that can be refreshed
648     */
649    class RefreshableObject implements Refreshable
650    {
651        private int refreshCount = 0;
652
653        /**
654         * Increment the refresh counter
655         */
656        @Override
657        public void refresh()
658        {
659            this.refreshCount++;
660        }
661
662        /**
663         * Return the number of time this object has been refreshed
664         *
665         * @return Number of times refresh() has been called
666         */
667        public int getRefreshCount()
668        {
669            return this.refreshCount;
670        }
671    }
672}