/** * 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.portal.lpkg.deployer.internal; import com.liferay.portal.kernel.concurrent.DefaultNoticeableFuture; import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayInputStream; import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import com.liferay.portal.kernel.lpkg.StaticLPKGResolver; import com.liferay.portal.kernel.util.CharPool; import com.liferay.portal.kernel.util.StreamUtil; import com.liferay.portal.kernel.util.StringBundler; import com.liferay.portal.kernel.util.StringPool; import com.liferay.portal.kernel.util.StringUtil; import com.liferay.portal.kernel.util.URLCodec; import com.liferay.portal.lpkg.deployer.internal.wrapper.bundle.URLStreamHandlerServiceServiceTrackerCustomizer; import com.liferay.portal.lpkg.deployer.internal.wrapper.bundle.WARBundleWrapperBundleActivator; import com.liferay.portal.util.PropsValues; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.osgi.framework.Bundle; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleEvent; import org.osgi.framework.BundleException; import org.osgi.framework.Constants; import org.osgi.framework.FrameworkEvent; import org.osgi.framework.FrameworkListener; import org.osgi.framework.Version; import org.osgi.framework.startlevel.BundleStartLevel; import org.osgi.framework.wiring.FrameworkWiring; import org.osgi.service.url.URLConstants; import org.osgi.util.tracker.BundleTrackerCustomizer; import org.osgi.util.tracker.ServiceTrackerCustomizer; /** * @author Shuyang Zhou */ public class LPKGBundleTrackerCustomizer implements BundleTrackerCustomizer<List<Bundle>> { public LPKGBundleTrackerCustomizer( BundleContext bundleContext, Map<String, URL> urls, Set<String> overrideFileNames) { _bundleContext = bundleContext; _urls = urls; _overrideFileNames = overrideFileNames; } @Override public List<Bundle> addingBundle(Bundle bundle, BundleEvent bundleEvent) { URL url = bundle.getEntry("liferay-marketplace.properties"); if (url == null) { return null; } String symbolicName = bundle.getSymbolicName(); if (symbolicName.equals( StaticLPKGResolver.getStaticLPKGBundleSymbolicName())) { return Collections.emptyList(); } List<Bundle> bundles = new ArrayList<>(); try { Enumeration<URL> enumeration = bundle.findEntries( "/", "*.jar", false); if (enumeration != null) { while (enumeration.hasMoreElements()) { url = enumeration.nextElement(); if (_checkOverridden(symbolicName, url)) { continue; } if (_isBundleInstalled(bundle, url)) { continue; } Bundle newBundle = _bundleContext.installBundle( url.getPath(), url.openStream()); BundleStartLevel bundleStartLevel = newBundle.adapt( BundleStartLevel.class); bundleStartLevel.setStartLevel( PropsValues. MODULE_FRAMEWORK_DYNAMIC_INSTALL_START_LEVEL); bundles.add(newBundle); } } enumeration = bundle.findEntries("/", "*.war", false); if (enumeration == null) { return bundles; } while (enumeration.hasMoreElements()) { url = enumeration.nextElement(); if (_checkOverridden(symbolicName, url)) { continue; } // Install a wrapper bundle for this WAR bundle. The wrapper // bundle defers the WAR bundle installation until the WAB // protocol handler is ready. The installed WAR bundle is always // tied its wrapper bundle. When the wrapper bundle is // uninstalled, its wrapped WAR bundle will also be unintalled. Bundle newBundle = _bundleContext.installBundle( url.getPath(), _toWARWrapperBundle(bundle, url)); BundleStartLevel bundleStartLevel = newBundle.adapt( BundleStartLevel.class); bundleStartLevel.setStartLevel( PropsValues.MODULE_FRAMEWORK_DYNAMIC_INSTALL_START_LEVEL); bundles.add(newBundle); } Bundle systemBundle = _bundleContext.getBundle(0); FrameworkWiring frameworkWiring = systemBundle.adapt( FrameworkWiring.class); final DefaultNoticeableFuture<FrameworkEvent> defaultNoticeableFuture = new DefaultNoticeableFuture<>(); frameworkWiring.refreshBundles( null, new FrameworkListener() { @Override public void frameworkEvent(FrameworkEvent frameworkEvent) { defaultNoticeableFuture.set(frameworkEvent); } }); FrameworkEvent frameworkEvent = defaultNoticeableFuture.get(); if (frameworkEvent.getType() != FrameworkEvent.PACKAGES_REFRESHED) { throw frameworkEvent.getThrowable(); } } catch (Throwable t) { _log.error("Rollback bundle installation for " + bundles, t); for (Bundle newBundle : bundles) { try { newBundle.uninstall(); } catch (BundleException be) { _log.error("Unable to uninstall bundle " + newBundle, be); } } return null; } return bundles; } @Override public void modifiedBundle( Bundle bundle, BundleEvent bundleEvent, List<Bundle> bundles) { } @Override public void removedBundle( Bundle bundle, BundleEvent bundleEvent, List<Bundle> bundles) { if (bundle.getState() != Bundle.UNINSTALLED) { return; } String lpkgBundleSymbolicName = bundle.getSymbolicName(); String prefix = lpkgBundleSymbolicName.concat(StringPool.DASH); for (Bundle newBundle : bundles) { try { _uninstallBundle(prefix, newBundle); } catch (BundleException be) { _log.error( "Unable to uninstall " + newBundle + " in response to uninstallation of " + bundle, be); } } } private String _buildImportPackageString(Class<?>... classes) { StringBundler sb = new StringBundler(classes.length * 2); for (Class<?> clazz : classes) { Package pkg = clazz.getPackage(); sb.append(pkg.getName()); sb.append(StringPool.COMMA); } int index = sb.index(); if (index > 0) { sb.setIndex(index - 1); } return sb.toString(); } private boolean _checkOverridden(String symbolicName, URL url) throws BundleException { String path = url.getPath(); Matcher matcher = _pattern.matcher(path); if (matcher.matches()) { path = matcher.group(1) + matcher.group(4); } path = StringUtil.toLowerCase(path); if (_overrideFileNames.contains(path)) { Bundle bundle = _bundleContext.getBundle(url.getPath()); if (bundle != null) { _uninstallBundle(symbolicName.concat(StringPool.DASH), bundle); } if (_log.isInfoEnabled()) { _log.info("Disabled " + symbolicName + ":" + url.getPath()); } return true; } return false; } private boolean _isBundleInstalled(Bundle bundle, URL url) throws IOException { try (InputStream inputStream = url.openStream(); JarInputStream jarInputStream = new JarInputStream(inputStream)) { Manifest manifest = jarInputStream.getManifest(); Attributes attributes = manifest.getMainAttributes(); String symbolicName = attributes.getValue( Constants.BUNDLE_SYMBOLICNAME); Version version = new Version( attributes.getValue(Constants.BUNDLE_VERSION)); String location = url.getPath(); for (Bundle installedBundle : _bundleContext.getBundles()) { if (symbolicName.equals(installedBundle.getSymbolicName()) && version.equals(installedBundle.getVersion()) && !location.equals(installedBundle.getLocation())) { if (_log.isInfoEnabled()) { StringBundler sb = new StringBundler(); sb.append("Skipping installation of "); sb.append(symbolicName); sb.append(" with version "); sb.append(version.toString()); sb.append(" in "); sb.append(bundle.getSymbolicName()); sb.append(" because an identical bundle exists"); _log.info(sb.toString()); } return true; } } } return false; } private String _readServletContextName(URL url) throws IOException { String pathString = url.getPath(); String servletContextName = pathString.substring( pathString.lastIndexOf('/') + 1, pathString.lastIndexOf(".war")); int index = servletContextName.lastIndexOf('-'); if (index >= 0) { servletContextName = servletContextName.substring(0, index); } Path tempFilePath = Files.createTempFile(null, null); try (InputStream inputStream1 = url.openStream()) { Files.copy( inputStream1, tempFilePath, StandardCopyOption.REPLACE_EXISTING); try (ZipFile zipFile = new ZipFile(tempFilePath.toFile()); InputStream inputStream2 = zipFile.getInputStream( new ZipEntry( "WEB-INF/liferay-plugin-package.properties"))) { if (inputStream2 != null) { Properties properties = new Properties(); properties.load(inputStream2); String configuredServletContextName = properties.getProperty("servlet-context-name"); if (configuredServletContextName != null) { servletContextName = configuredServletContextName; } } } } finally { Files.delete(tempFilePath); } return servletContextName; } private InputStream _toWARWrapperBundle(Bundle bundle, URL url) throws IOException { StringBundler sb = new StringBundler(7); sb.append("lpkg://"); sb.append(URLCodec.encodeURL(bundle.getSymbolicName())); sb.append(StringPool.DASH); sb.append(bundle.getVersion()); sb.append(StringPool.SLASH); String servletContextName = _readServletContextName(url); sb.append(servletContextName); sb.append(".war"); String lpkgURL = sb.toString(); // The bundle URL changes after a reboot. To ensure we do not install // the same bundle multiple times over reboots, we must map the ever // changing bundle URL to a fixed LPKG URL. _urls.put(lpkgURL, url); String pathString = url.getPath(); String fileName = pathString.substring( pathString.lastIndexOf('/') + 1, pathString.lastIndexOf(".war")); String version = String.valueOf(bundle.getVersion()); int index = fileName.lastIndexOf('-'); if (index >= 0) { version = fileName.substring(index + 1); } try (UnsyncByteArrayOutputStream unsyncByteArrayOutputStream = new UnsyncByteArrayOutputStream()) { try (JarOutputStream jarOutputStream = new JarOutputStream( unsyncByteArrayOutputStream)) { _writeManifest( bundle, servletContextName, version, lpkgURL, jarOutputStream); _writeClasses( jarOutputStream, WARBundleWrapperBundleActivator.class, URLStreamHandlerServiceServiceTrackerCustomizer.class); } return new UnsyncByteArrayInputStream( unsyncByteArrayOutputStream.unsafeGetByteArray(), 0, unsyncByteArrayOutputStream.size()); } } private void _uninstallBundle(String prefix, Bundle bundle) throws BundleException { String symbolicName = bundle.getSymbolicName(); if (symbolicName.startsWith(prefix) && symbolicName.endsWith("-wrapper")) { String wrappedBundleSymbolicName = symbolicName.substring( prefix.length(), symbolicName.length() - 8); Version version = bundle.getVersion(); for (Bundle curBundle : _bundleContext.getBundles()) { if (wrappedBundleSymbolicName.equals( curBundle.getSymbolicName()) && version.equals(curBundle.getVersion())) { curBundle.uninstall(); } } } bundle.uninstall(); } private void _writeClasses( JarOutputStream jarOutputStream, Class<?>... classes) throws IOException { for (Class<?> clazz : classes) { String className = clazz.getName(); String path = StringUtil.replace( className, CharPool.PERIOD, CharPool.SLASH); String resourcePath = path.concat(".class"); jarOutputStream.putNextEntry(new ZipEntry(resourcePath)); ClassLoader classLoader = clazz.getClassLoader(); StreamUtil.transfer( classLoader.getResourceAsStream(resourcePath), jarOutputStream, false); jarOutputStream.closeEntry(); } } private void _writeManifest( Bundle bundle, String contextName, String version, String lpkgURL, JarOutputStream jarOutputStream) throws IOException { Manifest manifest = new Manifest(); Attributes attributes = manifest.getMainAttributes(); attributes.putValue( Constants.BUNDLE_ACTIVATOR, WARBundleWrapperBundleActivator.class.getName()); attributes.putValue(Constants.BUNDLE_MANIFESTVERSION, "2"); attributes.putValue( Constants.BUNDLE_SYMBOLICNAME, bundle.getSymbolicName() + "-" + contextName + "-wrapper"); attributes.putValue(Constants.BUNDLE_VERSION, version); attributes.putValue( Constants.IMPORT_PACKAGE, _buildImportPackageString( BundleActivator.class, BundleStartLevel.class, ServiceTrackerCustomizer.class, URLConstants.class)); attributes.putValue("Liferay-WAB-Context-Name", contextName); attributes.putValue("Liferay-WAB-LPKG-URL", lpkgURL); attributes.putValue( "Liferay-WAB-Start-Level", String.valueOf( PropsValues.MODULE_FRAMEWORK_DYNAMIC_INSTALL_START_LEVEL)); attributes.putValue("Manifest-Version", "2"); jarOutputStream.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME)); manifest.write(jarOutputStream); jarOutputStream.closeEntry(); } private static final Log _log = LogFactoryUtil.getLog( LPKGBundleTrackerCustomizer.class); private static final Pattern _pattern = Pattern.compile( "/(.*?)(-\\d+\\.\\d+\\.\\d+)(\\..+)?(\\.[jw]ar)"); private final BundleContext _bundleContext; private final Set<String> _overrideFileNames; private final Map<String, URL> _urls; }