/*
* Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.wso2.carbon.launcher.extensions;
import org.wso2.carbon.launcher.Constants;
import org.wso2.carbon.launcher.extensions.model.BundleInfo;
import org.wso2.carbon.launcher.extensions.model.BundleInstallStatus;
import org.wso2.carbon.launcher.extensions.model.BundleLocation;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* This class defines the utility functions used when implementing the OSGi-lib deployment capability.
*
* @since 5.1.0
*/
public class OSGiLibBundleDeployerUtils {
private static final Logger logger = Logger.getLogger(OSGiLibBundleDeployerUtils.class.getName());
/**
* Updates the bundles.info file of the specified Carbon Runtime based on the OSGi bundles deployed in the
* {@value org.wso2.carbon.launcher.Constants#OSGI_LIB} directory. The OSGi bundle information in the
* bundles.info file in a Carbon runtime is used to install and start the bundles at the server startup for the
* particular profile.
* <p>
* The mechanism used in updating the bundles.info file is as follows:
* 1. The existing OSGi bundle information within the specified Carbon Runtime are read.
* 2. The new OSGi bundle information are compared with the above mentioned existing OSGi bundle information
* and a map of new, installable OSGi bundle information and the OSGi bundle information removable from existing
* bundle information are obtained.
* 3. If the number of both installable and removable OSGi bundles are zero, then no Profile update is made. Else,
* the Profile is updated.
* 4. During Profile update, the existing, non OSGi-lib bundle information are merged with the new OSGi bundle
* information.
*
* @param carbonHome the {@link String} representation of carbon.home
* @param carbonProfile the name of the Carbon Runtime of which the bundles.info is to be updated
* @param newBundlesInfo the new OSGi bundle information
* @throws IOException if an I/O error occurs
*/
public static synchronized void updateOSGiLib(String carbonHome, String carbonProfile,
List<BundleInfo> newBundlesInfo) throws IOException {
// validates the arguments provided
if ((carbonHome == null) || (carbonHome.isEmpty())) {
throw new IllegalArgumentException("Carbon home specified is invalid");
}
if ((carbonProfile == null) || (carbonProfile.isEmpty())) {
throw new IllegalArgumentException("Carbon Runtime specified is invalid");
}
if (newBundlesInfo == null) {
throw new IllegalArgumentException("No new OSGi bundle information specified, for updating the " +
"Carbon Runtime: " + carbonProfile);
}
Path bundlesInfoFile = Paths.get(carbonHome, Constants.PROFILE_REPOSITORY, carbonProfile, "configuration",
"org.eclipse.equinox.simpleconfigurator", Constants.BUNDLES_INFO);
// retrieves the OSGi bundle information defined in the existing bundles.info file
Map<BundleLocation, List<BundleInfo>> existingBundlesInfo = Files.readAllLines(bundlesInfoFile)
.stream()
.filter(line -> !line.startsWith("#"))
.map(BundleInfo::getInstance)
.collect(Collectors.groupingBy(BundleInfo::isFromOSGiLib));
Map<BundleInstallStatus, List<BundleInfo>> updatableBundles =
getUpdatableBundles(newBundlesInfo, existingBundlesInfo.get(BundleLocation.OSGI_LIB_BUNDLE));
if ((updatableBundles.get(BundleInstallStatus.TO_BE_INSTALLED).size() > 0) || (
updatableBundles.get(BundleInstallStatus.TO_BE_REMOVED).size() > 0)) {
logger.log(Level.FINE, getBundleInstallationSummary(updatableBundles));
List<BundleInfo> effectiveNewBundleInfo = mergeOSGiLibWithExistingBundlesInfo(newBundlesInfo,
existingBundlesInfo);
updateBundlesInfoFile(effectiveNewBundleInfo, bundlesInfoFile);
logger.log(Level.INFO,
"Successfully updated the OSGi bundle information of Carbon Runtime: " + carbonProfile);
} else {
logger.log(Level.FINE, String.format("No changes detected in the %s directory in comparison with the "
+ "profile, skipped the OSGi bundle information update for Carbon Runtime: %s", Constants.OSGI_LIB,
carbonProfile));
}
}
/**
* Scans through the specified directory and constructs corresponding {@code BundleInfo} instances.
* <p>
* No duplicated OSGi bundles are returned.
*
* @param sourceDirectory the source folder in which the OSGi bundles reside
* @return the constructed {@link BundleInfo} instances list
* @throws IOException if an I/O error occurs or if the {@code sourceDirectory} is invalid
*/
public static List<BundleInfo> getBundlesInfo(Path sourceDirectory) throws IOException {
if ((sourceDirectory == null) || (!Files.exists(sourceDirectory))) {
throw new IOException("Invalid OSGi bundle source directory. The specified path may not exist or " +
"user may not have required file permissions for the specified path: " + sourceDirectory);
}
return Files.list(sourceDirectory)
.parallel()
.map(child -> {
BundleInfo bundleInfo = null;
try {
bundleInfo = getBundleInfo(child).orElse(null);
} catch (IOException e) {
logger.log(Level.WARNING, "Error when loading the OSGi bundle information from " + child, e);
}
return bundleInfo;
})
.distinct()
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
/**
* Constructs a {@code BundleInfo} instance out of the OSGi bundle file path specified.
* <p>
* If the specified file path does not satisfy the requirements of an OSGi bundle, no {@code BundleInfo} instance
* will be created.
*
* @param bundlePath path to the OSGi bundle from which the {@link BundleInfo} is to be generated
* @return a {@link BundleInfo} instance
* @throws IOException if an I/O error occurs or if an invalid {@code bundlePath} is found
*/
private static Optional<BundleInfo> getBundleInfo(Path bundlePath) throws IOException {
if ((bundlePath == null) || (!Files.exists(bundlePath))) {
throw new IOException("Invalid OSGi bundle path. The specified path may not exist or " +
"user may not have required file permissions for the specified path");
}
Path bundleFileName = bundlePath.getFileName();
if (bundleFileName == null) {
throw new IOException("Specified OSGi bundle file name is null: " + bundlePath);
}
String fileName = bundleFileName.toString();
if (!fileName.endsWith(".jar")) {
return Optional.empty();
}
try (JarFile jarFile = new JarFile(bundlePath.toString())) {
Manifest manifest = jarFile.getManifest();
if ((manifest == null) || (manifest.getMainAttributes() == null)) {
throw new IOException("Invalid OSGi bundle found in the " + Constants.OSGI_LIB + " folder");
}
String bundleSymbolicName = manifest.getMainAttributes().getValue("Bundle-SymbolicName");
String bundleVersion = manifest.getMainAttributes().getValue("Bundle-Version");
if (bundleSymbolicName == null || bundleVersion == null) {
throw new IOException("Required bundle manifest headers do not exist");
}
logger.log(Level.FINE,
"Loading information from OSGi bundle: " + bundleSymbolicName + ":" + bundleVersion + "...");
if (bundleSymbolicName.contains(";")) {
bundleSymbolicName = bundleSymbolicName.split(";")[0];
}
// checks whether this bundle is a fragment or not
boolean isFragment = (manifest.getMainAttributes().getValue("Fragment-Host") != null);
int defaultBundleStartLevel = 4;
BundleInfo generated = new BundleInfo(bundleSymbolicName, bundleVersion,
"../../" + Constants.OSGI_LIB + "/" + fileName, defaultBundleStartLevel, isFragment);
logger.log(Level.FINE,
"Successfully loaded information from OSGi bundle: " + bundleSymbolicName + ":" + bundleVersion);
return Optional.of(generated);
}
}
/**
* Returns the OSGi bundles information which are to be either added or removed from the existing set of bundle
* information, in order to bring the existing bundle information up-to-date with new bundle information.
*
* @param newBundlesInfo the new OSGi bundle information
* @param existingBundleInfo the existing OSGi bundle information
* @return two groups of OSGi bundle information, one group containing newly installable OSGi bundle information
* and the other group contains a list of OSGi bundle information to be removed
*/
private static Map<BundleInstallStatus, List<BundleInfo>> getUpdatableBundles(List<BundleInfo> newBundlesInfo,
List<BundleInfo> existingBundleInfo) {
Map<BundleInstallStatus, List<BundleInfo>> updatableBundles = new HashMap<>();
// retrieves the newly installable OSGi bundle information
List<BundleInfo> bundlesToBeInstalled = Optional.ofNullable(newBundlesInfo)
.orElse(new ArrayList<>())
.stream()
.filter(bundleInfo -> !Optional.ofNullable(existingBundleInfo)
.orElse(new ArrayList<>()).contains(bundleInfo))
.collect(Collectors.toList());
// retrieves the OSGi bundle information to be uninstalled
List<BundleInfo> bundlesToBeRemoved = Optional.ofNullable(existingBundleInfo)
.orElse(new ArrayList<>())
.stream()
.filter(bundleInfo -> !Optional.ofNullable(newBundlesInfo)
.orElse(new ArrayList<>()).contains(bundleInfo))
.collect(Collectors.toList());
// sets the list for newly installable OSGi bundles
updatableBundles.put(BundleInstallStatus.TO_BE_INSTALLED, bundlesToBeInstalled);
// sets the list for OSGi bundles to be uninstalled
updatableBundles.put(BundleInstallStatus.TO_BE_REMOVED, bundlesToBeRemoved);
return updatableBundles;
}
/**
* Merges the existing OSGi bundle information with the newly retrieved OSGi bundle information.
*
* @param newBundlesInfo the new OSGi bundle information to be added
* @param existingBundleInfo the existing OSGi bundle information
* @return merged result of the existing OSGi bundle information with the newly retrieved OSGi bundle information
*/
private static List<BundleInfo> mergeOSGiLibWithExistingBundlesInfo(List<BundleInfo> newBundlesInfo,
Map<BundleLocation, List<BundleInfo>> existingBundleInfo) {
List<BundleInfo> effectiveBundlesInfo = existingBundleInfo.get(BundleLocation.NON_OSGI_LIB_BUNDLE);
if (effectiveBundlesInfo != null) {
effectiveBundlesInfo.addAll(newBundlesInfo);
} else {
effectiveBundlesInfo = newBundlesInfo;
}
return effectiveBundlesInfo
.stream()
.distinct()
.collect(Collectors.toList());
}
/**
* Updates the specified bundles.info file with the specified OSGi bundle information.
*
* @param info the OSGi bundle information instances
* @param bundlesInfoFilePath the bundles.info file path, to be updated
* @throws IOException if an I/O error occurs
*/
private static void updateBundlesInfoFile(List<BundleInfo> info, Path bundlesInfoFilePath) throws IOException {
if ((bundlesInfoFilePath != null) && (Files.exists(bundlesInfoFilePath))) {
if (info != null) {
List<String> bundleInfoLines = new ArrayList<>();
info
.stream()
.forEach(information -> bundleInfoLines.add(information.toString()));
Path parent = bundlesInfoFilePath.getParent();
if (parent != null) {
Path newBundlesInfoFile = Paths.get(parent.toString(), "new.info");
Files.write(newBundlesInfoFile, bundleInfoLines);
Files.deleteIfExists(bundlesInfoFilePath);
Files.move(newBundlesInfoFile, newBundlesInfoFile.resolveSibling("bundles.info"));
}
}
} else {
throw new IOException("Invalid file path. The specified path may not exist or " +
"user may not have required file permissions for the specified path: " + bundlesInfoFilePath);
}
}
/**
* Returns a list of WSO2 Carbon Runtime names.
*
* @param carbonHome the WSO2 Carbon home
* @return a list of WSO2 Carbon Runtime names
* @throws IOException if an I/O error occurs
*/
public static List<String> getCarbonProfiles(String carbonHome) throws IOException {
Path carbonProfilesHome = Paths.get(carbonHome, Constants.PROFILE_REPOSITORY);
Path osgiRepoPath = Paths.get(carbonHome, Constants.OSGI_REPOSITORY);
Stream<Path> profiles = Files.list(carbonProfilesHome);
List<String> profileNames = new ArrayList<>();
profiles
.parallel()
.filter(profile -> !osgiRepoPath.equals(profile))
.forEach(profile -> Optional.ofNullable(profile.getFileName())
.ifPresent(name -> profileNames.add(name.toString())));
if (profileNames.size() == 0) {
throw new IOException("No profiles found in " + carbonHome + "/" + Constants.PROFILE_REPOSITORY);
}
return profileNames;
}
/**
* Returns a message depicting the OSGi bundle installation/removal summary information.
*
* @param updatableBundleInfo the installable/removable OSGi bundle information
* @return a message depicting the OSGi bundle installation/removal summary information
*/
private static String getBundleInstallationSummary(Map<BundleInstallStatus, List<BundleInfo>> updatableBundleInfo) {
StringBuilder message = new StringBuilder("\nInstallation/Removal Summary of OSGi bundles from OSGi-lib\n");
Optional.ofNullable(updatableBundleInfo.get(BundleInstallStatus.TO_BE_INSTALLED))
.ifPresent(list -> {
if (list.size() > 0) {
message.append("\nOSGi bundles to be newly installed:\n");
list
.stream()
.forEach(bundleInfo -> message.append("Symbolic-name: ")
.append(bundleInfo.getBundleSymbolicName()).append(" --> ").append("Version: ")
.append(bundleInfo.getBundleVersion()).append("\n"));
}
});
Optional.ofNullable(updatableBundleInfo.get(BundleInstallStatus.TO_BE_REMOVED))
.ifPresent(list -> {
if (list.size() > 0) {
message.append("\nOSGi bundles to be removed:\n");
list
.stream()
.forEach(bundleInfo -> message.append("Symbolic-name: ")
.append(bundleInfo.getBundleSymbolicName()).append(" --> ").append("Version: ")
.append(bundleInfo.getBundleVersion()).append("\n"));
}
});
return message.toString();
}
}