001: /**
002: * Sequoia: Database clustering technology.
003: * Copyright (C) 2002-2004 French National Institute For Research In Computer
004: * Science And Control (INRIA).
005: * Contact: sequoia@continuent.org
006: *
007: * Licensed under the Apache License, Version 2.0 (the "License");
008: * you may not use this file except in compliance with the License.
009: * You may obtain a copy of the License at
010: *
011: * http://www.apache.org/licenses/LICENSE-2.0
012: *
013: * Unless required by applicable law or agreed to in writing, software
014: * distributed under the License is distributed on an "AS IS" BASIS,
015: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016: * See the License for the specific language governing permissions and
017: * limitations under the License.
018: *
019: * Initial developer(s): Emmanuel Cecchet.
020: * Contributor(s): Mathieu Peltier,Nicolas Modrzyk, Marc Herbert
021: */package org.continuent.sequoia.console.text;
022:
023: import java.io.IOException;
024: import java.io.InputStream;
025: import java.io.PrintWriter;
026: import java.util.ArrayList;
027: import java.util.Arrays;
028: import java.util.Iterator;
029: import java.util.List;
030: import java.util.prefs.Preferences;
031:
032: import jline.ConsoleReader;
033: import jline.History;
034:
035: import org.continuent.sequoia.common.i18n.ConsoleTranslate;
036: import org.continuent.sequoia.console.jmx.RmiJmxClient;
037: import org.continuent.sequoia.console.text.module.ControllerConsole;
038: import org.continuent.sequoia.console.text.module.VirtualDatabaseAdmin;
039: import org.continuent.sequoia.console.text.module.VirtualDatabaseConsole;
040:
041: /**
042: * This is the Sequoia controller console that allows remote administration and
043: * monitoring of any Sequoia controller.
044: *
045: * @author <a href="mailto:Emmanuel.Cecchet@inria.fr">Emmanuel Cecchet </a>
046: * @author <a href="mailto:Mathieu.Peltier@inrialpes.fr">Mathieu Peltier </a>
047: * @author <a href="mailto:Nicolas.Modrzyk@inrialpes.fr">Nicolas Modrzyk </a>
048: * @version 1.0
049: */
050: public class Console {
051: private static final Character PASSWORD_CHAR = new Character(
052: '\u0000');
053:
054: /** <code>ConsoleReader</code> allowing to reading input. */
055: private final ConsoleReader consoleReader;
056:
057: private final PrintWriter consoleOutputWriter;
058:
059: /** <code>true</code> if the console is used in interactive mode. */
060: private boolean interactive;
061:
062: private RmiJmxClient jmxClient;
063:
064: /** Virtual database administration console. */
065: private VirtualDatabaseAdmin adminModule;
066:
067: /** Virtual database console. */
068: private VirtualDatabaseConsole consoleModule;
069:
070: /** Controller Console */
071: private ControllerConsole controllerModule;
072:
073: /** Debug Mode */
074: private boolean debug;
075: private boolean silent = false;
076:
077: private boolean exitOnError;
078:
079: /**
080: * The number of history items to store in java Preferences file
081: */
082: private static final int STORED_HISTORY_SIZE = 100;
083:
084: /**
085: * <code>true</code> if colors should be displayed in interactive mode (work
086: * only on non Windows system).
087: */
088: private boolean printColor;
089:
090: private boolean sqlClientOnly;
091:
092: /**
093: * Creates a new <code>Console</code> instance.
094: *
095: * @param jmxClient to connect to the jmxServer
096: * @param in the input stream to get the command from
097: * @param interactive if set to <code>true</code> will display prompt
098: * @param debug <code>true</code> if debug mode should be activated.
099: * @param silent <code>true</code> if silent mode is activated
100: * @param exitOnError <code>true</code> if the console should exit on error
101: * in non interactive mode.
102: * @param sqlClientOnly set to <code>true</code> if the console should
103: * behave as a sql client only
104: * @throws IOException
105: */
106: public Console(RmiJmxClient jmxClient, InputStream in,
107: boolean interactive, boolean debug, boolean silent,
108: boolean exitOnError, boolean sqlClientOnly)
109: throws IOException {
110: this .consoleOutputWriter = new PrintWriter(System.out, true);
111: this .consoleReader = new ConsoleReader(in, consoleOutputWriter);
112: this .interactive = interactive;
113: this .jmxClient = jmxClient;
114: this .debug = debug;
115: this .silent = silent;
116: this .exitOnError = exitOnError;
117: this .sqlClientOnly = sqlClientOnly;
118:
119: controllerModule = new ControllerConsole(this );
120: adminModule = new VirtualDatabaseAdmin(this );
121: consoleModule = new VirtualDatabaseConsole(this );
122: setPrintColor(true);
123: consoleReader.addCompletor(controllerModule.getCompletor());
124: consoleReader.setHistory(loadHistory());
125:
126: Runtime.getRuntime().addShutdownHook(new Thread() {
127: public void run() {
128: storeHistory();
129: }
130: });
131: }
132:
133: private History loadHistory() {
134: jline.History jHistory = new jline.History();
135: try {
136: Preferences prefs = Preferences.userRoot().node(
137: this .getClass().getName());
138: String[] historyKeys = prefs.keys();
139: Arrays.sort(historyKeys, 0, historyKeys.length);
140: for (int i = 0; i < historyKeys.length; i++) {
141: String key = historyKeys[i];
142: String value = prefs.get(key, ""); //$NON-NLS-1$
143: jHistory.addToHistory(value);
144: }
145: } catch (Exception e) {
146: // unable to load prefs: do nothing
147: }
148: return jHistory;
149: }
150:
151: /**
152: * Retrieve the command history
153: *
154: * @return a List including the command history
155: */
156: public List getHistory() {
157: return consoleReader.getHistory().getHistoryList();
158: }
159:
160: /**
161: * Store the current command history
162: */
163: public void storeHistory() {
164: List history = consoleReader.getHistory().getHistoryList();
165: try {
166: Preferences prefs = Preferences.userRoot().node(
167: this .getClass().getName());
168: prefs.clear();
169: int historySize = history.size();
170: int start = Math.max(0, historySize - STORED_HISTORY_SIZE);
171: // save up to the last 100th history items only
172: // witht the stored index starting at 0
173: for (int i = start; i < historySize; i++) {
174: prefs.put(String.valueOf(i - start), (String) history
175: .get(i + start));
176: }
177: prefs.flush();
178: } catch (Exception e) {
179: // unable to store prefs: do nothing
180: }
181: }
182:
183: /**
184: * Should this console display color in interactive mode? Warning, colors only
185: * work on non Windows system.
186: *
187: * @param b <code>true</code> if the console should display color (ignored
188: * on Windows system).
189: */
190: public void setPrintColor(boolean b) {
191: String os = System.getProperty("os.name").toLowerCase();
192: boolean windows = os.indexOf("nt") > -1
193: || os.indexOf("windows") > -1;
194: if (windows)
195: printColor = false;
196: else
197: printColor = b;
198:
199: if (System.getProperty("sequoia.console.nocolor") != null)
200: printColor = false;
201: }
202:
203: /**
204: * Returns the interactive value.
205: *
206: * @return Returns the interactive.
207: */
208: public boolean isInteractive() {
209: return interactive;
210: }
211:
212: /**
213: * Test if the console should exit on error in non interactive mode.
214: *
215: * @return <code>true</code> if the console should exit on error in non
216: * interactive mode.
217: */
218: public boolean isExitOnError() {
219: return exitOnError;
220: }
221:
222: /**
223: * Main menu prompt handling.
224: */
225: public void handlePrompt() throws Exception {
226: if (sqlClientOnly)
227: sqlClientConsole();
228: else
229: controllerModule.handlePrompt();
230: }
231:
232: private void sqlClientConsole() {
233: try {
234: consoleModule.login(new String[] { "" });
235: } catch (Exception e) {
236: printError(ConsoleTranslate.get("module.command.got.error",
237: e.getMessage()), e);
238: System.exit(1);
239: }
240: consoleModule.handlePrompt();
241: }
242:
243: /**
244: * Reads a line from the console.
245: *
246: * @param prompt
247: * the prompt to display
248: * @return the trimmed line read from the console
249: * @throws ConsoleException
250: * if an error occured
251: */
252: public String readLine(String prompt) throws ConsoleException {
253: String line = "";
254: try {
255: if (interactive)
256: line = consoleReader.readLine(beautifiedPrompt(prompt));
257: else
258: // if the console is not interactive (i.e. read from a file),
259: // we also bypass jline console and we read the input stream ourselves
260: // to circumvent jline limitation wrt to utf-8 encoding on
261: // 3-bytes characters (like CKJ charsets)
262: // Ideally disabling JLine would be a distinct option (think
263: // interactive + CKJ characters).
264: // An interesting question is why do we echo but don't prompt.
265: line = readLineBypassJLine(false);
266:
267: } catch (IOException e) {
268: throw new ConsoleException(ConsoleTranslate.get(
269: "console.read.command.failed", e));
270: }
271: if (line != null)
272: line = line.trim();
273: return line;
274: }
275:
276: private String beautifiedPrompt(String barePrompt) {
277: String prompt = barePrompt + " > ";
278: if (printColor) {
279: prompt = ColorPrinter.getColoredMessage(prompt,
280: ColorPrinter.PROMPT);
281: }
282: return prompt;
283: }
284:
285: boolean lastWasCR = false;
286: List currentLine = new ArrayList();
287:
288: /**
289: * Implements SEQUOIA-887. We would like to create a BufferedReader to use its
290: * readLine() method but can't because its eager cache would steal bytes from
291: * JLine and drop them when we return, so painfully implement here our own
292: * readLine() method, tyring to be bug for bug compatible with JLine.
293: * <p>
294: * This is a quite ugly hack. Among others we cannot use any read buffering
295: * since consoleReader is exported and might be used elsewhere. At the very
296: * least we would like to encapsulate the consoleReader so we can avoid
297: * creating one in non-JLine mode. Re-open SEQUOIA-887 for such a proper fix.
298: */
299: private String readLineBypassJLine(boolean maskInput)
300: throws IOException {
301: // If JLine implements any kind of internal read buffering, we
302: // are screwed.
303: InputStream jlineInternal = consoleReader.getInput();
304:
305: /*
306: * Unfortunately we can't do this because InputStreamReader returns -1/EOF
307: * after every line!? So we have to decode bytes->characters by ourselves,
308: * see below. Because of this we will FAIL with exotic locales, see
309: * SEQUOIA-911
310: */
311: // Reader jlineInternal = new InputStreamReader(consoleReader.getInput());
312: currentLine.clear();
313:
314: int ch = jlineInternal.read();
315:
316: if (ch == -1 /* EOF */|| ch == 4 /* ASCII EOT */)
317: return null;
318:
319: /**
320: * @see java.io.BufferedReader#readLine(boolean)
321: * @see java.io.DataInputStream#readLine() and also the less elaborate JLine
322: * keybinding.properties
323: */
324: // discard any LF following a CR
325: if (lastWasCR && ch == '\n')
326: ch = jlineInternal.read();
327:
328: // EOF also counts as an end of line. Not sure this is what JLine does but
329: // it looks good.
330: while (ch != -1 && ch != '\n' && ch != '\r') {
331: currentLine.add(new Byte((byte) ch));
332: ch = jlineInternal.read();
333: }
334:
335: // SEQUOIA-911 FIXME: we may have found a '\n' or '\r' INSIDE a multibyte
336: // character. Definitely not a real newline.
337:
338: lastWasCR = (ch == '\r');
339:
340: // "cast" byte List into a primitive byte array
341: byte[] encoded = new byte[currentLine.size()];
342: Iterator it = currentLine.iterator();
343: for (int i = 0; it.hasNext(); i++)
344: encoded[i] = ((Byte) it.next()).byteValue();
345:
346: /**
347: * This String ctor is using the "default" java.nio.Charset encoding which
348: * is locale-dependent; a Good Thing.
349: */
350: String line = new String(encoded);
351:
352: if (maskInput)
353: consoleOutputWriter.println();
354: else
355: consoleOutputWriter.println(line);
356:
357: return line;
358: }
359:
360: /**
361: * Read password from the console.
362: *
363: * @param prompt the promp to display
364: * @return the password read from the console
365: * @throws ConsoleException if an error occured
366: */
367: public String readPassword(String prompt) throws ConsoleException {
368: try {
369: if (interactive)
370: return consoleReader.readLine(beautifiedPrompt(prompt),
371: PASSWORD_CHAR);
372:
373: return readLineBypassJLine(true);
374: } catch (IOException e) {
375: throw new ConsoleException(ConsoleTranslate.get(
376: "console.read.password.failed", e));
377: }
378: }
379:
380: /**
381: * Prints a String on the console. Use this method to print <em>things</em>
382: * returned by Sequoia controller.
383: *
384: * @param s the String to print
385: */
386: public void print(String s) {
387: System.out.print(s);
388: }
389:
390: /**
391: * Prints a String on the console.<br />
392: * Use this method to print <em>things</em> returned by Sequoia controller.
393: *
394: * @param s the String to print
395: */
396: public void println(String s) {
397: System.out.println(s);
398: }
399:
400: /**
401: * Print in color
402: *
403: * @param s the message to display
404: * @param color the color to use
405: */
406: private void println(String s, int color) {
407: if (printColor)
408: ColorPrinter.printMessage(s, System.out, color);
409: else
410: System.out.println(s);
411: }
412:
413: /**
414: * Prints a new line.
415: */
416: public void println() {
417: System.out.println();
418: }
419:
420: /**
421: * Prints an error message.<br />
422: * Use this method to print <em>error</em> messages coming either from
423: * Sequoia controller or from the console.
424: *
425: * @param message error message to print
426: */
427: public void printError(String message) {
428: if (printColor)
429: ColorPrinter.printMessage(message, System.err,
430: ColorPrinter.ERROR);
431: else
432: System.err.println(message);
433: }
434:
435: /**
436: * Prints an info message.<br />
437: * Use this method to print <em>info</em> messages coming from the console.
438: * An info message should not contain essential information that the user
439: * can't deduce from the commands he/she typed.
440: *
441: * @param message informational message to print
442: */
443: public void printInfo(String message) {
444: if (!silent) {
445: println(message, ColorPrinter.INFO);
446: }
447: }
448:
449: /**
450: * Prints an error message (and displays the stack trace of an Exception if
451: * the debug option is active).<br />
452: * Use this method to print <em>error</em> messages coming either from
453: * Sequoia controller or from the console.
454: *
455: * @param message error message to print
456: * @param e an exception
457: */
458: public void printError(String message, Exception e) {
459: if (debug)
460: e.printStackTrace();
461: printError(message);
462: }
463:
464: /**
465: * Returns the jmxClient value.
466: *
467: * @return Returns the jmxClient.
468: */
469: public RmiJmxClient getJmxClient() {
470: return jmxClient;
471: }
472:
473: /**
474: * Sets a new JmxClient (used when console started without being connected).
475: *
476: * @param jmxClient the new JMX client to use
477: */
478: public void setJmxClient(RmiJmxClient jmxClient) {
479: this .jmxClient = jmxClient;
480: }
481:
482: /**
483: * Returns the adminModule value.
484: *
485: * @return Returns the adminModule.
486: */
487: public VirtualDatabaseAdmin getAdminModule() {
488: return adminModule;
489: }
490:
491: /**
492: * Returns the consoleModule value.
493: *
494: * @return Returns the consoleModule.
495: */
496: public VirtualDatabaseConsole getConsoleModule() {
497: return consoleModule;
498: }
499:
500: /**
501: * Returns the controllerModule value.
502: *
503: * @return Returns the controllerModule.
504: */
505: public ControllerConsole getControllerModule() {
506: return controllerModule;
507: }
508:
509: /**
510: * Returns the consoleReader value.
511: *
512: * @return Returns the consoleReader.
513: */
514: public final ConsoleReader getConsoleReader() {
515: return consoleReader;
516: }
517:
518: /**
519: * (ugly!) pass-through setter to set the request delimiter on the
520: * VirtualDatabaseConsole from the command line options of the console
521: *
522: * @param delimiter the String representing the request delimiter
523: * @see VirtualDatabaseConsole#setRequestDelimiter(String)
524: */
525: public void setRequestDelimiter(String delimiter) {
526: consoleModule.setRequestDelimiter(delimiter);
527: }
528:
529: /**
530: * (ugly!) pass-through setter to enable/disabled multiline statement on the
531: * VirtualDatabaseConsole from the command line options of the console
532: *
533: * @param multilineStatementEnabled <code>true</code> if multiline stamement
534: * is enabled, <code>false</code> else
535: * @see VirtualDatabaseConsole#setRequestDelimiter(String)
536: */
537: public void enableMultilineStatements(
538: boolean multilineStatementEnabled) {
539: consoleModule
540: .enableMultilineStatement(multilineStatementEnabled);
541: }
542: }
|