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,&nbsp;suffix,&nbsp;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(),&nbsp;prefix,&nbsp;suffix,&nbsp;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(),&nbsp;prefix,&nbsp;suffix,&nbsp;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}