001/* 002 * Copyright (c) 2006-2015 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * Bogdan Stefanescu 011 * Florent Guillaume 012 * Julien Carsique 013 */ 014 015package org.nuxeo.runtime.osgi; 016 017import java.io.BufferedInputStream; 018import java.io.File; 019import java.io.FileInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.lang.reflect.Field; 023import java.net.MalformedURLException; 024import java.net.URL; 025import java.util.ArrayList; 026import java.util.Arrays; 027import java.util.Collections; 028import java.util.Comparator; 029import java.util.HashSet; 030import java.util.Iterator; 031import java.util.List; 032import java.util.Map; 033import java.util.Set; 034import java.util.StringTokenizer; 035import java.util.concurrent.ConcurrentHashMap; 036 037import org.apache.commons.logging.Log; 038import org.apache.commons.logging.LogFactory; 039 040import org.nuxeo.common.Environment; 041import org.nuxeo.common.codec.CryptoProperties; 042import org.nuxeo.common.utils.FileUtils; 043import org.nuxeo.common.utils.TextTemplate; 044import org.nuxeo.runtime.AbstractRuntimeService; 045import org.nuxeo.runtime.RuntimeServiceException; 046import org.nuxeo.runtime.Version; 047import org.nuxeo.runtime.api.Framework; 048import org.nuxeo.runtime.model.ComponentName; 049import org.nuxeo.runtime.model.RegistrationInfo; 050import org.nuxeo.runtime.model.RuntimeContext; 051import org.nuxeo.runtime.model.impl.ComponentPersistence; 052import org.nuxeo.runtime.model.impl.RegistrationInfoImpl; 053 054import org.osgi.framework.Bundle; 055import org.osgi.framework.BundleContext; 056import org.osgi.framework.Constants; 057import org.osgi.framework.FrameworkEvent; 058import org.osgi.framework.FrameworkListener; 059 060/** 061 * The default implementation of NXRuntime over an OSGi compatible environment. 062 * 063 * @author Bogdan Stefanescu 064 * @author Florent Guillaume 065 */ 066public class OSGiRuntimeService extends AbstractRuntimeService implements FrameworkListener { 067 068 public static final ComponentName FRAMEWORK_STARTED_COMP = new ComponentName("org.nuxeo.runtime.started"); 069 070 /** Can be used to change the runtime home directory */ 071 public static final String PROP_HOME_DIR = "org.nuxeo.runtime.home"; 072 073 /** The OSGi application install directory. */ 074 public static final String PROP_INSTALL_DIR = "INSTALL_DIR"; 075 076 /** The OSGi application config directory. */ 077 public static final String PROP_CONFIG_DIR = "CONFIG_DIR"; 078 079 /** The host adapter. */ 080 public static final String PROP_HOST_ADAPTER = "HOST_ADAPTER"; 081 082 public static final String PROP_NUXEO_BIND_ADDRESS = "nuxeo.bind.address"; 083 084 public static final String NAME = "OSGi NXRuntime"; 085 086 public static final Version VERSION = Version.parseString("1.4.0"); 087 088 private static final Log log = LogFactory.getLog(OSGiRuntimeService.class); 089 090 private final BundleContext bundleContext; 091 092 private final Map<String, RuntimeContext> contexts; 093 094 private boolean appStarted = false; 095 096 /** 097 * OSGi doesn't provide a method to lookup bundles by symbolic name. This table is used to map symbolic names to 098 * bundles. This map is not handling bundle versions. 099 */ 100 final Map<String, Bundle> bundles; 101 102 final ComponentPersistence persistence; 103 104 public OSGiRuntimeService(BundleContext context) { 105 this(new OSGiRuntimeContext(context.getBundle()), context); 106 } 107 108 public OSGiRuntimeService(OSGiRuntimeContext runtimeContext, BundleContext context) { 109 super(runtimeContext); 110 bundleContext = context; 111 bundles = new ConcurrentHashMap<>(); 112 contexts = new ConcurrentHashMap<>(); 113 String bindAddress = context.getProperty(PROP_NUXEO_BIND_ADDRESS); 114 if (bindAddress != null) { 115 properties.put(PROP_NUXEO_BIND_ADDRESS, bindAddress); 116 } 117 String homeDir = getProperty(PROP_HOME_DIR); 118 log.debug("Home directory: " + homeDir); 119 if (homeDir != null) { 120 workingDir = new File(homeDir); 121 } else { 122 workingDir = bundleContext.getDataFile("/"); 123 } 124 // environment may not be set by some bootstrappers (like tests) - we 125 // create it now if not yet created 126 Environment env = Environment.getDefault(); 127 if (env == null) { 128 Environment.setDefault(new Environment(workingDir)); 129 } 130 workingDir.mkdirs(); 131 persistence = new ComponentPersistence(this); 132 log.debug("Working directory: " + workingDir); 133 } 134 135 @Override 136 public String getName() { 137 return NAME; 138 } 139 140 @Override 141 public Version getVersion() { 142 return VERSION; 143 } 144 145 public BundleContext getBundleContext() { 146 return bundleContext; 147 } 148 149 @Override 150 public Bundle getBundle(String symbolicName) { 151 return bundles.get(symbolicName); 152 } 153 154 public Map<String, Bundle> getBundlesMap() { 155 return bundles; 156 } 157 158 public ComponentPersistence getComponentPersistence() { 159 return persistence; 160 } 161 162 public synchronized RuntimeContext createContext(Bundle bundle) { 163 RuntimeContext ctx = contexts.get(bundle.getSymbolicName()); 164 if (ctx == null) { 165 // workaround to handle fragment bundles 166 ctx = new OSGiRuntimeContext(bundle); 167 contexts.put(bundle.getSymbolicName(), ctx); 168 loadComponents(bundle, ctx); 169 } 170 return ctx; 171 } 172 173 public synchronized void destroyContext(Bundle bundle) { 174 RuntimeContext ctx = contexts.remove(bundle.getSymbolicName()); 175 if (ctx != null) { 176 ctx.destroy(); 177 } 178 } 179 180 public synchronized RuntimeContext getContext(Bundle bundle) { 181 return contexts.get(bundle.getSymbolicName()); 182 } 183 184 public synchronized RuntimeContext getContext(String symbolicName) { 185 return contexts.get(symbolicName); 186 } 187 188 @Override 189 protected void doStart() { 190 bundleContext.addFrameworkListener(this); 191 try { 192 loadConfig(); 193 } catch (IOException e) { 194 throw new RuntimeServiceException(e); 195 } 196 // load configuration if any 197 loadComponents(bundleContext.getBundle(), context); 198 } 199 200 @Override 201 protected void doStop() { 202 bundleContext.removeFrameworkListener(this); 203 try { 204 super.doStop(); 205 } finally { 206 context.destroy(); 207 } 208 } 209 210 protected void loadComponents(Bundle bundle, RuntimeContext ctx) { 211 String list = getComponentsList(bundle); 212 String name = bundle.getSymbolicName(); 213 log.debug("Bundle: " + name + " components: " + list); 214 if (list == null) { 215 return; 216 } 217 StringTokenizer tok = new StringTokenizer(list, ", \t\n\r\f"); 218 while (tok.hasMoreTokens()) { 219 String path = tok.nextToken(); 220 URL url = bundle.getEntry(path); 221 log.debug("Loading component for: " + name + " path: " + path + " url: " + url); 222 if (url != null) { 223 try { 224 ctx.deploy(url); 225 } catch (IOException e) { 226 // just log error to know where is the cause of the exception 227 log.error("Error deploying resource: " + url); 228 Framework.handleDevError(e); 229 throw new RuntimeServiceException("Cannot deploy: " + url, e); 230 } 231 } else { 232 String message = "Unknown component '" + path + "' referenced by bundle '" + name + "'"; 233 log.error(message + ". Check the MANIFEST.MF"); 234 Framework.handleDevError(null); 235 warnings.add(message); 236 } 237 } 238 } 239 240 public static String getComponentsList(Bundle bundle) { 241 return (String) bundle.getHeaders().get("Nuxeo-Component"); 242 } 243 244 protected boolean loadConfigurationFromProvider() throws IOException { 245 // TODO use a OSGi service for this. 246 Iterable<URL> provider = Environment.getDefault().getConfigurationProvider(); 247 if (provider == null) { 248 return false; 249 } 250 Iterator<URL> it = provider.iterator(); 251 ArrayList<URL> props = new ArrayList<>(); 252 ArrayList<URL> xmls = new ArrayList<>(); 253 while (it.hasNext()) { 254 URL url = it.next(); 255 String path = url.getPath(); 256 if (path.endsWith("-config.xml")) { 257 xmls.add(url); 258 } else if (path.endsWith(".properties")) { 259 props.add(url); 260 } 261 } 262 Comparator<URL> comp = new Comparator<URL>() { 263 @Override 264 public int compare(URL o1, URL o2) { 265 return o1.getPath().compareTo(o2.getPath()); 266 } 267 }; 268 Collections.sort(xmls, comp); 269 for (URL url : props) { 270 loadProperties(url); 271 } 272 for (URL url : xmls) { 273 context.deploy(url); 274 } 275 return true; 276 } 277 278 protected void loadConfig() throws IOException { 279 Environment env = Environment.getDefault(); 280 if (env != null) { 281 log.debug("Configuration: host application: " + env.getHostApplicationName()); 282 } else { 283 log.warn("Configuration: no host application"); 284 return; 285 } 286 287 File blacklistFile = new File(env.getConfig(), "blacklist"); 288 if (blacklistFile.isFile()) { 289 List<String> lines = FileUtils.readLines(blacklistFile); 290 Set<String> blacklist = new HashSet<>(); 291 for (String line : lines) { 292 line = line.trim(); 293 if (line.length() > 0) { 294 blacklist.add(line); 295 } 296 } 297 manager.setBlacklist(new HashSet<>(lines)); 298 } 299 300 if (loadConfigurationFromProvider()) { 301 return; 302 } 303 304 String configDir = bundleContext.getProperty(PROP_CONFIG_DIR); 305 if (configDir != null && configDir.contains(":/")) { // an url of a config file 306 log.debug("Configuration: " + configDir); 307 URL url = new URL(configDir); 308 log.debug("Configuration: loading properties url: " + configDir); 309 loadProperties(url); 310 return; 311 } 312 313 // TODO: in JBoss there is a deployer that will deploy nuxeo 314 // configuration files .. 315 boolean isNotJBoss4 = !isJBoss4(env); 316 317 File dir = env.getConfig(); 318 // File dir = new File(configDir); 319 String[] names = dir.list(); 320 if (names != null) { 321 Arrays.sort(names, new Comparator<String>() { 322 @Override 323 public int compare(String o1, String o2) { 324 return o1.compareToIgnoreCase(o2); 325 } 326 }); 327 printDeploymentOrderInfo(names); 328 for (String name : names) { 329 if (name.endsWith("-config.xml") || name.endsWith("-bundle.xml")) { 330 // TODO 331 // because of some dep bugs (regarding the deployment of 332 // demo-ds.xml) 333 // we cannot let the runtime deploy config dir at 334 // beginning... 335 // until fixing this we deploy config dir from 336 // NuxeoDeployer 337 if (isNotJBoss4) { 338 File file = new File(dir, name); 339 log.debug("Configuration: deploy config component: " + name); 340 try { 341 context.deploy(file.toURI().toURL()); 342 } catch (IOException e) { 343 throw new IllegalArgumentException("Cannot load config from " + file, e); 344 } 345 } 346 } else if (name.endsWith(".config") || name.endsWith(".ini") || name.endsWith(".properties")) { 347 File file = new File(dir, name); 348 log.debug("Configuration: loading properties: " + name); 349 loadProperties(file); 350 } else { 351 log.debug("Configuration: ignoring: " + name); 352 } 353 } 354 } else if (dir.isFile()) { // a file - load it 355 log.debug("Configuration: loading properties: " + dir); 356 loadProperties(dir); 357 } else { 358 log.debug("Configuration: no configuration file found"); 359 } 360 361 loadDefaultConfig(); 362 } 363 364 protected static void printDeploymentOrderInfo(String[] fileNames) { 365 if (log.isDebugEnabled()) { 366 StringBuilder buf = new StringBuilder(); 367 for (String fileName : fileNames) { 368 buf.append("\n\t" + fileName); 369 } 370 log.debug("Deployment order of configuration files: " + buf.toString()); 371 } 372 } 373 374 @Override 375 public void reloadProperties() throws IOException { 376 File dir = Environment.getDefault().getConfig(); 377 String[] names = dir.list(); 378 if (names != null) { 379 Arrays.sort(names, new Comparator<String>() { 380 @Override 381 public int compare(String o1, String o2) { 382 return o1.compareToIgnoreCase(o2); 383 } 384 }); 385 CryptoProperties props = new CryptoProperties(System.getProperties()); 386 for (String name : names) { 387 if (name.endsWith(".config") || name.endsWith(".ini") || name.endsWith(".properties")) { 388 FileInputStream in = new FileInputStream(new File(dir, name)); 389 try { 390 props.load(in); 391 } finally { 392 in.close(); 393 } 394 } 395 } 396 // replace the current runtime properties 397 properties = props; 398 } 399 } 400 401 /** 402 * Loads default properties. 403 * <p> 404 * Used for backward compatibility when adding new mandatory properties 405 * </p> 406 */ 407 protected void loadDefaultConfig() { 408 String varName = "org.nuxeo.ecm.contextPath"; 409 if (Framework.getProperty(varName) == null) { 410 properties.setProperty(varName, "/nuxeo"); 411 } 412 } 413 414 public void loadProperties(File file) throws IOException { 415 InputStream in = new BufferedInputStream(new FileInputStream(file)); 416 try { 417 loadProperties(in); 418 } finally { 419 in.close(); 420 } 421 } 422 423 public void loadProperties(URL url) throws IOException { 424 InputStream in = url.openStream(); 425 try { 426 loadProperties(in); 427 } finally { 428 if (in != null) { 429 in.close(); 430 } 431 } 432 } 433 434 public void loadProperties(InputStream in) throws IOException { 435 properties.load(in); 436 } 437 438 /** 439 * Overrides the default method to be able to include OSGi properties. 440 */ 441 @Override 442 public String getProperty(String name, String defValue) { 443 String value = properties.getProperty(name); 444 if (value == null) { 445 value = bundleContext.getProperty(name); 446 if (value == null) { 447 return defValue == null ? null : expandVars(defValue); 448 } 449 } 450 if (("${" + name + "}").equals(value)) { 451 // avoid loop, don't expand 452 return value; 453 } 454 return expandVars(value); 455 } 456 457 /** 458 * Overrides the default method to be able to include OSGi properties. 459 */ 460 @Override 461 public String expandVars(String expression) { 462 return new TextTemplate(getProperties()) { 463 @Override 464 public String getVariable(String name) { 465 String value = super.getVariable(name); 466 if (value == null) { 467 value = bundleContext.getProperty(name); 468 } 469 return value; 470 } 471 472 }.processText(expression); 473 } 474 475 protected void notifyComponentsOnStarted() { 476 List<RegistrationInfo> ris = new ArrayList<>(manager.getRegistrations()); 477 Collections.sort(ris, new RIApplicationStartedComparator()); 478 for (RegistrationInfo ri : ris) { 479 try { 480 ri.notifyApplicationStarted(); 481 } catch (RuntimeException e) { 482 log.error("Failed to notify component '" + ri.getName() + "' on application started", e); 483 } 484 } 485 } 486 487 protected static class RIApplicationStartedComparator implements Comparator<RegistrationInfo> { 488 @Override 489 public int compare(RegistrationInfo r1, RegistrationInfo r2) { 490 int cmp = Integer.compare(r1.getApplicationStartedOrder(), r2.getApplicationStartedOrder()); 491 if (cmp == 0) { 492 // fallback on name order, to be deterministic 493 cmp = r1.getName().getName().compareTo(r2.getName().getName()); 494 } 495 return cmp; 496 } 497 } 498 499 public void fireApplicationStarted() { 500 synchronized (this) { 501 if (appStarted) { 502 return; 503 } 504 appStarted = true; 505 } 506 try { 507 persistence.loadPersistedComponents(); 508 } catch (RuntimeException | IOException e) { 509 log.error("Failed to load persisted components", e); 510 } 511 // deploy a fake component that is marking the end of startup 512 // XML components that needs to be deployed at the end need to put a 513 // requirement 514 // on this marker component 515 deployFrameworkStartedComponent(); 516 notifyComponentsOnStarted(); 517 // print the startup message 518 printStatusMessage(); 519 } 520 521 /* --------------- FrameworkListener API ------------------ */ 522 523 @Override 524 public void frameworkEvent(FrameworkEvent event) { 525 if (event.getType() == FrameworkEvent.STARTED) { 526 fireApplicationStarted(); 527 } 528 } 529 530 private void printStatusMessage() { 531 StringBuilder msg = new StringBuilder(); 532 msg.append("Nuxeo Platform Started\n"); 533 if (getStatusMessage(msg)) { 534 log.info(msg); 535 } else { 536 log.error(msg); 537 } 538 } 539 540 protected void deployFrameworkStartedComponent() { 541 RegistrationInfoImpl ri = new RegistrationInfoImpl(FRAMEWORK_STARTED_COMP); 542 ri.setContext(context); 543 // this will register any pending components that waits for the 544 // framework to be started 545 manager.register(ri); 546 } 547 548 public Bundle findHostBundle(Bundle bundle) { 549 String hostId = (String) bundle.getHeaders().get(Constants.FRAGMENT_HOST); 550 log.debug("Looking for host bundle: " + bundle.getSymbolicName() + " host id: " + hostId); 551 if (hostId != null) { 552 int p = hostId.indexOf(';'); 553 if (p > -1) { // remove version or other extra information if any 554 hostId = hostId.substring(0, p); 555 } 556 RuntimeContext ctx = contexts.get(hostId); 557 if (ctx != null) { 558 log.debug("Context was found for host id: " + hostId); 559 return ctx.getBundle(); 560 } else { 561 log.warn("No context found for host id: " + hostId); 562 563 } 564 } 565 return null; 566 } 567 568 protected File getEclipseBundleFileUsingReflection(Bundle bundle) { 569 try { 570 Object proxy = bundle.getClass().getMethod("getLoaderProxy").invoke(bundle); 571 Object loader = proxy.getClass().getMethod("getBundleLoader").invoke(proxy); 572 URL root = (URL) loader.getClass().getMethod("findResource", String.class).invoke(loader, "/"); 573 Field field = root.getClass().getDeclaredField("handler"); 574 field.setAccessible(true); 575 Object handler = field.get(root); 576 Field entryField = handler.getClass().getSuperclass().getDeclaredField("bundleEntry"); 577 entryField.setAccessible(true); 578 Object entry = entryField.get(handler); 579 Field fileField = entry.getClass().getDeclaredField("file"); 580 fileField.setAccessible(true); 581 return (File) fileField.get(entry); 582 } catch (ReflectiveOperationException e) { 583 log.error("Cannot access to eclipse bundle system files of " + bundle.getSymbolicName()); 584 return null; 585 } 586 } 587 588 @Override 589 public File getBundleFile(Bundle bundle) { 590 File file; 591 String location = bundle.getLocation(); 592 String vendor = Framework.getProperty(Constants.FRAMEWORK_VENDOR); 593 String name = bundle.getSymbolicName(); 594 595 if ("Eclipse".equals(vendor)) { // equinox framework 596 log.debug("getBundleFile (Eclipse): " + name + "->" + location); 597 return getEclipseBundleFileUsingReflection(bundle); 598 } else if (location.startsWith("file:")) { // nuxeo osgi adapter 599 try { 600 file = FileUtils.urlToFile(location); 601 } catch (MalformedURLException e) { 602 log.error("getBundleFile: Unable to create " + " for bundle: " + name + " as URI: " + location); 603 return null; 604 } 605 } else { // may be a file path - this happens when using 606 // JarFileBundle (for ex. in nxshell) 607 file = new File(location); 608 } 609 if (file != null && file.exists()) { 610 log.debug("getBundleFile: " + name + " bound to file: " + file); 611 return file; 612 } else { 613 log.debug("getBundleFile: " + name + " cannot bind to nonexistent file: " + file); 614 return null; 615 } 616 } 617 618 public static final boolean isJBoss4(Environment env) { 619 if (env == null) { 620 return false; 621 } 622 String hn = env.getHostApplicationName(); 623 String hv = env.getHostApplicationVersion(); 624 if (hn == null || hv == null) { 625 return false; 626 } 627 return "JBoss".equals(hn) && hv.startsWith("4"); 628 } 629 630}