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