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 * Nuxeo - initial API and implementation 018 * Kevin Leturc <kleturc@nuxeo.com> 019 */ 020package org.nuxeo.runtime.api; 021 022import java.io.File; 023import java.io.IOException; 024import java.net.MalformedURLException; 025import java.net.URL; 026import java.nio.file.Files; 027import java.nio.file.Path; 028import java.nio.file.attribute.FileAttribute; 029import java.util.List; 030import java.util.Properties; 031import java.util.function.Supplier; 032 033import javax.security.auth.callback.CallbackHandler; 034import javax.security.auth.login.LoginContext; 035import javax.security.auth.login.LoginException; 036 037import org.apache.commons.io.FileDeleteStrategy; 038import org.apache.commons.lang3.StringUtils; 039import org.apache.commons.logging.Log; 040import org.apache.commons.logging.LogFactory; 041import org.nuxeo.common.Environment; 042import org.nuxeo.common.collections.ListenerList; 043import org.nuxeo.runtime.RuntimeService; 044import org.nuxeo.runtime.RuntimeServiceEvent; 045import org.nuxeo.runtime.RuntimeServiceException; 046import org.nuxeo.runtime.RuntimeServiceListener; 047import org.nuxeo.runtime.api.login.LoginAs; 048import org.nuxeo.runtime.api.login.LoginService; 049import org.nuxeo.runtime.trackers.files.FileEvent; 050import org.nuxeo.runtime.trackers.files.FileEventTracker; 051 052/** 053 * This class is the main entry point to a Nuxeo runtime application. 054 * <p> 055 * It offers an easy way to create new sessions, to access system services and other resources. 056 * <p> 057 * There are two type of services: 058 * <ul> 059 * <li>Global Services - these services are uniquely defined by a service class, and there is an unique instance of the 060 * service in the system per class. 061 * <li>Local Services - these services are defined by a class and an URI. This type of service allows multiple service 062 * instances for the same class of services. Each instance is uniquely defined in the system by an URI. 063 * </ul> 064 * 065 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 066 */ 067public final class Framework { 068 069 private static final Log log = LogFactory.getLog(Framework.class); 070 071 private static Boolean testModeSet; 072 073 /** 074 * Global dev property 075 * 076 * @since 5.6 077 * @see #isDevModeSet() 078 */ 079 public static final String NUXEO_DEV_SYSTEM_PROP = "org.nuxeo.dev"; 080 081 /** 082 * Global testing property 083 * 084 * @since 5.6 085 * @see #isTestModeSet() 086 */ 087 public static final String NUXEO_TESTING_SYSTEM_PROP = "org.nuxeo.runtime.testing"; 088 089 /** 090 * Property to control strict runtime mode 091 * 092 * @since 5.6 093 * @see #handleDevError(Throwable) 094 * @deprecated since 9.1 This property is not documented and doesn't work. 095 */ 096 @Deprecated 097 public static final String NUXEO_STRICT_RUNTIME_SYSTEM_PROP = "org.nuxeo.runtime.strict"; 098 099 /** 100 * The runtime instance. 101 */ 102 private static RuntimeService runtime; 103 104 private static final ListenerList listeners = new ListenerList(); 105 106 /** 107 * A class loader used to share resources between all bundles. 108 * <p> 109 * This is useful to put resources outside any bundle (in a directory on the file system) and then refer them from 110 * XML contributions. 111 * <p> 112 * The resource directory used by this loader is ${nuxeo_data_dir}/resources whee ${nuxeo_data_dir} is usually 113 * ${nuxeo_home}/data 114 */ 115 protected static SharedResourceLoader resourceLoader; 116 117 /** 118 * Whether or not services should be exported as OSGI services. This is controlled by the ${ecr.osgi.services} 119 * property. The default is false. 120 */ 121 protected static Boolean isOSGiServiceSupported; 122 123 // Utility class. 124 private Framework() { 125 } 126 127 public static void initialize(RuntimeService runtimeService) { 128 if (runtime != null) { 129 throw new RuntimeServiceException("Nuxeo Framework was already initialized"); 130 } 131 runtime = runtimeService; 132 reloadResourceLoader(); 133 runtime.start(); 134 } 135 136 public static void reloadResourceLoader() { 137 File rs = new File(Environment.getDefault().getData(), "resources"); 138 rs.mkdirs(); 139 URL url; 140 try { 141 url = rs.toURI().toURL(); 142 } catch (MalformedURLException e) { 143 throw new RuntimeServiceException(e); 144 } 145 resourceLoader = new SharedResourceLoader(new URL[] { url }, Framework.class.getClassLoader()); 146 } 147 148 /** 149 * Reload the resources loader, keeping URLs already tracked, and adding possibility to add or remove some URLs. 150 * <p> 151 * Useful for hot reload of jars. 152 * 153 * @since 5.6 154 */ 155 public static void reloadResourceLoader(List<URL> urlsToAdd, List<URL> urlsToRemove) { 156 File rs = new File(Environment.getDefault().getData(), "resources"); 157 rs.mkdirs(); 158 URL[] existing = null; 159 if (resourceLoader != null) { 160 existing = resourceLoader.getURLs(); 161 } 162 // reinit 163 URL url; 164 try { 165 url = rs.toURI().toURL(); 166 } catch (MalformedURLException e) { 167 throw new RuntimeException(e); 168 } 169 SharedResourceLoader loader = new SharedResourceLoader(new URL[] { url }, Framework.class.getClassLoader()); 170 // add back existing urls unless they should be removed, and add new 171 // urls 172 if (existing != null) { 173 for (URL oldURL : existing) { 174 if (urlsToRemove == null || !urlsToRemove.contains(oldURL)) { 175 loader.addURL(oldURL); 176 } 177 } 178 } 179 if (urlsToAdd != null) { 180 for (URL newURL : urlsToAdd) { 181 loader.addURL(newURL); 182 } 183 } 184 resourceLoader = loader; 185 } 186 187 public static void shutdown() throws InterruptedException { 188 if (runtime == null) { 189 throw new IllegalStateException("runtime not exist"); 190 } 191 try { 192 runtime.stop(); 193 } finally { 194 runtime = null; 195 } 196 } 197 198 /** 199 * Tests whether or not the runtime was initialized. 200 * 201 * @return true if the runtime was initialized, false otherwise 202 */ 203 public static synchronized boolean isInitialized() { 204 return runtime != null; 205 } 206 207 public static SharedResourceLoader getResourceLoader() { 208 return resourceLoader; 209 } 210 211 /** 212 * Gets the runtime service instance. 213 * 214 * @return the runtime service instance 215 */ 216 public static RuntimeService getRuntime() { 217 return runtime; 218 } 219 220 /** 221 * Gets a service given its class. 222 */ 223 public static <T> T getService(Class<T> serviceClass) { 224 ServiceProvider provider = DefaultServiceProvider.getProvider(); 225 if (provider != null) { 226 return provider.getService(serviceClass); 227 } 228 checkRuntimeInitialized(); 229 // TODO impl a runtime service provider 230 return runtime.getService(serviceClass); 231 } 232 233 /** 234 * Gets a service given its class. 235 * 236 * @deprecated since 9.10, use {@link #getService} instead 237 */ 238 @Deprecated 239 public static <T> T getLocalService(Class<T> serviceClass) { 240 return getService(serviceClass); 241 } 242 243 /** 244 * Lookup a registered object given its key. 245 */ 246 public static Object lookup(String key) { 247 return null; // TODO 248 } 249 250 /** 251 * Runs the given {@link Runnable} while logged in as a system user. 252 * 253 * @param runnable what to run 254 * @since 8.4 255 */ 256 public static void doPrivileged(Runnable runnable) { 257 try { 258 LoginContext loginContext = login(); 259 try { 260 runnable.run(); 261 } finally { 262 if (loginContext != null) { // may be null in tests 263 loginContext.logout(); 264 } 265 } 266 } catch (LoginException e) { 267 throw new RuntimeException(e); 268 } 269 } 270 271 /** 272 * Calls the given {@link Supplier} while logged in as a system user and returns its result. 273 * 274 * @param supplier what to call 275 * @return the supplier's result 276 * @since 8.4 277 */ 278 public static <T> T doPrivileged(Supplier<T> supplier) { 279 try { 280 LoginContext loginContext = login(); 281 try { 282 return supplier.get(); 283 } finally { 284 if (loginContext != null) { // may be null in tests 285 loginContext.logout(); 286 } 287 } 288 } catch (LoginException e) { 289 throw new RuntimeException(e); 290 } 291 } 292 293 /** 294 * Login in the system as the system user (a pseudo-user having all privileges). 295 * 296 * @return the login session if successful. Never returns null. 297 * @throws LoginException on login failure 298 */ 299 public static LoginContext login() throws LoginException { 300 checkRuntimeInitialized(); 301 LoginService loginService = runtime.getService(LoginService.class); 302 if (loginService != null) { 303 return loginService.login(); 304 } 305 return null; 306 } 307 308 /** 309 * Login in the system as the system user (a pseudo-user having all privileges). The given username will be used to 310 * identify the user id that called this method. 311 * 312 * @param username the originating user id 313 * @return the login session if successful. Never returns null. 314 * @throws LoginException on login failure 315 */ 316 public static LoginContext loginAs(String username) throws LoginException { 317 checkRuntimeInitialized(); 318 LoginService loginService = runtime.getService(LoginService.class); 319 if (loginService != null) { 320 return loginService.loginAs(username); 321 } 322 return null; 323 } 324 325 /** 326 * Login in the system as the given user without checking the password. 327 * 328 * @param username the user name to login as. 329 * @return the login context 330 * @throws LoginException if any error occurs 331 * @since 5.4.2 332 */ 333 public static LoginContext loginAsUser(String username) throws LoginException { 334 return getService(LoginAs.class).loginAs(username); 335 } 336 337 /** 338 * Login in the system as the given user using the given password. 339 * 340 * @param username the username to login 341 * @param password the password 342 * @return a login session if login was successful. Never returns null. 343 * @throws LoginException if login failed 344 */ 345 public static LoginContext login(String username, Object password) throws LoginException { 346 checkRuntimeInitialized(); 347 LoginService loginService = runtime.getService(LoginService.class); 348 if (loginService != null) { 349 return loginService.login(username, password); 350 } 351 return null; 352 } 353 354 /** 355 * Login in the system using the given callback handler for login info resolution. 356 * 357 * @param cbHandler used to fetch the login info 358 * @return the login context 359 * @throws LoginException 360 */ 361 public static LoginContext login(CallbackHandler cbHandler) throws LoginException { 362 checkRuntimeInitialized(); 363 LoginService loginService = runtime.getService(LoginService.class); 364 if (loginService != null) { 365 return loginService.login(cbHandler); 366 } 367 return null; 368 } 369 370 public static void sendEvent(RuntimeServiceEvent event) { 371 Object[] listenersArray = listeners.getListeners(); 372 for (Object listener : listenersArray) { 373 ((RuntimeServiceListener) listener).handleEvent(event); 374 } 375 } 376 377 /** 378 * Registers a listener to be notified about runtime events. 379 * <p> 380 * If the listener is already registered, do nothing. 381 * 382 * @param listener the listener to register 383 */ 384 public static void addListener(RuntimeServiceListener listener) { 385 listeners.add(listener); 386 } 387 388 /** 389 * Removes the given listener. 390 * <p> 391 * If the listener is not registered, do nothing. 392 * 393 * @param listener the listener to remove 394 */ 395 public static void removeListener(RuntimeServiceListener listener) { 396 listeners.remove(listener); 397 } 398 399 /** 400 * Gets the given property value if any, otherwise null. 401 * <p> 402 * The framework properties will be searched first then if any matching property is found the system properties are 403 * searched too. 404 * 405 * @param key the property key 406 * @return the property value if any or null otherwise 407 */ 408 public static String getProperty(String key) { 409 return getProperty(key, null); 410 } 411 412 /** 413 * Gets the given property value if any, otherwise returns the given default value. 414 * <p> 415 * The framework properties will be searched first then if any matching property is found the system properties are 416 * searched too. 417 * 418 * @param key the property key 419 * @param defValue the default value to use 420 * @return the property value if any otherwise the default value 421 */ 422 public static String getProperty(String key, String defValue) { 423 checkRuntimeInitialized(); 424 return runtime.getProperty(key, defValue); 425 } 426 427 /** 428 * Gets all the framework properties. The system properties are not included in the returned map. 429 * 430 * @return the framework properties map. Never returns null. 431 */ 432 public static Properties getProperties() { 433 checkRuntimeInitialized(); 434 return runtime.getProperties(); 435 } 436 437 /** 438 * Expands any variable found in the given expression with the value of the corresponding framework property. 439 * <p> 440 * The variable format is ${property_key}. 441 * <p> 442 * System properties are also expanded. 443 */ 444 public static String expandVars(String expression) { 445 checkRuntimeInitialized(); 446 return runtime.expandVars(expression); 447 } 448 449 public static boolean isOSGiServiceSupported() { 450 if (isOSGiServiceSupported == null) { 451 isOSGiServiceSupported = Boolean.valueOf(isBooleanPropertyTrue("ecr.osgi.services")); 452 } 453 return isOSGiServiceSupported.booleanValue(); 454 } 455 456 /** 457 * Returns true if dev mode is set. 458 * <p> 459 * Activating this mode, some of the code may not behave as it would in production, to ease up debugging and working 460 * on developing the application. 461 * <p> 462 * For instance, it'll enable hot-reload if some packages are installed while the framework is running. It will also 463 * reset some caches when that happens. 464 */ 465 public static boolean isDevModeSet() { 466 return isBooleanPropertyTrue(NUXEO_DEV_SYSTEM_PROP); 467 } 468 469 /** 470 * Returns true if test mode is set. 471 * <p> 472 * Activating this mode, some of the code may not behave as it would in production, to ease up testing. 473 */ 474 public static boolean isTestModeSet() { 475 if (testModeSet == null) { 476 testModeSet = isBooleanPropertyTrue(NUXEO_TESTING_SYSTEM_PROP); 477 } 478 return testModeSet; 479 } 480 481 /** 482 * Returns true if given property is false when compared to a boolean value. Returns false if given property in 483 * unset. 484 * <p> 485 * Checks for the system properties if property is not found in the runtime properties. 486 * 487 * @since 5.8 488 */ 489 public static boolean isBooleanPropertyFalse(String propName) { 490 String v = getProperty(propName); 491 if (v == null) { 492 v = System.getProperty(propName); 493 } 494 if (StringUtils.isBlank(v)) { 495 return false; 496 } 497 return !Boolean.parseBoolean(v); 498 } 499 500 /** 501 * Returns true if given property is true when compared to a boolean value. 502 * <p> 503 * Checks for the system properties if property is not found in the runtime properties. 504 * 505 * @since 5.6 506 */ 507 public static boolean isBooleanPropertyTrue(String propName) { 508 String v = getProperty(propName); 509 if (v == null) { 510 v = System.getProperty(propName); 511 } 512 return Boolean.parseBoolean(v); 513 } 514 515 /** 516 * Since 5.6, this method stops the application if property {@link #NUXEO_STRICT_RUNTIME_SYSTEM_PROP} is set to 517 * true, and one of the following errors occurred during startup. 518 * <ul> 519 * <li>Component XML parse error. 520 * <li>Contribution to an unknown extension point. 521 * <li>Component with an unknown implementation class (the implementation entry exists in the XML descriptor but 522 * cannot be resolved to a class). 523 * <li>Uncatched exception on extension registration / unregistration (either in framework or user component code) 524 * <li>Uncatched exception on component activation / deactivation (either in framework or user component code) 525 * <li>Broken Nuxeo-Component MANIFEST entry. (i.e. the entry cannot be resolved to a resource) 526 * </ul> 527 * <p> 528 * Before 5.6, this method stopped the application if development mode was enabled (i.e. org.nuxeo.dev system 529 * property is set) but this is not the case anymore to handle a dev mode that does not stop the runtime framework 530 * when using hot reload. 531 * 532 * @param t the exception or null if none 533 * @deprecated since 9.1 DON'T USE THIS METHOD ANYMORE, its behavior is not documented. It also seems to not work. 534 * If you want to stop server startup add error messages to {@link RuntimeService#getMessageHandler()}. 535 */ 536 @Deprecated 537 public static void handleDevError(Throwable t) { 538 if (isBooleanPropertyTrue(NUXEO_STRICT_RUNTIME_SYSTEM_PROP)) { 539 System.err.println("Fatal error caught in strict runtime mode => exiting."); 540 if (t != null) { 541 t.printStackTrace(); 542 } 543 System.exit(1); 544 } else if (t != null) { 545 log.error(t, t); 546 } 547 } 548 549 /** 550 * @see FileEventTracker 551 * @param aFile The file to delete 552 * @param aMarker the marker Object 553 */ 554 public static void trackFile(File aFile, Object aMarker) { 555 FileEvent.onFile(Framework.class, aFile, aMarker).send(); 556 } 557 558 /** 559 * Strategy is not customizable anymore. 560 * 561 * @deprecated 562 * @since 6.0 563 * @see #trackFile(File, Object) 564 * @see org.nuxeo.runtime.trackers.files.FileEventTracker.SafeFileDeleteStrategy 565 * @param file The file to delete 566 * @param marker the marker Object 567 * @param fileDeleteStrategy ignored deprecated parameter 568 */ 569 @Deprecated 570 public static void trackFile(File file, Object marker, FileDeleteStrategy fileDeleteStrategy) { 571 trackFile(file, marker); 572 } 573 574 /** 575 * @since 6.0 576 */ 577 protected static void checkRuntimeInitialized() { 578 if (runtime == null) { 579 throw new IllegalStateException("Runtime not initialized"); 580 } 581 } 582 583 /** 584 * Creates an empty file in the framework temporary-file directory ({@code nuxeo.tmp.dir} vs {@code java.io.tmpdir} 585 * ), using the given prefix and suffix to generate its name. 586 * <p> 587 * Invoking this method is equivalent to invoking 588 * <code>{@link File#createTempFile(java.lang.String, java.lang.String, java.io.File) 589 * File.createTempFile(prefix, suffix, Environment.getDefault().getTemp())}</code>. 590 * <p> 591 * The {@link #createTempFilePath(String, String, FileAttribute...)} method provides an alternative method to create 592 * an empty file in the framework temporary-file directory. Files created by that method may have more restrictive 593 * access permissions to files created by this method and so may be more suited to security-sensitive applications. 594 * 595 * @param prefix The prefix string to be used in generating the file's name; must be at least three characters long 596 * @param suffix The suffix string to be used in generating the file's name; may be <code>null</code>, in which case 597 * the suffix <code>".tmp"</code> will be used 598 * @return An abstract pathname denoting a newly-created empty file 599 * @throws IllegalArgumentException If the <code>prefix</code> argument contains fewer than three characters 600 * @throws IOException If a file could not be created 601 * @throws SecurityException If a security manager exists and its <code> 602 * {@link java.lang.SecurityManager#checkWrite(java.lang.String)}</code> method does not allow a file to 603 * be created 604 * @since 8.1 605 * @see File#createTempFile(String, String, File) 606 * @see Environment#getTemp() 607 * @see #createTempFilePath(String, String, FileAttribute...) 608 * @see #createTempDirectory(String, FileAttribute...) 609 */ 610 public static File createTempFile(String prefix, String suffix) throws IOException { 611 try { 612 return File.createTempFile(prefix, suffix, getTempDir()); 613 } catch (IOException e) { 614 throw new IOException("Could not create temp file in " + getTempDir(), e); 615 } 616 } 617 618 /** 619 * @return the Nuxeo temp dir returned by {@link Environment#getTemp()}. If the Environment fails to initialize, 620 * then returns the File denoted by {@code "nuxeo.tmp.dir"} System property, or {@code "java.io.tmpdir"}. 621 * @since 8.1 622 */ 623 private static File getTempDir() { 624 Environment env = Environment.getDefault(); 625 File temp = env != null ? env.getTemp() 626 : new File(System.getProperty("nuxeo.tmp.dir", System.getProperty("java.io.tmpdir"))); 627 temp.mkdirs(); 628 return temp; 629 } 630 631 /** 632 * Creates an empty file in the framework temporary-file directory ({@code nuxeo.tmp.dir} vs {@code java.io.tmpdir} 633 * ), using the given prefix and suffix to generate its name. The resulting {@code Path} is associated with the 634 * default {@code FileSystem}. 635 * <p> 636 * Invoking this method is equivalent to invoking 637 * {@link Files#createTempFile(Path, String, String, FileAttribute...) 638 * Files.createTempFile(Environment.getDefault().getTemp().toPath(), prefix, suffix, attrs)}. 639 * 640 * @param prefix the prefix string to be used in generating the file's name; may be {@code null} 641 * @param suffix the suffix string to be used in generating the file's name; may be {@code null}, in which case " 642 * {@code .tmp}" is used 643 * @param attrs an optional list of file attributes to set atomically when creating the file 644 * @return the path to the newly created file that did not exist before this method was invoked 645 * @throws IllegalArgumentException if the prefix or suffix parameters cannot be used to generate a candidate file 646 * name 647 * @throws UnsupportedOperationException if the array contains an attribute that cannot be set atomically when 648 * creating the directory 649 * @throws IOException if an I/O error occurs or the temporary-file directory does not exist 650 * @throws SecurityException In the case of the default provider, and a security manager is installed, the 651 * {@link SecurityManager#checkWrite(String) checkWrite} method is invoked to check write access to the 652 * file. 653 * @since 8.1 654 * @see Files#createTempFile(Path, String, String, FileAttribute...) 655 * @see Environment#getTemp() 656 * @see #createTempFile(String, String) 657 */ 658 public static Path createTempFilePath(String prefix, String suffix, FileAttribute<?>... attrs) throws IOException { 659 try { 660 return Files.createTempFile(getTempDir().toPath(), prefix, suffix, attrs); 661 } catch (IOException e) { 662 throw new IOException("Could not create temp file in " + getTempDir(), e); 663 } 664 } 665 666 /** 667 * Creates a new directory in the framework temporary-file directory ({@code nuxeo.tmp.dir} vs 668 * {@code java.io.tmpdir}), using the given prefix to generate its name. The resulting {@code Path} is associated 669 * with the default {@code FileSystem}. 670 * <p> 671 * Invoking this method is equivalent to invoking {@link Files#createTempDirectory(Path, String, FileAttribute...) 672 * Files.createTempDirectory(Environment.getDefault().getTemp().toPath(), prefix, suffix, attrs)}. 673 * 674 * @param prefix the prefix string to be used in generating the directory's name; may be {@code null} 675 * @param attrs an optional list of file attributes to set atomically when creating the directory 676 * @return the path to the newly created directory that did not exist before this method was invoked 677 * @throws IllegalArgumentException if the prefix cannot be used to generate a candidate directory name 678 * @throws UnsupportedOperationException if the array contains an attribute that cannot be set atomically when 679 * creating the directory 680 * @throws IOException if an I/O error occurs or the temporary-file directory does not exist 681 * @throws SecurityException In the case of the default provider, and a security manager is installed, the 682 * {@link SecurityManager#checkWrite(String) checkWrite} method is invoked to check write access when 683 * creating the directory. 684 * @since 8.1 685 * @see Files#createTempDirectory(Path, String, FileAttribute...) 686 * @see Environment#getTemp() 687 * @see #createTempFile(String, String) 688 */ 689 public static Path createTempDirectory(String prefix, FileAttribute<?>... attrs) throws IOException { 690 try { 691 return Files.createTempDirectory(getTempDir().toPath(), prefix, attrs); 692 } catch (IOException e) { 693 throw new IOException("Could not create temp directory in " + getTempDir(), e); 694 } 695 } 696 697}