summaryrefslogtreecommitdiffstats
path: root/logo/src/xlogo/kernel/userspace
diff options
context:
space:
mode:
authorMarko Živković <[email protected]>2014-06-11 10:10:33 +0000
committerMarko Živković <[email protected]>2014-06-11 10:10:33 +0000
commit1107aa0763e3d7554408c401d2a1dbed11a94c51 (patch)
tree7074264bc7b63f2ee5ee14a39458380fcce1904b /logo/src/xlogo/kernel/userspace
Add initial directories and files
git-svn-id: https://svn.code.sf.net/p/xlogo4schools/svn/trunk@1 3b0d7934-f7ef-4143-9606-b51f2e2281fd
Diffstat (limited to 'logo/src/xlogo/kernel/userspace')
-rw-r--r--logo/src/xlogo/kernel/userspace/ErrorManager.java201
-rw-r--r--logo/src/xlogo/kernel/userspace/GlobalVariableTable.java73
-rw-r--r--logo/src/xlogo/kernel/userspace/ProcedureErrorMessage.java106
-rw-r--r--logo/src/xlogo/kernel/userspace/PropertyListTable.java165
-rw-r--r--logo/src/xlogo/kernel/userspace/UserSpace.java790
-rw-r--r--logo/src/xlogo/kernel/userspace/context/ContextManager.java301
-rw-r--r--logo/src/xlogo/kernel/userspace/context/ContextSwitcher.java43
-rw-r--r--logo/src/xlogo/kernel/userspace/context/LogoContext.java312
-rw-r--r--logo/src/xlogo/kernel/userspace/context/NetworkContext.java206
-rw-r--r--logo/src/xlogo/kernel/userspace/context/RecordContext.java146
-rw-r--r--logo/src/xlogo/kernel/userspace/context/UserContext.java191
-rw-r--r--logo/src/xlogo/kernel/userspace/files/LogoFile.java755
-rw-r--r--logo/src/xlogo/kernel/userspace/files/LogoFileContainer.java39
-rw-r--r--logo/src/xlogo/kernel/userspace/files/LogoFilesManager.java560
-rw-r--r--logo/src/xlogo/kernel/userspace/files/RecordFile.java234
-rw-r--r--logo/src/xlogo/kernel/userspace/procedures/ExecutablesContainer.java59
-rw-r--r--logo/src/xlogo/kernel/userspace/procedures/ExecutablesProvider.java36
-rw-r--r--logo/src/xlogo/kernel/userspace/procedures/Procedure.java763
-rw-r--r--logo/src/xlogo/kernel/userspace/procedures/ProcedureErrorType.java80
-rw-r--r--logo/src/xlogo/kernel/userspace/procedures/ProceduresManager.java557
20 files changed, 5617 insertions, 0 deletions
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<String, Void> errorFiles = new HashMap<String, Void>();
+ private final HashMap<String, Collection<String>> ambiguousProcToFiles = new HashMap<String, Collection<String>>();
+
+ private final ArrayList<ErrorListener> errorListeners = new ArrayList<ErrorListener>();
+
+ 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<String> 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<String> 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<String> fileNames : ambiguousProcToFiles.values())
+ {
+ if (fileNames.contains(fileName))
+ return;
+ }
+ // No more ambiguities for file
+
+ notifyAllErrorsCorrected(fileName);
+ }
+
+ @Override
+ public void ambiguityDetected(String procedureName, Map<String, Procedure> fileToProcedure)
+ {
+ ambiguousProcToFiles.put(procedureName, new ArrayList<String>(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<String> conflictingFiles : ambiguousProcToFiles.values())
+ if (conflictingFiles.contains(fileName))
+ return true;
+ return false;
+ }
+
+ @Override
+ public Collection<ProcedureErrorMessage> getAllErrors()
+ {
+ ArrayList<ProcedureErrorMessage> allErrorMessages = new ArrayList<ProcedureErrorMessage>();
+ // Not the most efficient impl possible
+ allErrorMessages.addAll(fileErrorDetector.getAllErrors());
+ allErrorMessages.addAll(ambiguityDetector.getAllErrors());
+ return allErrorMessages;
+ }
+
+ @Override
+ public Collection<String> getAllErroneousFiles()
+ {
+ TreeMap<String, Void> allErrorFiles = new TreeMap<String, Void>();
+
+ for(String fileName : errorFiles.keySet())
+ allErrorFiles.put(fileName, null);
+
+ for(Collection<String> files : ambiguousProcToFiles.values())
+ for(String fileName : files)
+ allErrorFiles.put(fileName, null);
+
+ return allErrorFiles.keySet();
+ }
+
+ public Collection<String> getErrorMessages(String fileName)
+ {
+ ArrayList<String> messages = new ArrayList<String>();
+
+ 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<String, String> globale;
+
+ public GlobalVariableTable()
+ {
+ globale = new HashMap<String, String>();
+ }
+
+ public Set<String> 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 <String> 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<String>();
+ this.fileNames.add(fileName);
+ }
+
+ /**
+ * This can be used for ambiguity messages
+ * @see #ProcedureErrorMessage(ProcedureErrorType, String, String)
+ */
+ public ProcedureErrorMessage(ProcedureErrorType type, String procedureDescription, Collection<String> fileNames)
+ {
+ this.type = type;
+ this.procedureDescription = procedureDescription;
+ this.fileNames = fileNames;
+ }
+
+ public ProcedureErrorType getErrorType()
+ {
+ return type;
+ }
+
+ public String getProcedureDescription()
+ {
+ return procedureDescription;
+ }
+
+ public Collection<String> 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<String, HashMap<String, String>> propList;
+
+ public PropertyListTable()
+ {
+ propList = new HashMap<String, HashMap<String,String>>();
+ }
+
+
+ /**
+ * 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<String, String>());
+ }
+ 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<String> 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<String> 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:
+ * <p>
+ * - All Defined Procedures <br>
+ * - All Global variables <br>
+ * - All GUI Objects (generated by a Logo program)<br>
+ * - All Property lists
+ * <p>
+ * The new "Workspace" does not resemble the old workspace at all, although it still provides access to these symbol tables.
+ * <p>
+ * New in XLogo4Schools: multiple Files. Procedures must be unique within all files of a context.
+ * If multiple procedures with the same name are defined <br>
+ * - within one file : The file will not be usable, until errors and ambiguities are fixed (like in XLogo), <br>
+ * - among multiple files : All of these files will not be executable, <br> //TODO or the ambiguous procedures will not be executed
+ * unless the ambiguity is resolved.
+ * <p>
+ * 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.
+ * <p>
+ * (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.
+ * <p>
+ * 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. <p>
+ * If the procedure is ambiguous, cannot decide which one to redefine => IllegalArgumentException <br>
+ * If the procedure is already defined once, that definition will be redefined in its original LogoFile. <br>
+ * 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<Procedure> getExecutables()
+ {
+ try
+ {
+ return proceduresManager.getExecutables();
+ }
+ catch (Exception e) { showErrorDialog(e); }
+ return new ArrayList<Procedure>();
+ }
+
+ @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<String> getAllProcedureNames()
+ {
+ try
+ {
+ return proceduresManager.getAllProcedureNames();
+ }
+ catch (Exception e) { showErrorDialog(e); }
+ return new ArrayList<String>();
+ }
+
+ @Override
+ public Collection<String> getAllProcedureNames(String fileName)
+ {
+ try
+ {
+ return proceduresManager.getAllProcedureNames(fileName);
+ }
+ catch (Exception e) { showErrorDialog(e); }
+ return new ArrayList<String>();
+ }
+
+ @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<String> getAllConflictingFiles()
+ {
+ try
+ {
+ return proceduresManager.getAllConflictingFiles();
+ }
+ catch (Exception e) { showErrorDialog(e); }
+ return new ArrayList<String>();
+ }
+
+ @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<ProcedureErrorMessage> getAllErrors()
+ {
+ try
+ {
+ return errorManager.getAllErrors();
+ }
+ catch (Exception e) { showErrorDialog(e); }
+ return new ArrayList<ProcedureErrorMessage>();
+ }
+
+ @Override
+ public Collection<String> getAllErroneousFiles()
+ {
+ try
+ {
+ return errorManager.getAllErroneousFiles();
+ }
+ catch (Exception e) { showErrorDialog(e); }
+ return new ArrayList<String>();
+
+ }
+
+ @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<String> getErrorMessages(String fileName)
+ {
+ try
+ {
+ return errorManager.getErrorMessages(fileName);
+ }
+ catch (Exception e) { showErrorDialog(e); }
+ return new ArrayList<String>();
+ }
+
+}
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} <br>
+ * <b> The Contexts and Priorities and multiplicity</b>
+ * 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<NetworkContext> networkStack = new Stack<NetworkContext>();
+
+ 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.<br>
+ * It notifies ContextProviderListeners if the context has actually changed after calling this.
+ * <p>
+ * The policy for context switches is defines in the interface here : {@link X4SModeSwitcher}
+ * <p>
+ * 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<ContextSwitchListener> contextSwitchListeners = new ArrayList<ContextSwitcher.ContextSwitchListener>();
+
+ @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 <b>record or network</b> 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. <br>
+ * Afterwards it will kill the current record context. <br>
+ * 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<ModeChangeListener> modeChangeListeners = new ArrayList<ModeChangeListener>();
+
+ @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<MessageListener> broadcastListeners = new ArrayList<MessageListener>();
+
+ @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 <p>
+ * 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.
+ * <p>
+ * 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<String, LogoFile> files = new HashMap<String, LogoFile>();
+
+ private final HashMap<String, HashMap<String, Procedure>> procedureTable = new HashMap<String, HashMap<String, Procedure>>();
+
+ // private Map<String, Procedure> executables = new HashMap<String,
+ // Procedure>();
+
+ private final GuiMap guiMap = new GuiMap();
+
+ public LogoContext() { }
+
+ /*
+ * Symbol table getters.
+ */
+
+ /**
+ * All the files in this context
+ * @return
+ */
+ public Map<String, LogoFile> getFilesTable()
+ {
+ return files;
+ }
+
+ /**
+ * All Executable procedures from all files. Procedures with equal names,
+ * from different files, will be included in the same list. <br>
+ * This Map is used to keep track of all defined procedures and to resolve
+ * name conflicts.
+ */
+ public HashMap<String, HashMap<String, Procedure>> 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> procedureMapListener = new ArrayList<ProcedureMapListener>();
+
+ /**
+ * 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<String, Procedure> 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. <br>
+ * 1. It parses its contents from a serialized context string ({@link #toString()}) <br>
+ * 2. Its files are created virtually ({@link xlogo.storage.Storable#isVirtual()}). They are gone when the network mode stops, leaving no trace. <br>
+ * 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.
+ * <p>
+ * 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.<br>
+ *
+ * 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.<br>
+ * <p>
+ * 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.
+ * <p>
+ * <p>
+ * 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.
+ * <p>
+ * In the current implementation, "affichable" is completely removed. <br>
+ * I regulate this effect by either firing events or not.
+ * <p>
+ * 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
+ * <p>
+ * 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.
+ * <p>
+ * 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<MessageListener> timerEventListeners = new ArrayList<MessageListener>();
+
+ 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. <br>
+ * This happens only once, in the constructor. <br>
+ * 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<String>(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<String> list = new ArrayList<String>(WSManager.getUserConfig().getFileOrder());
+ Map<String,LogoFile> filesTable = getFilesTable();
+
+ if (filesTable.size() != list.size())
+ {
+ Iterator<String> 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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<String, Procedure> 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<Procedure> 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<String, Procedure>();
+ allProcedures = new ArrayList<Procedure>();
+ }
+
+ 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, <br>
+ * 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<String> procedures = new ArrayList<String>(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<File>(){
+ 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
+ * <p>
+ * In XLogo, this was {@code Editor.analyseprocedure()}
+ * <p><p>
+ * <b>Refactored:</b><br>
+ * Initially all that happens in
+ * <li> {@link #setText(String)}
+ * <li> {@link #parseText(BufferedReader)}
+ * <li> {@link #nextProcedure(BufferedReader)}
+ * <li> {@link Procedure#Procedure() }
+ * <li> {@link #untilEnd() }<br>
+ * 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.<p>
+ * 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.
+ *
+ * <p><p>
+ * <b>New Mechanism:</b><br>
+ * In XLogo, as soon as an error was found in the document, an exception was thrown and displayed to the user. <br>
+ * The new approach is to first split the document wherever a line starts with a token 'end'.
+ * <p>
+ * [#belongs to procedure 1<br>
+ * ... <br>
+ * end][#belongs to procedure 2 <br>
+ * ... <br>
+ * end] ...
+ * <p>
+ * These parts of the document are given to the constructor {@code Procedure#Procedure(String)},
+ * so the procedure can maintain its own state
+ * <br>
+ * 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'
+ * <br>
+ * 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
+ * <li> UNINITIALIZED
+ * <li> EXECUTABLE
+ * <li> COMMENT_ONLY (for white space and comments at the end of the document)
+ * <li> ERROR
+ * <li> AMBIGUOUS_NAME <br>
+ * <p>
+ * 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<String, Void> deleted = new HashMap<String, Void>();
+ 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<String> oldExecutables = new ArrayList<String>(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.
+ * <p>
+ * 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 <br>
+ * Delete all procedures
+ */
+ private void resetProcedureTables()
+ {
+ allProcedures.clear();
+ executables.clear();
+ invalidateText();
+ setText(null);
+ }
+
+ /**
+ * Implementation of the Logo Command "define". <br>
+ * FileChangeListeners are notified.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * Existing procedures with the same name are just redefined, as in XLogo.
+ * <p>
+ * @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! <br>
+ * 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<Procedure> getExecutables()
+ {
+ if (hasErrors())
+ return new ArrayList<Procedure>();
+ return executables.values();
+ }
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ERROR DETECTOR
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ */
+
+ @Override
+ public boolean hasErrors()
+ {
+ return hasError;
+ }
+
+ public Collection<ProcedureErrorMessage> getAllErrors()
+ {
+ ArrayList<ProcedureErrorMessage> allErrors = new ArrayList<ProcedureErrorMessage>();
+ 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<String> getAllProcedureNames()
+ {
+ if (hasErrors())
+ return new ArrayList<String>();
+
+ ArrayList<String> procedureNames = new ArrayList<String>();
+
+ for (Procedure p : executables.values())
+ procedureNames.add(p.getName());
+
+ return procedureNames;
+ }
+
+ @Override
+ public Collection<String> 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<ProcedureMapListener> procedureMapListeners = new ArrayList<ProcedureMapListener>();
+
+ @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<String> 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<String> 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. <br>
+ * 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.
+ * <p>
+ * 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<FileContainerChangeListener> fileListeners = new ArrayList<FileContainerChangeListener>();
+
+ 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} <br>
+ * 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. <br>
+ * 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<LogoFile> 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<p>
+ * @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<String> getAllErroneousFiles()
+ {
+ ArrayList<String> erroneousFiles = new ArrayList<String>();
+
+ 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<ProcedureErrorMessage> getAllErrors()
+ {
+ ArrayList<ProcedureErrorMessage> allErrors = new ArrayList<ProcedureErrorMessage>();
+ for (LogoFile file : context.getFilesTable().values())
+ allErrors.addAll(file.getAllErrors());
+ return allErrors;
+ }
+
+ // Error listeners
+
+ private final ArrayList<ErrorListener> errorListeners = new ArrayList<ErrorListener>();
+
+ @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<MessageListener> timerEventListeners = new ArrayList<MessageListener>();
+
+ @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<Procedure> getExecutables();
+
+ public Procedure getExecutable(String procedureName);
+
+ public boolean isExecutable(String name);
+
+ /**
+ * The Logo command {@code eraseProcedure} <p>
+ * Delete the specified procedure. <br>
+ * 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.
+ *
+ * <p>
+ * 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<String> variable = new ArrayList<String>();
+ public Stack<String> optVariables = new Stack<String>(); // Marko : why Stack??? [so bad]
+ public Stack<StringBuffer> optVariablesExp = new Stack<StringBuffer>(); // 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.
+ * <p>
+ * [leading <b>empty lines</b> and <b>comments</b> => stored into comment] <br>
+ * <b>to procedureName</b> [<b>variables</b>] [<b>optional variables</b>] <br>
+ * [<b>body</b>] <br>
+ * <b>end</b><br>
+ *
+ * @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.<br>
+ * 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<ProcedureErrorType> errors = new ArrayList<ProcedureErrorType>();
+
+ /**
+ * @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<ProcedureErrorType> 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]. <br>
+ * 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).<br>
+ * On the fly it was rewritten completely, because of its initial programming style and inefficiency. <br>
+ * (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<AmbiguityListener> ambiguityListeners = new ArrayList<AmbiguityListener>();
+
+ 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<String> procedures)
+ {
+ for (String procedureName : procedures)
+ addProcedure(fileName, procedureName);
+
+ if (procedures.size() > 0)
+ notifyDefined(fileName, procedures);
+
+ for (String procedureName : procedures)
+ {
+ Map<String, Procedure> 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<String, Procedure> 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<String> 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<String,HashMap<String, Procedure>> procedureTable = context.getProcedureTable();
+
+ Map<String, Procedure> 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<String, Procedure> 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<String, HashMap<String, Procedure>> procedureTable = context.getProcedureTable();
+ HashMap<String, Procedure> 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<String, Procedure>();
+ 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. <br>
+ * 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<Procedure> getExecutables()
+ {
+ ArrayList<Procedure> executables = new ArrayList<Procedure>();
+ for(Map<String, Procedure> 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<String, Procedure> 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<String, Procedure> fileToProc = context.getProcedureTable().get(procedureName.toLowerCase());
+ return (fileToProc != null && fileToProc.size() == 1);
+ }
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * PROCEDURE MAPPER
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ */
+
+ /**
+ * Get all executable procedure names
+ * @see #getExecutables()
+ */
+ @Override
+ public Collection<String> getAllProcedureNames()
+ {
+ ArrayList<String> executables = new ArrayList<String>();
+ for(Map<String, Procedure> 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<String> 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<ProcedureMapListener> procedureMapListeners = new ArrayList<ProcedureMapListener>();
+
+ @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<String> 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<String> 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<String, ? extends Map<String, Procedure>> entry : context.getProcedureTable().entrySet())
+ {
+ if (entry.getValue().size() > 1)
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns all ambiguity problems
+ */
+ @Override
+ public Collection<ProcedureErrorMessage> getAllErrors()
+ {
+ ArrayList<ProcedureErrorMessage> ambiguities = new ArrayList<ProcedureErrorMessage>();
+
+ for (Entry<String, HashMap<String, Procedure>> entry : context.getProcedureTable().entrySet())
+ {
+ HashMap<String, Procedure> fileToProc = entry.getValue();
+
+ if (fileToProc.size() < 2)
+ continue;
+
+ ambiguities.add(
+ new ProcedureErrorMessage(
+ ProcedureErrorType.AMBIGUOUS,
+ entry.getKey(),
+ fileToProc.keySet()
+ )
+ );
+ }
+
+ return ambiguities;
+ }
+
+ @Override
+ public Collection<String> getAllConflictingFiles()
+ {
+ ArrayList<String> conflictingFiles = new ArrayList<String>();
+
+ for (Entry<String, HashMap<String, Procedure>> entry : context.getProcedureTable().entrySet())
+ {
+ HashMap<String, Procedure> 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<String, Procedure> 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<String, Procedure> 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);
+ }
+
+
+
+
+}