Quality First SoftwareQuality First Software
qflib
Examples
IntroductionPackagesExamplesAPI DocumentationProjects
On this page
Package de.qfs.lib.log
Creating log messages
Configuration of the logging system
Setting the level for a Logger
Embedding the log window from qflog
Logging and applets
The "Hello world" applet revisited
Package de.qfs.lib.util
The ArgsParser
The logging system and the command line
Localization via a MapResourceBundle
Package de.qfs.lib.gui
Creating a sorted table
Deutsche Version
Package de.qfs.lib.log
Erzeugen von Logmeldungen
Konfiguration des Logging Systems
Einstellen der level für die Logger
Einbinden des Logfensters von qflog
Logging und Applets
Ein Wiedersehen mit dem "Hello world" Applet
Package de.qfs.lib.util
Der ArgsParser
Logging System und Kommandozeile
Lokalisierung mit dem MapResourceBundle
Package de.qfs.lib.gui
Eine Tabelle mit automatischer Sortierung
Package de.qfs.lib.log

Even though the API reference for the de.qfs.lib.log package may look complex, its use is very straightforward. It can be reduced to the following three points:

  • Creating log messages anywhere in a program.
  • The configuration of the logging system at program startup.
  • Management of the logging system during program execution.

Let's start with some of the basics. Every log message has an associated level of importance. There are five major levels, each with a sublevel for details. The following constants for the levels are provided with the Log class:

ERR, ERRDETAIL Error messages
WRN, WRNDETAIL Warnings
MSG, MSGDETAIL General messages
MTD, MTDDETAIL Method calls
DBG, DBGDETAIL Debugging

In addition to the level and the message itself, a timestamp, the acive thread and the class and method that created the messages are provided. The output of a log message is formatted as follows:

level (timestamp) thread class.method: message

The timestamp, thread and class are created automatically. Unfortunately Java doesn't provide the means to determine the currently executing method, so it has to be passed by hand.

Creation of log messages

Lets take a look at how log messages are created. This should always be done through the use of a Logger. Every class that generates log messages should use its own Logger:

 1  package mypackage;
 2
 3  import de.qfs.lib.log.Log;
 4  import de.qfs.lib.log.Logger;
 5
 6  public class MyClass {
 7
 8      private final static Logger logger = new Logger (MyClass.class);
 9
10      public void myMethod(int var)
11      {
12          if (logger.level >= Log.MTD) {
13                logger.log(Log.MTD, "myMethod(int)",
14                         logger.level < Log.MTDDETAIL ? "" :
15                         "var: " + var);
16          }
17
18          if (logger.level >= Log.DBG) {
19              logger.log(Log.DBG, "myMethod(int)", "Debug message");
20          }
21
22          try {
23              // ...
24          } catch (UnimportantException ex) {
25              if (logger.level >= Log.MSG) {
26                  logger.log(Log.MSG, "myMethod(int)", ex);
27              }
28          } catch (Exception ex) {
29              if (logger.level >= Log.ERR) {
30                  logger.log("myMethod(int)", ex);
31              }
32          }
33      }
34  }
Example 1: Use of a Logger

When creating a Logger, the backward reference to the containing class (example 1, line 8) is necessary in order to add the classname to the log messages. It also enables control of the Logger's level attribute at class and package level. But more on that later.

Example 1 illustrates three common calls of the log method of a Logger. The simplest case is the output of a debug message in line 19. The parameters are the level of the message, the current method and the message itself.

This call is protected with a check of logger.level (line 18), so the log message is created only if the Logger's level is at least Log.DBG. This is the most effecive way to reduce CPU and memory usage, since in the negative case neither gets the log method called, nor are its parameters constructed. Especially the construction of the message parameter can be very expensive for more complex log messages. To gain control over log message generation at runtime, this kind of protection is strictly required.

The method log (lines 12-16) is similar to the call in line 19, except for an additional check. The parameter var is passed only if the Logger's level is at least Log.MTDDETAIL. If it is exactly Log.MTD the log message is generated, but without the parameter. This extra check is optional, but if it is made, method calls can still be monitored while avoiding the price for the construction of the message string except where information about the parameters is truly needed.

Unfortunately Exceptions are very common and have to be logged a lot. Line 30 illustrates the simple case of using level Log.ERR for the message. The Exception's stack trace will be logged at level Log.ERRDETAIL. Passing an explicit level is also possible, as shown in line 26. This call causes the Exception to be logged at level Log.MSG and its stack trace at level Log.MSGDETAIL.

Configuration of the logging system

Next we'll concern ourselves with some configuration issues. What happens to the log messages after they are created? Details about the various kinds of filters being applied can be found in the API reference for the class Log.

With qflib version 0.98.0 the LogSetup class was added to the de.qfs.lib.util package. It greatly simplifies the initlialization of the logging system through command line arguments and is introduced in one of the lib.util examples. The following examples illustrate some common use cases and explain the logging basics on which LogSetup is built:

 1  package mypackage;
 2
 3  import java.io.IOException;
 4
 5  import de.qfs.lib.log.Log;
 6  import de.qfs.lib.log.FileLogWriter;
 7  import de.qfs.lib.logrmi.RemoteLogWriter;
 8
 9  public class MyMainClass {
10
11      public static void main(String[] args)
12      {
13          Log.setOutputLevel(Log.WRNDETAIL);
14
15          try {
16              FileLogWriter.logToFile("myApp", "myapp.log", FileLogWriter.MODE_CREATE, true);
17          } catch (IOException ex) {
18              // ...
19          }
20
21          try {
22              RemoteLogWriter.logRemote("qflog", "myApp", -1, false, true);
23          } catch (Exception ex) {
24              // ...
25          }
26
27          // ...
28
29          // get ready to quit
30          Log.setQueueing(false);
31          FileLogWriter.stopLogging();
32          RemoteLogWriter.stopLogging();
33
34          System.exit(0);
35      }
36  }
Example 2: Configuration of the logging system

The most trivial case is output to System.err. By default, all generated messages will end up there. If a log file or the log server qflog is used, it is probably a good idea to constrain System.err output to e.g. errors and warnings, as shown in example 2, line 14.

Creating a log file is best done with the method FileLogWriter.logToFile, which takes care of all the details (line 16). Please see the API reference for FileLogWriter.logToFile for details about its parameters.

All that is needed to use the log server qflog with your program is shown in line 22. You'll find the details about this call in the API reference for RemoteLogWriter.logRemote.

Upon termination of your program it is wise to make sure the logging system is shut down cleanly (lines 30-32). First the call Log.setQueueing(false) ensures that all buffered messages will be flushed. Then the log file is closed and the connection to qflog is terminated.

Setting the level for a Logger

If you configure the logging system as shown in example 2, you'll find that messages with a higher level than Log.WRNDETAIL are not only banned from System.err, but also from your log file. The reason is the aforementioned mechanism that controls log message generation through the level attribute of a Logger. Its default setting is to prevent generation of messages with a level higher than Log.WRNDETAIL. That can be changed:

 1  package mypackage;
 2
 3  import de.qfs.lib.log.Log;
 4  import de.qfs.lib.log.Logger;
 5  import de.qfs.lib.util.ArgsParser;
 6  import de.qfs.lib.util.MalformedArgumentListException;
 7
 8  public class MyMainClass {
 9
10      public static void main(String[] args)
11      {
12          Logger.setDefaultLogLevel(Log.MSGDETAIL);
13          Logger.setLogLevel("de.qfs.", Log.ERRDETAIL);
14          Logger.setLogLevel("mypackage.", Log.MTD);
15          Logger.setLogLevel("mypackage.NewClass", Log.DBGDETAIL);
16
17          ArgsParser parser = new ArgsParser (new String[] {"+log-*"});
18          try {
19              parser.parse(args);
20              Logger.setLogLevels(parser.getOptions());
21          } catch (MalformedArgumentListException ex) {
22              // ...
23          }
24      }
25  }
Example 3: Setting the level of a Logger

Example 3 shows two of the possible ways of configuring log message generation: by explicitly setting levels in the source code and through command line arguments. In line 12 the level for all classes is increased to Log.MSGDETAIL. Line 13 constrains messages from qflib to errors. Lines 14 and 15 allow messages up to level Log.MTD for all classes from mypackage, for the class mypackage.NewClass even up to Log.DBGDETAIL. The dot ('.') at the end of the package name in lines 13 and 14 is essential. The order in which these calls are executed has no effect.

While this method illustrates how levels work it is not very elegant, since modification of the settings can only be achieved through recompilation. A better way is to read the settings from a property file and pass them directly to Logger.setLogLevels at program startup.

In a similar way the ArgsParser class from the de.qfs.lib.util package of the qflib can be used to read the settings from the command line, as shown in lines 17-23. Settings similar to those from lines 12-15 are obtained through the call

java MyMainClass -log-defaults:6 -log-de.qfs.:2 -log-mypackage.:7 -log-mypackage.NewClass:10

Most convenient is the use of the log server qflog with which the settings can be edited through a graphical interface even during the runtime of your program. The necessary steps on your side were already explained in example 2. For installation and use of qflog see the qflog manual.

Embedding the log window from qflog

The log window from qflog can be embedded directly into your program. This is quite simple, as the following example demonstrates:

 1  package mypackage;
 2
 3  import de.qfs.lib.log.Log;
 4  import de.qfs.apps.qflog.logview.LogView;
 5
 6  public class MyMainClass {
 7
 8      public static void main(String[] args)
 9      {
10          Log.setOutputLevel(Log.WRNDETAIL);
11
12  	    LogView logview = LogView.logToView("myApp");
13  	    logview.getFrame().show();
14
15          // ...
16
17          // get ready to quit
18          Log.setQueueing(false);
19
20          System.exit(0);
21      }
22  }
Example 4: Embedding the LogView component

The LogView component is initialized in line 12 and that's already all there is to it. For real use you probably don't want to open the log window right at program startup, as shown in line 13, but rather via a menu item or a button.

Logging and applets

Writing an applet that runs in all common browsers is about as hard as designing an interesting web page that looks the same everywhere. There are quite a few hidden incompatibilities between the Java VM implementations in Netscape Navigator, Microsoft Internet Explorer and the Java plugin from SUN which is available in versions ranging from 1.1 to 1.3.

An additional problem is the sandbox which restricts an applet's capabilities for security reasons. While a signed applet is able to break out of the sandbox, signing is not easy and, of course, the procedure is different for the three major VMs.

The "Hello world" applet revisited

All that doesn't make writing and debugging applets any easier, so logging would come in very handy. Unfortunately qflib used to suffer (and still does a bit) from just the same problems, but we tried to work around them starting with version 0.97.0.

The worst problem used to be that many versions of Internet Explorer were released without RMI support. Since there was an indirect RMI dependence in the Logger class, it was impossible to use the de.qfs.lib.log package in an applet that had to run in such a browser, even if one only wanted to log to the Java console. Since version 0.97.0 RMI dependence has been moved out of the de.qfs.lib.log package.

Nevertheless the RMI classes are still necessary to connect to qflog. Fortunately, Microsoft at least provide them as an add-on that can be downloaded from their Java resources page. It is enough to install this package on your development system, it is not needed for deployment of the applet.

The next problem qflib suffers from is the sandbox-imposed restriction on network connections. A non-trusted applet may only talk to the server from which is was loaded and may not listen for connections on its own. This means that qflog has to be run on the machine on which the weserver is hosted and that RMI callbacks don't work, which are needed to control the log levels of the Applet's Loggers. The latter is not quite true for Internet Explorer, provided the RMI classes are installed, which has some workaround implemented for callbacks. Nevertheless we're planning to implement an alternative polling system which will no longer depend on callbacks.

Here is an example applet that has all the necessary elements to use logging as far as possible:

  1 import java.applet.Applet;
  2 import java.awt.Graphics;
  3 import java.util.StringTokenizer;
  4
  5 import de.qfs.lib.log.Log;
  6 import de.qfs.lib.log.Logger;
  7
  8 // do NOT import any RMI dependent stuff, otherwise the applet won't initialize
  9 // import de.qfs.lib.logrmi.RemoteLogFilter;
 10
 11 public class HelloWorldApplet
 12     extends Applet
 13 {
 14     private final static Logger logger = new Logger (HelloWorldApplet.class);
 15
 16     private final static String[][] pInfo = {
 17         {"outputlevel", "string", "log level for Java console"},
 18         {"loglevels",   "string", "log levels for the Loggers"}
 19     };
 20
 21     public void init()
 22     {
 23         String outLevel = getParameter("outputlevel");
 24         System.err.println("outLevel: " + outLevel);
 25         if (outLevel != null) {
 26             try {
 27                 Log.setOutputLevel(Integer.parseInt(outLevel));
 28             } catch (NumberFormatException ex) {
 29             }
 30         }
 31
 32         String logLevels = getParameter("loglevels");
 33         System.err.println("logLevels: " + logLevels);
 34         if (logLevels != null) {
 35             setLogLevels(logLevels);
 36         }
 37
 38         String host = getCodeBase().getHost();
 39         try {
 40             de.qfs.lib.logrmi.RemoteLogFilter.logRemote("//" + host + "/qflog", "applet");
 41         } catch (Throwable ex) {
 42             // ignore
 43         }
 44
 45         if (logger.level >= Log.MTD) {
 46             logger.log(Log.MTD, "init()", "");
 47         }
 48     }
 49
 50     public void destroy()
 51     {
 52         if (logger.level >= Log.MTD) {
 53             logger.log(Log.MTD, "destroy()", "");
 54         }
 55         Log.setQueueing(false);
 56         try {
 57             de.qfs.lib.logrmi.RemoteLogFilter.stopLogging();
 58         } catch (Throwable ex) {
 59             // ignore
 60         }
 61     }
 62
 63     public void paint(Graphics g)
 64     {
 65         if (logger.level >= Log.MTD) {
 66             logger.log(Log.MTD, "paint(Graphics)",
 67                        logger.level < Log.MTDDETAIL ? "" :
 68                        "g: " + g);
 69         }
 70         System.err.println("paint");
 71         g.drawString("Hello world!", 50, 25);
 72     }
 73
 74     public String[][] getParameterInfo()
 75     {
 76         return pInfo;
 77     }
 78
 79     private void setLogLevels(String levels)
 80     {
 81         StringTokenizer st = new StringTokenizer (levels, "=,");
 82         while(st.hasMoreTokens()) {
 83             String name = st.nextToken();
 84             System.err.println("name: " + name);
 85             if (st.hasMoreTokens()) {
 86                 String level = st.nextToken();
 87                 System.err.println("level: " + level);
 88                 try {
 89                     int lvl = Integer.parseInt(level);
 90                     System.err.println("lvl: " + lvl);
 91                     if (name.equals("default")) {
 92                         Logger.setDefaultLogLevel(lvl);
 93                     } else {
 94                         Logger.setLogLevel(name, lvl);
 95                     }
 96                 } catch (NumberFormatException ex) {
 97                 }
 98             }
 99         }
100     }
101 }
Example 1: "Hello world" applet

As you can see in line 9, you must not import any RMI dependent classes directly, otherwise applet initialization will fail with a ClassNotFoundException in an Internet Explorer that lacks RMI support. Instead you must use the fully qualified classname as shown in lines 40 and 57. That way the ClassNotFoundException can be caught and safely ignored.

The rest of the applet is quite straightforward and doesn't introduce any new concepts, though a look at lines 32-36 and the setLogLevels method starting at line 79 might be interesting. They show a possible way to control the output log level for the Java console and the initial levels of the Loggers through applet parameters. The follwing HTML example shows how to make use of this (lines 11-12).

 1  <html>
 2    <head>
 3      <title> Hello world applet </title>
 4    </head>
 5    <body>
 6      Here is the output of my program:
 7      <applet code="HelloWorldApplet.class"
 8              codebase="Your codebase here"
 9              archive="qflib_11.jar,collections.jar"
10              width="150" height="25">
11        <param name="outputlevel" value="4">
12        <param name="loglevels" value="default=5,HelloWorldApplet=8,de.qfs.=2">
13      </applet>
14    </body>
15  </html>
Example 2: HTML page for the "Hello world" applet
Package de.qfs.lib.util

Here we will take a look at how some of the most useful classes of the de.qfs.lib.util package work.

Parsing command line arguments with an ArgsParser

Parsing a program's command line arguments is a recurring task. With the help of an ArgsParser this task can be reduced to defining which options are allowed and which of them may or must have parameters. Then let the ArgsParser do its job and get the parsed options in a Hashtable.

Additional options can be read from a property file. Multiple occurence of the same option is possible as well as defining similarly named options through the use of a simple wildcard mechanism.

The following example illustrates the configuration and use of an ArgsParser for a program with the following arguments:

java MyMainClass [-version] [-dir <directory>] [-option <opt>]* [-log-<name> <level>]* <file>

i.e. it expects the options version, dir and option, where dir and option need parameters and option may occur multiple times. Beyond that the logging system will be configured through options starting with log- (see example 3 for the de.qfs.lib.log package). Following the options a file name argument is expected.

 1  package mypackage;
 2
 3  import java.util.Hashtable;
 4
 5  import de.qfs.lib.util.ArgsParser;
 6  import de.qfs.lib.util.MalformedArgumentListException;
 7
 8  public class MyMainClass {
 9
10      public static void main(String[] args)
11      {
12          ArgsParser parser = new ArgsParser (new String[] {
13              "-version",
14              "+dir",
15              "m+option",
16              "+log-*"
17          });
18
19          String filename;
20          try {
21              int pos = parser.parse(args);
22              if (pos >= args.length) {
23                  // Missing file argument, print error and exit
24              }
25              filename = args[pos];
26          } catch (MalformedArgumentListException ex) {
27              // Bad options, print error and exit
28          }
29          Hashtable options = parser.getOptions();
30      }
31  }
Example 1: Parsing command line arguments with an ArgsParser

Calling MyMainClass from the above examples like

java MyMainClass -dir /home -option opt1 -option opt2 myfile

will result in filename having the value "myfile" and the Hashtable options be filled as follows:

Key Value
"dir" "/home"
"option" ["opt1", "opt2"]

Configuration of the logging system through command line arguments

Version 0.98 introduces the new LogSetup class which sets up the logging system based on command line options read from an ArgsParser. You can use it to set most of the parameters of the Log class (output level, etc.) and the levels of the Loggers as well as to redirect log messages to one or multiple logfiles, contact the log server qflog and even to integrate the LogView component of the log server directly into your application.

Please see the API reference for LogSetup for the available options and their use. The following example demonstrates the ease of use of the LogSetup class:

 1  package mypackage;
 2
 3  import de.qfs.lib.util.ArgsParser;
 4  import de.qfs.lib.util.LogSetup;
 5  import de.qfs.lib.util.MalformedArgumentListException;
 6
 7  public class MyMainClass {
 8
 9      public static void main(String[] args)
10      {
11          ArgsParser parser = new ArgsParser (new String[] {
12              "-version",
13              "+dir",
14              "m+option"
15          });
16
17          // Add the LogSetup's options to the parser
18          LogSetup.instance().addOptions(parser);
19          try {
20              int pos = parser.parse(args);
21          } catch (MalformedArgumentListException ex) {
22              // Bad options, print error and exit
23          }
24          // Evaluate the logging options
25          LogSetup.instance().setupLogging("myApp", parser);
26
27          //...
28
29          // Cleanup when you are done
30          LogSetup.instance().stopLogging();
31      }
32  }
Example 2: How to use LogSetup

Localization via a MapResourceBundle

Java offers reasonable localization support through the ResourceBundle mechanism. The class MapResourceBundle extends this by adding some useful features:

  • Reading properties from jar archives
  • Using default values instead of exception handling when querying resources
  • Special support for Icons
 1  package mypackage;
 2
 3  import de.qfs.lib.util.MapResourceBundle;
 4  import de.qfs.lib.gui.Message;
 5
 6  public class MyMainClass {
 7
 8      public static void main(String[] args)
 9      {
10          MapResourceBundle bundle = new MapResourceBundle ();
11
12          bundle.fetchProperties("/de/qfs/lib/resources/properties/qflib", MapResourceBundle.class);
13          bundle.fetchProperties("/mypackage/resources/myproperties", MyMainClass.class);
14
15          Message.setResources(bundle);
16
17          bundle.getString("dialog.yes.name", "Yes");
18          bundle.getIcon("myicons.arrow", null);
19      }
20  }
Example 3: Localization via a MapResourceBundle

First the MapResourceBundle is filled with the properties provided with qflib (line 12). In line 13 properties from mypackage are added, which may well override some qflib values. The classes passed as parameters are used to locate the property files in jar archives. The class files must reside in the same archive as the property files.

The MapResourceBundle thus initialized can now e.g. be passed to the Message class from the de.qfs.lib.gui package to get localized dialogs for errors and other messages.

Line 17 shows how to query the MapResourceBundle for a value from its properties. The default value passed as an argument will be returned in case the resource is not available. This eases the use of resources compared to handling exceptions.

For the code in line 18 that fetches an Icon to work, the resource "myicons.arrow" in the property file mypackage/resources/myproperties must have as a value the path for the Icon's file, e.g. "/mypackage/myicons/arrow.gif"

Package de.qfs.lib.gui

Ceating a table with automatic sorting

Among other things the de.qfs.lib.gui package contains an extension of the Swing JTable that offers filtering and sorting. Though this is a rather complex task, the interface that the user needs to concern himself wih is quite simple, since most of the default settings and capabilities shouldn't need any tampering with.

 1  package mypackage;
 2
 3  import javax.swing.JTable;
 4
 5  import de.qfs.lib.gui.SortedTableHelper;
 6
 7  public class MyClass {
 8
 9      public static JTable createTable(Object[][] rowData, Object[] columnNames)
10      {
11          JTable table = new JTable(rowData, columnNames);
12          SortedTableHelper helper = new SortedTableHelper (table);
13          helper.prepareTable();
14          return table;
15      }
16  }
Example 1: Using a SortedTableHelper to get a sorted table

Example 1 shows how to create a sorted table that exhibits the default behaviour: no filtering and using the default sort order for each column class (determined with TableMode.getColumnClass), implemented by a DefaultTableModelSorter.

There is a more complex demo that includes filtering as well as sorting, available as SortedTable.java in the demo directory. If you compile and run it you'll get a window with a table where you can try out different sort orders and filter settings. The table allows selection of multiple rows, so you can see the effect sorting has on the selection.

The sort order can be changed by clicking once on a column header. Clicking on the same header again will reverse the direction. A little arrow in the column header indicates the current sort order. Addititonally you can resize a column so that it fits the currently displayed values by double-clicking its header.

The sorted table will keep its selected rows even when the sort order changes as you can verify by selecting some rows and then changing the sort order. If you change the ListSelectionModel for the table's rows after activating the sort mechanism, you have to call helper.saveSelection(true) to keep this working.

You can use the ComboBox at the top of the window to chosse between different filter settings.

There is one thing about the implementation of a sorted table that you have to be aware of: The TableModel the table sees is no longer the original model that it was created with, but a FilteredAndSortedTableModel that is pushed in between the table and its original model. Since the sort order and the possible filtering are imposed by the FilteredAndSortedTableModel without changing the actual model, row numbers obtained from the table (e.g. with getSelectedRows() are no longer valid for the original model. To translate them, use helper.getSortedTableModel().getMappedRow(row). You can see the effect of the mapping by pressing the Dump button in the demo which causes the indices of the selected rows and their mapped values to be printed to System.err.


Last update: MM/dd/yyyy, Copyright © 2000 Quality First Software, Gregor Schmid