/*******************************************************************************
* FreeQDA, a software for professional qualitative research data
* analysis, such as interviews, manuscripts, journal articles, memos
* and field notes.
*
* Copyright (C) 2011 Dirk Kitscha, Jörg große Schlarmann
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*******************************************************************************/
package net.sf.freeqda.common.projectmanager;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import net.sf.freeqda.common.GenericTreeNode;
import net.sf.freeqda.common.JAXBUtils;
import net.sf.freeqda.common.tagregistry.TagManager;
public class ProjectManager implements TextCategoryNodeCreatedListener, TextNodeCreatedListener {
private static final String TEXT_CATEGORY_ROOT_NAME = Messages.ProjectManager_TextCategoryRootName;
private static final String EXCEPTION_WORKSPACE_UNINITIALIZED = Messages.ProjectManager_ExceptionWorkspaceUninitialized;
private static final String EXCEPTION_WORKSPACE_INITIALIIZATION_FAILED = Messages.ProjectManager_ExceptionWorkspaceInitializationFailed;
private static final String WORKSPACE_DEFAULT_NAME = "FQDAWorkspace"; //$NON-NLS-1$
private static final String EXCEPTION_NODE_IS_NULL = Messages.ProjectManager_ExceptionNodeIsNull;
private static final String TRASH_DIRECTORY_NAME = "Trash"; //$NON-NLS-1$
public static final String FQDA_FILE_COMMON_NAME = "file_"; //$NON-NLS-1$
public static final String FQDA_FILE_COMMON_SUFFIX = ".fqf"; //$NON-NLS-1$
public static final String FQDA_PROJECT_FILE_NAME = "project.fqd"; //$NON-NLS-1$
/**
* Stores the (singleton) instance to this class
*/
private static ProjectManager SINGLETON_INSTANCE;
/**
* Holds the reference to the DocumentManager class
*/
// private static final DocumentRegistry DOCUMENT_MANAGER = DocumentRegistry.getInstance();
/**
* The category id of the internal root node
*/
protected static final int ROOT_CATEGORY_ID = 0;
/**
* The text id of the internal root node
*/
protected static final int ROOT_TEXT_ID = 0;
private static final LinkedList<ProjectDataModifiedListener> listeners = new LinkedList<ProjectDataModifiedListener>();
public static void registerProjectModifiedListener(ProjectDataModifiedListener listener) {
listeners.add(listener);
}
public static void removeProjectLoadedListener(ProjectDataModifiedListener listener) {
listeners.remove(listener);
}
/**
* Stores the location of the workspace directory
*/
private File workspaceDirectory;
/**
* Stores the highest CategoryNode UID that is used in a project
*/
private int highestCategoryUID;
/**
* Stores the highest TextNode UID that is used in a project
*/
private int highestTextUID;
private File projectFile;
private File projectTrash;
private boolean isActive;
/**
* Returns the (singleton) instance of the TagManager class
* @return the (singleton) instance of the TagManager class
*/
public static final ProjectManager getInstance() {
if (SINGLETON_INSTANCE==null) {
synchronized (ProjectManager.class) {
if (SINGLETON_INSTANCE==null) {
SINGLETON_INSTANCE = new ProjectManager();
}
}
}
return SINGLETON_INSTANCE;
}
public void reset() {
highestCategoryUID = ROOT_CATEGORY_ID;
highestTextUID = ROOT_TEXT_ID;
lookupMapCategory = new HashMap<Integer, TextCategoryNode>();
lookupMapText = new HashMap<Integer, TextNode>();
setRootCategory(new TextCategoryNode(TEXT_CATEGORY_ROOT_NAME, ROOT_CATEGORY_ID));
setProjectName(null);
try {
workspaceDirectory = createWorkspaceDirectory();
} catch (Exception e) {
e.printStackTrace();
}
TagManager.getInstance().reset();
setActive(false);
}
public File getWorkspaceDirectory() {
if (workspaceDirectory != null) return workspaceDirectory;
else throw new NullPointerException(EXCEPTION_WORKSPACE_UNINITIALIZED);
}
public File createWorkspaceDirectory() throws IOException {
if (workspaceDirectory != null) return workspaceDirectory;
//FIXME replace workspace retrieval and creation by something more flexible
File workspaceDir1 = new File(System.getProperty("user.dir"), WORKSPACE_DEFAULT_NAME); //$NON-NLS-1$
if (workspaceDir1.exists() && workspaceDir1.isDirectory() && workspaceDir1.canRead() && workspaceDir1.canExecute() && workspaceDir1.canWrite()) return workspaceDir1;
File workspaceDir3 = new File(System.getProperty("user.home"), WORKSPACE_DEFAULT_NAME); //$NON-NLS-1$
if (workspaceDir3.exists() && workspaceDir3.isDirectory() && workspaceDir3.canRead() && workspaceDir3.canExecute() && workspaceDir3.canWrite()) return workspaceDir3;
if (workspaceDir1.mkdirs()) return workspaceDir1;
if (workspaceDir3.mkdirs()) return workspaceDir3;
throw new IOException(EXCEPTION_WORKSPACE_INITIALIIZATION_FAILED);
}
protected int getHighestCategoryUID() {
return highestCategoryUID;
}
protected int getHighestTextUID() {
return highestTextUID;
}
/**
* Internal Root node for the category tree.
*/
private TextCategoryNode rootCategory;
/**
* Contains all known categories.
*/
private HashMap<Integer, TextCategoryNode> lookupMapCategory;
/**
* Contains all known categories.
*/
private HashMap<Integer, TextNode> lookupMapText;
public HashMap<Integer, TextCategoryNode> getLookupMapCategory() {
return lookupMapCategory;
}
public void setLookupMapCategory(
HashMap<Integer, TextCategoryNode> lookupMapCategory) {
this.lookupMapCategory = lookupMapCategory;
}
public HashMap<Integer, TextNode> getLookupMapText() {
return lookupMapText;
}
public void setLookupMapText(HashMap<Integer, TextNode> lookupMapText) {
this.lookupMapText = lookupMapText;
}
protected void setRootCategory(TextCategoryNode rootCategory) {
this.rootCategory = rootCategory;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
public void updateHighestCategoryUID(int uid) {
if (highestCategoryUID < uid) {
highestCategoryUID = uid;
}
}
public void updateHighestTextUID(int uid) {
if (highestTextUID < uid) {
highestTextUID = uid;
}
}
/**
* The project name
*/
private String projectName;
/**
* Initializes the ProjectManager
*/
private ProjectManager() {
reset();
}
/**
* Creates a new TextCategoryNode object with a default name.
* This method is synchronized in order to prevent two threads from creating
* a TextCategoryNode with the same UID.
* The created TextCategoryNode is also stored in the CATEGORY_LOOKUP_MAP.
*
* @return a new instance of TextCategoryNode
*/
public synchronized TextCategoryNode createCategoryNode() {
highestCategoryUID++;
TextCategoryNode res = new TextCategoryNode(Messages.ProjectManager_CategoryNodeDefaultName+highestCategoryUID, highestCategoryUID);
lookupMapCategory.put(highestCategoryUID, res);
return res;
}
/**
* Creates a new TextNode object with a default name.
* This method is synchronized in order to prevent two threads from creating
* a TextNode with the same UID.
* The created TextNode is also stored in the TEXT_LOOKUP_MAP.
*
* @return a new instance of TextNode
*/
public synchronized TextNode createTextNode() {
highestTextUID++;
TextNode res = new TextNode(MessageFormat.format(Messages.ProjectManager_TextNodeDefaultName, new Object[] {highestTextUID}), highestTextUID, false);
lookupMapText.put(highestTextUID, res);
return res;
}
/**
* Returns the root category of the category tree.
*/
public TextCategoryNode getRootCategory() {
return rootCategory;
}
/**
* Inserts a TextCategoryNode into the TextCategoryTree.
*
* @param toAdd
* @param parentNode
*/
public synchronized void insert(TextCategoryNode toAdd, TextCategoryNode parentNode) {
/*
* Check contracts: The node to insert must not be null and its UID must be managed by this class
*/
if (toAdd == null) {
throw new NullPointerException(EXCEPTION_NODE_IS_NULL);
}
if (!lookupMapCategory.containsKey(toAdd.getUID())) {
throw new IllegalArgumentException(MessageFormat.format(Messages.ProjectManager_ExceptionUnmanagedCategory, new Object[] { toAdd }));
}
LinkedList<GenericTreeNode<TextCategoryNode>> listToAddTo = null;
if (parentNode == null) listToAddTo = rootCategory.getChildren();
else listToAddTo = parentNode.getChildren();
if (! listToAddTo.contains(toAdd)) {
listToAddTo.add(toAdd);
toAdd.setParentNode(parentNode);
}
/*
* save the changes to disk
*/
save();
}
/**
* Inserts a TextNode into the TextCategoryTree.
*
* @param toAdd
* @param parentNode
*/
public synchronized void insert(TextNode toAdd, TextCategoryNode parentNode) {
/*
* Check contracts: The node to insert must not be null and its UID must be managed by this class
*/
if (toAdd == null) {
throw new NullPointerException(EXCEPTION_NODE_IS_NULL);
}
if (!lookupMapText.containsKey(toAdd.getUID())) {
throw new IllegalArgumentException(MessageFormat.format(Messages.ProjectManager_ExceptionUnmanagedText, new Object[] { toAdd }));
}
TextCategoryNode nodeToAddTo = null;
if (parentNode == null) nodeToAddTo = rootCategory;
else nodeToAddTo = parentNode;
List<TextNode> listToAddTo = nodeToAddTo.getTextFileDescriptors();
if (! listToAddTo.contains(toAdd)) {
listToAddTo.add(toAdd);
}
/*
* save the changes to disk
*/
save();
}
@Override
public void NodeCreated(TextCategoryNodeCreatedEvent evt) {
insert(evt.getCreatedNode(), evt.getParentNode());
/*
* save the changes to disk
*/
save();
}
@Override
public void NodeCreated(TextNodeCreatedEvent evt) {
insert(evt.getCreatedNode(), evt.getParentNode());
/*
* save the changes to disk
*/
save();
}
/**
* Removes a TextCategoryNode from the TextCategoryTree.
* The created TextCategoryNode is also removed from the CATEGORY_LOOKUP_MAP.
* @param toRemove
*/
public synchronized void remove(TextCategoryNode toRemove) {
/*
* Check contracts: The node to remove must not be null and its UID must be managed by this class
*/
if (toRemove == null) {
throw new NullPointerException(EXCEPTION_NODE_IS_NULL);
}
if (!lookupMapCategory.containsKey(toRemove.getUID())) {
throw new IllegalArgumentException(MessageFormat.format(Messages.ProjectManager_ExceptionUnmanagedCategory, new Object[] { toRemove }));
}
/*
* Delete all child categories and texts recursivly
*/
removeRecursivly(toRemove);
/*
* save the changes to disk
*/
save();
}
private void removeRecursivly(TextCategoryNode toRemove) {
/*
* remove all text files in this category
*/
TextNode[] textFiles = toRemove.getTextFileDescriptors().toArray(new TextNode[0]);
for (int n=0; n<textFiles.length; n++) {
remove(textFiles[n], toRemove);
}
/*
* remove all child categories
*/
Object[] childCategories = toRemove.getChildren().toArray();
for (int n=0; n<childCategories.length; n++) {
remove((TextCategoryNode) childCategories[n]);
}
/*
* remove this category
*/
LinkedList<GenericTreeNode<TextCategoryNode>> listToRemoveFrom = null;
if (toRemove.getParentCategory() == null) listToRemoveFrom = rootCategory.getChildren();
else listToRemoveFrom = toRemove.getParentCategory().getChildren();
listToRemoveFrom.remove(toRemove);
lookupMapCategory.remove(toRemove);
}
/**
* Removes a TextNode from the TextCategoryTree.
* The TextNode is also removed from the TEXT_LOOKUP_MAP.
* @param toRemove
*/
public synchronized void remove(TextNode toRemove, TextCategoryNode parentNode) {
/*
* Check contracts: The node to remove must not be null and its UID must be managed by this class
*/
if (toRemove == null) {
throw new NullPointerException(EXCEPTION_NODE_IS_NULL);
}
if (!lookupMapText.containsKey(toRemove.getUID())) {
throw new IllegalArgumentException(MessageFormat.format(Messages.ProjectManager_ExceptionUnmanagedText, new Object[] { toRemove }));
}
if (parentNode == null) parentNode = rootCategory;
if (parentNode.getTextFileDescriptors().remove(toRemove)) {
TextNode removedNode = lookupMapText.remove(toRemove.getUID());
if (removedNode != null) {
File fileToRemove = new File(projectFile.getParentFile(), MessageFormat.format(FQDA_FILE_COMMON_NAME+"%1"+FQDA_FILE_COMMON_SUFFIX, new Object[] {toRemove.getUID()})); //$NON-NLS-1$
if (! fileToRemove.renameTo(new File(projectTrash, fileToRemove.getName()))) {
//TODO what if the move fails?
}
}
}
/*
* save the changes to disk
*/
save();
}
public void toggleTextNodeActive(TextNode textNode) {
if (textNode != null) {
textNode.setActivated(! textNode.isActivated());
save();
}
}
public String getProjectName() {
return projectName;
}
public File getProjectFile() {
return projectFile;
}
public void setProjectFile(File projectFile) {
this.projectFile = projectFile;
this.projectTrash = new File(projectFile.getParentFile(), TRASH_DIRECTORY_NAME);
if (!projectTrash.exists()) {
projectTrash.mkdir();
}
}
public void save() {
if (projectFile == null) return;
try {
JAXBUtils.saveProject(projectFile);
} catch (IOException e) {
e.printStackTrace();
}
}
public void load(File toLoad) throws IOException {
JAXBUtils.loadProject(toLoad);
setActive(true);
}
//
//
// public FQDADocumentType getFQDADocumentForTextNode(TextNode node) throws IOException {
// File documentFile = new File(getProjectFile().getParentFile(), "file_"+node.getUID()+".fqf");
// return JAXBUtils.loadDocument(documentFile);
// }
public boolean isActive() {
return isActive;
}
private void setActive(boolean isActive) {
this.isActive = isActive;
/*
* Update views
*/
fireProjectDataModifiedEvent();
}
public void fireProjectDataModifiedEvent() {
for (ProjectDataModifiedListener listener: listeners) {
listener.ProjectDataModified();
}
save();
}
public List<TextNode> getActiveTextNodes() {
LinkedList<TextNode> res = new LinkedList<TextNode>();
for (TextNode textNode: this.lookupMapText.values()) {
if (textNode.isActivated()) res.add(textNode);
}
return res;
}
}