/* * 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) { } } }