/*******************************************************************************
* Copyright (c) 2007 Business Objects Software Limited and others.
* All rights reserved.
* This file is made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Business Objects Software Limited - initial API and implementation
*******************************************************************************/
/*
* CALConsole.java
* Creation date: Jul 24, 2007.
* By: Edward Lam
*/
package org.openquark.cal.eclipse.ui.console;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.StreamHandler;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.debug.ui.IDebugUIConstants;
import org.eclipse.debug.ui.console.ConsoleColorProvider;
import org.eclipse.debug.ui.console.IConsoleColorProvider;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.graphics.Font;
import org.eclipse.ui.console.ConsolePlugin;
import org.eclipse.ui.console.IConsole;
import org.eclipse.ui.console.IConsoleManager;
import org.eclipse.ui.console.IOConsole;
import org.eclipse.ui.console.IOConsoleInputStream;
import org.eclipse.ui.console.IOConsoleOutputStream;
import org.openquark.cal.ConsoleRunner;
import org.openquark.cal.compiler.AdjunctSource;
import org.openquark.cal.compiler.CompilerMessage;
import org.openquark.cal.compiler.CompilerMessageLogger;
import org.openquark.cal.compiler.MessageLogger;
import org.openquark.cal.compiler.ModuleName;
import org.openquark.cal.compiler.ModuleNameResolver;
import org.openquark.cal.compiler.ScopedEntityNamingPolicy;
import org.openquark.cal.compiler.TypeExpr;
import org.openquark.cal.eclipse.core.CALModelManager;
import org.openquark.cal.eclipse.core.util.Util;
import org.openquark.cal.eclipse.ui.CALEclipseUIPlugin;
import org.openquark.cal.services.ProgramModelManager;
/**
* A console which can be used for interactive evaluation of CAL expressions.
* @author Edward Lam
*/
public class CALConsole extends IOConsole {
public final static String CONSOLE_TYPE = "calConsole"; //$NON-NLS-1$
// IDebugPreferenceConstants.CONSOLE_FONT / Eclipse 3.3: IDebugUIConstants.PREF_CONSOLE_FONT
public static final String CONSOLE_FONT= "org.eclipse.debug.ui.consoleFont";
/** The namespace for CAL log messages. */
private static final String calLoggerNamespace = "org.openquark.cal.eclipse.ui.console";
/** An instance of a Logger for cal messages. */
private Logger calLogger;
/** The ConsoleRunner instance for this console. */
private final CALConsoleRunner calConsoleRunner;
/** The name of the current module, with respect to which the evaluation will take place. */
private ModuleName currentModuleName = ModuleName.make("Cal.Core.Prelude");
/** The background job to read input from the user and take appropriate actions in response. */
private InputReadJob readJob;
/** Remembered previous commands. */
private List<String> commandHistory = new ArrayList<String>();
/** Listener for changes in the console font. */
private final IPropertyChangeListener propertyListener = new IPropertyChangeListener() {
public void propertyChange(PropertyChangeEvent event) {
String property = event.getProperty();
if (property.equals(CONSOLE_FONT)) {
setFont(JFaceResources.getFont(CONSOLE_FONT));
}
}
};
/**
* A StreamHandler which simply outputs log records to the given output stream.
* @author Edward Lam
*/
private static class OutputStreamStreamHandler extends StreamHandler {
/**
* Constructor for an OutputStreamStreamHandler
*/
public OutputStreamStreamHandler(OutputStream outputStream) {
super(outputStream, new ConsoleFormatter());
}
/** Override this to always flush the stream. */
@Override
public void publish(LogRecord record) {
super.publish(record);
flush();
}
/** Override to just flush the stream, we don't want to close System.out. */
@Override
public void close() {
flush();
}
}
/**
* A log message formatter that simply outputs the message of the log record,
* plus the text of any throwable.
* Used to print messages to the console.
*/
private static class ConsoleFormatter extends Formatter {
/**
* {@inheritDoc}
*/
@Override
public String format(LogRecord record) {
StringBuilder sb = new StringBuilder();
// Append the log message
sb.append(record.getMessage() + "\n");
// Append the throwable if there is one
if (record.getThrown() != null) {
try {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
record.getThrown().printStackTrace(pw);
pw.close();
sb.append(sw.toString());
} catch (Exception ex) {
sb.append("Failed to generate a stack trace for the throwable");
}
}
return sb.toString();
}
}
/**
* Console runner for the CAL Console.
* @author Edward Lam
*/
private class CALConsoleRunner extends ConsoleRunner {
/**
* Constructor for a CALConsoleRunner
* @param calLogger
*/
public CALConsoleRunner(Logger calLogger) {
super(calLogger);
}
/**
* {@inheritDoc}
*/
public ProgramModelManager getProgramModelManager() {
return CALConsole.this.getProgramModelManager();
}
/**
* {@inheritDoc}
*/
public boolean hasModuleSource(ModuleName moduleName) {
return CALModelManager.getCALModelManager().getInputSourceFile(moduleName) != null;
}
}
/**
* The Job which hangs around reading input from the console and responding to it with output.
* @author Edward Lam
*/
private class InputReadJob extends Job {
private final IOConsoleOutputStream outputStream;
private final IOConsoleInputStream inputStream;
private final StreamHandler consoleHandler;
/** Set to true by finishUp, indicating that this Job should exit. */
private volatile boolean shouldDie = false;
/**
* Constructor for an InputReadJob
*/
InputReadJob() {
super("Input Job for CAL"); //$NON-NLS-1$
IConsoleColorProvider colorProvider = new ConsoleColorProvider();
this.inputStream = CALConsole.this.getInputStream();
this.inputStream.setColor(colorProvider.getColor(IDebugUIConstants.ID_STANDARD_INPUT_STREAM));
this.outputStream = CALConsole.this.newOutputStream();
this.outputStream.setColor(colorProvider.getColor(IDebugUIConstants.ID_STANDARD_OUTPUT_STREAM));
consoleHandler = new OutputStreamStreamHandler(outputStream);
// Note that we can add other handlers which log more about what's happening.
calLogger.addHandler(consoleHandler);
}
/**
* Cause this job to wrap up whatever it's doing and exit.
* Called by CALConsole.dispose()
*/
protected void finishUp() {
this.shouldDie = true;
try {
calLogger.removeHandler(consoleHandler);
outputStream.close();
} catch (IOException e) {
Util.log(e, "Exception closing stream.");
}
// This is somewhat of a hack -- we know that inputStream returns from its wait after interruption
synchronized (runThreadAccessLock) {
if (runThread != null) {
runThread.interrupt();
}
}
}
private Thread runThread = null;
private final byte[] runThreadAccessLock = new byte[0]; // used to synchronize access to the runThread field.
@Override
protected IStatus run(IProgressMonitor monitor) {
synchronized (runThreadAccessLock) {
runThread = Thread.currentThread();
}
try {
outputStream.write("Enter an expression to evaluate. :h for help.\n");
displayPrompt();
byte[] b = new byte[1024];
int read = 0;
while (true) {
read = inputStream.read(b);
if (shouldDie) {
return Status.CANCEL_STATUS;
}
if (read > 0) {
String enteredText = new String(b, 0, read);
handleRequest_enteredText(enteredText);
}
displayPrompt();
}
} catch (IOException e) {
CALEclipseUIPlugin.log(e);
} finally {
synchronized (runThreadAccessLock) {
runThread = null;
}
}
// unreachable..
return Status.OK_STATUS;
}
/**
* Display the input prompt.
*/
void displayPrompt() {
try {
outputStream.write(getPrompt());
} catch (IOException e) {
CALEclipseUIPlugin.log(e);
}
}
}
/**
* Constructor for a CAL Console
*/
public CALConsole() {
super("CAL Console", CONSOLE_TYPE, null, true);
Font font = JFaceResources.getFont(CONSOLE_FONT);
setFont(font);
calLogger = Logger.getLogger(calLoggerNamespace);
calLogger.setLevel(Level.FINEST);
calLogger.setUseParentHandlers(false);
calConsoleRunner = new CALConsoleRunner(calLogger);
}
/**
* Show the CAL Console in the Console view.
* Note that this isn't thread safe as it internally iterates through the consoles in the console manager
* to find any existing CALConsole instance. This should be fine though if consoles are only show as a result of
* UI actions (which don't tend to happen concurrently).
*
* @return the CALConsole instance being shown. There will only be one of these in the Console manager.
*/
public static CALConsole showCALConsole() {
IConsoleManager consoleManager = ConsolePlugin.getDefault().getConsoleManager();
IConsole[] consoles = consoleManager.getConsoles();
CALConsole existingCALConsole = null;
for (IConsole console : consoles) {
if (console instanceof CALConsole) {
existingCALConsole = (CALConsole)console;
consoleManager.showConsoleView(console);
break;
}
}
if (existingCALConsole == null) {
CALConsoleFactory calConsoleFactory = new CALConsoleFactory();
calConsoleFactory.openConsole(); // This shows the console and adds it to the console manager.
existingCALConsole = calConsoleFactory.getConsole();
}
return existingCALConsole;
}
@Override
protected void init() {
JFaceResources.getFontRegistry().addListener(propertyListener);
readJob = new InputReadJob();
readJob.setSystem(true);
readJob.schedule();
}
@Override
protected void dispose() {
JFaceResources.getFontRegistry().removeListener(propertyListener);
readJob.finishUp();
readJob = null;
calLogger = null;
super.dispose();
}
@Override
public void clearConsole() {
// TODOEL: display prompt when clearing the console (if not running).
// TODOEL: Handle calls while something is running.
super.clearConsole();
}
/**
* Set the document's initial contents.
* Called by CALConsoleFactory.
*/
void initializeDocument() {
getDocument().set("");
}
/**
* @return the program model manager from the CALModelManager
*/
private ProgramModelManager getProgramModelManager() {
return CALModelManager.getCALModelManager().getProgramModelManager();
}
/**
* @return the name of the current module, with respect to which the evaluation will take place.
*/
private ModuleName getCurrentModuleName() {
return currentModuleName;
}
/**
* @return the string to display as the prompt
*/
private String getPrompt() {
return getCurrentModuleName() + ">";
}
/**
* Handle the given text as entered by the user.
* This may cause an expression to be run.
* @param enteredText the text which was entered
* @return whether execution terminated normally.
*/
private boolean handleRequest_enteredText(String enteredText) {
enteredText = enteredText.trim();
if (handleConsoleCommand(enteredText)) {
return true;
}
if (enteredText.length() < 1) {
return true;
}
return calConsoleRunner.runExpression(getCurrentModuleName(), enteredText, true);
}
/**
* Helper to dump the console help to the console.
*/
private void showHelp() {
calLogger.info(":h[elp] Show this help.");
calLogger.info(":sm <target_module_name> Set the module in which to evaluate.");
calLogger.info(":t <expression> Display the type of the given expression.");
calLogger.info(":rs [module name] Reset any cached CAFs in the named module, or all modules if none specified.");
calLogger.info("");
calLogger.info(":spc Show previously executed commands in a numbered list");
calLogger.info(":pc <command number> Execute a previous command indicated by the command number");
}
/**
* @param enteredText
* @return whether the entered text corresponds to a console command.
*/
private boolean handleConsoleCommand(String enteredText) {
if (!enteredText.startsWith(":")) {
commandHistory.add(enteredText);
return false;
}
String[] split = enteredText.split(" ");
List<String> tokens = new ArrayList<String>();
for (final String s : split) {
if (s.length() > 0) {
tokens.add(s);
}
}
String command = tokens.get(0).toLowerCase();
if (command.equals(":h") || command.equals(":help")) {
showHelp();
} else if (command.equals(":sm")) {
// set the module
if (checkNArgs(tokens, 1)) {
setModule(tokens.get(1));
}
commandHistory.add(enteredText);
} else if (command.equals(":t")) {
// display type
String argString = getTrimmedArgString(enteredText, ":t");
String qualifiedCodeExpression = calConsoleRunner.qualifyCodeExpression(currentModuleName, argString);
if (qualifiedCodeExpression != null) {
// pick a name which doesn't exist in the module.
String targetName = "target";
int index = 1;
while (getProgramModelManager().getModuleTypeInfo(currentModuleName).getFunctionalAgent(targetName) != null) {
targetName = "target" + index;
index++;
}
CompilerMessageLogger logger = new MessageLogger();
String scDef = targetName + " = \n" + qualifiedCodeExpression + "\n;";
TypeExpr te = getProgramModelManager().getTypeChecker().checkFunction(new AdjunctSource.FromText(scDef), currentModuleName, logger);
if (te == null) {
calLogger.info("Attempt to type expression has failed because of errors: ");
dumpCompilerMessages(logger);
} else {
//we want to display the type using fully qualified type constructor names, but also making use of
//preferred names of type are record variables
calLogger.info(" " + te.toString(true, ScopedEntityNamingPolicy.FULLY_QUALIFIED) + "\n");
}
}
commandHistory.add(enteredText);
} else if (command.equals(":rs")) {
// reset cached CAFs
if (tokens.size() == 1) {
calConsoleRunner.command_resetCachedResults(null);
} else {
String trimmedArgString = getTrimmedArgString(enteredText, ":rs");
ModuleName moduleName = resolveModuleNameInProgram(trimmedArgString);
if (moduleName != null) {
calConsoleRunner.command_resetCachedResults(moduleName);
}
}
commandHistory.add(enteredText);
} else if (command.equals(":spc")) {
// show previous commands
if (checkNArgs(tokens, 0)) {
int i = 1;
for (String commandString : commandHistory) {
calLogger.info(i + "> " + commandString);
i++;
}
}
} else if (command.equals(":pc")) {
// execute a previous command
if (checkNArgs(tokens, 1)) {
String argString = getTrimmedArgString(enteredText, ":pc");
try {
Integer n = Integer.decode(argString);
if (n.intValue() <= 0 || n.intValue() > commandHistory.size()) {
calLogger.info("\"" + argString + "\" is not a valid command number.");
} else {
handleRequest_enteredText(commandHistory.get(n.intValue() - 1));
}
} catch (NumberFormatException e) {
calLogger.info("\"" + argString + "\" is not a valid command number.");
}
}
} else {
calLogger.severe("Unknown command. :h for help.");
}
return true;
}
/**
* Helper method for handleConsoleCommand.
*
* @param tokens the tokens from the entered command
* @param nExpectedArgs the number of arguments expected for the command
* @return if the number of arguments is not the expected number (according to the size of the tokens list)
* then an error message is logged and false is returned. Otherwise true is returned.
*/
private boolean checkNArgs(List<String> tokens, int nExpectedArgs) {
if (tokens.size() != (nExpectedArgs + 1)) {
calLogger.severe("Invalid number of arguments.");
return false;
}
return true;
}
/**
* Helper method for handleConsoleCommand.
* @param enteredText the text which was entered
* @param commandString the string form of the token representing the command eg. ":h"
* @return the part of the entered text representing the arguments to the command, ie. enteredText minus the commandString.
* This will be a trimmed string.
*/
private static String getTrimmedArgString(String enteredText, String commandString) {
return enteredText.substring(enteredText.indexOf(commandString) + commandString.length()).trim();
}
/**
* Dump the messages contained in logger to the console
* @param logger A CompilerMessageLogger
*/
private void dumpCompilerMessages(CompilerMessageLogger logger) {
for (final CompilerMessage message : logger.getCompilerMessages()) {
calLogger.info(" " + message.toString());
}
}
/**
* Set the current module.
* @param moduleNameString the name of the module to be set as the current module.
* This may be unqualified or partially-qualified.
*/
private void setModule(String moduleNameString) {
// Do some work to resolve partially-qualified module names.
ModuleName resolvedModuleName = resolveModuleNameInProgram(moduleNameString);
if (resolvedModuleName != null) {
currentModuleName = resolvedModuleName;
}
}
/**
* Resolves the given module name in the context of the current program. If the given name cannot be unambiguously resolved, null is returned.
* @param moduleNameString the module name to resolve.
* @return the corresponding fully qualified module name, or null if the given name cannot be unambiguously resolved.
*/
private ModuleName resolveModuleNameInProgram(String moduleNameString) {
ModuleName[] moduleNamesInProgram = getProgramModelManager().getModuleNamesInProgram();
ModuleNameResolver moduleNameResolver = ModuleNameResolver.make(new HashSet<ModuleName>(Arrays.asList(moduleNamesInProgram)));
ModuleName moduleName = ModuleName.maybeMake(moduleNameString);
if (moduleName == null) {
calLogger.log(Level.INFO, moduleNameString + " is not a valid module.");
} else {
ModuleNameResolver.ResolutionResult resolution = moduleNameResolver.resolve(moduleName);
if (resolution.isKnownUnambiguous()) {
ModuleName resolvedModuleName = resolution.getResolvedModuleName();
// The module name can be resolved.
// Check that it is compiled.
if (getProgramModelManager().getModule(resolvedModuleName) == null) {
calLogger.log(Level.SEVERE, "Module \"" + resolvedModuleName + "\" has compilation errors (or is currently in the process of being compiled).");
} else {
return resolvedModuleName;
}
} else {
if (resolution.isAmbiguous()) {
// The partially qualified name is ambiguous, so show the potential matches
calLogger.log(Level.INFO, "The module name " + moduleName + " is ambiguous. Do you mean one of:");
ModuleName[] potentialMatches = resolution.getPotentialMatches();
for (final ModuleName element : potentialMatches) {
calLogger.log(Level.INFO, " " + element);
}
} else {
calLogger.log(Level.INFO, moduleName + " is not a valid module.");
}
}
}
return null;
}
/**
* External call to set the current module to the given module and run the given expression.
* @param moduleNameString the text of the module name.
* @param expressionText the text of the expression to run
* @return whether execution terminated normally.
*/
boolean handleRequest_setModuleAndRunExpression(String moduleNameString, String expressionText) {
// Add a newline so that subsequent output doesn't appear after the display prompt.
calLogger.info("");
setModule(moduleNameString);
boolean retVal = calConsoleRunner.runExpression(getCurrentModuleName(), expressionText, true);
// display the prompt.
readJob.displayPrompt();
return retVal;
}
/**
* External call to set the current module to the given module
* @param moduleNameString the text of the module name.
*/
void handleRequest_setModule(String moduleNameString) {
// Add a newline so that subsequent output doesn't appear after the display prompt.
calLogger.info("");
setModule(moduleNameString);
// display the prompt.
readJob.displayPrompt();
}
/**
* Terminate execution of the currently running expression if any.
*/
public void terminateExecution() {
calConsoleRunner.terminateExecution();
}
}