/*
* XLogo4Schools - A Logo Interpreter specialized for use in schools, based on XLogo by Loic 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 Zurich,
* in the year 2013 and/or during future work.
* It is a reengineered version of XLogo written by Loic Le Coq, published
* under the GPL License at http://xlogo.tuxfamily.org/
* Contents of this file were entirely written by Marko Zivkovic
*/
package xlogo.storage;
import java.io.File;
import java.io.IOException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import xlogo.AppSettings;
import xlogo.Logo;
import xlogo.messages.MessageKeys;
import xlogo.messages.async.dialog.DialogMessenger;
import xlogo.storage.global.GlobalConfig;
import xlogo.storage.global.GlobalConfig.GlobalProperty;
import xlogo.storage.user.UserConfig;
import xlogo.storage.user.UserConfig.UserProperty;
import xlogo.storage.user.UserConfigJSONSerializer;
import xlogo.storage.workspace.WorkspaceConfig;
import xlogo.storage.workspace.WorkspaceConfig.WorkspaceProperty;
import xlogo.storage.workspace.WorkspaceConfigJSONSerializer;
import xlogo.utils.Utils;
/**
* Singleton Class for maintaining XLogo4Schools workspace, properties, reading and writing the various config files
*
* The workspace can be entered without using a user account. in that case, one enters as a virtual user.
* While working as virtual user, the changes of preferences and the programs are not stored persistently.
* However, one can still export and import program files. This behavior is similar to XLogo's file management that uses the classic save/open machanisms.
*
*
XLogo4Schools maintains the following files and directories on the file system
*
*
user_home/X4S_GlobalConfig.ser - this file stores information about how to access the various workspaces ({@link GlobalConfig})
*
*
*
workspaceLocation/ - the folder of a workspace on the file system
* workspaceLocation/X4S_WorkspaceConfig.ser - Information about the workspace ({@link WorkspaceConfig})
* workspaceLocation/user_i/ - Project folder of "user i" in the workspace
* workspaceLocation/user_i/X4S_UserConfig.ser - User preferences and settings ({@link UserConfig})
* workspaceLocation/user_i/file_j_v.lgo - Version v of file_j.lgo (the last n versions are kept)
* workspaceLocation/user_i/competition_protocol_k.txt - the protocol of the k'th recorded competition/session of user i
*
* The files with the ending .ser are serialized objects. They are loaded from the file system when a workspace or userspace is entered.
* The files are (re-)written to the file system whenever a user space, a workspace or XLogo4Schools is left.
*
*
*
Invariant : there is always an active GlobalConfig, WorkspaceConfig, and UserConfig.
*
Upon creation or loading of some config, it decides which sub-config should be loaded.
*
GlobalConfig tries to enter the last used workspace if possible, otherwise it enters the virtual workspace.
* WorksspaceConfig tries to enter the last active userspace if possible, otherwise it enters the virtual userspace.
*
* @author Marko
*/
public class WSManager {
private static Logger logger = LogManager.getLogger(WSManager.class.getSimpleName());
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Singleton
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
private static WSManager instance;
private static boolean isConstructingSingleton = false;
public static WSManager getInstance() {
if (instance == null) {
if (isConstructingSingleton){
throw new IllegalStateException("Recursive Singleton Creation.");
}
isConstructingSingleton = true;
instance = new WSManager();
isConstructingSingleton = false;
}
return instance;
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Config Access
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/**
* This is a shortcut for {@code WSManager.getInstance().getWorkspaceConfigInstance()}
* This is usually not null {@link WSManager},
* but it is for a short time while a workspace is being switched or the program fails to enter a workspace.
* @return
*/
public static WorkspaceConfig getWorkspaceConfig() {
return getInstance().getWorkspaceConfigInstance();
}
/**
* This is a shortcut for {@code WSManager.getInstance().getGlobalConfigInstance()}
* This is never null by definition of {@link WSManager}
* @return
*/
public static GlobalConfig getGlobalConfig() {
return getInstance().getGlobalConfigInstance();
}
/**
* This is a shortcut for {@code WSManager.getInstance().getUserConfigInstance()}
* Note that this might be null, if no user has entered his user space.
* @return
*/
public static UserConfig getUserConfig() {
return getInstance().getUserConfigInstance();
}
/**
* @return the instance of the GlobalConfig
*/
public GlobalConfig getGlobalConfigInstance() {
return globalConfig.get();
}
/**
* @return the active workspace
*/
public WorkspaceConfig getWorkspaceConfigInstance() {
if (getGlobalConfigInstance().getCurrentWorkspace() != null){
return getGlobalConfigInstance().getCurrentWorkspace().get();
}
return null;
}
/**
* @return the active user
*/
public UserConfig getUserConfigInstance() {
WorkspaceConfig wc = getWorkspaceConfigInstance();
if (wc == null)
return null;
else
return wc.getActiveUser().get();
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Initialization
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
private USBWorkspaceManager usbManager;
private StorableObject globalConfig;
private boolean hasEnteredApplication = false;
private WSManager() {
usbManager = new USBWorkspaceManager(new USBWorkspaceManager.WorkspaceContainer(){
@Override
public void add(StorableObject usbwc) {
GlobalConfig gc = getGlobalConfigInstance();
if (!hasEnteredApplication){
gc.addWorkspace(usbwc);
enterWorkspace(gc, usbwc);
}
}
@Override
public void remove(StorableObject usbwc) {
GlobalConfig gc = getGlobalConfigInstance();
if (hasEnteredApplication){
if (usbwc.equals(gc.getCurrentWorkspace())){
// TODO translate
DialogMessenger.getInstance().dispatchError(
"USB Stick Removed",
"I will not be able to remember your changes. Please reinsert your USB stick.");
}
// else ignore
} else {
gc.removeWorkspace(usbwc.get().getWorkspaceName());
enterInitialWorkspace(gc);
}
}
});
globalConfig = new StorableObject(GlobalConfig.class,
GlobalConfig.DEFAULT_LOCATION).withCreationInitializer(new StorableObject.Initializer(){
@Override
public void init(GlobalConfig gc) {
StorableObject wc = createWorkspace(gc, WorkspaceConfig.DEFAULT_DIRECTORY);
createUser(wc, UserConfig.DEFAULT_DIRECTORY);
initGc(gc);
}
}).withLoadInitializer(new StorableObject.Initializer(){
@Override
public void init(GlobalConfig gc) {
initGc(gc);
}
});
try {
globalConfig = globalConfig.createOrLoad();
}
catch (Exception e) {
DialogMessenger.getInstance().dispatchError("Unable to Initilize Global Configuration", e.toString());
}
}
protected void initGc(GlobalConfig gc){
usbManager.init();
gc.cleanUpWorkspaces();
StorableObject wc = enterInitialWorkspace(gc);
if (wc != null && wc.get() != null){
enterInitialUserSpace(wc);
}
}
public void enterApplication(){
hasEnteredApplication = true;
}
/**
* This is used to have a workspace ready at the beginning, without any user interaction.
*
* Tries to enter workspaces with the following priority.
* 1. Last used workspace (if any)
* 2. Default workspace, if there is no last used workspace
* 3. Virtual Workspace, if entering or creating the default workspace failed for some reason.
*/
protected StorableObject enterInitialWorkspace(GlobalConfig gc) {
logger.trace("Entering initial workspace.");
if (gc.getAllWorkspaces().length == 0){
logger.warn("No workspaces available.");
return null;
}
String initialWs = usbManager.getFirstUSBWorkspace(gc.getAllWorkspaces());
if (initialWs != null) {
File wsDir = usbManager.getWorkspaceDirectory(initialWs);
StorableObject wc = WorkspaceConfigJSONSerializer.createOrLoad(wsDir, true);
return enterWorkspace(gc, wc);
}
initialWs = gc.getLastUsedWorkspace();
if (initialWs == null) {
initialWs = gc.getAllWorkspaces()[0];
}
return enterWorkspace(gc, initialWs);
}
public void enterInitialUserSpace(StorableObject wc) {
String user = wc.get().getLastActiveUser();
if (user != null && wc.get().existsUserLogically(user)){
enterUserSpace(user);
}
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* WORKSPACE CONFIG : create, delete, enter
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/**
* A new workspace is created in the defined directory.
* All Necessary files and folders are created and the workspace is logically added to the globalConfig.
* @see WorkspaceConfig#loadWorkspace(File)
* @param location
* @param name
*/
public void createWorkspace(File location, String name) {
File wsDir = StorableObject.getDirectory(location, name);
GlobalConfig gc = getGlobalConfig();
createWorkspace(gc, wsDir);
enterInitialWorkspace(gc);
}
/**
*
* @param gc - The global config, where the new workspace should be registered
* @param wsDir - the workspace directory
* @return
*/
protected StorableObject createWorkspace(GlobalConfig gc, File wsDir){
StorableObject wc;
wc = WorkspaceConfigJSONSerializer.createOrLoad(wsDir);
if (wc != null){
gc.addWorkspace(wc);
enterWorkspace(gc, wsDir.getName());
}
return wc;
}
public void deleteWorkspace(String wsName, boolean deleteFromDisk) {
GlobalConfig gc = getGlobalConfigInstance();
File wsDir = gc.getWorkspaceDirectory(wsName);
gc.leaveWorkspace();
gc.removeWorkspace(wsName);
if (deleteFromDisk)
deleteFullyRecursive(wsDir);
enterInitialWorkspace(gc);
}
/**
* @param wsDir
* @throws IllegalArgumentException if wsDir is not a legal workspace directory
*/
public void importWorkspace(File workspaceDir, String workspaceName) {
logger.trace("Importing workspace '" + workspaceName + "' from " + workspaceDir.getAbsolutePath());
if (!isWorkspaceDirectory(workspaceDir)) {
DialogMessenger.getInstance().dispatchError(Logo.messages.getString(MessageKeys.WS_ERROR_TITLE),
workspaceDir + " " + Logo.messages.getString(MessageKeys.WS_NOT_A_WORKSPACE_DIRECTORY));
return;
}
getGlobalConfigInstance().addWorkspace(workspaceName, workspaceDir.getParent());
enterWorkspace(workspaceName);
}
/**
* Load the workspace
* Always succeeds if workspaceName equals {@link WorkspaceConfig#VIRTUAL_WORKSPACE}
* @param workspaceName - the workspace to load and enter
* @throws IOException - if the old workspace could not be loaded
*/
public void enterWorkspace(String workspaceName) {
GlobalConfig gc = getGlobalConfigInstance();
enterWorkspace(gc, workspaceName);
}
protected StorableObject enterWorkspace(GlobalConfig gc, String workspaceName) {
StorableObject wc = gc.getCurrentWorkspace();
if(wc != null && workspaceName.equals(wc.get().getWorkspaceName())){
logger.trace("I'm already in workspace: " + workspaceName);
return gc.getCurrentWorkspace();
}
wc = null;
if (usbManager.isUSBDrive(workspaceName)) {
logger.trace("Retrieving USB workspace: " + workspaceName);
wc = usbManager.createOrLoad(workspaceName);
}
if (wc == null){
File wsDir = gc.getWorkspaceDirectory(workspaceName);
if (wsDir == null){
logger.error("Can't find workspace " + workspaceName);
return gc.getCurrentWorkspace();
}
wc = WorkspaceConfigJSONSerializer.createOrLoad(wsDir);
}
if (wc == null){
logger.error("Can't enter workspace because creation or laod failed for " + workspaceName);
return gc.getCurrentWorkspace();
}
return enterWorkspace(gc, wc);
}
protected StorableObject enterWorkspace(GlobalConfig gc, StorableObject wc) {
gc.enterWorkspace(wc);
enterInitialUserSpace(wc);
return gc.getCurrentWorkspace();
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* USER CONFIG : create, delete, enter
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/**
* A new user is created in the current workspace.
* All Necessary files and folders are created and the workspace is logically added to the globalConfig.
* @param username
*/
public void createUser(String username) {
StorableObject wc = globalConfig.get().getCurrentWorkspace();
if (wc == null || wc.get() == null){
throw new IllegalStateException("Cannot create a user directory outside of workspaces. Use createUser(File dir) for special cases.");
}
File userDir = StorableObject.getDirectory(wc.getLocation(), username);
createUser(wc, userDir);
}
private void createUser(StorableObject wc, File userDir){
StorableObject duc = new StorableObject(UserConfig.class, userDir);
try {
duc = duc.createOrLoad();
}
catch (Exception e) { }
if (!duc.isPersisted()){
logger.warn("Could not persist user files.");
}
duc.get().setDirectory(userDir);
wc.get().addUser(duc);
enterUserSpace(wc.get(), duc);
}
/**
* @param username
* @param deleteFromDisk
*/
public void deleteUser(String username, boolean deleteFromDisk) {
File location = StorableObject.getDirectory(getWorkspaceConfigInstance().getDirectory(), username);
getWorkspaceConfigInstance().removeUser(username);
if (deleteFromDisk) {
try {
deleteFullyRecursive(location);
}
catch (SecurityException e) {
System.out.println("Files not deleted: " + e.toString());
}
}
}
/**
* Import a user directory from anywhere in the file system to this workspace.
* All files in the user directory are copied. Already existing files might get overwritten.
* This has no effect if this is virtual.
* @param srcUserDir - a legal user directory anywhere on the file system
* @param destUsername - Existing files of targetUser are overwritten. If targetUser does not exist, it will be created first.
* @throws IllegalArgumentException
* @throws IOException
* @see WSManager#isUserDirectory(File)
*/
public void importUser(File srcUserDir, String destUsername) throws IllegalArgumentException, IOException {
logger.trace("Importing user '" + destUsername + "' from " + srcUserDir.getAbsolutePath());
if (!isUserDirectory(srcUserDir))
throw new IllegalArgumentException("Target directory is not a user directory.");
createUser(destUsername);
File wsDir = getWorkspaceConfig().getDirectory();
File targetUserDir = StorableObject.getDirectory(wsDir, destUsername);
copyFullyRecursive(srcUserDir, targetUserDir);
enterUserSpace(destUsername);
}
/**
* @throws IOException If the old userConfig could not be stored.
*/
public void enterUserSpace(String name) {
WorkspaceConfig wc = getWorkspaceConfigInstance();
if (wc == null)
throw new IllegalStateException("Must be in WorkspaceDirectory first to enter UserSpace.");
enterUserSpace(wc, name);
}
protected void enterUserSpace(WorkspaceConfig wc, String username) {
File userDir = StorableObject.getDirectory(wc.getDirectory(), username);
StorableObject uc = UserConfigJSONSerializer.createOrLoad(userDir);
if (uc != null){
enterUserSpace(wc, uc);
} else {
DialogMessenger.getInstance().dispatchError("Workspace Error", "Cannot enter workspace.");
}
}
protected void enterUserSpace(WorkspaceConfig wc, StorableObject uc) {
wc.enterUserSpace(uc);
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* SHORTCUTS
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
public void storeAllSettings() {
globalConfig.store();
StorableObject swc = globalConfig.get().getCurrentWorkspace();
if (swc == null) { return; }
swc.store();
StorableObject suc = swc.get().getActiveUser();
if (suc == null) { return; }
suc.store();
}
/**
* Make sure all threads are stopped and open files saved and closed.
*/
public void stopEverything(){
usbManager.stop();
storeAllSettings();
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* DIRECTORIES & FILE MANIPULATION
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
public static File[] listDirectories(File dir) {
File[] dirs = dir.listFiles(new java.io.FileFilter(){
public boolean accept(File pathname) {
return pathname.isDirectory();
}
});
if (dirs == null) {
dirs = new File[0];
}
return dirs;
}
/**
* A directory is considered a workspace directory,
* if it contains a file for {@link WorkspaceConfig}, as defined by {@link StorableObject#getFilePath(File, Class)}
* @param dir
* @return
*/
public static boolean isWorkspaceDirectory(File dir) {
if (dir == null)
return false;
if (!dir.isDirectory())
return false;
File wcf = StorableObject.getFilePath(dir, WorkspaceConfig.class);
if (!wcf.isFile())
return false;
return true;
}
/**
* A directory is considered a user directory,
* if it contains a file for {@link UserConfig}, as defined by {@link StorableObject#getFilePath(File, Class)}
* @param dir
* @return
*/
public static boolean isUserDirectory(File dir) {
if (dir == null)
return false;
if (!dir.isDirectory())
return false;
File ucf = StorableObject.getFilePath(dir, UserConfig.class);
if (!ucf.isFile())
return false;
return true;
}
/**
* If "from" denotes a file, then "to" should be a file too.
* The contents of file "from" are copied to file "to".
* "to" is created, if it does not exists, using mkdirs.
*
* If "from" denotes a directory, then "to" should be a directory too.
* The contents of directory "from" are copied recursively to directory "to".
* "to" is created, if it does not exists, using mkdirs.
* @param from - must exist
* @param to - must not exist
* @throws IOException
*/
public static void copyFullyRecursive(File from, File to) throws IOException {
if (!from.exists())
throw new IllegalArgumentException("'from' (" + from.toString() + ") must exist.");
if (from.isFile()) {
copyFile(from, to);
return;
}
// else to is directory
to.mkdirs();
for (File src : from.listFiles()) {
File dest = new File(to.toString() + File.separator + src.getName());
if (src.isFile()) {
copyFile(src, dest);
}
else if (src.isDirectory()) {
copyFullyRecursive(src, dest);
}
}
}
public static void copyFile(File from, File to) throws IOException {
if (!from.isFile())
throw new IllegalArgumentException("File 'from' (" + from.toString() + ") must exist.");
if (to.exists()) {
if (!to.isFile())
throw new IllegalArgumentException("File 'to' (" + from.toString() + ") must be a file.");
}
else {
File parent = to.getParentFile();
if (!parent.exists())
to.getParentFile().mkdirs();
}
Utils.copyFile(from, to);
}
/**
* @param victim
* @throws SecurityException If one tries to delete some directory that is not under control of this application (Workspace or User)
*/
public static void deleteFullyRecursive(File victim) throws SecurityException {
if (!victim.exists())
return;
if (victim.isFile()) {
victim.delete();
return;
}
if (!isGlobalConfigDirectory(victim) && !isWorkspaceDirectory(victim) && !isUserDirectory(victim)) {
String title = AppSettings.getInstance().translate("error.security.violation.title");
String message = AppSettings.getInstance().translate("error.attempt.delete.non.x4s.file");
DialogMessenger.getInstance().dispatchError(title, message + ' ' + victim.toString());
throw new SecurityException();
}
// Delete all sub-directories
for (File f : victim.listFiles()) {
uncheckedRecursiveDelete(f);
victim.delete();
}
// Delete directory itself
victim.delete();
}
/**
* CAUTION : Don't use this unless you are sure that the directory you want to delete is a XLogo4Schools directory.
* Otherwise it could delete just everything.
* @param victim
*/
private static void uncheckedRecursiveDelete(File victim) {
if (!victim.exists())
return;
if (victim.isFile()) {
victim.delete();
return;
}
// Delete all sub-directories
for (File f : victim.listFiles()) {
uncheckedRecursiveDelete(f);
victim.delete();
}
// Delete directory itself
victim.delete();
}
public static boolean isGlobalConfigDirectory(File dir) {
if (!dir.isDirectory())
return false;
String name = dir.getName();
if (!name.startsWith("X4S_"))
return false;
return true;
}
}