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