From 1107aa0763e3d7554408c401d2a1dbed11a94c51 Mon Sep 17 00:00:00 2001 From: Marko Živković Date: Wed, 11 Jun 2014 10:10:33 +0000 Subject: Add initial directories and files git-svn-id: https://svn.code.sf.net/p/xlogo4schools/svn/trunk@1 3b0d7934-f7ef-4143-9606-b51f2e2281fd --- logo/src/xlogo/kernel/userspace/ErrorManager.java | 201 ++++++ .../kernel/userspace/GlobalVariableTable.java | 73 ++ .../kernel/userspace/ProcedureErrorMessage.java | 106 +++ .../xlogo/kernel/userspace/PropertyListTable.java | 165 +++++ logo/src/xlogo/kernel/userspace/UserSpace.java | 790 +++++++++++++++++++++ .../kernel/userspace/context/ContextManager.java | 301 ++++++++ .../kernel/userspace/context/ContextSwitcher.java | 43 ++ .../kernel/userspace/context/LogoContext.java | 312 ++++++++ .../kernel/userspace/context/NetworkContext.java | 206 ++++++ .../kernel/userspace/context/RecordContext.java | 146 ++++ .../kernel/userspace/context/UserContext.java | 191 +++++ .../src/xlogo/kernel/userspace/files/LogoFile.java | 755 ++++++++++++++++++++ .../kernel/userspace/files/LogoFileContainer.java | 39 + .../kernel/userspace/files/LogoFilesManager.java | 560 +++++++++++++++ .../xlogo/kernel/userspace/files/RecordFile.java | 234 ++++++ .../userspace/procedures/ExecutablesContainer.java | 59 ++ .../userspace/procedures/ExecutablesProvider.java | 36 + .../kernel/userspace/procedures/Procedure.java | 763 ++++++++++++++++++++ .../userspace/procedures/ProcedureErrorType.java | 80 +++ .../userspace/procedures/ProceduresManager.java | 557 +++++++++++++++ 20 files changed, 5617 insertions(+) create mode 100644 logo/src/xlogo/kernel/userspace/ErrorManager.java create mode 100644 logo/src/xlogo/kernel/userspace/GlobalVariableTable.java create mode 100644 logo/src/xlogo/kernel/userspace/ProcedureErrorMessage.java create mode 100644 logo/src/xlogo/kernel/userspace/PropertyListTable.java create mode 100644 logo/src/xlogo/kernel/userspace/UserSpace.java create mode 100644 logo/src/xlogo/kernel/userspace/context/ContextManager.java create mode 100644 logo/src/xlogo/kernel/userspace/context/ContextSwitcher.java create mode 100644 logo/src/xlogo/kernel/userspace/context/LogoContext.java create mode 100644 logo/src/xlogo/kernel/userspace/context/NetworkContext.java create mode 100644 logo/src/xlogo/kernel/userspace/context/RecordContext.java create mode 100644 logo/src/xlogo/kernel/userspace/context/UserContext.java create mode 100644 logo/src/xlogo/kernel/userspace/files/LogoFile.java create mode 100644 logo/src/xlogo/kernel/userspace/files/LogoFileContainer.java create mode 100644 logo/src/xlogo/kernel/userspace/files/LogoFilesManager.java create mode 100644 logo/src/xlogo/kernel/userspace/files/RecordFile.java create mode 100644 logo/src/xlogo/kernel/userspace/procedures/ExecutablesContainer.java create mode 100644 logo/src/xlogo/kernel/userspace/procedures/ExecutablesProvider.java create mode 100644 logo/src/xlogo/kernel/userspace/procedures/Procedure.java create mode 100644 logo/src/xlogo/kernel/userspace/procedures/ProcedureErrorType.java create mode 100644 logo/src/xlogo/kernel/userspace/procedures/ProceduresManager.java (limited to 'logo/src/xlogo/kernel/userspace') diff --git a/logo/src/xlogo/kernel/userspace/ErrorManager.java b/logo/src/xlogo/kernel/userspace/ErrorManager.java new file mode 100644 index 0000000..f1b18ec --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/ErrorManager.java @@ -0,0 +1,201 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +import xlogo.interfaces.ErrorDetector.AmbiguityDetector.AmbiguityListener; +import xlogo.interfaces.ErrorDetector.FileErrorCollector; +import xlogo.kernel.userspace.procedures.Procedure; + +public class ErrorManager implements FileErrorCollector +{ + private final FileErrorCollector fileErrorDetector; + private final AmbiguityDetector ambiguityDetector; + + private final HashMap errorFiles = new HashMap(); + private final HashMap> ambiguousProcToFiles = new HashMap>(); + + private final ArrayList errorListeners = new ArrayList(); + + public ErrorManager(FileErrorCollector fileErrorDetector, AmbiguityDetector ambiguityDetector) + { + this.fileErrorDetector = fileErrorDetector; + this.ambiguityDetector = ambiguityDetector; + + initListeners(); + } + + private void initListeners() + { + fileErrorDetector.addErrorListener(new ErrorListener(){ + + @Override + public void errorsDetected(String fileName) + { + errorFiles.put(fileName, null); + notifyErrorDetected(fileName); + } + + @Override + public void allErrorsCorrected(String fileName) + { + errorFiles.remove(fileName); + + for (Collection fileNames : ambiguousProcToFiles.values()) + { + if (fileNames.contains(fileName)) + return; + } + // no more errors or ambiguities found + notifyAllErrorsCorrected(fileName); + } + }); + + ambiguityDetector.addAmbiguityListener(new AmbiguityListener(){ + + @Override + public void ambiguityResolved(String procedureName, String fileName) + { + Collection ambigFiles = ambiguousProcToFiles.get(procedureName); + + ambigFiles.remove(fileName); + + if (ambigFiles.size() == 0) + ambiguousProcToFiles.remove(procedureName); + + // [this check is not necessary. if it was ambiguous, it did not have errors.] + //if (errorFiles.containsKey(fileName)) + // return; + // No more file errors + + for (Collection fileNames : ambiguousProcToFiles.values()) + { + if (fileNames.contains(fileName)) + return; + } + // No more ambiguities for file + + notifyAllErrorsCorrected(fileName); + } + + @Override + public void ambiguityDetected(String procedureName, Map fileToProcedure) + { + ambiguousProcToFiles.put(procedureName, new ArrayList(fileToProcedure.keySet())); + for (String fileName : fileToProcedure.keySet()) + notifyErrorDetected(fileName); + } + }); + } + + @Override + public boolean hasErrors() + { + return errorFiles.size() > 0 || ambiguousProcToFiles.size() > 0; + } + + @Override + public boolean hasErrors(String fileName) + { + if (errorFiles.containsKey(fileName)) + return true; + + for (Collection conflictingFiles : ambiguousProcToFiles.values()) + if (conflictingFiles.contains(fileName)) + return true; + return false; + } + + @Override + public Collection getAllErrors() + { + ArrayList allErrorMessages = new ArrayList(); + // Not the most efficient impl possible + allErrorMessages.addAll(fileErrorDetector.getAllErrors()); + allErrorMessages.addAll(ambiguityDetector.getAllErrors()); + return allErrorMessages; + } + + @Override + public Collection getAllErroneousFiles() + { + TreeMap allErrorFiles = new TreeMap(); + + for(String fileName : errorFiles.keySet()) + allErrorFiles.put(fileName, null); + + for(Collection files : ambiguousProcToFiles.values()) + for(String fileName : files) + allErrorFiles.put(fileName, null); + + return allErrorFiles.keySet(); + } + + public Collection getErrorMessages(String fileName) + { + ArrayList messages = new ArrayList(); + + for(ProcedureErrorMessage pem : getAllErrors()) + if(pem.fileNames.contains(fileName)) + messages.add(pem.toString()); + + return messages; + } + + // These error event will directly update the gui => run on event dispatcher thread + + @Override + public void addErrorListener(ErrorListener listener) + { + errorListeners.add(listener); + } + + @Override + public void removeErrorListener(ErrorListener listener) + { + errorListeners.remove(listener); + } + + private void notifyErrorDetected(final String fileName) + { + for (ErrorListener listener : errorListeners) + listener.errorsDetected(fileName); + } + + private void notifyAllErrorsCorrected(final String fileName) + { + for (ErrorListener listener : errorListeners) + listener.allErrorsCorrected(fileName); + } + +} diff --git a/logo/src/xlogo/kernel/userspace/GlobalVariableTable.java b/logo/src/xlogo/kernel/userspace/GlobalVariableTable.java new file mode 100644 index 0000000..7d74560 --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/GlobalVariableTable.java @@ -0,0 +1,73 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace; + +import java.util.HashMap; +import java.util.Set; + +public class GlobalVariableTable +{ + /** + * All defined variables with their current value. + */ + protected HashMap globale; + + public GlobalVariableTable() + { + globale = new HashMap(); + } + + public Set getVariables() + { + return globale.keySet(); + } + + public String getValue(String var) + { + return globale.get(var.toLowerCase()); + } + + public void define(String var, String value) + { + globale.put(var.toLowerCase(), value); + } + + public void deleteVariable(String st) + { + globale.remove(st.toLowerCase()); + } + + /** + * Delete all Variables from the workspace + */ + public void deleteAllVariables() + { + globale.clear(); + } + +} diff --git a/logo/src/xlogo/kernel/userspace/ProcedureErrorMessage.java b/logo/src/xlogo/kernel/userspace/ProcedureErrorMessage.java new file mode 100644 index 0000000..2e2afc8 --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/ProcedureErrorMessage.java @@ -0,0 +1,106 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace; + +import java.util.ArrayList; +import java.util.Collection; + +import xlogo.Logo; +import xlogo.kernel.userspace.procedures.ProcedureErrorType; + +/** + * This is used to report document structure errors + * or ambiguity errors, either within or among files. + * @author Marko Zivkovic + * + */ +public class ProcedureErrorMessage +{ + ProcedureErrorType type; + String procedureDescription; + Collection fileNames; + + /** + * The description may be either the name (if known) or the line, where the error was found. + * @param type + * @param procedureDescription + * @param fileName + */ + public ProcedureErrorMessage(ProcedureErrorType type, String procedureDescription, String fileName) + { + this.type = type; + this.procedureDescription = procedureDescription; + this.fileNames = new ArrayList(); + this.fileNames.add(fileName); + } + + /** + * This can be used for ambiguity messages + * @see #ProcedureErrorMessage(ProcedureErrorType, String, String) + */ + public ProcedureErrorMessage(ProcedureErrorType type, String procedureDescription, Collection fileNames) + { + this.type = type; + this.procedureDescription = procedureDescription; + this.fileNames = fileNames; + } + + public ProcedureErrorType getErrorType() + { + return type; + } + + public String getProcedureDescription() + { + return procedureDescription; + } + + public Collection getFileNames() + { + return fileNames; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + for (String fileName : fileNames) + { + sb.append(fileName); + sb.append(", "); + } + sb.delete(sb.length()-2, sb.length()-1); + + sb.append(procedureDescription); + sb.append(": "); + sb.append(Logo.messages.getString(type.getDescription())); + + return sb.toString(); + } + +} \ No newline at end of file diff --git a/logo/src/xlogo/kernel/userspace/PropertyListTable.java b/logo/src/xlogo/kernel/userspace/PropertyListTable.java new file mode 100644 index 0000000..7764d60 --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/PropertyListTable.java @@ -0,0 +1,165 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace; + +import java.util.HashMap; +import java.util.Set; + +public class PropertyListTable +{ + + /** + * For all Property Lists + */ + private HashMap> propList; + + public PropertyListTable() + { + propList = new HashMap>(); + } + + + /** + * This method adds in the property List called "name" a value for the + * corresponding key + * + * @param name + * The property List 's name + * @param key + * The key for the value to add + * @param value + * The value to add + */ + public void addPropList(String name, String key, String value) + { + if (!propList.containsKey(name)) + { + propList.put(name, new HashMap()); + } + propList.get(name).put(key, value); + } + + /** + * This method removes a Property List + * + * @param name + * The property List 's name + */ + public void removePropList(String name) + { + if (propList.containsKey(name)) + { + propList.remove(name); + } + } + + /** + * This method removes a couple (key, value) from a Property List + * + * @param name + * The property List 's name + * @param key + * The key to delete + */ + public void removePropList(String name, String key) + { + if (propList.containsKey(name)) + { + if (propList.get(name).containsKey(key)) + propList.get(name).remove(key); + if (propList.get(name).isEmpty()) + propList.remove(name); + } + } + + /** + * This method returns a list that contains all couple key value + * + * @param name + * The Property List's name + * @return A list with all keys-values + */ + public String displayPropList(String name) + { + if (propList.containsKey(name)) + { + StringBuffer sb = new StringBuffer(); + sb.append("[ "); + Set set = propList.get(name).keySet(); + for (String key : set) + { + sb.append(key); + sb.append(" "); + String value = propList.get(name).get(key); + if (value.startsWith("\"")) + value = value.substring(1); + sb.append(value); + sb.append(" "); + } + sb.append("] "); + return sb.toString(); + } + else + return "[ ] "; + } + + /** + * This method return a value from a Property List + * + * @param name + * The Property List's name + * @param key + * The key for the chosen value + * @return The value for this key + */ + public String getPropList(String name, String key) + { + if (!propList.containsKey(name)) { return "[ ]"; } + if (!propList.get(name).containsKey(key)) + return "[ ]"; + return propList.get(name).get(key); + } + + /** + * Returns all defined Property List names + * + * @return A list with all Property List names + */ + public Set getPropListKeys() + { + return propList.keySet(); + } + + /** + * Delete all property Lists from the workspace + */ + public void deleteAllPropertyLists() + { + propList.clear(); + } +} diff --git a/logo/src/xlogo/kernel/userspace/UserSpace.java b/logo/src/xlogo/kernel/userspace/UserSpace.java new file mode 100644 index 0000000..d3b79d6 --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/UserSpace.java @@ -0,0 +1,790 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace; + +import java.util.ArrayList; +import java.util.Collection; + +import xlogo.Logo; +import xlogo.interfaces.BroadcasterErrorFileContainer; +import xlogo.interfaces.X4SModeSwitcher; +import xlogo.interfaces.ErrorDetector.FileErrorCollector; +import xlogo.kernel.gui.GuiMap; +import xlogo.kernel.userspace.context.ContextManager; +import xlogo.kernel.userspace.context.LogoContext; +import xlogo.kernel.userspace.files.LogoFileContainer; +import xlogo.kernel.userspace.files.LogoFilesManager; +import xlogo.kernel.userspace.procedures.ExecutablesProvider; +import xlogo.kernel.userspace.procedures.Procedure; +import xlogo.kernel.userspace.procedures.ProceduresManager; +import xlogo.messages.MessageKeys; +import xlogo.messages.async.dialog.DialogMessenger; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * This is a facade for what was before called Workspace in XLogo, and much more. + * XLogo's Workspace had: + *

+ * - All Defined Procedures
+ * - All Global variables
+ * - All GUI Objects (generated by a Logo program)
+ * - All Property lists + *

+ * The new "Workspace" does not resemble the old workspace at all, although it still provides access to these symbol tables. + *

+ * New in XLogo4Schools: multiple Files. Procedures must be unique within all files of a context. + * If multiple procedures with the same name are defined
+ * - within one file : The file will not be usable, until errors and ambiguities are fixed (like in XLogo),
+ * - among multiple files : All of these files will not be executable,
//TODO or the ambiguous procedures will not be executed + * unless the ambiguity is resolved. + *

+ * New are also the Contexts for user mode, contest mode, network mode. + * The contest mode is completely new. In XLogo, Network mode was implemented by replacing the whole workspace. + * Now only the Contexts with symbol tables are replaced. Why? Because we have an event driven architecture now. + * If the whole workspace (referring to XLogo's workspace, including event dispatchers) would be replaced, then all subscribers to the workspace would have to be re-mapped. + * By keeping the LogoFileManager and the ProceduresManager, subscribers to the user space (which are redirected to the managers) must not be re-mapped when a context switch happens. + *

+ * (Option for future: Procedures can be qualified with + * {@code fileName.procedureName}. But : A dot is not a legal character for a + * name anymore) + * + * @author Marko Zivkovic + * + */ +public class UserSpace implements X4SModeSwitcher, LogoFileContainer, BroadcasterErrorFileContainer, + ExecutablesProvider, FileErrorCollector +{ + + /** + * This is included for every call that is delegated. This way all errors are caught and if we're lucky, + * The application can still be used or terminated normally. At least the user has discovered a new bug and he or she + * can report it. + * + * TODO write a log file? + * + * @param e + */ + public static void showErrorDialog(Exception e) + { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + + DialogMessenger.getInstance().dispatchError(Logo.messages.getString(MessageKeys.WS_ERROR_TITLE), + sw.toString()); + } + + private final ContextManager contextManager; // Switches context when modes change, notifies the other managers. & causes mode and clock events + private final LogoFilesManager filesManager; // handles file specific requests & causes file events + private final ProceduresManager proceduresManager; // handles procedure specific requests & causes defined/undefined events + private final ErrorManager errorManager; // handles error specific requests & causes error events + + public UserSpace() + { + contextManager = new ContextManager(); + proceduresManager = new ProceduresManager(contextManager); + filesManager = new LogoFilesManager(contextManager); + errorManager = new ErrorManager(filesManager, proceduresManager); + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * X4S MODE SWITCHER + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + @Override + public String getSerializedContext() + { + try + { + return contextManager.getSerializedContext(); + } + catch (Exception e) { showErrorDialog(e); } + return ""; + } + + @Override + public boolean isNetworkMode() + { + try + { + return contextManager.isNetworkMode(); + } + catch (Exception e) { showErrorDialog(e); } + return false; + } + + @Override + public boolean isRecordMode() + { + try + { + return contextManager.isRecordMode(); + } + catch (Exception e) { showErrorDialog(e); } + return false; + } + + @Override + public boolean isUserMode() + { + try + { + return contextManager.isUserMode(); + } + catch (Exception e) { showErrorDialog(e); } + return true; + } + + @Override + public void startRecordMode(String[] fileNames) throws IllegalArgumentException, IOException + { + try + { + contextManager.startRecordMode(fileNames); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void stopRecordMode() + { + try + { + contextManager.stopRecordMode(); + } + catch (Exception e) { showErrorDialog(e); } + } + + public void pushNetworkMode(String networkContext) + { + try + { + contextManager.pushNetworkMode(networkContext); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void popNetworkMode() + { + try + { + contextManager.popNetworkMode(); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void addModeChangedListener(ModeChangeListener listener) + { + try + { + contextManager.addModeChangedListener(listener); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void removeModeChangedListener(ModeChangeListener listener) + { + try + { + contextManager.removeModeChangedListener(listener); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void addBroadcastListener(MessageListener listener) + { + try + { + contextManager.addBroadcastListener(listener); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void removeBroadcastListener(MessageListener listener) + { + try + { + contextManager.removeBroadcastListener(listener); + } + catch (Exception e) { showErrorDialog(e); } + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * LOGO FILE CONTAINER + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + @Override + public String[] getFileNames() + { + try + { + return filesManager.getFileNames(); + } + catch (Exception e) { showErrorDialog(e); } + return new String[0]; + } + + @Override + public String readFile(String name) + { + try + { + return filesManager.readFile(name); + } + catch (Exception e) { showErrorDialog(e); } + return ""; + } + + @Override + public String getOpenFileName() + { + try + { + return filesManager.getOpenFileName(); + } + catch (Exception e) { showErrorDialog(e); } + return null; + } + + @Override + public boolean existsFile(String name) + { + try + { + return filesManager.existsFile(name); + } + catch (Exception e) { showErrorDialog(e); } + return false; + } + + @Override + public void createFile(String fileName) throws IOException + { + try + { + filesManager.createFile(fileName); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void removeFile(String fileName) + { + try + { + filesManager.removeFile(fileName); + } + catch (Exception e) { showErrorDialog(e); } + } + + /** + * The file is expected to exist on the file system and to have a .lgo extension + * @param filePath + * @throws IOException + */ + public void importFile(File filePath) throws IOException + { + try + { + filesManager.importFile(filePath); + } + catch (Exception e) { showErrorDialog(e); } + } + + /** + * @throws IOException + * @see {@link LogoFilesManager#exportFile(String, File)} + */ + public void exportFile(String fileName, File dest) throws IOException + { + try + { + filesManager.exportFile(fileName, dest); + } + catch (Exception e) { showErrorDialog(e); } + } + + /** + * @throws IOException + * @see {@link LogoFilesManager#exportFile(String, File, String)} + */ + public void exportFile(String fileName, File location, String targetName) throws IOException + { + try + { + filesManager.exportFile(fileName, location, targetName); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public String makeUniqueFileName(String base) + { + try + { + return filesManager.makeUniqueFileName(base); + } + catch (Exception e) { showErrorDialog(e); } + return null; + } + + @Override + public void renameFile(String oldName, String newName) + { + try + { + filesManager.renameFile(oldName, newName); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void openFile(String fileName) + { + try + { + filesManager.openFile(fileName); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void closeFile(String fileName) + { + try + { + filesManager.closeFile(fileName); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void writeFileText(String fileName, String content) throws IOException + { + try + { + filesManager.writeFileText(fileName, content); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void storeFile(String fileName) throws IOException + { + try + { + filesManager.storeFile(fileName); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void addFileListener(FileContainerChangeListener listener) + { + try + { + filesManager.addFileListener(listener); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void removeFileListener(FileContainerChangeListener listener) + { + try + { + filesManager.removeFileListener(listener); + } + catch (Exception e) { showErrorDialog(e); } + } + + public String getLastEditedFileName() + { + try + { + return filesManager.getLastEditedFileName(); + } + catch (Exception e) { showErrorDialog(e); } + return null; + } + + public boolean isFilesListEditable() + { + try + { + return filesManager.isFilesListEditable(); + } + catch (Exception e) { showErrorDialog(e); } + return true; + } + + public boolean hasErrors(String fileName) + { + try + { + return filesManager.hasErrors(fileName) || proceduresManager.hasAmbiguousProcedures(fileName); + } + catch (Exception e) { showErrorDialog(e); } + return false; + } + + @Override + public void editAll() + { + try + { + filesManager.editAll(); + } + catch (Exception e) { showErrorDialog(e); } + } + + /** + * Resets the UserSpace by deleting all files (and procedures), all + * variables and all property lists. + *

+ * Note : deleting all files will cause a chain of events. + */ + public void eraseAll() + { + try + { + filesManager.eraseAll(); // should remove all files and procedures and fire the right events + LogoContext context = contextManager.getContext(); + context.getGlobals().deleteAllVariables(); + context.getPropertyLists().deleteAllPropertyLists(); + context.getGuiMap().clear(); + } + catch (Exception e) { showErrorDialog(e); } + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * PROCEDURE CONTAINER + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + /** + * This is the implementation of the Logo Command define. + * Its effect has changed since we have multiple files now, but the semantics are preserved. + * Logo programs will behave exactly as before.

+ * If the procedure is ambiguous, cannot decide which one to redefine => IllegalArgumentException
+ * If the procedure is already defined once, that definition will be redefined in its original LogoFile.
+ * If the procedure is not yet defined, a new File will be created "Generated Procedures" if does not yet exist, + * and the new procedure will be put there. + * + * @throws IllegalArgumentException if the procedure's name is ambiguous in the current context: I Don't know which one to redefine. + * @throws IllegalArgumentException if the procedure is not executable or it's owner does not exist in this context. + * @throws IOException - if the file "gerated procedure" could not be created in the context. + */ + @Override + public void defineProcedure(Procedure procedure) throws IOException + { + try + { + String procedureName = procedure.getName(); + if (proceduresManager.isProcedureAmbiguous(procedureName)) + throw new IllegalArgumentException("Attempt to redefine ambiguous procedure."); + + if (!isExecutable(procedureName)) + { + String fileName = Logo.messages.getString("ws.generated.procedure"); + if (!existsFile(fileName)) + createFile(fileName); + } + + proceduresManager.defineProcedure(procedure); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public Collection getExecutables() + { + try + { + return proceduresManager.getExecutables(); + } + catch (Exception e) { showErrorDialog(e); } + return new ArrayList(); + } + + @Override + public Procedure getExecutable(String procedureName) + { + try + { + return proceduresManager.getExecutable(procedureName); + } + catch (Exception e) { showErrorDialog(e); } + return null; + } + + @Override + public boolean isExecutable(String procedureName) + { + try + { + return proceduresManager.isExecutable(procedureName); + } + catch (Exception e) { showErrorDialog(e); } + return false; + } + + @Override + public void eraseProcedure(String procedureName) + { + try + { + proceduresManager.eraseProcedure(procedureName); + } + catch (Exception e) { showErrorDialog(e); } + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * The following symbol tables do not need a manager and no events + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + public GuiMap getGuiMap() + { + try + { + return contextManager.getContext().getGuiMap(); + } + catch (Exception e) { showErrorDialog(e); } + return new GuiMap(); + } + + public PropertyListTable getPropertyLists() + { + try + { + return contextManager.getContext().getPropertyLists(); + } + catch (Exception e) { showErrorDialog(e); } + return new PropertyListTable(); + } + + public GlobalVariableTable getGlobals() + { + try + { + return contextManager.getContext().getGlobals(); + } + catch (Exception e) { showErrorDialog(e); } + return new GlobalVariableTable(); + } + + @Override + public Collection getAllProcedureNames() + { + try + { + return proceduresManager.getAllProcedureNames(); + } + catch (Exception e) { showErrorDialog(e); } + return new ArrayList(); + } + + @Override + public Collection getAllProcedureNames(String fileName) + { + try + { + return proceduresManager.getAllProcedureNames(fileName); + } + catch (Exception e) { showErrorDialog(e); } + return new ArrayList(); + } + + @Override + public String getProcedureOwner(String procedureName) + { + try + { + return proceduresManager.getProcedureOwner(procedureName); + } + catch (Exception e) { showErrorDialog(e); } + return null; + } + + @Override + public void addProcedureMapListener(ProcedureMapListener listener) + { + try + { + proceduresManager.addProcedureMapListener(listener); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void removeProcedureMapListener(ProcedureMapListener listener) + { + try + { + proceduresManager.removeProcedureMapListener(listener); + } + catch (Exception e) { showErrorDialog(e); } + } + + /* + * Errors & Ambiguities + */ + + /** + * conflicting w.r.t. ambiguity of their procedures + */ + @Override + public Collection getAllConflictingFiles() + { + try + { + return proceduresManager.getAllConflictingFiles(); + } + catch (Exception e) { showErrorDialog(e); } + return new ArrayList(); + } + + @Override + public boolean hasAmbiguousProcedures(String fileName) + { + try + { + return proceduresManager.hasAmbiguousProcedures(fileName); + } + catch (Exception e) { showErrorDialog(e); } + return false; + } + + @Override + public boolean isProcedureAmbiguous(String procedureName) + { + try + { + return proceduresManager.isProcedureAmbiguous(procedureName); + } + catch (Exception e) { showErrorDialog(e); } + return false; + } + + @Override + public void addAmbiguityListener(AmbiguityListener listener) + { + try + { + proceduresManager.addAmbiguityListener(listener); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void removeAmbiguityListener(AmbiguityListener listener) + { + try + { + proceduresManager.removeAmbiguityListener(listener); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public boolean hasErrors() + { + try + { + return errorManager.hasErrors(); + } + catch (Exception e) { showErrorDialog(e); } + return false; + } + + @Override + public Collection getAllErrors() + { + try + { + return errorManager.getAllErrors(); + } + catch (Exception e) { showErrorDialog(e); } + return new ArrayList(); + } + + @Override + public Collection getAllErroneousFiles() + { + try + { + return errorManager.getAllErroneousFiles(); + } + catch (Exception e) { showErrorDialog(e); } + return new ArrayList(); + + } + + @Override + public void addErrorListener(ErrorListener listener) + { + try + { + errorManager.addErrorListener(listener); + } + catch (Exception e) { showErrorDialog(e); } + } + + @Override + public void removeErrorListener(ErrorListener listener) + { + try + { + errorManager.removeErrorListener(listener); + } + catch (Exception e) { showErrorDialog(e); } + } + + public Collection getErrorMessages(String fileName) + { + try + { + return errorManager.getErrorMessages(fileName); + } + catch (Exception e) { showErrorDialog(e); } + return new ArrayList(); + } + +} diff --git a/logo/src/xlogo/kernel/userspace/context/ContextManager.java b/logo/src/xlogo/kernel/userspace/context/ContextManager.java new file mode 100644 index 0000000..fe5dab7 --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/context/ContextManager.java @@ -0,0 +1,301 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace.context; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Stack; + +import xlogo.interfaces.MessageBroadcaster; +import xlogo.interfaces.X4SModeSwitcher; + +/** + * One of the four main roles in the {@link xlogo.kernel.userspace.UserSpace}
+ * The Contexts and Priorities and multiplicity + * 1. UserContext [1] : default context, lowest priority, cannot be killed. + * 2. RecordContext [0..1] : medium priority, killing it will kill all networking contexts too. + * 3. NetworkContext [0..*] : highest priority, can be stacked. + * + * @author Marko + */ +public class ContextManager implements X4SModeSwitcher, ContextSwitcher, MessageBroadcaster +{ + private LogoContext currentContext; + + private final UserContext userContext = new UserContext(); + private RecordContext recordContext; + private final Stack networkStack = new Stack(); + + private MessageListener contextMessageListener; + + public ContextManager() + { + currentContext = userContext; + initContextMessageListener(); + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * CONTEXT PROVIDER + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + /** + * After a context has been removed by stopXXXMode(), contextSwitch() will determine the next current context. + * It only peeks at the context collections, but does not modify them. Calling this does never harm, + * but after one of the context collections has been changed, it should be called.
+ * It notifies ContextProviderListeners if the context has actually changed after calling this. + *

+ * The policy for context switches is defines in the interface here : {@link X4SModeSwitcher} + *

+ * Note, this should be called before the mode switch events, because we first want to completely context switch, before we change the mode. + * Generally always publish events after the event has actually occurred. + * + */ + protected void contextSwitch() + { + LogoContext old = currentContext; + + if (networkStack.size() > 0) + currentContext = networkStack.peek(); + else + currentContext = recordContext != null ? recordContext : userContext; + + if(old != currentContext) + notifyContextSwitched(currentContext); + } + + /** + * returns the current context after the policy described in the in the interface {@link X4SModeSwitcher} + */ + @Override + public LogoContext getContext() + { + return currentContext; + } + + // Context switch : internal communication : no direct gui update, must not run on event dispatcher thread + + private final ArrayList contextSwitchListeners = new ArrayList(); + + @Override + public void addContextSwitchListener(ContextSwitchListener listener) + { + contextSwitchListeners.add(listener); + } + + @Override + public void removeContextSwitchListener(ContextSwitchListener listener) + { + contextSwitchListeners.remove(listener); + } + + public void notifyContextSwitched(LogoContext newContext) + { + for (ContextSwitchListener listener : contextSwitchListeners) + listener.contextSwitched(newContext); + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * MODE SWITCHER + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + @Override + public String getSerializedContext() + { + return currentContext.toString(); + } + + @Override + public boolean isUserMode() + { + return currentContext instanceof UserContext; + } + + @Override + public boolean isRecordMode() + { + return currentContext instanceof RecordContext; + } + + @Override + public boolean isNetworkMode() + { + return currentContext instanceof NetworkContext; + } + + /** + * A record mode can only be started from user mode. In other words, + * a record mode cannot be started, if there is another record or network mode active. + * @throws IllegalStateException If current mode is not user mode. + * @throws IllegalArgumentException If the fileNames are not well formed, null, or ambiguous + * @throws IOException If the record files could not be created => recording is impossible => record mode is impossible + */ + @Override + public void startRecordMode(String[] fileNames) throws IllegalArgumentException, IOException + { + if (!isUserMode()) + throw new IllegalStateException(); + + recordContext = new RecordContext(fileNames); + recordContext.addBroadcastListener(contextMessageListener); + + contextSwitch(); + + notifyContestModeStarted(); + } + + /** + * This will first regularly kill all network contexts, one after the other. Listeners will be notified for every kill.
+ * Afterwards it will kill the current record context.
+ * When this call returns, current mode is user mode. + * + * @throws IllegalStateException If there is no recordContext available + */ + @Override + public void stopRecordMode() throws IllegalStateException + { + if (recordContext == null) + return; // might be a quick double klick that causes this + + while(isNetworkMode()) + popNetworkMode(); + + recordContext = null; + + contextSwitch(); + + notifyRecordModeStopped(); + } + + /** + * @throws IllegalArgumentException If serializedContext is corrupted + */ + @Override + public void pushNetworkMode(String serializedContext) throws IllegalArgumentException + { + NetworkContext nc = new NetworkContext(serializedContext); + networkStack.push(nc); + + contextSwitch(); + + if (networkStack.size() == 1) + notifyNetworkModeStarted(); + } + + /** + * This will kill the current network context. + * @throws IllegalStateException - if there is no network context to kill. + */ + @Override + public void popNetworkMode() throws IllegalStateException + { + if (!isNetworkMode()) + throw new IllegalStateException("There is no network context to kill."); + + networkStack.pop(); + + contextSwitch(); + + if (!isNetworkMode()) + notifyNetworkModeStopped(); + } + + // Mode Change Listeners : update GUI => run on event dispatcher thread + + private ArrayList modeChangeListeners = new ArrayList(); + + @Override + public void addModeChangedListener(ModeChangeListener listener) + { + modeChangeListeners.add(listener); + } + + @Override + public void removeModeChangedListener(ModeChangeListener listener) + { + modeChangeListeners.remove(listener); + } + + private void notifyContestModeStarted() + { + for (ModeChangeListener listener : modeChangeListeners) + listener.recordModeStarted(); + } + + private void notifyRecordModeStopped() + { + for (ModeChangeListener listener : modeChangeListeners) + listener.recordModeStopped(); + } + + private void notifyNetworkModeStarted() + { + for (ModeChangeListener listener : modeChangeListeners) + listener.networkModeStarted(); + } + + private void notifyNetworkModeStopped() + { + for (ModeChangeListener listener : modeChangeListeners) + listener.networkModeStopped(); + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * CONTEXT MESSAGE LISTENERS (used to broadcast clock events from record mode) + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + private void initContextMessageListener() + { + contextMessageListener = new MessageListener(){ + + @Override + public void messageEvent(String source, String message) + { + for (MessageListener listener : broadcastListeners) + listener.messageEvent(source, message); + } + }; + } + + private final ArrayList broadcastListeners = new ArrayList(); + + @Override + public void addBroadcastListener(MessageListener listener) + { + broadcastListeners.add(listener); + } + + @Override + public void removeBroadcastListener(MessageListener listener) + { + broadcastListeners.remove(listener); + } + +} diff --git a/logo/src/xlogo/kernel/userspace/context/ContextSwitcher.java b/logo/src/xlogo/kernel/userspace/context/ContextSwitcher.java new file mode 100644 index 0000000..e38c645 --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/context/ContextSwitcher.java @@ -0,0 +1,43 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace.context; + + +public interface ContextSwitcher +{ + public LogoContext getContext(); + + public void addContextSwitchListener(ContextSwitchListener listener); + + public void removeContextSwitchListener(ContextSwitchListener listener); + + public interface ContextSwitchListener + { + public void contextSwitched(LogoContext newContext); + } +} diff --git a/logo/src/xlogo/kernel/userspace/context/LogoContext.java b/logo/src/xlogo/kernel/userspace/context/LogoContext.java new file mode 100644 index 0000000..15bdc1f --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/context/LogoContext.java @@ -0,0 +1,312 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace.context; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import xlogo.Logo; +import xlogo.interfaces.ProcedureMapper.ProcedureMapListener; +import xlogo.kernel.gui.GuiMap; +import xlogo.kernel.userspace.GlobalVariableTable; +import xlogo.kernel.userspace.PropertyListTable; +import xlogo.kernel.userspace.files.LogoFile; +import xlogo.kernel.userspace.procedures.Procedure; +import xlogo.storage.global.GlobalConfig; +/** + * A LogoContext contains all the symbol tables for execution of Logo programs

+ * Parts of this resemble the old Workspace class, but this has a new purpose and it is used differently. + * In XLogo4Schools, a LogoContext is only a container for a Logo environment - all the symbol tables. + *

+ * Note: To be consistent, the execution stack should be included in the context too, but this would (for now) need too much work + * to refactor the existing interpreter. It is for sure something that should be done in the future, when the entire interpreter is refactored. + * + * @author Marko Zivkovic - + * @author Loïc Le Coq - methods inherited from XLogo are marked with author-tag + */ +public class LogoContext +{ + private LogoFile openFile; + + private final GlobalVariableTable globals = new GlobalVariableTable(); + + private final PropertyListTable propertyLists = new PropertyListTable(); + + private final Map files = new HashMap(); + + private final HashMap> procedureTable = new HashMap>(); + + // private Map executables = new HashMap(); + + private final GuiMap guiMap = new GuiMap(); + + public LogoContext() { } + + /* + * Symbol table getters. + */ + + /** + * All the files in this context + * @return + */ + public Map getFilesTable() + { + return files; + } + + /** + * All Executable procedures from all files. Procedures with equal names, + * from different files, will be included in the same list.
+ * This Map is used to keep track of all defined procedures and to resolve + * name conflicts. + */ + public HashMap> getProcedureTable() + { + return procedureTable; + } + + /** + * Global Logo variables + * @return + */ + + public GlobalVariableTable getGlobals() + { + return globals; + } + + /** + * Logo property lists + * @return + */ + public PropertyListTable getPropertyLists() + { + return propertyLists; + } + + /** + * For all Gui Objects (Buttons, ComboBoxes...) + */ + public GuiMap getGuiMap() + { + return guiMap; + } + + /* + * Context dependent operations + */ + + public String[] getFileOrder() + { + return files.keySet().toArray(new String[files.size()]); + } + + public void openFile(String fileName) + { + openFile = files.get(fileName); + } + + public void closeFile() + { + openFile = null; + } + + public LogoFile getOpenFile() + { + return openFile; + } + + /** + * This is the preferred method to create a file within a context, because different contexts prefer different LogoFile configurations. + * @throws IllegalStateException if fileName already exists in the files table + * @throws IOException If the file could not be created on the file system and the text not written. + */ + public void createFile(String fileName, String text) throws IOException + { + if (files.containsKey(fileName)) + throw new IllegalStateException("Attempt to create already existing file."); + + LogoFile file = LogoFile.createNewFile(fileName); + file.setText(text); + file.store(); + + files.put(fileName, file); + installListeners(file); + } + + public void importFile(File path, String newFileName) throws IOException + { + if (files.containsKey(newFileName)) + throw new IllegalStateException("Attempt to create already existing file."); + + if (!path.isFile()) + throw new IllegalArgumentException("The specified file does not exist : " + path.toString()); + + String extension = GlobalConfig.LOGO_FILE_EXTENSION; + if(!path.getName().endsWith(extension)) + throw new IllegalArgumentException("Only accept " + extension + " files, but received : " + path.toString()); + + String fileName = path.getName(); + fileName = path.getName().substring(0, fileName.length() - extension.length()); + + LogoFile file = LogoFile.importFile(path, newFileName); + files.put(newFileName, file); + installListeners(file); + } + + /** + * Note: does not perform checks on validity of file names. + * @param oldName + * @param newName + */ + public void renameFile(String oldName, String newName) + { + if (oldName.equals(newName)) + return; + + LogoFile file = files.get(oldName); + + // must first re-map in files table because file.setFileName fires events + files.remove(oldName); + files.put(newName, file); + + file.setFileName(newName); + } + + private final ArrayList procedureMapListener = new ArrayList(); + + /** + * This should be used when a file is created or added to this context. + * Listeners that were previously added with {@link #addProcedureMapListener(ProcedureMapListener)} will be installed. + * @param file + */ + public void installListeners(LogoFile file) + { + for (ProcedureMapListener listener : procedureMapListener) + file.addProcedureMapListener(listener); + } + + /** + * This helps the Procedure Manager to register to ProcedureMap events from the logo files. + * The ProcedureManager does not need to be notified explicitly when a new file is created. + * @see #installListeners(LogoFile) + * @param listener + */ + public void addProcedureMapListener(ProcedureMapListener listener) + { + procedureMapListener.add(listener); + for (LogoFile file : files.values()) + file.addProcedureMapListener(listener); + } + + public void removeProcedureMapListener(ProcedureMapListener listener) + { + procedureMapListener.remove(listener); + for (LogoFile file : files.values()) + file.removeProcedureMapListener(listener); + } + + /* + * MISC + */ + + /** + * That's the String that is sent via TCP, and interpreted by the receiver in {@link NetworkContext}, + * using {@link #setWorkspace(String)} + * @author Loïc Le Coq + * @author Marko Zivkovic + * refactored using the new data structures. + */ + public String toString() + { + StringBuffer sb = new StringBuffer(); + + for (String key : getGlobals().getVariables()) + { + sb.append("-"); + sb.append(key); + sb.append("\n"); + sb.append(getGlobals().getValue(key)); + sb.append("\n"); + } + + for (HashMap fileToProc : procedureTable.values()) + { + if (fileToProc.size() != 1) + continue; + + Procedure procedure = null; + for (Procedure uniqueProc : fileToProc.values()) + procedure = uniqueProc; // retrieve the only procedure in fileToProc + + sb.append(Logo.messages.getString("pour") + " " + procedure.name); + for (int j = 0; j < procedure.nbparametre; j++) + { + sb.append(" :" + procedure.variable.get(j)); + } + sb.append("\n"); + sb.append(procedure.instruction); + sb.append(Logo.messages.getString("fin")); + sb.append("\n\n"); + } + return (new String(sb)); + } + + /** + * @return true : {@link FileContainerChangeListener} should be fired. + */ + public boolean fireFileEvents() + { + return true; + } + + /** + * @return true : {@link AmbiguityListener}, + * {@link ExecutablesChangedListener}, + * {@link ProceduresDefinedListener} should be fired. + */ + public boolean fireProcedureEvents() + { + return true; + } + + /** + * Default is true. Other contexts might override this. + * @return Whether it is allowed to create, delete, or rename files. + * @note : this is only a suggestion for the files manager and the gui, modification of the table is still possible + */ + public boolean isFilesListEditAllowed() + { + return true; + } +} diff --git a/logo/src/xlogo/kernel/userspace/context/NetworkContext.java b/logo/src/xlogo/kernel/userspace/context/NetworkContext.java new file mode 100644 index 0000000..937e513 --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/context/NetworkContext.java @@ -0,0 +1,206 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace.context; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; + +import xlogo.Logo; +import xlogo.kernel.userspace.files.LogoFile; + +/** + * The network context is very different compared to the other contexts.
+ * 1. It parses its contents from a serialized context string ({@link #toString()})
+ * 2. Its files are created virtually ({@link xlogo.storage.Storable#isVirtual()}). They are gone when the network mode stops, leaving no trace.
+ * 3. {@link #fireFileEvents()}} and {@link fireProcedureEvents()}} suggest the managers to not fire events, + * because the network files are not meant to be displayed in the editor. + * In XLogo this effect was achieved by setting the property affichable of procedures. + * @author Marko Zivkovic + */ +public class NetworkContext extends LogoContext +{ + + public NetworkContext(String networkContext) throws IllegalArgumentException + { + super(); + setContext(networkContext); + } + + /** + * This is used to include a remote context into XLogo4Schools and allow remote + * procedure calls via tcp. XLogo had this implemented in the Workspace class. + *

+ * Initially, in remote mode, the workspace was temporarily replaced by a new one. + * To indicate that the workspace with its procedures should not be displayed + * in the editor, Loïc had added setAffichable(false), meaning not displayable.
+ * + * Note that in XLogo, the workspace contained exactly one file. Therefore + * a simple swap in swap would do. However, in XLogo4Schools, we need more + * state information, and some of the state must be preserved. + * For example, in network mode, I do not want the user's procedures + * to be removed from the procedure list. I just want to disable the list.
+ *

+ * With this Network context, no events will be fired that would indicate, + * that some procedures or files have changed. But the ContextChangedListeners + * will be notified that we are now in network mode, thus they can disable + * the files list. + *

+ *

+ * Note that in XLogo, the workspace text has to be set in the editor just + * because analysProcedure() reads the Logo source code from there. + * In the network mode, It was actually not necessary to have it in the editor, + * because they didn't intend do display the text anyway (setAffichable(false)). + * setWorkspace and toString have probably been added late, so they just "hacked" + * that new property in. + * The existing "architecture" did not allow a cleaner extension of the + * system, so they must have added "affichable" for this only purpose. + *

+ * In the current implementation, "affichable" is completely removed.
+ * I regulate this effect by either firing events or not. + *

+ * Note, parsing of the context is inherited from XLogo. Its implementation is very optimistic. + * It assumes that only well-formed strings are provided. Therefore it throws no actual exceptions. + * I added IllegalArgumentException to provide a more general interface, + * and to prepare clients for the future (?) when this parsing is re-implemented. + * + * @param app + * @param wp + * @author Marko Zivkovic, Loic + */ + protected void setContext(String wp) throws IllegalArgumentException + { + StringReader sr = new StringReader(wp); + BufferedReader bfr = new BufferedReader(sr); + try + { + String input = parseVariables(bfr); + + if (input != null) + { + StringBuilder sb = new StringBuilder(); + sb.append(input); + sb.append("\n"); + readerToBuilder(sb, bfr); + + String vFileName = "netowork_file"; + createFile(vFileName, ""); + LogoFile file = getFilesTable().get(vFileName); + file.setText(sb.toString()); + } + } + catch (IOException e) + {} + finally + { + try + { + bfr.close(); + } + catch (IOException e) + {} + sr.close(); + } + } + + /** + * append all lines of br to sb + */ + private void readerToBuilder(StringBuilder sb, BufferedReader br) throws IOException + { + String line = null; + while((line = br.readLine()) != null) + { + sb.append(line); + sb.append("\n"); + } + sb.deleteCharAt(sb.length()-1); + } + + private String parseVariables(BufferedReader bfr) throws IOException, IllegalArgumentException + { + String input = ""; + while ((input = bfr.readLine()) != null) + { + if (!input.startsWith(Logo.messages.getString("pour"))) + { + String var = input.substring(1); // - expected + String value = bfr.readLine(); + getGlobals().define(var, value); + } + else + break; + } + return input; + } + + /** + * Files created in network mode are virtual. + */ + @Override + public void createFile(String fileName, String text) throws IllegalArgumentException + { + LogoFile vFile = LogoFile.createNewVirtualFile(fileName); + getFilesTable().put(fileName, vFile); + + LogoFile file = LogoFile.createNewVirtualFile(fileName); + file.setText(text); + + getFilesTable().put(fileName, file); + + installListeners(file); + } + + /** + * @return In network mode : false : {@link FileContainerChangeListener} events will not be fired. + */ + public boolean fireFileEvents() + { + return false; + } + + /** + * @return In network mode : false : {@link AmbiguityListener}, + * {@link xlogo.interfaces.ExecutablesChangedListener}, + * {@link xlogo.interfaces.ProceduresDefinedListener} events will not be fired. + */ + public boolean fireProcedureEvents() + { + return false; + } + + /** + * The Network context suggest that the user should not be allowed to create files from the gui. + * @see LogoContext#isFilesListEditAllowed() + */ + @Override + public boolean isFilesListEditAllowed() + { + return false; + } +} diff --git a/logo/src/xlogo/kernel/userspace/context/RecordContext.java b/logo/src/xlogo/kernel/userspace/context/RecordContext.java new file mode 100644 index 0000000..48237de --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/context/RecordContext.java @@ -0,0 +1,146 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace.context; + +import java.io.IOException; +import java.util.ArrayList; + +import xlogo.interfaces.MessageBroadcaster; +import xlogo.kernel.userspace.files.RecordFile; + +/** + * The context for contest/record mode. + * When creating a new RecordContext, a number of RecordFiles are created, for each name provided in the constructor + *

+ * Note that a RecordContext only works correctly with RecordFiles. + * Therefore FileManagers should always use context.createFile(); to add new files, and never put them directly into the files table. + *

+ * Besides, RecordContext suggests to not create new files ({@link #isFilesListEditAllowed()}), + * but files can still be created by using Logo commands, such as define or load. + * + * @author Marko + */ +public class RecordContext extends LogoContext implements MessageBroadcaster +{ + private String[] fileOrder; + + private MessageListener fileTimerListener; + private ArrayList timerEventListeners = new ArrayList(); + + public RecordContext(final String[] recordModeFileNames) throws IOException + { + super(); + this.fileOrder = recordModeFileNames; + initFileTimerListener(); + setupRecordFiles(); + } + + protected void setupRecordFiles() throws IOException + { + for(String fileName : fileOrder) + createFile(fileName, ""); + } + + private void initFileTimerListener() + { + fileTimerListener = new MessageListener(){ + + @Override + public void messageEvent(String source, String message) + { + for (MessageListener listener : timerEventListeners) + listener.messageEvent(source, message); + } + }; + } + + @Override + public void createFile(String fileName, String text) throws IOException + { + RecordFile file = RecordFile.createNewFile(fileName); + + if (text != null && text.length() > 0) + { + file.setText(text); + file.store(); + } + + installListeners(file); + file.addBroadcastListener(fileTimerListener); + + getFilesTable().put(fileName, file); + } + + public void openFile(String fileName) + { + super.openFile(fileName); + RecordFile file = (RecordFile) getFilesTable().get(fileName); + file.startRecord(); + } + + @Override + public void closeFile() + { + RecordFile file = (RecordFile) getOpenFile(); + file.pauseRecord(); + super.closeFile(); + } + + @Override + public String[] getFileOrder() + { + // TODO must extend file order for command "define" [otherwise constant number of files in record mode] + // - although it's not really correct, no visible consequences in app => no priority ... program works just as well + return fileOrder; + } + + /** + * The Record context suggest that the user should not be allowed to create files from the gui. + * @see LogoContext#isFilesListEditAllowed() + */ + @Override + public boolean isFilesListEditAllowed() + { + return false; + } + + + @Override + public void addBroadcastListener(MessageListener listener) + { + timerEventListeners.add(listener); + } + + @Override + public void removeBroadcastListener(MessageListener listener) + { + timerEventListeners.add(listener); + } + + +} diff --git a/logo/src/xlogo/kernel/userspace/context/UserContext.java b/logo/src/xlogo/kernel/userspace/context/UserContext.java new file mode 100644 index 0000000..064e665 --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/context/UserContext.java @@ -0,0 +1,191 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace.context; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; + +import xlogo.Logo; +import xlogo.kernel.userspace.files.LogoFile; +import xlogo.messages.MessageKeys; +import xlogo.messages.async.dialog.DialogMessenger; +import xlogo.storage.WSManager; +import xlogo.storage.global.GlobalConfig; +import xlogo.storage.user.UserConfig; + +public class UserContext extends LogoContext +{ + + /** + * Load and parse all the files in current user's source directory.
+ * This happens only once, in the constructor.
+ * Refresh is currently not planned, but it would be easy to add. + * + * @param userDir + */ + public UserContext() + { + loadUserFiles(); + } + + private void loadUserFiles() + { + UserConfig userConfig = WSManager.getUserConfig(); + + if (userConfig.isVirtual()) + return; + + File sourceDir = userConfig.getSourceDirectory(); + if (!sourceDir.exists()) + sourceDir.mkdirs(); + + if (!sourceDir.isDirectory()) + { + DialogMessenger.getInstance().dispatchMessage(Logo.messages.getString("ws.error.title"), + Logo.messages.getString("ws.error.userdir.not.dir")); + return; + } + + StringBuilder ioErrors = new StringBuilder(); + + for (String fileName : getLogoFileNamesFromDirectory(sourceDir)) + { + String name = fileName.substring(0, fileName.length() - GlobalConfig.LOGO_FILE_EXTENSION.length()); + userConfig.addFile(name); + try + { + LogoFile file = LogoFile.loadFile(name); + getFilesTable().put(file.getPlainName(), file); + + } + catch (IOException e) + { + ioErrors.append(e.toString()); + ioErrors.append("\n\n"); + } + } + + // must remove files from fileOrder that could not be found anymore. + for (String fileName : new ArrayList(userConfig.getFileOrder())) + { + if (!getFilesTable().containsKey(fileName)) + userConfig.getFileOrder().remove(fileName); + } + + if (ioErrors.length() > 0) + { + DialogMessenger.getInstance().dispatchMessage(Logo.messages.getString("ws.error.title"), + Logo.messages.getString("ws.error.could.not.load.logo.files") + "\n" + ioErrors.toString()); + } + } + + /** + * Caller must make sure that newName does not already exist. + */ + @Override + public void renameFile(String oldName, String newName) + { + super.renameFile(oldName, newName); + WSManager.getUserConfig().renameFile(oldName, newName); + } + + @Override + public void createFile(String fileName, String text) throws IOException + { + /* + * Eager creation of files in file order list in user config. + */ + + if (!WSManager.getUserConfig().isVirtual()) + super.createFile(fileName, text); + else + { + LogoFile file = LogoFile.createNewVirtualFile(fileName); + file.setText(text); + getFilesTable().put(fileName, file); + installListeners(file); + } + WSManager.getUserConfig().addFile(fileName); + } + + @Override + public void importFile(File path, String newFileName) + { + try + { + super.importFile(path, newFileName); + } + catch (IOException e) + { + DialogMessenger.getInstance().dispatchError(MessageKeys.GENERAL_ERROR_TITLE, "Could not import file : \n" + e.toString()); + } + WSManager.getUserConfig().addFile(newFileName); + } + + private String[] getLogoFileNamesFromDirectory(File dir) + { + return dir.list(new FilenameFilter(){ + public boolean accept(File file, String name) + { + return name.endsWith(".lgo"); + } + }); + } + + @Override + public String[] getFileOrder() + { + /* + * Lazy deletion from file order list in user config. + */ + ArrayList list = new ArrayList(WSManager.getUserConfig().getFileOrder()); + Map filesTable = getFilesTable(); + + if (filesTable.size() != list.size()) + { + Iterator iter = list.iterator(); + String current; + + while(iter.hasNext()) + { + current = iter.next(); + if(!filesTable.containsKey(current)) + iter.remove(); + } + } + + return list.toArray(new String[list.size()]); + } + + + +} diff --git a/logo/src/xlogo/kernel/userspace/files/LogoFile.java b/logo/src/xlogo/kernel/userspace/files/LogoFile.java new file mode 100644 index 0000000..92fe23c --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/files/LogoFile.java @@ -0,0 +1,755 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace.files; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; + +import xlogo.Logo; +import xlogo.interfaces.ErrorDetector; +import xlogo.interfaces.ProcedureMapper; +import xlogo.kernel.userspace.ProcedureErrorMessage; +import xlogo.kernel.userspace.procedures.ExecutablesContainer; +import xlogo.kernel.userspace.procedures.Procedure; +import xlogo.kernel.userspace.procedures.Procedure.State; +import xlogo.messages.MessageKeys; +import xlogo.messages.async.dialog.DialogMessenger; +import xlogo.storage.Storable; +import xlogo.storage.StorableDocument; +import xlogo.storage.WSManager; +import xlogo.storage.global.GlobalConfig; +import xlogo.storage.user.UserConfig; +import xlogo.storage.workspace.NumberOfBackups; +import xlogo.storage.workspace.WorkspaceConfig; +import xlogo.utils.Utils; + +/** + * This class holds the text file a user entered in the editor. + * It analyzes the text and maintains a symbol table for all defined procedures that live within it. + *

+ * The file does never store itself implicitly, except for when it is created using {@link #createNewFile(String)} or renamed using {@link #setFileName(String)} + * In every other case, {@link #store()}} or {@link #storeCopyToFile(File)}} must be invoked explicitly. + *

+ * The file's text can be set using {@link #setTextFromReader(BufferedReader)}} (preferred) or {@link #setText(String)}}. + * Both will try to parse the signature of all procedures using the constructor of {@link xlogo.kernel.userspace.procedures.Procedure} + * + * @author Marko Zivkovic, (Loïc Le Coq's parsing of procedures is not recognizable anymore.) + * + */ +public class LogoFile extends StorableDocument implements ExecutablesContainer, ProcedureMapper, ErrorDetector +{ + + /** + * + */ + private static final long serialVersionUID = 1117062836862782516L; + + /** + * UserConfig of the owner of this file + */ + private UserConfig userConfig; + + /** + * Contains only executable procedures + */ + private Map executables; + + /** + * Contains all procedures, no matter what the state is. + * The order of the list is relevant to reproduce the editor text after the Logo command 'eraseprocedure' + * (after {@link #deleteProcedure(String)}}) + */ + private ArrayList allProcedures; + + /** + * A flag that indicated whether the last parsing ended with errors or ambiguities + */ + private boolean hasError; + + /* + * CONSTRUCTOR & STATIC CONSTRUCTORS, FILE LOADERS + */ + + /** + * The LogoFile automatically sets its location to the current user's src directory, if that user is not virtual. + * @param fileName + * @throws IllegalArgumentException see : {@link Storable#setFileName()} + */ + protected LogoFile(String fileName) throws IllegalArgumentException + { + super(); + this.userConfig = WSManager.getUserConfig(); + if (!userConfig.isVirtual()) + setLocation(userConfig.getSourceDirectory()); + setFileName(fileName); + executables = new HashMap(); + allProcedures = new ArrayList(); + } + + public static LogoFile createNewVirtualFile(String fileName) + { + LogoFile file = null; + try + { + file = new LogoFile(fileName); + file.makeVirtual(); + } + catch (IllegalArgumentException ignore) { } + return file; + } + /** + * Create a new file and store it in the user's source directory. + * @throws IOException + * @throws IllegalArgumentException + */ + public static LogoFile createNewFile(String fileName) throws IOException, IllegalArgumentException + { + LogoFile file = new LogoFile(fileName); + file.setupFileSystem(); + return file; + } + + /** + * Load the specified file from the user's source directory and parse procedure structures. + * @param fileName - without extension + * @return + * @throws IOException + */ + public static LogoFile loadFile(String fileName) throws IOException + { + UserConfig userConfig = WSManager.getUserConfig(); + File path = userConfig.getLogoFilePath(fileName); + String text = Utils.readLogoFile(path.toString()); + LogoFile file = new LogoFile(fileName); + file.setText(text); + if (userConfig.isVirtual()) + file.makeVirtual(); + return file; + } + + /** + * Open any file on the file system and integrate it in the UserSpace. + * The file will be stored and made visible under the specified newFileName + * @param file + * @param newFileName + * @throws IOException + */ + public static LogoFile importFile(File path, String newFileName) throws IOException + { + String text = Utils.readLogoFile(path.toString()); + LogoFile file = new LogoFile(newFileName); + file.setText(text); + if (WSManager.getUserConfig().isVirtual()) + file.makeVirtual(); + file.store(); + return file; + } + + protected UserConfig getUserConfig() + { + return userConfig; + } + /** + * This assumes that the file name is well formed. No additional checks are performed + * Rename this LogoFile and the file on the file system, if it exists there. Notify all FileChangeListeners. + * This accepts name with or without .lgo extension. + * @param newFileName - without extension + */ + @Override + public void setFileName(String newFileName) + { + if (newFileName == null || newFileName.length() == 0) + { + DialogMessenger.getInstance().dispatchError( + Logo.messages.getString(MessageKeys.NAME_ERROR_TITLE), + Logo.messages.getString(MessageKeys.EMPTY_NAME)); + return; + } + + if (!Storable.checkLegalName(newFileName)) + { + DialogMessenger.getInstance().dispatchError( + Logo.messages.getString(MessageKeys.NAME_ERROR_TITLE), + Logo.messages.getString(MessageKeys.ILLEGAL_NAME) + " : " + newFileName); + return; + } + + String oldPlainName = getPlainName(); + super.setFileName(newFileName); + String newPlainName = getPlainName(); + + if (oldPlainName != null) + notifyRenamed(oldPlainName, newPlainName); + } + + @Override + public String getFileNameExtension() + { + return GlobalConfig.LOGO_FILE_EXTENSION; + } + + private void notifyRenamed(String oldName, String newName) + { + for(ProcedureMapListener listener : procedureMapListeners) + listener.ownerRenamed(oldName, newName); + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * STORE & LOAD FILE + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + /** + * If this is not virtual, create this file on the file system and create a backup folder for it. + * @throws IOException + */ + protected void setupFileSystem() throws IOException + { + if (isVirtual()) + return; + + File source = getFilePath(); + File backupFolder = userConfig.getFileBackupDir(getPlainName()); + + if (!source.getParentFile().exists()) + source.getParentFile().mkdirs(); + + if (!backupFolder.exists()) + backupFolder.mkdirs(); + + storeCopyToFile(source); + } + + /** + * If this is not virtual, store the file in the source folder of the UserSpace,
+ * and another copy in the backup folder, if this is required by {@link WorkspaceConfig#getNumberOfBackups()}. + */ + @Override + public void store() throws IOException + { + super.store(); + if (isVirtual()) + return; + doBackup(); + } + + @Override + public void delete() + { + super.delete(); + Collection procedures = new ArrayList(executables.keySet()); + executables.clear(); + allProcedures.clear(); + notifyDeleted(procedures); + } + + + /** + * Store a backup copy of this file. + * If the number of maximally allowed backups is exceeded, + * delete the oldest copies until the number of backups equals the limit + * defined by {@link WorkspaceConfig#getNumberOfBackups()}} + * @throws IOException + */ + private void doBackup() throws IOException + { + WorkspaceConfig wc = WSManager.getInstance().getWorkspaceConfigInstance(); + NumberOfBackups nob = wc.getNumberOfBackups(); + + File backupFile = userConfig.getBackupFilePath(getPlainName()); + File backupFolder = backupFile.getParentFile(); + if (!backupFolder.exists()) + backupFolder.mkdirs(); + + if (nob != NumberOfBackups.NO_BACKUPS) + storeCopyToFile(backupFile); + + if (nob == NumberOfBackups.INFINITE) + return; + + int max = nob.getNumber(); // max is >= 0 + // Assume no outer manipulation of that directory + File[] backups = backupFolder.listFiles(); + + int actual = backups.length; + if (actual <= max) + return; + + // must delete the oldest backups + Arrays.sort(backups, new Comparator(){ + public int compare(File f1, File f2) + { + return f2.getName().compareTo(f1.getName().toString()); + } + }); + + while (actual > max) + { + actual--; + backups[actual].delete(); + } + } + + /** + * The file path of this LogoFile in the source directory. + */ + @Override + public File getFilePath() + { + if (super.getFilePath() != null) + return super.getFilePath(); + return userConfig.getLogoFilePath(getPlainName()); + } + + @Override + public File getLocation() + { + if (super.getLocation() != null) + return super.getLocation(); + return userConfig.getSourceDirectory(); + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * SERIALIZATION AND DESERIALIZATION + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + @Override + protected String generateText() + { + StringBuilder text = new StringBuilder(); + + for(Procedure proc : allProcedures) + { + text.append(proc.getText()); + text.append("\n"); + } + + return text.toString(); + } + + /** + * Changes made 21.6.2013 and July 2013 + *

+ * In XLogo, this was {@code Editor.analyseprocedure()} + *

+ * Refactored:
+ * Initially all that happens in + *

  • {@link #setText(String)} + *
  • {@link #parseText(BufferedReader)} + *
  • {@link #nextProcedure(BufferedReader)} + *
  • {@link Procedure#Procedure() } + *
  • {@link #untilEnd() }
    + * was composed into one big and unreadable procedure {@code Editor.analyseprocedure()}. + * Note that the Editor (a GUI Controller) was responsible to parse text. + * {@code Editor.analyseprocedure()} took the text it was analyzing from the Editor's text component directly. + * Thus, whenever a procedure was programmatically defined, or if a workspace was received from the network, + * the text had to be written to the editor first before {@code Editor.analyseprocedure()} was called.

    + * Note that in the networking case, the received text was never meant to be displayed. + * In that case the Editor served merely as a temporary container such that analyseProcedure() could read the text from it. + * This was the only reason why the property "affichable" (displayable) was added to so many classes. + * + *

    + * New Mechanism:
    + * In XLogo, as soon as an error was found in the document, an exception was thrown and displayed to the user.
    + * The new approach is to first split the document wherever a line starts with a token 'end'. + *

    + * [#belongs to procedure 1
    + * ...
    + * end][#belongs to procedure 2
    + * ...
    + * end] ... + *

    + * These parts of the document are given to the constructor {@code Procedure#Procedure(String)}, + * so the procedure can maintain its own state + *
    + * Based on the type of errors, a Procedure can now detect several errors at a time and report them. + * The LogoFile can then report all errors that have been collected from its procedures. + * This approach allows to give more precise error messages to the user. + * Example: It is now possible to say which procedure is missing an 'end' + *
    + * In the new implementation, a Procedure is not necessarily executable. + * Whether it is executable, can be read from its state {@link Procedure.State}. + * Its state can be + *

  • UNINITIALIZED + *
  • EXECUTABLE + *
  • COMMENT_ONLY (for white space and comments at the end of the document) + *
  • ERROR + *
  • AMBIGUOUS_NAME
    + *

    + * Only EXECUTABLE procedures are included in the procedureTable of the {@link xlogo.kernel.userspace.context.LogoContext}, + * but all procedures are maintained by LogoFile. + * @param str + * @throws DocumentStructureException + * @throws IOException + */ + @Override + protected void parseText(BufferedReader br) + { + /* + * Keep old procedures before reset of procedure tables. + * procedures that remain in the end, will count as deleted. + * procedures that existed before, but have errors now, count as deleted. + */ + HashMap deleted = new HashMap(); + for (Procedure proc : executables.values()) + deleted.put(proc.getName(), null); + + /* + * Must notify that all old executables are deleted as soon as a single procedure has an error, + * We want the whole file to be not executable when there exists an error + */ + Collection oldExecutables = new ArrayList(executables.keySet()); + + /* + * We don't want the procedures to become ordered by creation time in the editor. + * The Logo command "define" was affected by this change, hence it was adapted to work as before. + *

    + * Because we delete all the procedures from the tables [unlike XLogo] every time before reading the file, + * the procedures will be stored in the order in which the user defined them last. + */ + resetProcedureTables(); // Added by Marko Zivkovic, 21.6.2013 + + // When the file is empty, it has no errors... + hasError = false; + + try + { + while (br.ready()) // next procedure + { + Procedure proc; + String procedureText = untilEnd(br); + if (procedureText.equals("")) + break; + proc = new Procedure(procedureText); + proc.setOwnerName(getPlainName()); + + if (proc.getState() == State.EXECUTABLE) + { + deleted.remove(proc.getName()); + } + addProcedure(proc); + + if(proc.getState() == State.ERROR || proc.getState() == State.AMBIGUOUS_NAME) + hasError = true; + } + } + catch (IOException e){} // This should not happen, because no actual IO happens + finally { try { br.close(); } catch (IOException e) { } } + + if(hasError) + { + notifyDeleted(oldExecutables); + return; + } + + if (deleted.size() > 0) + notifyDeleted(deleted.keySet()); + + if (executables.size() > 0) + notifyDefined(executables.keySet()); + } + + /** + * @return String until the token 'end' is found on a line, or until the end of the BufferedReader + * @throws IOException + */ + private static String untilEnd(BufferedReader br) throws IOException + { + String end = Logo.messages.getString("fin").toLowerCase(); + StringBuffer text = new StringBuffer(); + String line; + + while (br.ready()) + { + line = br.readLine(); + if (line == null) + break; + else if (line.trim().toLowerCase().equals(end)) + { + text.append(end); + break; + } + else + text.append(line + "\n"); + } + + return text.toString(); + } + + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * PROCEDURE CONTAINER + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + /** + * Note: does not notify
    + * Delete all procedures + */ + private void resetProcedureTables() + { + allProcedures.clear(); + executables.clear(); + invalidateText(); + setText(null); + } + + /** + * Implementation of the Logo Command "define".
    + * FileChangeListeners are notified. + *

    + * In XLogo, the command define had no effect, when an error was detected while parsing. + * The same is true here, because an IllegalStateException is thrown if procedure is not executable. + *

    + * In XLogo4Schools, to preserve semantics, we create a {@link Procedure} using its normal constructor and then check for errors. + * If errors exist in the procedure text, the procedure should not be defined in its destined file either. + * The responsibility whether a procedure is added to a file lies therefore in the interpreter. + *

    + * Existing procedures with the same name are just redefined, as in XLogo. + *

    + * @param procedure Expects an executable procedure. + * @throws IllegalStateException - if procedure is not Executable or its name is ambiguous in this file. + */ + @Override + public void defineProcedure(Procedure procedure) + { + if (procedure.getState() != State.EXECUTABLE) + throw new IllegalStateException("Attempt to define procedure which is not executable."); + + Procedure other = executables.get(procedure.name); + + invalidateText(); + + if (other != null) + { + if (other.getState() == State.AMBIGUOUS_NAME) + throw new IllegalStateException("Attempt to redefine ambiguous procedure."); + + other.redefine(procedure); + + }else + { + allProcedures.add(procedure); + executables.put(procedure.name, procedure); + } + notifyDefined(procedure.getName()); + } + + /** + * This is for the Logo command 'eraseprocedure' + * @param name + * @throws IllegalArgumentException + */ + @Override + public void eraseProcedure(String name) + { + Procedure proc = getExecutable(name); + if(proc == null) + throw new IllegalStateException("Attempt to erase procedure which exists not."); + allProcedures.remove(proc); + executables.remove(name); + invalidateText(); + notifyDeleted(proc.getName()); + } + + /** + * Note: Does not notify listeners!
    + * Semantics: If more than one procedures with the same name are defined in a document, + * all are marked ambiguous. The first one is kept in the executables list to track ambiguity. + * @param pr + */ + protected void addProcedure(Procedure pr) + { + if (pr.getState() == State.EXECUTABLE) + { + Procedure other = executables.get(pr.name); + + if(other != null) + { + other.makeAmbiguous(); + pr.makeAmbiguous(); + } + else + executables.put(pr.name, pr); + } + allProcedures.add(pr); + invalidateText(); + } + + @Override + public Procedure getExecutable(String name) + { + return executables.get(name); + } + + /** + * @param name + * @return Whether an executable procedure with the specified name exists + */ + @Override + public boolean isExecutable(String name) + { + return executables.get(name) != null; + } + + @Override + public Collection getExecutables() + { + if (hasErrors()) + return new ArrayList(); + return executables.values(); + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * ERROR DETECTOR + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + @Override + public boolean hasErrors() + { + return hasError; + } + + public Collection getAllErrors() + { + ArrayList allErrors = new ArrayList(); + for(Procedure proc : allProcedures) + { + for (xlogo.kernel.userspace.procedures.ProcedureErrorType e : proc.getErrors()) + { + String description = proc.getName(); + if (description == null) + { + description = proc.getText().length() < 100 ? + proc.getText() : + proc.getText().substring(0, 100) + "..."; + } + + allErrors.add(new ProcedureErrorMessage(e, description, getPlainName())); + } + } + return allErrors; + } + + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * PROCEDURE MAPPER + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + /** + * Only executables. + * If the file has errors, no procedure is returned. + */ + @Override + public Collection getAllProcedureNames() + { + if (hasErrors()) + return new ArrayList(); + + ArrayList procedureNames = new ArrayList(); + + for (Procedure p : executables.values()) + procedureNames.add(p.getName()); + + return procedureNames; + } + + @Override + public Collection getAllProcedureNames(String fileName) + { + if (fileName.equals(getPlainName())) + return getAllProcedureNames(); + return null; + } + + /** + * Behaves similar like contains(). If the procedure is in the file's executable list, then returns this file's plainName. Otherwise null. + */ + @Override + public String getProcedureOwner(String procedureName) + { + if (executables.containsKey(procedureName)) + return getPlainName(); + return null; + } + + // Procedure Map Listeners + + private final ArrayList procedureMapListeners = new ArrayList(); + + @Override + public void addProcedureMapListener(ProcedureMapListener listener) + { + procedureMapListeners.add(listener); + + if(executables.size() > 0) + notifyDefined(executables.keySet()); // TODO hmmm + } + + @Override + public void removeProcedureMapListener(ProcedureMapListener listener) + { + procedureMapListeners.remove(listener); + } + + protected void notifyDefined(Collection procedures) + { + for (ProcedureMapListener listener : procedureMapListeners) + listener.defined(getPlainName(), procedures); + } + + protected void notifyDefined(String procedure) + { + for (ProcedureMapListener listener : procedureMapListeners) + listener.defined(getPlainName(), procedure); + } + + protected void notifyDeleted(Collection collection) + { + for (ProcedureMapListener listener : procedureMapListeners) + listener.undefined(getPlainName(), collection); + } + + protected void notifyDeleted(String procedure) + { + for (ProcedureMapListener listener : procedureMapListeners) + listener.undefined(getPlainName(), procedure); + } + + +} diff --git a/logo/src/xlogo/kernel/userspace/files/LogoFileContainer.java b/logo/src/xlogo/kernel/userspace/files/LogoFileContainer.java new file mode 100644 index 0000000..9c077a9 --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/files/LogoFileContainer.java @@ -0,0 +1,39 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace.files; + +import xlogo.interfaces.BasicFileContainer; + + +public interface LogoFileContainer extends BasicFileContainer +{ + /** + * Logo Command implementation + */ + public void editAll(); +} diff --git a/logo/src/xlogo/kernel/userspace/files/LogoFilesManager.java b/logo/src/xlogo/kernel/userspace/files/LogoFilesManager.java new file mode 100644 index 0000000..7a54902 --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/files/LogoFilesManager.java @@ -0,0 +1,560 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace.files; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; + +import xlogo.Logo; +import xlogo.interfaces.ErrorDetector.FileErrorCollector; +import xlogo.kernel.userspace.ProcedureErrorMessage; +import xlogo.kernel.userspace.context.ContextSwitcher; +import xlogo.kernel.userspace.context.LogoContext; +import xlogo.kernel.userspace.context.ContextSwitcher.ContextSwitchListener; +import xlogo.messages.MessageKeys; +import xlogo.messages.async.dialog.DialogMessenger; +import xlogo.storage.Storable; +import xlogo.storage.global.GlobalConfig; + +/** + * This Manager is completely new, because XLogo did not support multiple files.
    + * During the requirements analysis, we have decided to maintain a global scope for procedures. + * That means a procedure defined in file A is visible in file B. + *

    + * If we find during testing that the global scope is confusing for children and it leads to many ambiguity conflicts, + * then the current architecture allows to easily switch to file-wide scope. Instead of retrieving executables from the context's procedure table, + * we can directly retrieve them from the currently open/active file. + * + * @author Marko Zivkovic + */ +public class LogoFilesManager implements LogoFileContainer, FileErrorCollector +{ + private final ContextSwitcher contextProvider; + private LogoContext context; + + private final ArrayList fileListeners = new ArrayList(); + + public LogoFilesManager(ContextSwitcher contextProvider) + { + this.contextProvider = contextProvider; + initContextSwitchListener(); + setContext(contextProvider.getContext()); + } + + private void initContextSwitchListener() + { + contextProvider.addContextSwitchListener(new ContextSwitchListener(){ + @Override + public void contextSwitched(LogoContext newContext) + { + setContext(newContext); + } + }); + } + + private void setContext(LogoContext newContext) + { + LogoContext old = context; + context = newContext; + + LogoFile openFile = newContext.getOpenFile(); + if (openFile != null) + closeFile(openFile.getPlainName()); + + if (newContext.fireFileEvents()) // Example : Network context does not change GUI, only internal change => no events + { + if (old != null && old.fireFileEvents()) + for(LogoFile file : old.getFilesTable().values()) + notifyFileRemoved(file.getPlainName()); + + for (String fileName : newContext.getFileOrder()) + { + notifyFileAdded(fileName); + if (context.getFilesTable().get(fileName).hasErrors()) + notifyErrorsDetected(fileName); + } + } + + if (old == null || old.isFilesListEditAllowed() != newContext.isFilesListEditAllowed()) + notifyRightsChanged(); + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * X4S Specific features and Logo command implementations + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + /** + * The implementation of the Logo command {@code editall} or {@code edall}
    + * In XLogo4Schools, we cannot open all files simultaneously to show all procedures. Instead, editall opens the file that was edited last. + */ + public void editAll() + { + String fileName = getLastEditedFileName(); + if (fileName == null) + return; + openFile(fileName); + } + + @Override + public void importFile(File filePath) throws IOException + { + String name = filePath.getName().substring(0, + filePath.getName().length() + - GlobalConfig.LOGO_FILE_EXTENSION.length()); + + if(existsFile(name)) + name = makeUniqueFileName(name); + context.importFile(filePath, name); + notifyFileAdded(name); + } + + /** + * If file is a directory, the exported file will be named fileName. + * Otherwise the Logo-file will be exported to the file specified by dest + * @param fileName + * @param dest + * @throws IOException + */ + public void exportFile(String fileName, File dest) throws IOException + { + + if (dest.isDirectory()) + exportFile(fileName, dest, fileName); + else + { + File parent = dest.getParentFile(); + String targetName = dest.getName(); + exportFile(fileName, parent, targetName); + } + } + + /** + * @param fileName - of a file in the current context + * @param location - an existing directory on the file system + * @param targetName - the exported file's name + * @throws IOException + */ + public void exportFile(String fileName, File location, String targetName) throws IOException + { + LogoFile file = context.getFilesTable().get(fileName); + + if(file == null) + throw new IllegalArgumentException("The specified fileName does not exist in the context."); + + if (!location.isDirectory()) + throw new IllegalArgumentException("The specified location does not exist : " + location.toString()); + + String extendedName = targetName; + + if(extendedName == null || extendedName.length() == 0) + extendedName = fileName; + + String extension = GlobalConfig.LOGO_FILE_EXTENSION; + + if(!extendedName.endsWith(extension)) + extendedName += extension; + + File target = new File(location.toString() + File.separator + extendedName); + file.storeCopyToFile(target); + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * LOGO FILE CONTAINER + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + @Override + public String[] getFileNames() + { + return context.getFileOrder(); + } + + @Override + public void createFile(String fileName) throws IOException + { + context.createFile(fileName, ""); + notifyFileAdded(fileName); + } + + @Override + public void writeFileText(String fileName, String content) + { + LogoFile file = context.getFilesTable().get(fileName); + + if (file == null) + throw new IllegalStateException("Attempt to write to inexistent file."); + + boolean hadErrors = file.hasErrors(); + + file.setText(content); + + if (file.hasErrors()) + notifyErrorsDetected(fileName); // notify anyway + else if (hadErrors) + notifyErrorsCorrected(fileName); + } + + @Override + public void storeFile(String fileName) throws IOException + { + context.getFilesTable().get(fileName).store(); + } + + /** + * The file is also deleted from the file system + */ + @Override + public void removeFile(String fileName) + { + LogoFile file = context.getFilesTable().get(fileName); + file.delete(); + context.getFilesTable().remove(fileName); + notifyFileRemoved(fileName); + } + + /** + * Deletes all files from the context and removes them from the context's tables.
    + * Note: The events caused be deleting the files should cause all the procedures to disappear from the tables as well. + * [But the files manager doesn't care about procedures] + */ + @Override + public void eraseAll() + { + Collection files = context.getFilesTable().values(); + + while (!files.isEmpty()) + { + LogoFile nextVictim = null; + for (LogoFile file : files) + { + nextVictim = file; + break; + } + nextVictim.delete(); + context.getFilesTable().remove(nextVictim.getPlainName()); + notifyFileRemoved(nextVictim.getPlainName()); + } + context.getFilesTable().clear(); + } + + @Override + public boolean existsFile(String name) + { + return context.getFilesTable().containsKey(name); + } + + @Override + public String readFile(String name) + { + return context.getFilesTable().get(name).getText(); + } + + /** + * Please make sure the renaming makes sense, otherwise an IllegalStateException is thrown at you. + */ + @Override + public void renameFile(String oldName, String newName) + { + if (oldName.equals(newName)) + return; + + if(!existsFile(oldName)) + { + DialogMessenger.getInstance().dispatchError( + Logo.messages.getString(MessageKeys.NAME_ERROR_TITLE), + Logo.messages.getString(MessageKeys.RENAME_INEXISTENT_FILE)); + return; + } + + if (existsFile(newName)) + { + DialogMessenger.getInstance().dispatchError( + Logo.messages.getString(MessageKeys.NAME_ERROR_TITLE), + Logo.messages.getString(MessageKeys.WS_FILENAME_EXISTS_ALREADY)); + return; + } + + if (newName == null || newName.length() == 0) + { + DialogMessenger.getInstance().dispatchError( + Logo.messages.getString(MessageKeys.NAME_ERROR_TITLE), + Logo.messages.getString(MessageKeys.EMPTY_NAME)); + return; + } + + if (!Storable.checkLegalName(newName)) + { + DialogMessenger.getInstance().dispatchError( + Logo.messages.getString(MessageKeys.NAME_ERROR_TITLE), + Logo.messages.getString(MessageKeys.ILLEGAL_NAME) + " : " + newName); + return; + } + + context.renameFile(oldName, newName); + notifyFileRenamed(oldName, newName); + } + + @Override + public String makeUniqueFileName(String base) + { + int i = 0; + String name = null; + do + { + name = base + i; + i++; + } while (existsFile(name)); + return name; + } + + /** + * @throws IllegalArgumentException if the specified file does not exist in the current context. + */ + @Override + public void openFile(String fileName) + { + if(!existsFile(fileName)) + throw new IllegalStateException("The specified file to open does not exist in the current context."); + + LogoFile openFile = context.getOpenFile(); + if(openFile != null) + closeFile(openFile.getPlainName()); + + context.openFile(fileName); + notifyFileOpened(fileName); + } + + /** + * This can handle only one open file. + * If the wrong filename is closed, nothing happens

    + * @throws IllegalStateException + */ + @Override + public void closeFile(String fileName) + { + LogoFile openFile = context.getOpenFile(); + if (openFile == null || !openFile.getPlainName().equals(fileName)) + throw new IllegalStateException("Attempting to close a file that was not opened."); + context.closeFile(); + notifyFileClosed(openFile.getPlainName()); + } + + /** + * returns null if no file is open. + */ + @Override + public String getOpenFileName() + { + LogoFile file = context.getOpenFile(); + if (file == null) + return null; + return file.getPlainName(); + } + + public boolean isFilesListEditable() + { + return context.isFilesListEditAllowed(); + } + + /** + * the name of the file that was edited last in this context. + */ + @Override + public String getLastEditedFileName() + { + Calendar latest = Calendar.getInstance(); + latest.setTimeInMillis(0); + + LogoFile result = null; + for (LogoFile file : context.getFilesTable().values()) + { + Calendar fileDefinedAt = file.getLastSync(); + if (latest.before(fileDefinedAt)) + { + result = file; + latest = fileDefinedAt; + } + } + if (result == null) + return null; + + return result.getPlainName(); + } + + // Change listeners : these event update the gui, they must run on the event dispatcher thread + + @Override + public void addFileListener(FileContainerChangeListener listener) + { + if (listener == null) + throw new IllegalArgumentException("listener must not be null."); + fileListeners.add(listener); + } + + @Override + public void removeFileListener(FileContainerChangeListener listener) + { + fileListeners.remove(listener); + } + + private void notifyFileAdded(final String fileName) + { + if (!context.fireFileEvents()) + return; + + for (FileContainerChangeListener listener : fileListeners) + listener.fileAdded(fileName); + } + + private void notifyFileRemoved(final String fileName) + { + if (!context.fireFileEvents()) + return; + + for (FileContainerChangeListener listener : fileListeners) + listener.fileRemoved(fileName); + } + + private void notifyFileRenamed(final String oldName, final String newName) + { + if (!context.fireFileEvents()) + return; + + for (FileContainerChangeListener listener : fileListeners) + listener.fileRenamed(oldName, newName); + } + + private void notifyFileOpened(final String fileName) + { + if (!context.fireFileEvents()) + return; + + for (FileContainerChangeListener listener : fileListeners) + listener.fileOpened(fileName); + } + + private void notifyFileClosed(final String fileName) + { + if (!context.fireFileEvents()) + return; + + for (FileContainerChangeListener listener : fileListeners) + listener.fileClosed(fileName); + } + + private void notifyRightsChanged() + { + for (FileContainerChangeListener listener : fileListeners) + listener.editRightsChanged(context.isFilesListEditAllowed()); + } + + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * ERROR COLLECTOR : these events do not update the gui directly, they must not run on the event dispatcher thread + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + @Override + public Collection getAllErroneousFiles() + { + ArrayList erroneousFiles = new ArrayList(); + + for(LogoFile file : context.getFilesTable().values()) + if(file.hasErrors()) + erroneousFiles.add(file.getPlainName()); + + return erroneousFiles; + } + + @Override + public boolean hasErrors() + { + for(LogoFile file : context.getFilesTable().values()) + if(file.hasErrors()) + return true; + return false; + } + + @Override + public boolean hasErrors(String fileName) + { + LogoFile file = context.getFilesTable().get(fileName); + if (file == null) + throw new IllegalStateException("The specified fileName does not exist in this context."); + + return file.hasErrors(); + } + + @Override + public Collection getAllErrors() + { + ArrayList allErrors = new ArrayList(); + for (LogoFile file : context.getFilesTable().values()) + allErrors.addAll(file.getAllErrors()); + return allErrors; + } + + // Error listeners + + private final ArrayList errorListeners = new ArrayList(); + + @Override + public void addErrorListener(ErrorListener listener) + { + errorListeners.add(listener); + } + + @Override + public void removeErrorListener(ErrorListener listener) + { + errorListeners.add(listener); + } + + + private void notifyErrorsDetected(String fileName) + { + if (!context.fireFileEvents()) + return; + for (ErrorListener listener : errorListeners) + listener.errorsDetected(fileName); + } + + private void notifyErrorsCorrected(String fileName) + { + if (!context.fireFileEvents()) + return; + for (ErrorListener listener : errorListeners) + listener.allErrorsCorrected(fileName); + } + + +} diff --git a/logo/src/xlogo/kernel/userspace/files/RecordFile.java b/logo/src/xlogo/kernel/userspace/files/RecordFile.java new file mode 100644 index 0000000..9220f28 --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/files/RecordFile.java @@ -0,0 +1,234 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace.files; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; + +import javax.swing.Timer; + +import sun.reflect.generics.reflectiveObjects.NotImplementedException; +import xlogo.Logo; +import xlogo.interfaces.MessageBroadcaster; +import xlogo.messages.async.dialog.DialogMessenger; +import xlogo.storage.WSManager; +import xlogo.storage.user.UserConfig; +import xlogo.utils.Utils; + +/** + * This is a {@link LogoFile} which is used in contest/record mode. + * @author Marko + */ +public class RecordFile extends LogoFile implements MessageBroadcaster +{ + private static final long serialVersionUID = -9137220313285199168L; + + private Timer timer; // the SWING Timer dispatchers on the EventDispatcher Thread => update GUI ok + private Date started; + private Date last; + private long totalMillis; + + /** + * @param fileName + */ + protected RecordFile(String fileName) + { + super(fileName); + } + + public static RecordFile createNewFile(String fileName) throws IOException + { + RecordFile file = new RecordFile(fileName); + file.setupFileSystem(); + return file; + } + + + /** + * @throws NotImplementedException A virtual contest/record mode makes no sense. + */ + public static RecordFile createNewVirtualFile(UserConfig userConfig, String fileName) + { + throw new NotImplementedException(); + } + + @Override + protected void setupFileSystem() throws IOException + { + File contestFileDir = getUserConfig().getContestFileDir(getPlainName()); + + if (!contestFileDir.exists()) + contestFileDir.mkdirs(); + } + + @Override + public File getFilePath() + { + return getUserConfig().getContestFilePath(getPlainName()); + } + + @Override + public void store() + { + long now = Calendar.getInstance().getTime().getTime(); + recordFile(getTimeStampHeader(totalMillis, started.getTime(), now)); + //pauseRecord(); // This is already called by Context at open/close. + // We actually never store normally, and we don't export these files. + } + + /** + * Set the timer + */ + public void startRecord() + { + this.started = Calendar.getInstance().getTime(); + this.last = Calendar.getInstance().getTime(); + + timer = new Timer(1000, + new ActionListener() + { + public void actionPerformed(ActionEvent arg0) + { + Date now = Calendar.getInstance().getTime(); + totalMillis += now.getTime() - last.getTime(); + last = now; + + String time = UserConfig.getMinSec(totalMillis); + String fileName = getPlainName(); + + for(MessageListener listener : timerEventListeners) + listener.messageEvent(fileName, time); + } + } + ); + timer.setRepeats(true); + timer.start(); + } + + /** + * Stop the timer and record recent changes with time stamp in contest directory. + * (Make sure the recent changes from the editor are before calling this) + */ + public void pauseRecord() + { + timer.stop(); + } + + + private void recordFile(final String header) + { + new Thread(new Runnable(){ + + @Override + public void run() + { + // Write to file's folder + File recordFile = getUserConfig().getRecordFilePath(getPlainName()); + File recordFolder = recordFile.getParentFile(); + if (!recordFolder.exists()) + recordFolder.mkdirs(); + + String content = header + getText(); + + try + { + Utils.writeLogoFile(recordFile.toString(), content); + } + catch (IOException e) + { + DialogMessenger.getInstance().dispatchMessage( + Logo.messages.getString("contest.error.title"), + Logo.messages.getString("contest.error.could.not.record.file") + "\n\n " + e.toString()); + } + + // append to command line too ... + PrintWriter out = null; + File logoFile = WSManager.getUserConfig().getCommandLineContestFile(); + try + { + out = new PrintWriter(new BufferedWriter(new FileWriter(logoFile, true))); + out.println(""); + out.println(getPlainName()); + out.println(content); + out.println("\n"); + } + catch (Exception e) + { + DialogMessenger.getInstance().dispatchMessage(Logo.messages.getString("contest.error.title"), + Logo.messages.getString("contest.could.not.store") + "\n" + e.toString()); + } + finally + { + if (out != null) + out.close(); + } + } + }).run(); + + } + + + private String getTimeStampHeader(long totalTime, long lastEditStarted, long lastEditEnded) + { + String tot = UserConfig.getMinSec(totalTime); + String lastStart = UserConfig.getTimeString(lastEditStarted); + String now = UserConfig.getTimeString(lastEditEnded); + + return "# Total Time : " + tot + "\n# Edited from : " + lastStart + "\n# Until : " + now + "\n\n"; + } + + /* + * Timer Listeners + */ + + private final ArrayList timerEventListeners = new ArrayList(); + + @Override + public void addBroadcastListener(MessageListener listener) + { + if(listener == null) + throw new IllegalArgumentException("Listener must not be null."); + timerEventListeners.add(listener); + listener.messageEvent(getPlainName(), UserConfig.getMinSec(totalMillis)); + } + + @Override + public void removeBroadcastListener(MessageListener listener) + { + timerEventListeners.remove(listener); + } + +} diff --git a/logo/src/xlogo/kernel/userspace/procedures/ExecutablesContainer.java b/logo/src/xlogo/kernel/userspace/procedures/ExecutablesContainer.java new file mode 100644 index 0000000..73c7295 --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/procedures/ExecutablesContainer.java @@ -0,0 +1,59 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace.procedures; + +import java.io.IOException; +import java.util.Collection; + +public interface ExecutablesContainer +{ + + public Collection getExecutables(); + + public Procedure getExecutable(String procedureName); + + public boolean isExecutable(String name); + + /** + * The Logo command {@code eraseProcedure}

    + * Delete the specified procedure.
    + * If there exists an ambiguity and multiple procedures with the same name + * are defined, remove all of them. + * + * @param name + */ + public void eraseProcedure(String procedureName); + + /** + * The Logo command {@code define} + * @param procedure + * @throws IOException + */ + public void defineProcedure(Procedure procedure) throws IOException; + +} diff --git a/logo/src/xlogo/kernel/userspace/procedures/ExecutablesProvider.java b/logo/src/xlogo/kernel/userspace/procedures/ExecutablesProvider.java new file mode 100644 index 0000000..6ae20ea --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/procedures/ExecutablesProvider.java @@ -0,0 +1,36 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace.procedures; + +import xlogo.interfaces.ProcedureMapper; +import xlogo.interfaces.ErrorDetector.AmbiguityDetector; + +public interface ExecutablesProvider extends ExecutablesContainer, ProcedureMapper, AmbiguityDetector +{ + +} diff --git a/logo/src/xlogo/kernel/userspace/procedures/Procedure.java b/logo/src/xlogo/kernel/userspace/procedures/Procedure.java new file mode 100644 index 0000000..c485432 --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/procedures/Procedure.java @@ -0,0 +1,763 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were initially written Loïc Le Coq. + * The were heavily refactored, changed and extended by Marko Zivkovic + */ + +/** + * Title : XLogo + * Description : XLogo is an interpreter for the Logo + * programming language + * + * @author Loïc Le Coq + */ +package xlogo.kernel.userspace.procedures; + +import java.util.Calendar; +import java.util.Stack; +import java.util.ArrayList; +import java.util.StringTokenizer; +import java.io.*; + +import xlogo.Logo; +import xlogo.kernel.Primitive; +import xlogo.utils.Utils; + +/** + * The Procedure in XLogo4Schools receives pieces of text, where a single procedure is expected to be defined. + * It parses the text and sets its fields accordingly. + * It maintains states, so we know whether a procedure is executable or whether it has errors and what type of error. + * With the new implementation, multiple types of errors can be detected at the same time. + * + *

    + * Affichable (displayable) is removed. In XLogo4Schools, every procedure is displayable. + * In XLogo, affichable was added to mark procedures that have been received over the network. + * They should only be running while the TCPConnection is open. + * They won't ever get displayed in the editor and they are removed, after the TCP connection is closed. + * In the new implementation, I can treat the received workspace as a virtual file, + * add its procedures to the UserSpace, without adding the file to the file list. + * Hence it cannot be opened while the procedures can still be executed. + * + * @author Marko Zivkovic, Loic + */ +public class Procedure +{ + public static final String specialCharacters = ":+-*/() []=<>&|"; + + /* Marko : TODO the following properties need to be encapsulated completely (Legacy from XLogo) + * I removed those properties that were named _sauve etc. which was used to temporarily redefine procedures, + * and then restore them in case there was an error while parsing. + */ + + // false lorsque c'est une procédure d'un fichier de démarrage + /** + * Whitespace and comments above a procedure in the editor + */ + public String comment; + public int nbparametre; + public String name; + public ArrayList variable = new ArrayList(); + public Stack optVariables = new Stack(); // Marko : why Stack??? [so bad] + public Stack optVariablesExp = new Stack(); // Marko : why Stack?? [so bad] + public String instruction = ""; + public StringBuffer instr = null; + + // Marko : I added these + private String text = null; + private String ownerName = null; + private Calendar defineTime = null; + + /** + * Create a procedure from a piece of text from the editor. + * The expected structure is as follows. + *

    + * [leading empty lines and comments => stored into comment]
    + * to procedureName [variables] [optional variables]
    + * [body]
    + * end
    + * + * @param text + * @throws IOException + */ + public Procedure(String text) + { + try + { + defineTime = Calendar.getInstance(); + this.text = text; + StringReader sr = new StringReader(text); + BufferedReader br = new BufferedReader(sr); + String line = parseComments(br); + if (state == State.COMMENT_ONLY) + return; + StringTokenizer st = new StringTokenizer(line); + parseName(st); + if (errors.contains(ProcedureErrorType.MISSING_TO)) + return; + parseVariables(st); + parseBody(br); + + if (state == State.UNINITIALIZED) + setState(State.EXECUTABLE); + } + catch(IOException ignore) { + /* this should not happen, no actual IO */ + } + } + + /** + * Create a procedure with all necessary values.
    + * If name is not legal, the Procedure state will switch to error, + * otherwise it will be executable + */ + public void redefine(Procedure newDefinition) + { + defineTime = Calendar.getInstance(); + this.name = newDefinition.name; + this.nbparametre = newDefinition.nbparametre; + this.variable = newDefinition.variable; + this.optVariables = newDefinition.optVariables; + this.optVariablesExp = newDefinition.optVariablesExp; + this.text = newDefinition.text; + this.errors = newDefinition.errors; + + ProcedureErrorType e = checkName(name); + if (e != ProcedureErrorType.NO_ERROR) + addError(e); + else + setState(State.EXECUTABLE); + } + + public Calendar getDefineTime() + { + return defineTime; + } + + public String getText() + { + return text; + } + + /* + * PROCEDURE STATE + */ + + private State state = State.UNINITIALIZED; + + private ArrayList errors = new ArrayList(); + + /** + * @see {@link State} + * @return + */ + public State getState() + { + return state; + } + + private void setState(State state) + { + this.state = state; + } + + /** + * Use this to indicate that this procedure is not executable because there is an ambiguity within its file. + */ + public void makeAmbiguous() + { + this.state = State.AMBIGUOUS_NAME; + if (!errors.contains(ProcedureErrorType.AMBIGUOUS)) + errors.add(ProcedureErrorType.AMBIGUOUS); + } + + /** + * States are: UNINITIALIZED, EXECUTABLE, COMMENT_ONLY, ERROR + * @author Marko + */ + public enum State + { + /** + * No values are set, or no check has been performed. + */ + UNINITIALIZED("procedure.unititialized"), + /** + * The procedure has a correct structure and can be executed. + */ + EXECUTABLE("procedure.executable"), + /** + * The procedure contains only a comment, no procedure definition. It is not executable + */ + COMMENT_ONLY("procedure.not.executable"), + /** + * The procedure structure could no be parsed entirely because it contains errors. + */ + ERROR("procedure.error"), + /** + * There is another procedures with the same name in the same file. + */ + AMBIGUOUS_NAME("procedure.ambiguous"); + + private String description; + + private State(String description) + { + this.description = description; + } + + public String getDescription() + { + return description; + } + + /** + * @return {@link #getDescription()} + */ + public String toString() + { + return getDescription(); + } + } + + /* + * ERROR REPORTING + */ + + public ArrayList getErrors() + { + return errors; + } + + private void addError(ProcedureErrorType e) + { + errors.add(e); + state = State.ERROR; + } + + public String getName() + { + return name; + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * OLD XLOGO FEATURES ... TODO behavior not always clear + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + /** + * @author Loïc Le Coq + */ + public void decoupe() + { + // Only cut procedures which are visible in the editor + if (null == instr) + { + instr = new StringBuffer(); + try + { + String line = ""; + StringReader sr = new StringReader(instruction); + BufferedReader bfr = new BufferedReader(sr); + int lineNumber = 0; + // Append the number of the line + instr.append("\\l"); + instr.append(lineNumber); + instr.append(" "); + while (bfr.ready()) + { + lineNumber++; + // read the line + try + { + line = bfr.readLine().trim(); + } + catch (NullPointerException e1) + { + break; + } + // delete comments + line = deleteComments(line); + line = Utils.decoupe(line).toString().trim(); + instr.append(line); + if (!line.equals("")) + { + instr.append(" "); + // Append the number of the line + instr.append("\\l"); + instr.append(lineNumber); + instr.append(" "); + } + } + } + catch (IOException e) + {} + // System.out.println("****************************"+name+"\n"+instr+"\n\n"); + } + } + + /** + * @author Loïc Le Coq + */ + private String deleteComments(String line) + { + int index = line.indexOf("#"); + while (index != -1) + { + if (index == 0) + { + return ""; + } + else if (line.charAt(index - 1) != '\\') + { + return line.substring(0, index); + } + else + index = line.indexOf("#", index + 1); + } + return line; + } + + /** + * @author Loïc Le Coq + */ + public String toString() + { + // return("nom "+name+" nombre paramètres "+nbparametre+" identifiant "+id+"\n"+variable.toString()+"\n"+instr+"\ninstrction_sauve"+instruction_sauve+"\ninstr_sauve\n"+instr_sauve); + StringBuffer sb = new StringBuffer(); + + sb.append(comment); + sb.append(Logo.messages.getString("pour") + " " + name); + for (int j = 0; j < nbparametre; j++) + { + sb.append(" :"); + sb.append(variable.get(j)); + } + for (int j = 0; j < optVariables.size(); j++) + { + sb.append(" [ :"); + sb.append(optVariables.get(j)); + sb.append(" "); + sb.append(optVariablesExp.get(j)); + sb.append(" ]"); + } + sb.append("\n"); + sb.append(instruction); + sb.append(Logo.messages.getString("fin")); + sb.append("\n"); + // System.out.println("a"+sb+"a"); + return new String(sb); + } + + /** + * @author Loïc Le Coq + */ + public StringBuffer cutInList() + { + // Only cut procedures which are visible in the editor + StringBuffer sb = new StringBuffer(); + try + { + String line = ""; + StringReader sr = new StringReader(instruction); + BufferedReader bfr = new BufferedReader(sr); + while (bfr.ready()) + { + try + { + line = bfr.readLine().trim(); + } + catch (NullPointerException e1) + { + break; + } + // delete comments + // line=deleteComments(line); + line = Utils.decoupe(line).toString().trim(); + sb.append("[ "); + sb.append(line); + sb.append(" ] "); + } + } + catch (IOException e) + {} + return sb; + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * PARSING + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + /** + * Read and save the comments that appear before the procedure + * @param br + * @return the first line that (probably) contains a procedure definition, + * or null if there are no more procedures + * @throws IOException + * @throws DocumentStructureException + */ + String parseComments(BufferedReader br) throws IOException + { + String line = null; + comment = ""; + while (br.ready()) + { + line = br.readLine(); + if (line == null) + break; + + if (isComment(line)) + { + comment += line + "\n"; + line = null; + } + else if (line.trim().equals("")) + { + comment += "\n"; + line = null; + } + else + break; + } + + if (line == null) + { + setState(State.COMMENT_ONLY); + } + + return line; + } + + /** + * Expects a line that starts with "to procedName" + * @return Error, NO_ERROR if name is ok + * @throws DocumentStructureException + */ + void parseName(StringTokenizer st) + { + String token = st.nextToken(); + + if (!token.toLowerCase().equals(Logo.messages.getString("pour").toLowerCase())) + { + addError(ProcedureErrorType.MISSING_TO); + return; + } + + if (!st.hasMoreTokens()) + { + addError(ProcedureErrorType.MISSING_NAME); + return; + } + + name = st.nextToken().toLowerCase(); + + ProcedureErrorType e = checkName(name); + if (!e.equals(ProcedureErrorType.NO_ERROR)) + addError(e); + } + + void parseVariables(StringTokenizer st) + { + // Then, We read the variables + // :a :b :c :d ..... + String token = null; + while (st.hasMoreTokens()) + { + token = st.nextToken(); + if (token.startsWith(":")) + { + String var = token.substring(1); + ProcedureErrorType e = checkValidVariable(var); + if (e != ProcedureErrorType.NO_ERROR) + { + addError(e); + return; + } + var = var.toLowerCase(); + variable.add(var); + } + else + break; + } + + nbparametre = variable.size(); + + if (token == null || token.startsWith(":")) + return; + + // read all remaining tokens into string buffer + StringBuffer sb = new StringBuffer(); + sb.append(token); + //sb.append(token); + while (st.hasMoreTokens()) + { + sb.append(" "); + sb.append(st.nextToken()); + } + // And finally, optional variables if there are some. + // [:a 100] [:b 20] [:c 234] ........... + + while (sb.length() > 0) + { + if (sb.indexOf("[") != 0) + { + addError(ProcedureErrorType.VAR_EXTRA_CHARS); + return; + } + + sb.deleteCharAt(0); + String[] arg = new String[2]; + extractOptionalVariable(sb, arg); + optVariables.push(arg[0].toLowerCase()); + /* Bug Fixed: list as Optionnal arguments + ** Eg: + ** to a [:var [a b c]] + * end + * when the string is formatted, we check that a white space + * is needed at the end of the argument + */ + + StringBuffer exp = Utils.decoupe(arg[1]); + if (exp.charAt(exp.length() - 1) != ' ') + exp.append(" "); + optVariablesExp.push(exp); + } + } + + /** + * Reads from sb into args the name and the default value of an optional variable [:a value].
    + * value can be expression: number, word, list, ... + * @author Loïc Le Coq + * @author Marko Zivkovic refactored + */ + void extractOptionalVariable(StringBuffer sb, String[] args) + { + + String variable=""; + String expression=""; + int compteur=1; + int id=0; + int id2=0; + boolean espace = false; + for (int i = 0; i < sb.length(); i++) + { + char ch = sb.charAt(i); + if (ch == '[') + compteur++; + else if (ch == ']') + { + if (id == 0) + { + addError(ProcedureErrorType.OPT_VAR_BRACKET); + return; + } + compteur--; + } + else if (ch == ' ') + { + if (!variable.equals("")) + { + if (!espace) + id = i; + espace = true; + } + } + else + { + if (!espace) + variable += ch; + } + if (compteur == 0) + { + id2 = i; + break; + } + } + if (!variable.startsWith(":")) + { + addError(ProcedureErrorType.VAR_COLON_EXPECTED); + return; + } + + variable = variable.substring(1, variable.length()).toLowerCase(); + ProcedureErrorType pet = checkValidVariable(variable); + + if (pet != ProcedureErrorType.NO_ERROR) + { + addError(pet); + return; + } + + + if (compteur != 0) + { + addError(ProcedureErrorType.OPT_VAR_BRACKET); + return; + } + + expression = sb.substring(id + 1, id2).trim(); + + if (expression.equals("")) + { + addError(ProcedureErrorType.VAR_MISSING_EXPRESSION); + return; + } + /* System.out.println(id+" "+id2); + System.out.println("a"+expression+"a"); + System.out.println("a"+variable+"a");*/ + sb.delete(0, id2 + 1); + // delete unnecessary space + while (sb.length() != 0 && sb.charAt(0) == ' ') + sb.deleteCharAt(0); + args[0] = variable; + args[1] = expression; + } + + /** + * Sets as body everything up the 'end' key word. + */ + private void parseBody(BufferedReader br) throws IOException + { + StringBuffer body = new StringBuffer(); + String to = Logo.messages.getString("pour").toLowerCase() + " "; + String end = Logo.messages.getString("fin").toLowerCase(); + instruction = null; + String line; + String lower; + + while (br.ready()) + { + line = br.readLine(); + if (null == line) + break; + lower = line.toLowerCase(); + if (lower.trim().equals(end)) // end of procedure + { + instruction = body.toString(); + if (br.ready() && br.readLine() != null) // Additional characters after end + addError(ProcedureErrorType.CHARS_AFTER_END); + return; // We are done + } + if (lower.startsWith(to)) // new procedure definition before end + { + addError(ProcedureErrorType.TO_BEFORE_END); + return; + } + body.append(line); + body.append("\n"); + } + + // no end was found + addError(ProcedureErrorType.MISSING_END); + + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * UTILITY + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + /** + * @return true if and only if line starts with '#' + */ + static boolean isComment(String line) + { + if (line.trim().startsWith("#")) + return true; + else + return false; + } + + static boolean isNumber(String token) + { + try + { + Double.parseDouble(token); + return true; + } + catch (NumberFormatException e) + { + return false; + } + } + + /** + * This method is essentially changed to fit with the new workspace (=UserSpace & LogoFile).
    + * On the fly it was rewritten completely, because of its initial programming style and inefficiency.
    + * (note that it was very long, and now it is only 3 lines) + * + * @param token + * @return Error - NO_ERROR if token is a legal procedure name + * @author Marko Zivkovic + */ + static ProcedureErrorType checkName(String token) + { + if (isNumber(token)) + return ProcedureErrorType.NAME_IS_NUMBER; + ProcedureErrorType e = checkSpecialCharacter(token); + if (e != ProcedureErrorType.NO_ERROR) + return e; + + if(Primitive.primitives.containsKey(token)) + return ProcedureErrorType.NAME_IS_KEY_WORD; + + return ProcedureErrorType.NO_ERROR; + } + + /** + * @param token + * @return null if a valid variable name, error message otherwise + */ + static ProcedureErrorType checkValidVariable(String token) + { + if (token.length() == 0) + return ProcedureErrorType.VAR_WHITESPACE_AFTER_COLON; + + if (isNumber(token)) + return ProcedureErrorType.VAR_IS_NUMBER; + + return checkSpecialCharacter(token); + } + + static ProcedureErrorType checkSpecialCharacter(String var) + { + StringTokenizer check = new StringTokenizer(var, specialCharacters, true); + + if ((check.countTokens() > 1) || ":+-*/() []=<>&|".indexOf(check.nextToken()) > -1) + return ProcedureErrorType.VAR_HAS_SPECIAL; + return ProcedureErrorType.NO_ERROR; + } + + /* + * OWNER : This is typically a file, but a procedure must not know whether it is owned by a file or something else + */ + + public String getOwnerName() + { + return ownerName; + } + + public void setOwnerName(String ownerName) + { + this.ownerName = ownerName; + } + +} diff --git a/logo/src/xlogo/kernel/userspace/procedures/ProcedureErrorType.java b/logo/src/xlogo/kernel/userspace/procedures/ProcedureErrorType.java new file mode 100644 index 0000000..3029320 --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/procedures/ProcedureErrorType.java @@ -0,0 +1,80 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace.procedures; + +import xlogo.messages.MessageKeys; + +public enum ProcedureErrorType +{ + NO_ERROR(MessageKeys.LOGO_ERROR_NO_ERROR), + MISSING_TO(MessageKeys.LOGO_ERROR_MISSING_TO), + MISSING_NAME(MessageKeys.LOGO_ERROR_MISSING_NAME), + NAME_IS_NUMBER(MessageKeys.LOGO_ERROR_NAME_IS_NUMBER), //Logo.messages.getString("erreur_nom_nombre_procedure") + /* + * Logo.messages.getString("caractere_special1") + "\n" + Logo.messages.getString("caractere_special2") + * + "\n" + Logo.messages.getString("caractere_special3") + " " + token + */ + NAME_HAS_SPECIAL_CHAR(MessageKeys.LOGO_ERROR_NAME_SPECIAL), + VAR_WHITESPACE_AFTER_COLON(MessageKeys.LOGO_ERROR_VAR_WHITE_AFTER_COLON), + /* + * Logo.messages.getString("caractere_special_variable") + "\n" + + Logo.messages.getString("caractere_special2") + "\n" + + Logo.messages.getString("caractere_special3") + " :" + var + */ + VAR_HAS_SPECIAL(MessageKeys.LOGO_ERROR_VAR_SPECIAL), + VAR_IS_NUMBER(MessageKeys.LOGO_ERROR_VAR_IS_NUMBER), + VAR_COLON_EXPECTED(MessageKeys.LOGO_ERROR_VAR_MISSING_COLON), + OPT_VAR_BRACKET(MessageKeys.LOGO_ERROR_OPT_VAR_BRACKET), + VAR_EXTRA_CHARS(MessageKeys.LOGO_ERROR_VAR_EXTRA_CHARS), + VAR_MISSING_EXPRESSION(MessageKeys.LOGO_ERROR_MISSING_EXPR), + CHARS_AFTER_END(MessageKeys.LOGO_ERROR_MORE_CHARS_AFTER_END), + MISSING_END(MessageKeys.LOGO_ERROR_MISSING_END), + TO_BEFORE_END(MessageKeys.LOGO_ERROR_TO_BEFORE_END), + NAME_IS_KEY_WORD(MessageKeys.LOGO_ERROR_NAME_IS_KEY_WORD), + AMBIGUOUS(MessageKeys.LOGO_ERROR_AMBIGUOUS); + + private String description; + + private ProcedureErrorType(String description) + { + this.description = description; + } + + public String getDescription() + { + return description; + } + + /** + * @return {@link #getDescription()} + */ + public String toString() + { + return getDescription(); + } +} \ No newline at end of file diff --git a/logo/src/xlogo/kernel/userspace/procedures/ProceduresManager.java b/logo/src/xlogo/kernel/userspace/procedures/ProceduresManager.java new file mode 100644 index 0000000..bcaea16 --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/procedures/ProceduresManager.java @@ -0,0 +1,557 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * 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; either version 2 of the License, or (at your option) + * any later version. 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace.procedures; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import xlogo.kernel.userspace.ProcedureErrorMessage; +import xlogo.kernel.userspace.context.ContextSwitcher; +import xlogo.kernel.userspace.context.LogoContext; +import xlogo.kernel.userspace.context.ContextSwitcher.ContextSwitchListener; +import xlogo.kernel.userspace.files.LogoFile; +import xlogo.kernel.userspace.procedures.Procedure.State; + +/** + * This class maintains the procedure table of all files in a context, and it reports ambiguity among them. + * @author Marko Zivkovic + */ +public class ProceduresManager implements ExecutablesProvider +{ + private LogoContext context; + + private final ArrayList ambiguityListeners = new ArrayList(); + + private ProcedureMapListener procedureMapListener; + + public ProceduresManager(ContextSwitcher contextProvider) + { + initProcedureMapListener(); + installContextSwitchListener(contextProvider); + setContext(contextProvider.getContext()); + } + + private void setContext(LogoContext newContext) + { + LogoContext old = this.context; + this.context = newContext; + + if (old != null) + old.removeProcedureMapListener(procedureMapListener); + + mapProcedures(newContext); + + /* Record Mode => new files => events + // Network Mode => only virtual files, invisible => no events + // that was achieved with the boolean "affichable" (displayable) in XLogo + if (!newContext.fireProcedureEvents()) + { + + return; + } + */ + if (old != null && old.fireProcedureEvents() && newContext.fireProcedureEvents()) // TODO changed + for(LogoFile file : old.getFilesTable().values()) + notifyDeleted(file.getPlainName(), file.getAllProcedureNames()); + + // Note : context will immediately fire event to notify what his contents are. + // Then these events are forwarded here, in procedureMapListener. + newContext.addProcedureMapListener(procedureMapListener); + } + + private void mapProcedures(LogoContext context) + { + context.getProcedureTable().clear(); + for (LogoFile file : context.getFilesTable().values()) + for (Procedure procedure : file.getExecutables()) + addProcedure(procedure); + } + + private void installContextSwitchListener(ContextSwitcher provider) + { + provider.addContextSwitchListener(new ContextSwitchListener(){ + + @Override + public void contextSwitched(LogoContext newContext) + { + setContext(newContext); + } + }); + } + + private void initProcedureMapListener() + { + // This one listens for changes in files. + procedureMapListener = new ProcedureMapListener() + { + /** + * This event is received when a document is assigned a new text and it contains no errors, + * or when this ProcedureManager is assigned a new context that already contains files. + */ + @Override + public void defined(String fileName, Collection procedures) + { + for (String procedureName : procedures) + addProcedure(fileName, procedureName); + + if (procedures.size() > 0) + notifyDefined(fileName, procedures); + + for (String procedureName : procedures) + { + Map fileNameToProcedure = context.getProcedureTable().get(procedureName); + if (fileNameToProcedure.size() > 1) + { + notifyAmbiguityDetected(procedureName, context.getProcedureTable().get(procedureName)); + } + } + + } + + /** + * This event is received when the Logo command "define" (re-)defined a command in a file (which already exists!) + */ + @Override + public void defined(String fileName, String procedureName) + { + addProcedure(fileName, procedureName); + + notifyDefined(fileName, procedureName); + + // Check ambiguity + Map fileNameToProcedure = context.getProcedureTable().get(procedureName); + if (fileNameToProcedure.size() > 1) + { + notifyAmbiguityDetected(procedureName, fileNameToProcedure); + return; + } + } + + /** + * This event is received when a file is deleted or when it is assigned a new text in which old procedures are missing. + */ + @Override + public void undefined(String fileName, Collection procedures) + { + for (String procedure : procedures) + undefined(fileName, procedure); + } + + /** + * This event is received when the Logo command "eraseProcedure" removes a single procedure from a document + * Depending on how many procedures with the same name are left across files after this procedure is removed, + * this will fire ExecutableChanged events or AmbiguityResolved events. + */ + @Override + public void undefined(String fileName, String procedureName) + { + Map> procedureTable = context.getProcedureTable(); + + Map fileToProc = procedureTable.get(procedureName); + + if (fileToProc == null) + return; + + // remove from fileToProc entry + fileToProc.remove(fileName); + + notifyDeleted(fileName, procedureName); + + if (fileToProc.size() == 0) + procedureTable.remove(procedureName); + else + notifyAmbiguityResolved(procedureName, fileName); + + if (fileToProc.size() == 1) + { + String winnerFile = null; + for (String wf : fileToProc.keySet()) + winnerFile = wf; // size == 1 => only 1 iteration + + notifyAmbiguityResolved(procedureName, winnerFile); + } + } + + @Override + public void ownerRenamed(String oldName, String newName) + { + // Note : very important that this event is catched after filemanager has renamed files + // very critical. but implementation guarantees this order. + LogoFile file = context.getFilesTable().get(newName); + // Rename keys in procedureTable + Map fileToProc; + for (Procedure proc : file.getExecutables()) + { + fileToProc = context.getProcedureTable().get(proc.getName()); + fileToProc.remove(oldName); + fileToProc.put(newName, proc); + } + } + }; + } + + /** + * @see #addProcedure(Procedure) + */ + protected void addProcedure(String fileName, String procedureName) throws IllegalArgumentException + { + Procedure procedure = context.getFilesTable().get(fileName).getExecutable(procedureName.toLowerCase()); + addProcedure(procedure); + } + + /** + * Adds the procedure to the procedureTable without firing events. + * Use {@link #defineProcedure(Procedure)}} if you want to notify listeners + * @param procedure + * @throws IllegalArgumentException + */ + protected void addProcedure(Procedure procedure) throws IllegalArgumentException + { + String procedureName = procedure.getName(); + + if (procedure.getState() != State.EXECUTABLE && procedure.getState() != State.AMBIGUOUS_NAME) + throw new IllegalArgumentException("Only executable procedures should be added to the context."); + + HashMap> procedureTable = context.getProcedureTable(); + HashMap fileToProc = procedureTable.get(procedureName); + + if (fileToProc == null) // The procedure name is not yet defined in the context. + { + // Must create fileToProc entry in procedureTable + fileToProc = new HashMap(); + procedureTable.put(procedureName, fileToProc); + } + + fileToProc.put(procedure.getOwnerName(), procedure); + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * EXECUTABLES CONTAINER + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + /** + * @param {@link Procedure#getOwnerName()} is supposed to be already mapped in the filesTable, and the procedure should be executable + * @throws IllegalArgumentException if the procedure's owner name is not mapped; + * @see {@link LogoFile#defineProcedure(Procedure)} + */ + @Override + public void defineProcedure(Procedure procedure) + { + LogoFile file = context.getFilesTable().get(procedure.getOwnerName()); + if (file == null) + throw new IllegalArgumentException("The file name \"" + + procedure.getOwnerName() + + "\" specified in procedure is not mapped to a file."); + + //addProcedure(procedure); + file.defineProcedure(procedure); // this will cause the necessary event cascade + } + + /** + * Erase + */ + @Override + public void eraseProcedure(String procedureName) + { + String lower = procedureName.toLowerCase(); + while(context.getProcedureTable().containsKey(lower)) + { + // contains key => there must be one + Procedure nextVictim = null; + for (Procedure proc : context.getProcedureTable().get(lower).values()) + { + // iterate only once : otherwise concurrent modification exception + nextVictim = proc; + break; + } + context.getFilesTable().get(nextVictim.getOwnerName()).eraseProcedure(nextVictim.getName()); + } + } + + /** + * Executable procedures of all files. Ambiguous procedures are not included.
    + * This list should not be used to do computations on it, because it must be created every time. + * Its purpose is to feed GUI lists + */ + @Override + public ArrayList getExecutables() + { + ArrayList executables = new ArrayList(); + for(Map fileToProc : context.getProcedureTable().values()) + if (fileToProc.size() == 1) // only one exists + for(Procedure proc : fileToProc.values()) + executables.add(proc); // only one is added + return executables; + } + + /** + * @return The specified procedure if and only if there is exactly one + * procedure definition for this name in the context, otherwise null. + */ + @Override + public Procedure getExecutable(String procedureName) + { + Map fileToProc = context.getProcedureTable().get(procedureName.toLowerCase()); + + if (fileToProc == null || fileToProc.size() > 1) + return null; + // There is exactly 1 procedure + for (Procedure proc : fileToProc.values()) + return proc; + // Code won't reach here + return null; + } + + /** + * @return true if and only if there exists exactly one procedure definition for the specified name in this context + */ + @Override + public boolean isExecutable(String procedureName) + { + Map fileToProc = context.getProcedureTable().get(procedureName.toLowerCase()); + return (fileToProc != null && fileToProc.size() == 1); + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * PROCEDURE MAPPER + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + /** + * Get all executable procedure names + * @see #getExecutables() + */ + @Override + public Collection getAllProcedureNames() + { + ArrayList executables = new ArrayList(); + for(Map fileToProc : context.getProcedureTable().values()) + if (fileToProc.size() == 1) // only one exists + for(Procedure proc : fileToProc.values()) + executables.add(proc.getName()); // only one is added + return executables; + } + + /** + * Get all procedures of that file, if its procedures are not conflicting with other files' procedures. + */ + @Override + public Collection getAllProcedureNames(String fileName) + { + if (!hasAmbiguousProcedures(fileName)) + return context.getFilesTable().get(fileName).getAllProcedureNames(); + return null; + } + + /** + * @return null if procedure does not exist or if it is ambiguous + */ + @Override + public String getProcedureOwner(String procedureName) + { + Procedure procedure = getExecutable(procedureName.toLowerCase()); + return procedure == null ? null : procedure.getOwnerName(); + } + + // Procedure Map Listeners : update gui => run on event dispatcher thread + + private final ArrayList procedureMapListeners = new ArrayList(); + + @Override + public void addProcedureMapListener(ProcedureMapListener listener) + { + procedureMapListeners.add(listener); + } + + @Override + public void removeProcedureMapListener(ProcedureMapListener listener) + { + procedureMapListeners.remove(listener); + } + + protected void notifyDefined(final String fileName, final Collection procedures) + { + if (!context.fireProcedureEvents()) + return; + if (procedures.size() == 0) + return; + for (ProcedureMapListener listener : procedureMapListeners) + listener.defined(fileName, procedures); + } + + protected void notifyDefined(final String fileName, final String procedure) + { + if (!context.fireProcedureEvents()) + return; + for (ProcedureMapListener listener : procedureMapListeners) + listener.defined(fileName, procedure); + + } + + protected void notifyDeleted(final String fileName, final Collection procedures) + { + if (!context.fireProcedureEvents()) + return; + if (procedures.size() == 0) + return; + + for (ProcedureMapListener listener : procedureMapListeners) + listener.undefined(fileName, procedures); + } + + protected void notifyDeleted(final String fileName, final String procedure) + { + if (!context.fireProcedureEvents()) + return; + for (ProcedureMapListener listener : procedureMapListeners) + listener.undefined(fileName, procedure); + } + + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * AMBIGUITY DETECTOR + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + /** + * @return Whether there exists a procedure name that is defined in more + * than one file in the UserSpace + */ + @Override + public boolean hasErrors() + { + for (Entry> entry : context.getProcedureTable().entrySet()) + { + if (entry.getValue().size() > 1) + return true; + } + return false; + } + + /** + * Returns all ambiguity problems + */ + @Override + public Collection getAllErrors() + { + ArrayList ambiguities = new ArrayList(); + + for (Entry> entry : context.getProcedureTable().entrySet()) + { + HashMap fileToProc = entry.getValue(); + + if (fileToProc.size() < 2) + continue; + + ambiguities.add( + new ProcedureErrorMessage( + ProcedureErrorType.AMBIGUOUS, + entry.getKey(), + fileToProc.keySet() + ) + ); + } + + return ambiguities; + } + + @Override + public Collection getAllConflictingFiles() + { + ArrayList conflictingFiles = new ArrayList(); + + for (Entry> entry : context.getProcedureTable().entrySet()) + { + HashMap fileToProc = entry.getValue(); + + if (fileToProc.size() < 2) + continue; + + conflictingFiles.addAll(fileToProc.keySet()); + } + + return conflictingFiles; + } + + @Override + public boolean hasAmbiguousProcedures(String fileName) + { + for (Procedure proc : context.getFilesTable().get(fileName).getExecutables()) + { + if(isProcedureAmbiguous(proc.getName())) + return true; + } + return false; + } + + @Override + public boolean isProcedureAmbiguous(String name) + { + Map ambigs = context.getProcedureTable().get(name.toLowerCase()); + if (ambigs == null) + return false; + return ambigs.size() > 1; + } + + + @Override + public void addAmbiguityListener(AmbiguityListener listener) + { + ambiguityListeners.add(listener); + } + + @Override + public void removeAmbiguityListener(AmbiguityListener listener) + { + ambiguityListeners.remove(listener); + } + + private void notifyAmbiguityDetected(String procedureName, Map fileToProcedure) + { + if(!context.fireProcedureEvents()) + return; + for (AmbiguityListener listener : ambiguityListeners) + listener.ambiguityDetected(procedureName, fileToProcedure); + } + + private void notifyAmbiguityResolved(String procedureName, String winnerFile) + { + if(!context.fireProcedureEvents()) + return; + for (AmbiguityListener listener : ambiguityListeners) + listener.ambiguityResolved(procedureName, winnerFile); + } + + + + +} -- cgit v1.2.3