/* * $Id:$ * IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved. * * http://www.izforge.com/izpack/ * http://izpack.codehaus.org/ * * Copyright 2006 Elmar Grom * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.izforge.izpack.util; import com.izforge.izpack.installer.AutomatedInstallData; import javax.swing.*; import java.io.File; import java.io.FileWriter; import java.text.DateFormatSymbols; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.*; /*---------------------------------------------------------------------------*/ /** * This class handles informing the user about unusual events during the installation process and * therefore about the possibility that the installation may not have succeeded at all or may have * succeeded only partially. Because the logger represents a single resource it is implemented as * singleton. * <p/> * <h1>Usage</h1> * To add a message to the install log call one of the <code>addMessage()</code> methods, using * the appropriate message index. If the message contains placeholders for variables, provide a * <code>String</code> array that supplies the necessary variable text. Calling * <code>addMessage()</code> will add an informative message to the log. These may be used to * indicate actions taken by the installer throughout the progression of the install. Calling * <code>addWarning()</code> will not only add the requested warning message to the log but also * cause the user to be alerted about the fact that the install might not have succeeded completely. * <code>addError()</code> goes one step further, by alerting the user that the installation has * failed. * <p/> * <h1>Adding Messages</h1> * Messages are divided into three categories: * <ol> * <li>informative/general messages * <li>warning messages * <li>error messages * </ol> * To add a message, define the text resource in the language packs. Then add a new constant for the * message, along with a brief description of the purpose of the message. If the message contains * place holders for variable text add an ordered list to the description, that lists all variables * in proper order, so that other programmers have no difficulty to form a correct call for the * message. The constants are defined in the individual interfaces <code>LogMessage</code>, * <code>LogWarning</code> and <code>LogError</code>. * <p/> * To derive a correct integer value for the message index add a new value to either * <code>MESSAGE_BASE</code>, <code>WARNING_BASE</code> or <code>ERROR_BASE</code>, * depending on the message category. Next, increment the MAX_ constant for the message category, to * ensure that the add... methods actually allow the message to be added. The key for the text * resource must be named either <code>log.message_</code>, <code>log.warning_</code> or * <code>log.error_</code> in accordance with the chosen index base. In addition, the key must be * appended by the message index (without the base, since this implementation will automatically * subtract the base). Variable place holders must conform to the specification for * <code>java.text.MessageFormat</code>. * <p/> * <h1>Debug Messages</h1> * The output of debug messages is controlled through system properties. These properties may be set * using the -D command line option. Please note, that the -D option is a command line switch for * the VM, not for IzPack. In order for this to work, these options must be listed on the command * line before IzPack! * <p/> * <h2>Turning Debug On</h2> * In order to receive debug output, it is necessary to turn this feature on explicitely. This is * done with the command line otprion: * <p/> * <pre> * -DIzPack.debug=on * </pre> * <p/> * <h2>Selecting Debug Channels</h2> * Setting the list of specific debug channels to trace is accomplished with the following command * line option: * <p/> * <pre> * -DIzPack.debug.channel=<channelA,channelB,...> * </pre> * <p/> * The parameter is a comma separated list of one or more channel identifiers. * <p/> * <h2>Dumping a List of Debug Channels</h2> * <p/> * <pre> * -DIzPack.debug.dumpList=on * </pre> * <p/> * To turn debug messages on * * @author Elmar Grom * @version 0.0.1 / 11/20/06 */ /*---------------------------------------------------------------------------*/ public class Log implements LogError, LogWarning, LogMessage { // -------------------------------------------------------------------------- // Constant Definitions // -------------------------------------------------------------------------- /** * The prefix for all text resources related to this class */ private static final String RESOURCE_PREFIX = "log."; /** * The formatting used for the report time stamp */ private static final String DATE_FORMAT = RESOURCE_PREFIX + "timeStamp"; /** * The prefix for building message keys */ private static final String MESSAGE_PREFIX = RESOURCE_PREFIX + "message_"; /** * The prefix for building warning message keys */ private static final String WARNING_PREFIX = RESOURCE_PREFIX + "warning_"; /** * The prefix for building error message keys */ private static final String ERROR_PREFIX = RESOURCE_PREFIX + "error_"; /** * System property to turn debug output on */ private static final String DEBUG_SWITCH = "IzPack.debug"; /** * System property to set the debug channels to trace */ private static final String CHANNEL_SPEC = "IzPack.debug.channel"; /** * System property to enable dumping of the list of debug channels that did record messages */ private static final String CHANNEL_LIST = "IzPack.debug.dumpList"; // -------------------------------------------------------------------------- // Variable Declarations // -------------------------------------------------------------------------- /** * The only instance of <code>Messenger</code> */ private static Log me = null; /** * The system dependent newline character sequence */ private String newline = System.getProperty("line.separator"); /** * Access to the installation information and the localized text resources */ private AutomatedInstallData installData = null; /** * The collection of installation messages */ private ArrayList<Record> messages = new ArrayList<Record>(); /** * The collection of warning messages */ private ArrayList<Record> warnings = new ArrayList<Record>(); /** * The collection of error messages */ private ArrayList<Record> errors = new ArrayList<Record>(); /** * The collection of debug messages */ private ArrayList<Record> debug = new ArrayList<Record>(); /** * The list of channels requested for debug output. A <code>Vector</code> must be used for * this purpose, since this is the only class that explicitly specifies that the * <code>equals()</code> method is used for determining if a particular object is contained. */ private Vector<String> channels = null; /** * This map keeps track of all channels that are recorded. It is used for information purposes. */ private Hashtable<String, String> recordedChannels = null; /** * This flag signals if debug messages should be recorded */ private boolean debugActive = false; /** * This flag signals that the identifiers of the recorded debug channels should be dumped */ private boolean dumpChannels = false; /*--------------------------------------------------------------------------*/ /** * This class is installed as singleton. Therefore the constructor is declared private. Use * <code>getInstance()</code> to get an instance. */ /*--------------------------------------------------------------------------*/ private Log() { // ---------------------------------------------------- // gain access to the install data // ---------------------------------------------------- installData = AutomatedInstallData.getInstance(); // ---------------------------------------------------- // get the debug setting // ---------------------------------------------------- String temp = System.getProperty(DEBUG_SWITCH); if ((temp != null) && (temp.toUpperCase().equals("ON"))) { debugActive = true; } if (debugActive) { // ---------------------------------------------------- // get the list of debug channels requested // ---------------------------------------------------- recordedChannels = new Hashtable<String, String>(); channels = new Vector<String>(); temp = System.getProperty(CHANNEL_LIST); if ((temp != null) && (temp.toUpperCase().equals("ON"))) { dumpChannels = true; } // ---------------------------------------------------- // get the list of debug channels requested // ---------------------------------------------------- temp = System.getProperty(CHANNEL_SPEC); if (temp != null) { String[] channelList = temp.split(","); channels.addAll(Arrays.asList(channelList)); } } } /*--------------------------------------------------------------------------*/ /** * Returns the only instance of <code>Log</code> * * @return the only instance of <code>Log</code> */ /*--------------------------------------------------------------------------*/ public static Log getInstance() { if (me == null) { me = new Log(); } return (me); } /*--------------------------------------------------------------------------*/ /** * This method records general installation message * * @param message the numeric identifier of the message to add, as defined in * {@link com.izforge.izpack.util.LogMessage <code>LogMessage</code>} * @param detail a string array of variable fields that should be inserted into the message text */ /*--------------------------------------------------------------------------*/ public void addMessage(int message, String[] detail) { if ((message >= LogMessage.MESSAGE_BASE) && (message < LogMessage.MAX_MESSAGE)) { messages.add(new Record(message, detail)); } } /*--------------------------------------------------------------------------*/ /** * This method records a general installation message, using a custom text template. It allows * cusom code to insert message text that is not defined in IzPack. IzPack internal code should * use the parallel version based on the message index. * * @param template the basic template of the message * @param detail a string array of variable fields that should be inserted into the message * text. Each array element will be inserted into the text template, replacing a marker. * @see java.text.MessageFormat#format(java.lang.String, java.lang.Object[]) */ /*--------------------------------------------------------------------------*/ public void addCustomMessage(String template, String[] detail) { messages.add(new Record(template, detail)); } /*--------------------------------------------------------------------------*/ /** * This method records a warning message to the list of messages * * @param message the numeric identifier of the message to add, as defined in * {@link com.izforge.izpack.util.LogWarning <code>LogWarning</code>} * @param detail a string array of variable fields that should be inserted into the message * text. Each array element will be inserted into the text template, replacing a marker. * @param exception the exception associated with the event or <code>null</code> if there was * none. * @see java.text.MessageFormat#format(java.lang.String, java.lang.Object[]) */ /*--------------------------------------------------------------------------*/ public void addWarning(int message, String[] detail, Throwable exception) { if ((message >= LogWarning.WARNING_BASE) && (message < LogWarning.MAX_WARNING)) { warnings.add(new Record(message, detail, exception)); } } /*--------------------------------------------------------------------------*/ /** * This method records a warning message, using a custom text template. It allows cusom code to * insert message text that is not defined in IzPack. IzPack internal code should use the * parallel version based on the message index. * * @param template the basic template for the message * @param detail a string array of variable fields that should be inserted into the message * text. Each array element will be inserted into the text template, replacing a marker. * @param exception the exception associated with the event or <code>null</code> if there was * none. * @see java.text.MessageFormat#format(java.lang.String, java.lang.Object[]) */ /*--------------------------------------------------------------------------*/ public void addCustomWarning(String template, String[] detail, Throwable exception) { warnings.add(new Record(template, detail, exception)); } /*--------------------------------------------------------------------------*/ /** * This method records an error message to the list of messages * * @param message the numeric identifier of the message to add, as defined in * {@link com.izforge.izpack.util.LogError <code>LogError</code>} * @param detail a string array of variable fields that should be inserted into the message * text. Each array element will be inserted into the text template, replacing a marker. * @param exception the exception associated with the event or <code>null</code> if there was * none. * @see java.text.MessageFormat#format(java.lang.String, java.lang.Object[]) */ /*--------------------------------------------------------------------------*/ public void addError(int message, String[] detail, Throwable exception) { if ((message >= LogError.ERROR_BASE) && (message < LogError.MAX_ERROR)) { errors.add(new Record(message, detail, exception)); installData.installSuccess = false; } } /*--------------------------------------------------------------------------*/ /** * This method records an error message, using a custom text template. It allows cusom code to * insert message text that is not defined in IzPack. IzPack internal code should use the * parallel version based on the message index. * * @param template the basic template for the message * @param detail a string array of variable fields that should be inserted into the message * text. Each array element will be inserted into the text template, replacing a marker. * @param exception the exception associated with the event or <code>null</code> if there was * none. * @see java.text.MessageFormat#format(java.lang.String, java.lang.Object[]) */ /*--------------------------------------------------------------------------*/ public void addCustomError(String template, String[] detail, Throwable exception) { errors.add(new Record(template, detail, exception)); } /*--------------------------------------------------------------------------*/ /** * This method provides a channel for debugging messages in IzPack development. Note that debug * messages are used solely for this purpose. Adding debug messages does not trigger user * notification. If the user should be notified about a specific situation, please also call the * appropriate (message, warning, error) method. Debug messages are not localized, please use * English only. * <p/> * In order to prevent flooding developers with messages that are generally of no interest, each * message may be associated with a specific channel. A message is associated with a channel * simply by providing a channel identifier as call parameter. There is no need for registering * channels beforehand. If <code>null</code> or an empty string is used as channel identifier * the message will be output, regardless of the channel filter applied. Please use this option * sparingly, just for really impotant messages of general interest. * <p/> * To receive output for select channels, start IzPack with the command line option * -DIzPack.debug.channel=, followed by a comma separated list of channel identifiers. * * @param template the basic template for the message * @param detail a string array of variable fields that should be inserted into the message * text. Each array element will be inserted into the text template, replacing a marker. * @param channel the debug channel the message is associated with. * @param exception the exception associated with the event or <code>null</code> if there was * none. */ /*--------------------------------------------------------------------------*/ public void addDebugMessage(String template, String[] detail, String channel, Throwable exception) { if (debugActive) { recordedChannels.put(channel, channel); if ((channel == null) || (channel.length() == 0) || channels.contains(channel)) { Record record = new Record(template, detail, exception, channel); debug.add(record); System.out.println(buildDebug(record)); } } } /*--------------------------------------------------------------------------*/ /** * Reports if any messages have been recorded. * * @return true if any messages have been recorded */ /*--------------------------------------------------------------------------*/ public boolean messagesRecorded() { return (!messages.isEmpty()); } /*--------------------------------------------------------------------------*/ /** * Reports if any warnings have been recorded. * * @return true if any warnings have been recorded */ /*--------------------------------------------------------------------------*/ public boolean warningsRecorded() { return (!warnings.isEmpty()); } /*--------------------------------------------------------------------------*/ /** * Reports if any errors have been recorded. * * @return true if any errors have been recorded */ /*--------------------------------------------------------------------------*/ public boolean errorsRecorded() { return (!errors.isEmpty()); } /*--------------------------------------------------------------------------*/ /** * Displays a dialog that informs the user about the fact that one or more unusual events have * occurred during installation. If nothing has been recorded, this method returns immediately. */ /*--------------------------------------------------------------------------*/ public void informUser() { String message = ""; int messageType = JOptionPane.WARNING_MESSAGE; // ---------------------------------------------------- // fins out if there are any warnings or errors, // otherwise return with no action // ---------------------------------------------------- if (errorsRecorded()) { messageType = JOptionPane.ERROR_MESSAGE; message = installData.langpack.getString(RESOURCE_PREFIX + "informUserFail"); } else if (warningsRecorded()) { messageType = JOptionPane.WARNING_MESSAGE; message = installData.langpack.getString(RESOURCE_PREFIX + "informUserPartial"); } else { return; } // ---------------------------------------------------- // present the warning message // ---------------------------------------------------- int userChoice = JOptionPane.showConfirmDialog(null, message, installData.langpack .getString(RESOURCE_PREFIX + "informUserTitle"), JOptionPane.YES_NO_OPTION, messageType); // ---------------------------------------------------- // if the user has elected to write the report, present // the file selection dialog. // ---------------------------------------------------- if (userChoice == JOptionPane.OK_OPTION) { writeReport(); } } /*--------------------------------------------------------------------------*/ /** * Presents the user with a dialog to select a file and location for the installation report and * writes a report contianing all messages to the user selected file. */ /*--------------------------------------------------------------------------*/ public void writeReport() { JFileChooser fileChooser = new JFileChooser(); fileChooser .setDialogTitle(installData.langpack.getString(RESOURCE_PREFIX + "saveLogTitle")); fileChooser.setSelectedFile(new File(installData.langpack.getString(RESOURCE_PREFIX + "LogFileName"))); int choice = fileChooser.showSaveDialog(null); if (choice == JFileChooser.APPROVE_OPTION) { writeReport(fileChooser.getSelectedFile().getAbsolutePath()); } } /*--------------------------------------------------------------------------*/ /** * Writes a report contianing all messages to the indicated file. * * @param file the fully qualifies name of the file to write to. */ /*--------------------------------------------------------------------------*/ public void writeReport(String file) { try { FileWriter writer = new FileWriter(file); String text = compileReport(); writer.write(text, 0, text.length()); writer.flush(); writer.close(); } catch (Throwable exception) { try { JOptionPane.showMessageDialog(null, installData.langpack.getString( (RESOURCE_PREFIX + "reportWriteError"), new String[]{file}), installData.langpack.getString(RESOURCE_PREFIX + "reportWriteErrorTitle"), JOptionPane.ERROR_MESSAGE); } catch (Throwable exception2) { // What todo if writing log report fails?? We can ignore // or write it exceptionally on stderr. exception2.printStackTrace(); } } } /*--------------------------------------------------------------------------*/ /** * Prepares an installation report from the recorded information * * @return the installation report */ /*--------------------------------------------------------------------------*/ private String compileReport() { StringBuffer report = new StringBuffer(); String dateFormat = installData.langpack.getString(DATE_FORMAT); int count; // ---------------------------------------------------- // insert header // ---------------------------------------------------- report .append("-------------------------------------------------------------------------------"); report.append(newline); report.append(installData.langpack.getString(RESOURCE_PREFIX + "reportHeading")); report.append(newline); report .append("-------------------------------------------------------------------------------"); report.append(newline); // ---------------------------------------------------- // insert a general warning message if appropriate // ---------------------------------------------------- if (errorsRecorded()) { report.append(newline); report.append(installData.langpack.getString(RESOURCE_PREFIX + "installFailed")); report.append(newline); } else if (warningsRecorded()) { report.append(newline); report.append(installData.langpack.getString(RESOURCE_PREFIX + "partialInstall")); report.append(newline); } // ---------------------------------------------------- // insert general information about the installation // ---------------------------------------------------- report.append(newline); report.append(installData.langpack.getString(RESOURCE_PREFIX + "messageCount", new String[]{Integer.toString(messages.size()), Integer.toString(warnings.size()), Integer.toString(errors.size())})); report.append(newline); report.append(newline); report.append(installData.langpack.getString(RESOURCE_PREFIX + "application", new String[]{ installData.info.getAppName(), installData.info.getAppVersion()})); report.append(newline); report.append(installData.langpack.getString(RESOURCE_PREFIX + "timePrefix", new String[]{new SimpleDateFormat(dateFormat, new DateFormatSymbols()) .format(new Date())})); report.append(newline); report.append(installData.langpack.getString(RESOURCE_PREFIX + "pathPrefix", new String[]{installData.getInstallPath()})); report.append(newline); // ---------------------------------------------------- // insert the recorded mnessages // ---------------------------------------------------- if (messagesRecorded()) { report.append(newline); report.append(installData.langpack.getString(RESOURCE_PREFIX + "messageHeading")); report.append(newline); report.append(newline); count = messages.size(); for (int i = 0; i < count; i++) { report.append(buildMessage(i)); } } // ---------------------------------------------------- // insert the recorded warning messages // ---------------------------------------------------- if (warningsRecorded()) { report.append(newline); report.append(installData.langpack.getString(RESOURCE_PREFIX + "warningHeading")); report.append(newline); report.append(newline); count = warnings.size(); for (int i = 0; i < count; i++) { report.append(buildWarning(i)); } } // ---------------------------------------------------- // insert the recorded error messages // ---------------------------------------------------- if (errorsRecorded()) { report.append(newline); report.append(installData.langpack.getString(RESOURCE_PREFIX + "errorHeading")); report.append(newline); report.append(newline); count = errors.size(); for (int i = 0; i < count; i++) { report.append(buildError(i)); } } // ---------------------------------------------------- // insert the debug messages // ---------------------------------------------------- if (debugActive) { report.append(newline); report.append(installData.langpack.getString(RESOURCE_PREFIX + "debugHeading")); report.append(newline); report.append(newline); count = errors.size(); for (int i = 0; i < count; i++) { report.append(buildDebug(i)); } } report .append("-------------------------------------------------------------------------------"); report.append(newline); return (report.toString()); } /*--------------------------------------------------------------------------*/ /** * Builds a general installation message from the information contained in the indicated record * * @param index the index of the requested message entry */ /*--------------------------------------------------------------------------*/ private String buildMessage(int index) { Record record = messages.get(index); StringBuffer message = new StringBuffer(); // ---------------------------------------------------- // append the message text // ---------------------------------------------------- message.append(installData.langpack.getString(RESOURCE_PREFIX + "messagePrefix", new String[]{Integer.toString(index)})); if (record.message >= 0) { message .append(installData.langpack.getString(MESSAGE_PREFIX + Integer.toString(record.message), record.variables)); } else { message.append(MessageFormat.format(record.template, new Object[]{record.variables})); } message.append(newline); return (message.toString()); } /*--------------------------------------------------------------------------*/ /** * Builds a warning message from the information contained in the indicated warning record * * @param index the index of the requested warning entry */ /*--------------------------------------------------------------------------*/ private String buildWarning(int index) { Record record = warnings.get(index); StringBuffer message = new StringBuffer(); // ---------------------------------------------------- // append the message text // ---------------------------------------------------- message.append(installData.langpack.getString(RESOURCE_PREFIX + "warningPrefix", new String[]{Integer.toString(index)})); if (record.message >= 0) { message .append(installData.langpack.getString(WARNING_PREFIX + Integer.toString(record.message - LogWarning.WARNING_BASE), record.variables)); } else { message.append(MessageFormat.format(record.template, new Object[]{record.variables})); } // ---------------------------------------------------- // append the exception // ---------------------------------------------------- if (record.exception != null) { message.append(newline); message.append(installData.langpack.getString(RESOURCE_PREFIX + "exceptionPrefix", new String[]{record.exception.toString()})); } message.append(newline); return (message.toString()); } /*--------------------------------------------------------------------------*/ /** * Builds an error message from the information contained in the indicated error record * * @param index the index of the requested error entry */ /*--------------------------------------------------------------------------*/ private String buildError(int index) { Record record = errors.get(index); StringBuffer message = new StringBuffer(); // ---------------------------------------------------- // append the message text // ---------------------------------------------------- message.append(installData.langpack.getString(RESOURCE_PREFIX + "errorPrefix", new String[]{Integer.toString(index)})); if (record.message >= 0) { message.append(installData.langpack.getString(ERROR_PREFIX + Integer.toString(record.message - LogError.ERROR_BASE), record.variables)); } else { message.append(MessageFormat.format(record.template, new Object[]{record.variables})); } // ---------------------------------------------------- // append the exception // ---------------------------------------------------- if (record.exception != null) { message.append(newline); message.append(installData.langpack.getString(RESOURCE_PREFIX + "exceptionPrefix", new String[]{record.exception.toString()})); } message.append(newline); return (message.toString()); } /*--------------------------------------------------------------------------*/ /** * Builds a debug message from the information contained in the indicated debug record * * @param index the index of the requested debug entry */ /*--------------------------------------------------------------------------*/ private String buildDebug(int index) { Record record = debug.get(index); return (buildDebug(record)); } /*--------------------------------------------------------------------------*/ /** * Builds a debug message from the information contained in a specific debug record * * @param record the debug record */ /*--------------------------------------------------------------------------*/ private String buildDebug(Record record) { StringBuffer message = new StringBuffer(); // ---------------------------------------------------- // append the message text // ---------------------------------------------------- if ((record.channel == null) || (record.channel.length() == 0)) { message.append("Debug - general: "); } else { message.append("Debug - ").append(record.channel).append(": "); } message.append(MessageFormat.format(record.template, new Object[]{record.variables})); // ---------------------------------------------------- // append the exception // ---------------------------------------------------- if (record.exception != null) { message.append(newline); message.append(installData.langpack.getString(RESOURCE_PREFIX + "exceptionPrefix", new String[]{record.exception.toString()})); } message.append(newline); return (message.toString()); } /*--------------------------------------------------------------------------*/ /** * Dumps the list of debug channels to stdout that have recorded messages */ /*--------------------------------------------------------------------------*/ public void dumpRecordedChannels() { if (debugActive && dumpChannels) { System.out.println(); System.out.println("The following debug channels did record messages:"); System.out.println(); Enumeration<String> list = recordedChannels.keys(); while (list.hasMoreElements()) { System.out.println(" - " + list.nextElement().toString()); } System.out.println(); } } // -------------------------------------------------------------------------- // class definition to handle individual entries // -------------------------------------------------------------------------- private class Record { String channel; String template; int message; String[] variables; Throwable exception; // ---------------------------------------------------- // Constructors // ---------------------------------------------------- Record(int message, String[] variables) { this.message = message; this.variables = variables; } Record(String template, String[] variables) { this.message = -1; this.template = template; this.variables = variables; } Record(int message, String[] variables, Throwable exception) { this.message = message; this.variables = variables; this.exception = exception; } Record(String template, String[] variables, Throwable exception) { this.message = -1; this.template = template; this.variables = variables; this.exception = exception; } Record(String template, String[] variables, Throwable exception, String channel) { this.message = -1; this.template = template; this.variables = variables; this.exception = exception; this.channel = channel; } } } /*---------------------------------------------------------------------------*/