/*
 * 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.log4j.net;

import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.spi.ErrorCode;
import org.apache.log4j.spi.LoggingEvent;

import javax.jms.JMSException;
import javax.jms.ObjectMessage;
import javax.jms.Session;
import javax.jms.Topic;
import javax.jms.TopicConnection;
import javax.jms.TopicConnectionFactory;
import javax.jms.TopicPublisher;
import javax.jms.TopicSession;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Properties;

/**
 * A simple appender that publishes events to a JMS Topic. The events are
 * serialized and transmitted as JMS message type {@link ObjectMessage}.
 * 
 * <p>
 * JMS {@link Topic topics} and {@link TopicConnectionFactory topic connection
 * factories} are administered objects that are retrieved using JNDI messaging
 * which in turn requires the retrieval of a JNDI {@link Context}.
 * 
 * <p>
 * There are two common methods for retrieving a JNDI {@link Context}. If a file
 * resource named <em>jndi.properties</em> is available to the JNDI API, it will
 * use the information found therein to retrieve an initial JNDI context. To
 * obtain an initial context, your code will simply call:
 * 
 * <pre>
 * InitialContext jndiContext = new InitialContext();
 * </pre>
 * 
 * <p>
 * Calling the no-argument <code>InitialContext()</code> method will also work
 * from within Enterprise Java Beans (EJBs) because it is part of the EJB
 * contract for application servers to provide each bean an environment naming
 * context (ENC).
 * 
 * <p>
 * In the second approach, several predetermined properties are set and these
 * properties are passed to the <code>InitialContext</code> constructor to
 * connect to the naming service provider. For example, to connect to JBoss
 * naming service one would write:
 * 
 * <pre>
 * Properties env = new Properties();
 * env.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
 * env.put(Context.PROVIDER_URL, "jnp://hostname:1099");
 * env.put(Context.URL_PKG_PREFIXES, "org.jboss.naming:org.jnp.interfaces");
 * InitialContext jndiContext = new InitialContext(env);
 * </pre>
 * 
 * where <em>hostname</em> is the host where the JBoss application server is
 * running.
 *
 * <p>
 * To connect to the the naming service of Weblogic application server one would
 * write:
 * 
 * <pre>
 * Properties env = new Properties();
 * env.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
 * env.put(Context.PROVIDER_URL, "t3://localhost:7001");
 * InitialContext jndiContext = new InitialContext(env);
 * </pre>
 * 
 * <p>
 * Other JMS providers will obviously require different values.
 * 
 * The initial JNDI context can be obtained by calling the no-argument
 * <code>InitialContext()</code> method in EJBs. Only clients running in a
 * separate JVM need to be concerned about the <em>jndi.properties</em> file and
 * calling {@link InitialContext#InitialContext()} or alternatively correctly
 * setting the different properties before calling
 * {@link InitialContext#InitialContext(java.util.Hashtable)} method.
 * 
 * 
 * @author Ceki G&uuml;lc&uuml;
 */
public class JMSAppender extends AppenderSkeleton {

    String securityPrincipalName;
    String securityCredentials;
    String initialContextFactoryName;
    String urlPkgPrefixes;
    String providerURL;
    String topicBindingName;
    String tcfBindingName;
    String userName;
    String password;
    boolean locationInfo;

    TopicConnection topicConnection;
    TopicSession topicSession;
    TopicPublisher topicPublisher;

    public JMSAppender() {
    }

    /**
     * The <b>TopicConnectionFactoryBindingName</b> option takes a string value. Its
     * value will be used to lookup the appropriate
     * <code>TopicConnectionFactory</code> from the JNDI context.
     */
    public void setTopicConnectionFactoryBindingName(String tcfBindingName) {
	this.tcfBindingName = tcfBindingName;
    }

    /**
     * Returns the value of the <b>TopicConnectionFactoryBindingName</b> option.
     */
    public String getTopicConnectionFactoryBindingName() {
	return tcfBindingName;
    }

    /**
     * The <b>TopicBindingName</b> option takes a string value. Its value will be
     * used to lookup the appropriate <code>Topic</code> from the JNDI context.
     */
    public void setTopicBindingName(String topicBindingName) {
	this.topicBindingName = topicBindingName;
    }

    /**
     * Returns the value of the <b>TopicBindingName</b> option.
     */
    public String getTopicBindingName() {
	return topicBindingName;
    }

    /**
     * Returns value of the <b>LocationInfo</b> property which determines whether
     * location (stack) info is sent to the remote subscriber.
     */
    public boolean getLocationInfo() {
	return locationInfo;
    }

    /**
     * Options are activated and become effective only after calling this method.
     */
    public void activateOptions() {
	TopicConnectionFactory topicConnectionFactory;

	try {
	    Context jndi;

	    LogLog.debug("Getting initial context.");
	    if (initialContextFactoryName != null) {
		Properties env = new Properties();
		env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactoryName);
		if (providerURL != null) {
		    env.put(Context.PROVIDER_URL, providerURL);
		} else {
		    LogLog.warn("You have set InitialContextFactoryName option but not the "
			    + "ProviderURL. This is likely to cause problems.");
		}
		if (urlPkgPrefixes != null) {
		    env.put(Context.URL_PKG_PREFIXES, urlPkgPrefixes);
		}

		if (securityPrincipalName != null) {
		    env.put(Context.SECURITY_PRINCIPAL, securityPrincipalName);
		    if (securityCredentials != null) {
			env.put(Context.SECURITY_CREDENTIALS, securityCredentials);
		    } else {
			LogLog.warn("You have set SecurityPrincipalName option but not the "
				+ "SecurityCredentials. This is likely to cause problems.");
		    }
		}
		jndi = new InitialContext(env);
	    } else {
		jndi = new InitialContext();
	    }

	    LogLog.debug("Looking up [" + tcfBindingName + "]");
	    topicConnectionFactory = (TopicConnectionFactory) lookup(jndi, tcfBindingName);
	    LogLog.debug("About to create TopicConnection.");
	    if (userName != null) {
		topicConnection = topicConnectionFactory.createTopicConnection(userName, password);
	    } else {
		topicConnection = topicConnectionFactory.createTopicConnection();
	    }

	    LogLog.debug("Creating TopicSession, non-transactional, " + "in AUTO_ACKNOWLEDGE mode.");
	    topicSession = topicConnection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);

	    LogLog.debug("Looking up topic name [" + topicBindingName + "].");
	    Topic topic = (Topic) lookup(jndi, topicBindingName);

	    LogLog.debug("Creating TopicPublisher.");
	    topicPublisher = topicSession.createPublisher(topic);

	    LogLog.debug("Starting TopicConnection.");
	    topicConnection.start();

	    jndi.close();
	} catch (JMSException e) {
	    errorHandler.error("Error while activating options for appender named [" + name + "].", e,
		    ErrorCode.GENERIC_FAILURE);
	} catch (NamingException e) {
	    errorHandler.error("Error while activating options for appender named [" + name + "].", e,
		    ErrorCode.GENERIC_FAILURE);
	} catch (RuntimeException e) {
	    errorHandler.error("Error while activating options for appender named [" + name + "].", e,
		    ErrorCode.GENERIC_FAILURE);
	}
    }

    protected Object lookup(Context ctx, String name) throws NamingException {
	Object result = JNDIUtil.lookupObject(ctx, name);
	if (result == null) {
	    String msg = "Could not find name [" + name + "].";
	    throw new NamingException(msg);
	}
	return result;
    }

    protected boolean checkEntryConditions() {
	String fail = null;

	if (this.topicConnection == null) {
	    fail = "No TopicConnection";
	} else if (this.topicSession == null) {
	    fail = "No TopicSession";
	} else if (this.topicPublisher == null) {
	    fail = "No TopicPublisher";
	}

	if (fail != null) {
	    errorHandler.error(fail + " for JMSAppender named [" + name + "].");
	    return false;
	} else {
	    return true;
	}
    }

    /**
     * Close this JMSAppender. Closing releases all resources used by the appender.
     * A closed appender cannot be re-opened.
     */
    public synchronized void close() {
	// The synchronized modifier avoids concurrent append and close operations

	if (this.closed)
	    return;

	LogLog.debug("Closing appender [" + name + "].");
	this.closed = true;

	try {
	    if (topicSession != null)
		topicSession.close();
	    if (topicConnection != null)
		topicConnection.close();
	} catch (JMSException e) {
	    LogLog.error("Error while closing JMSAppender [" + name + "].", e);
	} catch (RuntimeException e) {
	    LogLog.error("Error while closing JMSAppender [" + name + "].", e);
	}
	// Help garbage collection
	topicPublisher = null;
	topicSession = null;
	topicConnection = null;
    }

    /**
     * This method called by {@link AppenderSkeleton#doAppend} method to do most of
     * the real appending work.
     */
    public void append(LoggingEvent event) {
	if (!checkEntryConditions()) {
	    return;
	}

	try {
	    ObjectMessage msg = topicSession.createObjectMessage();
	    if (locationInfo) {
		event.getLocationInformation();
	    }
	    msg.setObject(event);
	    topicPublisher.publish(msg);
	} catch (JMSException e) {
	    errorHandler.error("Could not publish message in JMSAppender [" + name + "].", e,
		    ErrorCode.GENERIC_FAILURE);
	} catch (RuntimeException e) {
	    errorHandler.error("Could not publish message in JMSAppender [" + name + "].", e,
		    ErrorCode.GENERIC_FAILURE);
	}
    }

    /**
     * Returns the value of the <b>InitialContextFactoryName</b> option. See
     * {@link #setInitialContextFactoryName} for more details on the meaning of this
     * option.
     */
    public String getInitialContextFactoryName() {
	return initialContextFactoryName;
    }

    /**
     * Setting the <b>InitialContextFactoryName</b> method will cause this
     * <code>JMSAppender</code> instance to use the
     * {@link InitialContext#InitialContext(Hashtable)} method instead of the
     * no-argument constructor. If you set this option, you should also at least set
     * the <b>ProviderURL</b> option.
     * 
     * <p>
     * See also {@link #setProviderURL(String)}.
     */
    public void setInitialContextFactoryName(String initialContextFactoryName) {
	this.initialContextFactoryName = initialContextFactoryName;
    }

    public String getProviderURL() {
	return providerURL;
    }

    public void setProviderURL(String providerURL) {
	this.providerURL = providerURL;
    }

    String getURLPkgPrefixes() {
	return urlPkgPrefixes;
    }

    public void setURLPkgPrefixes(String urlPkgPrefixes) {
	this.urlPkgPrefixes = urlPkgPrefixes;
    }

    public String getSecurityCredentials() {
	return securityCredentials;
    }

    public void setSecurityCredentials(String securityCredentials) {
	this.securityCredentials = securityCredentials;
    }

    public String getSecurityPrincipalName() {
	return securityPrincipalName;
    }

    public void setSecurityPrincipalName(String securityPrincipalName) {
	this.securityPrincipalName = securityPrincipalName;
    }

    public String getUserName() {
	return userName;
    }

    /**
     * The user name to use when
     * {@link TopicConnectionFactory#createTopicConnection(String, String) creating
     * a topic session}. If you set this option, you should also set the
     * <b>Password</b> option. See {@link #setPassword(String)}.
     */
    public void setUserName(String userName) {
	this.userName = userName;
    }

    public String getPassword() {
	return password;
    }

    /**
     * The paswword to use when creating a topic session.
     */
    public void setPassword(String password) {
	this.password = password;
    }

    /**
     * If true, the information sent to the remote subscriber will include caller's
     * location information. By default no location information is sent to the
     * subscriber.
     */
    public void setLocationInfo(boolean locationInfo) {
	this.locationInfo = locationInfo;
    }

    /**
     * Returns the TopicConnection used for this appender. Only valid after
     * activateOptions() method has been invoked.
     */
    protected TopicConnection getTopicConnection() {
	return topicConnection;
    }

    /**
     * Returns the TopicSession used for this appender. Only valid after
     * activateOptions() method has been invoked.
     */
    protected TopicSession getTopicSession() {
	return topicSession;
    }

    /**
     * Returns the TopicPublisher used for this appender. Only valid after
     * activateOptions() method has been invoked.
     */
    protected TopicPublisher getTopicPublisher() {
	return topicPublisher;
    }

    /**
     * The JMSAppender sends serialized events and consequently does not require a
     * layout.
     */
    public boolean requiresLayout() {
	return false;
    }
}
