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