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;
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.lang.StringUtils;
039import org.apache.commons.logging.Log;
040import org.apache.commons.logging.LogFactory;
041
042import org.nuxeo.common.Environment;
043import org.nuxeo.common.collections.ListenerList;
044import org.nuxeo.runtime.RuntimeService;
045import org.nuxeo.runtime.RuntimeServiceEvent;
046import org.nuxeo.runtime.RuntimeServiceException;
047import org.nuxeo.runtime.RuntimeServiceListener;
048import org.nuxeo.runtime.api.login.LoginAs;
049import org.nuxeo.runtime.api.login.LoginService;
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    private static Boolean testModeSet;
073
074    /**
075     * Global dev property
076     *
077     * @since 5.6
078     * @see #isDevModeSet()
079     */
080    public static final String NUXEO_DEV_SYSTEM_PROP = "org.nuxeo.dev";
081
082    /**
083     * Global testing property
084     *
085     * @since 5.6
086     * @see #isTestModeSet()
087     */
088    public static final String NUXEO_TESTING_SYSTEM_PROP = "org.nuxeo.runtime.testing";
089
090    /**
091     * Property to control strict runtime mode
092     *
093     * @since 5.6
094     * @see #handleDevError(Throwable)
095     */
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() {
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     * Login in the system as the system user (a pseudo-user having all privileges).
247     *
248     * @return the login session if successful. Never returns null.
249     * @throws LoginException on login failure
250     */
251    public static LoginContext login() throws LoginException {
252        checkRuntimeInitialized();
253        LoginService loginService = runtime.getService(LoginService.class);
254        if (loginService != null) {
255            return loginService.login();
256        }
257        return null;
258    }
259
260    /**
261     * Login in the system as the system user (a pseudo-user having all privileges). The given username will be used to
262     * identify the user id that called this method.
263     *
264     * @param username the originating user id
265     * @return the login session if successful. Never returns null.
266     * @throws LoginException on login failure
267     */
268    public static LoginContext loginAs(String username) throws LoginException {
269        checkRuntimeInitialized();
270        LoginService loginService = runtime.getService(LoginService.class);
271        if (loginService != null) {
272            return loginService.loginAs(username);
273        }
274        return null;
275    }
276
277    /**
278     * Login in the system as the given user without checking the password.
279     *
280     * @param username the user name to login as.
281     * @return the login context
282     * @throws LoginException if any error occurs
283     * @since 5.4.2
284     */
285    public static LoginContext loginAsUser(String username) throws LoginException {
286        return getLocalService(LoginAs.class).loginAs(username);
287    }
288
289    /**
290     * Login in the system as the given user using the given password.
291     *
292     * @param username the username to login
293     * @param password the password
294     * @return a login session if login was successful. Never returns null.
295     * @throws LoginException if login failed
296     */
297    public static LoginContext login(String username, Object password) throws LoginException {
298        checkRuntimeInitialized();
299        LoginService loginService = runtime.getService(LoginService.class);
300        if (loginService != null) {
301            return loginService.login(username, password);
302        }
303        return null;
304    }
305
306    /**
307     * Login in the system using the given callback handler for login info resolution.
308     *
309     * @param cbHandler used to fetch the login info
310     * @return the login context
311     * @throws LoginException
312     */
313    public static LoginContext login(CallbackHandler cbHandler) throws LoginException {
314        checkRuntimeInitialized();
315        LoginService loginService = runtime.getService(LoginService.class);
316        if (loginService != null) {
317            return loginService.login(cbHandler);
318        }
319        return null;
320    }
321
322    public static void sendEvent(RuntimeServiceEvent event) {
323        Object[] listenersArray = listeners.getListeners();
324        for (Object listener : listenersArray) {
325            ((RuntimeServiceListener) listener).handleEvent(event);
326        }
327    }
328
329    /**
330     * Registers a listener to be notified about runtime events.
331     * <p>
332     * If the listener is already registered, do nothing.
333     *
334     * @param listener the listener to register
335     */
336    public static void addListener(RuntimeServiceListener listener) {
337        listeners.add(listener);
338    }
339
340    /**
341     * Removes the given listener.
342     * <p>
343     * If the listener is not registered, do nothing.
344     *
345     * @param listener the listener to remove
346     */
347    public static void removeListener(RuntimeServiceListener listener) {
348        listeners.remove(listener);
349    }
350
351    /**
352     * Gets the given property value if any, otherwise null.
353     * <p>
354     * The framework properties will be searched first then if any matching property is found the system properties are
355     * searched too.
356     *
357     * @param key the property key
358     * @return the property value if any or null otherwise
359     */
360    public static String getProperty(String key) {
361        return getProperty(key, null);
362    }
363
364    /**
365     * Gets the given property value if any, otherwise returns the given default value.
366     * <p>
367     * The framework properties will be searched first then if any matching property is found the system properties are
368     * searched too.
369     *
370     * @param key the property key
371     * @param defValue the default value to use
372     * @return the property value if any otherwise the default value
373     */
374    public static String getProperty(String key, String defValue) {
375        checkRuntimeInitialized();
376        return runtime.getProperty(key, defValue);
377    }
378
379    /**
380     * Gets all the framework properties. The system properties are not included in the returned map.
381     *
382     * @return the framework properties map. Never returns null.
383     */
384    public static Properties getProperties() {
385        checkRuntimeInitialized();
386        return runtime.getProperties();
387    }
388
389    /**
390     * Expands any variable found in the given expression with the value of the corresponding framework property.
391     * <p>
392     * The variable format is ${property_key}.
393     * <p>
394     * System properties are also expanded.
395     */
396    public static String expandVars(String expression) {
397        checkRuntimeInitialized();
398        return runtime.expandVars(expression);
399    }
400
401    public static boolean isOSGiServiceSupported() {
402        if (isOSGiServiceSupported == null) {
403            isOSGiServiceSupported = Boolean.valueOf(isBooleanPropertyTrue("ecr.osgi.services"));
404        }
405        return isOSGiServiceSupported.booleanValue();
406    }
407
408    /**
409     * Returns true if dev mode is set.
410     * <p>
411     * Activating this mode, some of the code may not behave as it would in production, to ease up debugging and working
412     * on developing the application.
413     * <p>
414     * For instance, it'll enable hot-reload if some packages are installed while the framework is running. It will also
415     * reset some caches when that happens.
416     * <p>
417     * Before 5.6, when activating this mode, the Runtime Framework stopped on low-level errors, see
418     * {@link #handleDevError(Throwable)} but this behaviour has been removed.
419     */
420    public static boolean isDevModeSet() {
421        return isBooleanPropertyTrue(NUXEO_DEV_SYSTEM_PROP);
422    }
423
424    /**
425     * Returns true if test mode is set.
426     * <p>
427     * Activating this mode, some of the code may not behave as it would in production, to ease up testing.
428     */
429    public static boolean isTestModeSet() {
430        if (testModeSet == null) {
431            testModeSet = isBooleanPropertyTrue(NUXEO_TESTING_SYSTEM_PROP);
432        }
433        return testModeSet;
434    }
435
436    /**
437     * Returns true if given property is false when compared to a boolean value. Returns false if given property in
438     * unset.
439     * <p>
440     * Checks for the system properties if property is not found in the runtime properties.
441     *
442     * @since 5.8
443     */
444    public static boolean isBooleanPropertyFalse(String propName) {
445        String v = getProperty(propName);
446        if (v == null) {
447            v = System.getProperty(propName);
448        }
449        if (StringUtils.isBlank(v)) {
450            return false;
451        }
452        return !Boolean.parseBoolean(v);
453    }
454
455    /**
456     * Returns true if given property is true when compared to a boolean value.
457     * <p>
458     * Checks for the system properties if property is not found in the runtime properties.
459     *
460     * @since 5.6
461     */
462    public static boolean isBooleanPropertyTrue(String propName) {
463        String v = getProperty(propName);
464        if (v == null) {
465            v = System.getProperty(propName);
466        }
467        return Boolean.parseBoolean(v);
468    }
469
470    /**
471     * Since 5.6, this method stops the application if property {@link #NUXEO_STRICT_RUNTIME_SYSTEM_PROP} is set to
472     * true, and one of the following errors occurred during startup.
473     * <ul>
474     * <li>Component XML parse error.
475     * <li>Contribution to an unknown extension point.
476     * <li>Component with an unknown implementation class (the implementation entry exists in the XML descriptor but
477     * cannot be resolved to a class).
478     * <li>Uncatched exception on extension registration / unregistration (either in framework or user component code)
479     * <li>Uncatched exception on component activation / deactivation (either in framework or user component code)
480     * <li>Broken Nuxeo-Component MANIFEST entry. (i.e. the entry cannot be resolved to a resource)
481     * </ul>
482     * <p>
483     * Before 5.6, this method stopped the application if development mode was enabled (i.e. org.nuxeo.dev system
484     * property is set) but this is not the case anymore to handle a dev mode that does not stop the runtime framework
485     * when using hot reload.
486     *
487     * @param t the exception or null if none
488     */
489    public static void handleDevError(Throwable t) {
490        if (isBooleanPropertyTrue(NUXEO_STRICT_RUNTIME_SYSTEM_PROP)) {
491            System.err.println("Fatal error caught in strict " + "runtime mode => exiting.");
492            if (t != null) {
493                t.printStackTrace();
494            }
495            System.exit(1);
496        } else if (t != null) {
497            log.error(t, t);
498        }
499    }
500
501    /**
502     * @see FileEventTracker
503     * @param aFile The file to delete
504     * @param aMarker the marker Object
505     */
506    public static void trackFile(File aFile, Object aMarker) {
507        FileEvent.onFile(Framework.class, aFile, aMarker).send();
508    }
509
510    /**
511     * Strategy is not customizable anymore.
512     *
513     * @deprecated
514     * @since 6.0
515     * @see #trackFile(File, Object)
516     * @see org.nuxeo.runtime.trackers.files.FileEventTracker.SafeFileDeleteStrategy
517     * @param file The file to delete
518     * @param marker the marker Object
519     * @param fileDeleteStrategy ignored deprecated parameter
520     */
521    @Deprecated
522    public static void trackFile(File file, Object marker, FileDeleteStrategy fileDeleteStrategy) {
523        trackFile(file, marker);
524    }
525
526    /**
527     * @since 6.0
528     */
529    protected static void checkRuntimeInitialized() {
530        if (runtime == null) {
531            throw new IllegalStateException("Runtime not initialized");
532        }
533    }
534
535    public static void main(String[] args) {
536    }
537
538    /**
539     * Creates an empty file in the framework temporary-file directory ({@code nuxeo.tmp.dir} vs {@code java.io.tmpdir}
540     * ), using the given prefix and suffix to generate its name.
541     * <p>
542     * Invoking this method is equivalent to invoking
543     * <code>{@link File#createTempFile(java.lang.String, java.lang.String, java.io.File)
544     * File.createTempFile(prefix,&nbsp;suffix,&nbsp;Environment.getDefault().getTemp())}</code>.
545     * <p>
546     * The {@link #createTempFilePath(String, String, FileAttribute...)} method provides an alternative method to create
547     * an empty file in the framework temporary-file directory. Files created by that method may have more restrictive
548     * access permissions to files created by this method and so may be more suited to security-sensitive applications.
549     *
550     * @param prefix The prefix string to be used in generating the file's name; must be at least three characters long
551     * @param suffix The suffix string to be used in generating the file's name; may be <code>null</code>, in which case
552     *            the suffix <code>".tmp"</code> will be used
553     * @return An abstract pathname denoting a newly-created empty file
554     * @throws IllegalArgumentException If the <code>prefix</code> argument contains fewer than three characters
555     * @throws IOException If a file could not be created
556     * @throws SecurityException If a security manager exists and its <code>
557     *             {@link java.lang.SecurityManager#checkWrite(java.lang.String)}</code> method does not allow a file to
558     *             be created
559     * @since 8.1
560     * @see File#createTempFile(String, String, File)
561     * @see Environment#getTemp()
562     * @see #createTempFilePath(String, String, FileAttribute...)
563     * @see #createTempDirectory(String, FileAttribute...)
564     */
565    public static File createTempFile(String prefix, String suffix) throws IOException {
566        try {
567            return File.createTempFile(prefix, suffix, getTempDir());
568        } catch (IOException e) {
569            throw new IOException("Could not create temp file in " + getTempDir(), e);
570        }
571    }
572
573    /**
574     * @return the Nuxeo temp dir returned by {@link Environment#getTemp()}. If the Environment fails to initialize,
575     *         then returns the File denoted by {@code "nuxeo.tmp.dir"} System property, or {@code "java.io.tmpdir"}.
576     * @since 8.1
577     */
578    private static File getTempDir() {
579        Environment env = Environment.getDefault();
580        File temp = env != null ? env.getTemp() : new File(System.getProperty("nuxeo.tmp.dir",
581                System.getProperty("java.io.tmpdir")));
582        temp.mkdirs();
583        return temp;
584    }
585
586    /**
587     * Creates an empty file in the framework temporary-file directory ({@code nuxeo.tmp.dir} vs {@code java.io.tmpdir}
588     * ), using the given prefix and suffix to generate its name. The resulting {@code Path} is associated with the
589     * default {@code FileSystem}.
590     * <p>
591     * Invoking this method is equivalent to invoking
592     * {@link Files#createTempFile(Path, String, String, FileAttribute...)
593     * Files.createTempFile(Environment.getDefault().getTemp().toPath(),&nbsp;prefix,&nbsp;suffix,&nbsp;attrs)}.
594     *
595     * @param prefix the prefix string to be used in generating the file's name; may be {@code null}
596     * @param suffix the suffix string to be used in generating the file's name; may be {@code null}, in which case "
597     *            {@code .tmp}" is used
598     * @param attrs an optional list of file attributes to set atomically when creating the file
599     * @return the path to the newly created file that did not exist before this method was invoked
600     * @throws IllegalArgumentException if the prefix or suffix parameters cannot be used to generate a candidate file
601     *             name
602     * @throws UnsupportedOperationException if the array contains an attribute that cannot be set atomically when
603     *             creating the directory
604     * @throws IOException if an I/O error occurs or the temporary-file directory does not exist
605     * @throws SecurityException In the case of the default provider, and a security manager is installed, the
606     *             {@link SecurityManager#checkWrite(String) checkWrite} method is invoked to check write access to the
607     *             file.
608     * @since 8.1
609     * @see Files#createTempFile(Path, String, String, FileAttribute...)
610     * @see Environment#getTemp()
611     * @see #createTempFile(String, String)
612     */
613    public static Path createTempFilePath(String prefix, String suffix, FileAttribute<?>... attrs) throws IOException {
614        try {
615            return Files.createTempFile(getTempDir().toPath(), prefix, suffix, attrs);
616        } catch (IOException e) {
617            throw new IOException("Could not create temp file in " + getTempDir(), e);
618        }
619    }
620
621    /**
622     * Creates a new directory in the framework temporary-file directory ({@code nuxeo.tmp.dir} vs
623     * {@code java.io.tmpdir}), using the given prefix to generate its name. The resulting {@code Path} is associated
624     * with the default {@code FileSystem}.
625     * <p>
626     * Invoking this method is equivalent to invoking {@link Files#createTempDirectory(Path, String, FileAttribute...)
627     * Files.createTempDirectory(Environment.getDefault().getTemp().toPath(),&nbsp;prefix,&nbsp;suffix,&nbsp;attrs)}.
628     *
629     * @param prefix the prefix string to be used in generating the directory's name; may be {@code null}
630     * @param attrs an optional list of file attributes to set atomically when creating the directory
631     * @return the path to the newly created directory that did not exist before this method was invoked
632     * @throws IllegalArgumentException if the prefix cannot be used to generate a candidate directory name
633     * @throws UnsupportedOperationException if the array contains an attribute that cannot be set atomically when
634     *             creating the directory
635     * @throws IOException if an I/O error occurs or the temporary-file directory does not exist
636     * @throws SecurityException In the case of the default provider, and a security manager is installed, the
637     *             {@link SecurityManager#checkWrite(String) checkWrite} method is invoked to check write access when
638     *             creating the directory.
639     * @since 8.1
640     * @see Files#createTempDirectory(Path, String, FileAttribute...)
641     * @see Environment#getTemp()
642     * @see #createTempFile(String, String)
643     */
644    public static Path createTempDirectory(String prefix, FileAttribute<?>... attrs) throws IOException {
645        try {
646            return Files.createTempDirectory(getTempDir().toPath(), prefix, attrs);
647        } catch (IOException e) {
648            throw new IOException("Could not create temp directory in " + getTempDir(), e);
649        }
650    }
651
652}