/*******************************************************************************
* Copyright (c) 2004, 2010 BREDEX GmbH.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BREDEX GmbH - initial API and implementation and/or initial documentation
*******************************************************************************/
package org.eclipse.jubula.autagent.commands;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.apache.commons.lang.StringUtils;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.adaptor.EclipseStarter;
import org.eclipse.jubula.autagent.AutStarter;
import org.eclipse.jubula.autagent.monitoring.MonitoringDataStore;
import org.eclipse.jubula.autagent.monitoring.MonitoringUtil;
import org.eclipse.jubula.communication.internal.message.StartAUTServerStateMessage;
import org.eclipse.jubula.tools.internal.constants.AUTStartResponse;
import org.eclipse.jubula.tools.internal.constants.AutConfigConstants;
import org.eclipse.jubula.tools.internal.constants.AutEnvironmentConstants;
import org.eclipse.jubula.tools.internal.constants.StringConstants;
import org.eclipse.jubula.tools.internal.registration.AutIdentifier;
import org.eclipse.jubula.tools.internal.utils.EnvironmentUtils;
import org.eclipse.jubula.tools.internal.utils.ZipUtil;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author BREDEX GmbH
* @created Jul 10, 2007
*
*/
public abstract class AbstractStartToolkitAut implements IStartAut {
/** the logger */
private static Logger log =
LoggerFactory.getLogger(AbstractStartToolkitAut.class);
/**
* the name of the bundle JAR Manifest Attribute that indicates that the
* bundle is a source-bundle
*/
private static final String SOURCE_BUNDLE_MANIFEST_ATTR =
"Eclipse-SourceBundle"; //$NON-NLS-1$
/**
* the message to send back if the command for starting the AUTServer could
* not created
*/
private StartAUTServerStateMessage m_errorMessage;
/** true if executable file and -javaagent are set */
private boolean m_isAgentSet = false;
/**
*
* {@inheritDoc}
*/
public StartAUTServerStateMessage startAut(Map<String, String> parameters)
throws IOException {
StartAUTServerStateMessage envCheckMsg = validateEnvironment();
AutIdentifier autId = new AutIdentifier(parameters.get(
AutConfigConstants.AUT_ID));
if (envCheckMsg == null) {
if (!MonitoringUtil.checkForDuplicateAutID(String.valueOf(autId))) {
MonitoringDataStore cm = MonitoringDataStore.getInstance();
cm.putConfigMap(autId.getID(), parameters);
}
File workingDir = getWorkingDir(parameters);
String java = createBaseCmd(parameters);
String[] cmdArray = createCmdArray(java, parameters);
String[] envArray = createEnvArray(parameters, m_isAgentSet);
// if no environment variables set
if ((envArray == null) && log.isInfoEnabled()) {
log.info("envArray: NULL"); //$NON-NLS-1$
}
if (log.isInfoEnabled()) {
StringBuffer logMessage = new StringBuffer();
for (int i = 0; i < cmdArray.length; i++) {
logMessage.append(cmdArray[i]
+ IStartAut.WHITESPACE_DELIMITER);
}
log.info("starting AUT with command: " //$NON-NLS-1$
+ logMessage.toString());
}
StartAUTServerStateMessage stateMessage = executeCommand(cmdArray,
envArray, workingDir, autId);
stateMessage.setAutId(autId);
return stateMessage;
}
envCheckMsg.setAutId(autId);
return envCheckMsg;
}
/**
* Validates the runtime environment. This method should be overridden
* in subclasses which need a specific environment.
* @return null if the environment is OK or a message with a specific
* error code.
*/
protected StartAUTServerStateMessage validateEnvironment() {
return null;
}
/**
*
* @param parameters startup parameters for the AUT.
* @return the working directory for the AUT, or <code>null</code> if no
* working directory was defined.
*/
protected File getWorkingDir(Map parameters) {
String autWorkDir =
(String)parameters.get(AutConfigConstants.WORKING_DIR);
if (autWorkDir == null) {
autWorkDir = StringConstants.EMPTY;
}
File workingDir = new File(autWorkDir);
if (!workingDir.isDirectory() || !workingDir.exists()) {
if (log.isInfoEnabled()) {
log.info("Working dir: invalid"); //$NON-NLS-1$
}
workingDir = null;
}
return workingDir;
}
/**
* Creates the environment variables for starting the AUTServer.
* @param parameters startup parameters for the AUT.
* @param isAgentSet true if executable file and agent are set.
* @return the environment settings as array.
*/
protected String[] createEnvArray(Map<String, String> parameters,
boolean isAgentSet) {
m_isAgentSet = isAgentSet;
final String environment =
parameters.get(AutConfigConstants.ENVIRONMENT);
final boolean generate = Boolean.valueOf(parameters.get(
AutConfigConstants.NAME_TECHNICAL_COMPONENTS));
Properties oldProp = EnvironmentUtils.getProcessEnvironment();
String[] newEnvArray = null;
if (generate) {
Properties generateProperty = new Properties();
generateProperty.setProperty(
AutEnvironmentConstants.GENERATE_COMPONENT_NAMES,
String.valueOf(generate));
oldProp = EnvironmentUtils
.setEnvironment(oldProp, generateProperty);
newEnvArray = EnvironmentUtils.propToStrArray(
oldProp, IStartAut.PROPERTY_DELIMITER);
}
if ((environment != null) && (environment.trim().length() != 0)) {
String[] envArray = EnvironmentUtils.strToStrArray(environment, "\r\n"); //$NON-NLS-1$
Properties newProp = EnvironmentUtils.strArrayToProp(
envArray, IStartAut.PROPERTY_DELIMITER);
newProp = EnvironmentUtils.setEnvironment(oldProp, newProp);
newEnvArray = EnvironmentUtils.propToStrArray(
newProp, IStartAut.PROPERTY_DELIMITER);
}
return newEnvArray;
}
/**
*
* @param parameters startup parameters for the AUT.
* @return a <code>String</code> that represents a
* call to an executable. Ex. "java" or "/opt/java1.6/java".
*/
protected abstract String createBaseCmd(Map<String, String> parameters)
throws IOException;
/**
*
* @param baseCmd The base command to execute. For example, "java".
* @param parameters startup parameters for the AUT.
* @return an <code>Array</code> of <code>String</code>s representing
* a command line.
*/
protected abstract String[] createCmdArray(String baseCmd,
Map<String, String> parameters);
/**
* Executes the given command in the given environment with the
* given working directory.
* @param cmdArray The command line to execute.
* @param envArray The execution environment.
* @param workingDir The working directory.
* @param autId id of aut.
* @return a <code>StartAutServerStateMessage</code> which either describes an error
* condition or just tells the originator that the AUT was started correctly.
*/
protected StartAUTServerStateMessage executeCommand(String [] cmdArray,
String [] envArray, File workingDir, AutIdentifier autId)
throws IOException {
final AutStarter autAgent = AutStarter.getInstance();
Process process = Runtime.getRuntime().exec(cmdArray, envArray,
workingDir);
if (isErrorMessage()) {
log.error("AbstractStartToolkitAut - executeCommand: " //$NON-NLS-1$
+ getErrorMessage());
return getErrorMessage();
}
if (!autAgent.watchAUT(process, m_isAgentSet, autId)) {
process.destroy(); // new AUTServer could not be watched
return createBusyMessage();
}
return new StartAUTServerStateMessage(AUTStartResponse.OK);
}
/**
* Internal: generate a return message with the information about the problem. This
* is used in other methods to propagate errors.
*
* @param errorMessage The errorMessage to store.
*/
protected void setErrorMessage(StartAUTServerStateMessage errorMessage) {
m_errorMessage = errorMessage;
}
/**
* Internal: get a return message with the information about a problem. This
* is used in other methods to propagate errors.
*
* @return Returns the errorMessage.
*/
protected StartAUTServerStateMessage getErrorMessage() {
if (m_errorMessage == null) {
m_errorMessage = new StartAUTServerStateMessage(
AUTStartResponse.ERROR, "Unexpected error, no detail available."); //$NON-NLS-1$
}
return m_errorMessage;
}
/**
* Internal: checks whether there is currently an error message.
*
* @return <code>true</code> if an error has occurred and there is an
* error message available. Otherwise <code>false</code>.
*/
protected boolean isErrorMessage() {
return m_errorMessage != null;
}
/**
* Creates a <code>StartAUTServerStateMessage</code> with an
* <code>ERROR</code> state and a description that the server is already running.
* This message will eventually be returned be <code>execute()</code>.
*
* @return a new <code>StartAUTServerStateMessage</code>
*/
protected StartAUTServerStateMessage createBusyMessage() {
return new StartAUTServerStateMessage(AUTStartResponse.ERROR,
"AUTServer is already running"); //$NON-NLS-1$
}
/**
*
* @param bundleId The ID of the bundle to search for classpath entries.
* @return classpath entries contained within the bundle with the given ID.
* If
* <ul>
* <li>the bundle cannot be resolved to a file, or</li>
* <li>the bundle is not a JAR file and the bundle's directory contains no JAR files</li>
* </ul>
* an empty array will be returned.
*/
private static String[] getClasspathEntriesForBundleId(String bundleId) {
Bundle mainBundle = getBundleForID(bundleId);
ArrayList<Bundle> bundleAndFragmentList = new ArrayList<>();
bundleAndFragmentList.add(mainBundle);
// Checks if the bundles are from us, so we only add fragments
// from our bundles and not from others (like slf4j)
if (StringUtils.containsIgnoreCase(bundleId, "jubula") //$NON-NLS-1$
|| StringUtils.containsIgnoreCase(bundleId, "guidancer")) { //$NON-NLS-1$
bundleAndFragmentList.addAll(getFragmentsForBundleId(bundleId));
}
List<String> classpathEntries = new ArrayList<String>();
for (Bundle bundle : bundleAndFragmentList) {
classpathEntries.addAll(getPathforBundle(bundle));
}
return classpathEntries.toArray(new String[classpathEntries.size()]);
}
/**
* Determines the file-system path to the jar for the given bundle and also
* for nested jars within this jar
*
* @param bundle the bundle to get the path for
* @return A list containing the path to the jar, or several paths if the
* jar contained nested jars
*/
public static List<String> getPathforBundle(Bundle bundle) {
List<String> path = new ArrayList<String>();
try {
File bundleFile = FileLocator.getBundleFile(bundle);
if (bundleFile.isFile()) {
// bundle file is not a directory, so we assume it's a JAR file
path.add(bundleFile.getAbsolutePath());
// since the classloader cannot handle nested JARs, we need to extract
// all known nested JARs and add them to the classpath
try {
// assuming that it's a JAR/ZIP file
File[] createdFiles = ZipUtil.unzipTempJars(bundleFile);
for (int i = 0; i < createdFiles.length; i++) {
path.add(createdFiles[i].
getAbsolutePath());
}
} catch (IOException e) {
log.error("An error occurred while trying to extract nested JARs from " + bundle.getSymbolicName(), e); //$NON-NLS-1$
}
} else {
Enumeration<URL> e = bundle.findEntries(
"/", "*.jar", true); //$NON-NLS-1$//$NON-NLS-2$
if (e != null) {
while (e.hasMoreElements()) {
URL jarUrl = e.nextElement();
File jarFile =
new File(bundleFile + jarUrl.getFile());
if (!isJarFileWithManifestAttr(
jarFile, SOURCE_BUNDLE_MANIFEST_ATTR)) {
path.add(jarFile.getAbsolutePath());
}
}
}
}
} catch (IOException ioe) {
log.error("Bundle with ID '" + bundle.getSymbolicName() + "' could not be resolved to a file.", ioe); //$NON-NLS-1$//$NON-NLS-2$
}
return path;
}
/**
* Looks for the fragments which belong to the given Bundle. This search
* also includes non active bundles.
* @param mainBundle the bundle to find the fragments for
* @return the list with the fragments that have been found
*/
private static List<Bundle> fragmentLookupWithInactive(Bundle mainBundle) {
Bundle[] bundles = EclipseStarter.getSystemBundleContext().
getBundles();
List<Bundle> fragments = new ArrayList<Bundle>();
for (Bundle bundle : bundles) {
String fragmentHost = bundle.getHeaders().get(Constants.
FRAGMENT_HOST);
if (fragmentHost != null) {
if (fragmentHost.contains(StringConstants.SEMICOLON)) {
fragmentHost = fragmentHost.split(
StringConstants.SEMICOLON)[0];
}
if (fragmentHost.equals(mainBundle.getSymbolicName())) {
for (Bundle fragment : fragments) {
if (fragment.getSymbolicName().equals(
bundle.getSymbolicName())
&& bundle.getVersion().compareTo(
fragment.getVersion()) > 0) {
fragments.remove(fragment);
}
}
fragments.add(bundle);
}
}
}
return fragments;
}
/**
* Looks for the bundle with the given ID and the highest Version. This
* search also includes non active bundles.
*
* @param bundleId
* the bundle ID to look for
* @return the bundle
*/
private static Bundle bundleLookupWithInactive(String bundleId) {
BundleContext systemBundleContext = EclipseStarter
.getSystemBundleContext();
Bundle result = null;
if (systemBundleContext != null) {
Bundle[] bundles = systemBundleContext.getBundles();
Version currVersion = Version.emptyVersion;
for (Bundle bundle : bundles) {
if (bundle.getSymbolicName().equals(bundleId)
&& bundle.getVersion().compareTo(currVersion) > 0) {
result = bundle;
currVersion = bundle.getVersion();
}
}
} else {
log.warn("systemBundleContext is null - skipping bundleLookupWithInactive()"); //$NON-NLS-1$
}
return result;
}
/**
* Looks for the bundle with the given ID and the highest Version. This
* search also includes non active bundles.
*
* @param bundleId
* the bundle ID to look for
* @return the bundle
*/
public static Bundle getBundleForID(String bundleId) {
Bundle bundle = Platform.getBundle(bundleId);
if (bundle == null) {
bundle = bundleLookupWithInactive(bundleId);
if (bundle == null) {
log.error("No bundle found for ID '" + bundleId + "'."); //$NON-NLS-1$//$NON-NLS-2$
}
}
return bundle;
}
/**
*
* @param file The file to check.
* @param manifestAttr The name of the Manifest Attribute to check for.
* @return <code>true</code> iff all of the following statements apply:<ul>
* <li><code>file</code> is a valid, existing JAR file</li>
* <li><code>file</code> has a JAR Manifest</li>
* <li><code>file</code>'s JAR Manifest contains an Attribute
* named <code>manifestAttr</code></li>
* <li>no error occurs while performing the above checks</li>
* </ul>
*/
private static boolean isJarFileWithManifestAttr(
File file,
String manifestAttr) {
try {
JarFile jarFile = new JarFile(file);
try {
Manifest manifest = jarFile.getManifest();
if (manifest != null) {
return manifest.getMainAttributes().containsKey(
new Attributes.Name(manifestAttr));
}
} catch (IOException ioe) {
log.error("Error while reading JAR file.", ioe); //$NON-NLS-1$
} finally {
try {
jarFile.close();
} catch (IOException ioe) {
log.error("Error while closing JAR file.", ioe); //$NON-NLS-1$
}
}
} catch (IOException ioe) {
log.error("Error while opening JAR file.", ioe); //$NON-NLS-1$
} catch (SecurityException se) {
log.error("Error while opening JAR file.", se); //$NON-NLS-1$
}
return false;
}
/**
*
* @param bundleId The ID of the bundle to search for a classpath.
* @return the classpath contained within the bundle with the given ID.
* If
* <ul>
* <li>the bundle cannot be resolved to a file, or</li>
* <li>the bundle is not a JAR file and the bundle's directory contains no JAR files</li>
* </ul>
* an empty String will be returned.
*/
public static String getClasspathForBundleId(String bundleId) {
String[] classPath = getClasspathEntriesForBundleId(bundleId);
return createClassPath(classPath);
}
/**
* Creates a class path sting for a given string array
* @param classPath the array
* @return a the class path in the same order as the array
*/
protected static String createClassPath(String[] classPath) {
StringBuilder pathBuilder = new StringBuilder();
for (String entry : classPath) {
pathBuilder.append(entry).append(PATH_SEPARATOR);
}
return pathBuilder.length() == 0 ? "" //$NON-NLS-1$
: pathBuilder.substring(0,
pathBuilder.lastIndexOf(PATH_SEPARATOR));
}
/**
* Adds the parameters for remote debugging to the given command List
*
* @param cmds
* the command List
* @param isDirectExec
* true if the AUT is started by exec and not by a JVM
*/
protected void addDebugParams(List<String> cmds, boolean isDirectExec) {
final String rcDebug = IStartAut.RC_DEBUG;
if (rcDebug != null) {
if (isDirectExec) {
cmds.add("-vmargs -Xms128m -Xmx512m"); //$NON-NLS-1$
}
cmds.add("-Xdebug"); //$NON-NLS-1$
cmds.add("-Xnoagent"); //$NON-NLS-1$
cmds.add("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=" + rcDebug); //$NON-NLS-1$
cmds.add("-Djava.compiler=NONE"); //$NON-NLS-1$
}
}
/**
* Return the bundle id of the RC bundle
* @return the bundle name
*/
public abstract String getRcBundleId();
/**
* Finds fragments for the given bundle in the running Platform. If no
* active fragments are found, e.g. when jre version is below the minimum
* BREE of a bundle, we are also adding non-active (installed) fragments.
*
* @param rcBundleId the bundle name
* @return the fragments which have been found
*/
public static List<Bundle> getFragmentsForBundleId(String rcBundleId) {
Bundle fragmentHost = getBundleForID(rcBundleId);
ArrayList<Bundle> fragments = new ArrayList<Bundle>();
Bundle[] f = Platform.getFragments(fragmentHost);
if (f == null) {
fragments.addAll(
fragmentLookupWithInactive(fragmentHost));
} else {
for (Bundle fragment : f) {
fragments.add(fragment);
}
}
return fragments;
}
}