/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. */ package com.liferay.util.resiliency.spi.provider; import com.liferay.portal.kernel.exception.PortalException; import com.liferay.portal.kernel.process.ClassPathUtil; import com.liferay.portal.kernel.resiliency.mpi.MPIHelperUtil; import com.liferay.portal.kernel.resiliency.spi.MockSPI; import com.liferay.portal.kernel.resiliency.spi.MockSPIProvider; import com.liferay.portal.kernel.resiliency.spi.SPIUtil; import com.liferay.portal.kernel.resiliency.spi.provider.SPIProvider; import com.liferay.portal.kernel.test.CaptureHandler; import com.liferay.portal.kernel.test.JDKLoggerTestUtil; import com.liferay.portal.kernel.test.ReflectionTestUtil; import com.liferay.portal.kernel.test.rule.CodeCoverageAssertor; import com.liferay.portal.kernel.util.CharPool; import com.liferay.portal.kernel.util.PortalClassLoaderUtil; import com.liferay.portal.kernel.util.Props; import com.liferay.portal.kernel.util.PropsKeys; import com.liferay.portal.kernel.util.PropsUtil; import com.liferay.portal.kernel.util.ProxyUtil; import com.liferay.portal.kernel.util.StringBundler; import com.liferay.portal.kernel.util.StringPool; import com.liferay.portal.kernel.util.StringUtil; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.LogRecord; import javax.servlet.ServletContextEvent; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.springframework.mock.web.MockServletContext; /** * @author Shuyang Zhou */ public class SPIClassPathContextListenerTest { @ClassRule public static final CodeCoverageAssertor codeCoverageAssertor = CodeCoverageAssertor.INSTANCE; @Before public void setUp() throws Exception { // Embedded lib directory File spiEmbeddedLibDir = new File( _CONTEXT_PATH, _EMBEDDED_LIB_DIR_NAME); spiEmbeddedLibDir.mkdir(); _jarFile = new File(spiEmbeddedLibDir, "jarFile.jar"); _jarFile.createNewFile(); File notJarFile = new File(spiEmbeddedLibDir, "notJarFile.zip"); notJarFile.createNewFile(); // Embedded lib ext directory File spiEmbeddedLibExtDir = new File( _CONTEXT_PATH, _EMBEDDED_LIB_EXT_DIR_NAME); spiEmbeddedLibExtDir.mkdir(); _extJarFile = new File(spiEmbeddedLibExtDir, "extJarFile.jar"); _extJarFile.createNewFile(); File extNotJarFile = new File( spiEmbeddedLibExtDir, "extNotJarFile.zip"); extNotJarFile.createNewFile(); // Global lib 1 directory for portal-kernel.jar File globalLib1Dir = new File(_CONTEXT_PATH, _GLOBAL_LIB_1_DIR_NAME); globalLib1Dir.mkdir(); _portalServiceJarFile = new File(globalLib1Dir, "portal-kernel.jar"); _portalServiceJarFile.createNewFile(); _global1JarFile = new File(globalLib1Dir, "global1JarFile.jar"); _global1JarFile.createNewFile(); File global1NotJarFile = new File( globalLib1Dir, "global1NotJarFile.zip"); global1NotJarFile.createNewFile(); // Global lib 2 directory for JDBC driver File globalLib2Dir = new File(_CONTEXT_PATH, _GLOBAL_LIB_2_DIR_NAME); globalLib2Dir.mkdir(); _jdbcDriverJarFile = new File(globalLib2Dir, "jdbcDriver.jar"); _jdbcDriverJarFile.createNewFile(); _global2JarFile = new File(globalLib2Dir, "global2JarFile.jar"); _global2JarFile.createNewFile(); File global2NotJarFile = new File( globalLib2Dir, "global2NotJarFile.zip"); global2NotJarFile.createNewFile(); // Mock lookup final Map<String, URL> resources = new HashMap<>(); final String driverClassName = "TestDriver"; putResource(resources, _jdbcDriverJarFile, driverClassName); putResource( resources, _portalServiceJarFile, PortalException.class.getName()); final Method getMethod = Props.class.getMethod("get", String.class); PropsUtil.setProps( (Props)ProxyUtil.newProxyInstance( Props.class.getClassLoader(), new Class<?>[] {Props.class}, new InvocationHandler() { @Override public Object invoke( Object proxy, Method method, Object[] args) { if (getMethod.equals(method)) { if (args[0].equals( PropsKeys.JDBC_DEFAULT_DRIVER_CLASS_NAME)) { return driverClassName; } return StringPool.BLANK; } throw new UnsupportedOperationException(); } })); PortalClassLoaderUtil.setClassLoader( new ClassLoader() { @Override public URL getResource(String name) { URL url = resources.get(name); if (url != null) { return url; } return super.getResource(name); } }); } @After public void tearDown() { deleteFile(new File(_CONTEXT_PATH, _EMBEDDED_LIB_DIR_NAME)); deleteFile(new File(_CONTEXT_PATH, _GLOBAL_LIB_1_DIR_NAME)); deleteFile(new File(_CONTEXT_PATH, _GLOBAL_LIB_2_DIR_NAME)); } @Test public void testClassPathGeneration() { try (CaptureHandler captureHandler = JDKLoggerTestUtil.configureJDKLogger( SPIClassPathContextListener.class.getName(), Level.FINE)) { // With log List<LogRecord> logRecords = captureHandler.getLogRecords(); _mockServletContext.addInitParameter( "spiProviderClassName", "InvalidSPIProvider"); SPIClassPathContextListener spiClassPathContextListener = new SPIClassPathContextListener(); spiClassPathContextListener.contextInitialized( new ServletContextEvent(_mockServletContext)); StringBundler sb = new StringBundler(); sb.append(_jarFile.getAbsolutePath()); sb.append(File.pathSeparator); sb.append(_global1JarFile.getAbsolutePath()); sb.append(File.pathSeparator); sb.append(_portalServiceJarFile.getAbsolutePath()); sb.append(File.pathSeparator); sb.append(_global2JarFile.getAbsolutePath()); sb.append(File.pathSeparator); sb.append(_jdbcDriverJarFile.getAbsolutePath()); sb.append(File.pathSeparator); sb.append(_extJarFile.getAbsolutePath()); sb.append(File.pathSeparator); sb.append(_CONTEXT_PATH); sb.append("/WEB-INF/classes"); String spiClassPath = sb.toString(); Assert.assertEquals( spiClassPath, SPIClassPathContextListener.SPI_CLASS_PATH); Assert.assertEquals(logRecords.toString(), 2, logRecords.size()); LogRecord logRecord = logRecords.get(0); Assert.assertEquals( "SPI class path " + spiClassPath, logRecord.getMessage()); logRecord = logRecords.get(1); Assert.assertEquals( "Unable to create SPI provider with name InvalidSPIProvider", logRecord.getMessage()); // Without log logRecords = captureHandler.resetLogLevel(Level.OFF); ReflectionTestUtil.setFieldValue( SPIUtil.class, "_spi", new MockSPI()); try { spiClassPathContextListener.contextInitialized( new ServletContextEvent(_mockServletContext)); } finally { ReflectionTestUtil.setFieldValue(SPIUtil.class, "_spi", null); } Assert.assertEquals( spiClassPath, SPIClassPathContextListener.SPI_CLASS_PATH); Assert.assertTrue(logRecords.isEmpty()); } } @Test public void testLoadClassDirectly() throws Exception { String jvmClassPath = ClassPathUtil.getJVMClassPath(false); URL[] urls = ClassPathUtil.getClassPathURLs(jvmClassPath); ClassLoader parentClassLoader = new URLClassLoader(urls, null); ClassLoader childClassLoader = new URLClassLoader( urls, parentClassLoader); Class<?> clazz = SPIClassPathContextListener.loadClassDirectly( childClassLoader, TestClass.class.getName()); Assert.assertNotSame(TestClass.class, clazz); Assert.assertEquals(TestClass.class.getName(), clazz.getName()); Assert.assertSame(childClassLoader, clazz.getClassLoader()); Assert.assertSame( clazz, ReflectionTestUtil.invoke( childClassLoader, "findLoadedClass", new Class<?>[] {String.class}, TestClass.class.getName())); Assert.assertNull( ReflectionTestUtil.invoke( parentClassLoader, "findLoadedClass", new Class<?>[] {String.class}, TestClass.class.getName())); Assert.assertSame( clazz, SPIClassPathContextListener.loadClassDirectly( childClassLoader, TestClass.class.getName())); } @Test public void testMissingGlobalLibDir1() throws IOException { testMissingDir(_GLOBAL_LIB_1_DIR_NAME); } @Test public void testMissingGlobalLibDir2() throws IOException { testMissingDir(_GLOBAL_LIB_2_DIR_NAME); } @Test public void testMissingSPIEmbeddedLibDir() throws IOException { testMissingDir(_EMBEDDED_LIB_DIR_NAME); } @Test public void testMissingSPIEmbeddedLibExtDir() throws IOException { testMissingDir(_EMBEDDED_LIB_EXT_DIR_NAME); } @Test public void testRegistration() { // Register File embeddedLibDir = new File(_CONTEXT_PATH, _EMBEDDED_LIB_DIR_NAME); SPIClassPathContextListener spiClassPathContextListener = new SPIClassPathContextListener(); _mockServletContext.addInitParameter( "spiProviderClassName", MockSPIProvider.class.getName()); spiClassPathContextListener.contextInitialized( new ServletContextEvent(_mockServletContext)); AtomicReference<SPIProvider> spiProviderReference = SPIClassPathContextListener.spiProviderReference; Assert.assertNotNull(spiProviderReference.get()); List<SPIProvider> spiProviders = MPIHelperUtil.getSPIProviders(); Assert.assertEquals(spiProviders.toString(), 1, spiProviders.size()); Assert.assertSame(spiProviderReference.get(), spiProviders.get(0)); // Duplicate register try (CaptureHandler captureHandler = JDKLoggerTestUtil.configureJDKLogger( SPIClassPathContextListener.class.getName(), Level.SEVERE)) { List<LogRecord> logRecords = captureHandler.getLogRecords(); spiClassPathContextListener.contextInitialized( new ServletContextEvent(_mockServletContext)); Assert.assertEquals(logRecords.toString(), 1, logRecords.size()); LogRecord logRecord = logRecords.get(0); Assert.assertEquals( "Duplicate SPI provider " + spiProviderReference.get() + " is already registered in servlet context " + _mockServletContext.getContextPath(), logRecord.getMessage()); } // Unregister spiClassPathContextListener.contextDestroyed( new ServletContextEvent(_mockServletContext)); Assert.assertNull(spiProviderReference.get()); spiProviders = MPIHelperUtil.getSPIProviders(); Assert.assertTrue(spiProviders.isEmpty()); // Duplicate unregister spiClassPathContextListener.contextDestroyed( new ServletContextEvent(_mockServletContext)); Assert.assertNull(spiProviderReference.get()); spiProviders = MPIHelperUtil.getSPIProviders(); Assert.assertTrue(spiProviders.isEmpty()); // Register from SPI _mockServletContext.addInitParameter( "spiProviderClassName", MockSPIProvider.class.getName()); ReflectionTestUtil.setFieldValue(SPIUtil.class, "_spi", new MockSPI()); try { spiClassPathContextListener.contextInitialized( new ServletContextEvent(_mockServletContext)); } finally { ReflectionTestUtil.setFieldValue(SPIUtil.class, "_spi", null); } spiProviderReference = SPIClassPathContextListener.spiProviderReference; Assert.assertNotNull(spiProviderReference.get()); spiProviders = MPIHelperUtil.getSPIProviders(); Assert.assertEquals(spiProviders.toString(), 1, spiProviders.size()); Assert.assertSame(spiProviderReference.get(), spiProviders.get(0)); embeddedLibDir.delete(); } protected void deleteFile(File file) { Queue<File> fileQueue = new LinkedList<>(); fileQueue.offer(file); while ((file = fileQueue.poll()) != null) { if (file.isFile()) { file.delete(); } else if (file.isDirectory()) { File[] files = file.listFiles(); if (files.length == 0) { file.delete(); } else { Collections.addAll(fileQueue, files); fileQueue.add(file); } } } } protected void putResource( Map<String, URL> resources, File jarFile, String className) throws MalformedURLException { String resourceName = className.replace( CharPool.PERIOD, CharPool.SLASH); resourceName = resourceName.concat(".class"); URL url = new URL( "file://" + jarFile.getAbsolutePath() + "!/" + resourceName); resources.put(resourceName, url); } protected void testMissingDir(String dirName) throws IOException { // Does not exist File file = new File(_CONTEXT_PATH, dirName); deleteFile(file); SPIClassPathContextListener spiClassPathContextListener = new SPIClassPathContextListener(); try { spiClassPathContextListener.contextInitialized( new ServletContextEvent(_mockServletContext)); Assert.fail(); } catch (RuntimeException re) { Assert.assertEquals( "Unable to find directory " + file.getAbsolutePath(), re.getMessage()); } // Not a directory file.deleteOnExit(); file.createNewFile(); try { spiClassPathContextListener.contextInitialized( new ServletContextEvent(_mockServletContext)); Assert.fail(); } catch (RuntimeException re) { Assert.assertEquals( "Unable to find directory " + file.getAbsolutePath(), re.getMessage()); } finally { file.delete(); } } private static final String _CONTEXT_PATH = StringUtil.toLowerCase( System.getProperty("java.io.tmpdir")); private static final String _EMBEDDED_LIB_DIR_NAME = "/embeddedLib"; private static final String _EMBEDDED_LIB_EXT_DIR_NAME = "/embeddedLib/ext"; private static final String _GLOBAL_LIB_1_DIR_NAME = "/globalLib1"; private static final String _GLOBAL_LIB_2_DIR_NAME = "/globalLib2"; private File _extJarFile; private File _global1JarFile; private File _global2JarFile; private File _jarFile; private File _jdbcDriverJarFile; private final MockServletContext _mockServletContext = new MockServletContext() { { addInitParameter("spiEmbeddedLibDir", _EMBEDDED_LIB_DIR_NAME); } @Override public String getRealPath(String path) { return _CONTEXT_PATH; } }; private File _portalServiceJarFile; private static class TestClass { } }