info.magnolia.imaging.caching.CachingImageStreamerRepositoryTest.java Source code

Java tutorial

Introduction

Here is the source code for info.magnolia.imaging.caching.CachingImageStreamerRepositoryTest.java

Source

/**
 * This file Copyright (c) 2009-2012 Magnolia International
 * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
 *
 *
 * This file is dual-licensed under both the Magnolia
 * Network Agreement and the GNU General Public License.
 * You may elect to use one or the other of these licenses.
 *
 * This file is distributed in the hope that it will be
 * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
 * Redistribution, except as permitted by whichever of the GPL
 * or MNA you select, is prohibited.
 *
 * 1. For the GPL license (GPL), you can redistribute and/or
 * modify this file under the terms of the GNU General
 * Public License, Version 3, as published by the Free Software
 * Foundation.  You should have received a copy of the GNU
 * General Public License, Version 3 along with this program;
 * if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * 2. For the Magnolia Network Agreement (MNA), this file
 * and the accompanying materials are made available under the
 * terms of the MNA which accompanies this distribution, and
 * is available at http://www.magnolia-cms.com/mna.html
 *
 * Any modifications to this file must keep this entire header
 * intact.
 *
 */
package info.magnolia.imaging.caching;

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.*;
import info.magnolia.cms.core.Content;
import info.magnolia.cms.core.HierarchyManager;
import info.magnolia.cms.core.NodeData;
import info.magnolia.cms.util.ContentUtil;
import info.magnolia.context.JCRSessionStrategy;
import info.magnolia.context.MgnlContext;
import info.magnolia.imaging.AbstractRepositoryTestCase;
import info.magnolia.imaging.DefaultImageStreamer;
import info.magnolia.imaging.ImageGenerator;
import info.magnolia.imaging.ImageStreamer;
import info.magnolia.imaging.ImagingException;
import info.magnolia.imaging.OutputFormat;
import info.magnolia.imaging.ParameterProvider;
import info.magnolia.imaging.ParameterProviderFactory;
import info.magnolia.imaging.operations.ImageOperationChain;
import info.magnolia.imaging.operations.load.URLImageLoader;
import info.magnolia.imaging.parameters.ContentParameterProvider;
import info.magnolia.imaging.parameters.SimpleEqualityContentWrapper;
import info.magnolia.jcr.wrapper.DelegateSessionWrapper;
import info.magnolia.module.ModuleManagementException;
import info.magnolia.module.ModuleManager;
import info.magnolia.module.ModuleManagerImpl;
import info.magnolia.module.ModuleRegistry;
import info.magnolia.module.model.ModuleDefinition;
import info.magnolia.module.model.reader.ModuleDefinitionReader;
import info.magnolia.test.ComponentsTestUtil;
import info.magnolia.test.mock.MockContext;

import java.awt.GraphicsEnvironment;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import javax.imageio.ImageIO;
import javax.jcr.RepositoryException;
import javax.jcr.Session;

import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;

/**
 * TODO : this is sort of a load test and uses a real (in memory) repository.
 * TODO : cleanup.
 *
 * @version $Id$
 */
public class CachingImageStreamerRepositoryTest extends AbstractRepositoryTestCase {
    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory
            .getLogger(CachingImageStreamerRepositoryTest.class);

    @Override
    @Before
    public void setUp() throws Exception {
        // this used to set autostart to false, but I'm not sure why.
        // It seems fine as is.

        super.setUp();

        // Now replace JcrSessionStrategy instances (current ctx and system ctx) with a wrapper than ensures we only save once, for the purpose of these tests.
        final MockContext systemContext = (MockContext) MgnlContext.getSystemContext();
        systemContext.setRepositoryStrategy(new DelegatingSessionStrategy(systemContext.getRepositoryStrategy()));

        final MockContext regularContext = (MockContext) MgnlContext.getInstance();
        regularContext.setRepositoryStrategy(new DelegatingSessionStrategy(regularContext.getRepositoryStrategy()));

        final GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        if (!ge.isHeadless()) {
            log.warn("This test should run in headless mode as the server will likely run headless too!!!!!");
        }
    }

    @Test
    public void testRequestForSimilarUncachedImageOnlyGeneratesItOnce() throws Exception {
        final HierarchyManager srcHM = MgnlContext.getHierarchyManager("website");
        final String srcPath = "/foo/bar";
        ContentUtil.createPath(srcHM, srcPath);

        // ParameterProvider for tests - return a new instance of the same node everytime
        // if we'd return the same src instance everytime, the purpose of this test would be null
        final ParameterProviderFactory<Object, Content> ppf = new TestParameterProviderFactory(srcHM, srcPath);

        final OutputFormat png = new OutputFormat();
        png.setFormatName("png");

        final BufferedImage dummyImg = ImageIO.read(getClass().getResourceAsStream("/funnel.gif"));
        assertNotNull("Couldn't load dummy test image", dummyImg);

        final ImageGenerator<ParameterProvider<Content>> generator = mock(ImageGenerator.class);
        when(generator.getParameterProviderFactory()).thenReturn(ppf);
        when(generator.getName()).thenReturn("test");
        when(generator.getOutputFormat(isA(ParameterProvider.class))).thenReturn(png);

        // aaaaand finally, here's the real reason for this test !
        when(generator.generate(isA(ParameterProvider.class))).thenReturn(dummyImg);

        // yeah, we're using a "wrong" workspace for the image cache, to avoid having to setup a custom one in this test
        final HierarchyManager hm = MgnlContext.getHierarchyManager("config");
        final ImageStreamer streamer = new CachingImageStreamer(hm, ppf.getCachingStrategy(),
                new DefaultImageStreamer());

        // Generator instances will always be the same (including paramProvFac)
        // since they are instantiated with the module config and c2b.
        // ParamProv is a new instance every time.
        // streamer can (must) be the same - once single HM, one cache.

        // thread pool of 10, launching 8 requests, can we hit some concurrency please ?
        final ExecutorService executor = Executors.newFixedThreadPool(10);
        final ByteArrayOutputStream[] outs = new ByteArrayOutputStream[8];
        final Future[] futures = new Future[8];
        for (int i = 0; i < outs.length; i++) {
            outs[i] = new ByteArrayOutputStream();
            futures[i] = executor.submit(new TestJob(generator, streamer, outs[i]));
        }
        executor.shutdown();
        executor.awaitTermination(30, TimeUnit.SECONDS);

        for (Future<?> future : futures) {
            assertTrue(future.isDone());
            assertFalse(future.isCancelled());
            // ignore the results of TestJob - all we care about is if an exception was thrown
            // and if there was any, it is kept in Future until we call Future.get()
            future.get();
        }

        final NodeData cachedNodeData = hm.getNodeData("/test/website/foo/bar/generated-image");
        // update node meta data
        Content cachedNode = hm.getContent("/test/website/foo/bar");
        cachedNode.getMetaData().setModificationDate();
        cachedNode.save();
        final InputStream res = cachedNodeData.getStream();
        final ByteArrayOutputStream cachedOut = new ByteArrayOutputStream();
        IOUtils.copy(res, cachedOut);

        // assert all outs are the same
        for (int i = 1; i < outs.length; i++) {
            // TODO assert they're all equals byte to byte to the source? or in size? can't as-is since we convert...
            final byte[] a = outs[i - 1].toByteArray();
            final byte[] b = outs[i].toByteArray();
            assertTrue(a.length > 0);
            assertEquals("Different sizes (" + Math.abs(a.length - b.length) + " bytes diff.) with i=" + i,
                    a.length, b.length);
            assertTrue("not equals for outs/" + i, Arrays.equals(a, b));
            outs[i - 1] = null; // cleanup all those byte[], or we'll soon run out of memory
        }
        assertTrue("failed comparing last thread's result with what we got from hierarchyManager",
                Arrays.equals(outs[outs.length - 1].toByteArray(), cachedOut.toByteArray()));
        outs[outs.length - 1] = null;

        // now start again another bunch of requests... they should ALL get their results from the cache
        final ExecutorService executor2 = Executors.newFixedThreadPool(10);
        final ByteArrayOutputStream[] outs2 = new ByteArrayOutputStream[8];
        final Future[] futures2 = new Future[8];
        for (int i = 0; i < outs2.length; i++) {
            outs2[i] = new ByteArrayOutputStream();
            futures2[i] = executor2.submit(new TestJob(generator, streamer, outs2[i]));
        }
        executor2.shutdown();
        executor2.awaitTermination(30, TimeUnit.SECONDS);

        for (Future<?> future : futures2) {
            assertTrue(future.isDone());
            assertFalse(future.isCancelled());
            // ignore the results of TestJob - all we care about is if an exception was thrown
            // and if there was any, it is kept in Future until we call Future.get()
            future.get();
        }

        final NodeData cachedNodeData2 = hm.getNodeData("/test/website/foo/bar/generated-image");
        final InputStream res2 = cachedNodeData2.getStream();
        final ByteArrayOutputStream cachedOut2 = new ByteArrayOutputStream();
        IOUtils.copy(res2, cachedOut2);

        // assert all outs are the same
        for (int i = 1; i < outs2.length; i++) {
            // TODO assert they're all equals byte to byte to the source? or in size? can't as-is since we re-save..
            final byte[] a = outs2[i - 1].toByteArray();
            final byte[] b = outs2[i].toByteArray();
            assertTrue(a.length > 0);
            assertEquals("Different sizes (" + Math.abs(a.length - b.length) + " bytes diff.) with i=" + i,
                    a.length, b.length);
            assertTrue("not equals for outs2/" + i, Arrays.equals(a, b));
            outs2[i - 1] = null;
        }
        assertTrue("failed comparing last thread's result with what we got from hierarchyManager",
                Arrays.equals(outs2[outs2.length - 1].toByteArray(), cachedOut2.toByteArray()));

        outs2[outs2.length - 1] = null;
    }

    @Test
    public void testGenerateAndStoreIsDoneUnderSystemContext()
            throws RepositoryException, IOException, ImagingException {
        // GIVEN
        final HierarchyManager srcHM = MgnlContext.getHierarchyManager("website");
        final String srcPath = "/foo/bar";
        ContentUtil.createPath(srcHM, srcPath);

        // ParameterProvider for tests - return a new instance of the same node everytime
        // if we'd return the same src instance everytime, the purpose of this test would be null
        final ParameterProviderFactory<Object, Content> ppf = new TestParameterProviderFactory(srcHM, srcPath);

        final OutputFormat png = new OutputFormat();
        png.setFormatName("png");

        final BufferedImage dummyImg = ImageIO.read(getClass().getResourceAsStream("/funnel.gif"));
        assertNotNull("Couldn't load dummy test image", dummyImg);

        final ImageGenerator<ParameterProvider<Content>> generator = mock(ImageGenerator.class);
        when(generator.getParameterProviderFactory()).thenReturn(ppf);
        when(generator.getName()).thenReturn("test");
        when(generator.getOutputFormat(isA(ParameterProvider.class))).thenReturn(png);

        when(generator.generate(isA(ParameterProvider.class))).thenReturn(dummyImg);

        // yeah, we're using a "wrong" workspace for the image cache, to avoid having to setup a custom one in this test
        final HierarchyManager hm = MgnlContext.getHierarchyManager("config");

        final CachingImageStreamer streamer = new CachingImageStreamer(hm, ppf.getCachingStrategy(),
                new DefaultImageStreamer());

        // WHEN
        streamer.generateAndStore(generator, generator.getParameterProviderFactory().newParameterProviderFor(null));

        // THEN
        final Session systemSession = MgnlContext.getSystemContext().getJCRSession("config");
        final Session session = MgnlContext.getJCRSession("config");
        assertTrue(systemSession instanceof SingleSaveSessionWrapper);
        assertTrue(session instanceof SingleSaveSessionWrapper);
        assertTrue("should have saved in system session", ((SingleSaveSessionWrapper) systemSession).saved);
        assertFalse("should not have saved in regular session", ((SingleSaveSessionWrapper) session).saved);
    }

    /**
     * This test is not executed by default - too long !
     * Used to reproduce the "session already closed issue", see MGNLIMG-59.
     * Set the "expiration" property of the jobs map in CachingImageStreamer to a longer value
     * to have more chances of reproducing the problem.
     */
    @Ignore
    @Test
    public void testConcurrencyAndJCRSessions() throws Exception {
        final HierarchyManager srcHM = MgnlContext.getHierarchyManager("website");
        final String srcPath = "/foo/bar";
        ContentUtil.createPath(srcHM, srcPath);

        // ParameterProvider for tests - return a new instance of the same node everytime
        // if we'd return the same src instance everytime, the purpose of this test would be null
        final ParameterProviderFactory<Object, Content> ppf = new TestParameterProviderFactory(srcHM, srcPath);

        final OutputFormat png = new OutputFormat();
        png.setFormatName("png");

        final ImageOperationChain<ParameterProvider<Content>> generator = new ImageOperationChain<ParameterProvider<Content>>();
        final URLImageLoader<ParameterProvider<Content>> load = new URLImageLoader<ParameterProvider<Content>>();
        load.setUrl(getClass().getResource("/funnel.gif").toExternalForm());
        generator.addOperation(load);
        generator.setOutputFormat(png);
        generator.setName("foo blob bar");
        generator.setParameterProviderFactory(ppf);

        // yeah, we're using a "wrong" workspace for the image cache, to avoid having to setup a custom one in this test
        final HierarchyManager hm = MgnlContext.getHierarchyManager("config");

        final ImageStreamer streamer = new CachingImageStreamer(hm, ppf.getCachingStrategy(),
                new DefaultImageStreamer());

        // thread pool of 10, launching 8 requests, can we hit some concurrency please ?
        final ExecutorService executor = Executors.newFixedThreadPool(10);
        final ByteArrayOutputStream[] outs = new ByteArrayOutputStream[8];
        final Future[] futures = new Future[8];
        for (int i = 0; i < outs.length; i++) {
            final int ii = i;
            outs[i] = new ByteArrayOutputStream();
            futures[i] = executor.submit(new Runnable() {
                @Override
                public void run() {
                    final ParameterProvider p = generator.getParameterProviderFactory()
                            .newParameterProviderFor(null);
                    try {
                        streamer.serveImage(generator, p, outs[ii]);
                    } catch (Exception e) {
                        throw new RuntimeException(e); // TODO
                    }
                }
            });
        }
        executor.shutdown();
        executor.awaitTermination(30, TimeUnit.SECONDS);

        for (Future<?> future : futures) {
            assertTrue(future.isDone());
            assertFalse(future.isCancelled());
            // ignore the results of TestJob - but if there was an exception thrown by TestJob.call(),
            // it is only thrown back at us when we call get() below. (so the test will fail badly if the job threw an exception)
            Object ignored = future.get();
        }

        shutdownRepository(true);

        // sleep for a while so that the jobs map's expiration thread can kick in !
        Thread.sleep(10000);
    }

    // just a generation job for tests
    private class TestJob implements Callable<Object> {
        private final ImageGenerator generator;
        private final ImageStreamer streamer;
        private final OutputStream out;

        public TestJob(ImageGenerator generator, ImageStreamer streamer, final OutputStream out) {
            this.generator = generator;
            this.streamer = streamer;
            this.out = out;
        }

        @Override
        public Object call() throws Exception {
            final ParameterProvider p = generator.getParameterProviderFactory().newParameterProviderFor(null);
            streamer.serveImage(generator, p, out);
            return null;
        }
    }

    // TODO - this is an ugly hack to workaround MAGNOLIA-2593 - we should review RepositoryTestCase
    @Override
    protected void initDefaultImplementations() throws IOException, ModuleManagementException {
        //MgnlTestCase clears factory before running this method, so we have to instrument factory here rather then in setUp() before calling super.setUp()
        ModuleRegistry registry = mock(ModuleRegistry.class);
        ComponentsTestUtil.setInstance(ModuleRegistry.class, registry);

        final ModuleDefinitionReader fakeReader = new ModuleDefinitionReader() {
            @Override
            public ModuleDefinition read(Reader in) throws ModuleManagementException {
                return null;
            }

            @Override
            public Map readAll() throws ModuleManagementException {
                Map m = new HashMap();
                m.put("moduleDef", "dummy");
                return m;
            }

            @Override
            public ModuleDefinition readFromResource(String resourcePath) throws ModuleManagementException {
                return null;
            }
        };
        final ModuleManagerImpl fakeModuleManager = new ModuleManagerImpl(null, fakeReader) {
            @Override
            public List loadDefinitions() throws ModuleManagementException {
                // TODO Auto-generated method stub
                return new ArrayList();
            }
        };
        ComponentsTestUtil.setInstance(ModuleManager.class, fakeModuleManager);
        super.initDefaultImplementations();
    }

    /*
    final IMocksControl iMocksControl = createStrictControl();
    // can't be strict on method call order on hm, because we can't guarantee which of the 8 threads will create the node
    iMocksControl.checkOrder(false);
    // not exactly sure what this entails; hopefully it doesn't render the test useless...
    iMocksControl.makeThreadSafe(true);
    final HierarchyManager hm = iMocksControl.createMock(HierarchyManager.class);
    final Content root = createStrictMock(Content.class);
    final Content t = createStrictMock(Content.class);
    final Content m = createStrictMock(Content.class);
    final Content p = createStrictMock(Content.class);
    final Content y = createStrictMock(Content.class);
        
    // first 8 request: node doesn't exist.
    for (int i = 0; i < 8; i++) {
    // generator name + path provided by ParameterProviderFactory
    expect(hm.isExist("/test/my/param/yo")).andReturn(false);
    }
        
    // one of these 8 threads creates the node
    expect(hm.getRoot()).andReturn(root);
    expect(root.hasContent("test")).andReturn(false);
    expect(root.createContent("test", ItemType.CONTENT)).andReturn(t);
    expect(t.hasContent("my")).andReturn(false);
    expect(t.createContent("my", ItemType.CONTENT)).andReturn(m);
    expect(m.hasContent("param")).andReturn(false);
    expect(m.createContent("param", ItemType.CONTENT)).andReturn(p);
    expect(p.hasContent("yo")).andReturn(false);
    expect(p.createContent("yo", ItemType.CONTENT)).andReturn(y);
    expect(y.hasNodeData("generated-image")).andReturn(false);
        
    // 8 more requests, the node exists in the hm
    for (int i = 0; i < 8; i++) {
    expect(hm.isExist("/test/my/param/yo")).andReturn(true);
    }
        
    replay(hm, root, t, m, p, y);
     */

    /**
     * A JCRSessionStrategy which allows returning a given session instead of the regular one.
     */
    private static class DelegatingSessionStrategy implements JCRSessionStrategy {
        private final Map<String, Session> jcrSessions = new HashMap<String, Session>();
        private final JCRSessionStrategy delegate;

        DelegatingSessionStrategy(JCRSessionStrategy delegate) {
            this.delegate = delegate;
        }

        @Override
        public Session getSession(String workspaceName) throws RepositoryException {
            if (jcrSessions.containsKey(workspaceName)) {
                final Session session = jcrSessions.get(workspaceName);
                assertThat(session, is(instanceOf(SingleSaveSessionWrapper.class)));
                return session;
            } else {
                final Session session = delegate.getSession(workspaceName);
                assertThat(session, is(not(instanceOf(SingleSaveSessionWrapper.class))));
                final SingleSaveSessionWrapper sessionWrapper = new SingleSaveSessionWrapper(session);
                jcrSessions.put(workspaceName, sessionWrapper);
                return sessionWrapper;
            }
        }

        @Override
        public void release() {
            delegate.release();
        }
    }

    private static class TestParameterProviderFactory implements ParameterProviderFactory {
        private final HierarchyManager srcHM;
        private final String srcPath;

        public TestParameterProviderFactory(HierarchyManager srcHM, String srcPath) {
            this.srcHM = srcHM;
            this.srcPath = srcPath;
        }

        @Override
        public ParameterProvider<Content> newParameterProviderFor(Object environment) {
            try {
                final Content src = srcHM.getContent(srcPath);
                // copied from ContentParameterProviderFactory
                return new ContentParameterProvider(new SimpleEqualityContentWrapper(src));
            } catch (RepositoryException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public CachingStrategy getCachingStrategy() {
            return new ContentBasedCachingStrategy();
        }
    }

    private static class SingleSaveSessionWrapper extends DelegateSessionWrapper {
        boolean saved = false;

        public SingleSaveSessionWrapper(Session session) throws RepositoryException {
            super(session);
        }

        @Override
        public synchronized void save() throws RepositoryException {
            if (saved) {
                fail("save() was called more than once");
            } else {
                saved = true;
            }
            super.save();
        }
    }
}