/* * 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.launchpad.base.app; import static org.apache.felix.framework.util.FelixConstants.LOG_LEVEL_PROP; import java.io.PrintStream; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import org.apache.felix.framework.Logger; import org.apache.sling.launchpad.api.LaunchpadContentProvider; import org.apache.sling.launchpad.base.impl.ClassLoaderResourceProvider; import org.apache.sling.launchpad.base.impl.Sling; import org.apache.sling.launchpad.base.shared.Launcher; import org.apache.sling.launchpad.base.shared.Notifiable; import org.apache.sling.launchpad.base.shared.SharedConstants; import org.osgi.framework.Bundle; import org.osgi.framework.BundleException; import org.osgi.framework.ServiceReference; /** * 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 MainDelegate implements Launcher { /** 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 = "org.apache.sling.commons.log.level"; /** The Sling configuration property name setting the initial log file */ private static final String PROP_LOG_FILE = "org.apache.sling.commons.log.file"; /** The Sling system property name setting the bootstrap log level */ private static final String PROP_BOOT_LOG_LEVEL = "sling.launchpad.log.level"; /** 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"; private Notifiable notifiable; /** The parsed command line mapping (Sling) option name to option value */ private Map<String, String> commandLine; private String slingHome; private Sling sling; @Override public void setNotifiable(Notifiable notifiable) { this.notifiable = notifiable; } @Override public void setCommandLine(Map<String, String> args) { commandLine = new HashMap<String, String>(); parseCommandLine(args, commandLine); } @Override public void setSlingHome(String slingHome) { this.slingHome = slingHome; } @Override public boolean start() { Map<String, String> props = new HashMap<String, String>(); // parse the command line (exit in case of failure) if (commandLine == null) { setCommandLine(new HashMap<String, String>()); } // if sling.home was set on the command line, set it in the properties if (slingHome != null) { props.put(SharedConstants.SLING_HOME, slingHome); } else if (commandLine.containsKey(SharedConstants.SLING_HOME)) { props.put(SharedConstants.SLING_HOME, commandLine.get(SharedConstants.SLING_HOME)); } // ensure sling.launchpad is set if (!commandLine.containsKey(SharedConstants.SLING_LAUNCHPAD)) { commandLine.put(SharedConstants.SLING_LAUNCHPAD, slingHome); } // check sling.properties in the command line final String slingPropertiesProp = commandLine.remove(SharedConstants.SLING_PROPERTIES); if (slingPropertiesProp != null) { props.put(SharedConstants.SLING_PROPERTIES, slingPropertiesProp); } // 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)); } final Logger logger = new SlingLogger(); // default log level: prevent tons of needless WARN from the framework logger.setLogLevel(Logger.LOG_ERROR); if ( System.getProperty(PROP_BOOT_LOG_LEVEL) != null ) { try { logger.setLogLevel( Integer.parseInt(System.getProperty(PROP_BOOT_LOG_LEVEL))); } catch (final NumberFormatException ex) { // just ignore } } try { LaunchpadContentProvider resProvider = new ClassLoaderResourceProvider( getClass().getClassLoader()); // creating the instance launches the framework and we are done here // .. sling = new Sling(notifiable, logger, resProvider, props) { // overwrite the loadPropertiesOverride method to inject the // command line arguments unconditionally. These will not be // persisted in any properties file, though @Override protected void loadPropertiesOverride( Map<String, String> properties) { if (commandLine != null) { properties.putAll(commandLine); } // Display port number on console, in case HttpService doesn't. This is logged as late as // possible in order to pick up defaults from the Sling property files, although system // property substitutions will be missed. info("HTTP server port: " + properties.get(PROP_PORT), null); } }; // we successfully started it return true; } catch (BundleException be) { error("Failed to Start OSGi framework", be); } // we failed to start return false; } @Override public void stop() { if (sling != null) { sling.destroy(); sling = null; } } /** * Parses the command line in <code>args</code> and sets appropriate Sling * configuration options in the <code>props</code> map. */ private static void parseCommandLine(Map<String, String> args, Map<String, String> props) { /* * NOTE: We expect command line args to be suitable to use as * properties to launch Sling. Any standalone Java application * command line args have to be translated into Sling launcher * properties by the Main class. For deployment of the Launchpad * JAR into older launchers we keep converting existing command * line args for now. New command line arguments must solely be * known and converted in the Main class and not here. */ for (Entry<String, String> arg : args.entrySet()) { if (arg.getKey().length() == 1) { String value = arg.getValue(); switch (arg.getKey().charAt(0)) { case 'l': if (value == arg.getKey()) { terminate("Missing log level value", 1); continue; } props.put(PROP_LOG_LEVEL, value); break; case 'f': if (value == arg.getKey()) { terminate("Missing log file value", 1); continue; } else if ("-".equals(value)) { value = ""; } props.put(PROP_LOG_FILE, value); break; case 'c': if (value == arg.getKey()) { terminate("Missing directory value", 1); continue; } props.put(SharedConstants.SLING_HOME, value); break; case 'p': if (value == arg.getKey()) { terminate("Missing port value", 1); continue; } try { // just to verify it is a number Integer.parseInt(value); props.put(PROP_PORT, value); } catch (RuntimeException e) { terminate("Bad port: " + value, 1); } break; case 'a': if (value == arg.getKey()) { terminate("Missing address value", 1); continue; } info("Setting the address to bind to is not supported, binding to 0.0.0.0", null); break; default: terminate("Unrecognized option " + arg.getKey(), 1); break; } } else { info("Setting " + arg.getKey() + "=" + arg.getValue(), null); props.put(arg.getKey(), arg.getValue()); } } } /** Return the log level code for the string */ private static int toLogLevelInt(String level, int defaultLevel) { try { int logLevel = Integer.parseInt(level); if (logLevel >= 0 && logLevel < logLevels.length) { return logLevel; } } catch (NumberFormatException nfe) { // might be a log level string } for (int i = 0; i < logLevels.length; i++) { if (logLevels[i].equalsIgnoreCase(level)) { return i; } } return defaultLevel; } // ---------- console logging /** prints a simple usage plus optional error message and exists with code */ private static void terminate(String message, int code) { if (message != null) { error(message + " (use -h for more information)", null); } System.exit(code); } // emit an debugging message to standard out static void debug(String message, Throwable t) { log(System.out, "*DEBUG*", message, t); } // emit an informational message to standard out static void info(String message, Throwable t) { log(System.out, "*INFO *", message, t); } // emit an warning message to standard out static void warn(String message, Throwable t) { log(System.out, "*WARN *", message, t); } // emit an error message to standard err static void error(String message, Throwable t) { log(System.err, "*ERROR*", message, t); } private static final DateFormat fmt = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss.SSS "); // helper method to format the message on the correct output channel // the throwable if not-null is also prefixed line by line with the prefix private static void log(PrintStream out, String prefix, String message, Throwable t) { final StringBuilder linePrefixBuilder = new StringBuilder(); synchronized (fmt) { linePrefixBuilder.append(fmt.format(new Date())); } linePrefixBuilder.append(prefix); linePrefixBuilder.append(" ["); linePrefixBuilder.append(Thread.currentThread().getName()); linePrefixBuilder.append("] "); final String linePrefix = linePrefixBuilder.toString(); synchronized (out) { out.print(linePrefix); out.println(message); if (t != null) { t.printStackTrace(new PrintStream(out) { @Override public void println(String x) { synchronized (this) { print(linePrefix); super.println(x); flush(); } } }); } } } private static class SlingLogger extends Logger { @Override protected void doLog(Bundle bundle, @SuppressWarnings("rawtypes") ServiceReference sr, int level, String msg, Throwable throwable) { // unwind throwable if it is a BundleException if ((throwable instanceof BundleException) && (((BundleException) throwable).getNestedException() != null)) { throwable = ((BundleException) throwable).getNestedException(); } final StringBuilder sb = new StringBuilder(); if (sr != null) { sb.append("SvcRef "); sb.append(sr); sb.append(" "); } else if (bundle != null) { sb.append("Bundle '"); sb.append(String.valueOf(bundle.getBundleId())); sb.append("' "); } sb.append(msg); if ( throwable != null ) { sb.append(" ("); sb.append(throwable); sb.append(")"); } final String s = sb.toString(); switch (level) { case LOG_DEBUG: debug("DEBUG: " + s); break; case LOG_INFO: info("INFO: " + s, throwable); break; case LOG_WARNING: warn("WARNING: " + s, throwable); break; case LOG_ERROR: error("ERROR: " + s, throwable); break; default: warn("UNKNOWN[" + level + "]: " + s, null); } } } }