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 }