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 /*
19 * Copyright (C) The Apache Software Foundation. All rights reserved.
20 */
21
22 package com.klopotek.utils.log;
23
24 import java.sql;
25 import java.util;
26 import org.apache.log4j;
27 import org.apache.log4j.helpers;
28 import org.apache.log4j.spi;
29
30 /**
31 The JDBCAppender, writes messages into a database
32
33 <p><b>The JDBCAppender is configurable at runtime by setting options in two alternatives : </b></p>
34 <dir>
35 <p><b>1. Use a configuration-file</b></p>
36 <p>Define the options in a file (<A HREF="configfile_example.txt">example</A>) and call a <code>PropertyConfigurator.configure(filename)</code> in your code.</p>
37 <p><b>2. Use the methods of JDBCAppender to do it</b></p>
38 <p>Call <code>JDBCAppender::setOption(JDBCAppender.xxx_OPTION, String value)</code> to do it analogically without a configuration-file (<A HREF="code_example2.java">example</A>)</p>
39 </dir>
40
41 <p>All available options are defined as static String-constants in JDBCAppender named xxx_OPTION.</p>
42
43 <p><b>Here is a description of all available options :</b></p>
44 <dir>
45 <p><b>1. Database-options to connect to the database</b></p>
46 <p>- <b>URL_OPTION</b> : a database url of the form jdbc:subprotocol:subname</p>
47 <p>- <b>USERNAME_OPTION</b> : the database user on whose behalf the connection is being made</p>
48 <p>- <b>PASSWORD_OPTION</b> : the user's password</p>
49
50 <p><b>2. Connector-option to specify your own JDBCConnectionHandler</b></p>
51 <p>- <b>CONNECTOR_OPTION</b> : a classname which is implementing the JDBCConnectionHandler-interface</p>
52 <p>This interface is used to get a customized connection.</p>
53 <p>If in addition the database-options are given, these options will be used as arguments for the JDBCConnectionHandler-interface to get a connection.</p>
54 <p>Else if no database-options are given, the JDBCConnectionHandler-interface is called without them.</p>
55 <p>Else if this option is not defined, the database-options are required to open a connection by the JDBCAppender.</p>
56
57 <p><b>3. SQL-option to specify a static sql-statement which will be performed with every occuring message-event</b></p>
58 <p>- <b>SQL_OPTION</b> : a sql-statement which will be used to write to the database</p>
59 <p>Use the variable <b>@MSG@</b> on a location in the statement, which has to be dynamically replaced by the message-text.</p>
60 <p>If you give this option, the table-option and columns-option will be ignored !</p>
61
62 <p><b>4. Table-option to specify a table contained by the database</b></p>
63 <p>- <b>TABLE_OPTION</b> : the table in which the logging will be done</p>
64
65 <p><b>5. Columns-option to describe the important columns of the table (Not nullable columns are mandatory to describe!)</b></p>
66 <p>- <b>COLUMNS_OPTION</b> : a formatted list of column-descriptions</p>
67 <p>Each column description consists of</p>
68 <dir>
69 <p>- the <b><i>name</i></b> of the column (required)</p>
70 <p>- a <b><i>logtype</i></b> which is a static constant of class LogType (required)</p>
71 <p>- and a <b><i>value</i></b> which depends by the LogType (optional/required, depending by logtype)</p>
72 </dir>
73 <p>Here is a description of the available logtypes of class <b>{@link LogType}</b> and how to handle the <b><i>value</i></b>:</p>
74 <dir>
75 <p>o <b>MSG</b> = a value will be ignored, the column will get the message. (One columns need to be of this type!)</p>
76 <p>o <b>STATIC</b> = the value will be filled into the column with every logged message. (Ensure that the type of value can be casted into the sql-type of the column!)</p>
77 <p>o <b>ID</b> = value must be a classname, which implements the JDBCIDHandler-interface.</p>
78 <p>o <b>TIMESTAMP</b> = a value will be ignored, the column will be filled with a actually timestamp with every logged message.</p>
79 <p>o <b>EMPTY</b> = a value will be ignored, the column will be ignored when writing to the database (Ensure to fill not nullable columns by a database trigger!)</p>
80 </dir>
81 <p>If there are more than one column to describe, the columns must be separated by a Tabulator-delimiter (unicode0008) !</p>
82 <p>The arguments of a column-description must be separated by the delimiter '~' !</p>
83 <p><i>(Example : name1~logtype1~value1 name2~logtype2~value2...)</i></p>
84
85 <p><b>6. Layout-options to define the layout of the messages (optional)</b></p>
86 <p>- <b>_</b> : the layout wont be set by a xxx_OPTION</p>
87 <p>See the configuration-file and code examples below...</p>
88 <p>The default is a layout of the class {@link org.apache.log4j.PatternLayout} with the pattern=%m which representate only the message.</p>
89
90 <p><b>7. Buffer-option to define the size of the message-event-buffer (optional)</b></p>
91 <p>- <b>BUFFER_OPTION</b> : define how many messages will be buffered until they will be updated to the database.</p>
92 <p>The default is buffer=1, which will do a update with every happened message-event.</p>
93
94 <p><b>8. Commit-option to define a auto-commitment</b></p>
95 <p>- <b>COMMIT_OPTION</b> : define whether updated messages should be committed to the database (Y) or not (N).</p>
96 <p>The default is commit=Y.</p>
97 </dir>
98
99 <p><b>The sequence of some options is important :</b></p>
100 <dir>
101 <p><b>1. Connector-option OR/AND Database-options</b></p>
102 <p>Any database connection is required !</p>
103 <p><b>2. (Table-option AND Columns-option) OR SQL-option</b></p>
104 <p>Anything of that is required ! Whether where to write something OR what to write somewhere...;-)</p>
105 <p><b>3. All other options can be set at any time...</b></p>
106 <p>The other options are optional and have a default initialization, which can be customized.</p>
107 </dir>
108
109 <p><b>Here is a <b>configuration-file example</b>, which can be used as argument for the <b>PropertyConfigurator</b> : </b><A HREF="configfile_example.txt"> configfile_example.txt</A></p>
110
111 <p><b>Here is a <b>code-example</b> to configure the JDBCAppender <b>with a configuration-file</b> : </b><A HREF="code_example1.java"> code_example1.java</A></p>
112
113 <p><b>Here is a <b>another code-example</b> to configure the JDBCAppender <b>without a configuration-file</b> : </b><A HREF="code_example2.java"> code_example2.java</A></p>
114
115
116
117 <p><b>Author : </b><A HREF="mailto:t.fenner@klopotek.de">Thomas Fenner</A></p>
118
119 @since 1.0
120 */
121 public class JDBCAppender extends AppenderSkeleton
122 {
123 /**
124 A database-option to to set a database url of the form jdbc:subprotocol:subname.
125 */
126 public static final String URL_OPTION = "url";
127
128 /**
129 A database-option to set the database user on whose behalf the connection is being made.
130 */
131 public static final String USERNAME_OPTION = "username";
132
133 /**
134 A database-option to set the user's password.
135 */
136 public static final String PASSWORD_OPTION = "password";
137
138 /**
139 A table-option to specify a table contained by the database
140 */
141 public static final String TABLE_OPTION = "table";
142
143 /**
144 A connector-option to specify your own JDBCConnectionHandler
145 */
146 public static final String CONNECTOR_OPTION = "connector";
147
148 /**
149 A columns-option to describe the important columns of the table
150 */
151 public static final String COLUMNS_OPTION = "columns";
152
153 /**
154 A sql-option to specify a static sql-statement which will be performed with every occuring message-event
155 */
156 public static final String SQL_OPTION = "sql";
157
158 /**
159 A buffer-option to define the size of the message-event-buffer
160 */
161 public static final String BUFFER_OPTION = "buffer";
162
163 /**
164 A commit-option to define a auto-commitment
165 */
166 public static final String COMMIT_OPTION = "commit";
167
168
169 //Variables to store the options values setted by setOption() :
170 private String url = null;
171 private String username = null;
172 private String password = null;
173 private String table = null;
174 private String connection_class = null;
175 private String sql = null;
176 private boolean docommit = true;
177 private int buffer_size = 1;
178 private JDBCConnectionHandler connectionHandler = null;
179
180 //This buffer stores message-events.
181 //When the buffer_size is reached, the buffer will be flushed and the messages will updated to the database.
182 private ArrayList buffer = new ArrayList();
183
184 //Database-connection
185 private Connection con = null;
186
187 //This class encapsulate the logic which is necessary to log into a table
188 private JDBCLogger jlogger = new JDBCLogger();
189
190 //Flags :
191 //A flag to indicate a established database connection
192 private boolean connected = false;
193 //A flag to indicate configuration status
194 private boolean configured = false;
195 //A flag to indicate that everything is ready to get append()-commands.
196 private boolean ready = false;
197
198 /**
199 If program terminates close the database-connection and flush the buffer
200 */
201 public void finalize()
202 {
203 close();
204 super.finalize();
205 }
206
207 /**
208 Internal method. Returns a array of strings containing the available options which can be set with method setOption()
209 */
210 public String[] getOptionStrings()
211 {
212 // The sequence of options in this string is important, because setOption() is called this way ...
213 return new String[]{CONNECTOR_OPTION, URL_OPTION, USERNAME_OPTION, PASSWORD_OPTION, SQL_OPTION, TABLE_OPTION, COLUMNS_OPTION, BUFFER_OPTION, COMMIT_OPTION};
214 }
215
216
217 /**
218 Sets all necessary options
219 */
220 public void setOption(String _option, String _value)
221 {
222 _option = _option.trim();
223 _value = _value.trim();
224
225 if(_option == null || _value == null) return;
226 if(_option.length() == 0 || _value.length() == 0) return;
227
228 _value = _value.trim();
229
230 if(_option.equals(CONNECTOR_OPTION))
231 {
232 if(!connected) connection_class = _value;
233 }
234 else if(_option.equals(URL_OPTION))
235 {
236 if(!connected) url = _value;
237 }
238 else if(_option.equals(USERNAME_OPTION))
239 {
240 if(!connected) username = _value;
241 }
242 else if(_option.equals(PASSWORD_OPTION))
243 {
244 if(!connected) password = _value;
245 }
246 else if(_option.equals(SQL_OPTION))
247 {
248 sql = _value;
249 }
250 else if(_option.equals(TABLE_OPTION))
251 {
252 if(sql != null) return;
253 table = _value;
254 }
255 else if(_option.equals(COLUMNS_OPTION))
256 {
257 if(sql != null) return;
258
259 String name = null;
260 int logtype = -1;
261 String value = null;
262 String column = null;
263 String arg = null;
264 int num_args = 0;
265 int num_columns = 0;
266 StringTokenizer st_col;
267 StringTokenizer st_arg;
268
269 //Columns are TAB-separated
270 st_col = new StringTokenizer(_value, " ");
271
272 num_columns = st_col.countTokens();
273
274 if(num_columns < 1)
275 {
276 errorHandler.error("JDBCAppender::setOption(), Invalid COLUMN_OPTION value : " + _value + " !");
277 return;
278 }
279
280 for(int i=1; i<=num_columns; i++)
281 {
282 column = st_col.nextToken();
283
284 //Arguments are ~-separated
285 st_arg = new StringTokenizer(column, "~");
286
287 num_args = st_arg.countTokens();
288
289 if(num_args < 2)
290 {
291 errorHandler.error("JDBCAppender::setOption(), Invalid COLUMN_OPTION value : " + _value + " !");
292 return;
293 }
294
295 for(int j=1; j<=num_args; j++)
296 {
297 arg = st_arg.nextToken();
298
299 if(j == 1) name = arg;
300 else if(j == 2)
301 {
302 try
303 {
304 logtype = Integer.parseInt(arg);
305 }
306 catch(Exception e)
307 {
308 logtype = LogType.parseLogType(arg);
309 }
310
311 if(!LogType.isLogType(logtype))
312 {
313 errorHandler.error("JDBCAppender::setOption(), Invalid COLUMN_OPTION LogType : " + arg + " !");
314 return;
315 }
316 }
317 else if(j == 3) value = arg;
318 }
319
320 if(!setLogType(name, logtype, value)) return;
321 }
322 }
323 else if(_option.equals(BUFFER_OPTION))
324 {
325 try
326 {
327 buffer_size = Integer.parseInt(_value);
328 }
329 catch(Exception e)
330 {
331 errorHandler.error("JDBCAppender::setOption(), Invalid BUFFER_OPTION value : " + _value + " !");
332 return;
333 }
334 }
335 else if(_option.equals(COMMIT_OPTION))
336 {
337 docommit = _value.equals("Y");
338 }
339
340 if(_option.equals(SQL_OPTION) || _option.equals(TABLE_OPTION))
341 {
342 if(!configured) configure();
343 }
344 }
345
346 /**
347 Internal method. Returns true, you may define your own layout...
348 */
349 public boolean requiresLayout()
350 {
351 return true;
352 }
353
354
355 /**
356 Internal method. Close the database connection & flush the buffer.
357 */
358 public void close()
359 {
360 flush_buffer();
361 if(connection_class == null)
362 {
363 try{con.close();}catch(Exception e){errorHandler.error("JDBCAppender::close(), " + e);}
364 }
365 this.closed = true;
366 }
367
368
369 /**
370 You have to call this function for all provided columns of your log-table !
371 */
372 public boolean setLogType(String _name, int _logtype, Object _value)
373 {
374 if(sql != null) return true;
375
376 if(!configured)
377 {
378 if(!configure()) return false;
379 }
380
381 try
382 {
383 jlogger.setLogType(_name, _logtype, _value);
384 }
385 catch(Exception e)
386 {
387 errorHandler.error("JDBCAppender::setLogType(), " + e);
388 return false;
389 }
390
391 return true;
392 }
393
394
395 /**
396 Internal method. Appends the message to the database table.
397 */
398 public void append(LoggingEvent event)
399 {
400 if(!ready)
401 {
402 if(!ready())
403 {
404 errorHandler.error("JDBCAppender::append(), Not ready to append !");
405 return;
406 }
407 }
408
409 buffer.add(event);
410
411 if(buffer.size() >= buffer_size) flush_buffer();
412 }
413
414
415 /**
416 Internal method. Flushes the buffer.
417 */
418 public void flush_buffer()
419 {
420 try
421 {
422 int size = buffer.size();
423
424 if(size < 1) return;
425
426 for(int i=0; i<size; i++)
427 {
428 LoggingEvent event = (LoggingEvent)buffer.get(i);
429
430 //Insert message into database
431 jlogger.append(layout.format(event));
432 }
433
434 buffer.clear();
435
436 if(docommit) con.commit();
437 }
438 catch(Exception e)
439 {
440 errorHandler.error("JDBCAppender::flush_buffer(), " + e + " : " + jlogger.getErrorMsg());
441 try{con.rollback();} catch(Exception ex){}
442 return;
443 }
444 }
445
446
447 /**
448 Internal method. Returns true, when the JDBCAppender is ready to append messages to the database, else false.
449 */
450 public boolean ready()
451 {
452 if(ready) return true;
453
454 if(!configured) return false;
455
456 ready = jlogger.ready();
457
458 if(!ready){errorHandler.error(jlogger.getErrorMsg());}
459
460 return ready;
461 }
462
463
464 /**
465 Internal method. Connect to the database.
466 */
467 protected void connect() throws Exception
468 {
469 if(connected) return;
470
471 try
472 {
473 if(connection_class == null)
474 {
475 if(url == null) throw new Exception("JDBCAppender::connect(), No URL defined.");
476
477 if(username == null) throw new Exception("JDBCAppender::connect(), No USERNAME defined.");
478
479 if(password == null) throw new Exception("JDBCAppender::connect(), No PASSWORD defined.");
480
481 connectionHandler = new DefaultConnectionHandler();
482 }
483 else
484 {
485 connectionHandler = (JDBCConnectionHandler)(Class.forName(connection_class).newInstance());
486 }
487
488 if(url != null && username != null && password != null)
489 {
490 con = connectionHandler.getConnection(url, username, password);
491 }
492 else
493 {
494 con = connectionHandler.getConnection();
495 }
496
497 if(con.isClosed())
498 {
499 throw new Exception("JDBCAppender::connect(), JDBCConnectionHandler returns no connected Connection !");
500 }
501 }
502 catch(Exception e)
503 {
504 throw new Exception("JDBCAppender::connect(), " + e);
505 }
506
507 connected = true;
508 }
509
510 /**
511 Internal method. Configures for appending...
512 */
513 protected boolean configure()
514 {
515 if(configured) return true;
516
517 if(!connected)
518 {
519 if((connection_class == null) && (url == null || username == null || password == null))
520 {
521 errorHandler.error("JDBCAppender::configure(), Missing database-options or connector-option !");
522 return false;
523 }
524
525 try
526 {
527 connect();
528 }
529 catch(Exception e)
530 {
531 connection_class = null;
532 url = null;
533 errorHandler.error("JDBCAppender::configure(), " + e);
534 return false;
535 }
536 }
537
538 if(sql == null && table == null)
539 {
540 errorHandler.error("JDBCAppender::configure(), No SQL_OPTION or TABLE_OPTION given !");
541 return false;
542 }
543
544 if(!jlogger.isConfigured())
545 {
546 try
547 {
548 jlogger.setConnection(con);
549
550 if(sql == null)
551 {
552 jlogger.configureTable(table);
553 }
554 else jlogger.configureSQL(sql);
555 }
556 catch(Exception e)
557 {
558 errorHandler.error("JDBCAppender::configure(), " + e);
559 return false;
560 }
561 }
562
563 //Default Message-Layout
564 if(layout == null)
565 {
566 layout = new PatternLayout("%m");
567 }
568
569 configured = true;
570
571 return true;
572 }
573 }
574
575 /**
576 This is a default JDBCConnectionHandler used by JDBCAppender
577 */
578 class DefaultConnectionHandler implements JDBCConnectionHandler
579 {
580 Connection con = null;
581
582 public Connection getConnection()
583 {
584 return con;
585 }
586
587 public Connection getConnection(String _url, String _username, String _password)
588 {
589 try
590 {
591 if(con != null && !con.isClosed()) con.close();
592 con = DriverManager.getConnection(_url, _username, _password);
593 con.setAutoCommit(false);
594 }
595 catch(Exception e){}
596
597 return con;
598 }
599 }
600
601
602
603
604
605