From 1107aa0763e3d7554408c401d2a1dbed11a94c51 Mon Sep 17 00:00:00 2001 From: Marko Živković Date: Wed, 11 Jun 2014 10:10:33 +0000 Subject: Add initial directories and files git-svn-id: https://svn.code.sf.net/p/xlogo4schools/svn/trunk@1 3b0d7934-f7ef-4143-9606-b51f2e2281fd --- .../src/xlogo/kernel/userspace/files/LogoFile.java | 755 +++++++++++++++++++++ .../kernel/userspace/files/LogoFileContainer.java | 39 ++ .../kernel/userspace/files/LogoFilesManager.java | 560 +++++++++++++++ .../xlogo/kernel/userspace/files/RecordFile.java | 234 +++++++ 4 files changed, 1588 insertions(+) create mode 100644 logo/src/xlogo/kernel/userspace/files/LogoFile.java create mode 100644 logo/src/xlogo/kernel/userspace/files/LogoFileContainer.java create mode 100644 logo/src/xlogo/kernel/userspace/files/LogoFilesManager.java create mode 100644 logo/src/xlogo/kernel/userspace/files/RecordFile.java (limited to 'logo/src/xlogo/kernel/userspace/files') diff --git a/logo/src/xlogo/kernel/userspace/files/LogoFile.java b/logo/src/xlogo/kernel/userspace/files/LogoFile.java new file mode 100644 index 0000000..92fe23c --- /dev/null +++ b/logo/src/xlogo/kernel/userspace/files/LogoFile.java @@ -0,0 +1,755 @@ +/* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loïc Le Coq + * Copyright (C) 2013 Marko Zivkovic + * + * Contact Information: marko88zivkovic at gmail dot com + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. This program is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. You should have received a copy of the + * GNU General Public License along with this program; if not, write to the Free + * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + * This Java source code belongs to XLogo4Schools, written by Marko Zivkovic + * during his Bachelor thesis at the computer science department of ETH Zürich, + * in the year 2013 and/or during future work. + * + * It is a reengineered version of XLogo written by Loïc Le Coq, published + * under the GPL License at http://xlogo.tuxfamily.org/ + * + * Contents of this file were entirely written by Marko Zivkovic + */ + +package xlogo.kernel.userspace.files; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; + +import xlogo.Logo; +import xlogo.interfaces.ErrorDetector; +import xlogo.interfaces.ProcedureMapper; +import xlogo.kernel.userspace.ProcedureErrorMessage; +import xlogo.kernel.userspace.procedures.ExecutablesContainer; +import xlogo.kernel.userspace.procedures.Procedure; +import xlogo.kernel.userspace.procedures.Procedure.State; +import xlogo.messages.MessageKeys; +import xlogo.messages.async.dialog.DialogMessenger; +import xlogo.storage.Storable; +import xlogo.storage.StorableDocument; +import xlogo.storage.WSManager; +import xlogo.storage.global.GlobalConfig; +import xlogo.storage.user.UserConfig; +import xlogo.storage.workspace.NumberOfBackups; +import xlogo.storage.workspace.WorkspaceConfig; +import xlogo.utils.Utils; + +/** + * This class holds the text file a user entered in the editor. + * It analyzes the text and maintains a symbol table for all defined procedures that live within it. + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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