/* * RHQ Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.enterprise.communications.util.prefs; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.List; import java.util.prefs.Preferences; import mazz.i18n.Logger; import mazz.i18n.Msg; import org.rhq.enterprise.communications.i18n.CommI18NFactory; import org.rhq.enterprise.communications.i18n.CommI18NResourceKeys; /** * This provides a way to initially setup a set of preferences. Normally, this is used when a new installation is laid * down and the user needs to be asked for some preference values to customize the installation with the user's own * settings. This could concievably also be used to customize any set of preferences, even those that already exist or * those that have recently been {@link PreferencesUpgrade upgraded}. * * @author John Mazzitelli */ public class Setup { /** * If the user enters this string at a prompt, it means the preference should not be set and its internal default * should be assumed. */ private static final String INTERNAL_DEFAULT_PROMPT_VALUE = "!*"; /** * If the user enters this string at a prompt, it means the preference help text needs to be shown to the user. */ private static final String HELP_PROMPT_VALUE = "!?"; /** * If the user enters this string at a prompt, it means the user wants to prematurely stop the setup, but leave the * settings already setup intact. */ private static final String STOP_PROMPT_VALUE = "!+"; /** * If the user enters this string at a prompt, it means the user wants to prematurely stop the setup, and abort any * changes made, thus reverting the preferences back to their original values. */ private static final String CANCEL_PROMPT_VALUE = "!-"; /** * Logger */ private static final Logger LOG = CommI18NFactory.getLogger(Setup.class); /** * I18N messages for output to console */ private static final Msg MSG = CommI18NFactory.getMsg(); private final Preferences m_preferences; private final String m_introMessage; private final List<SetupInstruction> m_instructions; private final PromptInput m_in; private final PrintWriter m_out; /** * Creates a new {@link Setup} object. * * @param preferences the set of preferences that need to be customized * @param intro_message a human-readable string that will be {@link #getOut() output} prior to starting the * {@link #setup()} - this can be used to explain to the user what these set of preferences are * for (may be <code>null</code>) * @param instructions the set of instructions this class will use to customize the preferences * @param in the input where the answers to the setup prompts will come from * @param out the output stream where the setup prompts will be output */ public Setup(Preferences preferences, String intro_message, List<SetupInstruction> instructions, PromptInput in, PrintWriter out) { m_preferences = preferences; m_introMessage = intro_message; m_instructions = instructions; m_in = in; m_out = out; } /** * Returns the preferences that are to be customized via this setup instance. * * @return preferences */ public Preferences getPreferences() { return m_preferences; } /** * Returns the intro message that will be printed to the {@link #getOut() output stream} before the {@link #setup()} * begins processing the instructions. If this is <code>null</code>, it will be ignored. * * @return human-readable introductory message (may be <code>null</code>) */ public String getIntroMessage() { return m_introMessage; } /** * Returns the set of instructions that are used to ask the setup questions. * * @return instructions */ public List<SetupInstruction> getInstructions() { return m_instructions; } /** * Returns the input object where the answers to the setup prompts will be read from. * * @return in */ public PromptInput getIn() { return m_in; } /** * Returns the output stream where the prompts and any other output messages will be written to. * * @return out */ public PrintWriter getOut() { return m_out; } /** * Performs the setup by asking questions and setting preferences according to this object's * {@link #getInstructions() setup instructions}. * * @return <code>true</code> if the setup finished; <code>false</code> if the user canceled the setup and reverted * back to the original values * * @throws RuntimeException if failed to access the preferences backend store */ public boolean setup() { PromptInput in = getIn(); PrintWriter out = getOut(); List<SetupInstruction> all_instructions = getInstructions(); Preferences preferences = getPreferences(); ByteArrayOutputStream backup = backupPreferences(preferences); // print an introductory message to the user explaining that we are going to ask some questions if (all_instructions.size() > 0) { if (getIntroMessage() != null) { out.println(getIntroMessage()); } out.println(MSG.getMsg(CommI18NResourceKeys.SETUP_STANDARD_INTRO)); } boolean user_stop = false; // will be true if the user asked to prematurely stop the setup but keep the current values boolean user_cancel = false; // will be true if the user asked to cancel the setup and restore the original values // for each setup instruction, ask the user to define the preference value as per the instruction for (SetupInstruction instruction : all_instructions) { // give the instruction the set of preferences that are being setup and // give the instruction a chance to pre-process its internal state before we begin LOG.debug(CommI18NResourceKeys.SETUP_PREPROCESS_INSTRUCTION, instruction); instruction.setPreferences(preferences); instruction.preProcess(); // get the information needed to prompt the user for the new preference value String pref_name = instruction.getPreferenceName(); String default_value = instruction.getDefaultValue(); String prompt_message = instruction.getPromptMessage(); String help_message = instruction.getHelpMessage(); boolean no_echo = instruction.isUsingNoEchoPrompt(); String new_value = null; if (prompt_message != null) { boolean is_valid; do { new_value = prompt(prompt_message, pref_name, help_message, default_value, in, out, no_echo); is_valid = true; // assume this new value is valid unless our validity checker (if one exists) tells us otherwise if (new_value != null) { if (new_value.length() > 0) { user_stop = new_value.equals(STOP_PROMPT_VALUE); user_cancel = new_value.equals(CANCEL_PROMPT_VALUE); if ((instruction.getValidityChecker() != null) && !(user_stop || user_cancel)) { is_valid = instruction.getValidityChecker().checkValidity(pref_name, new_value, preferences, out); } } else { new_value = default_value; } } } while (!is_valid); if (!(user_stop || user_cancel)) { LOG.debug(CommI18NResourceKeys.SETUP_NEW_VALUE, pref_name, new_value); } } else { // not required to prompt - use the instruction's default - no need to validate since that value came from the instruction directly new_value = default_value; LOG.debug(CommI18NResourceKeys.SETUP_NEW_VALUE_NO_PROMPT, pref_name, new_value); } if (user_stop || user_cancel) { break; // user asked to stop so do not process any more instructions } // now that we got the new value, put it in the preferences if (new_value != null) { preferences.put(pref_name, new_value); } else { // a null means the user wants to pick up our internal defaults, so we'll just remove the preference preferences.remove(pref_name); } // give the instruction a chance to post-process the preferences in case it needs to // do some more things to the preferences after the user entered the new preference value LOG.debug(CommI18NResourceKeys.SETUP_POSTPROCESS_INSTRUCTION, instruction); instruction.postProcess(); } // print out a message so the user knows we are done asking questions and log the new set of preference values if (user_stop) { out.println(MSG.getMsg(CommI18NResourceKeys.SETUP_USER_STOP, preferences.absolutePath())); } else if (user_cancel) { out.println(MSG.getMsg(CommI18NResourceKeys.SETUP_USER_CANCEL, preferences.absolutePath())); restorePreferences(backup); } else { out.println(MSG.getMsg(CommI18NResourceKeys.SETUP_COMPLETE, preferences.absolutePath())); } if (LOG.isDebugEnabled()) { LOG.debug(CommI18NResourceKeys.SETUP_COMPLETE_WITH_DUMP, preferencesDump(preferences)); } return !user_cancel; } /** * Prints the prompt message to the output stream and waits for the new value to be input. If the input is empty * (e.g. the user just hit the return key), an empty string is returned. A help message will be shown if the user * asks for it. * * <p>Note that the <code>default_value</code> is only used to show in the prompt. Specifically, it will not be * returned if the user simply hit the ENTER key without entering a value. In that case, an empty string is * returned; the caller should detect this and set the value to the default as appropriate.</p> * * <p>The returned value will be <code>null</code> if the user wants to rely on the system internal default and not * set the preference value at all.</p> * * @param prompt_message the prompt message printed to the output stream that tells the user what is being asked * for * @param pref_name the actual preference name that is to be set to the answer of the prompt * @param help_message a more detailed help message that tells the user what is being asked for * @param default_value what the value should be if the user just hits the ENTER key * @param in where the user input is coming from * @param out the stream where the prompt messages will be written * @param no_echo if <code>true</code>, user should not see what is being typed at the prompt * * @return the new value entered by the user that came across in the input stream (may be <code>null</code>) * * @throws RuntimeException if failed to read from the input stream */ protected String prompt(String prompt_message, String pref_name, String help_message, String default_value, PromptInput in, PrintWriter out, boolean no_echo) { String full_prompt = prompt_message + " [" + ((default_value != null) ? default_value : INTERNAL_DEFAULT_PROMPT_VALUE) + "] : "; String new_value = ""; boolean keep_asking = true; while (keep_asking) { out.print(full_prompt); out.flush(); try { if (no_echo) { new_value = in.readLineNoEcho(); } else { new_value = in.readLine(); } } catch (IOException e) { // this will abort the entire setup - but there is nothing we can do about this error, so that's what we have to do throw new RuntimeException(e); } if (new_value.equals(HELP_PROMPT_VALUE)) { out.println(help_message); out.println("(" + pref_name + ")"); } else if (new_value.equals(INTERNAL_DEFAULT_PROMPT_VALUE)) { new_value = null; keep_asking = false; } else { keep_asking = false; } } return new_value; } /** * Given a set of preferences, this will dump each name/value in the returned string separated with a newline. * * @param prefs the preferences whose values are to be dumped in the given string * * @return the preference name/value pairs separated by newlines */ protected String preferencesDump(Preferences prefs) { try { StringBuffer dump = new StringBuffer(prefs.toString() + '\n'); String[] pref_keys = prefs.keys(); for (int i = 0; i < pref_keys.length; i++) { dump.append(pref_keys[i]); dump.append('='); dump.append(prefs.get(pref_keys[i], "<>")); dump.append('\n'); } return dump.toString(); } catch (Exception e) { return e.toString(); } } /** * Given a set of preferences, this will export them and return them in the given stream; in effect backing up their * values. * * @param preferences the preferences to backup * * @return the backed up preferences. * * @throws RuntimeException if failed to access the backing store */ private ByteArrayOutputStream backupPreferences(Preferences preferences) throws RuntimeException { try { ByteArrayOutputStream backup = new ByteArrayOutputStream(); preferences.exportSubtree(backup); return backup; } catch (Exception e) { // should rarely occur, but if it does, something is probably wrong with the backing store so no need to continue throw new RuntimeException(e); } } /** * Given a stream containing a {@link #backupPreferences(Preferences) backed up set of preferences}, this will * import them back in; in effect restoring their values. * * @param backup the original, backed up preferences * * @throws RuntimeException if failed to access the backing store */ private void restorePreferences(ByteArrayOutputStream backup) throws RuntimeException { try { getPreferences().clear(); Preferences.importPreferences(new ByteArrayInputStream(backup.toByteArray())); } catch (Exception e) { // should rarely occur, but if it does, something is probably wrong with the backing store so no need to continue throw new RuntimeException(e); } } }