001/*
002 * (C) Copyright 2006-2018 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 *     Bogdan Stefanescu
018 *     Florent Guillaume
019 *     Julien Carsique
020 */
021package org.nuxeo.runtime.osgi;
022
023import static java.nio.charset.StandardCharsets.UTF_8;
024
025import java.io.BufferedInputStream;
026import java.io.File;
027import java.io.FileInputStream;
028import java.io.IOException;
029import java.io.InputStream;
030import java.lang.reflect.Field;
031import java.net.MalformedURLException;
032import java.net.URL;
033import java.util.ArrayList;
034import java.util.Arrays;
035import java.util.Comparator;
036import java.util.Iterator;
037import java.util.List;
038import java.util.Map;
039import java.util.Set;
040import java.util.StringTokenizer;
041import java.util.concurrent.ConcurrentHashMap;
042import java.util.stream.Collectors;
043import java.util.stream.Stream;
044
045import org.apache.commons.io.FileUtils;
046import org.apache.logging.log4j.LogManager;
047import org.apache.logging.log4j.Logger;
048import org.nuxeo.common.Environment;
049import org.nuxeo.common.codec.CryptoProperties;
050import org.nuxeo.common.utils.TextTemplate;
051import org.nuxeo.runtime.AbstractRuntimeService;
052import org.nuxeo.runtime.RuntimeServiceException;
053import org.nuxeo.runtime.Version;
054import org.nuxeo.runtime.api.Framework;
055import org.nuxeo.runtime.model.ComponentName;
056import org.nuxeo.runtime.model.RuntimeContext;
057import org.nuxeo.runtime.model.impl.ComponentPersistence;
058import org.nuxeo.runtime.model.impl.RegistrationInfoImpl;
059import org.osgi.framework.Bundle;
060import org.osgi.framework.BundleContext;
061import org.osgi.framework.Constants;
062import org.osgi.framework.FrameworkEvent;
063import org.osgi.framework.FrameworkListener;
064
065/**
066 * The default implementation of NXRuntime over an OSGi compatible environment.
067 *
068 * @author Bogdan Stefanescu
069 * @author Florent Guillaume
070 */
071public class OSGiRuntimeService extends AbstractRuntimeService implements FrameworkListener {
072
073    public static final ComponentName FRAMEWORK_STARTED_COMP = new ComponentName("org.nuxeo.runtime.started");
074
075    /** Can be used to change the runtime home directory */
076    public static final String PROP_HOME_DIR = "org.nuxeo.runtime.home";
077
078    /** The OSGi application install directory. */
079    public static final String PROP_INSTALL_DIR = "INSTALL_DIR";
080
081    /** The OSGi application config directory. */
082    public static final String PROP_CONFIG_DIR = "CONFIG_DIR";
083
084    /** The host adapter. */
085    public static final String PROP_HOST_ADAPTER = "HOST_ADAPTER";
086
087    public static final String PROP_NUXEO_BIND_ADDRESS = "nuxeo.bind.address";
088
089    public static final String NAME = "OSGi NXRuntime";
090
091    public static final Version VERSION = Version.parseString("1.4.0");
092
093    private static final Logger log = LogManager.getLogger(OSGiRuntimeService.class);
094
095    private final BundleContext bundleContext;
096
097    private final Map<String, RuntimeContext> contexts;
098
099    private boolean appStarted = false;
100
101    /**
102     * OSGi doesn't provide a method to lookup bundles by symbolic name. This table is used to map symbolic names to
103     * bundles. This map is not handling bundle versions.
104     */
105    final Map<String, Bundle> bundles;
106
107    final ComponentPersistence persistence;
108
109    public OSGiRuntimeService(BundleContext context) {
110        this(new OSGiRuntimeContext(context.getBundle()), context);
111    }
112
113    public OSGiRuntimeService(OSGiRuntimeContext runtimeContext, BundleContext context) {
114        super(runtimeContext);
115        bundleContext = context;
116        bundles = new ConcurrentHashMap<>();
117        contexts = new ConcurrentHashMap<>();
118        String bindAddress = context.getProperty(PROP_NUXEO_BIND_ADDRESS);
119        if (bindAddress != null) {
120            properties.put(PROP_NUXEO_BIND_ADDRESS, bindAddress);
121        }
122        String homeDir = getProperty(PROP_HOME_DIR);
123        log.debug("Home directory: {}", homeDir);
124        if (homeDir != null) {
125            workingDir = new File(homeDir);
126        } else {
127            workingDir = bundleContext.getDataFile("/");
128        }
129        // environment may not be set by some bootstrappers (like tests) - we create it now if not yet created
130        Environment env = Environment.getDefault();
131        if (env == null) {
132            env = new Environment(workingDir);
133            Environment.setDefault(env);
134            env.setServerHome(workingDir);
135            env.init();
136        }
137        workingDir.mkdirs();
138        persistence = new ComponentPersistence(this);
139        log.debug("Working directory: {}", workingDir);
140    }
141
142    @Override
143    public String getName() {
144        return NAME;
145    }
146
147    @Override
148    public Version getVersion() {
149        return VERSION;
150    }
151
152    public BundleContext getBundleContext() {
153        return bundleContext;
154    }
155
156    @Override
157    public Bundle getBundle(String symbolicName) {
158        return bundles.get(symbolicName);
159    }
160
161    public Map<String, Bundle> getBundlesMap() {
162        return bundles;
163    }
164
165    public ComponentPersistence getComponentPersistence() {
166        return persistence;
167    }
168
169    public synchronized RuntimeContext createContext(Bundle bundle) {
170        RuntimeContext ctx = contexts.get(bundle.getSymbolicName());
171        if (ctx == null) {
172            // workaround to handle fragment bundles
173            ctx = new OSGiRuntimeContext(bundle);
174            contexts.put(bundle.getSymbolicName(), ctx);
175            loadComponents(bundle, ctx);
176        }
177        return ctx;
178    }
179
180    public synchronized void destroyContext(Bundle bundle) {
181        RuntimeContext ctx = contexts.remove(bundle.getSymbolicName());
182        if (ctx != null) {
183            ctx.destroy();
184        }
185    }
186
187    public synchronized RuntimeContext getContext(Bundle bundle) {
188        return contexts.get(bundle.getSymbolicName());
189    }
190
191    public synchronized RuntimeContext getContext(String symbolicName) {
192        return contexts.get(symbolicName);
193    }
194
195    @Override
196    protected void doStart() {
197        bundleContext.addFrameworkListener(this);
198        loadComponents(bundleContext.getBundle(), context);
199    }
200
201    @Override
202    protected void doStop() {
203        // do not destroy context since component manager is already shutdown
204        bundleContext.removeFrameworkListener(this);
205        super.doStop();
206    }
207
208    protected void loadComponents(Bundle bundle, RuntimeContext ctx) {
209        String list = getComponentsList(bundle);
210        String name = bundle.getSymbolicName();
211        if (list == null) {
212            log.debug("Bundle {} doesn't have components", name);
213            return;
214        }
215        log.trace("Load Bundle: {} / Components: {}", name, list);
216        StringTokenizer tok = new StringTokenizer(list, ", \t\n\r\f");
217        while (tok.hasMoreTokens()) {
218            String path = tok.nextToken();
219            URL url = bundle.getEntry(path);
220            if (url != null) {
221                log.trace("Load component {} [{}]", name, url);
222                try {
223                    ctx.deploy(url);
224                } catch (IOException e) {
225                    // just log error to know where is the cause of the exception
226                    log.error("Error deploying resource: {}", url);
227                    throw new RuntimeServiceException("Cannot deploy: " + url, e);
228                }
229            } else {
230                String message = "Unknown component '" + path + "' referenced by bundle '" + name + "'";
231                log.error(message + ". Check the MANIFEST.MF");
232                messageHandler.addError(message);
233            }
234        }
235    }
236
237    public static String getComponentsList(Bundle bundle) {
238        return (String) bundle.getHeaders().get("Nuxeo-Component");
239    }
240
241    protected boolean loadConfigurationFromProvider() throws IOException {
242        // TODO use a OSGi service for this.
243        Iterable<URL> provider = Environment.getDefault().getConfigurationProvider();
244        if (provider == null) {
245            return false;
246        }
247        Iterator<URL> it = provider.iterator();
248        List<URL> props = new ArrayList<>();
249        List<URL> xmls = new ArrayList<>();
250        while (it.hasNext()) {
251            URL url = it.next();
252            String path = url.getPath();
253            if (path.endsWith("-config.xml")) {
254                xmls.add(url);
255            } else if (path.endsWith(".properties")) {
256                props.add(url);
257            }
258        }
259        xmls.sort(Comparator.comparing(URL::getPath));
260        for (URL url : props) {
261            loadProperties(url);
262        }
263        for (URL url : xmls) {
264            context.deploy(url);
265        }
266        return true;
267    }
268
269    @Override
270    protected void loadConfig() throws IOException {
271        Environment env = Environment.getDefault();
272        if (env != null) {
273            log.debug("Configuration: host application: {}", env.getHostApplicationName());
274        } else {
275            log.warn("Configuration: no host application");
276            return;
277        }
278
279        File blacklistFile = new File(env.getConfig(), "blacklist");
280        if (blacklistFile.isFile()) {
281            Set<String> lines = FileUtils.readLines(blacklistFile, UTF_8)
282                                         .stream()
283                                         .map(String::trim)
284                                         .filter(line -> !line.isEmpty())
285                                         .collect(Collectors.toSet());
286            manager.setBlacklist(lines);
287        }
288
289        if (loadConfigurationFromProvider()) {
290            return;
291        }
292
293        String configDir = bundleContext.getProperty(PROP_CONFIG_DIR);
294        if (configDir != null && configDir.contains(":/")) { // an url of a config file
295            URL url = new URL(configDir);
296            log.debug("Configuration: loading properties from: {}", configDir);
297            loadProperties(url);
298            return;
299        }
300
301        // TODO: in JBoss there is a deployer that will deploy nuxeo
302        // configuration files ..
303        boolean isNotJBoss4 = !isJBoss4(env);
304
305        File dir = env.getConfig();
306        // File dir = new File(configDir);
307        String[] names = dir.list();
308        if (names != null) {
309            Arrays.sort(names, String::compareToIgnoreCase);
310            log.debug("Deployment order of configuration files: {}",
311                    () -> Stream.of(names).reduce((n1, n2) -> n1 + "\n\t" + n2).map(n -> "\n\t" + n).orElse(""));
312            for (String name : names) {
313                if (name.endsWith("-config.xml") || name.endsWith("-bundle.xml")) {
314                    // TODO because of some dep bugs (regarding the deployment of demo-ds.xml), we cannot let the
315                    // runtime deploy config dir at beginning...
316                    // until fixing this we deploy config dir from NuxeoDeployer
317                    if (isNotJBoss4) {
318                        File file = new File(dir, name);
319                        log.trace("Configuration: deploy config component: {}", name);
320                        try {
321                            context.deploy(file.toURI().toURL());
322                        } catch (IOException e) {
323                            throw new IllegalArgumentException("Cannot load config from " + file, e);
324                        }
325                    }
326                } else if (name.endsWith(".config") || name.endsWith(".ini") || name.endsWith(".properties")) {
327                    File file = new File(dir, name);
328                    log.trace("Configuration: loading properties: {}", name);
329                    loadProperties(file);
330                } else {
331                    log.trace("Configuration: ignoring: {}", name);
332                }
333            }
334        } else if (dir.isFile()) { // a file - load it
335            log.debug("Configuration: loading properties: {}", dir);
336            loadProperties(dir);
337        } else {
338            log.debug("Configuration: no configuration file found");
339        }
340
341        loadDefaultConfig();
342    }
343
344    @Override
345    public void reloadProperties() throws IOException {
346        File dir = Environment.getDefault().getConfig();
347        String[] names = dir.list();
348        if (names != null) {
349            Arrays.sort(names, String::compareToIgnoreCase);
350            CryptoProperties props = new CryptoProperties(System.getProperties());
351            for (String name : names) {
352                if (name.endsWith(".config") || name.endsWith(".ini") || name.endsWith(".properties")) {
353                    try (FileInputStream in = new FileInputStream(new File(dir, name))) {
354                        props.load(in);
355                    }
356                }
357            }
358            // replace the current runtime properties
359            properties = props;
360        }
361    }
362
363    /**
364     * Loads default properties.
365     * <p>
366     * Used for backward compatibility when adding new mandatory properties
367     * </p>
368     */
369    protected void loadDefaultConfig() {
370        String varName = "org.nuxeo.ecm.contextPath";
371        if (Framework.getProperty(varName) == null) {
372            properties.setProperty(varName, "/nuxeo");
373        }
374    }
375
376    public void loadProperties(File file) throws IOException {
377        try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
378            loadProperties(in);
379        }
380    }
381
382    public void loadProperties(URL url) throws IOException {
383        try (InputStream in = url.openStream()) {
384            loadProperties(in);
385        }
386    }
387
388    public void loadProperties(InputStream in) throws IOException {
389        properties.load(in);
390    }
391
392    /**
393     * Overrides the default method to be able to include OSGi properties.
394     */
395    @Override
396    public String getProperty(String name, String defValue) {
397        String value = properties.getProperty(name);
398        if (value == null) {
399            value = bundleContext.getProperty(name);
400            if (value == null) {
401                return defValue == null ? null : expandVars(defValue);
402            }
403        }
404        if (("${" + name + "}").equals(value)) {
405            // avoid loop, don't expand
406            return value;
407        }
408        return expandVars(value);
409    }
410
411    /**
412     * Overrides the default method to be able to include OSGi properties.
413     */
414    @Override
415    public String expandVars(String expression) {
416        return new TextTemplate(getProperties()) {
417            @Override
418            public String getVariable(String name) {
419                String value = super.getVariable(name);
420                if (value == null) {
421                    value = bundleContext.getProperty(name);
422                }
423                return value;
424            }
425
426        }.processText(expression);
427    }
428
429    protected void startComponents() {
430        synchronized (this) {
431            if (appStarted) {
432                return;
433            }
434            appStarted = true;
435        }
436        try {
437            persistence.loadPersistedComponents();
438        } catch (RuntimeException | IOException e) {
439            log.error("Failed to load persisted components", e);
440        }
441        // deploy a fake component that is marking the end of startup
442        // XML components that needs to be deployed at the end need to put a
443        // requirement
444        // on this marker component
445        deployFrameworkStartedComponent();
446        // ============ activate and start components =======
447        manager.start();
448        // create a snapshot of the started components - TODO should this be optional?
449        manager.snapshot();
450        // ==================================================
451        // print the startup message
452        printStatusMessage();
453    }
454
455    /* --------------- FrameworkListener API ------------------ */
456
457    @Override
458    public void frameworkEvent(FrameworkEvent event) {
459        if (event.getType() != FrameworkEvent.STARTED) {
460            return;
461        }
462        startComponents();
463    }
464
465    private void printStatusMessage() {
466        StringBuilder msg = new StringBuilder();
467        msg.append("Nuxeo Platform Started\n");
468        if (getStatusMessage(msg)) {
469            log.info(msg);
470        } else {
471            log.error(msg);
472            if (Boolean.getBoolean("nuxeo.start.strict")) {
473                throw new IllegalStateException("Startup aborted due to previous failures (strict mode)");
474            }
475        }
476    }
477
478    protected void deployFrameworkStartedComponent() {
479        RegistrationInfoImpl ri = new RegistrationInfoImpl(FRAMEWORK_STARTED_COMP);
480        ri.setContext(context);
481        // this will register any pending components that waits for the
482        // framework to be started
483        manager.register(ri);
484    }
485
486    public Bundle findHostBundle(Bundle bundle) {
487        String hostId = (String) bundle.getHeaders().get(Constants.FRAGMENT_HOST);
488        log.debug("Looking for host bundle: {} host id: {}", bundle.getSymbolicName(), hostId);
489        if (hostId != null) {
490            int p = hostId.indexOf(';');
491            if (p > -1) { // remove version or other extra information if any
492                hostId = hostId.substring(0, p);
493            }
494            RuntimeContext ctx = contexts.get(hostId);
495            if (ctx != null) {
496                log.debug("Context was found for host id: {}", hostId);
497                return ctx.getBundle();
498            } else {
499                log.warn("No context found for host id: {}", hostId);
500
501            }
502        }
503        return null;
504    }
505
506    protected File getEclipseBundleFileUsingReflection(Bundle bundle) {
507        try {
508            Object proxy = bundle.getClass().getMethod("getLoaderProxy").invoke(bundle);
509            Object loader = proxy.getClass().getMethod("getBundleLoader").invoke(proxy);
510            URL root = (URL) loader.getClass().getMethod("findResource", String.class).invoke(loader, "/");
511            Field field = root.getClass().getDeclaredField("handler");
512            field.setAccessible(true);
513            Object handler = field.get(root);
514            Field entryField = handler.getClass().getSuperclass().getDeclaredField("bundleEntry");
515            entryField.setAccessible(true);
516            Object entry = entryField.get(handler);
517            Field fileField = entry.getClass().getDeclaredField("file");
518            fileField.setAccessible(true);
519            return (File) fileField.get(entry);
520        } catch (ReflectiveOperationException e) {
521            log.error("Cannot access to eclipse bundle system files of " + bundle.getSymbolicName());
522            return null;
523        }
524    }
525
526    @Override
527    public File getBundleFile(Bundle bundle) {
528        File file;
529        String location = bundle.getLocation();
530        String vendor = Framework.getProperty(Constants.FRAMEWORK_VENDOR);
531        String name = bundle.getSymbolicName();
532
533        if ("Eclipse".equals(vendor)) { // equinox framework
534            log.debug("getBundleFile (Eclipse): {}->{}", name, location);
535            return getEclipseBundleFileUsingReflection(bundle);
536        } else if (location.startsWith("file:")) { // nuxeo osgi adapter
537            try {
538                file = org.nuxeo.common.utils.FileUtils.urlToFile(location);
539            } catch (MalformedURLException e) {
540                log.error("getBundleFile: Unable to create file for bundle name: {} as URI: {}", name, location);
541                return null;
542            }
543        } else { // may be a file path - this happens when using
544            // JarFileBundle (for ex. in nxshell)
545            file = new File(location);
546        }
547        if (file.exists()) {
548            log.debug("getBundleFile: {} bound to file: {}", name, file);
549            return file;
550        } else {
551            log.debug("getBundleFile: {} cannot bind to nonexistent file: {}", name, file);
552            return null;
553        }
554    }
555
556    public static boolean isJBoss4(Environment env) {
557        if (env == null) {
558            return false;
559        }
560        String hn = env.getHostApplicationName();
561        String hv = env.getHostApplicationVersion();
562        if (hn == null || hv == null) {
563            return false;
564        }
565        return "JBoss".equals(hn) && hv.startsWith("4");
566    }
567
568}