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