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