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