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