/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
* file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
* to you 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.weasis.launcher;
import java.io.File;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import java.util.jar.Pack200.Unpacker;
import java.util.zip.GZIPInputStream;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.service.startlevel.StartLevel;
public class AutoProcessor {
/**
* The property name used for the bundle directory.
**/
public static final String AUTO_DEPLOY_DIR_PROPERTY = "felix.auto.deploy.dir"; //$NON-NLS-1$
/**
* The default name used for the bundle directory.
**/
public static final String AUTO_DEPLOY_DIR_VALUE = "bundle"; //$NON-NLS-1$
/**
* The property name used to specify auto-deploy actions.
**/
public static final String AUTO_DEPLOY_ACTION_PROPERTY = "felix.auto.deploy.action"; //$NON-NLS-1$
/**
* The property name used to specify auto-deploy start level.
**/
public static final String AUTO_DEPLOY_STARTLEVEL_PROPERTY = "felix.auto.deploy.startlevel"; //$NON-NLS-1$
/**
* The name used for the auto-deploy install action.
**/
public static final String AUTO_DEPLOY_INSTALL_VALUE = "install"; //$NON-NLS-1$
/**
* The name used for the auto-deploy start action.
**/
public static final String AUTO_DEPLOY_START_VALUE = "start"; //$NON-NLS-1$
/**
* The name used for the auto-deploy update action.
**/
public static final String AUTO_DEPLOY_UPDATE_VALUE = "update"; //$NON-NLS-1$
/**
* The name used for the auto-deploy uninstall action.
**/
public static final String AUTO_DEPLOY_UNINSTALL_VALUE = "uninstall"; //$NON-NLS-1$
/**
* The property name prefix for the launcher's auto-install property.
**/
public static final String AUTO_INSTALL_PROP = "felix.auto.install"; //$NON-NLS-1$
/**
* The property name prefix for the launcher's auto-start property.
**/
public static final String AUTO_START_PROP = "felix.auto.start"; //$NON-NLS-1$
public static final String PACK200_COMPRESSION = ".pack.gz"; //$NON-NLS-1$
/**
* Used to instigate auto-deploy directory process and auto-install/auto-start configuration property processing
* during.
*
* @param configMap
* Map of configuration properties.
* @param context
* The system bundle context.
* @param weasisLoader
**/
public static void process(Map<String, String> configMap, BundleContext context, WeasisLoader weasisLoader) {
Map<String, String> map = (configMap == null) ? new HashMap<>() : configMap;
processAutoDeploy(map, context, weasisLoader);
processAutoProperties(map, context, weasisLoader);
}
/**
* <p>
* Processes bundles in the auto-deploy directory, performing the specified deploy actions.
* </p>
*/
private static void processAutoDeploy(Map<String, String> configMap, BundleContext context, WeasisLoader weasisLoader) {
// Determine if auto deploy actions to perform.
String action = configMap.get(AUTO_DEPLOY_ACTION_PROPERTY);
action = (action == null) ? "" : action; //$NON-NLS-1$
List<String> actionList = new ArrayList<>();
StringTokenizer st = new StringTokenizer(action, ","); //$NON-NLS-1$
while (st.hasMoreTokens()) {
String s = st.nextToken().trim().toLowerCase();
if (s.equals(AUTO_DEPLOY_INSTALL_VALUE) || s.equals(AUTO_DEPLOY_START_VALUE)
|| s.equals(AUTO_DEPLOY_UPDATE_VALUE) || s.equals(AUTO_DEPLOY_UNINSTALL_VALUE)) {
actionList.add(s);
}
}
// Perform auto-deploy actions.
if (!actionList.isEmpty()) {
// Retrieve the Start Level service, since it will be needed
// to set the start level of the installed bundles.
StartLevel sl = (StartLevel) context
.getService(context.getServiceReference(org.osgi.service.startlevel.StartLevel.class.getName()));
// Get start level for auto-deploy bundles.
int startLevel = sl.getInitialBundleStartLevel();
if (configMap.get(AUTO_DEPLOY_STARTLEVEL_PROPERTY) != null) {
try {
startLevel = Integer.parseInt(configMap.get(AUTO_DEPLOY_STARTLEVEL_PROPERTY).toString());
} catch (NumberFormatException ex) {
// Ignore and keep default level.
}
}
// Get list of already installed bundles as a map.
Map<String, Bundle> installedBundleMap = new HashMap<>();
Bundle[] bundles = context.getBundles();
for (int i = 0; i < bundles.length; i++) {
installedBundleMap.put(bundles[i].getLocation(), bundles[i]);
}
// Get the auto deploy directory.
String autoDir = configMap.get(AUTO_DEPLOY_DIR_PROPERTY);
autoDir = (autoDir == null) ? AUTO_DEPLOY_DIR_VALUE : autoDir;
// Look in the specified bundle directory to create a list
// of all JAR files to install.
File[] files = new File(autoDir).listFiles();
List<File> jarList = new ArrayList<>();
if (files != null) {
Arrays.sort(files);
for (int i = 0; i < files.length; i++) {
if (files[i].getName().endsWith(".jar")) { //$NON-NLS-1$
jarList.add(files[i]);
}
}
}
weasisLoader.setMax(jarList.size());
// Install bundle JAR files and remember the bundle objects.
final List<Bundle> startBundleList = new ArrayList<>();
for (int i = 0; i < jarList.size(); i++) {
// Look up the bundle by location, removing it from
// the map of installed bundles so the remaining bundles
// indicate which bundles may need to be uninstalled.
File jar = jarList.get(i);
Bundle b = installedBundleMap.remove((jar).toURI().toString());
try {
weasisLoader.writeLabel(WeasisLoader.LBL_DOWNLOADING + " " + jar.getName()); //$NON-NLS-1$
// If the bundle is not already installed, then install it
// if the 'install' action is present.
if ((b == null) && actionList.contains(AUTO_DEPLOY_INSTALL_VALUE)) {
b = installBundle(context, jarList.get(i).toURI().toString());
}
// If the bundle is already installed, then update it
// if the 'update' action is present.
else if (b != null && actionList.contains(AUTO_DEPLOY_UPDATE_VALUE)) {
b.update();
}
// If we have found and/or successfully installed a bundle,
// then add it to the list of bundles to potentially start
// and also set its start level accordingly.
if (b != null) {
weasisLoader.setValue(i + 1);
if (!isFragment(b)) {
startBundleList.add(b);
sl.setBundleStartLevel(b, startLevel);
}
}
} catch (Exception ex) {
System.err.println("Auto-deploy install: " + ex //$NON-NLS-1$
+ ((ex.getCause() != null) ? " - " + ex.getCause() : "")); //$NON-NLS-1$ //$NON-NLS-2$
}
}
// Uninstall all bundles not in the auto-deploy directory if
// the 'uninstall' action is present.
if (actionList.contains(AUTO_DEPLOY_UNINSTALL_VALUE)) {
for ( Iterator<Entry<String, Bundle>> it = installedBundleMap.entrySet().iterator(); it.hasNext();) {
Entry<String, Bundle> entry = it.next();
Bundle b = entry.getValue();
if (b.getBundleId() != 0) {
try {
b.uninstall();
} catch (BundleException ex) {
printError(ex, "Auto-deploy uninstall: "); //$NON-NLS-1$
}
}
}
}
// Start all installed and/or updated bundles if the 'start'
// action is present.
if (actionList.contains(AUTO_DEPLOY_START_VALUE)) {
for (int i = 0; i < startBundleList.size(); i++) {
try {
startBundleList.get(i).start();
} catch (BundleException ex) {
printError(ex, "Auto-deploy start: "); //$NON-NLS-1$
}
}
}
}
}
/**
* <p>
* Processes the auto-install and auto-start properties from the specified configuration properties.
* </p>
*/
private static void processAutoProperties(Map<String, String> configMap, BundleContext context, WeasisLoader weasisLoader) {
// Retrieve the Start Level service, since it will be needed
// to set the start level of the installed bundles.
StartLevel sl = (StartLevel) context
.getService(context.getServiceReference(org.osgi.service.startlevel.StartLevel.class.getName()));
// Retrieve all auto-install and auto-start properties and install
// their associated bundles. The auto-install property specifies a
// space-delimited list of bundle URLs to be automatically installed
// into each new profile, while the auto-start property specifies
// bundles to be installed and started. The start level to which the
// bundles are assigned is specified by appending a ".n" to the
// property name, where "n" is the desired start level for the list
// of bundles. If no start level is specified, the default start
// level is assumed.
Map<String, BundleElement> bundleList = new HashMap<>();
Set set = configMap.keySet();
for (Iterator item = set.iterator(); item.hasNext();) {
String key = ((String) item.next()).toLowerCase();
// Ignore all keys that are not an auto property.
if (!key.startsWith(AUTO_INSTALL_PROP) && !key.startsWith(AUTO_START_PROP)) {
continue;
}
// If the auto property does not have a start level,
// then assume it is the default bundle start level, otherwise
// parse the specified start level.
int startLevel = sl.getInitialBundleStartLevel();
try {
startLevel = Integer.parseInt(key.substring(key.lastIndexOf('.') + 1));
} catch (NumberFormatException ex) {
System.err.println("Invalid start level: " + key); //$NON-NLS-1$
}
boolean canBeStarted = key.startsWith(AUTO_START_PROP);
StringTokenizer st = new StringTokenizer(configMap.get(key), "\" ", true); //$NON-NLS-1$
for (String location = nextLocation(st); location != null; location = nextLocation(st)) {
String bundleName = getBundleNameFromLocation(location);
if (!"System Bundle".equals(bundleName)) { //$NON-NLS-1$
BundleElement b = new BundleElement(startLevel, location, canBeStarted);
bundleList.put(bundleName, b);
}
}
}
weasisLoader.setMax(bundleList.size());
final Map<String, Bundle> installedBundleMap = new HashMap<>();
Bundle[] bundles = context.getBundles();
for (int i = 0; i < bundles.length; i++) {
String bundleName = getBundleNameFromLocation(bundles[i].getLocation());
if (bundleName == null) {
// Should never happen
continue;
}
try {
BundleElement b = bundleList.get(bundleName);
// Remove the bundles in cache when they are not in the config.properties list
if (b == null) {
if (!"System Bundle".equals(bundleName)) {//$NON-NLS-1$
bundles[i].uninstall();
System.out.println("Uninstall not used: " + bundleName); //$NON-NLS-1$
}
continue;
}
// Remove snapshot version to install it every time
if (bundles[i].getVersion().getQualifier().endsWith("SNAPSHOT")) { //$NON-NLS-1$
bundles[i].uninstall();
System.out.println("Uninstall SNAPSHOT: " + bundleName); //$NON-NLS-1$
continue;
}
installedBundleMap.put(bundleName, bundles[i]);
} catch (Exception e) {
System.err.println("Cannot remove from OSGI cache: " + bundleName); //$NON-NLS-1$
}
}
int bundleIter = 0;
// Parse and install the bundles associated with the key.
for (Iterator<Entry<String, BundleElement>> iter = bundleList.entrySet().iterator(); iter.hasNext();) {
Entry<String, BundleElement> element = iter.next();
String bundleName = element.getKey();
BundleElement bundle = element.getValue();
if (bundle == null) {
// Should never happen
continue;
}
try {
weasisLoader.writeLabel(WeasisLoader.LBL_DOWNLOADING + " " + bundleName); //$NON-NLS-1$
// Do not download again the same bundle version but with different location or already in installed
// in cache from a previous version of Weasis
Bundle b = installedBundleMap.get(bundleName);
if (b == null) {
b = installBundle(context, bundle.getLocation());
installedBundleMap.put(bundleName, b);
}
sl.setBundleStartLevel(b, bundle.getStartLevel());
loadTranslationBundle(context, b, installedBundleMap);
} catch (Exception ex) {
if (bundleName.contains(System.getProperty("native.library.spec"))) { //$NON-NLS-1$
System.err.println("Cannot install native bundle: " + bundleName); //$NON-NLS-1$
} else {
printError(ex, "Cannot install bundle: " + bundleName); //$NON-NLS-1$
if (ex.getCause() != null) {
ex.printStackTrace();
}
}
} finally {
bundleIter++;
weasisLoader.setValue(bundleIter);
}
}
weasisLoader.writeLabel(Messages.getString("AutoProcessor.start")); //$NON-NLS-1$
// Now loop through the auto-start bundles and start them.
for (Iterator<Entry<String, BundleElement>> iter = bundleList.entrySet().iterator(); iter.hasNext();) {
Entry<String, BundleElement> element = iter.next();
String bundleName = element.getKey();
BundleElement bundle = element.getValue();
if (bundle == null) {
// Should never happen
continue;
}
if (bundle.isCanBeStarted()) {
try {
Bundle b = installedBundleMap.get(bundleName);
if (b == null) {
// Try to reinstall
b = installBundle(context, bundle.getLocation());
}
if (b != null) {
b.start();
}
} catch (Exception ex) {
printError(ex, "Cannot start bundle: " + bundleName); //$NON-NLS-1$
}
}
}
}
private static String getBundleNameFromLocation(String location) {
if (location != null) {
int index = location.lastIndexOf("/"); //$NON-NLS-1$
String name = index >= 0 ? location.substring(index + 1) : location;
index = name.lastIndexOf(".jar"); //$NON-NLS-1$
return index >= 0 ? name.substring(0, index) : name;
}
return null;
}
private static void loadTranslationBundle(BundleContext context, Bundle b,
final Map<String, Bundle> installedBundleMap) {
if (WeasisLauncher.modulesi18n != null) {
if (b != null) {
StringBuilder p = new StringBuilder(b.getSymbolicName());
p.append("-i18n-"); //$NON-NLS-1$
// From 2.0.0, i18n module can be plugged in any version. The date (the qualifier)
// will update the version.
p.append("2.0.0"); //$NON-NLS-1$
p.append(".jar"); //$NON-NLS-1$
String filename = p.toString();
String value = WeasisLauncher.modulesi18n.getProperty(filename);
if (value != null) {
String baseURL = System.getProperty("weasis.i18n"); //$NON-NLS-1$
if (baseURL != null) {
String uri = baseURL + (baseURL.endsWith("/") ? filename : "/" + filename); //$NON-NLS-1$ //$NON-NLS-2$
String bundleName = getBundleNameFromLocation(filename);
try {
Bundle b2 = installedBundleMap.get(bundleName);
if (b2 == null) {
b2 = context.installBundle(uri, FileUtil.getAdaptedConnection(new URI(uri).toURL()).getInputStream());
installedBundleMap.put(bundleName, b);
}
if (b2 != null && !value.equals(b2.getVersion().getQualifier())) {
if (b2.getLocation().startsWith(baseURL)) {
b2.update();
} else {
// Handle same bundle version with different location
try {
b2.uninstall();
context.installBundle(uri, FileUtil.getAdaptedConnection(new URI(uri).toURL()).getInputStream());
installedBundleMap.put(bundleName, b);
} catch (Exception exc) {
System.err.println("Cannot install translation pack: " + uri); //$NON-NLS-1$
}
}
}
} catch (Exception e) {
System.err.println("Cannot install translation pack: " + uri); //$NON-NLS-1$
}
}
}
}
}
}
private static void printError(Exception ex, String prefix) {
System.err.println(prefix + " (" + ex //$NON-NLS-1$
+ ((ex.getCause() != null) ? " - " + ex.getCause() : "") + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
private static String nextLocation(StringTokenizer st) {
String retVal = null;
if (st.countTokens() > 0) {
String tokenList = "\" "; //$NON-NLS-1$
StringBuilder tokBuf = new StringBuilder(10);
String tok = null;
boolean inQuote = false;
boolean tokStarted = false;
boolean exit = false;
while ((st.hasMoreTokens()) && (!exit)) {
tok = st.nextToken(tokenList);
if (tok.equals("\"")) { //$NON-NLS-1$
inQuote = !inQuote;
if (inQuote) {
tokenList = "\""; //$NON-NLS-1$
} else {
tokenList = "\" "; //$NON-NLS-1$
}
} else if (tok.equals(" ")) { //$NON-NLS-1$
if (tokStarted) {
retVal = tokBuf.toString();
tokStarted = false;
tokBuf = new StringBuilder(10);
exit = true;
}
} else {
tokStarted = true;
tokBuf.append(tok.trim());
}
}
// Handle case where end of token stream and
// still got data
if ((!exit) && (tokStarted)) {
retVal = tokBuf.toString();
}
}
return retVal;
}
private static boolean isFragment(Bundle bundle) {
return bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null;
}
private static Bundle installBundle(BundleContext context, String location) throws Exception {
boolean pack = location.endsWith(PACK200_COMPRESSION);
if (pack) {
// Remove the pack classifier from the location path
location = location.substring(0, location.length() - 8);
pack = context.getBundle(location) == null;
}
if (pack) {
final URL url = new URL(location + PACK200_COMPRESSION);
final PipedInputStream in = new PipedInputStream();
final PipedOutputStream out = new PipedOutputStream(in);
new Thread(() -> {
JarOutputStream jarStream = null;
try {
InputStream is = FileUtil.getAdaptedConnection(url).getInputStream();
Unpacker unpacker = Pack200.newUnpacker();
jarStream = new JarOutputStream(out);
unpacker.unpack(new GZIPInputStream(is), jarStream);
} catch (Exception e1) {
System.err.println("Cannot install pack bundle: " + url); //$NON-NLS-1$
e1.printStackTrace();
} finally {
FileUtil.safeClose(jarStream);
}
}).start();
return context.installBundle(location, in);
}
return context.installBundle(location, FileUtil.getAdaptedConnection(new URI(location).toURL()).getInputStream());
}
static class BundleElement {
private final int startLevel;
private final String location;
private final boolean canBeStarted;
public BundleElement(int startLevel, String location, boolean canBeStarted) {
this.startLevel = startLevel;
this.location = location;
this.canBeStarted = canBeStarted;
}
public int getStartLevel() {
return startLevel;
}
public String getLocation() {
return location;
}
public boolean isCanBeStarted() {
return canBeStarted;
}
}
}