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 }