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