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