1 /******************************************************************************* 2 * Copyright (c) 2005, 2019 Cognos Incorporated, IBM Corporation and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * Cognos Incorporated - initial API and implementation 13 * IBM Corporation - bug fixes and enhancements 14 * Code 9 - bug fixes and enhancements 15 *******************************************************************************/ 16 package org.eclipse.equinox.servletbridge; 17 18 import java.io.*; 19 import java.lang.reflect.InvocationTargetException; 20 import java.lang.reflect.Method; 21 import java.net.MalformedURLException; 22 import java.net.URL; 23 import java.security.*; 24 import java.util.*; 25 import java.util.jar.*; 26 import javax.servlet.ServletConfig; 27 import javax.servlet.ServletContext; 28 29 /** 30 * The FrameworkLauncher provides the logic to: 31 * 1) init 32 * 2) deploy 33 * 3) start 34 * 4) stop 35 * 5) undeploy 36 * 6) destroy 37 * an instance of the OSGi framework. 38 * These 6 methods are provided to help manage the life-cycle and are called from outside this 39 * class by the BridgeServlet. To create an extended FrameworkLauncher over-ride these methods to allow 40 * custom behavior. 41 */ 42 public class FrameworkLauncher { 43 44 private static final String REFERENCE_SCHEME = "reference:"; //$NON-NLS-1$ 45 private static final String CONFIG_INI = "config.ini"; //$NON-NLS-1$ 46 private static final String DOT_JAR = ".jar"; //$NON-NLS-1$ 47 private static final String WS_DELIM = " \t\n\r\f"; //$NON-NLS-1$ 48 protected static final String FILE_SCHEME = "file:"; //$NON-NLS-1$ 49 protected static final String FRAMEWORK_BUNDLE_NAME = "org.eclipse.osgi"; //$NON-NLS-1$ 50 protected static final String STARTER = "org.eclipse.core.runtime.adaptor.EclipseStarter"; //$NON-NLS-1$ 51 protected static final String NULL_IDENTIFIER = "@null"; //$NON-NLS-1$ 52 protected static final String OSGI_FRAMEWORK = "osgi.framework"; //$NON-NLS-1$ 53 protected static final String OSGI_FRAMEWORK_EXTENSIONS = "osgi.framework.extensions"; //$NON-NLS-1$ 54 protected static final String OSGI_INSTANCE_AREA = "osgi.instance.area"; //$NON-NLS-1$ 55 protected static final String OSGI_CONFIGURATION_AREA = "osgi.configuration.area"; //$NON-NLS-1$ 56 protected static final String OSGI_INSTALL_AREA = "osgi.install.area"; //$NON-NLS-1$ 57 protected static final String OSGI_FORCED_RESTART = "osgi.forcedRestart"; //$NON-NLS-1$ 58 protected static final String RESOURCE_BASE = "/WEB-INF/"; //$NON-NLS-1$ 59 protected static final String ECLIPSE = "eclipse/"; //$NON-NLS-1$ 60 protected static final String LAUNCH_INI = "launch.ini"; //$NON-NLS-1$ 61 62 private static final String EXTENSIONBUNDLE_DEFAULT_BSN = "org.eclipse.equinox.servletbridge.extensionbundle"; //$NON-NLS-1$ 63 private static final String EXTENSIONBUNDLE_DEFAULT_VERSION = "1.3.0"; //$NON-NLS-1$ 64 private static final String MANIFEST_VERSION = "Manifest-Version"; //$NON-NLS-1$ 65 private static final String BUNDLE_MANIFEST_VERSION = "Bundle-ManifestVersion"; //$NON-NLS-1$ 66 private static final String BUNDLE_NAME = "Bundle-Name"; //$NON-NLS-1$ 67 private static final String BUNDLE_SYMBOLIC_NAME = "Bundle-SymbolicName"; //$NON-NLS-1$ 68 private static final String BUNDLE_VERSION = "Bundle-Version"; //$NON-NLS-1$ 69 private static final String FRAGMENT_HOST = "Fragment-Host"; //$NON-NLS-1$ 70 private static final String EXPORT_PACKAGE = "Export-Package"; //$NON-NLS-1$ 71 72 private static final String CONFIG_COMMANDLINE = "commandline"; //$NON-NLS-1$ 73 private static final String CONFIG_EXTENDED_FRAMEWORK_EXPORTS = "extendedFrameworkExports"; //$NON-NLS-1$ 74 private static final String CONFIG_OVERRIDE_AND_REPLACE_EXTENSION_BUNDLE = "overrideAndReplaceExtensionBundle"; //$NON-NLS-1$ 75 76 static final PermissionCollection allPermissions = new PermissionCollection() { 77 private static final long serialVersionUID = 482874725021998286L; 78 // The AllPermission permission 79 Permission allPermission = new AllPermission(); 80 81 // A simple PermissionCollection that only has AllPermission 82 @Override 83 public void add(Permission permission) { 84 // do nothing 85 } 86 87 @Override 88 public boolean implies(Permission permission) { 89 return true; 90 } 91 92 @Override 93 public Enumeration<Permission> elements() { 94 return new Enumeration<Permission>() { 95 int cur = 0; 96 97 @Override 98 public boolean hasMoreElements() { 99 return cur < 1; 100 } 101 102 @Override 103 public Permission nextElement() { 104 if (cur == 0) { 105 cur = 1; 106 return allPermission; 107 } 108 throw new NoSuchElementException(); 109 } 110 }; 111 } 112 }; 113 114 static { 115 // We do this to ensure the anonymous Enumeration class in allPermissions is pre-loaded 116 if (allPermissions.elements() == null) IllegalStateException()117 throw new IllegalStateException(); 118 } 119 120 protected ServletConfig config; 121 protected ServletContext context; 122 protected String resourceBase; 123 private File platformDirectory; 124 private ClassLoader frameworkContextClassLoader; 125 private CloseableURLClassLoader frameworkClassLoader; 126 init(ServletConfig servletConfig)127 void init(ServletConfig servletConfig) { 128 config = servletConfig; 129 context = servletConfig.getServletContext(); 130 initResourceBase(); 131 init(); 132 } 133 134 /** 135 * try to find the resource base for this webapp by looking for the launcher initialization file. 136 */ initResourceBase()137 protected void initResourceBase() { 138 try { 139 if (context.getResource(RESOURCE_BASE + LAUNCH_INI) != null) { 140 resourceBase = RESOURCE_BASE; 141 return; 142 } 143 if (context.getResource(RESOURCE_BASE + ECLIPSE + LAUNCH_INI) != null) { 144 resourceBase = RESOURCE_BASE + ECLIPSE; 145 return; 146 } 147 } catch (MalformedURLException e) { 148 // ignore 149 } 150 // If things don't work out, use the default resource base 151 resourceBase = RESOURCE_BASE; 152 } 153 154 /** 155 * init is the first method called on the FrameworkLauncher and can be used for any initial setup. 156 * The default behavior is to do nothing. 157 */ init()158 public void init() { 159 // do nothing for now 160 } 161 162 /** 163 * destroy is the last method called on the FrameworkLauncher and can be used for any final cleanup. 164 * The default behavior is to do nothing. 165 */ destroy()166 public void destroy() { 167 // do nothing for now 168 } 169 170 /** 171 * deploy is used to move the OSGi framework libraries into a location suitable for execution. 172 * The default behavior is to copy the contents of the webapp's WEB-INF/eclipse directory 173 * to the webapp's temp directory. 174 */ deploy()175 public synchronized void deploy() { 176 if (platformDirectory != null) { 177 context.log("Framework is already deployed"); //$NON-NLS-1$ 178 return; 179 } 180 181 File servletTemp = (File) context.getAttribute("javax.servlet.context.tempdir"); //$NON-NLS-1$ 182 platformDirectory = new File(servletTemp, "eclipse"); //$NON-NLS-1$ 183 if (!platformDirectory.exists()) { 184 platformDirectory.mkdirs(); 185 } 186 187 copyResource(resourceBase + "configuration/", new File(platformDirectory, "configuration")); //$NON-NLS-1$ //$NON-NLS-2$ 188 copyResource(resourceBase + "features/", new File(platformDirectory, "features")); //$NON-NLS-1$ //$NON-NLS-2$ 189 File plugins = new File(platformDirectory, "plugins"); //$NON-NLS-1$ 190 copyResource(resourceBase + "plugins/", plugins); //$NON-NLS-1$ 191 copyResource(resourceBase + "p2/", new File(platformDirectory, "p2")); //$NON-NLS-1$ //$NON-NLS-2$ 192 deployExtensionBundle(plugins); 193 copyResource(resourceBase + ".eclipseproduct", new File(platformDirectory, ".eclipseproduct")); //$NON-NLS-1$ //$NON-NLS-2$ 194 } 195 196 /** 197 * deployExtensionBundle will generate the Servletbridge extensionbundle if it is not already present in the platform's 198 * plugin directory. By default it exports "org.eclipse.equinox.servletbridge" and a versioned export of the Servlet API. 199 * Additional exports can be added by using the "extendedFrameworkExports" initial-param in the ServletConfig 200 */ deployExtensionBundle(File plugins)201 private void deployExtensionBundle(File plugins) { 202 // we might want to parameterize the extension bundle BSN in the future 203 final String extensionBundleBSN = EXTENSIONBUNDLE_DEFAULT_BSN; 204 File extensionBundleFile = findExtensionBundleFile(plugins, extensionBundleBSN); 205 206 if (extensionBundleFile == null) 207 generateExtensionBundle(plugins, extensionBundleBSN, EXTENSIONBUNDLE_DEFAULT_VERSION); 208 else if (Boolean.valueOf(config.getInitParameter(CONFIG_OVERRIDE_AND_REPLACE_EXTENSION_BUNDLE)).booleanValue()) { 209 String extensionBundleVersion = findExtensionBundleVersion(extensionBundleFile, extensionBundleBSN); 210 if (extensionBundleFile.isDirectory()) { 211 deleteDirectory(extensionBundleFile); 212 } else { 213 extensionBundleFile.delete(); 214 } 215 generateExtensionBundle(plugins, extensionBundleBSN, extensionBundleVersion); 216 } else { 217 processExtensionBundle(extensionBundleFile); 218 } 219 } 220 findExtensionBundleFile(File plugins, final String extensionBundleBSN)221 private File findExtensionBundleFile(File plugins, final String extensionBundleBSN) { 222 FileFilter extensionBundleFilter = new FileFilter() { 223 @Override 224 public boolean accept(File candidate) { 225 return candidate.getName().startsWith(extensionBundleBSN + "_"); //$NON-NLS-1$ 226 } 227 }; 228 File[] extensionBundles = plugins.listFiles(extensionBundleFilter); 229 if (extensionBundles.length == 0) 230 return null; 231 232 if (extensionBundles.length > 1) { 233 for (int i = 1; i < extensionBundles.length; i++) { 234 if (extensionBundles[i].isDirectory()) { 235 deleteDirectory(extensionBundles[i]); 236 } else { 237 extensionBundles[i].delete(); 238 } 239 } 240 } 241 return extensionBundles[0]; 242 } 243 findExtensionBundleVersion(File extensionBundleFile, String extensionBundleBSN)244 private String findExtensionBundleVersion(File extensionBundleFile, String extensionBundleBSN) { 245 String fileName = extensionBundleFile.getName(); 246 if (fileName.endsWith(DOT_JAR)) { 247 return fileName.substring(extensionBundleBSN.length() + 1, fileName.length() - DOT_JAR.length()); 248 } 249 return fileName.substring(extensionBundleBSN.length() + 1); 250 } 251 generateExtensionBundle(File plugins, String extensionBundleBSN, String extensionBundleVersion)252 private void generateExtensionBundle(File plugins, String extensionBundleBSN, String extensionBundleVersion) { 253 Manifest mf = new Manifest(); 254 Attributes attribs = mf.getMainAttributes(); 255 attribs.putValue(MANIFEST_VERSION, "1.0"); //$NON-NLS-1$ 256 attribs.putValue(BUNDLE_MANIFEST_VERSION, "2"); //$NON-NLS-1$ 257 attribs.putValue(BUNDLE_NAME, "Servletbridge Extension Bundle"); //$NON-NLS-1$ 258 attribs.putValue(BUNDLE_SYMBOLIC_NAME, extensionBundleBSN); 259 attribs.putValue(BUNDLE_VERSION, extensionBundleVersion); 260 attribs.putValue(FRAGMENT_HOST, "system.bundle; extension:=framework"); //$NON-NLS-1$ 261 262 String packageExports = null; 263 if (context.getMajorVersion() > 3) { 264 // we really have no idea what the packages or versions are, it all just a guess ... 265 String servletVersion = context.getMajorVersion() + "." + context.getMinorVersion(); //$NON-NLS-1$ 266 packageExports = "org.eclipse.equinox.servletbridge; version=1.1" + //$NON-NLS-1$ 267 ", javax.servlet; version=" + servletVersion + //$NON-NLS-1$ 268 ", javax.servlet.annotation; version=" + servletVersion + //$NON-NLS-1$ 269 ", javax.servlet.descriptor; version=" + servletVersion + //$NON-NLS-1$ 270 ", javax.servlet.http; version=" + servletVersion + //$NON-NLS-1$ 271 ", javax.servlet.resources; version=" + servletVersion; //$NON-NLS-1$ 272 } else if (context.getMajorVersion() == 3) { 273 // We know spec version 3.0 corresponds to package version 2.6 274 // we are guessing future 3.x spec versions will increment package versions minor, so ... 275 String servletVersion = (context.getMajorVersion() - 1) + "." + (context.getMinorVersion() + 6); //$NON-NLS-1$ 276 String specVersion = context.getMajorVersion() + "." + context.getMinorVersion(); //$NON-NLS-1$ 277 packageExports = "org.eclipse.equinox.servletbridge; version=1.1" + //$NON-NLS-1$ 278 ", javax.servlet; version=" + servletVersion + //$NON-NLS-1$ 279 ", javax.servlet; version=" + specVersion + //$NON-NLS-1$ 280 ", javax.servlet.annotation; version=" + servletVersion + //$NON-NLS-1$ 281 ", javax.servlet.annotation; version=" + specVersion + //$NON-NLS-1$ 282 ", javax.servlet.descriptor; version=" + servletVersion + //$NON-NLS-1$ 283 ", javax.servlet.descriptor; version=" + specVersion + //$NON-NLS-1$ 284 ", javax.servlet.http; version=" + servletVersion + //$NON-NLS-1$ 285 ", javax.servlet.http; version=" + specVersion + //$NON-NLS-1$ 286 ", javax.servlet.resources; version=" + servletVersion + //$NON-NLS-1$ 287 ", javax.servlet.resources; version=" + specVersion; //$NON-NLS-1$ 288 } else { 289 // We know spec version 2.x directly correspond to package versions 290 String servletVersion = context.getMajorVersion() + "." + context.getMinorVersion(); //$NON-NLS-1$ 291 packageExports = "org.eclipse.equinox.servletbridge; version=1.1" + //$NON-NLS-1$ 292 ", javax.servlet; version=" + servletVersion + //$NON-NLS-1$ 293 ", javax.servlet.http; version=" + servletVersion + //$NON-NLS-1$ 294 ", javax.servlet.resources; version=" + servletVersion; //$NON-NLS-1$ 295 } 296 297 String extendedExports = config.getInitParameter(CONFIG_EXTENDED_FRAMEWORK_EXPORTS); 298 if (extendedExports != null && extendedExports.trim().length() != 0) 299 packageExports += ", " + extendedExports; //$NON-NLS-1$ 300 301 attribs.putValue(EXPORT_PACKAGE, packageExports); 302 writeJarFile(new File(plugins, extensionBundleBSN + "_" + extensionBundleVersion + DOT_JAR), mf); //$NON-NLS-1$ 303 } 304 processExtensionBundle(File extensionBundleFile)305 private void processExtensionBundle(File extensionBundleFile) { 306 String fileName = extensionBundleFile.getName(); 307 if (fileName.endsWith(DOT_JAR)) { 308 Manifest mf = readJarFile(extensionBundleFile); 309 if (mf == null) 310 return; 311 Attributes attributes = mf.getMainAttributes(); 312 String exportPackage = (String) attributes.remove(new Attributes.Name("X-Deploy-Export-Package")); //$NON-NLS-1$ 313 if (exportPackage != null) { 314 attributes.putValue("Export-Package", exportPackage); //$NON-NLS-1$ 315 writeJarFile(extensionBundleFile, mf); 316 } 317 } 318 } 319 writeJarFile(File jarFile, Manifest mf)320 private void writeJarFile(File jarFile, Manifest mf) { 321 try { 322 JarOutputStream jos = null; 323 try { 324 jos = new JarOutputStream(new FileOutputStream(jarFile), mf); 325 jos.finish(); 326 } finally { 327 if (jos != null) 328 jos.close(); 329 } 330 } catch (IOException e) { 331 context.log("Error writing extension bundle", e); //$NON-NLS-1$ 332 } 333 } 334 readJarFile(File jarFile)335 private Manifest readJarFile(File jarFile) { 336 try { 337 JarInputStream jis = null; 338 try { 339 jis = new JarInputStream(new FileInputStream(jarFile)); 340 return jis.getManifest(); 341 } finally { 342 if (jis != null) 343 jis.close(); 344 } 345 } catch (IOException e) { 346 context.log("Error reading extension bundle", e); //$NON-NLS-1$ 347 } 348 return null; 349 } 350 351 /** undeploy is the reverse operation of deploy and removes the OSGi framework libraries from their 352 * execution location. Typically this method will only be called if a manual undeploy is requested in the 353 * ServletBridge. 354 * By default, this method removes the OSGi install and also removes the workspace. 355 */ undeploy()356 public synchronized void undeploy() { 357 if (platformDirectory == null) { 358 context.log("Undeploy unnecessary. - (not deployed)"); //$NON-NLS-1$ 359 return; 360 } 361 362 if (frameworkClassLoader != null) { 363 throw new IllegalStateException("Could not undeploy Framework - (not stopped)"); //$NON-NLS-1$ 364 } 365 366 deleteDirectory(new File(platformDirectory, "configuration")); //$NON-NLS-1$ 367 deleteDirectory(new File(platformDirectory, "features")); //$NON-NLS-1$ 368 deleteDirectory(new File(platformDirectory, "plugins")); //$NON-NLS-1$ 369 deleteDirectory(new File(platformDirectory, "workspace")); //$NON-NLS-1$ 370 deleteDirectory(new File(platformDirectory, "p2")); //$NON-NLS-1$ 371 372 new File(platformDirectory, ".eclipseproduct").delete(); //$NON-NLS-1$ 373 platformDirectory = null; 374 } 375 376 /** start is used to "start" a previously deployed OSGi framework 377 * The default behavior will read launcher.ini to create a set of initial properties and 378 * use the "commandline" configuration parameter to create the equivalent command line arguments 379 * available when starting Eclipse. 380 */ start()381 public synchronized void start() { 382 if (platformDirectory == null) 383 throw new IllegalStateException("Could not start the Framework - (not deployed)"); //$NON-NLS-1$ 384 385 if (frameworkClassLoader != null) { 386 context.log("Framework is already started"); //$NON-NLS-1$ 387 return; 388 } 389 390 Map<String, String> initialPropertyMap = buildInitialPropertyMap(); 391 String[] args = buildCommandLineArguments(); 392 393 // Handle commandline -D properties 394 for (String arg : args) { 395 if (arg.startsWith("-D")) { //$NON-NLS-1$ 396 int equalsIndex = arg.indexOf('='); 397 if (equalsIndex == -1) { 398 initialPropertyMap.put(arg.substring(2), ""); //$NON-NLS-1$ 399 } else { 400 String key = arg.substring(2, equalsIndex); 401 String value = arg.substring(equalsIndex + 1); 402 if (value.startsWith("\"") && value.endsWith("\"")) //$NON-NLS-1$//$NON-NLS-2$ 403 value = value.substring(1, value.length() - 1); 404 setInitialProperty(initialPropertyMap, key, value); 405 } 406 } 407 } 408 409 ClassLoader original = Thread.currentThread().getContextClassLoader(); 410 try { 411 System.setProperty("osgi.framework.useSystemProperties", "false"); //$NON-NLS-1$ //$NON-NLS-2$ 412 413 URL[] frameworkURLs = findFrameworkURLs(initialPropertyMap); 414 frameworkClassLoader = new ChildFirstURLClassLoader(frameworkURLs, this.getClass().getClassLoader()); 415 Class<?> clazz = frameworkClassLoader.loadClass(STARTER); 416 417 Method setInitialProperties = clazz.getMethod("setInitialProperties", Map.class); //$NON-NLS-1$ 418 setInitialProperties.invoke(null, initialPropertyMap); 419 420 registerRestartHandler(clazz); 421 422 Method runMethod = clazz.getMethod("startup", String[].class, Runnable.class); //$NON-NLS-1$ 423 runMethod.invoke(null, args, null); 424 425 frameworkContextClassLoader = Thread.currentThread().getContextClassLoader(); 426 } catch ( 427 428 InvocationTargetException ite) { 429 Throwable t = ite.getTargetException(); 430 if (t == null) 431 t = ite; 432 context.log("Error while starting Framework", t); //$NON-NLS-1$ 433 throw new RuntimeException(t.getMessage()); 434 } catch (Exception e) { 435 context.log("Error while starting Framework", e); //$NON-NLS-1$ 436 throw new RuntimeException(e.getMessage()); 437 } finally { 438 Thread.currentThread().setContextClassLoader(original); 439 } 440 } 441 findFrameworkURLs(Map<String, String> initialPropertyMap)442 private URL[] findFrameworkURLs(Map<String, String> initialPropertyMap) { 443 List<URL> frameworkURLs = new ArrayList<>(); 444 String installArea = initialPropertyMap.get(OSGI_INSTALL_AREA); 445 if (installArea.startsWith(FILE_SCHEME)) { 446 installArea = installArea.substring(FILE_SCHEME.length()); 447 } 448 File installBase = new File(installArea); 449 450 // OSGi framework 451 String osgiFramework = initialPropertyMap.get(OSGI_FRAMEWORK); 452 File osgiFrameworkFile = null; 453 if (osgiFramework == null) { 454 // search for osgi.framework in osgi.install.area 455 String path = new File(installBase, "plugins").toString(); //$NON-NLS-1$ 456 path = searchFor(FRAMEWORK_BUNDLE_NAME, path); 457 if (path == null) 458 throw new RuntimeException("Could not find framework"); //$NON-NLS-1$ 459 460 osgiFrameworkFile = new File(path); 461 } else { 462 if (osgiFramework.startsWith(FILE_SCHEME)) { 463 osgiFramework = osgiFramework.substring(FILE_SCHEME.length()); 464 } 465 osgiFrameworkFile = new File(osgiFramework); 466 if (!osgiFrameworkFile.isAbsolute()) 467 osgiFrameworkFile = new File(installBase, osgiFramework); 468 } 469 470 try { 471 URL frameworkURL = osgiFrameworkFile.toURL(); 472 frameworkURLs.add(frameworkURL); 473 // ensure the framework URL is absolute 474 initialPropertyMap.put(OSGI_FRAMEWORK, frameworkURL.toExternalForm()); 475 } catch (MalformedURLException e) { 476 throw new RuntimeException("Could not find framework -- " + e.getMessage()); //$NON-NLS-1$ 477 } 478 479 // OSGi framework extensions 480 String osgiFrameworkExtensions = initialPropertyMap.get(OSGI_FRAMEWORK_EXTENSIONS); 481 if (osgiFrameworkExtensions != null) { 482 StringTokenizer tokenizer = new StringTokenizer(osgiFrameworkExtensions, ","); //$NON-NLS-1$ 483 while (tokenizer.hasMoreTokens()) { 484 String extension = tokenizer.nextToken().trim(); 485 if (extension.length() == 0) 486 continue; 487 488 URL extensionURL = findExtensionURL(extension, osgiFrameworkFile); 489 if (extensionURL != null) { 490 frameworkURLs.add(extensionURL); 491 } 492 } 493 } 494 return frameworkURLs.toArray(new URL[frameworkURLs.size()]); 495 } 496 findExtensionURL(String extension, File osgiFrameworkFile)497 private URL findExtensionURL(String extension, File osgiFrameworkFile) { 498 File extensionFile = null; 499 if (extension.startsWith(REFERENCE_SCHEME)) { 500 extension = extension.substring(REFERENCE_SCHEME.length()); 501 if (!extension.startsWith(FILE_SCHEME)) 502 throw new RuntimeException("Non-file scheme for framework extension URL -- " + extension); //$NON-NLS-1$ 503 extension = extension.substring(FILE_SCHEME.length()); 504 extensionFile = new File(extension); 505 if (!extensionFile.isAbsolute()) 506 extensionFile = new File(osgiFrameworkFile.getParentFile(), extension); 507 } else { 508 String fullExtensionPath = searchFor(extension, osgiFrameworkFile.getParent()); 509 if (fullExtensionPath == null) 510 return null; 511 extensionFile = new File(fullExtensionPath); 512 } 513 514 try { 515 return extensionFile.toURL(); 516 } catch (MalformedURLException e) { 517 throw new RuntimeException("Could not find framework extension -- " + extensionFile.getAbsolutePath() + " : " + e.getMessage()); //$NON-NLS-1$ //$NON-NLS-2$ 518 } 519 } 520 registerRestartHandler(Class<?> starterClazz)521 private void registerRestartHandler(Class<?> starterClazz) throws IllegalAccessException, InvocationTargetException { 522 Method registerFrameworkShutdownHandler = null; 523 try { 524 registerFrameworkShutdownHandler = starterClazz.getDeclaredMethod("internalAddFrameworkShutdownHandler", Runnable.class); //$NON-NLS-1$ 525 if (!registerFrameworkShutdownHandler.isAccessible()) { 526 registerFrameworkShutdownHandler.setAccessible(true); 527 } 528 Runnable restartHandler = createRestartHandler(starterClazz); 529 registerFrameworkShutdownHandler.invoke(null, restartHandler); 530 } catch (NoSuchMethodException e) { 531 // Ok. However we will not support restart events. Log this as info 532 context.log(starterClazz.getName() + " does not support setting a shutdown handler. Restart handling is disabled."); //$NON-NLS-1$ 533 return; 534 } 535 536 } 537 createRestartHandler(Class<?> starterClazz)538 private Runnable createRestartHandler(Class<?> starterClazz) throws NoSuchMethodException { 539 final Method getProperty = starterClazz.getDeclaredMethod("getProperty", String.class); //$NON-NLS-1$ 540 if (!getProperty.isAccessible()) { 541 getProperty.setAccessible(true); 542 } 543 Runnable restartHandler = new Runnable() { 544 @Override 545 public void run() { 546 try { 547 String forcedRestart = (String) getProperty.invoke(null, OSGI_FORCED_RESTART); 548 if (Boolean.valueOf(forcedRestart).booleanValue()) { 549 stop(); 550 start(); 551 } 552 } catch (InvocationTargetException ite) { 553 Throwable t = ite.getTargetException(); 554 if (t == null) 555 t = ite; 556 throw new RuntimeException(t.getMessage()); 557 } catch (Exception e) { 558 throw new RuntimeException(e.getMessage()); 559 } 560 } 561 }; 562 return restartHandler; 563 } 564 565 /** buildInitialPropertyMap create the initial set of properties from the contents of launch.ini 566 * and for a few other properties necessary to launch defaults are supplied if not provided. 567 * The value '@null' will set the map value to null. 568 * @return a map containing the initial properties 569 */ 570 @SuppressWarnings("rawtypes") buildInitialPropertyMap()571 protected Map<String, String> buildInitialPropertyMap() { 572 Map<String, String> initialPropertyMap = new HashMap<>(); 573 Properties launchProperties = loadProperties(resourceBase + LAUNCH_INI); 574 for (Map.Entry entry : launchProperties.entrySet()) { 575 String key = (String) entry.getKey(); 576 String value = (String) entry.getValue(); 577 setInitialProperty(initialPropertyMap, key, value); 578 } 579 580 try { 581 // install.area if not specified 582 if (initialPropertyMap.get(OSGI_INSTALL_AREA) == null) 583 initialPropertyMap.put(OSGI_INSTALL_AREA, platformDirectory.toURL().toExternalForm()); 584 585 // configuration.area if not specified 586 if (initialPropertyMap.get(OSGI_CONFIGURATION_AREA) == null) { 587 File configurationDirectory = new File(platformDirectory, "configuration"); //$NON-NLS-1$ 588 if (!configurationDirectory.exists()) { 589 configurationDirectory.mkdirs(); 590 } 591 initialPropertyMap.put(OSGI_CONFIGURATION_AREA, configurationDirectory.toURL().toExternalForm()); 592 } 593 594 // instance.area if not specified 595 if (initialPropertyMap.get(OSGI_INSTANCE_AREA) == null) { 596 File workspaceDirectory = new File(platformDirectory, "workspace"); //$NON-NLS-1$ 597 if (!workspaceDirectory.exists()) { 598 workspaceDirectory.mkdirs(); 599 } 600 initialPropertyMap.put(OSGI_INSTANCE_AREA, workspaceDirectory.toURL().toExternalForm()); 601 } 602 603 // read values from config.ini 604 Properties configurationProperties = loadConfigurationFile(initialPropertyMap); 605 606 // osgi.framework if not specified 607 if (initialPropertyMap.get(OSGI_FRAMEWORK) == null) { 608 String osgiFramework = configurationProperties.getProperty(OSGI_FRAMEWORK); 609 if (osgiFramework != null) 610 initialPropertyMap.put(OSGI_FRAMEWORK, osgiFramework); 611 } 612 613 // osgi.framework.extensions if not specified 614 if (initialPropertyMap.get(OSGI_FRAMEWORK_EXTENSIONS) == null) { 615 String osgiFrameworkExtensions = configurationProperties.getProperty(OSGI_FRAMEWORK_EXTENSIONS); 616 if (osgiFrameworkExtensions != null) 617 initialPropertyMap.put(OSGI_FRAMEWORK_EXTENSIONS, osgiFrameworkExtensions); 618 } 619 620 } catch (MalformedURLException e) { 621 throw new RuntimeException("Error establishing location"); //$NON-NLS-1$ 622 } 623 624 return initialPropertyMap; 625 } 626 setInitialProperty(Map<String, String> initialPropertyMap, String key, String value)627 private void setInitialProperty(Map<String, String> initialPropertyMap, String key, String value) { 628 if (key.endsWith("*")) { //$NON-NLS-1$ 629 if (value.equals(NULL_IDENTIFIER)) { 630 clearPrefixedSystemProperties(key.substring(0, key.length() - 1), initialPropertyMap); 631 } 632 } else if (value.equals(NULL_IDENTIFIER)) 633 initialPropertyMap.put(key, null); 634 else 635 initialPropertyMap.put(key, value); 636 } 637 loadConfigurationFile(Map<String, String> initialPropertyMap)638 private Properties loadConfigurationFile(Map<String, String> initialPropertyMap) { 639 InputStream is = null; 640 try { 641 String installArea = initialPropertyMap.get(OSGI_INSTALL_AREA); 642 if (installArea.startsWith(FILE_SCHEME)) { 643 installArea = installArea.substring(FILE_SCHEME.length()); 644 } 645 File installBase = new File(installArea); 646 647 String configurationArea = initialPropertyMap.get(OSGI_CONFIGURATION_AREA); 648 if (configurationArea.startsWith(FILE_SCHEME)) { 649 configurationArea = configurationArea.substring(FILE_SCHEME.length()); 650 } 651 File configurationBase = new File(configurationArea); 652 if (!configurationBase.isAbsolute()) 653 configurationBase = new File(installBase, configurationArea); 654 655 File configurationFile = new File(configurationBase, CONFIG_INI); 656 if (!configurationFile.exists()) 657 return null; 658 659 Properties configProperties = new Properties(); 660 is = new BufferedInputStream(new FileInputStream(configurationFile)); 661 configProperties.load(is); 662 return configProperties; 663 } catch (Throwable t) { 664 context.log("Error reading configuration file -- " + t.toString()); //$NON-NLS-1$ 665 return null; 666 } finally { 667 if (is != null) 668 try { 669 is.close(); 670 } catch (IOException e) { 671 // unexpected 672 e.printStackTrace(); 673 } 674 } 675 } 676 677 /** 678 * clearPrefixedSystemProperties clears System Properties by writing null properties in the targetPropertyMap that match a prefix 679 */ clearPrefixedSystemProperties(String prefix, Map<String, String> targetPropertyMap)680 private static void clearPrefixedSystemProperties(String prefix, Map<String, String> targetPropertyMap) { 681 for (Object key : System.getProperties().keySet()) { 682 String propertyName = (String) key; 683 if (propertyName.startsWith(prefix) && !targetPropertyMap.containsKey(propertyName)) { 684 targetPropertyMap.put(propertyName, null); 685 } 686 } 687 } 688 689 /** 690 * buildCommandLineArguments parses the commandline config parameter into a set of arguments 691 * @return an array of String containing the commandline arguments 692 */ buildCommandLineArguments()693 protected String[] buildCommandLineArguments() { 694 List<String> args = new ArrayList<>(); 695 696 String commandLine = config.getInitParameter(CONFIG_COMMANDLINE); 697 if (commandLine != null) { 698 StringTokenizer tokenizer = new StringTokenizer(commandLine, WS_DELIM); 699 while (tokenizer.hasMoreTokens()) { 700 String arg = tokenizer.nextToken(); 701 if (arg.startsWith("\"")) { //$NON-NLS-1$ 702 if (arg.endsWith("\"")) { //$NON-NLS-1$ 703 if (arg.length() >= 2) { 704 // strip the beginning and ending quotes 705 arg = arg.substring(1, arg.length() - 1); 706 } 707 } else { 708 String remainingArg = tokenizer.nextToken("\""); //$NON-NLS-1$ 709 arg = arg.substring(1) + remainingArg; 710 // skip to next whitespace separated token 711 tokenizer.nextToken(WS_DELIM); 712 } 713 } else if (arg.startsWith("'")) { //$NON-NLS-1$ 714 if (arg.endsWith("'")) { //$NON-NLS-1$ 715 if (arg.length() >= 2) { 716 // strip the beginning and ending quotes 717 arg = arg.substring(1, arg.length() - 1); 718 } 719 } else { 720 String remainingArg = tokenizer.nextToken("'"); //$NON-NLS-1$ 721 arg = arg.substring(1) + remainingArg; 722 // skip to next whitespace separated token 723 tokenizer.nextToken(WS_DELIM); 724 } 725 } else if (arg.startsWith("-D")) { //$NON-NLS-1$ 726 int matchIndex = arg.indexOf("=\""); //$NON-NLS-1$ 727 if (matchIndex != -1) { 728 if (!arg.substring(matchIndex + 2).endsWith("\"") && tokenizer.hasMoreTokens()) { //$NON-NLS-1$ 729 arg += tokenizer.nextToken("\"") + "\""; //$NON-NLS-1$ //$NON-NLS-2$ 730 // skip to next whitespace separated token 731 tokenizer.nextToken(WS_DELIM); 732 } 733 } 734 } 735 args.add(arg); 736 } 737 } 738 return args.toArray(new String[] {}); 739 } 740 741 /** 742 * stop is used to "shutdown" the framework and make it avialable for garbage collection. 743 * The default implementation also has special handling for Apache Commons Logging to "release" any 744 * resources associated with the frameworkContextClassLoader. 745 */ stop()746 public synchronized void stop() { 747 if (platformDirectory == null) { 748 context.log("Shutdown unnecessary. (not deployed)"); //$NON-NLS-1$ 749 return; 750 } 751 752 if (frameworkClassLoader == null) { 753 context.log("Framework is already shutdown"); //$NON-NLS-1$ 754 return; 755 } 756 757 ClassLoader original = Thread.currentThread().getContextClassLoader(); 758 try { 759 Class<?> clazz = frameworkClassLoader.loadClass(STARTER); 760 Method method = clazz.getDeclaredMethod("shutdown"); //$NON-NLS-1$ 761 Thread.currentThread().setContextClassLoader(frameworkContextClassLoader); 762 method.invoke(clazz); 763 764 // ACL keys its loggers off of the ContextClassLoader which prevents GC without calling release. 765 // This section explicitly calls release if ACL is used. 766 try { 767 clazz = this.getClass().getClassLoader().loadClass("org.apache.commons.logging.LogFactory"); //$NON-NLS-1$ 768 method = clazz.getDeclaredMethod("release", ClassLoader.class); //$NON-NLS-1$ 769 method.invoke(clazz, frameworkContextClassLoader); 770 } catch (ClassNotFoundException e) { 771 // ignore, ACL is not being used 772 } 773 774 } catch (Exception e) { 775 context.log("Error while stopping Framework", e); //$NON-NLS-1$ 776 return; 777 } finally { 778 frameworkClassLoader.close(); 779 frameworkClassLoader = null; 780 frameworkContextClassLoader = null; 781 Thread.currentThread().setContextClassLoader(original); 782 } 783 } 784 785 /** 786 * copyResource is a convenience method to recursively copy resources from the ServletContext to 787 * an installation target. The default behavior will create a directory if the resourcepath ends 788 * in '/' and a file otherwise. 789 * @param resourcePath - The resource root path 790 * @param target - The root location where resources are to be copied 791 */ copyResource(String resourcePath, File target)792 protected void copyResource(String resourcePath, File target) { 793 if (resourcePath.endsWith("/")) { //$NON-NLS-1$ 794 target.mkdir(); 795 Set<String> paths = context.getResourcePaths(resourcePath); 796 if (paths == null) 797 return; 798 for (String path : paths) { 799 File newFile = new File(target, path.substring(resourcePath.length())); 800 copyResource(path, newFile); 801 } 802 } else { 803 try { 804 if (target.createNewFile()) { 805 InputStream is = null; 806 OutputStream os = null; 807 try { 808 is = context.getResourceAsStream(resourcePath); 809 if (is == null) 810 return; 811 os = new FileOutputStream(target); 812 byte[] buffer = new byte[8192]; 813 int bytesRead = is.read(buffer); 814 while (bytesRead != -1) { 815 os.write(buffer, 0, bytesRead); 816 bytesRead = is.read(buffer); 817 } 818 } finally { 819 if (is != null) 820 is.close(); 821 822 if (os != null) 823 os.close(); 824 } 825 } 826 } catch (IOException e) { 827 context.log("Error copying resources", e); //$NON-NLS-1$ 828 } 829 } 830 } 831 832 /** 833 * deleteDirectory is a convenience method to recursively delete a directory 834 * @param directory - the directory to delete. 835 * @return was the delete successful 836 */ deleteDirectory(File directory)837 protected static boolean deleteDirectory(File directory) { 838 if (directory.isDirectory()) { 839 File[] files = directory.listFiles(); 840 for (File file : files) { 841 if (file.isDirectory()) { 842 deleteDirectory(file); 843 } else { 844 file.delete(); 845 } 846 } 847 } 848 return directory.delete(); 849 } 850 851 /** 852 * Used when to set the ContextClassLoader when the BridgeServlet delegates to a Servlet 853 * inside the framework 854 * @return a Classloader with the OSGi framework's context class loader. 855 */ getFrameworkContextClassLoader()856 public synchronized ClassLoader getFrameworkContextClassLoader() { 857 return frameworkContextClassLoader; 858 } 859 860 /** 861 * Platfom Directory is where the OSGi software is installed 862 * @return the framework install location 863 */ getPlatformDirectory()864 protected synchronized File getPlatformDirectory() { 865 return platformDirectory; 866 } 867 868 /** 869 * loadProperties is a convenience method to load properties from a servlet context resource 870 * @param resource - The target to read properties from 871 * @return the properties 872 */ loadProperties(String resource)873 protected Properties loadProperties(String resource) { 874 Properties result = new Properties(); 875 InputStream in = null; 876 try { 877 URL location = context.getResource(resource); 878 if (location != null) { 879 in = location.openStream(); 880 result.load(in); 881 } 882 } catch (MalformedURLException e) { 883 // no url to load from 884 } catch (IOException e) { 885 // its ok if there is no file 886 } finally { 887 if (in != null) { 888 try { 889 in.close(); 890 } catch (IOException e) { 891 // ignore 892 } 893 } 894 } 895 return result; 896 } 897 898 /*************************************************************************** 899 * See org.eclipse.core.launcher [copy of searchFor, findMax, 900 * compareVersion, getVersionElements] TODO: If these methods were made 901 * public and static we could use them directly 902 **************************************************************************/ 903 904 /** 905 * Searches for the given target directory starting in the "plugins" subdirectory 906 * of the given location. If one is found then this location is returned; 907 * otherwise an exception is thrown. 908 * @param target 909 * 910 * @return the location where target directory was found 911 * @param start the location to begin searching 912 */ searchFor(final String target, String start)913 protected String searchFor(final String target, String start) { 914 FileFilter filter = new FileFilter() { 915 @Override 916 public boolean accept(File candidate) { 917 return candidate.getName().equals(target) || candidate.getName().startsWith(target + "_"); //$NON-NLS-1$ 918 } 919 }; 920 File[] candidates = new File(start).listFiles(filter); 921 if (candidates == null) 922 return null; 923 String[] arrays = new String[candidates.length]; 924 for (int i = 0; i < arrays.length; i++) { 925 arrays[i] = candidates[i].getName(); 926 } 927 int result = findMax(arrays); 928 if (result == -1) 929 return null; 930 return candidates[result].getAbsolutePath().replace(File.separatorChar, '/') + (candidates[result].isDirectory() ? "/" : ""); //$NON-NLS-1$//$NON-NLS-2$ 931 } 932 findMax(String[] candidates)933 protected int findMax(String[] candidates) { 934 int result = -1; 935 Object maxVersion = null; 936 for (int i = 0; i < candidates.length; i++) { 937 String name = candidates[i]; 938 String version = ""; //$NON-NLS-1$ // Note: directory with version suffix is always > than directory without version suffix 939 int index = name.indexOf('_'); 940 if (index != -1) 941 version = name.substring(index + 1); 942 Object currentVersion = getVersionElements(version); 943 if (maxVersion == null) { 944 result = i; 945 maxVersion = currentVersion; 946 } else { 947 if (compareVersion((Object[]) maxVersion, (Object[]) currentVersion) < 0) { 948 result = i; 949 maxVersion = currentVersion; 950 } 951 } 952 } 953 return result; 954 } 955 956 /** 957 * Compares version strings. 958 * @param left 959 * @param right 960 * @return result of comparison, as integer; 961 * <code><0</code> if left < right; 962 * <code>0</code> if left == right; 963 * <code>>0</code> if left > right; 964 */ compareVersion(Object[] left, Object[] right)965 private int compareVersion(Object[] left, Object[] right) { 966 967 int result = ((Integer) left[0]).compareTo((Integer) right[0]); // compare major 968 if (result != 0) 969 return result; 970 971 result = ((Integer) left[1]).compareTo((Integer) right[1]); // compare minor 972 if (result != 0) 973 return result; 974 975 result = ((Integer) left[2]).compareTo((Integer) right[2]); // compare service 976 if (result != 0) 977 return result; 978 979 return ((String) left[3]).compareTo((String) right[3]); // compare qualifier 980 } 981 982 /** 983 * Do a quick parse of version identifier so its elements can be correctly compared. 984 * If we are unable to parse the full version, remaining elements are initialized 985 * with suitable defaults. 986 * @param version 987 * @return an array of size 4; first three elements are of type Integer (representing 988 * major, minor and service) and the fourth element is of type String (representing 989 * qualifier). Note, that returning anything else will cause exceptions in the caller. 990 */ getVersionElements(String version)991 private Object[] getVersionElements(String version) { 992 if (version.endsWith(DOT_JAR)) 993 version = version.substring(0, version.length() - 4); 994 Object[] result = {Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0), ""}; //$NON-NLS-1$ 995 StringTokenizer t = new StringTokenizer(version, "."); //$NON-NLS-1$ 996 String token; 997 int i = 0; 998 while (t.hasMoreTokens() && i < 4) { 999 token = t.nextToken(); 1000 if (i < 3) { 1001 // major, minor or service ... numeric values 1002 try { 1003 result[i++] = Integer.valueOf(token); 1004 } catch (Exception e) { 1005 // invalid number format - use default numbers (0) for the rest 1006 break; 1007 } 1008 } else { 1009 // qualifier ... string value 1010 result[i++] = token; 1011 } 1012 } 1013 return result; 1014 } 1015 1016 /** 1017 * The ChildFirstURLClassLoader alters regular ClassLoader delegation and will check the URLs 1018 * used in its initialization for matching classes before delegating to it's parent. 1019 * Sometimes also referred to as a ParentLastClassLoader 1020 */ 1021 protected static class ChildFirstURLClassLoader extends CloseableURLClassLoader { 1022 private static final boolean CHILDFIRST_REGISTERED_AS_PARALLEL; 1023 1024 static { 1025 boolean registeredAsParallel; 1026 try { 1027 Method parallelCapableMetod = ClassLoader.class.getDeclaredMethod("registerAsParallelCapable", (Class[]) null); //$NON-NLS-1$ 1028 parallelCapableMetod.setAccessible(true); 1029 registeredAsParallel = ((Boolean) parallelCapableMetod.invoke(null, (Object[]) null)).booleanValue(); 1030 } catch (Throwable e) { 1031 // must do everything to avoid failing in clinit 1032 registeredAsParallel = false; 1033 } 1034 CHILDFIRST_REGISTERED_AS_PARALLEL = registeredAsParallel; 1035 } 1036 ChildFirstURLClassLoader(URL[] urls, ClassLoader parent)1037 public ChildFirstURLClassLoader(URL[] urls, ClassLoader parent) { 1038 super(urls, parent, false); 1039 } 1040 1041 @Override getResource(String name)1042 public URL getResource(String name) { 1043 URL resource = findResource(name); 1044 if (resource == null) { 1045 ClassLoader parent = getParent(); 1046 if (parent != null) 1047 resource = parent.getResource(name); 1048 } 1049 return resource; 1050 } 1051 1052 @Override loadClass(String name, boolean resolve)1053 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 1054 if (isRegisteredAsParallel()) { 1055 return loadClass0(name, resolve); 1056 } 1057 synchronized (this) { 1058 return loadClass0(name, resolve); 1059 } 1060 } 1061 loadClass0(String name, boolean resolve)1062 private Class<?> loadClass0(String name, boolean resolve) throws ClassNotFoundException { 1063 Class<?> clazz = findLoadedClass(name); 1064 if (clazz == null) { 1065 try { 1066 clazz = findClass(name); 1067 } catch (ClassNotFoundException e) { 1068 ClassLoader parent = getParent(); 1069 if (parent != null) 1070 clazz = parent.loadClass(name); 1071 else 1072 clazz = getSystemClassLoader().loadClass(name); 1073 } 1074 } 1075 1076 if (resolve) 1077 resolveClass(clazz); 1078 1079 return clazz; 1080 } 1081 1082 // we want to ensure that the framework has AllPermissions 1083 @Override getPermissions(CodeSource codesource)1084 protected PermissionCollection getPermissions(CodeSource codesource) { 1085 return allPermissions; 1086 } 1087 1088 @Override isRegisteredAsParallel()1089 protected boolean isRegisteredAsParallel() { 1090 return CHILDFIRST_REGISTERED_AS_PARALLEL; 1091 } 1092 1093 } 1094 1095 } 1096