001/* 002 * (C) Copyright 2006-2020 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 * Anahide Tchertchian 021 */ 022package org.nuxeo.runtime.osgi; 023 024import static java.nio.charset.StandardCharsets.UTF_8; 025 026import java.io.BufferedInputStream; 027import java.io.File; 028import java.io.FileInputStream; 029import java.io.IOException; 030import java.io.InputStream; 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.RuntimeMessage; 053import org.nuxeo.runtime.RuntimeMessage.Level; 054import org.nuxeo.runtime.RuntimeMessage.Source; 055import org.nuxeo.runtime.Version; 056import org.nuxeo.runtime.api.Framework; 057import org.nuxeo.runtime.model.ComponentName; 058import org.nuxeo.runtime.model.RuntimeContext; 059import org.nuxeo.runtime.model.impl.ComponentPersistence; 060import org.nuxeo.runtime.model.impl.RegistrationInfoImpl; 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 Logger log = LogManager.getLogger(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, true); 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 loadComponents(bundleContext.getBundle(), context, false); 201 } 202 203 @Override 204 protected void doStop() { 205 // do not destroy context since component manager is already shutdown 206 bundleContext.removeFrameworkListener(this); 207 super.doStop(); 208 } 209 210 protected void loadComponents(Bundle bundle, RuntimeContext ctx, boolean isFragment) { 211 String name = bundle.getSymbolicName(); 212 if (isFragment && name.equals(context.getBundle().getSymbolicName())) { 213 // avoid deploying again runtime components as a fragment (already handled in #doStart) 214 return; 215 } 216 String list = getComponentsList(bundle); 217 if (list == null) { 218 log.debug("Bundle {} doesn't have components", name); 219 return; 220 } 221 log.trace("Load Bundle: {} / Components: {}", name, list); 222 StringTokenizer tok = new StringTokenizer(list, ", \t\n\r\f"); 223 while (tok.hasMoreTokens()) { 224 String path = tok.nextToken(); 225 URL url = bundle.getEntry(path); 226 if (url != null) { 227 log.trace("Load component {} [{}]", name, url); 228 try { 229 ctx.deploy(url); 230 } catch (IOException e) { 231 log.error(e, e); 232 messageHandler.addMessage(new RuntimeMessage(Level.ERROR, e.getMessage(), Source.BUNDLE, name)); 233 } 234 } else { 235 String message = "Unknown component '" + path + "' referenced by bundle '" + name + "'"; 236 log.error(message + ". Check the MANIFEST.MF"); 237 messageHandler.addMessage(new RuntimeMessage(Level.ERROR, message, Source.BUNDLE, name)); 238 } 239 } 240 } 241 242 public static String getComponentsList(Bundle bundle) { 243 return (String) bundle.getHeaders().get("Nuxeo-Component"); 244 } 245 246 protected boolean loadConfigurationFromProvider() throws IOException { 247 // TODO use a OSGi service for this. 248 Iterable<URL> provider = Environment.getDefault().getConfigurationProvider(); 249 if (provider == null) { 250 return false; 251 } 252 Iterator<URL> it = provider.iterator(); 253 List<URL> props = new ArrayList<>(); 254 List<URL> xmls = new ArrayList<>(); 255 while (it.hasNext()) { 256 URL url = it.next(); 257 String path = url.getPath(); 258 if (path.endsWith("-config.xml")) { 259 xmls.add(url); 260 } else if (path.endsWith(".properties")) { 261 props.add(url); 262 } 263 } 264 xmls.sort(Comparator.comparing(URL::getPath)); 265 for (URL url : props) { 266 loadProperties(url); 267 } 268 for (URL url : xmls) { 269 context.deploy(url); 270 } 271 return true; 272 } 273 274 @Override 275 protected void loadConfig() throws IOException { 276 Environment env = Environment.getDefault(); 277 if (env != null) { 278 log.debug("Configuration: host application: {}", env.getHostApplicationName()); 279 } else { 280 log.warn("Configuration: no host application"); 281 return; 282 } 283 284 File blacklistFile = new File(env.getConfig(), "blacklist"); 285 if (blacklistFile.isFile()) { 286 Set<String> lines = FileUtils.readLines(blacklistFile, UTF_8) 287 .stream() 288 .map(String::trim) 289 .filter(line -> !line.isEmpty()) 290 .collect(Collectors.toSet()); 291 manager.setBlacklist(lines); 292 } 293 294 if (loadConfigurationFromProvider()) { 295 return; 296 } 297 298 String configDir = bundleContext.getProperty(PROP_CONFIG_DIR); 299 if (configDir != null && configDir.contains(":/")) { // an url of a config file 300 URL url = new URL(configDir); 301 log.debug("Configuration: loading properties from: {}", configDir); 302 loadProperties(url); 303 return; 304 } 305 306 // TODO: in JBoss there is a deployer that will deploy nuxeo 307 // configuration files .. 308 boolean isNotJBoss4 = !isJBoss4(env); 309 310 File dir = env.getConfig(); 311 String[] names = dir.list(); 312 if (names != null) { 313 Arrays.sort(names, String::compareToIgnoreCase); 314 log.debug("Deployment order of configuration files: {}", 315 () -> Stream.of(names).reduce((n1, n2) -> n1 + "\n\t" + n2).map(n -> "\n\t" + n).orElse("")); 316 for (String name : names) { 317 if (name.endsWith("-config.xml") || name.endsWith("-bundle.xml")) { 318 // TODO because of some dep bugs (regarding the deployment of demo-ds.xml), we cannot let the 319 // runtime deploy config dir at beginning... 320 // until fixing this we deploy config dir from NuxeoDeployer 321 if (isNotJBoss4) { 322 File file = new File(dir, name); 323 log.trace("Configuration: deploy config component: {}", name); 324 try { 325 context.deploy(file.toURI().toURL()); 326 } catch (IOException e) { 327 String message = String.format("Error deploying config %s (%s)", name, e.getMessage()); 328 log.error(message, e); 329 messageHandler.addMessage(new RuntimeMessage(Level.ERROR, message, Source.CONFIG, name)); 330 } 331 } 332 } else if (name.endsWith(".config") || name.endsWith(".ini") || name.endsWith(".properties")) { 333 File file = new File(dir, name); 334 log.trace("Configuration: loading properties: {}", name); 335 loadProperties(file); 336 } else { 337 log.trace("Configuration: ignoring: {}", name); 338 } 339 } 340 } else if (dir.isFile()) { // a file - load it 341 log.debug("Configuration: loading properties: {}", dir); 342 loadProperties(dir); 343 } else { 344 log.debug("Configuration: no configuration file found"); 345 } 346 347 loadDefaultConfig(); 348 } 349 350 @Override 351 public void reloadProperties() throws IOException { 352 File dir = Environment.getDefault().getConfig(); 353 String[] names = dir.list(); 354 if (names != null) { 355 Arrays.sort(names, String::compareToIgnoreCase); 356 CryptoProperties props = new CryptoProperties(System.getProperties()); 357 for (String name : names) { 358 if (name.endsWith(".config") || name.endsWith(".ini") || name.endsWith(".properties")) { 359 try (FileInputStream in = new FileInputStream(new File(dir, name))) { 360 props.load(in); 361 } 362 } 363 } 364 // replace the current runtime properties 365 properties = props; 366 } 367 } 368 369 /** 370 * Loads default properties. 371 * <p> 372 * Used for backward compatibility when adding new mandatory properties 373 * </p> 374 */ 375 protected void loadDefaultConfig() { 376 String varName = "org.nuxeo.ecm.contextPath"; 377 if (Framework.getProperty(varName) == null) { 378 properties.setProperty(varName, "/nuxeo"); 379 } 380 } 381 382 public void loadProperties(File file) throws IOException { 383 try (InputStream in = new BufferedInputStream(new FileInputStream(file))) { 384 loadProperties(in); 385 } 386 } 387 388 public void loadProperties(URL url) throws IOException { 389 try (InputStream in = url.openStream()) { 390 loadProperties(in); 391 } 392 } 393 394 public void loadProperties(InputStream in) throws IOException { 395 properties.load(in); 396 } 397 398 /** 399 * Overrides the default method to be able to include OSGi properties. 400 */ 401 @Override 402 public String getProperty(String name, String defValue) { 403 String value = properties.getProperty(name); 404 if (value == null) { 405 value = bundleContext.getProperty(name); 406 if (value == null) { 407 return defValue == null ? null : expandVars(defValue); 408 } 409 } 410 if (("${" + name + "}").equals(value)) { 411 // avoid loop, don't expand 412 return value; 413 } 414 return expandVars(value); 415 } 416 417 /** 418 * Overrides the default method to be able to include OSGi properties. 419 */ 420 @Override 421 public String expandVars(String expression) { 422 return new TextTemplate(getProperties()) { 423 @Override 424 public String getVariable(String name) { 425 String value = super.getVariable(name); 426 if (value == null) { 427 value = bundleContext.getProperty(name); 428 } 429 return value; 430 } 431 432 }.processText(expression); 433 } 434 435 protected void startComponents() { 436 synchronized (this) { 437 if (appStarted) { 438 return; 439 } 440 appStarted = true; 441 } 442 try { 443 persistence.loadPersistedComponents(); 444 } catch (RuntimeException | IOException e) { 445 log.error("Failed to load persisted components", e); 446 } 447 // deploy a fake component that is marking the end of startup 448 // XML components that needs to be deployed at the end need to put a 449 // requirement 450 // on this marker component 451 deployFrameworkStartedComponent(); 452 // ============ activate and start components ======= 453 manager.start(); 454 // create a snapshot of the started components - TODO should this be optional? 455 manager.snapshot(); 456 // ================================================== 457 // print the startup message 458 printStatusMessage(); 459 } 460 461 /* --------------- FrameworkListener API ------------------ */ 462 463 @Override 464 public void frameworkEvent(FrameworkEvent event) { 465 if (event.getType() != FrameworkEvent.STARTED) { 466 return; 467 } 468 startComponents(); 469 } 470 471 private void printStatusMessage() { 472 StringBuilder msg = new StringBuilder(); 473 msg.append("Nuxeo Platform Started\n"); 474 if (getStatusMessage(msg)) { 475 log.info(msg); 476 } else { 477 log.error(msg); 478 if (Boolean.getBoolean("nuxeo.start.strict")) { 479 throw new IllegalStateException("Startup aborted due to previous failures (strict mode)"); 480 } 481 } 482 } 483 484 protected void deployFrameworkStartedComponent() { 485 RegistrationInfoImpl ri = new RegistrationInfoImpl(FRAMEWORK_STARTED_COMP); 486 ri.setContext(context); 487 // this will register any pending components that waits for the 488 // framework to be started 489 manager.register(ri); 490 } 491 492 public Bundle findHostBundle(Bundle bundle) { 493 String hostId = (String) bundle.getHeaders().get(Constants.FRAGMENT_HOST); 494 log.debug("Looking for host bundle: {} host id: {}", bundle.getSymbolicName(), hostId); 495 if (hostId != null) { 496 int p = hostId.indexOf(';'); 497 if (p > -1) { // remove version or other extra information if any 498 hostId = hostId.substring(0, p); 499 } 500 RuntimeContext ctx = contexts.get(hostId); 501 if (ctx != null) { 502 log.debug("Context was found for host id: {}", hostId); 503 return ctx.getBundle(); 504 } else { 505 log.warn("No context found for host id: {}", hostId); 506 507 } 508 } 509 return null; 510 } 511 512 @Override 513 public File getBundleFile(Bundle bundle) { 514 File file; 515 String location = bundle.getLocation(); 516 String name = bundle.getSymbolicName(); 517 518 if (location.startsWith("file:")) { // nuxeo osgi adapter 519 try { 520 file = org.nuxeo.common.utils.FileUtils.urlToFile(location); 521 } catch (MalformedURLException e) { 522 log.error("getBundleFile: Unable to create file for bundle name: {} as URI: {}", name, location); 523 return null; 524 } 525 } else { // may be a file path - this happens when using 526 // JarFileBundle (for ex. in nxshell) 527 file = new File(location); 528 } 529 if (file.exists()) { 530 log.debug("getBundleFile: {} bound to file: {}", name, file); 531 return file; 532 } else { 533 log.debug("getBundleFile: {} cannot bind to nonexistent file: {}", name, file); 534 return null; 535 } 536 } 537 538 public static boolean isJBoss4(Environment env) { 539 if (env == null) { 540 return false; 541 } 542 String hn = env.getHostApplicationName(); 543 String hv = env.getHostApplicationVersion(); 544 if (hn == null || hv == null) { 545 return false; 546 } 547 return "JBoss".equals(hn) && hv.startsWith("4"); 548 } 549 550}