/*
* $Id$
*
* Copyright 2006, The jCoderZ.org Project. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials
* provided with the distribution.
* * Neither the name of the jCoderZ.org Project nor the names of
* its contributors may be used to endorse or promote products
* derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.jcoderz.commons.logging;
import java.io.CharArrayWriter;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.ErrorManager;
import java.util.logging.Filter;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import javax.jms.JMSException;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import org.jcoderz.commons.LoggableImpl;
/**
* This log handler publishes log messages onto a jms queue. Information for the
* configuration has to be provided in the logging properties file.
* The following configuration can to be provided:<br>
*
* <code>org.jcoderz.commons.logging.JmsHandler.factory:</code><br>
* Name of the jms connection factory, is mandatory.<br><br>
*
* <code>org.jcoderz.commons.logging.JmsHandler.queue:</code><br>
* Name of the jms queue, is mandatory.<br><br>
*
* <code>org.jcoderz.commons.logging.JmsHandler.filter:</code><br>
* Name of class implementing java.util.logging.Filter and which is to
* set as filter for this. If this is not specified, the default filter is
* used, which filters according to the symbol ids.<br><br>
*
* <code>org.jcoderz.commons.logging.JmsHandler.messageids:</code><br>
* A comma or space separated list of message symbol ids for those
* messages, which are loggable for the default filter. The ids have either
* to be specified as integer values with base 10, or by prefixing with 0x as
* hex values.
*
*/
public class JmsHandler
extends Handler
{
private static final String CLASSNAME = JmsHandler.class.getName();
private static final String CLIENT_ID
= "JmsHandler@" + LoggableImpl.INSTANCE_ID;
private static final String JMS_FACTORY_NAME_PROPERTY
= CLASSNAME + "." + "factory";
private static final String JMS_QUEUE_NAME_PROPERTY
= CLASSNAME + "." + "queue";
private static final String JMS_FILTER_PROPERTY
= CLASSNAME + "." + "filter";
private static final String JMS_FORMATTER_PROPERTY
= CLASSNAME + "." + "formatter";
/**
* The created sessions to not behave transactional, i.e. do not put a tx
* boundary around sending several messages.
*/
private static final boolean SESSION_TRANSACTION_MODE = false;
/**
* The acknowledge mode, is being ignored for senders.
*/
private static final int SESSION_ACKNOWLEDGE_MODE = Session.AUTO_ACKNOWLEDGE;
/**
* Stores the jms queue session for the current thread. With respect to the
* JMS specification a session and the resources it provides must be used by
* only one thread.
*/
private final ThreadLocal mJmsSessions = new ThreadLocal();
/**
* Stores the jms queue sender for the current thread.
*/
private final ThreadLocal mJmsSenders = new ThreadLocal();
/**
* When closing this handler, all sessions have to be closed (closing a
* session is the only session method, which is allowed to be called from
* another than the session controlling thread). This gives access to all
* sessions and will store WeakReferences to allow a session being garbage
* collected if the corresponding thread dies.
*/
private final List mAllSessions = new ArrayList();
private final LogManager mManager = LogManager.getLogManager();
private QueueConnection mJmsConnection = null;
private Queue mJmsQueue = null;
private Context mContext;
private String mFactoryName;
private String mQueueName;
/**
* This is the formatter to be used for formatting log records before they
* are put onto the jms queue.
* It formats the log record into a xml message using the
* {@link XmlPrinter}. The stack trace of messages and exceptions is
* neglected.
*
*/
private static final class DefaultFormatter
extends Formatter
{
/** A PrintWriter is used by the XmlPrinter. */
private final ThreadLocal mPrintWriters = new ThreadLocal();
/** A CharWriter is used by the PrintWriter. */
private final ThreadLocal mCharWriters = new ThreadLocal();
private final XmlPrinter mXmlPrinter;
private final DisplayOptions mDisplayOptions;
private DefaultFormatter ()
throws InstantiationException
{
mXmlPrinter = new XmlPrinter();
mDisplayOptions = new DisplayOptions();
mDisplayOptions.displayMessageStackTrace(false);
mDisplayOptions.displayStackTrace(false);
mXmlPrinter.setDisplayOptions(mDisplayOptions);
}
/** {@inheritDoc} */
public String format (LogRecord record)
{
final CharArrayWriter writer = getCharWriter();
writer.reset();
final PrintWriter printer = getPrintWriter();
mXmlPrinter.print(printer, new LogElement(record));
return writer.toString();
}
private PrintWriter getPrintWriter ()
{
PrintWriter rc = (PrintWriter) mPrintWriters.get();
if (rc == null)
{
final CharArrayWriter cw = getCharWriter();
rc = new PrintWriter(cw);
mPrintWriters.set(rc);
}
return rc;
}
private CharArrayWriter getCharWriter ()
{
CharArrayWriter rc = (CharArrayWriter) mCharWriters.get();
if (rc == null)
{
rc = new CharArrayWriter();
mCharWriters.set(rc);
}
return rc;
}
}
/**
* Creates a new instance of this and initialises resources. It retrieves
* configuration parameters from the LogManagers and connects to the jms
* provider.
*
* @throws SecurityException If no permission to do the tasks.
* @throws NamingException If the jms connection factory lookup fails.
* @throws InstantiationException If not all required configuration
* parameters are specified.
* @throws IllegalAccessException If illegal access to a class.
* @throws ClassNotFoundException If a specified class name could not be
* found.
* @throws JMSException If an error connecting to the JmsProvider occurs.
*/
public JmsHandler ()
throws SecurityException,
NamingException,
InstantiationException,
IllegalAccessException,
ClassNotFoundException,
JMSException
{
super();
mManager.checkAccess();
configure();
connect();
}
/** {@inheritDoc} */
public void close ()
throws SecurityException
{
mManager.checkAccess();
setFormatter(null);
setFilter(null);
synchronized (mAllSessions)
{
while (! mAllSessions.isEmpty())
{
final WeakReference ref = (WeakReference) mAllSessions.remove(0);
final QueueSession session = (QueueSession) ref.get();
if (session != null)
{
try
{
session.close();
}
catch (Exception ex)
{
reportError("Error closing jms session: " + session,
ex, ErrorManager.CLOSE_FAILURE);
}
}
}
}
try
{
mJmsConnection.close();
}
catch (JMSException jex)
{
reportError("Error closing jms connection: " + mJmsConnection,
jex, ErrorManager.CLOSE_FAILURE);
}
}
/** {@inheritDoc} */
public void flush ()
{
// nop
}
/** {@inheritDoc} */
public void publish (final LogRecord record)
{
if (getFilter().isLoggable(record))
{
sendRecord(record);
}
}
private void configure ()
throws NamingException,
SecurityException,
InstantiationException,
IllegalAccessException,
ClassNotFoundException
{
configureJndiContext();
configureResources();
configureFilter();
configureFormatter();
}
private void configureJndiContext ()
throws NamingException
{
final Context context = new InitialContext();
mContext = context;
}
private void configureFilter ()
throws SecurityException,
InstantiationException,
IllegalAccessException,
ClassNotFoundException
{
final String filterClass = mManager.getProperty(JMS_FILTER_PROPERTY);
if (filterClass == null || filterClass.length() == 0)
{
setFilter(new MessageIdFilter());
}
else
{
try
{
AccessController.doPrivileged(
new PrivilegedExceptionAction()
{
public Object run ()
throws SecurityException,
InstantiationException,
IllegalAccessException,
ClassNotFoundException
{
setFilter((Filter) Class.forName(filterClass)
.newInstance());
return null;
}
}
);
}
catch (PrivilegedActionException e)
{
final InstantiationException iex = new InstantiationException(
"Could not install the Filter: " + filterClass);
iex.initCause(e);
throw iex;
}
}
}
private void configureFormatter ()
throws SecurityException,
InstantiationException,
IllegalAccessException,
ClassNotFoundException
{
final String formatter = mManager.getProperty(JMS_FORMATTER_PROPERTY);
if (formatter == null || formatter.length() == 0)
{
setFormatter(new DefaultFormatter());
}
else
{
try
{
AccessController.doPrivileged(
new PrivilegedExceptionAction()
{
public Object run ()
throws SecurityException,
InstantiationException,
IllegalAccessException,
ClassNotFoundException
{
setFormatter((Formatter) Class.forName(formatter)
.newInstance());
return null;
}
}
);
}
catch (PrivilegedActionException e)
{
final InstantiationException iex
= new InstantiationException(
"Could not install the Formatter: " + formatter);
iex.initCause(e);
throw iex;
}
}
}
private void configureResources ()
throws InstantiationException
{
final String factory = mManager.getProperty(JMS_FACTORY_NAME_PROPERTY);
if ((factory == null) || (factory.length() == 0))
{
throw new InstantiationException("No jms connection factory configured"
+ " in properties file with property: "
+ JMS_FACTORY_NAME_PROPERTY);
}
final String queue = mManager.getProperty(JMS_QUEUE_NAME_PROPERTY);
if ((queue == null) || (queue.length() == 0))
{
throw new InstantiationException("No jms queue configured in "
+ "properties file with property: " + JMS_QUEUE_NAME_PROPERTY);
}
mFactoryName = factory;
mQueueName = queue;
}
/**
* Creates a connection to the jms provider and performs a lookup for the
* queue, which will receive the log messages.
*
* @throws NamingException
* @throws JMSException
*/
private void connect ()
throws NamingException, JMSException
{
final QueueConnectionFactory factory
= (QueueConnectionFactory) mContext.lookup(mFactoryName);
final Queue queue = (Queue) mContext.lookup(mQueueName);
mJmsConnection = factory.createQueueConnection();
mJmsConnection.setClientID(CLIENT_ID);
mJmsQueue = queue;
}
/**
* Gets the queue sender for the current thread. If there is no sender yet,
* this thread's session is used for creating a new sender.
*
* @return queue sender for the current thread.
*
* @throws JMSException if an error occurs.
*/
private QueueSender getSender ()
throws JMSException
{
QueueSender rc = (QueueSender) mJmsSenders.get();
if (rc == null)
{
try
{
rc = (QueueSender) AccessController.doPrivileged(
new PrivilegedExceptionAction()
{
public Object run ()
throws JMSException
{
return installSender();
}
});
}
catch (PrivilegedActionException e)
{
final JMSException jex
= new JMSException("Could not install a QueueSender:" + e);
jex.initCause(e);
throw jex;
}
}
return rc;
}
/**
* Gets the queue session for the current thread. If there is no session yet,
* a new session is created and initialized.
*
* @return queue session for the current thread.
*
* @throws JMSException if an error occurs.
*/
private QueueSession getSession ()
throws JMSException
{
QueueSession rc = (QueueSession) mJmsSessions.get();
if (rc == null)
{
final QueueSession session = mJmsConnection.createQueueSession(
SESSION_TRANSACTION_MODE, SESSION_ACKNOWLEDGE_MODE);
mJmsSessions.set(session);
synchronized (mAllSessions)
{
mAllSessions.add(new WeakReference(session));
}
rc = session;
}
return rc;
}
private QueueSender installSender ()
throws JMSException
{
final QueueSession session = getSession();
QueueSender sender = null;
try
{
sender = session.createSender(mJmsQueue);
mJmsSenders.set(sender);
}
finally
{
if (sender == null)
{
// An error ocurred on this session. Close it, it might occur again.
mJmsSessions.set(null);
session.close();
}
}
return sender;
}
private void sendRecord (final LogRecord record)
{
String text = null;
try
{
text = getFormatter().format(record);
}
catch (Exception ex)
{
reportError("Error formatting the log record",
ex, ErrorManager.FORMAT_FAILURE);
}
if (text != null)
{
try
{
final TextMessage msg = getSession().createTextMessage();
msg.setText(text);
getSender().send(msg);
}
catch (JMSException ex)
{
reportError("Error publishing a log record", ex,
ErrorManager.WRITE_FAILURE);
}
}
}
}