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