View Javadoc

1   /*
2    * Copyright 2007 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.goetz.domino.log4j;
18  
19  import java.net.InetAddress;
20  import java.net.UnknownHostException;
21  import java.util.Date;
22  
23  import lotus.domino.Database;
24  import lotus.domino.DateTime;
25  import lotus.domino.Document;
26  import lotus.domino.NotesException;
27  import lotus.domino.Session;
28  
29  import org.apache.log4j.AppenderSkeleton;
30  import org.apache.log4j.Layout;
31  import org.apache.log4j.SimpleLayout;
32  import org.apache.log4j.helpers.LogLog;
33  import org.apache.log4j.helpers.PatternConverter;
34  import org.apache.log4j.helpers.PatternParser;
35  import org.apache.log4j.spi.ErrorCode;
36  import org.apache.log4j.spi.LoggingEvent;
37  
38  /***
39   * This is the base appender for a log4j agent and servlet appender 
40   * implementation that log to a Domino database using a memory buffer and 
41   * delayed writing.
42   *  
43   * @author Bernd G?tz
44   */
45  public abstract class AbstractAppender extends AppenderSkeleton {
46  
47      /***
48       * The total number of records processed by this Appender.
49       */
50      private long totalLogLinesCount = 0;
51      
52      /***
53       * If this appender has been initialized or not.
54       */
55      private boolean initialized = false;
56  
57  	protected LogDocument doc = new LogDocument();
58  	
59  	private Date lastWrite = new Date();
60  	
61  	private static int DEFAULT_FLUSHTIMEOUT = 20000;
62  	
63  	private int flushTimeout = DEFAULT_FLUSHTIMEOUT;
64  	
65      /***
66       * Creates a new DominoAppender.
67       */
68      public AbstractAppender() {
69          super();
70      }
71  
72      /***
73  	 * Reads the message property that contains a pattern
74  	 * converter layout including an additional variable: %u
75  	 * for agent user.
76  	 *
77  	 * @param message the pattern layout
78  	 */
79  	public void setMessage(String message) {
80  		doc.setMessage(message);
81  	}
82  
83      /***
84  	 * Returns the currently set message.
85  	 *
86  	 */
87  	public String getMessage() {
88  		return doc.getMessage();
89  	}
90  
91  	/***
92  	 * Sets the name under which the log entries appear.
93  	 * 
94  	 * @param appName application name
95  	 */
96  	public void setApplicationName(String appName) {
97  		doc.setApplicationName(appName);
98  	}
99  	
100 	/***
101 	 * Returns the application name under which the log entries appear.
102 	 * 
103 	 * @return application name
104 	 */
105 	public String getApplicationName() {
106 		return doc.getApplicationName();
107 	}
108 	
109 	/***
110      * Sets the form from the log4j properties.
111      *
112      * @param form the form name.
113      */
114     public void setFormName(String formName) {
115     	doc.setFormName(formName);
116     }
117 
118 	/***
119      * Returns the form name.
120      *
121      */
122     public String getFormName() {
123     	return doc.getFormName();
124     }
125 
126     /***
127      * Sets the internal buffer size.
128      *
129      * @param bufferSize the new buffer size.
130      */
131     public void setMaxLines(int maxLines) {
132         doc.setMaxLines(maxLines);
133     }
134 
135     /***
136      * Sets the internal buffer size.
137      *
138      */
139     public int getMaxLines() {
140         return doc.getMaxLines();
141     }
142 
143     /***
144      * Sets the Domino serverName to log to.
145      *
146      * @param serverName the server name where the databaseName to log to is on.
147      */
148     public void setServer(String serverName) {
149         doc.setServerName(serverName);
150     }
151 
152     /***
153      * Returns the serverName name.
154      *
155      */
156     public String getServer() {
157         return doc.getServerName();
158     }
159 
160     /***
161 	 * Returns the databaseName name.
162 	 *
163 	 */
164 	public String getDatabase() {
165 		return doc.getDatabaseName();
166 	}
167 
168 	/***
169      * Sets the databaseName path to log to.
170      *
171      * @param databaseName the path to the databaseName to log to.
172      */
173     public void setDatabase(String databaseName) {
174     	doc.setDatabaseName(databaseName);
175     }
176 
177     public int getFlushTimeout() {
178 		return flushTimeout;
179 	}
180 
181 	public void setFlushTimeout(int flushTimeout) {
182 		this.flushTimeout = flushTimeout;
183 	}
184 
185 	/***
186      * Always returns false. This Appender creates itĄs own
187      * SimpleLayout if no Layout is supplied.
188      *
189      * @return always false
190      */
191     public boolean requiresLayout() {
192         return false;
193     }
194 
195     /***
196      * Returns the Layout used by this Appender.
197      *
198      * @return the current Layout.
199      */
200     public Layout getLayout() {
201         return this.layout;
202     }
203 
204     /***
205      * Test if this Appender can append LoggingEvents.
206      *
207      * @param event the LoggingEvent to process.
208      * @return false if the appender is not ready to get LoggingEvents,
209      * true otherwise.
210      */
211     protected boolean checkEntryConditions(LoggingEvent event) 
212     	throws NotesException {
213 
214         if (closed) {
215             LogLog.warn("Appender [" + name + "] closed. CanĄt append.");
216             return false;
217         }
218         // TODO: check database and current doc id here?
219         return true;
220     }
221 
222     /***
223      * Writes the log entry to the notes document.
224      * 
225      * A log4j pattern parser is used. In addition %u is supported for user 
226      * name. Sample: %d %t %u - %m
227      *
228      * @param doc current log document
229      * @param event the LoggingEvent to act on.
230      * @throws NotesException if the Appender could not
231      * write to the current Document.
232      */
233     protected void addEvent(LoggingEvent event)
234             throws NotesException {
235     	// 1. replace %u by current agent user:
236     	String p;
237     	if (doc.messageContainsUserVariable()) {
238         	p = replace(doc.getMessage(), "%u", retrieveUserName());
239     	} else {
240         	p = doc.getMessage();
241     	}
242         PatternConverter pc = new PatternParser(p).parse();
243     	
244         StringBuffer buf = new StringBuffer();
245 
246         while (pc != null) {
247             pc.format(buf, event);
248             pc = pc.next;
249         }
250         doc.add(buf.toString());
251         totalLogLinesCount++;
252 
253         // log exception stack traces:
254         if (event.getThrowableInformation() != null) {
255         	int l = event.getThrowableStrRep().length;
256         	String[] t = event.getThrowableStrRep();
257         	for (int i = 0; i < l; i++) {
258         		doc.add(t[i]);
259                 totalLogLinesCount++;
260         	}
261         }
262     }
263 
264     /***
265 	 * Initializes application name and path. 
266 	 * 
267 	 * This is being called only once per appender instance, in contrast to 
268 	 * <code>initAppend()</code> and <code>releaseAppend()</code> which are
269 	 * being called each log <code>append</code> call. 
270 	 * 
271 	 */
272 	protected abstract void initialize(LoggingEvent event) 
273 		throws Exception;
274 
275 	/***
276 	 * Returns the session for this log entry.
277 	 * 
278 	 * Domino invalidates the session for each new thread, the private session
279 	 * object.
280 	 * <p>The method is not called <code>getSession</code> because bean 
281 	 * naming conventions would then interpret "session" as a property of the
282 	 * agent.</p>  
283 	 * 
284 	 * @return current session.
285 	 * @throws NotesException
286 	 */
287 	abstract Session retrieveSession() throws NotesException;
288 
289 	/***
290      * Gets the Domino Database to log to.
291      * Initializes communication with Domino, if this is the first call.
292      * 
293      * @return the Domino Database object to log to.
294      */
295     abstract Database getDominoDatabase(Session session) 
296     	throws NotesException;
297     
298     /***
299      * Initialises the appender, e.g. with thread initialisation.
300      * 
301      * @throws NotesException
302      */
303     abstract void initAppend() throws NotesException;
304 
305 	/***
306 	 * Called to release resources on a per log statement level, e.g. thread
307 	 * cleanup.
308 	 * 
309 	 * This method is always called, even in the case of an error. So be aware
310 	 * that some resources might not be valid anymore. 
311 	 */
312 	abstract void releaseAppend();
313 
314     /***
315      * Return the user name.
316      *
317 	 * <p>The method is not called <code>getUserName</code> because bean 
318 	 * naming conventions would then interpret "userName" as a property of the
319 	 * agent.</p>
320 	 *   
321      * @throws NotesException
322      */
323     abstract String retrieveUserName() throws NotesException;
324 
325 	/***
326      * Appends the specified event to this DominoAppender. The
327      * Logger is responsible for this.
328      *
329      * @param event the LoggingEvent to append.
330      */
331     public void append(LoggingEvent event) {
332         try {
333         	if (closed) {
334                 LogLog.warn("Appender '" + getName() + 
335                 		"' closed, canĄt append.");
336                 return;
337             }
338         	// one time init for the layout:
339             if (layout == null) {
340                 layout = new SimpleLayout();
341             }
342             initAppend();
343             if (!initialized) {
344             	LogLog.debug("Initialize appender '" + getName() + 
345             			"' now once.");
346                 initialize(event);
347                 initialized = true;
348             }
349             if (doc.getCurrentSize() == 0) {
350             	doc.setStartTime(new Date());
351             }
352             addEvent(event);
353             // either one of the following conditions leads to writing the 
354             // document: 
355             // 1) the document is full
356             // 2) the number of maximum lines has been reached
357             // 3) it's more than n seconds ago that we did not write the 
358             //    document
359             if (doc.isFull()) {
360             	LogLog.debug("Document is full now");
361                 writeDocument();
362                 totalLogLinesCount += doc.getCurrentLines();
363                 doc.reset(new Date());
364                 return;
365             }
366             if (((new Date()).getTime() - lastWrite.getTime()) >= flushTimeout) {
367             	// flush the document to the database:
368             	LogLog.debug("Flush timeout value reached");
369             	writeDocument();
370             }
371         } catch (NotesException e) {
372             if (e.id == 4000) {
373                 LogLog.debug("Field is too large, dropping current document");
374             	// field is too large, drop current document:
375             	doc.reset(new Date());
376             }
377     	    errorHandler.error("Received Notes exception", e,
378   				   ErrorCode.GENERIC_FAILURE);
379         } catch (Exception e) {
380     	    errorHandler.error("Received exception", e,
381    				   ErrorCode.GENERIC_FAILURE);
382         } finally {
383         	releaseAppend();
384         }
385     }
386     
387     /***
388      * Returns the Domino Document that the current LoggingEvents should be
389      * written to.
390      *
391      * @return the Domino Document to write logs to.
392      */
393     protected Document getLogDocument(Database db) 
394     	throws NotesException {
395     	
396     	LogLog.debug("Return current log document");
397     	
398     	Document document = null;
399     	
400 		if (doc.getCurrentDocId() == null) {
401 			// No document was created yet, start it now:
402 			LogLog.debug("Create new log document");
403             document = db.createDocument();
404 		} else {
405 			// a current document exists, use it:
406 			try {
407 				LogLog.debug("Retrieve log document using id '" + 
408 					doc.getCurrentDocId() + "'");
409 				document = db.getDocumentByID(doc.getCurrentDocId());
410 				if ((document == null) || (!document.isValid()) ||
411 					(document.isDeleted())) {
412 					// not good, create a new one:
413 					LogLog.debug("Create new log docunent anyway");
414 					document = db.createDocument();
415 				}
416 			} catch (NotesException e) {
417 				// the id is invalid, start a new document:
418 				LogLog.debug("Invalid id, create new log document anyway");
419 				document = db.createDocument();
420 			}
421 		}
422         return document;
423     }
424 
425     /***
426      * Writes the document to the database without the finish date.
427      * 
428      * @param session
429      * @throws NotesException
430      */
431     private void writeDocument() 
432     	throws NotesException {
433     	
434     	if (!doc.isDirty()) {
435     		LogLog.debug("No new entries in cache, don't write to database");
436     		return;
437     	}
438     	
439     	LogLog.debug("Writing Notes document now...");
440     	lastWrite = new Date();
441 
442     	Session session = null;
443     	Database db = null;
444     	Document document = null;
445 
446     	try {
447     		
448     		session = retrieveSession();
449     		if (session != null) {
450     	    	db = getDominoDatabase(session);
451     	    	document = getLogDocument(db);
452     	    	
453     	    	boolean isNew = document.isNewNote();
454     	
455     	    	if (isNew) {
456     	    		LogLog.debug("Document is new, initializing...");
457     	    		// only write these fields if the document was just created:
458     		        document.replaceItemValue("Form", doc.getFormName());
459     		        String serverName = session.getServerName();
460     		        if (serverName.length() == 0) {
461     		        	// local (or other reasons?)
462     		        	try {
463     		        		LogLog.debug("Trying to get the host name...");
464     		        		serverName = InetAddress.getLocalHost().getHostName();
465     		        	} catch (UnknownHostException e) {
466     		        		LogLog.debug(
467     		        				"Unknown host exception, setting server name to 'local'");
468     		        		serverName = "local";
469     		        	}
470     		        }
471     		        //document.replaceItemValue("UserName", session.getUserName());
472     		        document.replaceItemValue("Server", serverName);
473     		        document.replaceItemValue("AppName", doc.getApplicationName());
474     		        document.replaceItemValue("AppPath", doc.getApplicationPath());
475     		        DateTime start = retrieveSession().createDateTime(doc.getStartTime());
476     		        document.replaceItemValue("StartTime", start);
477     		        start.recycle();
478     	    	}
479     	    	
480     	    	LogLog.debug("Setting events now...");
481     	        document.replaceItemValue("Events", doc.getEvents());
482     	
483     	        if (doc.getFinishTime() != null) {
484     	        	// finish the document:
485     	        	LogLog.debug("Finish the document now...");
486     	            DateTime t = retrieveSession().createDateTime("Today");
487     	            t.setNow();
488     	            document.replaceItemValue("FinishTime", t);        
489     	            document.replaceItemValue("EventCount", 
490     	            		new Long(doc.getCurrentLines()));
491     	            doc.setCurrentDocId(null);
492     	            t.recycle();
493     	        }
494     	        LogLog.debug("Saving Domino document now...");
495     	        document.save();
496     	        if (isNew && (doc.getFinishTime() == null)) {
497     	        	// we only get a valid note id after save:
498     	        	doc.setCurrentDocId(document.getNoteID());
499     	        }
500     		}
501     	} finally {
502     		if (document != null) {
503     			document.recycle();
504     		}
505     		if (db != null) {
506     			db.recycle();
507     		}
508 //    		if (session != null) {
509 //    			session.recycle();
510 //    		}
511     	}
512     }
513     
514     /***
515      * Closes this appender.
516      * 
517      */
518     public void close() {
519     	// TODO write a tail to the last log doc.
520         LogLog.debug("Closing the appender '" + getName() + "'");
521         try {
522         	writeDocument();
523             releaseAppend();
524         } catch (NotesException e) {
525             LogLog.error("Caught Notes exception", e);
526         }
527         if (!closed) {
528         	closed = true;
529         }
530     }
531 
532     /***
533 	 * Replace a pattern in a string with Java 1.3.
534 	 *
535 	 * @param s is the original String which may contain substring aOldPattern
536 	 * @param oldPattern is the non-empty substring which is to be replaced
537 	 * @param newPattern is the replacement for aOldPattern
538 	 */
539 	protected String replace(final String s,
540 			final String oldPattern, final String newPattern) {
541 		if (oldPattern.equals("")) {
542 			throw new IllegalArgumentException("Old pattern must have content.");
543 		}
544 
545 		final StringBuffer result = new StringBuffer();
546 		//startIdx and idxOld delimit various chunks of aInput; these
547 		//chunks always end where aOldPattern begins
548 		int startIdx = 0;
549 		int idxOld = 0;
550 		while ((idxOld = s.indexOf(oldPattern, startIdx)) >= 0) {
551 			//grab a part of aInput which does not include aOldPattern
552 			result.append(s.substring(startIdx, idxOld));
553 			//add aNewPattern to take place of aOldPattern
554 			result.append(newPattern);
555 
556 			//reset the startIdx to just after the current match, to see
557 			//if there are any further matches
558 			startIdx = idxOld + oldPattern.length();
559 		}
560 		//the final chunk will go to the end of aInput
561 		result.append(s.substring(startIdx));
562 		return result.toString();
563 	}
564 
565 }