Introduction

This tutorial will show you how to handle a classical technical feature : the Log (or Logger) feature

The class should be named IamLog and should have 5 methods:

  • log(String message) : to simply output a trace in the log file
  • info(String message) : to output a message at level info
  • warn(String message) : the same as info, with level warn
  • error(String message) : the same as info, with level error
  • debug(String message) : the same as info, with level debug

Exercise description

This exercise goal is to make you program an IamLog Class, which is an utility class, allowing to write and keep technical information while the program is executing

When you will be in your first job, the log files are very important, as they are most of the time the only way to find what happened on the customer installation.

A concrete usecase

Your company product, an IAM software, is deployed on several customers installations. One day, one of your customer is calling your company's support to report that your application unexpectedly crashed, doing a particular action

This case has never been encountered by the support team, and they delegate this bug resolution to you

Fortunately, you added a Logging feature to your application, meaning that all important or critical operations made during the application execution are reported in a text file

Your log file content may look like this

2014/05/31 - 10:06:14.795 : fr.tbr.iamcore.Main.main()[19] : [INFO] Beginning of the program
2014/05/31 - 10:06:14.797 : fr.tbr.iamcore.Main.main()[21] : [INFO] proposing the main menu
2014/05/31 - 10:06:19.180 : fr.tbr.iamcore.Main.main()[30] : [SEVERE] got an exception, unable to continue
2014/05/31 - 10:06:19.180 : fr.tbr.iamcore.Main.main()[31] : [SEVERE] null

Thanks to this technical information, you know that the bug is located in the Main class, in the main(), between the lines 21 and 30

So you open your code :

public static void main(String[] args) {

    IamLog log = IamLog.getLogger(Main.class);
    System.out.println("Welcome in the IAM System");
    log.info("Beginning of the program");
    Scanner scanner = new Scanner(System.in);
    log.info("proposing the main menu"); //line 21
    System.out.println("Please choose among the propositions");
    System.out.println("Create a user (1), Modify a user (2), Delete a user (3)? Your choice (1|2|3)?");
    int answer;
    try {
        answer = scanner.nextInt(); // user action
        log.info("got answer: " + answer);
    } catch (Exception e) {
        log.error("got an exception, unable to continue"); //line 30
        log.error(e.getMessage());
        return;
    }
    log.info("continue with choice: " + answer);
}

You suddenly remember how you managed to output the log information: you did it through the IamLog object. You created one local variable named "log" and called the info() and error() methods with the appropriate parameters. It has nothing with the user dialog, and it does not appear on his console, but is only reported in the log text file sent by the customer

Levels of log, such as "info" or "errror" allow to filter what is output in the log file. As an example, on production systems, as the load is heavy for the system running the application, the customer may want to disable any log, except critical (error) messages. This can be done thanks to the use of different methods, indicating whether a message is really important or not

Bug resolution

And you notice now, that this can happen only if the user input is not satisfying the nextInt() contract, the input has to be an int

So you assume that the input was not correct, and ask for a screenshot of what he did to encounter the error.

What the customers send is below

The user did type a wrong option!

Remember

that what is obvious to you, may seem weird or even is totally misunderstood by the real user of the application. So in this case, you should have informed the user that the option was not an expected one, and in the same time, you can advice the customer to read more carefully the program instructions

Goal of this exercise

This exercise will make you apply several concepts, seen (or partially seen) during lectures, such as:

  • String manipulations
  • Dates usage
  • Files usage
  • The built-in java.util.logging features

Warm up : see how it works in Java

As a first try for this feature implementation, we will stay with the most basic information, the date and time, and the message for this trace should be enough

As seen on this lecture, we have to manipulate date and time through the type Date

A brief recall of the java philosophy

Everything is an object...

In Java, we've seen that almost everything was Object. This is why anything we will do will begin by the creation of a Java class

Since the beginning of this course, we have seen two kinds of Java Class

  • Data model classes : they are classes representing an entity of your program, this is usually called the Data model of your application
  • Entry points of your program, where the main method is. Entry points allows the Java Virtual Machine to execute a part of the program, it is a point where the global program flow can start.
  • Service classes: There is an other kind of class, we will call it a Service class. A service is a technical entity (a class in our case) that provides technical operations, allowing to improve/operate the application execution, this is for instance the case with the Authenticator class

Objects using other Objects? This is a Java application

So we have our entry point: the Main class (at least with a main method). For the project you have to realize, this Main class will be, through the main method, the place to call the other objects for the application

For now we have made only one object before, the Authenticator object. Remember?

package fr.tbr.iamcore.authentication;

/**
 * This class allows to perform an authentication for a user trying to access the application
 * {@link #authenticate(String, String)} for further detail about the authentication feature
 * @author Tom
 */
public class Authenticator {

	private String login;
	private boolean authenticated;

	/**
	 * This method allows to check if the user is granted according to its couple (login/pwd)
	 * @param userLogin the input login
	 * @param userPassword the input password
	 * @return true if the user is authenticated
	 */
	public boolean authenticate(String userLogin, String userPassword){
		String foundPassword = getUserPassword(userLogin);
		if (foundPassword == null || foundPassword.trim().isEmpty()){
			//user was not found or no way to find a password
			return false;
		}
		authenticated = userPassword.equals(foundPassword) ;
		if (authenticated){
			System.out.println("Access is granted !");
			this.login = userLogin; //stores the login for further use
		}else{
			System.out.println("Access is denied ...");
		}
		return authenticated;
	}

	public String getLogin() {
		return login;
	}

	public boolean isAuthenticated() {
		return authenticated;
	}

	private String getUserPassword(String user){
		//Should be implemented later, it should check against a database for instance or in a file.
		return user;
	}
} 

Notice that there is no entry point, so to use this object in the program, you'll have to use it in a class holding a main method, such as the following example:

package fr.tbr.iamcore;

import java.util.Scanner;
import fr.tbr.iamcore.authentication.Authenticator;

/**
 * The main entry point of the identity program
 * @author Tom
 */
public class Main {

	public static void main(String[] args) {
		System.out.println("Welcome in the IAM System");
		Scanner scanner = new Scanner(System.in);
		System.out.println("Please authenticate, type your login :");
		String readLogin = scanner.next();
		System.out.println("type your password :");
		String readPassword = scanner.next();
		//Notice the Authenticator usage
		Authenticator authenticator = new Authenticator();
		authenticator.authenticate(readLogin, readPassword);
		if (!authenticator.isAuthenticated()){
			System.out.println("Invalid user/password combination, exiting...");
		}
		System.out.println("Successfully authenticated");
		System.out.println("Please choose among the propositions");
		System.out.println("Create a user (1), Modify a user (2), Delete a user (3)? Your choice (1|2|3)?");
		int answer;
		try {
			answer = scanner.nextInt();
		} catch (Exception e) {
			return;
		}
		scanner.close();
	}
}

This should have warmed you up about how to design and manipulate objects in Java.

Back to the Logger exercise

As specified by the Exercise description, the IamLog class has to have 5 methods, according to this description, we can draw this UML schema

We can now create the IamLog class

Create the class

Hereafter can be a way to realize the IAMLog class
package fr.tbr.iamcore.logging;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * The logger for the IAM Log
 *
 * @author Tom
 */
public class IamLog {

	static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd - HH:mm:ss.SSS");
	private String loggingEntity;

	/**
	 * Creates an instance of the logger with the given parameter as a
	 * "logging entity".
	 *
	 * @param loggingEntity
	 */
	public IamLog(String loggingEntity) {
		this.loggingEntity = loggingEntity;
	}

	/**
	 * Output a log entry composed of the message and the level parameter
	 * The call log("Hello all", "INFO");
	 * will output: {the current date} - [INFO] - Hello all
	 *
	 * @param message
	 *            the message you want to output
	 * @param level
	 *            the importance of the message
	 */
	public void log(String message, String level) {
		String trace = sdf.format(new Date()) + " " + loggingEntity + " - ["	+ level + "] - " + message;
		System.out.println(trace);

	}

	/**
	 * output an INFO message
	 *
	 * @param message
	 *            the message to record
	 */
	public void info(String message) {
		log(message, "INFO");
	}

	/**
	 * output an WARN message
	 *
	 * @param message
	 *            the message to record
	 */
	public void warn(String message) {
		log(message, "WARN");
	}

	/**
	 * output an ERROR message
	 *
	 * @param message
	 *            the message to record
	 */
	public void error(String message) {
		log(message, "ERROR");
	}

	/**
	 * output a DEBUG message
	 *
	 * @param message
	 *            the message to record
	 */
	public void debug(String message) {
		log(message, "DEBUG");
	}

}

Test it

Now we can take our entry point class, the Main class, and use it as shown in the first part.
You've probably noticed, that we have to create a new instance of the IamLog class, for each "loggingEntity". In fact, a logging entity could be whatever you like. For this tutorial, we will create a new instance of the IamLog logger, for each class in which we want to trace
package fr.tbr.iamcore;

import fr.tbr.iamcore.logging.IamLog;

public class Main {

	public static void main(String[] args) {
		//...
		IamLog logger = new IamLog(Main.class.getSimpleName());
		logger.debug("a sample debug test");
		//... next code
	}
}
 
This code will print the following output:
2014/06/07 - 19:46:14.779 Main - [DEBUG] - a sample debug test

Outputing stuffs in the standard output - where the program will interact with the user - is not very convenient. So we need to have a more proper way to trace technical informations

This is why, our first improvement will be to write all these traces into a File

Source code

Click here for the first step source code

Improvements

We've seen that the fact to log directly in the console output is not the good way to proceed, especially if your program is a console application

Logging in a file

We will need the File object, defined just below

Introducing the File, FileWriter, FileReader APIs

The Java Development Kit has a built in File management feature, provided by the File object

The File object is like a pointer on the concrete file you are attempting to access. It provides useful method to allow to test if a file exists, to create it if not etc.

The File object can be created using a path as a constructor, see the following example

File loggingFile = new File("C:/my/path/to/file");

To gain write and read access to this file you are forced to use respectively a FileWriter and a FileReader. These objects take a File as a parameter

FileWriter writer = new FileWriter(loggingFile);
writer.write("Created an IamLog instance, beginning of the log file");

For the reader, it is often useful to use a scanner, so you have convenient methods to read the file (next())

FileReader reader = new FileReader(loggingFile);
Scanner scan = new Scanner(reader);
String firstLine = scan.next();

Taking the previous hint in consideration, you can now improve your IamLog by implanting a file management in the constructor, this way:

public IamLog(String loggingEntity) {
	File loggingFile = new File("C:/my/path/to/file");
	if (!loggingFile.exists()){ // test if the file is really existing on the file system
	    try{
			//try to create the file if it does not exist
			loggingFile.createNewFile();
		}catch(IOException ioe){
			//IOException is the standard exception when you have problem while accessing
			//to a physical system resource
			System.out.println("An error occurred while preparing the log file");
		}
	}
	try {
		this.writer = new FileWriter(loggingFile);
		this.writer.write("Created an IamLog instance, beginning of the log file");
		this.writer.flush(); concretely outputting the string in the file
	} catch (IOException e) {
		//Could not write in the file
	}
	this.loggingEntity = loggingEntity;
}

Then you can improve the log() method by replacing the console output with the file one

public void log(String message, String level) {
	String trace = sdf.format(new Date()) + " " + loggingEntity + " - ["	+ level + "] - " + message;
	//old call : System.out.println(trace);
	//new call:
	try {
		this.writer.write(trace);
		this.writer.flush(); // this will concretely output the string in the file
	} catch (IOException e) {
		e.printStackTrace(); //Handle exception here
	}
}

From FileWriter to PrintWriter

After the first run, we obtain that in the targeted log file

Created an IamLog instance, beginning of the log file2014/06/09 - 11:52:10.729 Main - [DEBUG] - a sample debug test

It lacks line feed... Indeed, the FileWriter provides no writeln() or println() method. So you have two choices, you can improve the writer by wrapping around it a PrintWriter, or you can concatenate the standard "line separator" to the String end.

You have to replace the type of writer by PrintWriter, which will give you access to useful methods like println()

//... extract of the IamLog constructor
try {
	this.writer = new PrintWriter(new FileWriter(loggingFile));
	writer.println("Created an IamLog instance, beginning of the log file");
	writer.flush();
} catch (IOException e) {
	//Could not write in the file
}

//the log method
public void log(String message, String level) {
    String trace = sdf.format(new Date()) + " " + loggingEntity + " - ["	+ level + "] - " + message;
    //old call : System.out.println(trace);
    //new call:
    this.writer.println(trace);
    writer.flush();

}
//...
//Just a hint : If you want to get the standard line separator:
System.getProperty("line.separator");
//...

This provides the following output now:

Created an IamLog instance, beginning of the log file
2014/06/09 - 12:10:19.592 Main - [DEBUG] - a sample debug test

Don't let the developer too much freedom

In the log method, you can provide whatever you want as a second parameter, there is no constraint on the level parameter

Example

These calls

logger.log("a sample", "INFO");
logger.log("a bad sample", "BlahBlah");

Will produce

2014/06/09 - 12:10:19.592 Main - [INFO] - a sample
2014/06/09 - 12:10:19.592 Main - [BlahBlah] - a bad sample

This is not good, because it means every developer can have his way to output logs.To avoid that, we can use two possibilities :

  • Public constants so the developer will have predefined values to call the method
  • Enumeration which is a standard way to represent a list of constant values

Define constants

To define constants, you have to use a combination of both static final keywords, if you want to make them public, simply add public, just as the following block of code

public class IamLog {

	public static final String INFO = "INFO";
	public static final String WARN = "WARN";
	public static final String DEBUG = "DEBUG";
	public static final String ERROR = "ERROR";
//...
}

To use them, just proceed as follow

logger.log("a sample", IamLog.INFO); 

This is a bit better, but you still cannot avoid to use a random String, to achieve that functionality, we will need the Enumeration feature

Define Enumerations

An enumeration is a predefine "enumeration"(say a list) of constants, this is a Java entity so it can be considered as a very particular class.

As a result of this remark, an enumeration constitutes a type

The enumeration can be provided in its own file, or in an existing class. This is this last solution that will be chosen:

public class IamLog {

//	public static final String INFO = "INFO";
//	public static final String WARN = "WARN";
//	public static final String DEBUG = "DEBUG";
//	public static final String ERROR = "ERROR";
	/**
	 * Levels for the logging feature
	 * @author Tom
	 */
	public enum Level{
		INFO,
		WARN,
		DEBUG,
		ERROR
	}
//...
	//Notice the modification in the log method, the second parameter is of type "Level",
	//instead of String
	public void log(String message, Level level) {
//...
}

And the usage is as specified below

logger.log("a sample", Level.INFO);
logger.log("a good sample", Level.WARN);

If you try to put something else (a raw String for instance, like "BlahBlah"), you will now have a compilation error

Source code

Click here to have the improved version

Using the embedded JDK logger

The JDK embeds a ready-to-use logger feature, the tutorial will soon include the description of how to use it. You still have a working sample in the source code below

Source code

The last IamLog state