001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 * 
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 * 
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.log4j.net;
019
020import org.apache.log4j.AppenderSkeleton;
021import org.apache.log4j.helpers.LogLog;
022import org.apache.log4j.spi.ErrorCode;
023import org.apache.log4j.spi.LoggingEvent;
024
025import javax.jms.JMSException;
026import javax.jms.ObjectMessage;
027import javax.jms.Session;
028import javax.jms.Topic;
029import javax.jms.TopicConnection;
030import javax.jms.TopicConnectionFactory;
031import javax.jms.TopicPublisher;
032import javax.jms.TopicSession;
033import javax.naming.Context;
034import javax.naming.InitialContext;
035import javax.naming.NameNotFoundException;
036import javax.naming.NamingException;
037import java.util.Properties;
038
039/**
040 * A simple appender that publishes events to a JMS Topic. The events
041 * are serialized and transmitted as JMS message type {@link
042 * ObjectMessage}.
043
044 * <p>JMS {@link Topic topics} and {@link TopicConnectionFactory topic
045 * connection factories} are administered objects that are retrieved
046 * using JNDI messaging which in turn requires the retrieval of a JNDI
047 * {@link Context}.
048
049 * <p>There are two common methods for retrieving a JNDI {@link
050 * Context}. If a file resource named <em>jndi.properties</em> is
051 * available to the JNDI API, it will use the information found
052 * therein to retrieve an initial JNDI context. To obtain an initial
053 * context, your code will simply call:
054
055   <pre>
056   InitialContext jndiContext = new InitialContext();
057   </pre>
058  
059 * <p>Calling the no-argument <code>InitialContext()</code> method
060 * will also work from within Enterprise Java Beans (EJBs) because it
061 * is part of the EJB contract for application servers to provide each
062 * bean an environment naming context (ENC).
063    
064 * <p>In the second approach, several predetermined properties are set
065 * and these properties are passed to the <code>InitialContext</code>
066 * constructor to connect to the naming service provider. For example,
067 * to connect to JBoss naming service one would write:
068
069<pre>
070   Properties env = new Properties( );
071   env.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
072   env.put(Context.PROVIDER_URL, "jnp://hostname:1099");
073   env.put(Context.URL_PKG_PREFIXES, "org.jboss.naming:org.jnp.interfaces");
074   InitialContext jndiContext = new InitialContext(env);
075</pre>
076
077   * where <em>hostname</em> is the host where the JBoss application
078   * server is running.
079   *
080   * <p>To connect to the the naming service of Weblogic application
081   * server one would write:
082
083<pre>
084   Properties env = new Properties( );
085   env.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
086   env.put(Context.PROVIDER_URL, "t3://localhost:7001");
087   InitialContext jndiContext = new InitialContext(env);
088</pre>
089
090  * <p>Other JMS providers will obviously require different values.
091  * 
092  * The initial JNDI context can be obtained by calling the
093  * no-argument <code>InitialContext()</code> method in EJBs. Only
094  * clients running in a separate JVM need to be concerned about the
095  * <em>jndi.properties</em> file and calling {@link
096  * InitialContext#InitialContext()} or alternatively correctly
097  * setting the different properties before calling {@link
098  * InitialContext#InitialContext(java.util.Hashtable)} method.
099
100
101   @author Ceki G&uuml;lc&uuml; */
102public class JMSAppender extends AppenderSkeleton {
103
104  String securityPrincipalName;
105  String securityCredentials;
106  String initialContextFactoryName;
107  String urlPkgPrefixes;
108  String providerURL;
109  String topicBindingName;
110  String tcfBindingName;
111  String userName;
112  String password;
113  boolean locationInfo;
114
115  TopicConnection  topicConnection;
116  TopicSession topicSession;
117  TopicPublisher  topicPublisher;
118
119  public
120  JMSAppender() {
121  }
122
123  /**
124     The <b>TopicConnectionFactoryBindingName</b> option takes a
125     string value. Its value will be used to lookup the appropriate
126     <code>TopicConnectionFactory</code> from the JNDI context.
127   */
128  public
129  void setTopicConnectionFactoryBindingName(String tcfBindingName) {
130    this.tcfBindingName = tcfBindingName;
131  }
132
133  /**
134     Returns the value of the <b>TopicConnectionFactoryBindingName</b> option.
135   */
136  public
137  String getTopicConnectionFactoryBindingName() {
138    return tcfBindingName;
139  }
140
141  /**
142     The <b>TopicBindingName</b> option takes a
143     string value. Its value will be used to lookup the appropriate
144     <code>Topic</code> from the JNDI context.
145   */
146  public
147  void setTopicBindingName(String topicBindingName) {
148    this.topicBindingName = topicBindingName;
149  }
150
151  /**
152     Returns the value of the <b>TopicBindingName</b> option.
153   */
154  public
155  String getTopicBindingName() {
156    return topicBindingName;
157  }
158
159
160  /**
161     Returns value of the <b>LocationInfo</b> property which
162     determines whether location (stack) info is sent to the remote
163     subscriber. */
164  public
165  boolean getLocationInfo() {
166    return locationInfo;
167  }
168
169  /**
170   *  Options are activated and become effective only after calling
171   *  this method.*/
172  public void activateOptions() {
173    TopicConnectionFactory  topicConnectionFactory;
174
175    try {
176      Context jndi;
177
178      LogLog.debug("Getting initial context.");
179      if(initialContextFactoryName != null) {
180        Properties env = new Properties( );
181        env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactoryName);
182        if(providerURL != null) {
183          env.put(Context.PROVIDER_URL, providerURL);
184        } else {
185          LogLog.warn("You have set InitialContextFactoryName option but not the "
186                     +"ProviderURL. This is likely to cause problems.");
187        }
188        if(urlPkgPrefixes != null) {
189          env.put(Context.URL_PKG_PREFIXES, urlPkgPrefixes);
190        }
191        
192        if(securityPrincipalName != null) {
193          env.put(Context.SECURITY_PRINCIPAL, securityPrincipalName);
194          if(securityCredentials != null) {
195            env.put(Context.SECURITY_CREDENTIALS, securityCredentials);
196          } else {
197            LogLog.warn("You have set SecurityPrincipalName option but not the "
198                        +"SecurityCredentials. This is likely to cause problems.");
199          }
200        }       
201        jndi = new InitialContext(env);
202      } else {
203        jndi = new InitialContext();
204      }
205
206      LogLog.debug("Looking up ["+tcfBindingName+"]");
207      topicConnectionFactory = (TopicConnectionFactory) lookup(jndi, tcfBindingName);
208      LogLog.debug("About to create TopicConnection.");
209      if(userName != null) {
210        topicConnection = topicConnectionFactory.createTopicConnection(userName, 
211                                                                       password); 
212      } else {
213        topicConnection = topicConnectionFactory.createTopicConnection();
214      }
215
216      LogLog.debug("Creating TopicSession, non-transactional, "
217                   +"in AUTO_ACKNOWLEDGE mode.");
218      topicSession = topicConnection.createTopicSession(false,
219                                                        Session.AUTO_ACKNOWLEDGE);
220
221      LogLog.debug("Looking up topic name ["+topicBindingName+"].");
222      Topic topic = (Topic) lookup(jndi, topicBindingName);
223
224      LogLog.debug("Creating TopicPublisher.");
225      topicPublisher = topicSession.createPublisher(topic);
226      
227      LogLog.debug("Starting TopicConnection.");
228      topicConnection.start();
229
230      jndi.close();
231    } catch(JMSException e) {
232      errorHandler.error("Error while activating options for appender named ["+name+
233                         "].", e, ErrorCode.GENERIC_FAILURE);
234    } catch(NamingException e) {
235      errorHandler.error("Error while activating options for appender named ["+name+
236                         "].", e, ErrorCode.GENERIC_FAILURE);
237    } catch(RuntimeException e) {
238      errorHandler.error("Error while activating options for appender named ["+name+
239                         "].", e, ErrorCode.GENERIC_FAILURE);
240    }
241  }
242
243  protected Object lookup(Context ctx, String name) throws NamingException {
244    try {
245      return ctx.lookup(name);
246    } catch(NameNotFoundException e) {
247      LogLog.error("Could not find name ["+name+"].");
248      throw e;
249    }
250  }
251
252  protected boolean checkEntryConditions() {
253    String fail = null;
254
255    if(this.topicConnection == null) {
256      fail = "No TopicConnection";
257    } else if(this.topicSession == null) {
258      fail = "No TopicSession";
259    } else if(this.topicPublisher == null) {
260      fail = "No TopicPublisher";
261    }
262
263    if(fail != null) {
264      errorHandler.error(fail +" for JMSAppender named ["+name+"].");
265      return false;
266    } else {
267      return true;
268    }
269  }
270
271  /**
272     Close this JMSAppender. Closing releases all resources used by the
273     appender. A closed appender cannot be re-opened. */
274  public synchronized void close() {
275    // The synchronized modifier avoids concurrent append and close operations
276
277    if(this.closed)
278      return;
279
280    LogLog.debug("Closing appender ["+name+"].");
281    this.closed = true;
282
283    try {
284      if(topicSession != null)
285        topicSession.close();
286      if(topicConnection != null)
287        topicConnection.close();
288    } catch(JMSException e) {
289      LogLog.error("Error while closing JMSAppender ["+name+"].", e);
290    } catch(RuntimeException e) {
291      LogLog.error("Error while closing JMSAppender ["+name+"].", e);
292    }
293    // Help garbage collection
294    topicPublisher = null;
295    topicSession = null;
296    topicConnection = null;
297  }
298
299  /**
300     This method called by {@link AppenderSkeleton#doAppend} method to
301     do most of the real appending work.  */
302  public void append(LoggingEvent event) {
303    if(!checkEntryConditions()) {
304      return;
305    }
306
307    try {
308      ObjectMessage msg = topicSession.createObjectMessage();
309      if(locationInfo) {
310        event.getLocationInformation();
311      }
312      msg.setObject(event);
313      topicPublisher.publish(msg);
314    } catch(JMSException e) {
315      errorHandler.error("Could not publish message in JMSAppender ["+name+"].", e,
316                         ErrorCode.GENERIC_FAILURE);
317    } catch(RuntimeException e) {
318      errorHandler.error("Could not publish message in JMSAppender ["+name+"].", e,
319                         ErrorCode.GENERIC_FAILURE);
320    }
321  }
322
323  /**
324   * Returns the value of the <b>InitialContextFactoryName</b> option.
325   * See {@link #setInitialContextFactoryName} for more details on the
326   * meaning of this option.
327   * */
328  public String getInitialContextFactoryName() {
329    return initialContextFactoryName;    
330  }
331  
332  /**
333   * Setting the <b>InitialContextFactoryName</b> method will cause
334   * this <code>JMSAppender</code> instance to use the {@link
335   * InitialContext#InitialContext(Hashtable)} method instead of the
336   * no-argument constructor. If you set this option, you should also
337   * at least set the <b>ProviderURL</b> option.
338   * 
339   * <p>See also {@link #setProviderURL(String)}.
340   * */
341  public void setInitialContextFactoryName(String initialContextFactoryName) {
342    this.initialContextFactoryName = initialContextFactoryName;
343  }
344
345  public String getProviderURL() {
346    return providerURL;    
347  }
348
349  public void setProviderURL(String providerURL) {
350    this.providerURL = providerURL;
351  }
352
353  String getURLPkgPrefixes( ) {
354    return urlPkgPrefixes;
355  }
356
357  public void setURLPkgPrefixes(String urlPkgPrefixes ) {
358    this.urlPkgPrefixes = urlPkgPrefixes;
359  }
360  
361  public String getSecurityCredentials() {
362    return securityCredentials;    
363  }
364
365  public void setSecurityCredentials(String securityCredentials) {
366    this.securityCredentials = securityCredentials;
367  }
368  
369  
370  public String getSecurityPrincipalName() {
371    return securityPrincipalName;    
372  }
373
374  public void setSecurityPrincipalName(String securityPrincipalName) {
375    this.securityPrincipalName = securityPrincipalName;
376  }
377
378  public String getUserName() {
379    return userName;    
380  }
381
382  /**
383   * The user name to use when {@link
384   * TopicConnectionFactory#createTopicConnection(String, String)
385   * creating a topic session}.  If you set this option, you should
386   * also set the <b>Password</b> option. See {@link
387   * #setPassword(String)}.
388   * */
389  public void setUserName(String userName) {
390    this.userName = userName;
391  }
392
393  public String getPassword() {
394    return password;    
395  }
396
397  /**
398   * The paswword to use when creating a topic session.  
399   */
400  public void setPassword(String password) {
401    this.password = password;
402  }
403
404
405  /**
406      If true, the information sent to the remote subscriber will
407      include caller's location information. By default no location
408      information is sent to the subscriber.  */
409  public void setLocationInfo(boolean locationInfo) {
410    this.locationInfo = locationInfo;
411  }
412
413  /**
414   * Returns the TopicConnection used for this appender.  Only valid after
415   * activateOptions() method has been invoked.
416   */
417  protected TopicConnection  getTopicConnection() {
418    return topicConnection;
419  }
420
421  /**
422   * Returns the TopicSession used for this appender.  Only valid after
423   * activateOptions() method has been invoked.
424   */
425  protected TopicSession  getTopicSession() {
426    return topicSession;
427  }
428
429  /**
430   * Returns the TopicPublisher used for this appender.  Only valid after
431   * activateOptions() method has been invoked.
432   */
433  protected TopicPublisher  getTopicPublisher() {
434    return topicPublisher;
435  }
436  
437  /** 
438   * The JMSAppender sends serialized events and consequently does not
439   * require a layout.
440   */
441  public boolean requiresLayout() {
442    return false;
443  }
444}