View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17
    18  package org.apache.log4j.net;
    19
    20  import org.apache.log4j.AppenderSkeleton;
    21  import org.apache.log4j.helpers.LogLog;
    22  import org.apache.log4j.spi.ErrorCode;
    23  import org.apache.log4j.spi.LoggingEvent;
    24
    25  import javax.jms.JMSException;
    26  import javax.jms.ObjectMessage;
    27  import javax.jms.Session;
    28  import javax.jms.Topic;
    29  import javax.jms.TopicConnection;
    30  import javax.jms.TopicConnectionFactory;
    31  import javax.jms.TopicPublisher;
    32  import javax.jms.TopicSession;
    33  import javax.naming.Context;
    34  import javax.naming.InitialContext;
    35  import javax.naming.NameNotFoundException;
    36  import javax.naming.NamingException;
    37  import java.util.Properties;
    38
    39  /**
    40   * A simple appender that publishes events to a JMS Topic. The events
    41   * are serialized and transmitted as JMS message type {@link
    42   * ObjectMessage}.
    43
    44   * <p>JMS {@link Topic topics} and {@link TopicConnectionFactory topic
    45   * connection factories} are administered objects that are retrieved
    46   * using JNDI messaging which in turn requires the retrieval of a JNDI
    47   * {@link Context}.
    48
    49   * <p>There are two common methods for retrieving a JNDI {@link
    50   * Context}. If a file resource named <em>jndi.properties</em> is
    51   * available to the JNDI API, it will use the information found
    52   * therein to retrieve an initial JNDI context. To obtain an initial
    53   * context, your code will simply call:
    54
    55     <pre>
    56     InitialContext jndiContext = new InitialContext();
    57     </pre>
    58
    59   * <p>Calling the no-argument <code>InitialContext()</code> method
    60   * will also work from within Enterprise Java Beans (EJBs) because it
    61   * is part of the EJB contract for application servers to provide each
    62   * bean an environment naming context (ENC).
    63
    64   * <p>In the second approach, several predetermined properties are set
    65   * and these properties are passed to the <code>InitialContext</code>
    66   * constructor to connect to the naming service provider. For example,
    67   * to connect to JBoss naming service one would write:
    68
    69  <pre>
    70     Properties env = new Properties( );
    71     env.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
    72     env.put(Context.PROVIDER_URL, "jnp://hostname:1099");
    73     env.put(Context.URL_PKG_PREFIXES, "org.jboss.naming:org.jnp.interfaces");
    74     InitialContext jndiContext = new InitialContext(env);
    75  </pre>
    76
    77     * where <em>hostname</em> is the host where the JBoss application
    78     * server is running.
    79     *
    80     * <p>To connect to the the naming service of Weblogic application
    81     * server one would write:
    82
    83  <pre>
    84     Properties env = new Properties( );
    85     env.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
    86     env.put(Context.PROVIDER_URL, "t3://localhost:7001");
    87     InitialContext jndiContext = new InitialContext(env);
    88  </pre>
    89
    90    * <p>Other JMS providers will obviously require different values.
    91    *
    92    * The initial JNDI context can be obtained by calling the
    93    * no-argument <code>InitialContext()</code> method in EJBs. Only
    94    * clients running in a separate JVM need to be concerned about the
    95    * <em>jndi.properties</em> file and calling {@link
    96    * InitialContext#InitialContext()} or alternatively correctly
    97    * setting the different properties before calling {@link
    98    * InitialContext#InitialContext(java.util.Hashtable)} method.
    99
    100
    101    @author Ceki G&uuml;lc&uuml; */
    102 public 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 }