/*
* Copyright © 2013 Nokia Corporation. All rights reserved.
* Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation.
* Oracle and Java are trademarks or registered trademarks of Oracle and/or its
* affiliates. Other product and company names mentioned herein may be trademarks
* or trade names of their respective owners.
* See LICENSE.TXT for license information.
*/
package com.nokia.example.favouriteartists.tool;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.Calendar;
import javax.microedition.io.CommConnection;
import javax.microedition.io.Connector;
import javax.microedition.io.file.FileConnection;
import javax.microedition.midlet.MIDlet;
/**
* Simple class for logging messages to either console and/or files.
* Support for log levels isn't provided but instead two constants
* {@link #TEST} and {@link #IMPL} are provided. In the source code
* one should use one of these constants to check whether logging should
* be done.
* <pre>
* // In test code:
* if (Log.TEST) Log.note("Test started");
*
* // In stub implementation:
* if (Log.IMPL) Log.note("Method x called");
* </pre>
* <p>
*
* For log files there are two locations to which the Log class tries
* to create the file to. First the memorycard is tried (<code>fileconn.dir.memorycard</code>),
* and if that wasn't successful then the Recordings directory is tried
* (<code>fileconn.dir.recordings</code>).
* <p>
*
* The implementation assumes that the FileConnection API is supported.
* Otherwise a {@link ClassNotFoundException} will be thrown when trying
* to access this class.
* <p>
*/
public class Log {
/** Index of the {@link PrintStream} attached to a file in the {@link #streams} array. */
private static final int FILE_INDX = 0;
/** Index of the {@link PrintStream} attached to a <code>System.out</code> in the {@link #streams} array. */
private static final int SYSOUT_INDX = 1;
/** Index of the {@link PrintStream} associated with a CommConnection in the {@link #streams} array. */
private static final int COMM_INDX = 2;
/**
* Holds the {@link PrintStream} objects which are used for outputting log messages.
* If an array element value is <code>null</code> then it is skipped.
*/
private static PrintStream[] streams = new PrintStream[3];
/** If not <code>null</code> then this is the {@link FileConnection} that is being used for log output. */
private static FileConnection fileConn = null;
/** If not <code>null</code> then this is the {@link CommConnection} used for log output. */
private static CommConnection commConn = null;
/** If set then all log messages will be forwarded to the delegate. */
private static LogDelegate delegate = null;
/** Default COM port identifier. */
private static final String DEFAULT_COM_PORT_ID = "USB1";
/**
* Indicates whether test case logging has been enabled or disabled.
* Enables excluding Log calls completely if used in a surrounding
* if-condition.
*/
public static final boolean TEST = true;
/**
* Like {@link #TEST} but meant for excluding Log calls from
* stub implementation code.
*/
public static final boolean IMPL = false;
/** Logging setup identifier. */
private static String id;
/**
* Enable or disable printing log messages to a file.
* File logging should be disabled when exiting the MIDlet as it
* will also close the opened {@link FileConnection}.
*
* @param enabled if <code>true</code> then a new file is created,
* <code>false</code> disables logging to a file.
* @param filenamePrefix prefix of the filename part of the file path.
*
* @throws IOException if a file could not be created.
* @throws SecurityException if no permission was given to create a file.
*/
public static void setFileLogger(boolean enabled, String filenamePrefix) throws IOException, SecurityException {
setFileLogger(enabled, filenamePrefix, false);
}
/**
* Enable or disable printing log messages to a file.
* File logging should be disabled when exiting the MIDlet as it
* will also close the opened {@link FileConnection}.
*
* @param enabled if <code>true</code> then a new file is created,
* <code>false</code> disables logging to a file.
* @param filenamePrefix prefix of the filename part of the file path.
* @param append if <code>true</code> then don't use timestamp in the filename but append to existing log file.
* @throws IOException if a file could not be created.
* @throws SecurityException if no permission was given to create a file.
*/
public static void setFileLogger(boolean enabled, String filenamePrefix, boolean append)
throws IOException, SecurityException
{
if (enabled) {
openFileConnection(filenamePrefix, append);
} else {
if (streams[FILE_INDX] != null) {
streams[FILE_INDX].close();
streams[FILE_INDX] = null;
}
if (fileConn != null) {
try {
fileConn.close();
} catch (IOException e) {
}
fileConn = null;
}
}
}
/**
* <p>Enable or disable logging using {@link CommConnection}. If <code>commPortId</code>
* value is <code>null</code> then the default COM port identifier will be
* used.</p>
*
* <p>Remember to add the permission <code>javax.microedition.io.Connector.comm</code>
* to the JAD file.</p>
*
* @param enabled <code>true</code> to enable and <code>false</code> to disable.
* @param commPortId optional COM port identifier.
*
* @throws IOException if a CommConnection could not be opened.
* @throws SecurityException if no permission was given to create the connection.
*/
public static void setCommPortLogger(boolean enabled, String commPortId)
throws IOException, SecurityException
{
if (commPortId == null) {
commPortId = DEFAULT_COM_PORT_ID;
}
if (enabled) {
openCommConnection(commPortId);
} else {
if (streams[COMM_INDX] != null) {
streams[COMM_INDX].close();
streams[COMM_INDX] = null;
}
if (commConn != null) {
try {
commConn.close();
} catch (IOException e) {
}
commConn = null;
}
}
}
/**
* Opens a {@link CommConnection} using the given COM port identifier.
*
* @param commPortId the COM port identifier, must not be <code>null</code>.
*
* @throws IOException if a CommConnection could not be opened.
* @throws SecurityException if no permission was given to create the connection.
*/
private static void openCommConnection(String commPortId)
throws IOException, SecurityException
{
String portIds = System.getProperty("microedition.commports");
if (portIds == null || portIds.indexOf(commPortId) == -1) {
throw new IOException("COM port not available: " + commPortId);
}
commConn = (CommConnection) Connector.open("comm:" + commPortId);
streams[COMM_INDX] = new PrintStream(commConn.openOutputStream());
}
/**
* Opens the {@link FileConnection} and a {@link PrintStream}.
*
* @param filenamePrefix prefix of the filename part of the file path.
* @param append if <code>true</code> then don't use timestamp in the filename but append to existing log file.
* @throws IOException if a file could not be created.
* @throws SecurityException if no permission was given to create a file.
* @throws NullPointerException if <code>filenamePrefix</code> is <code>null</code>.
*/
private static void openFileConnection(String filenamePrefix, boolean append)
throws IOException, SecurityException
{
if (!System.getProperty("microedition.io.file.FileConnection.version").equals("1.0")) {
// FileConnection API version 1.0 isn't supported.
// Probably a bit unnecessary check as if it isn't supported
// a ClassNotFoundException would have been thrown earlier.
throw new IOException("FileConnection not available");
}
final String filename = createLogFilename(filenamePrefix, !append);
final String[] pathProperties = {"fileconn.dir.memorycard", "fileconn.dir.recordings"};
String path = null;
// Attempt to create a file to the directories specified by the
// system properties in array pathProperties.
for (int i = 0; i < pathProperties.length; i++) {
path = System.getProperty(pathProperties[i]);
// Only throw declared exceptions if this is the last path
// to try.
try {
if (path == null) {
if (i < (pathProperties.length - 1)) {
continue;
} else {
throw new IOException("Path not available: " + pathProperties[i]);
}
}
FileConnection fConn = (FileConnection)
Connector.open(path + filename, Connector.READ_WRITE);
OutputStream os = null;
if (append) {
if (!fConn.exists()) {
fConn.create();
}
os = fConn.openOutputStream(fConn.fileSize());
} else {
// Assume that createLogFilename creates such a filename
// that is enough to separate filenames even if they
// are created in a short interval (seconds).
fConn.create();
os = fConn.openOutputStream();
}
streams[FILE_INDX] = new PrintStream(os);
// Opening the connection and stream was successful so don't
// try other paths.
fileConn = fConn;
break;
} catch (SecurityException se) {
if (i == (pathProperties.length - 1)) {
throw se;
}
} catch (IOException ioe) {
if (i == (pathProperties.length - 1)) {
throw ioe;
}
}
}
}
/**
* Creates a filename to be used for the log file.
*
* @param filenamePrefix prefix for the filename.
* @param useTimestamp if <code>true</code> then append timestamp to the filename.
* @return a log file name.
*/
private static String createLogFilename(String filenamePrefix, boolean useTimestamp) {
StringBuffer fn = new StringBuffer(filenamePrefix);
if (useTimestamp) {
fn.append('_');
appendTimeStamp(fn, true);
}
fn.append(".log");
return fn.toString();
}
/**
* Appends 2 digits to the given {@link StringBuffer}. If the
* <code>num</code> is less than 10 a 0 will be appended before it.
*
* @param sb the destination for the digits.
* @param num the positive integer number to append.
*
* @throws NullPointerException if <code>sb</code> is <code>null</code>.
*/
private static void append2Digits(StringBuffer sb, int num) {
if (num < 10) {
sb.append('0');
}
sb.append(num);
}
/**
* Prints 2 digits to the given {@link PrintStream}. If the
* <code>num</code> is less than 10 a 0 will be appended before it.
*
* @param ps the destination for the digits.
* @param num the positive integer number to append.
*
* @throws NullPointerException if <code>ps</code> is <code>null</code>.
*/
private static final void print2Digits(PrintStream ps, int num) {
if (num < 10) {
ps.print('0');
}
ps.print(num);
}
/**
* Append a timestamp to the given {@link StringBuffer}.
* The syntax is <code>hhMMss</code> for time and <code>ddMMyyyy</code>
* for date. Example without a date: <code>102404</code>, and with a date:
* <code>26042007_102404</code>.
*
* @param sb the destination for the timestamp.
* @param includeDate if <code>true</code> then date is also included in the timestamp.
*
* @throws NullPointerException if <code>sb</code> is <code>null</code>.
*/
public static void appendTimeStamp(StringBuffer sb, boolean includeDate) {
Calendar c = Calendar.getInstance();
if (includeDate) {
int day = c.get(Calendar.DAY_OF_MONTH);
int month = c.get(Calendar.MONTH) + 1;
int year = c.get(Calendar.YEAR);
append2Digits(sb, day);
append2Digits(sb, month);
sb.append(year);
sb.append('_');
}
int hour = c.get(Calendar.HOUR_OF_DAY);
int mins = c.get(Calendar.MINUTE);
int secs = c.get(Calendar.SECOND);
append2Digits(sb, hour);
append2Digits(sb, mins);
append2Digits(sb, secs);
}
/**
* Same as {@link #appendTimeStamp(StringBuffer, boolean)} but prints
* the timestamp parts directly to the given PrintStream
*
* @param ps stream where to print the timestamp.
* @param includeDate if <code>true</code> then date is also included in the timestamp.
*
* @throws NullPointerException if <code>ps</code> is <code>null</code>.
*/
public static void printTimeStamp(PrintStream ps, boolean includeDate) {
Calendar c = Calendar.getInstance();
if (includeDate) {
int day = c.get(Calendar.DAY_OF_MONTH);
int month = c.get(Calendar.MONTH) + 1;
int year = c.get(Calendar.YEAR);
print2Digits(ps, day);
print2Digits(ps, month);
ps.print(year);
ps.print('_');
}
int hour = c.get(Calendar.HOUR_OF_DAY);
int mins = c.get(Calendar.MINUTE);
int secs = c.get(Calendar.SECOND);
print2Digits(ps, hour);
print2Digits(ps, mins);
print2Digits(ps, secs);
}
/**
* Enable or disable printing log messages to console (<code>System.out</code>).
*
* @param enabled defines whether console log is enabled (<code>true</code>) or disabled (<code>false</code>).
*/
public static void setConsoleLogger(boolean enabled) {
if (enabled) {
streams[SYSOUT_INDX] = System.out;
} else {
streams[SYSOUT_INDX] = null;
}
}
/**
* @return <code>true</code> if printing log output to a file is enabled.
*/
public static boolean isUsingFiles() {
return (streams[FILE_INDX] != null);
}
/**
* @return <code>true</code> if printing log output to console is enabled.
*/
public static boolean isUsingConsole() {
return (streams[SYSOUT_INDX] != null);
}
/**
* @return <code>true</code> if printing log output using a CommConnection is enabled.
*/
public static boolean isUsingCommConn() {
return (streams[COMM_INDX] != null);
}
/**
* Print the specified message to the enabled streams. A <code>null</code>
* message will be replaced with an empty string.
*
* @param printObj if <code>true</code> then then the <code>obj</code> Object will be included in the message.
* @param message the message to print.
* @param obj the optional object to include in the message.
* @param loggerName the optional logger name, if <code>null</code> then this will not be included.
* @param levelName name of the logging level, must not be <code>null</code>.
*/
public static synchronized void print(boolean printObj, String message, Object obj,
String loggerName, String levelName)
{
if (delegate != null) {
delegate.print(printObj, message, obj, loggerName, levelName);
return;
}
String objStr = (obj == null ? "NULL" : obj.toString());
if (message == null) {
message = "";
}
for (int i = 0; i < streams.length; i++) {
PrintStream ps = streams[i];
if (ps != null) {
printTimeStamp(ps, false);
ps.print(' ');
if (loggerName != null) {
ps.print('[');
ps.print(loggerName);
ps.print("] ");
}
ps.print(levelName);
ps.print(' ');
ps.print(message);
if (printObj) {
ps.print(": ");
ps.print(objStr);
}
ps.println();
ps.flush();
}
}
}
/**
* Print the specified message to the enabled streams. A <code>null</code>
* message will be replaced with an empty string.
*
* @param error if <code>true</code> then the printed message will indicate that this is an error message.
* @param printObj if <code>true</code> then then the <code>obj</code> Object will be included in the message.
* @param message the message to print.
* @param obj the optional object to include in the message.
*/
private static void print(boolean error, boolean printObj, String message, Object obj) {
print(printObj, message, obj, null, (error ? "**ERROR**" : "TRACE"));
}
/**
* Prints the specified message to enabled log streams.
*
* @param message message to print. If null only timestamp will be printed.
*/
public static void note(String message) {
print(false, false, message, null);
}
/**
* Prints the specified message and the object to enabled log streams.
*
* @param message message to print. If null only timestamp and value will be printed.
*/
public static void note(String message, Object value) {
print(false, true, message, value);
}
/**
* Prints the specified message and integer value to enabled log streams.
*
* @param message message to print. If null only timestamp and value will be printed.
*/
public static void note(String message, int value) {
print(false, true, message, new Integer(value));
}
/**
* Prints the specified message and long integer value to enabled log streams.
*
* @param message message to print. If null only timestamp and value will be printed.
*/
public static void note(String message, long value) {
print(false, true, message, new Long(value));
}
/**
* Prints the specified error message and boolean value to enabled log streams.
*
* @param message message to print. If null only timestamp and value will be printed.
*/
public static void note(String message, boolean value) {
print(false, true, message, new Boolean(value));
}
/**
* Prints the specified error message to enabled log streams.
*
* @param message message to print. If null only timestamp will be printed.
*/
public static void error(String message) {
print(true, false, message, null);
}
/**
* Prints the specified error message and the object to enabled log streams.
*
* @param message message to print. If null only timestamp and value will be printed.
*/
public static void error(String message, Object value) {
print(true, true, message, value);
}
/**
* Prints the specified error message and integer value to enabled log streams.
*
* @param message message to print. If null only timestamp and value will be printed.
*/
public static void error(String message, int value) {
print(true, true, message, new Integer(value));
}
/**
* Prints the specified error message and long integer value to enabled log streams.
*
* @param message message to print. If null only timestamp and value will be printed.
*/
public static void error(String message, long value) {
print(true, true, message, new Long(value));
}
/**
* Prints the specified error message and boolean value to enabled log streams.
*
* @param message message to print. If null only timestamp and value will be printed.
*/
public static void error(String message, boolean value) {
print(true, true, message, new Boolean(value));
}
//=== Log initialization and closing related methods ======================>
/**
* @return logging setup identifier or <code>null</code> if not initialized yet.
*/
public static String getId() {
return Log.id;
}
/**
* <p>Defines a delegate that will take care of printing the log messages to a file,
* console or where ever. If set, then {@link Log} will not print anything
* to any of the defined outputs.</p>
*
* <p>Delegate enables a MIDlet to use another logging library without losing
* SA Library's log messages.</p>
*
* @param delegate the log delegate, <code>null</code> to unset the delegate.
*/
public static void setDelegate(LogDelegate delegate) {
Log.delegate = delegate;
}
/**
* Enables or disables console (sysout/commconn) and file logging based on the
* classname (packagename not counted for) of the specified MIDlet.
*
* @return <code>true</code> if log initialization was successful and
* <code>false</code> if an error occurred.
*/
public static boolean initLogging(MIDlet midlet) {
return initLogging(midlet, null);
}
/**
* Enables or disables console (sysout/commconn) and file logging based on the
* classname (packagename not counted for) of the specified MIDlet.
* The identifier can be used for allowing MIDlet specific logging settings
* in case a MIDlet suite has multiple MIDlets. The
* MIDlet specific JAD attribute names are prefixed with
* <code>[id]-</code>, e.g. if identifier is <code>FgMidlet</code>
* the attributes are be <code>FgMidlet-logfile</code>,
* <code>FgMidlet-logsysout</code> and <code>FgMidlet-logcomm</code>.
* If MIDlet specific attributes are not defined then the common ones
* are used instead, i.e. ones without the identifier prefix.
* Note that since all logging methods are static methods this means
* that there can be only one setup for a single classloader. So if
* MIDlet suite is a normal one with multiple MIDlets then
* usually they all use the same Log class and thus only one setup is possible.
*
* @return <code>true</code> if log initialization was successful and
* <code>false</code> if an error occurred.
*/
public static boolean initLogging(MIDlet midlet, String id) {
if (id == null) {
id = midlet.getClass().getName();
int lastDot = id.lastIndexOf('.');
if (lastDot > -1) {
id = id.substring(lastDot + 1);
}
}
Log.id = id;
try {
// Read default log settings from Jad attributes.
if (isEnabled(midlet, id, "logfile", false)) {
boolean append = isEnabled(midlet, id, "logfile-append", false);
Log.setFileLogger(true, id, append);
} else {
Log.setFileLogger(false, id);
}
if (isEnabled(midlet, id, "logsysout", false)) {
Log.setConsoleLogger(true);
} else {
Log.setConsoleLogger(false);
}
if (isEnabled(midlet, id, "logcomm", false)) {
Log.setCommPortLogger(true, midlet.getAppProperty("logcomm-id"));
} else {
Log.setCommPortLogger(false, null);
}
return true;
} catch (IOException ioe) {
return false;
}
}
/**
* Checks the value of a Jad attribute and returns the corresponding
* boolean value.
* Only attributes that have <code>enabled</code> and <code>disabled</code>
* should be used with this method.
*
* @param midlet the MIDlet which Jad attributes to read.
* @param id logging setup identifier or <code>null</code> to use common logging setup.
* @param attrName name of the attribute to check.
* @param defaultValue default value to be used in case the attribute is not defined.
*
* @return <code>true</code> if the specified attribute has the value
* <code>enabled</code> and <code>false</code> otherwise.
*/
private static final boolean isEnabled(MIDlet midlet, String id, String attrName, boolean defaultValue) {
String origAttrName = attrName;
if (id != null) {
// Read setup specific attribute first.
attrName = id + "-" + attrName;
}
String value = midlet.getAppProperty(attrName);
if (value == null) {
if (id != null) {
// Setup specific value not specified - use common logging setup instead.
return isEnabled(midlet, null, origAttrName, defaultValue);
}
return defaultValue;
} else if (value.equalsIgnoreCase("enabled")) {
return true;
} else {
return false;
}
}
/**
* Closes logging connections.
*/
public static final void closeLogging() {
try {
setFileLogger(false, null);
}
catch (Exception e) {
}
try {
setCommPortLogger(false, null);
}
catch (Exception e) {
}
}
}