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.spi.LoggingEvent;
    22  import org.apache.log4j.spi.ErrorCode;
    23  import org.apache.log4j.helpers.LogLog;
    24
    25  import java.util.Properties;
    26  import javax.jms.TopicConnection;
    27  import javax.jms.TopicConnectionFactory;
    28  import javax.jms.Topic;
    29  import javax.jms.TopicPublisher;
    30  import javax.jms.TopicSession;
    31  import javax.jms.Session;
    32  import javax.jms.ObjectMessage;
    33  import javax.naming.InitialContext;
    34  import javax.naming.Context;
    35  import javax.naming.NameNotFoundException;
    36  import javax.naming.NamingException;
    37
    38  /***
    39   * A simple appender that publishes events to a JMS Topic. The events
    40   * are serialized and transmitted as JMS message type {@link
    41   * ObjectMessage}.
    42
    43   * <p>JMS {@link Topic topics} and {@link TopicConnectionFactory topic
    44   * connection factories} are administered objects that are retrieved
    45   * using JNDI messaging which in turn requires the retreival of a JNDI
    46   * {@link Context}.
    47
    48   * <p>There are two common methods for retrieving a JNDI {@link
    49   * Context}. If a file resource named <em>jndi.properties</em> is
    50   * available to the JNDI API, it will use the information found
    51   * therein to retrieve an initial JNDI context. To obtain an initial
    52   * context, your code will simply call:
    53
    54     <pre>
    55     InitialContext jndiContext = new InitialContext();
    56     </pre>
    57
    58   * <p>Calling the no-argument <code>InitialContext()</code> method
    59   * will also work from within Enterprise Java Beans (EJBs) because it
    60   * is part of the EJB contract for application servers to provide each
    61   * bean an environment naming context (ENC).
    62
    63   * <p>In the second approach, several predetermined properties are set
    64   * and these properties are passed to the <code>InitialContext</code>
    65   * contructor to connect to the naming service provider. For example,
    66   * to connect to JBoss naming service one would write:
    67
    68  <pre>
    69     Properties env = new Properties( );
    70     env.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
    71     env.put(Context.PROVIDER_URL, "jnp://hostname:1099");
    72     env.put(Context.URL_PKG_PREFIXES, "org.jboss.naming:org.jnp.interfaces");
    73     InitialContext jndiContext = new InitialContext(env);
    74  </pre>
    75
    76     * where <em>hostname</em> is the host where the JBoss applicaiton
    77     * server is running.
    78     *
    79     * <p>To connect to the the naming service of Weblogic application
    80     * server one would write:
    81
    82  <pre>
    83     Properties env = new Properties( );
    84     env.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
    85     env.put(Context.PROVIDER_URL, "t3://localhost:7001");
    86     InitialContext jndiContext = new InitialContext(env);
    87  </pre>
    88
    89    * <p>Other JMS providers will obviously require different values.
    90    *
    91    * The initial JNDI context can be obtained by calling the
    92    * no-argument <code>InitialContext()</code> method in EJBs. Only
    93    * clients running in a separate JVM need to be concerned about the
    94    * <em>jndi.properties</em> file and calling {@link
    95    * InitialContext#InitialContext()} or alternatively correctly
    96    * setting the different properties before calling {@link
    97    * InitialContext#InitialContext(java.util.Hashtable)} method.
    98
    99
    100    @author Ceki G&uuml;lc&uuml; */
    101 public class JMSAppender extends AppenderSkeleton {
    102
    103   String securityPrincipalName;
    104   String securityCredentials;
    105   String initialContextFactoryName;
    106   String urlPkgPrefixes;
    107   String providerURL;
    108   String topicBindingName;
    109   String tcfBindingName;
    110   String userName;
    111   String password;
    112   boolean locationInfo;
    113
    114   TopicConnection  topicConnection;
    115   TopicSession topicSession;
    116   TopicPublisher  topicPublisher;
    117
    118   public
    119   JMSAppender() {
    120   }
    121
    122   /***
    123      The <b>TopicConnectionFactoryBindingName</b> option takes a
    124      string value. Its value will be used to lookup the appropriate
    125      <code>TopicConnectionFactory</code> from the JNDI context.
    126    */
    127   public
    128   void setTopicConnectionFactoryBindingName(String tcfBindingName) {
    129     this.tcfBindingName = tcfBindingName;
    130   }
    131
    132   /***
    133      Returns the value of the <b>TopicConnectionFactoryBindingName</b> option.
    134    */
    135   public
    136   String getTopicConnectionFactoryBindingName() {
    137     return tcfBindingName;
    138   }
    139
    140   /***
    141      The <b>TopicBindingName</b> option takes a
    142      string value. Its value will be used to lookup the appropriate
    143      <code>Topic</code> from the JNDI context.
    144    */
    145   public
    146   void setTopicBindingName(String topicBindingName) {
    147     this.topicBindingName = topicBindingName;
    148   }
    149
    150   /***
    151      Returns the value of the <b>TopicBindingName</b> option.
    152    */
    153   public
    154   String getTopicBindingName() {
    155     return topicBindingName;
    156   }
    157
    158
    159   /***
    160      Returns value of the <b>LocationInfo</b> property which
    161      determines whether location (stack) info is sent to the remote
    162      subscriber. */
    163   public
    164   boolean getLocationInfo() {
    165     return locationInfo;
    166   }
    167
    168   /***
    169    *  Options are activated and become effective only after calling
    170    *  this method.*/
    171   public void activateOptions() {
    172     TopicConnectionFactory  topicConnectionFactory;
    173
    174     try {
    175       Context jndi;
    176
    177       LogLog.debug("Getting initial context.");
    178       if(initialContextFactoryName != null) {
    179 	Properties env = new Properties( );
    180 	env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactoryName);
    181 	if(providerURL != null) {
    182 	  env.put(Context.PROVIDER_URL, providerURL);
    183 	} else {
    184 	  LogLog.warn("You have set InitialContextFactoryName option but not the "
    185 		     +"ProviderURL. This is likely to cause problems.");
    186 	}
    187 	if(urlPkgPrefixes != null) {
    188 	  env.put(Context.URL_PKG_PREFIXES, urlPkgPrefixes);
    189 	}
    190
    191 	if(securityPrincipalName != null) {
    192 	  env.put(Context.SECURITY_PRINCIPAL, securityPrincipalName);
    193 	  if(securityCredentials != null) {
    194 	    env.put(Context.SECURITY_CREDENTIALS, securityCredentials);
    195 	  } else {
    196 	    LogLog.warn("You have set SecurityPrincipalName option but not the "
    197 			+"SecurityCredentials. This is likely to cause problems.");
    198 	  }
    199 	}
    200 	jndi = new InitialContext(env);
    201       } else {
    202 	jndi = new InitialContext();
    203       }
    204
    205       LogLog.debug("Looking up ["+tcfBindingName+"]");
    206       topicConnectionFactory = (TopicConnectionFactory) lookup(jndi, tcfBindingName);
    207       LogLog.debug("About to create TopicConnection.");
    208       if(userName != null) {
    209 	topicConnection = topicConnectionFactory.createTopicConnection(userName,
    210 								       password);
    211       } else {
    212 	topicConnection = topicConnectionFactory.createTopicConnection();
    213       }
    214
    215       LogLog.debug("Creating TopicSession, non-transactional, "
    216 		   +"in AUTO_ACKNOWLEDGE mode.");
    217       topicSession = topicConnection.createTopicSession(false,
    218 							Session.AUTO_ACKNOWLEDGE);
    219
    220       LogLog.debug("Looking up topic name ["+topicBindingName+"].");
    221       Topic topic = (Topic) lookup(jndi, topicBindingName);
    222
    223       LogLog.debug("Creating TopicPublisher.");
    224       topicPublisher = topicSession.createPublisher(topic);
    225
    226       LogLog.debug("Starting TopicConnection.");
    227       topicConnection.start();
    228
    229       jndi.close();
    230     } catch(Exception e) {
    231       errorHandler.error("Error while activating options for appender named ["+name+
    232 			 "].", e, ErrorCode.GENERIC_FAILURE);
    233     }
    234   }
    235
    236   protected Object lookup(Context ctx, String name) throws NamingException {
    237     try {
    238       return ctx.lookup(name);
    239     } catch(NameNotFoundException e) {
    240       LogLog.error("Could not find name ["+name+"].");
    241       throw e;
    242     }
    243   }
    244
    245   protected boolean checkEntryConditions() {
    246     String fail = null;
    247
    248     if(this.topicConnection == null) {
    249       fail = "No TopicConnection";
    250     } else if(this.topicSession == null) {
    251       fail = "No TopicSession";
    252     } else if(this.topicPublisher == null) {
    253       fail = "No TopicPublisher";
    254     }
    255
    256     if(fail != null) {
    257       errorHandler.error(fail +" for JMSAppender named ["+name+"].");
    258       return false;
    259     } else {
    260       return true;
    261     }
    262   }
    263
    264   /***
    265      Close this JMSAppender. Closing releases all resources used by the
    266      appender. A closed appender cannot be re-opened. */
    267   public synchronized void close() {
    268     // The synchronized modifier avoids concurrent append and close operations
    269
    270     if(this.closed)
    271       return;
    272
    273     LogLog.debug("Closing appender ["+name+"].");
    274     this.closed = true;
    275
    276     try {
    277       if(topicSession != null)
    278 	topicSession.close();
    279       if(topicConnection != null)
    280 	topicConnection.close();
    281     } catch(Exception e) {
    282       LogLog.error("Error while closing JMSAppender ["+name+"].", e);
    283     }
    284     // Help garbage collection
    285     topicPublisher = null;
    286     topicSession = null;
    287     topicConnection = null;
    288   }
    289
    290   /***
    291      This method called by {@link AppenderSkeleton#doAppend} method to
    292      do most of the real appending work.  */
    293   public void append(LoggingEvent event) {
    294     if(!checkEntryConditions()) {
    295       return;
    296     }
    297
    298     try {
    299       ObjectMessage msg = topicSession.createObjectMessage();
    300       if(locationInfo) {
    301 	event.getLocationInformation();
    302       }
    303       msg.setObject(event);
    304       topicPublisher.publish(msg);
    305     } catch(Exception e) {
    306       errorHandler.error("Could not publish message in JMSAppender ["+name+"].", e,
    307 			 ErrorCode.GENERIC_FAILURE);
    308     }
    309   }
    310
    311   /***
    312    * Returns the value of the <b>InitialContextFactoryName</b> option.
    313    * See {@link #setInitialContextFactoryName} for more details on the
    314    * meaning of this option.
    315    * */
    316   public String getInitialContextFactoryName() {
    317     return initialContextFactoryName;
    318   }
    319
    320   /***
    321    * Setting the <b>InitialContextFactoryName</b> method will cause
    322    * this <code>JMSAppender</code> instance to use the {@link
    323    * InitialContext#InitialContext(Hashtable)} method instead of the
    324    * no-argument constructor. If you set this option, you should also
    325    * at least set the <b>ProviderURL</b> option.
    326    *
    327    * <p>See also {@link #setProviderURL(String)}.
    328    * */
    329   public void setInitialContextFactoryName(String initialContextFactoryName) {
    330     this.initialContextFactoryName = initialContextFactoryName;
    331   }
    332
    333   public String getProviderURL() {
    334     return providerURL;
    335   }
    336
    337   public void setProviderURL(String providerURL) {
    338     this.providerURL = providerURL;
    339   }
    340
    341   String getURLPkgPrefixes( ) {
    342     return urlPkgPrefixes;
    343   }
    344
    345   public void setURLPkgPrefixes(String urlPkgPrefixes ) {
    346     this.urlPkgPrefixes = urlPkgPrefixes;
    347   }
    348
    349   public String getSecurityCredentials() {
    350     return securityCredentials;
    351   }
    352
    353   public void setSecurityCredentials(String securityCredentials) {
    354     this.securityCredentials = securityCredentials;
    355   }
    356
    357
    358   public String getSecurityPrincipalName() {
    359     return securityPrincipalName;
    360   }
    361
    362   public void setSecurityPrincipalName(String securityPrincipalName) {
    363     this.securityPrincipalName = securityPrincipalName;
    364   }
    365
    366   public String getUserName() {
    367     return userName;
    368   }
    369
    370   /***
    371    * The user name to use when {@link
    372    * TopicConnectionFactory#createTopicConnection(String, String)
    373    * creating a topic session}.  If you set this option, you should
    374    * also set the <b>Password</b> option. See {@link
    375    * #setPassword(String)}.
    376    * */
    377   public void setUserName(String userName) {
    378     this.userName = userName;
    379   }
    380
    381   public String getPassword() {
    382     return password;
    383   }
    384
    385   /***
    386    * The paswword to use when creating a topic session.
    387    */
    388   public void setPassword(String password) {
    389     this.password = password;
    390   }
    391
    392
    393   /***
    394       If true, the information sent to the remote subscriber will
    395       include caller's location information. By default no location
    396       information is sent to the subscriber.  */
    397   public void setLocationInfo(boolean locationInfo) {
    398     this.locationInfo = locationInfo;
    399   }
    400
    401   /***
    402    * Returns the TopicConnection used for this appender.  Only valid after
    403    * activateOptions() method has been invoked.
    404    */
    405   protected TopicConnection  getTopicConnection() {
    406     return topicConnection;
    407   }
    408
    409   /***
    410    * Returns the TopicSession used for this appender.  Only valid after
    411    * activateOptions() method has been invoked.
    412    */
    413   protected TopicSession  getTopicSession() {
    414     return topicSession;
    415   }
    416
    417   /***
    418    * Returns the TopicPublisher used for this appender.  Only valid after
    419    * activateOptions() method has been invoked.
    420    */
    421   protected TopicPublisher  getTopicPublisher() {
    422     return topicPublisher;
    423   }
    424
    425   /***
    426    * The JMSAppender sends serialized events and consequently does not
    427    * require a layout.
    428    */
    429   public boolean requiresLayout() {
    430     return false;
    431   }
    432 }