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