blob: 8a7892630d7737178de49dbcd9558aa567246acd [file] [log] [blame]
/*
* 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.commons.logservice.internal;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleException;
import org.osgi.framework.BundleListener;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentConstants;
import org.osgi.service.log.LogEntry;
import org.osgi.service.log.LogListener;
import org.osgi.service.log.LogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The <code>LogReaderServiceFactory</code> TODO
*/
public class LogSupport implements BundleListener, ServiceListener,
FrameworkListener {
/**
* The service property name of the component name (value is
* "component.name"). Note: We use a private constant here to not create a
* unneded dependency on the org.osgi.service.component package.
*/
private static final String COMPONENT_NAME = ComponentConstants.COMPONENT_NAME; // "component.name";
/**
* The empty enumeration currently returned on the {@link #getLog()} call
* because we do not currently record the log events.
*/
private final Enumeration<?> EMPTY = Collections.enumeration(Collections.emptyList());
// The registered LogListeners
private LogListenerProxy[] listeners;
// The lock used to guard concurrent access to the listeners array
private final Object listenersLock = new Object();
// The loggers by bundle id used for logging messages originated from
// specific bundles
private Map<Long, Logger> loggers = new HashMap<Long, Logger>();
// the worker thread actually sending LogEvents to LogListeners
private LogEntryDispatcher logEntryDispatcher;
/* package */LogSupport() {
logEntryDispatcher = new LogEntryDispatcher(this);
logEntryDispatcher.start();
}
/* package */void shutdown() {
// terminate the dispatcher and wait for its termination here
logEntryDispatcher.terminate();
try {
logEntryDispatcher.join(1000L);
} catch (InterruptedException ie) {
// don't care
}
// drop all listeners
synchronized (listenersLock) {
listeners = null;
}
}
// ---------- LogReaderService interface -----------------------------------
/* package */void addLogListener(Bundle bundle, LogListener listener) {
synchronized (listenersLock) {
LogListenerProxy llp = new LogListenerProxy(bundle, listener);
if (listeners == null) {
listeners = new LogListenerProxy[] { llp };
} else if (getListener(listener) < 0) {
LogListenerProxy[] newListeners = new LogListenerProxy[listeners.length + 1];
System.arraycopy(listeners, 0, newListeners, 0,
listeners.length);
newListeners[listeners.length] = llp;
listeners = newListeners;
}
}
}
/* package */void removeLogListener(LogListener listener) {
synchronized (listenersLock) {
// no listeners registered, nothing to do
if (listeners == null) {
return;
}
// listener is not registered, nothing to do
int idx = getListener(listener);
if (idx < 0) {
return;
}
LogListenerProxy[] newListeners = new LogListenerProxy[listeners.length - 1];
if (idx > 0) {
System.arraycopy(listeners, 0, newListeners, 0, idx);
}
if (idx < listeners.length) {
System.arraycopy(listeners, idx + 1, newListeners, 0,
newListeners.length - idx);
}
listeners = newListeners;
}
}
/**
* Removes all registered LogListeners belonging to the given bundle. This
* is the task required by the specification from a Log Service
* implemenation:
* <p>
* <blockquote> When a bundle which registers a LogListener object is
* stopped or otherwise releases the Log Reader Service, the Log Reader
* Service must remove all of the bundle's listeners.</blockquote>
* <p>
*
* @param bundle The bundle whose listeners are to be removed.
*/
/* package */void removeLogListeners(Bundle bundle) {
// grab an immediate copy of the array
LogListenerProxy[] current = getListeners();
if (current == null) {
return;
}
// check for listeners by bundle
for (int i = 0; i < current.length; i++) {
if (current[i].hasBundle(bundle)) {
removeLogListener(current[i]);
}
}
}
private int getListener(LogListener listener) {
if (listeners != null) {
for (int i = 0; i < listeners.length; i++) {
if (listeners[i].isSame(listener)) {
return i;
}
}
}
// fall back to not found
return -1;
}
/**
* Returns the currently registered LogListeners
*/
private LogListenerProxy[] getListeners() {
synchronized (listenersLock) {
return listeners;
}
}
/**
* Returns an empty enumeration for now because we do not implement log
* entry recording for the moment.
*/
Enumeration<?> getLog() {
return EMPTY;
}
// ---------- Firing a log event -------------------------------------------
/**
* Logs the given log entry to the log file and enqueues for the dispatching
* to the registered LogListeners in a separate worker thread.
*/
/* package */void fireLogEvent(LogEntry logEntry) {
// actually log it to SLF4J
logOut(logEntry);
// enqueue for asynchronous delivery
logEntryDispatcher.enqueLogEntry(logEntry);
}
// ---------- BundleListener -----------------------------------------------
/**
* Listens for Bundle events and logs the respective events according to the
* Log Service specification. In addition, all LogListener instances
* registered for stopped bundles are removed by this method.
*/
public void bundleChanged(BundleEvent event) {
String message;
switch (event.getType()) {
case BundleEvent.INSTALLED:
message = "BundleEvent INSTALLED";
break;
case BundleEvent.STARTED:
message = "BundleEvent STARTED";
break;
case BundleEvent.STOPPED:
// this is special, as we have to fix the listener list for
// stopped bundles
removeLogListeners(event.getBundle());
message = "BundleEvent STOPPED";
break;
case BundleEvent.UPDATED:
message = "BundleEvent UPDATED";
break;
case BundleEvent.UNINSTALLED:
message = "BundleEvent UNINSTALLED";
break;
case BundleEvent.RESOLVED:
message = "BundleEvent RESOLVED";
break;
case BundleEvent.UNRESOLVED:
message = "BundleEvent UNRESOLVED";
break;
default:
message = "BundleEvent " + event.getType();
}
LogEntry entry = new LogEntryImpl(event.getBundle(), null,
LogService.LOG_INFO, message, null);
fireLogEvent(entry);
}
// ---------- ServiceListener ----------------------------------------------
/**
* Listens for Service events and logs the respective events according to
* the Log Service specification.
*/
public void serviceChanged(ServiceEvent event) {
int level = LogService.LOG_INFO;
String message;
switch (event.getType()) {
case ServiceEvent.REGISTERED:
message = "ServiceEvent REGISTERED";
break;
case ServiceEvent.MODIFIED:
message = "ServiceEvent MODIFIED";
level = LogService.LOG_DEBUG;
break;
case ServiceEvent.UNREGISTERING:
message = "ServiceEvent UNREGISTERING";
break;
default:
message = "ServiceEvent " + event.getType();
}
String s = (event.getServiceReference().getBundle() == null)
? null
: "Bundle " + event.getServiceReference().getBundle();
s = (s == null) ? message : s + " " + message;
LogEntry entry = new LogEntryImpl(
event.getServiceReference().getBundle(),
event.getServiceReference(), level, message, null);
fireLogEvent(entry);
}
// ---------- FrameworkListener --------------------------------------------
/**
* Listens for Framework events and logs the respective events according to
* the Log Service specification.
* <p>
* In the case of a Framework ERROR which is a ClassNotFoundException for an
* unresolved bundle, the message is logged at INFO level instead of ERROR
* level as prescribed by the spec. This is because such a situation should
* not really result in a Framework ERROR but the Apache Felix framework has
* no means of controlling this at the moment (framework 1.0.4 release).
*/
public void frameworkEvent(FrameworkEvent event) {
int level = LogService.LOG_INFO;
String message;
Throwable exception = event.getThrowable();
switch (event.getType()) {
case FrameworkEvent.STARTED:
message = "FrameworkEvent STARTED";
break;
case FrameworkEvent.ERROR:
message = "FrameworkEvent ERROR";
// special precaution for Felix.loadBundleClass event overkill
// FIXME: actually, the error is ok, if the bundle failed to
// resolve
if (exception instanceof BundleException) {
StackTraceElement[] ste = exception.getStackTrace();
if (ste != null && ste.length > 0
&& "loadBundleClass".equals(ste[0].getMethodName())) {
message += ": Class " + exception.getMessage()
+ " not found";
if (event.getBundle() != null) {
message += " in bundle "
+ event.getBundle().getSymbolicName() + " ("
+ event.getBundle().getBundleId() + ")";
}
level = LogService.LOG_INFO;
exception = null; // don't care for a stack trace here
break;
}
}
level = LogService.LOG_ERROR;
break;
case FrameworkEvent.PACKAGES_REFRESHED:
message = "FrameworkEvent PACKAGES REFRESHED";
break;
case FrameworkEvent.STARTLEVEL_CHANGED:
message = "FrameworkEvent STARTLEVEL CHANGED";
break;
case FrameworkEvent.WARNING:
message = "FrameworkEvent WARNING";
break;
case FrameworkEvent.INFO:
message = "FrameworkEvent INFO";
break;
default:
message = "FrameworkEvent " + event.getType();
}
String s = (event.getBundle() == null) ? null : "Bundle "
+ event.getBundle();
s = (s == null) ? message : s + " " + message;
LogEntry entry = new LogEntryImpl(event.getBundle(), null, level,
message, exception);
fireLogEvent(entry);
}
// ---------- Effective logging --------------------------------------------
/**
* Get a logger for messages orginating from the given bundle. If no bundle
* is specified, we use the system bundle logger.
*
* @param bundle The bundle for which a logger is to be returned.
* @return The Logger for the bundle.
*/
private Logger getLogger(Bundle bundle) {
Long bundleId = new Long((bundle == null) ? 0 : bundle.getBundleId());
Logger log = loggers.get(bundleId);
if (log == null) {
String name;
if (bundle == null) {
// if we have no bundle, use the system bundle's name
name = Constants.SYSTEM_BUNDLE_SYMBOLICNAME;
} else {
// otherwise use the bundle symbolic name
name = bundle.getSymbolicName();
// if the bundle has no symbolic name, use the location
if (name == null) {
name = bundle.getLocation();
}
// if the bundle also has no location, use the bundle Id
if (name == null) {
name = String.valueOf(bundle.getBundleId());
}
}
log = LoggerFactory.getLogger(name);
loggers.put(bundleId, log);
}
return log;
}
/**
* Actually logs the given log entry to the logger for the bundle recorded
* in the log entry.
*/
private void logOut(LogEntry logEntry) {
// /* package */ void logOut(Bundle bundle, ServiceReference sr, int
// level, String message, Throwable exception) {
// get the logger for the bundle
Logger log = getLogger(logEntry.getBundle());
StringBuffer msg = new StringBuffer();
ServiceReference sr = logEntry.getServiceReference();
if (sr != null) {
msg.append("Service [");
if (sr.getProperty(Constants.SERVICE_PID) != null) {
msg.append(sr.getProperty(Constants.SERVICE_PID)).append(',');
} else if (sr.getProperty(COMPONENT_NAME) != null) {
msg.append(sr.getProperty(COMPONENT_NAME)).append(',');
} else if (sr.getProperty(Constants.SERVICE_DESCRIPTION) != null) {
msg.append(sr.getProperty(Constants.SERVICE_DESCRIPTION)).append(
',');
}
msg.append(sr.getProperty(Constants.SERVICE_ID)).append("] ");
}
if (logEntry.getMessage() != null) {
msg.append(logEntry.getMessage());
}
Throwable exception = logEntry.getException();
if (exception != null) {
msg.append(" (").append(exception).append(')');
}
String message = msg.toString();
switch (logEntry.getLevel()) {
case LogService.LOG_DEBUG:
log.debug(message, exception);
break;
case LogService.LOG_INFO:
log.info(message, exception);
break;
case LogService.LOG_WARNING:
log.warn(message, exception);
break;
case LogService.LOG_ERROR:
log.error(message, exception);
break;
default:
if (logEntry.getLevel() > LogService.LOG_DEBUG) {
log.trace(message, exception);
} else if (logEntry.getLevel() < LogService.LOG_ERROR) {
log.error(message, exception);
}
break;
}
}
// ---------- internal class -----------------------------------------------
/**
* The <code>LogListenerProxy</code> class is a proxy to the actually
* registered <code>LogListener</code> which also records the bundle
* registering the listener. This allows for the removal of the log
* listeners registered by bundles which have not been removed before the
* bundle has been stopped.
*/
private static class LogListenerProxy implements LogListener {
private final int runningBundle = Bundle.STARTING | Bundle.ACTIVE
| Bundle.STOPPING;
private final Bundle bundle;
private final LogListener delegatee;
public LogListenerProxy(Bundle bundle, LogListener delegatee) {
this.bundle = bundle;
this.delegatee = delegatee;
}
public void logged(LogEntry entry) {
if ((bundle.getState() & runningBundle) != 0) {
delegatee.logged(entry);
}
}
/* package */boolean isSame(LogListener listener) {
return listener == delegatee || listener == this;
}
/* package */boolean hasBundle(Bundle bundle) {
return this.bundle == bundle;
}
}
/**
* The <code>LogEntryDispatcher</code> implements the worker thread
* responsible for delivering log events to the log listeners.
*/
private static class LogEntryDispatcher extends Thread {
// provides the actual log listeners on demand
private final LogSupport logSupport;
// the queue of log events to be dispatched
private final BlockingQueue<LogEntry> dispatchQueue;
// true as long as the thread is active
private boolean active;
LogEntryDispatcher(LogSupport logSupport) {
super("LogEntry Dispatcher");
this.logSupport = logSupport;
this.dispatchQueue = new LinkedBlockingQueue<LogEntry>();
this.active = true;
}
/**
* Add a log entry for dispatching.
*/
void enqueLogEntry(LogEntry logEntry) {
dispatchQueue.offer(logEntry);
}
/**
* Get the next log entry for dispatching. This method blocks until an
* event is available or the thread is interrupted.
*
* @return The next event to dispatch
* @throws InterruptedException If the thread has been interrupted while
* waiting for a log event to dispatch.
*/
LogEntry dequeueLogEntry() throws InterruptedException {
return dispatchQueue.take();
}
/**
* Terminates this work thread by resetting the active flag and
* interrupting itself such that the {@link #dequeueLogEntry()} is
* aborted for the thread to terminate.
*/
void terminate() {
active = false;
interrupt();
}
/**
* Runs the actual log event dispatching. This method continues to get
* log events from the {@link #dequeueLogEntry()} method until the
* active flag is reset.
*/
@Override
public void run() {
while (active) {
LogEntry logEntry = null;
try {
logEntry = dequeueLogEntry();
} catch (InterruptedException ie) {
// don't care, this is expected
}
// dispatch the log entry
if (logEntry != null) {
// grab an immediate copy of the array
LogListener[] logListeners = logSupport.getListeners();
// fire the events outside of the listenersLock
if (logListeners != null) {
for (LogListener logListener : logListeners) {
try {
logListener.logged(logEntry);
} catch (Throwable t) {
// should we really care ??
}
}
}
}
}
}
}
}