/** * 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.module.framework.ThrowableCollector; import com.liferay.portal.kernel.util.GetterUtil; import com.liferay.portal.kernel.util.HashMapDictionary; import com.liferay.portal.kernel.util.StreamUtil; import com.liferay.portal.kernel.util.StringUtil; import com.liferay.portal.lpkg.deployer.LPKGDeployer; import com.liferay.portal.lpkg.deployer.LPKGVerifier; import com.liferay.portal.lpkg.deployer.LPKGVerifyException; import com.liferay.portal.util.PropsValues; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collections; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.jar.Attributes; import java.util.jar.JarFile; 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.BundleContext; import org.osgi.framework.BundleException; import org.osgi.framework.Constants; import org.osgi.framework.FrameworkEvent; import org.osgi.framework.FrameworkListener; import org.osgi.framework.startlevel.BundleStartLevel; import org.osgi.framework.wiring.FrameworkWiring; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; import org.osgi.service.url.URLConstants; import org.osgi.service.url.URLStreamHandlerService; import org.osgi.util.tracker.BundleTracker; /** * @author Shuyang Zhou */ @Component(immediate = true, service = LPKGDeployer.class) public class DefaultLPKGDeployer implements LPKGDeployer { @Activate public void activate(BundleContext bundleContext) { try { _activate(bundleContext); } catch (Throwable t) { _throwableCollector.collect(t); } } @Override public List<Bundle> deploy(BundleContext bundleContext, File lpkgFile) throws IOException { Path lpkgFilePath = lpkgFile.toPath(); if (!lpkgFilePath.startsWith(_deploymentDirPath)) { throw new LPKGVerifyException( "Unable to deploy " + lpkgFile + " from outside the deployment directory " + _deploymentDirPath); } List<Bundle> oldBundles = _lpkgVerifier.verify(lpkgFile); for (Bundle bundle : oldBundles) { try { bundle.uninstall(); if (_log.isInfoEnabled()) { _log.info( "Uninstalled older LPKG bundle " + bundle + " in order to install " + lpkgFile); } String location = bundle.getLocation(); if (!location.equals(lpkgFile.getCanonicalPath()) && Files.deleteIfExists(Paths.get(bundle.getLocation())) && _log.isInfoEnabled()) { _log.info( "Removed old LPKG bundle " + bundle.getLocation()); } } catch (BundleException be) { _log.error( "Unable to uninstall " + bundle + " in order to install " + lpkgFile, be); } } try { String location = lpkgFile.getCanonicalPath(); Bundle lpkgBundle = bundleContext.getBundle(location); if (lpkgBundle != null) { return Collections.emptyList(); } List<Bundle> bundles = new ArrayList<>(); lpkgBundle = bundleContext.installBundle( location, toBundle(lpkgFile)); BundleStartLevel bundleStartLevel = lpkgBundle.adapt( BundleStartLevel.class); bundleStartLevel.setStartLevel( PropsValues.MODULE_FRAMEWORK_DYNAMIC_INSTALL_START_LEVEL); bundles.add(lpkgBundle); List<Bundle> newBundles = _lpkgBundleTracker.getObject(lpkgBundle); if (newBundles != null) { bundles.addAll(newBundles); } if (LPKGIndexValidatorThreadLocal.isEnabled()) { _lpkgIndexValidator.updateIntegrityProperties(); } if (!oldBundles.isEmpty()) { _refreshRemovalPendingBundles(bundleContext, lpkgBundle); } return bundles; } catch (Exception e) { throw new IOException(e); } } @Override public Map<Bundle, List<Bundle>> getDeployedLPKGBundles() { return _lpkgBundleTracker.getTracked(); } @Override public InputStream toBundle(File lpkgFile) throws IOException { try (UnsyncByteArrayOutputStream unsyncByteArrayOutputStream = new UnsyncByteArrayOutputStream()) { try (ZipFile zipFile = new ZipFile(lpkgFile); JarOutputStream jarOutputStream = new JarOutputStream( unsyncByteArrayOutputStream)) { _writeManifest(zipFile, jarOutputStream); Enumeration<? extends ZipEntry> zipEntries = zipFile.entries(); while (zipEntries.hasMoreElements()) { ZipEntry zipEntry = zipEntries.nextElement(); jarOutputStream.putNextEntry( new ZipEntry(zipEntry.getName())); StreamUtil.transfer( zipFile.getInputStream(zipEntry), jarOutputStream, false); jarOutputStream.closeEntry(); } } return new UnsyncByteArrayInputStream( unsyncByteArrayOutputStream.unsafeGetByteArray(), 0, unsyncByteArrayOutputStream.size()); } } @Deactivate protected void deactivate() { _lpkgBundleTracker.close(); } private void _activate(final BundleContext bundleContext) throws Exception { Dictionary<String, Object> properties = new HashMapDictionary<>(); properties.put( URLConstants.URL_HANDLER_PROTOCOL, new String[] {"lpkg"}); bundleContext.registerService( URLStreamHandlerService.class.getName(), new LPKGURLStreamHandlerService(_urls), properties); _deploymentDirPath = _getDeploymentDirPath(bundleContext); Path overrideDirPath = _deploymentDirPath.resolve("override"); List<File> jarFiles = _scanFiles(overrideDirPath, ".jar", true); _uninstallOrphanOverridingJars(bundleContext, jarFiles); List<File> warFiles = _scanFiles(overrideDirPath, ".war", true); _uninstallOrphanOverridingWars(bundleContext, warFiles); _lpkgBundleTracker = new BundleTracker<>( bundleContext, ~Bundle.UNINSTALLED, new LPKGBundleTrackerCustomizer( bundleContext, _urls, _toFileNames(jarFiles, warFiles))); _lpkgBundleTracker.open(); List<File> lpkgFiles = _scanFiles(_deploymentDirPath, ".lpkg", false); _lpkgIndexValidator.setLPKGDeployer(this); _lpkgIndexValidator.setJarFiles(jarFiles); boolean updateIntegrityProperties = _lpkgIndexValidator.validate( lpkgFiles); boolean enabled = LPKGIndexValidatorThreadLocal.isEnabled(); LPKGIndexValidatorThreadLocal.setEnabled(false); try { _instalLPKGs(bundleContext, lpkgFiles); _installOverrideJars(bundleContext, jarFiles); _installOverrideWars(bundleContext, warFiles); if (updateIntegrityProperties) { _lpkgIndexValidator.updateIntegrityProperties(); } } finally { LPKGIndexValidatorThreadLocal.setEnabled(enabled); } } private Path _getDeploymentDirPath(BundleContext bundleContext) throws IOException { String deploymentDir = GetterUtil.getString( bundleContext.getProperty("lpkg.deployer.dir"), PropsValues.MODULE_FRAMEWORK_MARKETPLACE_DIR); Path deploymentDirPath = Paths.get(deploymentDir); Files.createDirectories(deploymentDirPath); return deploymentDirPath; } private void _installOverrideJars( BundleContext bundleContext, List<File> jarFiles) throws Exception { for (File jarFile : jarFiles) { String location = _LPKG_OVERRIDE_PREFIX.concat( jarFile.getCanonicalPath()); Bundle jarBundle = bundleContext.getBundle(location); if (jarBundle != null) { if (_log.isInfoEnabled()) { _log.info("Using overriding JAR bundle " + location); } continue; } jarBundle = bundleContext.installBundle( location, new FileInputStream(jarFile)); BundleStartLevel bundleStartLevel = jarBundle.adapt( BundleStartLevel.class); bundleStartLevel.setStartLevel( PropsValues.MODULE_FRAMEWORK_DYNAMIC_INSTALL_START_LEVEL); _startBundle(jarBundle); if (_log.isInfoEnabled()) { _log.info("Installed override JAR bundle " + location); } } } private void _installOverrideWars( BundleContext bundleContext, List<File> warFiles) throws Exception { Properties properties = _loadOverrideWarsProperties(bundleContext); Path osgiWarDir = Paths.get(PropsValues.MODULE_FRAMEWORK_WAR_DIR); boolean modified = false; for (File warFile : warFiles) { String sourceLocation = warFile.getCanonicalPath(); String targetLocation = properties.getProperty(sourceLocation); if (targetLocation != null) { if (_log.isInfoEnabled()) { _log.info("Using overridding WAR bundle " + targetLocation); } continue; } Path sourceWarPath = warFile.toPath(); Path targetWarPath = osgiWarDir.resolve( sourceWarPath.getFileName()); Files.copy( sourceWarPath, targetWarPath, StandardCopyOption.REPLACE_EXISTING); targetLocation = targetWarPath.toString(); properties.put(sourceLocation, targetLocation); if (_log.isInfoEnabled()) { _log.info("Deployed override WAR bundle to " + targetLocation); } modified = true; } if (modified) { _saveOverrideWarsProperties(bundleContext, properties); } } private void _instalLPKGs( BundleContext bundleContext, List<File> lpkgFiles) { for (File lpkgFile : lpkgFiles) { try { List<Bundle> bundles = deploy(bundleContext, lpkgFile); for (Bundle bundle : bundles) { _startBundle(bundle); } } catch (Exception e) { _log.error("Unable to deploy LPKG file " + lpkgFile, e); } } } private Properties _loadOverrideWarsProperties(BundleContext bundleContext) throws IOException { Bundle bundle = bundleContext.getBundle(0); BundleContext systemBundleContext = bundle.getBundleContext(); File overrideWarsPropertiesFile = systemBundleContext.getDataFile( "override-wars.properties"); Properties overrideWarsProperties = new Properties(); if (overrideWarsPropertiesFile.exists()) { try (InputStream inputStream = new FileInputStream( overrideWarsPropertiesFile)) { overrideWarsProperties.load(inputStream); } } return overrideWarsProperties; } /** * @see FrameworkWiring#getRemovalPendingBundles */ private void _refreshRemovalPendingBundles( BundleContext bundleContext, Bundle bundle) throws Exception { Bundle systemBundle = bundleContext.getBundle(0); FrameworkWiring frameworkWiring = systemBundle.adapt( FrameworkWiring.class); final DefaultNoticeableFuture<FrameworkEvent> defaultNoticeableFuture = new DefaultNoticeableFuture<>(); if (_log.isInfoEnabled()) { _log.info( "Start refreshing references to point to the new bundle " + bundle); } frameworkWiring.refreshBundles( null, new FrameworkListener() { @Override public void frameworkEvent(FrameworkEvent frameworkEvent) { defaultNoticeableFuture.set(frameworkEvent); } }); FrameworkEvent frameworkEvent = defaultNoticeableFuture.get(); if (frameworkEvent.getType() == FrameworkEvent.PACKAGES_REFRESHED) { if (_log.isInfoEnabled()) { _log.info( "Finished refreshing references to point to the new " + "bundle " + bundle); } } else { throw new Exception( "Unable to refresh references to the new bundle " + bundle + " because of framework event " + frameworkEvent, frameworkEvent.getThrowable()); } } private void _saveOverrideWarsProperties( BundleContext bundleContext, Properties properties) throws IOException { Bundle bundle = bundleContext.getBundle(0); BundleContext systemBundleContext = bundle.getBundleContext(); File overrideWarsPropertiesFile = systemBundleContext.getDataFile( "override-wars.properties"); try (OutputStream outputStream = new FileOutputStream( overrideWarsPropertiesFile)) { properties.store(outputStream, null); } } private List<File> _scanFiles( Path dirPath, String extension, boolean checkFileName) throws IOException { if (Files.notExists(dirPath)) { return Collections.emptyList(); } List<File> files = new ArrayList<>(); try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream( dirPath)) { for (Path path : directoryStream) { String pathName = StringUtil.toLowerCase( String.valueOf(path.getFileName())); if (!pathName.endsWith(extension)) { continue; } if (checkFileName) { Matcher matcher = _pattern.matcher(pathName); if (matcher.matches()) { if (_log.isWarnEnabled()) { _log.warn( "Override file " + path + " has an invalid name and will be ignored"); } continue; } } files.add(path.toFile()); } } return files; } private void _startBundle(Bundle bundle) { Dictionary<String, String> headers = bundle.getHeaders(); String fragmentHost = headers.get(Constants.FRAGMENT_HOST); if (fragmentHost != null) { return; } try { bundle.start(); } catch (BundleException be) { _log.error("Unable to start " + bundle, be); } } private Set<String> _toFileNames(List<File> jarFiles, List<File> warFiles) { Set<String> fileNames = new HashSet<>(); for (File file : jarFiles) { fileNames.add(StringUtil.toLowerCase(file.getName())); } for (File file : warFiles) { fileNames.add(StringUtil.toLowerCase(file.getName())); } return fileNames; } private void _uninstallOrphanOverridingJars( BundleContext bundleContext, List<File> jarFiles) throws BundleException { for (Bundle bundle : bundleContext.getBundles()) { String location = bundle.getLocation(); if (!location.startsWith(_LPKG_OVERRIDE_PREFIX)) { continue; } String filePath = location.substring( _LPKG_OVERRIDE_PREFIX.length()); if (jarFiles.contains(new File(filePath))) { continue; } bundle.uninstall(); if (_log.isInfoEnabled()) { _log.info( "Uninstalled orphan overriding JAR bundle " + location); } } } private void _uninstallOrphanOverridingWars( BundleContext bundleContext, List<File> warFiles) throws IOException { Properties properties = _loadOverrideWarsProperties(bundleContext); Set<Entry<Object, Object>> entrySet = properties.entrySet(); Iterator<Entry<Object, Object>> iterator = entrySet.iterator(); boolean modified = false; while (iterator.hasNext()) { Entry<Object, Object> entry = iterator.next(); if (warFiles.contains(new File((String)entry.getKey()))) { continue; } iterator.remove(); Files.deleteIfExists(Paths.get((String)entry.getValue())); modified = true; } if (modified) { _saveOverrideWarsProperties(bundleContext, properties); } } private void _writeManifest( ZipFile zipFile, JarOutputStream jarOutputStream) throws IOException { Manifest manifest = new Manifest(); Attributes attributes = manifest.getMainAttributes(); Properties properties = new Properties(); properties.load( zipFile.getInputStream( zipFile.getEntry("liferay-marketplace.properties"))); attributes.putValue( Constants.BUNDLE_DESCRIPTION, properties.getProperty("description")); attributes.putValue(Constants.BUNDLE_MANIFESTVERSION, "2"); attributes.putValue( Constants.BUNDLE_SYMBOLICNAME, properties.getProperty("title")); attributes.putValue( Constants.BUNDLE_VERSION, properties.getProperty("version")); attributes.putValue("Liferay-Releng-Bundle-Type", "lpkg"); attributes.putValue("Manifest-Version", "2"); jarOutputStream.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME)); manifest.write(jarOutputStream); jarOutputStream.closeEntry(); } private static final String _LPKG_OVERRIDE_PREFIX = "LPKG-Override::"; private static final Log _log = LogFactoryUtil.getLog( DefaultLPKGDeployer.class); private static final Pattern _pattern = Pattern.compile( "/?(.*?)(-\\d+\\.\\d+\\.\\d+)(\\..+)?(\\.[jw]ar)"); private Path _deploymentDirPath; private BundleTracker<List<Bundle>> _lpkgBundleTracker; @Reference private LPKGIndexValidator _lpkgIndexValidator; @Reference private LPKGVerifier _lpkgVerifier; @Reference(target = "(throwable.collector=initial.bundles)") private ThrowableCollector _throwableCollector; private final Map<String, URL> _urls = new ConcurrentHashMap<>(); }