Monday, September 22, 2008

Inside Sling: How Sling boots

In this post, I will give an overview on how Sling starts up, how Felix is started as the OSGi framework, how Sling is configured, and how Sling loads other bundles.
For this, we need to have a look at the org.apache.sling.launchpad.base project ( sling/launchpad/base ).

Class overview

Let's start with an overview of the most important classes involved in the startup process:

  • blue boxes are Sling classes, from package org.apache.sling.launcher.base
  • green boxes are Felix classes, from package org.apache.felix.framework
  • the violet box is an OSGi framework interface, the BundleActivator

launchpad classes

The ResourceProvider abstract class forms an interface for Sling to be able to locate arbitrary resources. In this case, the only implementation is the ClassLoaderResourceProvider, which will load resources from the class loader passed in the constructor. Note that this is not the org.apache.sling.api.ResourceProvider interface, but an abstract class in the package org.apache.sling.launcher.base.

The Sling class holds a reference to a Felix instance (as it starts the instance), and to a Logger instance (which it also instantiates). At the same time, it implements the BundleActivator interface. This is an OSGi framework interface that needs to be implemented by classes that start and stop a bundle. Felix accepts a list of BundleActivators in it's constructor, and these will serve as the bundles that are initially started, and which are responsible for starting any other bundles required. We'll look at Sling's bundle start and stop methods in a minute.

BootstrapInstaller also implements BundleActivator and is also passed to Felix at startup. The BootstrapInstaller will load all the other bundles that make up Sling. We'll look at that as well.

Sequence overview

Before we go into details, let's look at the startup sequence from a high level:

  1. a logger is instantiated
  2. a ClassLoaderResourceProvider is created, passing the Main class' class loader
  3. a Sling instance is created, passing the previous two objects
  4. install bundles are set
  5. the OSGi execution environment is set
  6. an instance of BootstrapInstaller is created
  7. a Felix instance is created, passing the Logger and a list of BundleActivators, consisting of the sling instance itself and the BootstrapInstaller just created
  8. Felix' start method is called
  9. eventually, as it will start up the bundles passed in the constructor, Felix will call the start() method of the sling instance
  10. Felix will also call the start() method of the BootstrapInstaller
  11. this in turn will call installBundles() and
  12. startBundles(), which I will dicuss a little later

launchpad sequence

Details

Now that we have seen that objects involved and the high level sequence of things, let's dig down a bit deeper, along the lines of the sequence:

the main method

First, let's have a look at the Main class' main method (see below: org.apache.sling.launcher.app.main.Main)

  • Line 102 creates a Map, props to store properties that will be passed to Sling when instantiated.
  • Line 105 creates another Map, commandLine in which command line configuration statements will be collected. This collection is deferred to the parseCommandline method. That method will scan the commandline for switches that affect the port, log file, log level or sling home directory, and collect the configuration into the commandLine Map which it was passed.
  • Line 110 propagates a setting of sling.home from the commandLine Map to the props Map
  • Lines 119-126 determine the log level from the command line and sets it in the commandLine Map accordingly. This won't take effect until Sling later calls back to the loadPropertiesOverride() method. For a start, a Logger is instantiated and set to a log level of LOG_ERROR in lines 127 and 131.
  • Line 138 creates the ClassLoaderResourceProvider
  • Line 140 instantiates a Sling instance and makes an inline overrive of the loadPropertiesOverride method. This method is an overrideable callback provided for classes extending Sling, and will be called by Sling.loadConfigProperties.
  • Line 159 establishes a VM shutdown hook which will call Sling.destroy() before the VM shuts down

Sling configuration

Actual Sling startup takes place in the constructor, so let's have a closer look at that (Line numbers refer to org.apache.sling.launcher.app.main.Main). Line 182 delegates configuration setup to loadConfigProperties(), so let's first have a look at how the Sling configuration is calculated.

  • load the properties file (l.299) and all includes (l.302)
  • overwrite the loaded properties with properties passed in the constructor (if any duplicates) (l.306)
  • look for the sling.home property in the system properties, the properties just merged, and set to "sling" if not found (l.310-321)
  • merge any properties found in ${sling.home}/sling.properties (l.328-330).
  • overwrite properties with any system properties that have the same name (sling.home is not affected). This step can be prevented by setting sling.ignoreSystemProperties to true. (l.338-346)
  • load includes that have been added through the last steps (l.349)
  • call loadPropertiesOverride -- Sling's own implementation does nothing, but subclasses may override this to inject configuration
  • append to the list of boot delegation and system packages (l.355, 357): given the prefix sling.bootdelegation. and the osgiProp org.osgi.framework.bootdelegation, it will
    • look for an entry that starts with sling.bootdelegation. and add the value to the end of the current value of org.osgi.framework.bootdelegation (adding a comma if that value is not empty so far)
    • unless the entry starts with sling.bootdelegation.class. &ldash; in that case, it will interpret the rest of the property name as a class name, and add the value only if the class can be loaded.
  • substitute variables (e.g. ${sling.home}) (l.368-371)
  • substitute context: values (l.374 - 419) (in short: for any value that starts with context:/, copy the file that is specified by the remainder of the value to sling.home and replace the original value of the property with the absolute path of the copied file)
  • as a last step, the unsubstituted properties are written back to ${sling.home}/sling.properties.

The remainder of the Sling constructor

So, we have reached line 183 in Sling.java, and are before step (4) in the sequence diagram above. Let's see what happens in the rest of Sling's constructor.

  • setInstallBundles() is called in line 185. I have not seen any use of this in a sling properties file, but it looks like properties of the pattern sling.install.some.bundle.name.here will result in ,some.bundle.name.here being appended to sling.install.bundles.
  • setExecutionEnvironment() is called in line 188. This will set the property org.osgi.framework.executionenvironment (see section 3.3 of the OSGi R4 core specification).
  • A list of BundleActivators is created, including the Sling instance in construction and a new BootstrapInstaller.
  • In line 199, a new Felix instance is created, passing the list of bundle activators just created, the properties, and the logger
  • Line 200 starts the Felix environment.

Now we're at step (8) in the sequence diagram. Felix has started up, the Sling constructor returns. And all that was to be seen so far didn't look like very much, so where's all the rest happening? We'll take up the thread by looking at the start() methods of Sling and BootstrapInstaller. These were both registered as startup bundle activators when calling the Felix constructor, so this is how Felix again plays the ball to Sling.

Sling.start() will just do one thing: it will register a ContextProtocolHandler to handle URLs that start with context:. Any such URL will Sling's resourceProvider (so in this case, the ClassLoaderResourceProvider) to resolve these URLs.

BootstrapInstaller.start() does a bit more. For one thing, it should be noted that this method will be run only once: when Sling is started for the first time. This is achieved by persisting the installation state to a data file managed by Felix (l 147-165), and by reading that data back on method start and not running the code if it was run before (l 103-127).

  • Lines 129-134: the DeploymentPackageInstaller will wait until Framework and Deployment Admin service have started up, and then look for deployment packages in resources/bundles (on the class loader), and install any found (if not already installed). See also section 114 of the OSGi Compendium R4.
  • Lines 143-145 install any bundles provided as jar files in resources/corebundles or resources/bundles, if not already installed, keeping track of the bundles so installed
  • Line 168 will call startBundles, which will start all the bundles just installed. It will also try to set the start level of each bundle to 1, if the start level service is available (OSGi Core R4, Section 8).

Code

org.apache.sling.launcher.app.Sling, Revision 657002

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sling.launcher.app;

import static org.apache.felix.framework.util.FelixConstants.EMBEDDED_EXECUTION_PROP;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.SortedMap;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Map.Entry;

import org.apache.felix.framework.Felix;
import org.apache.felix.framework.Logger;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.osgi.service.url.URLConstants;
import org.osgi.service.url.URLStreamHandlerService;

/**
 * The <code>Sling</code> serves as the starting point for Sling.
 * <ul>
 * <li>The {@link #Sling(Logger, ResourceProvider, Map)} method launches
 * Apache <code>Felix</code> as the OSGi framework implementation we use.
 * </ul>
 * <p>
 * <b>Launch Configuration</b>
 * <p>
 * The Apache <code>Felix</code> framework requires configuration parameters
 * to be specified for startup. This servlet builds the list of parameters from
 * three locations:
 * <ol>
 * <li>The <code>com/day/osgi/servlet/Sling.properties</code> is read from
 * the servlet class path. This properties file contains default settings.</li>
 * <li>Extensions of this servlet may provide additional properties to be
 * loaded overwriting the {@link #loadPropertiesOverride(Map)} method.
 * <li>Finally, web application init parameters are added to the properties and
 * may overwrite existing properties of the same name(s).
 * </ol>
 * <p>
 * After loading all properties, variable substitution takes place on the
 * property values. A variable is indicated as <code>${&lt;prop-name&gt;}</code>
 * where <code>&lt;prop-name&gt;</code> is the name of a system or
 * configuration property (configuration properties override system properties).
 * Variables may be nested and are resolved from inner-most to outer-most. For
 * example, the property value <code>${outer-${inner}}</code> is resolved by
 * first resolving <code>${inner}</code> and then resolving the property whose
 * name is the catenation of <code>outer-</code> and the result of resolving
 * <code>${inner}</code>.
 * <p>
 */
public class Sling implements BundleActivator {

    /** Pseduo class version ID to keep the IDE quite. */
    private static final long serialVersionUID = 1L;

    /**
     * The name of the configuration property defining the Sling home directory
     * (value is "sling.home"). This is a Platform file system directory below
     * which all runtime data, such as the Felix bundle archives, logfiles, CRX
     * repository, etc., is located.
     * <p>
     * The value of this property, if not set as a system property defaults to
     * the <i>sling</i> directory in the current working directory.
     *
     * @see #SLING_HOME_URL
     */
    public static final String SLING_HOME = "sling.home";

    /**
     * The name of the configuration property defining the Sling home directory
     * as an URL (value is "sling.home.url").
     * <p>
     * The value of this property is assigned the value of
     * <code>new File(${sling.home}).toURI().toString()</code> before
     * resolving the property variables.
     *
     * @see #SLING_HOME
     */
    public static final String SLING_HOME_URL = "sling.home.url";

    /**
     * The name of the configuration property defining a properties file
     * defining a list of bundles, which are installed into the framework when
     * it has been launched (value is "org.apache.osgi.bundles").
     * <p>
     * This configuration property is generally set in the web application
     * configuration and may be referenced in all property files (default, user
     * supplied and web application parameters) used to build the framework
     * configuration.
     */
    public static final String OSGI_FRAMEWORK_BUNDLES = "org.apache.osgi.bundles";

    /**
     * The property to be set to ignore the system properties when building the
     * Felix framework properties (value is "sling.ignoreSystemProperties"). If
     * this is property is set to <code>true</code> (case does not matter),
     * the system properties will not be used by
     * {@link #loadConfigProperties(Map)}.
     */
    public static final String SLING_IGNORE_SYSTEM_PROPERTIES = "sling.ignoreSystemProperties";

    /**
     * The name of the default launcher properties file to setup the environment
     * for the <code>Felix</code> framework (value is "sling.properties").
     * <p>
     * Extensions of this class may overwrite some or all properties in this
     * file through Web Application parameters or other properties files.
     */
    public static final String CONFIG_PROPERTIES = "sling.properties";

    public static final String PROP_SYSTEM_PACKAGES = "org.apache.sling.launcher.system.packages";

    /**
     * The simple logger to log messages during startup and shutdown to
     */
    protected final Logger logger;

    private ResourceProvider resourceProvider;

    /**
     * The <code>Felix</code> instance loaded on {@link #init()} and stopped
     * on {@link #destroy()}.
     */
    private Felix felix;

    /**
     * The <code>BundleContext</code> of the OSGi framework system bundle.
     * This is used for service registration and service access to get at the
     * delegatee servlet.
     */
    private BundleContext bundleContext;

    /**
     * Initializes this servlet by loading the framework configuration
     * properties, starting the OSGi framework (Apache Felix) and exposing the
     * system bundle context and the <code>Felix</code> instance as servlet
     * context attributes.
     *
     * @throws BundleException if the framework cannot be initialized.
     */
    public Sling(Logger logger, ResourceProvider resourceProvider,
            Map<String, String> propOverwrite) throws BundleException {

        this.logger = logger;
        this.resourceProvider = resourceProvider;

        this.logger.log(Logger.LOG_INFO, "Starting Sling");

        // read the default parameters
        Map<String, String> props = this.loadConfigProperties(propOverwrite);

        // check for auto-start bundles
        this.setInstallBundles(props);

        // ensure execution environment
        this.setExecutionEnvironment(props);

        // make sure Felix does not exit the VM when terminating ...
        props.put(EMBEDDED_EXECUTION_PROP, "true");

        // the custom activator list just contains this servlet
        List<BundleActivator> activators = new ArrayList<BundleActivator>();
        activators.add(this);
        activators.add(new BootstrapInstaller(logger, resourceProvider));

        // create the framework and start it
        Felix tmpFelix = new Felix(logger, props, activators);
        tmpFelix.start();

        // only assign field if start succeeds
        this.felix = tmpFelix;

        // log sucess message
        this.logger.log(Logger.LOG_INFO, "Sling started");
    }

    /**
     * Destroys this servlet by shutting down the OSGi framework and hence the
     * delegatee servlet if one is set at all.
     */
    public final void destroy() {
        // shutdown the Felix container
        if (felix != null) {
            logger.log(Logger.LOG_INFO, "Shutting down Sling");
            felix.stopAndWait();
            logger.log(Logger.LOG_INFO, "Sling stopped");
            felix = null;
        }
    }

    // ---------- BundleActivator ----------------------------------------------

    /**
     * Called when the OSGi framework is being started. This implementation
     * registers as a service listener for the
     * <code>javax.servlet.Servlet</code> class and calls the
     * {@link #doStartBundle()} method for implementations to execute more
     * startup tasks. Additionally the <code>context</code> URL protocol
     * handler is registered.
     *
     * @param bundleContext The <code>BundleContext</code> of the system
     *            bundle of the OSGi framework.
     * @throws Exception May be thrown if the {@link #doStartBundle()} throws.
     */
    public final void start(BundleContext bundleContext) throws Exception {
        this.bundleContext = bundleContext;

        // register the context URL handler
        Hashtable<String, Object> props = new Hashtable<String, Object>();
        props.put(URLConstants.URL_HANDLER_PROTOCOL, new String[] { "context" });
        ContextProtocolHandler contextHandler = new ContextProtocolHandler(
            this.resourceProvider);
        bundleContext.registerService(URLStreamHandlerService.class.getName(),
            contextHandler, props);

        // execute optional bundle startup tasks of an extension
        this.doStartBundle();
    }

    /**
     * Called when the OSGi framework is being shut down. This implementation
     * first calls the {@link #doStopBundle()} method method before
     * unregistering as a service listener and ungetting an servlet delegatee if
     * one has been acquired.
     *
     * @param bundleContext The <code>BundleContext</code> of the system
     *            bundle of the OSGi framework.
     */
    public final void stop(BundleContext bundleContext) {
        // execute optional bundle stop tasks of an extension
        try {
            this.doStopBundle();
        } catch (Exception e) {
            this.logger.log(Logger.LOG_ERROR, "Unexpected exception caught", e);
        }

        // drop bundle context reference
        this.bundleContext = null;
    }

    // ---------- Configuration Loading ----------------------------------------

    /**
     * Loads the configuration properties in the configuration property file
     * associated with the framework installation; these properties are
     * accessible to the framework and to bundles and are intended for
     * configuration purposes. By default, the configuration property file is
     * located in the <tt>conf/</tt> directory of the Felix installation
     * directory and is called "<tt>config.properties</tt>". The
     * installation directory of Felix is assumed to be the parent directory of
     * the <tt>felix.jar</tt> file as found on the system class path property.
     * The precise file from which to load configuration properties can be set
     * by initializing the "<tt>felix.config.properties</tt>" system
     * property to an arbitrary URL.
     *
     * @return A <tt>Properties</tt> instance or <tt>null</tt> if there was
     *         an error.
     */
    private Map<String, String> loadConfigProperties(
            Map<String, String> propOverwrite) throws BundleException {
        // The config properties file is either specified by a system
        // property or it is in the same directory as the Felix JAR file.
        // Try to load it from one of these places.
        Map<String, String> props = new HashMap<String, String>();

        // Read the properties file.
        this.load(props, CONFIG_PROPERTIES);

        // resolve inclusions (and remove property)
        this.loadIncludes(props, null);

        // overwrite default properties with initial overwrites
        if (propOverwrite != null) {
            props.putAll(propOverwrite);
        }

        // check whether sling.home is overwritten by system property
        String slingHome = System.getProperty(SLING_HOME);
        if (slingHome == null || slingHome.length() == 0) {

            // no system property, ensure default setting
            slingHome = props.get(SLING_HOME);
            if (slingHome == null || slingHome.length() == 0) {
                slingHome = "sling";
                this.logger.log(Logger.LOG_INFO,
                    "sling.home is not defined. Using '" + slingHome + "'");
            }
        }

        // resolve variables and ensure sling.home is an absolute path
        slingHome = substVars(slingHome, SLING_HOME, null, props);
        File slingHomeFile = new File(slingHome).getAbsoluteFile();
        slingHome = slingHomeFile.getAbsolutePath();

        // overlay with ${sling.home}/sling.properties
        this.logger.log(Logger.LOG_INFO, "Starting sling in " + slingHome);
        File propFile = new File(slingHome, CONFIG_PROPERTIES);
        this.load(props, propFile);

        // create a copy of the properties to perform variable substitution
        Map<String, String> origProps = props;
        props = new HashMap<String, String>();
        props.putAll(origProps);

        // check system properties for any overrides (except sling.home !)
        String ignoreSystemProperties = props.get(SLING_IGNORE_SYSTEM_PROPERTIES);
        if (!"true".equalsIgnoreCase(ignoreSystemProperties)) {
            for (String name : props.keySet()) {
                String sysProp = System.getProperty(name);
                if (sysProp != null) {
                    props.put(name, sysProp);
                }
            }
        }

        // resolve inclusions again
        this.loadIncludes(props, slingHome);

        // overwrite properties, this is not persisted as such
        this.loadPropertiesOverride(props);

        // resolve boot delegation and system packages
        this.resolve(props, "org.osgi.framework.bootdelegation",
            "sling.bootdelegation.");
        this.resolve(props, "org.osgi.framework.system.packages",
            "sling.system.packages.");

        // reset back the sling home property
        // might have been overwritten by system properties, included
        // files or the sling.properties file
        origProps.put(SLING_HOME, slingHome);
        props.put(SLING_HOME, slingHome);
        props.put(SLING_HOME_URL, slingHomeFile.toURI().toString());

        // Perform variable substitution for system properties.
        for (Entry<String, String> entry : props.entrySet()) {
            entry.setValue(substVars(entry.getValue(), entry.getKey(), null,
                props));
        }

        // look for context:/ URLs to substitute
        for (Entry<String, String> entry : props.entrySet()) {
            String name = entry.getKey();
            String value = entry.getValue();
            if (value != null && value.startsWith("context:/")) {
                String path = value.substring("context:/".length() - 1);

                InputStream src = this.resourceProvider.getResourceAsStream(path);
                if (src != null) {
                    File target = new File(slingHome, path);
                    OutputStream dest = null;
                    try {
                        // only copy file if not existing
                        if (!target.exists()) {
                            target.getParentFile().mkdirs();
                            dest = new FileOutputStream(target);
                            byte[] buf = new byte[2048];
                            int rd;
                            while ((rd = src.read(buf)) >= 0) {
                                dest.write(buf, 0, rd);
                            }
                        }

                        // after copying replace property and add url property
                        entry.setValue(target.getAbsolutePath());

                        // also set the new property on the unsubstituted props
                        origProps.put(name, "${sling.home}" + path);

                    } catch (IOException ioe) {
                        this.logger.log(Logger.LOG_ERROR, "Cannot copy file "
                            + value + " to " + target, ioe);
                    } finally {
                        if (dest != null) {
                            try {
                                dest.close();
                            } catch (IOException ignore) {
                            }
                        }
                        try {
                            src.close();
                        } catch (IOException ignore) {
                        }
                    }
                }
            }
        }

        // write the unsubstituted properties back to the overlay file
        OutputStream os = null;
        try {
            // ensure parent folder(s)
            propFile.getParentFile().mkdirs();

            os = new FileOutputStream(propFile);

            // copy the values into a temporary properties structure to store
            Properties tmp = new Properties();
            tmp.putAll(origProps);
            tmp.store(os, "Overlay properties for configuration");
        } catch (Exception ex) {
            this.logger.log(Logger.LOG_ERROR,
                "Error loading overlay properties from " + propFile, ex);
        } finally {
            if (os != null) {
                try {
                    os.close();
                } catch (IOException ex2) {
                    // Nothing we can do.
                }
            }
        }

        return props;
    }

    /**
     * Scans the properties for any properties starting with the given
     * <code>prefix</code> (e.g. <code>sling.bootdelegation.</code>).
     * <ol>
     * <li>Each such property is checked, whether it actually starts with
     * <code>prefix<b>class.</b></code>. If so, the rest of the property
     * name is assumed to be a fully qualified class name which is check,
     * whether it is visible. If so, the value of the property is appended to
     * the value of the <code>osgiProp</code>. If the class cannot be loaded,
     * the property is ignored.
     * <li>Otherwise, if the property does not contain a fully qualified class
     * name, the value of the property is simply appended to the
     * <code>osgiProp</code>.
     * </ol>
     *
     * @param props The <code>Properties</code> to be scanned.
     * @param osgiProp The name of the property in <code>props</code> to which
     *            any matching property values are appended.
     * @param prefix The prefix of properties to handle.
     */
    private void resolve(Map<String, String> props, String osgiProp,
            String prefix) {
        final String propVal = props.get(osgiProp);
        StringBuffer prop = new StringBuffer(propVal == null ? "" : propVal);
        boolean mod = false;
        for (Entry<String, String> pEntry : props.entrySet()) {
            String key = pEntry.getKey();
            if (key.startsWith(prefix)) {
                if (key.indexOf("class.") == prefix.length()) {
                    // prefix is followed by checker class name
                    String className = key.substring(prefix.length()
                        + "class.".length());
                    try {
                        Class.forName(className, true,
                            this.getClass().getClassLoader());
                    } catch (Throwable t) {
                        // don't really care, but class checking failed, so we
                        // do not add
                        this.logger.log(Logger.LOG_DEBUG, "Class " + className
                            + " not found. Ignoring '" + pEntry.getValue()
                            + "' for property " + osgiProp);
                        continue;
                    }
                }

                // get here if class is known or no checker class
                this.logger.log(Logger.LOG_DEBUG, "Adding '"
                    + pEntry.getValue() + "' to property " + osgiProp);
                if (prop.length() > 0) {
                    prop.append(',');
                }
                prop.append(pEntry.getValue());
                mod = true;
            }
        }

        // replace the property with the modified property
        if (mod) {
            this.logger.log(Logger.LOG_DEBUG, "Setting property " + osgiProp
                + " to " + prop.toString());
            props.put(osgiProp, prop.toString());
        }
    }

    private void setInstallBundles(Map<String, String> props) {
        String prefix = "sling.install.";
        Set<String> levels = new TreeSet<String>();
        for (String key : props.keySet()) {
            if (key.startsWith(prefix)) {
                levels.add(key.substring(prefix.length()));
            }
        }

        StringBuffer buf = new StringBuffer();
        for (String level : levels) {
            if (buf.length() > 0) {
                buf.append(',');
            }
            buf.append(level);
        }

        props.put(prefix + "bundles", buf.toString());
    }

    /**
     * Ensures sensible Execution Environment setting. If the
     * <code>org.osgi.framework.executionenvironment</code> property is set in
     * the configured properties or the system properties, we ensure that older
     * settings for J2SE-1.2, J2SE-1.3 and J2SE-1.4 are included. If the
     * property is neither set in the configuration properties nor in the system
     * properties, the property is not set.
     *
     * @param props The configuration properties to check and optionally ammend.
     */
    private void setExecutionEnvironment(Map<String, String> props) {
        // get the current Execution Environment setting
        String ee = props.get(Constants.FRAMEWORK_EXECUTIONENVIRONMENT);
        if (ee == null) {
            ee = System.getProperty(Constants.FRAMEWORK_EXECUTIONENVIRONMENT);
        }

        // if there is a setting, ensure J2SE-1.2/3/4/5 is included in the list
        if (ee != null) {
            int javaMinor;
            try {
                String specVString = System.getProperty("java.specification.version");
                javaMinor = Version.parseVersion(specVString).getMinor();
            } catch (IllegalArgumentException iae) {
                // don't care, assume minimal sling version (1.5)
                javaMinor = 5;
            }

            for (int i = 2; i <= javaMinor; i++) {
                String exEnv = "J2SE-1." + i;
                if (ee.indexOf(exEnv) < 0) {
                    ee += "," + exEnv;
                }
            }

            this.logger.log(Logger.LOG_INFO,
                "Using Execution Environment setting: " + ee);
            props.put(Constants.FRAMEWORK_EXECUTIONENVIRONMENT, ee);
        } else {
            this.logger.log(Logger.LOG_INFO,
                "Not using Execution Environment setting");
        }
    }

    // ---------- Extension support --------------------------------------------

    /**
     * Loads additional properties into the <code>properties</code> object.
     * <p>
     * This implementation does nothing and may be overwritten by extensions
     * requiring additional properties to be set.
     * <p>
     * This method is called when the servlet is initialized to prepare the
     * configuration for <code>Felix</code>. Implementations may add
     * properties from implementation specific sources. Properties added here
     * overwrite properties loaded from the default properties file and may be
     * overwritten by parameters set in the web application.
     * <p>
     * The <code>properties</code> object has not undergone variable
     * substition and properties added by this method may also contain values
     * refererring to other properties.
     * <p>
     * The properties added in this method will not be persisted in the
     * <code>sling.properties</code> file in the <code>sling.home</code>
     * directory.
     *
     * @param properties The <code>Properties</code> object to which custom
     *            properties may be added.
     */
    protected void loadPropertiesOverride(Map<String, String> properties) {
    }

    /**
     * Returns the <code>BundleContext</code> of the system bundle of the OSGi
     * framework launched by this servlet. This method only returns a non-<code>null</code>
     * object after the system bundle of the framework has been started and
     * before it is being stopped.
     */
    protected final BundleContext getBundleContext() {
        return this.bundleContext;
    }

    /**
     * Executes additional startup tasks and is called by the
     * {@link #start(BundleContext)} method.
     * <p>
     * This implementation does nothing and may be overwritten by extensions
     * requiring additional startup tasks.
     *
     * @throws Exception May be thrown in case of problems.
     */
    protected void doStartBundle() throws Exception {
    }

    /**
     * Executes additional shutdown tasks and is called by the
     * {@link #stop(BundleContext)} method.
     * <p>
     * This implementation does nothing and may be overwritten by extensions
     * requiring additional shutdown tasks.
     * <p>
     * When overwriting this method, it must be made sure, that no exception may
     * be thrown, otherwise unexpected behaviour may result.
     */
    protected void doStopBundle() {
    }

    // ---------- Property file support ----------------------------------------

    /**
     * Looks for <code>sling.include</code> and <code>sling.include.*</code>
     * properties in the <code>props</code> and loads properties form the
     * respective locations.
     * <p>
     * Each <code>sling.include</code> (or <code>sling.include.*</code>)
     * property may contain a comma-separated list of resource and/or file names
     * to be loaded from. The includes are loaded in alphabetical order of the
     * property names.
     * <p>
     * Each reasource path is first tried to be loaded through the
     * {@link #resourceProvider}. If that fails, the resource path is tested as
     * a file. If relative <code>slingHome</code> is used as the parent if not
     * <code>null</code>, otherwise the current working directory is used as
     * the parent.
     * <p>
     * Any non-existing resource is silently ignored.
     * <p>
     * When the method returns, the <code>sling.include</code> and
     * <code>sling.include.*</code> properties are not contained in the
     * <code>props</code> any more.
     *
     * @param props The <code>Properties</code> containing the
     *            <code>sling.include</code> and <code>sling.include.*</code>
     *            properties. This is also the destination for the new
     *            properties loaded.
     * @param slingHome The parent directory used to resolve relative path names
     *            if loading from a file. This may be <code>null</code> in
     *            which case the current working directory is used as the
     *            parent.
     */
    private void loadIncludes(Map<String, String> props, String slingHome) {
        // Build the sort map of include properties first
        // and remove include elements from the properties
        SortedMap<String, String> includes = new TreeMap<String, String>();
        for (Iterator<Entry<String, String>> pi = props.entrySet().iterator(); pi.hasNext();) {
            Entry<String, String> entry = pi.next();
            if (entry.getKey().startsWith("sling.include.")
                || entry.getKey().equals("sling.include")) {
                includes.put(entry.getKey(), entry.getValue());
                pi.remove();
            }
        }

        for (Iterator<Entry<String, String>> ii = includes.entrySet().iterator(); ii.hasNext();) {
            Map.Entry<String, String> entry = ii.next();
            String key = entry.getKey();
            String include = entry.getValue();

            // ensure variable resolution on this property
            include = substVars(include, key, null, props);

            StringTokenizer tokener = new StringTokenizer(include, ",");
            while (tokener.hasMoreTokens()) {
                String file = tokener.nextToken().trim();
                InputStream is = this.resourceProvider.getResourceAsStream(file);
                try {
                    if (is == null && slingHome != null) {
                        File resFile = new File(file);
                        if (!resFile.isAbsolute()) {
                            resFile = new File(slingHome, file);
                        }
                        if (resFile.canRead()) {
                            is = new FileInputStream(resFile);
                            file = resFile.getAbsolutePath(); // for logging
                        }
                    }

                    if (is != null) {
                        this.load(props, is);
                    }
                } catch (IOException ioe) {
                    this.logger.log(Logger.LOG_ERROR,
                        "Error loading config properties from " + file, ioe);
                }
            }
        }
    }

    /**
     * Load properties from the given resource file, which is accessed through
     * the {@link #resourceProvider}. If the resource does not exist, nothing
     * is loaded.
     *
     * @param props The <code>Properties</code> into which the loaded
     *            properties are loaded
     * @param resource The resource from which to load the resources
     */
    private void load(Map<String, String> props, String resource) {
        InputStream is = this.resourceProvider.getResourceAsStream(resource);
        if (is != null) {
            try {
                this.load(props, is);
            } catch (IOException ioe) {
                this.logger.log(Logger.LOG_ERROR,
                    "Error loading config properties from " + resource, ioe);
            }
        }
    }

    /**
     * Load properties from the given file. If the resource cannot be read from
     * (e.g. because it does not exist), nothing is loaded.
     *
     * @param props The <code>Properties</code> into which the loaded
     *            properties are loaded
     * @param file The <code>File</code> to load the properties from
     */
    private void load(Map<String, String> props, File file) {
        if (file != null && file.canRead()) {
            try {
                this.load(props, new FileInputStream(file));
            } catch (IOException ioe) {
                this.logger.log(Logger.LOG_ERROR,
                    "Error loading config properties from "
                        + file.getAbsolutePath(), ioe);
            }
        }
    }

    private void load(Map<String, String> props, InputStream ins)
            throws IOException {
        try {
            Properties tmp = new Properties();
            tmp.load(ins);

            for (Map.Entry<Object, Object> entry : tmp.entrySet()) {
                props.put((String) entry.getKey(), (String) entry.getValue());
            }
        } finally {
            try {
                ins.close();
            } catch (IOException ioe2) {
                // ignore
            }
        }
    }

    // ---------- Property file variable substition support --------------------

    /**
     * The starting delimiter of variable names (value is "${").
     */
    private static final String DELIM_START = "${";

    /**
     * The ending delimiter of variable names (value is "}").
     */
    private static final String DELIM_STOP = "}";

    /**
     * This method performs property variable substitution on the specified
     * value. If the specified value contains the syntax
     * <tt>${&lt;prop-name&gt;}</tt>, where <tt>&lt;prop-name&gt;</tt>
     * refers to either a configuration property or a system property, then the
     * corresponding property value is substituted for the variable placeholder.
     * Multiple variable placeholders may exist in the specified value as well
     * as nested variable placeholders, which are substituted from inner most to
     * outer most. Configuration properties override system properties.
     *
     * @param val The string on which to perform property substitution.
     * @param currentKey The key of the property being evaluated used to detect
     *            cycles.
     * @param cycleMap Map of variable references used to detect nested cycles.
     * @param configProps Set of configuration properties.
     * @return The value of the specified string after system property
     *         substitution.
     * @throws IllegalArgumentException If there was a syntax error in the
     *             property placeholder syntax or a recursive variable
     *             reference.
     */
    private static String substVars(String val, String currentKey,
            Map<String, String> cycleMap, Map<String, String> configProps)
            throws IllegalArgumentException {
        // If there is currently no cycle map, then create
        // one for detecting cycles for this invocation.
        if (cycleMap == null) {
            cycleMap = new HashMap<String, String>();
        }

        // Put the current key in the cycle map.
        cycleMap.put(currentKey, currentKey);

        // Assume we have a value that is something like:
        // "leading ${foo.${bar}} middle ${baz} trailing"

        // Find the first ending '}' variable delimiter, which
        // will correspond to the first deepest nested variable
        // placeholder.
        int stopDelim = val.indexOf(DELIM_STOP);

        // Find the matching starting "${" variable delimiter
        // by looping until we find a start delimiter that is
        // greater than the stop delimiter we have found.
        int startDelim = val.indexOf(DELIM_START);
        while (stopDelim >= 0) {
            int idx = val.indexOf(DELIM_START, startDelim
                + DELIM_START.length());
            if ((idx < 0) || (idx > stopDelim)) {
                break;
            } else if (idx < stopDelim) {
                startDelim = idx;
            }
        }

        // If we do not have a start or stop delimiter, then just
        // return the existing value.
        if ((startDelim < 0) && (stopDelim < 0)) {
            return val;
        }
        // At this point, we found a stop delimiter without a start,
        // so throw an exception.
        else if (((startDelim < 0) || (startDelim > stopDelim))
            && (stopDelim >= 0)) {
            throw new IllegalArgumentException(
                "stop delimiter with no start delimiter: " + val);
        }

        // At this point, we have found a variable placeholder so
        // we must perform a variable substitution on it.
        // Using the start and stop delimiter indices, extract
        // the first, deepest nested variable placeholder.
        String variable = val.substring(startDelim + DELIM_START.length(),
            stopDelim);

        // Verify that this is not a recursive variable reference.
        if (cycleMap.get(variable) != null) {
            throw new IllegalArgumentException("recursive variable reference: "
                + variable);
        }

        // Get the value of the deepest nested variable placeholder.
        // Try to configuration properties first.
        String substValue = (configProps != null)
                ? configProps.get(variable)
                : null;
        if (substValue == null) {
            // Ignore unknown property values.
            substValue = System.getProperty(variable, "");
        }

        // Remove the found variable from the cycle map, since
        // it may appear more than once in the value and we don't
        // want such situations to appear as a recursive reference.
        cycleMap.remove(variable);

        // Append the leading characters, the substituted value of
        // the variable, and the trailing characters to get the new
        // value.
        val = val.substring(0, startDelim) + substValue
            + val.substring(stopDelim + DELIM_STOP.length(), val.length());

        // Now perform substitution again, since there could still
        // be substitutions to make.
        val = substVars(val, currentKey, cycleMap, configProps);

        // Return the value.
        return val;
    }
}

org.apache.sling.launcher.app.BootstrapInstaller, Revision 688973

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.sling.launcher.app;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.felix.framework.Logger;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.service.startlevel.StartLevel;

/**
 * The <code>BootstrapInstaller</code> class is installed into the OSGi
 * framework as an activator to be called when the framework is starting up.
 * Upon startup all bundles from the {@link #PATH_CORE_BUNDLES} and the
 * {@link #PATH_BUNDLES} location are checked whether they are already installed
 * or not. If they are not installed, they are installed, their start level set
 * to 1 and started. Any bundle already installed is not installed again and
 * will also not be started here.
 */
class BootstrapInstaller implements BundleActivator {

    /**
     * The Bundle location scheme (protocol) used for bundles installed by this
     * activator (value is "slinginstall:"). The path part of the Bundle
     * location of Bundles installed by this class is the name (without the
     * path) of the resource from which the Bundle was installed.
     */
    public static final String SCHEME = "slinginstall:";

    /**
     * The location the core Bundles (value is "resources/corebundles"). These
     * bundles are installed first.
     */
    public static final String PATH_CORE_BUNDLES = "resources/corebundles";

    /**
     * The location the additional Bundles (value is "resources/bundles"). These
     * Bundles are installed after the {@link #PATH_CORE_BUNDLES core Bundles}.
     */
    public static final String PATH_BUNDLES = "resources/bundles";

    /**
     * The {@link Logger} use for logging messages during installation and
     * startup.
     */
    private final Logger logger;

    /**
     * The {@link ResourceProvider} used to access the Bundle jar files to
     * install.
     */
    private final ResourceProvider resourceProvider;

    /** The data file which works as a marker to detect the first startup. */
    private static final String DATA_FILE = "bootstrapinstaller.ser";

    BootstrapInstaller(Logger logger, ResourceProvider resourceProvider) {
        this.logger = logger;
        this.resourceProvider = resourceProvider;
    }

    /**
     * Installs any Bundles missing in the current framework instance. The
     * Bundles are verified by the Bundle location string. All missing Bundles
     * are first installed and then started in the order of installation.
     * Also install all deployment packages.
     *
     * This installation stuff is only performed during the first startup!
     */
    public void start(BundleContext context) throws Exception {
        boolean alreadyInstalled = false;
        final File dataFile = context.getDataFile(DATA_FILE);
        if ( dataFile != null && dataFile.exists() ) {
            try {
                final FileInputStream fis = new FileInputStream(dataFile);
                try {
                    final ObjectInputStream ois = new ObjectInputStream(fis);
                    try {
                        alreadyInstalled = ois.readBoolean();
                    } finally {
                        try {
                            ois.close();
                        } catch (IOException ignore) {}
                    }
                } finally {
                    try {
                        fis.close();
                    } catch (IOException ignore) {}
                }
            } catch (IOException ioe) {
                logger.log(Logger.LOG_ERROR, "IOException during reading of installed flag.", ioe);
            }
        }

        if ( !alreadyInstalled ) {
            // register deployment package support
            final DeploymentPackageInstaller dpi =
                new DeploymentPackageInstaller(context, logger, resourceProvider);
            context.addFrameworkListener(dpi);
            context.addServiceListener(dpi, "("
                    + Constants.OBJECTCLASS + "=" + DeploymentPackageInstaller.DEPLOYMENT_ADMIN + ")");

            // list all existing bundles
            Bundle[] bundles = context.getBundles();
            Map<String, Bundle> byLocation = new HashMap<String, Bundle>();
            for (int i = 0; i < bundles.length; i++) {
                byLocation.put(bundles[i].getLocation(), bundles[i]);
            }

            // install bundles
            List<Bundle> installed = new LinkedList<Bundle>();
            installBundles(context, byLocation, PATH_CORE_BUNDLES, installed);
            installBundles(context, byLocation, PATH_BUNDLES, installed);

            try {
                final FileOutputStream fos = new FileOutputStream(dataFile);
                try {
                    final ObjectOutputStream oos = new ObjectOutputStream(fos);
                    try {
                        oos.writeBoolean(true);
                    } finally {
                        try {
                            oos.close();
                        } catch (IOException ignore) {}
                    }
                } finally {
                    try {
                        fos.close();
                    } catch (IOException ignore) {}
                }
            } catch (IOException ioe) {
                logger.log(Logger.LOG_ERROR, "IOException during writing of installed flag.", ioe);
            }

            // set start levels on the bundles and start them
            startBundles(context, installed);
        }
    }

    /** Nothing to be done on stop */
    public void stop(BundleContext context) {
    }

    /**
     * Install the Bundles from JAR files found in the given <code>parent</code>
     * path.
     *
     * @param context The <code>BundleContext</code> used to install the new
     *            Bundles.
     * @param currentBundles The currently installed Bundles indexed by their
     *            Bundle location.
     * @param parent The path to the location in which to look for JAR files to
     *            install. Only resources whose name ends with <em>.jar</em>
     *            are considered for installation.
     * @param installed The list of Bundles installed by this method. Each
     *            Bundle successfully installed is added to this list.
     */
    private void installBundles(BundleContext context,
            Map<String, Bundle> currentBundles, String parent,
            List<Bundle> installed) {

        Iterator<String> res = resourceProvider.getChildren(parent);
        while (res.hasNext()) {

            String path = res.next();

            if (path.endsWith(".jar")) {

                // check for an already installed Bundle with the given location
                String location = SCHEME
                    + path.substring(path.lastIndexOf('/') + 1);
                if (currentBundles.containsKey(location)) {
                    continue;
                }

                // try to access the JAR file, ignore if not possible
                InputStream ins = resourceProvider.getResourceAsStream(path);
                if (ins == null) {
                    continue;
                }

                // install the JAR file as a bundle
                Bundle newBundle;
                try {
                    newBundle = context.installBundle(location, ins);
                    logger.log(Logger.LOG_INFO, "Bundle "
                        + newBundle.getSymbolicName() + " installed from "
                        + location);
                } catch (BundleException be) {
                    logger.log(Logger.LOG_ERROR, "Bundle installation from "
                        + location + " failed", be);
                    continue;
                }

                // finally add the bundle to the list for later start
                installed.add(newBundle);
            }
        }
    }

    /**
     * Starts the Bundles in the <code>bundles</code> list. If the framework
     * provides an active <code>StartLevel</code> service, the start levels of
     * the Bundles is first set to <em>1</em>.
     */
    private void startBundles(BundleContext context, List<Bundle> bundles) {

        // the start level service to set the initial start level
        ServiceReference ref = context.getServiceReference(StartLevel.class.getName());
        StartLevel startLevel = (ref != null)
                ? (StartLevel) context.getService(ref)
                : null;

        // start all bundles
        for (Bundle bundle : bundles) {

            if (startLevel != null) {
                startLevel.setBundleStartLevel(bundle, 1);
            }

            try {
                bundle.start();
            } catch (BundleException be) {
                logger.log(Logger.LOG_ERROR, "Bundle "
                    + bundle.getSymbolicName() + " could not be started", be);
            }
        }

        // release the start level service
        if (ref != null) {
            context.ungetService(ref);
        }
    }

}

org.apache.sling.launcher.app.main.Main, Revision 667440

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sling.launcher.app.main;

import static org.apache.felix.framework.util.FelixConstants.LOG_LEVEL_PROP;

import java.util.HashMap;
import java.util.Map;

import org.apache.felix.framework.Logger;
import org.apache.sling.commons.log.LogManager;
import org.apache.sling.launcher.app.ClassLoaderResourceProvider;
import org.apache.sling.launcher.app.ResourceProvider;
import org.apache.sling.launcher.app.Sling;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;

/**
 * The <code>Main</code> class is a simple Java Application which interprests
 * the command line and creates the {@link Sling} launcher class and thus starts
 * the OSGi framework. In addition a shutdown thread is registered to ensure
 * proper shutdown on VM termination.
 * <p>
 * The supported command line options are:
 * <dl>
 * <dt>-l loglevel</dt>
 * <dd>Sets the initial loglevel as an integer in the range 0 to 4 or as one of
 * the well known level strings FATAL, ERROR, WARN, INFO or DEBUG. This option
 * overwrites the <code>org.apache.sling.osg.log.level</code> setting the
 * <code>sling.properties</code> file.</dd>
 * <dt>-f logfile</dt>
 * <dd>The log file, \"-\" for stdout (default logs/error.log). This option
 * overwrites the <code>org.apache.sling.osg.log.file</code> setting the
 * <code>sling.properties</code> file.</dd>
 * <dt>-c slinghome</dt>
 * <dd>The directory in which Sling locates its initial configuration file
 * <code>sling.properties</code> and where files of Sling itself such as the
 * Apache Felix bundle archive or the JCR repository files are stored (default
 * sling).</dd>
 * <dt>-a address</dt>
 * <dd>The interfact to bind to (use 0.0.0.0 for any). This option is not
 * implemented yet.</dd>
 * <dt>-p port</dt>
 * <dd>The port to listen (default 8080) to handle HTTP requests. This option
 * overwrites the <code>org.osgi.service.http.port</code> setting the
 * <code>sling.properties</code> file.</dd>
 * <dt>-h</dt>
 * <dd>Prints a simple usage message listing all available command line
 * options.</dd>
 * </dl>
 */
public class Main {

    /** Mapping between log level numbers and names */
    private static final String[] logLevels = { "FATAL", "ERROR", "WARN",
        "INFO", "DEBUG" };

    /** The Sling configuration property name setting the initial log level */
    private static final String PROP_LOG_LEVEL = LogManager.LOG_LEVEL;

    /** The Sling configuration property name setting the initial log file */
    private static final String PROP_LOG_FILE = LogManager.LOG_FILE;

    /** Default log level setting if no set on command line (value is "INFO"). */
    private static final int DEFAULT_LOG_LEVEL = Logger.LOG_INFO;

    /**
     * The configuration property setting the port on which the HTTP service
     * listens
     */
    private static final String PROP_PORT = "org.osgi.service.http.port";

    /** The default port on which the HTTP service listens. */
    private static final String DEFAULT_PORT = "8080";

    /**
     * The property value to export the Servlet API 2.5 from the system
     * bundle.
     */
    private static final String SERVLET_API_EXPORT =
        "javax.servlet;javax.servlet.http;javax.servlet.resources; version=2.5";

    /** The parsed command line mapping (Sling) option name to option value */
    private static Map<String, String> commandLine;

    public static void main(String[] args) throws Exception {
        // creating the instance launches the framework and we are done here ..
        Map<String, String> props = new HashMap<String, String>();

        // parse the command line (exit in case of failure)
        commandLine = new HashMap<String, String>();
        commandLine.put(PROP_PORT, DEFAULT_PORT);
        parseCommandLine(args, commandLine);

        // if sling.home was set on the command line, set it in the properties
        if (commandLine.containsKey(Sling.SLING_HOME)) {
            props.put(Sling.SLING_HOME, commandLine.get(Sling.SLING_HOME));
        }

        // overwrite the loadPropertiesOverride method to inject the command
        // line arguments unconditionally. These will not be persisted in any
        // properties file, though

        // set up and configure Felix Logger
        int logLevel;
        if (!commandLine.containsKey(PROP_LOG_LEVEL)) {
            logLevel = DEFAULT_LOG_LEVEL;
        } else {
            logLevel = toLogLevelInt(commandLine.get(PROP_LOG_LEVEL),
                DEFAULT_LOG_LEVEL);
            commandLine.put(LOG_LEVEL_PROP, String.valueOf(logLevel));
        }
        Logger logger = new Logger();

        // prevent tons of needless WARN from the framework
        // logger.setLogLevel(logLevel);
        logger.setLogLevel(Logger.LOG_ERROR);

        // prevent tons of needless WARN messages from the framework
        // logger.setLogLevel(logLevel);
        logger.setLogLevel(Logger.LOG_ERROR);

        try {
            ResourceProvider resProvider = new ClassLoaderResourceProvider(
                Main.class.getClassLoader());
            Sling sling = new Sling(logger, resProvider, props) {
                protected void loadPropertiesOverride(
                        Map<String, String> properties) {
                    if (commandLine != null) {
                        properties.putAll(commandLine);
                    }

                    // add Servlet API to the system bundle exports
                    String sysExport = properties.get(Constants.FRAMEWORK_SYSTEMPACKAGES);
                    if (sysExport == null) {
                        sysExport = SERVLET_API_EXPORT;
                    } else {
                        sysExport += "," + SERVLET_API_EXPORT;
                    }
                    properties.put(Constants.FRAMEWORK_SYSTEMPACKAGES,
                        sysExport);
                }
            };

            Runtime.getRuntime().addShutdownHook(new TerminateSling(sling));
        } catch (BundleException be) {
            log("Failed to Start OSGi framework");
            be.printStackTrace(System.err);
            System.exit(2);
        }
    }

    private static class TerminateSling extends Thread {
        private final Sling sling;

        TerminateSling(Sling sling) {
            super("Sling Terminator");
            this.sling = sling;
        }

        public void run() {
            if (this.sling != null) {
                this.sling.destroy();
            }
        }
    }

    /**
     * Parses the command line in <code>args</code> and sets appropriate Sling
     * configuration options in the <code>props</code> map.
     */
    private static void parseCommandLine(String[] args,
            Map<String, String> props) {
        for (int argc = 0; argc < args.length; argc++) {
            String arg = args[argc];
            if (arg.startsWith("-")) {

                // require at least another character naming the option
                if (arg.length() != 2) {
                    usage("Missing option name", 1);
                }

                // option argument is following the current option
                argc++;
                String value = argc < args.length ? args[argc] : null;

                switch (arg.charAt(1)) {
                    case 'l':
                        if (value == null) {
                            usage("Missing log level value", 1);
                            continue;
                        }
                        try {
                            int logLevel = Integer.parseInt(value);
                            value = toLogLevel(logLevel);
                        } catch (NumberFormatException nfe) {
                            // might be a log level string
                            value = checkLogLevel(value);
                        }
                        if (value != null) {
                            props.put(PROP_LOG_LEVEL, value);
                        }
                        break;

                    case 'f':
                        if (value == null) {
                            usage("Missing log file value", 1);
                            continue;
                        } else if ("-".equals(value)) {
                            value = "";
                        }
                        props.put(PROP_LOG_FILE, value);
                        break;

                    case 'c':
                        if (value == null) {
                            usage("Missing directory value", 1);
                            continue;
                        }
                        props.put(Sling.SLING_HOME, value);
                        break;

                    case 'p':
                        if (value == null) {
                            usage("Missing port value", 1);
                            continue;
                        }
                        try {
                            // just to verify it is a number
                            Integer.parseInt(value);
                            props.put(PROP_PORT, value);
                        } catch (RuntimeException e) {
                            usage("Bad port: " + value, 1);
                        }
                        break;

                    case 'a':
                        if (value == null) {
                            usage("Missing address value", 1);
                            continue;
                        }
                        log("Setting the address to bind to is not supported, binding to 0.0.0.0");
                        break;

                    case 'h':
                        usage(null, 0);

                    default:
                        usage("Unrecognized option " + arg, 1);
                        break;
                }
            }
        }
    }

    /** prints a simple usage plus optional error message and exists with code */
    private static void usage(String message, int code) {
        if (message != null) {
            log(message);
            log("");
        }

        log("usage: "
            + Main.class.getName()
            + " [ -l loglevel ] [ -f logfile ] [ -c slinghome ] [ -a address ] [ -p port ] [ -h ]");

        log("    -l loglevel   the initial loglevel (0..4, FATAL, ERROR, WARN, INFO, DEBUG)");
        log("    -f logfile    the log file, \"-\" for stdout (default logs/error.log)");
        log("    -c slinghome  the sling context directory (default sling)");
        log("    -a address    the interfact to bind to (use 0.0.0.0 for any) (not supported yet)");
        log("    -p port       the port to listen to (default 8080)");
        log("    -h            prints this usage message");

        // exiting now
        System.exit(code);
    }

    /** Writes the message to stderr output */
    private static void log(String message) {
        System.err.println(message);
    }

    /** Converts the loglevel code to a loglevel string name */
    private static String toLogLevel(int level) {
        if (level >= 0 && level < logLevels.length) {
            return logLevels[level];
        }

        usage("Bad log level: " + level, 1);
        return null;
    }

    /** Verifies the log level is one of the known values, returns null otherwise */
    private static String checkLogLevel(String level) {
        for (int i = 0; i < logLevels.length; i++) {
            if (logLevels[i].equalsIgnoreCase(level)) {
                return logLevels[i];
            }
        }

        usage("Bad log level: " + level, 1);
        return null;
    }

    /** Return the log level code for the string */
    private static int toLogLevelInt(String level, int defaultLevel) {
        for (int i = 0; i < logLevels.length; i++) {
            if (logLevels[i].equalsIgnoreCase(level)) {
                return i;
            }
        }

        return defaultLevel;
    }
}

Tuesday, September 9, 2008

Sling OSGi Track pt 7: configuring services at runtime

This is the continuation of

In this part, I will explain how to use the OSGi configuration management to configure services at runtime.

Suppose I want a service that can do more than just say "hello". I want the phrase the service returns from the sayHello() method to be configurable at runtime. This is easily done through OSGi configuration management.

Aims:

  • Show how to introduce a configurable property into an OSGi bundle
  • Show how to change the configuration through the OSGi configuration manager

Ingredients:

Files:

Outline:

  • Project Structure
  • Changes to the implementation
  • The pom
  • The results
  • Testing

Execution

Project Structure

From here on, I am no longer bundling the sample client script, as we've done our own client servlet in part 6 of this track. So we're back to just the pom, the interface and the implementation.

.
|-- pom.xml
`-- src
    `-- main
        `-- java
            `-- mh
                `-- osgitest
                    |-- SampleService.java
                    `-- SampleServiceImpl.java
Changes to the implementation

The only semantic change is in lines 18 and 21: a property helloPhrase is introduced and the value that is returned from the "sayHello()" method is read from that property.

Lines 11-14 introduce a constant, PROP_HELLO_PHRASE. The value of that constant will be used as a key to store the actual value. Line 12 points to the DEFAULT_HELLO_PHRASE constant for the default value. The following would have been equivalent, but it's better practice to use constants, so that the default value can be reffered to from Java code.

	/**
	 * @scr.property value="hello"
	 */
	public static final String PROP_HELLO_PHRASE = "mh.osgitest.helloPhrase";

the activate() method is called when the component is activated. At that point, we can obtain configuration values from the properties that are part of the ComponentContext that is passed as the method's argument.

package mh.osgitest;

import org.osgi.service.component.ComponentContext;

/**
 * @scr.component immediate="true"
 * @scr.service interface="SampleService"
 */
public class SampleServiceImpl implements SampleService {

	/**
	 * @scr.property valueRef="DEFAULT_HELLO_PHRASE"
	 */
	public static final String PROP_HELLO_PHRASE = "mh.osgitest.helloPhrase";

	public static final String DEFAULT_HELLO_PHRASE = "hello";

	private String helloPhrase;

	public String sayHello() {
		return helloPhrase;
	}

	protected void activate(ComponentContext context) {
		Object helloPhrase = context.getProperties().get(PROP_HELLO_PHRASE);
		if (helloPhrase != null)
			this.helloPhrase = helloPhrase.toString();
	}
}
The pom

The pom is very straight forward. Nothing special here.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>mh.studies</groupId>
	<artifactId>mh.studies.sling.osgitest</artifactId>
	<name>OSGI Test Bundle</name>
	<version>0.0.7</version>
	<packaging>bundle</packaging>
	<description />
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.5</source>
					<target>1.5</target>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.sling</groupId>
				<artifactId>maven-sling-plugin</artifactId>
				<version>2.0.2-incubator</version>
				<executions>
					<execution>
						<id>install-bundle</id>
						<goals>
							<goal>install</goal>
						</goals>
						<configuration>
							<slingUrl> http://localhost:7402/system/console/install</slingUrl>
							<user>admin</user>
							<password>admin</password>
						</configuration>
					</execution>
				</executions>
			</plugin>
			<plugin>
			    <groupId>org.apache.felix</groupId>
			    <artifactId>maven-scr-plugin</artifactId>
			    <version>1.0.7</version>
			    <executions>
			        <execution>
			            <id>generate-scr-scrdescriptor</id>
			            <goals>
			                <goal>scr</goal>
			            </goals>
			        </execution>
			    </executions>
			</plugin>			
			<plugin>
				<groupId>org.apache.felix</groupId>
				<artifactId>maven-bundle-plugin</artifactId>
				<version>1.4.3</version>
				<extensions>true</extensions>
				<configuration>
					<instructions>
						<Export-Package>mh.osgitest</Export-Package>
						<Import-Package>
                            org.osgi.framework;version="1.3.0",
                            org.osgi.service.component
                        </Import-Package>
						<Bundle-SymbolicName>${pom.artifactId}</Bundle-SymbolicName>
						<Bundle-Name>${pom.name}</Bundle-Name>
						<Bundle-Vendor>Moritz Havelock</Bundle-Vendor>
					</instructions>
				</configuration>
			</plugin>
		</plugins>
	</build>
	<repositories>
		<repository>
			<id>apache.incubating</id>
			<name>Apache Incubating Repository</name>
			<url>http://people.apache.org/repo/m2-incubating-repository</url>
		</repository>
	</repositories>
	<pluginRepositories>
		<pluginRepository>
			<id>apache.incubating.plugins</id>
			<name>Apache Incubating Plugin Repository</name>
			<url>http://people.apache.org/repo/m2-incubating-repository
			</url>
		</pluginRepository>
	</pluginRepositories>
	<dependencies>
		<dependency>
			<groupId>org.apache.felix</groupId>
			<artifactId>org.osgi.core</artifactId>
			<version>1.0.1</version>
		</dependency>
            <dependency>
            <groupId>org.apache.felix</groupId>
            <artifactId>org.osgi.compendium</artifactId>
            <version>1.0.1</version>
        </dependency>
	</dependencies>
</project>
The results

After deploying the Bundle, I do a quick test and call http://localhost:7402/system/osgitest/info. That requires that the Servlet from the last part is still installed. If you have not followed along, grab it from here: http://in-the-sling.blogspot.com/2008/09/sling-osgi-track-pt-6-sling-servlets-i.html.

As expected, I get the result "hello".

Now, I hit the OSGi configuration management console at http://localhost:7402/system/console/configMgr and select my bundle from the drop down:

snap002 2008-09-09

Don't be confused by the naming of the entries. They can easily be replaced, but I will keep focused on the important stuff for a while and leave these out of consideration.

I get the following screen, which allows me to configure the component's properties. In our case, that is the "helloPhrase" property, which I can change by entering a new value into the text box and hitting "save".

snap003 2008-09-09

 

Testing

The final test is to again hit http://localhost:7402/system/osgitest/info, which should now return the new value of the "helloPhrase" property.

Technorati Tags: ,,

Wednesday, September 3, 2008

Sling OSGI Track pt 6: Sling Servlets I

This is the continuation of

In this part, I will show how to create your own Sling Servlet and deploy it as an OSGi Bundle.

As you may have seen from the sling documentation, there is a pretty straightforward way to create scripts on the search paths (/apps, /libs) that will be executed depending on properties of the requested resource (most notably sling:resourceType.

You can do the same using servlets, if you prefer.
Using servlets, you can also do a lot more than with scripts, and we'll have a first look at what can be done right now.
As a first easy example, I will show how to create a servlet that uses the service created in the previous parts and makes the output available at /system/osgitest/info .

Aims:

  • Show how to create a Sling Servlet and register it to respond to a preset path.

Ingredients:

Files:

Outline:

  • Project structure
  • The servlet implementation
  • The pom: nothing special
  • Maven dependencies: oops
  • Build, Deploy, Test

Execution:

Project structure

The project structure is simpler than anything we had so far. We actually need no more than the servlet implementation and the pom. Let's have a look:

.
|-- pom.xml
`-- src
    `-- main
        `-- java
            `-- mh
                `-- osgitest
                    `-- client
                        `-- OsgiTestClientServlet.java
The servlet implementation

Let's have a look at the servlet implementation:

  • The servlet extends SlingSafeMethodsServlet (l. 25).
    This is a sling-provided class that can be extended to implement servlets that do not have side-effects, i.e. do not modify data. See http://svn.apache.org/viewvc/incubator/sling/trunk/api/src/main/java/org/apache/sling/api/servlets/SlingSafeMethodsServlet.java?view=markup for the details.
  • The servlet overrides the doGet method (l. 31ff).
    In this method, it checks whether it's "service" variable is set (which is of the type 'SampleService' from our previous parts of this track), and if so calls sayHello() on the object to write the result to the output stream.
  • OK, so how can the service variable ever be anything but null? That happens through the src.reference directive in line 27. This will create an OSGi service descriptor, and the OSGi container will inject the reference to a SampleService instance, if available.
  • In line 22, the sling.servlet.paths property is set to /system/osgitest/info. This creates the mapping to the URL /system/osgitest/info, which the servlet will respond to.
    There are many other options of creating mappings depending on the resource that is being accessed, depending on method, etc. Part of this is explained in http://incubator.apache.org/sling/site/servlet-resolution.html, but that description is outdated. I'll look into the other options in further posts.
package mh.osgitest.client;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;

import mh.osgitest.SampleService;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;

/**
 * A Servlet that uses the service provided in the mh.osgitest to retrieve the
 * phrase "hello".
 * 
 * @scr.component immediate="true" metatype="no"
 * @scr.service interface="javax.servlet.Servlet"
 * @scr.property name="service.description" value="Sample OSGi Client Servlet"
 * @scr.property name="service.vendor" value="Moritz Havelock"
 * @scr.property name="sling.servlet.paths" value="/system/osgitest/info"
 */
@SuppressWarnings("serial")
public class OsgiTestClientServlet extends SlingSafeMethodsServlet {
	
	/** @scr.reference */
	private final SampleService service = null;

	@Override
	protected void doGet(SlingHttpServletRequest request,
			SlingHttpServletResponse response) throws ServletException,
			IOException {
		if (service == null) {
			response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
					"no reference to service available");
		} else {
			response.setContentType("text/plain");
			response.getOutputStream().print(service.sayHello());
		}
	}
}
The pom: nothing special

The pom looks quite familiar. What's different this time?
On lines 93-97, we have a dependency on the org.apache.sling.api artifact. That is because we are using sling specific types in the servlet implementation. I have here used the version 2.0.0.incubator-SNAPSHOT, which is what comes with CRX Cup edition.
On lines 98-102, we depend on the mh.studies.sling.osgitest artifact. This should be in your m2 repository from the previous tracks.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <groupId>mh.studies</groupId>
   <artifactId>mh.studies.sling.osgitest.client</artifactId>
   <packaging>bundle</packaging>
   <name>A Servlet Client for the OSGi Test Bundle</name>
   <version>0.0.1</version>
   <description />
   <build>
      <plugins>
         <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
               <source>1.5</source>
               <target>1.5</target>
            </configuration>
         </plugin>
         <plugin>
            <groupId>org.apache.sling</groupId>
            <artifactId>maven-sling-plugin</artifactId>
            <version>2.0.2-incubator</version>
            <executions>
               <execution>
                  <id>install-bundle</id>
                  <goals>
                     <goal>install</goal>
                  </goals>
                  <configuration>
                     <slingUrl>http://localhost:7402/system/console/install</slingUrl>
                     <user>admin</user>
                     <password>admin</password>
                  </configuration>
               </execution>
            </executions>
         </plugin>
         <plugin>
            <groupId>org.apache.felix</groupId>
            <artifactId>maven-scr-plugin</artifactId>
            <version>1.0.7</version>
            <executions>
               <execution>
                  <id>generate-scr-scrdescriptor</id>
                  <goals>
                     <goal>scr</goal>
                  </goals>
               </execution>
            </executions>
         </plugin>
         <plugin>
            <groupId>org.apache.felix</groupId>
            <artifactId>maven-bundle-plugin</artifactId>
            <version>1.4.3</version>
            <extensions>true</extensions>
            <configuration>
               <instructions>
                  <Export-Package></Export-Package>
                  <Private-Package>mh.osgitest.client</Private-Package>
                  <Bundle-SymbolicName>${pom.artifactId}</Bundle-SymbolicName>
                  <Bundle-Name>${pom.name}</Bundle-Name>
               </instructions>
            </configuration>
         </plugin>
      </plugins>
   </build>
   <repositories>
      <repository>
         <id>apache.incubating</id>
         <name>Apache Incubating Repository</name>
         <url>http://people.apache.org/repo/m2-incubating-repository</url>
      </repository>
   </repositories>
   <pluginRepositories>
      <pluginRepository>
         <id>apache.incubating.plugins</id>
         <name>Apache Incubating Plugin Repository</name>
         <url>http://people.apache.org/repo/m2-incubating-repository 
         </url>
      </pluginRepository>
   </pluginRepositories>
   <dependencies>
      <dependency>
         <groupId>javax.servlet</groupId>
         <artifactId>servlet-api</artifactId>
         <version>2.4</version>
      </dependency>
      <dependency>
         <groupId>org.apache.felix</groupId>
         <artifactId>org.osgi.core</artifactId>
         <version>1.0.0</version>
      </dependency>
      <dependency>
         <groupId>org.apache.sling</groupId>
         <artifactId>org.apache.sling.api</artifactId>
         <version>2.0.0.incubator-SNAPSHOT</version>
      </dependency>
      <dependency>
         <groupId>mh.studies</groupId>
         <artifactId>mh.studies.sling.osgitest</artifactId>
         <version>0.0.5</version>
      </dependency>
   </dependencies>
</project>
Maven dependencies: oops

Everything looks easy so far, but there's one catch: no public m2 repository can satisfy the dependency on org.apache.sling.api-2.0.0.incubator-SNAPSHOT, so that when you now run mvn package, you will end up with the following:

D:\projekte\workspace-sling\mh.studies.sling.osgitest.client>mvn package
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building A Servlet Client for the OSGi Test Bundle
[INFO]    task-segment: [package]
[INFO] ------------------------------------------------------------------------
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] snapshot org.apache.sling:org.apache.sling.api:2.0.0.incubator-SNAPSHOT: checking for updates from apache.incubating
Downloading: http://people.apache.org/repo/m2-incubating-repository/org/apache/sling/org.apache.sling.api/2.0.0.incubator-SNAPSHOT/org.apache.sling.api-2.0.0.incubator-SNAPSHOT.pom

Downloading: http://people.apache.org/repo/m2-incubating-repository/org/apache/sling/org.apache.sling.api/2.0.0.incubator-SNAPSHOT/org.apache.sling.api-2.0.0.incubator-SNAPSHOT.jar

[INFO] ------------------------------------------------------------------------
[ERROR] BUILD ERROR
[INFO] ------------------------------------------------------------------------
[INFO] Failed to resolve artifact.

Missing:
----------
1) org.apache.sling:org.apache.sling.api:jar:2.0.0.incubator-SNAPSHOT

  Try downloading the file manually from the project website.

  Then, install it using the command:
      mvn install:install-file -DgroupId=org.apache.sling -DartifactId=org.apache.sling.api -Dversion=2.0.0.incubator-SNAPSHOT -Dpackaging=jar -Dfile=/path/to/file

  Alternatively, if you host your own repository you can deploy the file there:
      mvn deploy:deploy-file -DgroupId=org.apache.sling -DartifactId=org.apache.sling.api -Dversion=2.0.0.incubator-SNAPSHOT -Dpackaging=jar -Dfile=/path/to/file -Durl=[url] -DrepositoryId=[id]

  Path to dependency:
        1) mh.studies:mh.studies.sling.osgitest.client:bundle:0.0.1
        2) org.apache.sling:org.apache.sling.api:jar:2.0.0.incubator-SNAPSHOT

----------
1 required artifact is missing.

for artifact:
  mh.studies:mh.studies.sling.osgitest.client:bundle:0.0.1

from the specified remote repositories:
  central (http://repo1.maven.org/maven2),
  apache.incubating (http://people.apache.org/repo/m2-incubating-repository)


[INFO] ------------------------------------------------------------------------
[INFO] For more information, run Maven with the -e switch
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3 seconds
[INFO] Finished at: Wed Sep 03 16:56:39 CEST 2008
[INFO] Final Memory: 5M/9M
[INFO] ------------------------------------------------------------------------

Don't let it bring you down -- the problem is easily solved, even if it takes a few steps. Note the lines 21 - 26 above. They already show that there is a solution at hand. Now all we need is the jar file. Don't look on the sling website, it's already on your machine. Go to the place you started CRX Quickstart from, and you'll find it here: ./crx-quickstart/server/runtime/0/_/WEB-INF/resources/bundles/org.apache.sling.api-2.0.0-incubator-SNAPSHOT.jar. Now, you only need to execute the following:

mvn install:install-file -DgroupId=org.apache.sling -DartifactId=org.apache.sling.api -Dversion=2.0.0.incubator-SNAPSHOT -Dpackaging=jar \
  -Dfile=<crx quickstart dir>/crx-quickstart/server/runtime/0/_/WEB-INF/resources/bundles/org.apache.sling.api-2.0.0-incubator-SNAPSHOT.jar
Build, Deploy, Test

All that remains is mvn clean install, and go to http://localhost:7402/system/osgitest/info