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