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