package LinGUIne.model;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.TreeSet;
import org.eclipse.core.runtime.IPath;
import LinGUIne.serialization.ProjectTranslator;
import LinGUIne.utilities.FileUtils;
import LinGUIne.utilities.ParameterCheck;
/**
* Represents a LinGUIne Project with a name, a file system location, and
* containing Project Data.
* Note: A Project for which hasProjectFiles returns false is NOT complete and
* ready to be used; createProjectFiles must be called first.
* TODO: Add checks for hasProjectFiles to avoid errors if there are none.
*
* @author Kyle Mullins
*/
public class Project {
/*
* Enum used for specifying a subdirectory of a Project.
*/
public static enum Subdirectory{
Data, Results, Annotations;
}
/*
* Constants for directory and file names used when creating a Project on
* disk.
*/
public static final String DATA_SUBDIR = "data";
public static final String RESULTS_SUBDIR = "results";
public static final String ANNOTATIONS_SUBDIR = "annotations";
public static final String PROJECT_FILE = "linguine.project";
public static final String DATA_SUBDIR_DISPLAY = "Project Data";
public static final String RESULTS_SUBDIR_DISPLAY = "Results";
public static final String ANNOTATIONS_SUBDIR_DISPLAY = ANNOTATIONS_SUBDIR;
private IPath parentDirectory;
private String projectName;
private RootProjectGroup projDataGroup;
private RootProjectGroup resultsGroup;
private RootProjectGroup annotationsGroup;
/*
* Maps ProjectData to its assigned id.
*/
private TreeMap<IProjectData, Integer> projectData;
/*
* Maps a Result to the set of ids for the associated ProjectData.
*/
private TreeMap<Result, HashSet<Integer>> results;
/*
* Maps a Project Data id to its associated AnnotationSet.
*/
private HashMap<Integer, AnnotationSet> annotationSets;
/*
* Maps a ProjectGroup to its assigned id.
*/
private HashMap<ProjectGroup, Integer> groups;
/*
* Maps a Project Group id to the set of ids for Project Data located within
* the group.
*/
private HashMap<Integer, HashSet<Integer>> groupContents;
private boolean hasProjectFiles;
private int lastId;
private HashSet<ProjectListener> listeners;
/**
* Creates a new Project without a name or any Project Data of any kind.
* Adds root ProjectGroups to hold ProjectData, Results, and AnnotationSets.
* Note: A Project created with this constructor is NOT complete; it must
* be given a name.
*/
public Project(){
hasProjectFiles = false;
lastId = 0;
listeners = new HashSet<ProjectListener>();
projectData = new TreeMap<IProjectData, Integer>();
results = new TreeMap<Result, HashSet<Integer>>();
annotationSets = new HashMap<Integer, AnnotationSet>();
groups = new HashMap<ProjectGroup, Integer>();
groupContents = new HashMap<Integer, HashSet<Integer>>();
projDataGroup = new RootProjectGroup(
DATA_SUBDIR_DISPLAY, DATA_SUBDIR);
resultsGroup = new RootProjectGroup(
RESULTS_SUBDIR_DISPLAY, RESULTS_SUBDIR);
annotationsGroup = new RootProjectGroup(
ANNOTATIONS_SUBDIR_DISPLAY, ANNOTATIONS_SUBDIR);
annotationsGroup.setHidden(true);
addGroup(projDataGroup);
addGroup(resultsGroup);
addGroup(annotationsGroup);
}
/**
* Creates a Project just as the default constructor, but the given name is
* assigned as well.
* Note: Parameter projName cannot be null.
*
* @param projName The name of the Project.
*/
public Project(String projName){
this();
ParameterCheck.notNull(projName, "projName");
projectName = projName;
}
/**
* Parses the given projectFile and creates a new Project based on it.
*
* @param projectFile The linguine.project file for the new Project.
*
* @return A new Project based on the given linguine.project file or null
* if an error occurred or the file was incorrect.
*/
public static Project createFromFile(File projectFile){
Project newProj;
try(BufferedReader reader = Files.newBufferedReader(projectFile.toPath(),
Charset.defaultCharset())){
IPath parentDir = FileUtils.toEclipsePath(projectFile.
getParentFile().getParentFile());
String jsonStr = "";
while(reader.ready()){
jsonStr += reader.readLine();
jsonStr += "\n";
}
newProj = ProjectTranslator.fromJson(jsonStr, parentDir);
}
catch(IOException ioe) {
ioe.printStackTrace();
return null;
}
newProj.hasProjectFiles = true;
return newProj;
}
/**
* Returns whether or not this Project has the file structure on disk needed
* in order for it to be usable. Instances for which this function returns
* false are not usable until createProjectFiles has been called.
*/
public boolean hasProjectFiles(){
return hasProjectFiles;
}
/**
* Sets the name of this Project.
* Note: Parameter projName cannot be null.
*/
public void setName(String projName){
ParameterCheck.notNull(projName, "projName");
projectName = projName;
}
/**
* Returns this Project's name.
*/
public String getName(){
return projectName;
}
/**
* Sets the path of the root directory in which this Project resides on
* disk.
*/
public void setParentDirectory(IPath parentDir){
parentDirectory = parentDir;
}
/**
* Returns the path of this Project's root directory on disk.
*/
public IPath getParentDirectory(){
return parentDirectory;
}
/**
* Returns the path of this Project's directory which is a folder of the
* Project's name within the parent directory.
*
* @return The path of the Project directory, or null if the Project is
* missing either a name or a parent directory.
*/
public IPath getProjectDirectory(){
if(projectName == null || parentDirectory == null){
return null;
}
return parentDirectory.append(projectName);
}
/**
* Returns the path of the requested subdirectory of this Project's
* directory.
*
* @param subdir Enum denoting for which subdirectory to return the path.
*
* @return The path of the requested subdirectory or null if an invalid
* enum option is provided.
*/
public IPath getSubdirectory(Subdirectory subdir){
String groupName;
switch(subdir){
case Data:
groupName = DATA_SUBDIR_DISPLAY;
break;
case Results:
groupName = RESULTS_SUBDIR_DISPLAY;
break;
case Annotations:
groupName = ANNOTATIONS_SUBDIR;
break;
default:
return null;
}
return getPathToGroup(getGroup(groupName));
}
/**
* Returns the full path to the given ProjectGroup or null if it is not in
* this Project.
*/
public IPath getPathToGroup(ProjectGroup group){
if(containsGroup(group)){
return getProjectDirectory().append(group.getGroupPath());
}
return null;
}
/**
* Returns the path of the project file for this Project.
*
* @return Path to the project file.
*/
public IPath getProjectFile(){
return getProjectDirectory().append(PROJECT_FILE);
}
/**
* Adds the given ProjectData to the Project if it is not already in the
* Project and is not null.
* The ProjectData is added to the given ProjectGroup.
* Note: Results and AnnotationSets should not be added with this method,
* they should instead use the addResult and addAnnotation respectively.
*
* @param projData The ProjectData to be added to the Project.
*
* @return True iff the ProjectData was successfully added, false
* otherwise.
*/
public boolean addProjectData(IProjectData projData,
ProjectGroup parentGroup){
if(projData == null || projectData.containsKey(projData) ||
!groups.containsKey(parentGroup)){
return false;
}
int id = getNextId();
projectData.put(projData, id);
annotationSets.put(id, null);
addDataToGroup(projData, parentGroup);
notifyListeners();
return true;
}
/**
* Adds the given ProjectData to the Project as addProjectData(IProjectData,
* ProjectGroup) but uses the root ProjectData group as a default
* ProjectGroup.
*/
public boolean addProjectData(IProjectData projData){
return addProjectData(projData, projDataGroup);
}
/**
* Adds the given Result to the Project as dependent upon all of the
* ProjectData provided in the analyzedData collection. Null Results or
* ProjectData collections are disallowed, and all of the ProjectData
* objects in the collection must be present within this Project.
* The Result is added to the given ProjectGroup.
*
* @param result The Result to be added.
* @param analyzedData A collection of ProjectData objects upon which the
* Result is dependent.
* @param parentGroup The ProjectGroup that the Result is to be placed in.
*
* @return True iff the Result was added successfully, false otherwise.
*/
public boolean addResult(Result result,
Collection<IProjectData> analyzedData, ProjectGroup parentGroup){
HashSet<Integer> dataIds = new HashSet<Integer>();
if(analyzedData == null || analyzedData.isEmpty()){
return false;
}
for(IProjectData projData: analyzedData){
if(!containsProjectData(projData)){
return false;
}
dataIds.add(projectData.get(projData));
}
if(addProjectData(result, parentGroup)){
results.put(result, dataIds);
return true;
}
return false;
}
/**
* Adds the given Result to the Project as addResult(Result,
* Collection<IProjectData>, ProjectGroup) but uses the root Results group
* as a default ProjectGroup.
*/
public boolean addResult(Result result,
Collection<IProjectData> analyzedData){
return addResult(result, analyzedData, resultsGroup);
}
/**
* Adds the given AnnotationSet to the Project as markup for the given
* ProjectData. Both the AnnotationSet and ProjectData objects must not be
* null, and the ProjectData object must be both in the Project and not
* already annotated.
* The AnnotationSet is added to the root annotations group as a default.
*
* @param annotationSet The AnnotationSet to be added to the Project.
* @param annotatedData The ProjectData that the AnnotationSet is marking up.
*
* @return True iff the AnnotationSet was successfully added, false
* otherwise.
*/
public boolean addAnnotation(AnnotationSet annotationSet, IProjectData annotatedData){
int dataId;
if(containsProjectData(annotatedData) && !isAnnotated(annotatedData)){
dataId = projectData.get(annotatedData);
if(addProjectData(annotationSet, annotationsGroup)){
annotationSets.put(dataId, annotationSet);
return true;
}
}
return false;
}
/**
* Adds the given ProjectGroup to this Project if it wasn't already added.
*
* @param newGroup The new ProjectGroup to be added.
*
* @return True if the ProjectGroup was added successfully, false
* otherwise.
*/
public boolean addGroup(ProjectGroup newGroup){
if(newGroup == null || groups.containsKey(newGroup)){
return false;
}
int id = getNextId();
groups.put(newGroup, id);
groupContents.put(id, new HashSet<Integer>());
notifyListeners();
return true;
}
/**
* Adds the given Project Data to the given ProjectGroup if they are both
* in this Project.
* Note: This also removes the Project Data from its previous group,
* if any, as Project Data may only be in one group at a time.
*
* @param projData The Project Data to be added to the group.
* @param group The ProjectGroup into which the Project Data is to be
* added.
*
* @return True iff the data was added successfully to the group, false
* otherwise.
*/
public boolean addDataToGroup(IProjectData projData, ProjectGroup group){
if(containsGroup(group) && containsProjectData(projData)){
int projDataId = projectData.get(projData);
removeProjectDataFromGroup(projDataId);
groupContents.get(groups.get(group)).add(projDataId);
//TODO: Move the ProjectData File as needed
return true;
}
return false;
}
/**
* Attempts to remove the given ProjectData from this Project and returns
* whether or not the operation was successful.
*
* @param projData The ProjectData to be removed from the Project.
*
* @return True iff the ProjectData was removed, false if it doesn't exist.
*/
public boolean removeProjectData(IProjectData projData){
//TODO: Should this function also remove associated Results/Annotations?
if(containsProjectData(projData)){
int projDataId = projectData.remove(projData);
removeProjectDataFromGroup(projDataId);
notifyListeners();
return true;
}
return false;
}
/**
* Removes the given Result from this Project if it exists.
*
* @param result The Result to be removed.
*
* @return True iff the Result was removed, false if it doesn't exist.
*/
public boolean removeResult(Result result){
if(results.containsKey(result)){
results.remove(result);
removeProjectData(result);
return true;
}
return false;
}
/**
* Removes from this Project the AnnotationSet associated with the given
* ProjectData if there is one.
*
* @param annotatedData The annotated ProjectData for which to remove the
* AnnotationSet.
*
* @return True iff the AnnotationSet was removed, false if the given
* ProjectData was not annotated.
*/
public boolean removeAnnotationFrom(IProjectData annotatedData){
if(containsProjectData(annotatedData)){
int id = projectData.get(annotatedData);
if(annotationSets.containsKey(id)){
AnnotationSet removedSet = annotationSets.remove(id);
removeProjectData(removedSet);
return true;
}
}
return false;
}
/**
* Removes from this Project the given ProjectGroup and all of its child
* groups.
*
* @param group The ProjectGroup to be removed.
*
* @return True iff the ProjectGroup was removed, false if the group was
* not in this Project to begin with.
*/
public boolean removeGroup(ProjectGroup group){
if(containsGroup(group)){
int id = groups.remove(group);
groupContents.remove(id);
group.removeParent();
for(ProjectGroup childGroup: group.getChildren()){
removeGroup(childGroup);
}
notifyListeners();
return true;
}
return false;
}
/**
* Returns whether or not the given ProjectData is in this Project.
*/
public boolean containsProjectData(IProjectData projData){
if(projData == null){
return false;
}
return projectData.containsKey(projData);
}
/**
* Returns whether or not this Project contains ProjectData of the given
* name.
*/
public boolean containsProjectData(String projDataName){
return getProjectData(projDataName) != null;
}
/**
* Returns whether or not the given ProjectGroup is in this Project.
*/
public boolean containsGroup(ProjectGroup group){
return groups.containsKey(group);
}
/**
* Returns whether or not this Project contains a ProjectGroup of the given
* name.
*/
public boolean containsGroup(String groupName){
return getGroup(groupName) != null;
}
/**
* Returns the ProjectData with the given name if one such ProjectData
* object exists in this Project.
*
* @param projDataName The name of the ProjectData to return.
*
* @return The ProjectData instance of the given name, or null.
*/
public IProjectData getProjectData(String projDataName){
for(IProjectData data: projectData.keySet()){
if(data.getName().equals(projDataName)){
return data;
}
}
return null;
}
/**
* Returns a collection of all the ProjectData objects in this Project.
*/
public Collection<IProjectData> getProjectData(){
return projectData.keySet();
}
/**
* Returns all of the Results in this Project.
*/
public Collection<Result> getResults(){
return results.keySet();
}
/**
* Returns all of the original Project Data objects in this Project (i.e.
* neither AnnotationSets nor Results).
*/
public Collection<IProjectData> getOriginalData(){
ArrayList<IProjectData> originalData = new ArrayList<IProjectData>();
for(IProjectData projData: projectData.keySet()){
if(!(projData instanceof Result) && !(projData instanceof AnnotationSet)){
originalData.add((TextData)projData);
}
}
return originalData;
}
/**
* Returns all of the Project Data associated with the given Result object
* or an empty collection if the Result is not in this Project.
*/
public Collection<IProjectData> getDataForResult(Result result){
if(containsProjectData(result)){
HashSet<IProjectData> affectedData = new HashSet<IProjectData>();
for(int projDataId: results.get(result)){
affectedData.add(getProjectDataById(projDataId));
}
return affectedData;
}
return new HashSet<IProjectData>();
}
/**
* Returns the Project Data associated with the given AnnotationSet object
* or null if the AnnotationSet is not in this Project.
*/
public IProjectData getDataForAnnotation(AnnotationSet annotation){
for(Entry<Integer, AnnotationSet> pair: annotationSets.entrySet()){
if(annotation.compareTo(pair.getValue()) == 0){
return getProjectDataById(pair.getKey());
}
}
return null;
}
/**
* Returns all of the ProjectGroups in this Project.
*/
public Collection<ProjectGroup> getGroups(){
return groups.keySet();
}
/**
* Returns the ProjectGroup with the given name if one exists in this
* Project.
*
* @param groupName The display name of the ProjectGroup to be returned.
*
* @return The ProjectGroup instance of the given name, or null.
*/
public ProjectGroup getGroup(String groupName){
for(ProjectGroup group: groups.keySet()){
if(group.getName().equals(groupName)){
return group;
}
}
return null;
}
/**
* Returns all of the Project Data contained within the given ProjectGroup
* or an empty collection if there is nothing in the group.
*/
public Collection<IProjectData> getDataInGroup(ProjectGroup group){
TreeSet<IProjectData> groupedData = new TreeSet<IProjectData>();
if(containsGroup(group)){
int groupId = groups.get(group);
for(int projDataId: groupContents.get(groupId)){
groupedData.add(getProjectDataById(projDataId));
}
}
return groupedData;
}
/**
* Returns the ProjectGroup containing the given ProjectData or null if the
* given data does not exist in this Project.
*/
public ProjectGroup getGroupFor(IProjectData projData){
if(containsProjectData(projData)){
int id = projectData.get(projData);
for(Entry<Integer, HashSet<Integer>> groupPair:
groupContents.entrySet()){
for(Integer dataId: groupPair.getValue()){
if(id == dataId){
return getGroupById(groupPair.getKey());
}
}
}
}
return null;
}
/**
* Returns whether or not the given ProjectData object is annotated in this
* Project.
*/
public boolean isAnnotated(IProjectData projData){
if(containsProjectData(projData)){
int id = projectData.get(projData);
return annotationSets.get(id) != null;
}
return false;
}
/**
* Returns the AnnotationSet associated with the given ProjectData (if any).
*/
public AnnotationSet getAnnotation(IProjectData projData){
if(containsProjectData(projData)){
int id = projectData.get(projData);
return annotationSets.get(id);
}
return null;
}
/**
* Returns whether or not the given ProjectData has any Results associated
* with it.
*
* @param projData The ProjectData for which to search for Results.
*
* @return True iff the given ProjectData has Results, false otherwise.
*/
public boolean hasResults(IProjectData projData){
if(containsProjectData(projData)){
int id = projectData.get(projData);
for(HashSet<Integer> dataIds: results.values()){
for(Integer projectDataId: dataIds){
if(projectDataId == id){
return true;
}
}
}
}
return false;
}
/**
* Returns all of the Results which are associated with the given
* ProjectData.
*
* @param projData The ProjectData for which to look up Results.
*
* @return A collection of Results associated with projData or an empty
* collection if there are none or it does not exist in this
* Project.
*/
public Collection<Result> getResults(IProjectData projData){
ArrayList<Result> dataResults = new ArrayList<Result>();
if(containsProjectData(projData)){
int id = projectData.get(projData);
for(Entry<Result, HashSet<Integer>> resultPair: results.entrySet()){
for(int projectDataId: resultPair.getValue()){
if(projectDataId == id){
dataResults.add(resultPair.getKey());
break;
}
}
}
}
return dataResults;
}
/**
* Returns whether or not there exists the given Result type for the given
* ProjectData.
*
* @param projData The ProjectData for which to look up the given Result
* type.
* @param clazz The type of Result for which to search.
*
* @return True iff the given ProjectData has a Result of the given type,
* false otherwise.
*/
public <T extends Result> boolean hasResultType(IProjectData projData, Class<T> clazz){
return getResultType(projData, clazz) != null;
}
/**
* Returns the given Result type for the given ProjectData if it exists.
*
* @param projData The ProjectData for which to look up the given Result
* type.
* @param clazz The type of Result for which to search.
*
* @return A Result of the given type for the given ProjectData, or null
* if either the ProjectData does not exist or there was no
* matching Result.
*/
public <T extends Result> Result getResultType(IProjectData projData,
Class<T> clazz){
if(containsProjectData(projData)){
int id = projectData.get(projData);
for(Entry<Result, HashSet<Integer>> resultPair: results.entrySet()){
if(resultPair.getKey().getClass() == clazz){
for(int projectDataId: resultPair.getValue()){
if(projectDataId == id){
return resultPair.getKey();
}
}
}
}
}
return null;
}
/**
* Creates all of the necessary files and directories for an empty Project
* of this Project's name and parent directory.
*
* @return True iff all files and folders were created successfully, false
* otherwise.
*
* @throws IOException If files or directories cannot be created in the
* Project's parent directory.
*/
public boolean createProjectFiles() throws IOException{
IPath projectDir = getProjectDirectory();
if(projectDir == null || Files.exists(projectDir.toFile().toPath()) ||
hasProjectFiles){
return false;
}
//Create root project directory
Files.createDirectory(projectDir.toFile().toPath());
for(ProjectGroup group: groups.keySet()){
group.createGroupDirectory(projectDir);
}
//Create project file
updateProjectFile();
hasProjectFiles = true;
return true;
}
/**
* Deletes all ProjectData from disk as well as the Project file and all
* Project directories.
*
* @return True iff all ProjectData, files, and directories were deleted
* successfully, false if the Project had no Project files.
*
* @throws IOException If any of the Project's files could not be deleted
* from disk for any reason.
*/
public boolean deleteProjectContentsOnDisk() throws IOException{
if(hasProjectFiles)
{
//Delete all ProjectData
for(IProjectData projData: projectData.keySet()){
projData.deleteContentsOnDisk();
}
deleteProjectFiles();
//Delete all ProjectGroups
for(ProjectGroup group: groups.keySet()){
group.deleteGroupDirectory(getProjectDirectory());
}
hasProjectFiles = false;
return true;
}
return false;
}
/**
* Updates the project file to reflect the current state of the Project.
* Creates the file if it does not already exist.
*
* @throws IOException
*/
public void updateProjectFile() throws IOException{
Path projectFilePath = getProjectFile().toFile().toPath();
try(BufferedWriter writer = Files.newBufferedWriter(projectFilePath,
Charset.defaultCharset())){
String jsonStr = ProjectTranslator.toJson(this);
if(jsonStr != null){
writer.write(jsonStr);
}
else{
//TODO: Throw exception of some sort
}
}
catch(IOException ioe){
ioe.printStackTrace();
}
}
/**
* Returns the Project name.
*/
public String toString(){
return getName();
}
@Override
public int hashCode(){
return getName().hashCode();
}
/**
* Registers the given listener which will get notified whenever this
* Project is modified.
*
* @param listener The listener to be registered.
*/
public void addListener(ProjectListener listener){
listeners.add(listener);
}
/**
* Unregisters the given listener such that it will no longer receive
* notifications when this Project is modified.
*
* @param listener The listener to be unregistered.
*/
public void removeListener(ProjectListener listener){
listeners.remove(listener);
}
/**
* Notifies all listeners on this Project that it was modified.
*/
private void notifyListeners(){
for(ProjectListener listener: listeners){
listener.notify(this);
}
}
/**
* Deletes the Project file and all Project sub-directories, followed by the
* Project directory itself.
*
* @throws IOException If any of the files/directories could not be deleted
* for any reason.
*/
private void deleteProjectFiles() throws IOException{
IPath projectDir = getProjectDirectory();
//Delete project file
Files.delete(getProjectFile().toFile().toPath());
//Delete root project directory
Files.delete(projectDir.toFile().toPath());
}
/**
* Looks up Project Data by its id.
*/
private IProjectData getProjectDataById(int id){
for(Entry<IProjectData, Integer> projData: projectData.entrySet()){
if(projData.getValue() == id){
return projData.getKey();
}
}
return null;
}
/**
* Looks up a ProjectGroup by its id.
*/
private ProjectGroup getGroupById(int id){
for(Entry<ProjectGroup, Integer> group: groups.entrySet()){
if(group.getValue() == id){
return group.getKey();
}
}
return null;
}
/**
* Removes the Project Data with the given id from its parent ProjectGroup.
* Note: This function leaves the Project Data in an invalid state, as it is
* not belonging to at least one ProjectGroup.
*/
private void removeProjectDataFromGroup(int projDataId){
for(HashSet<Integer> contents: groupContents.values()){
for(int dataId: contents){
if(dataId == projDataId){
contents.remove(projDataId);
return;
}
}
}
}
private int getNextId(){
return lastId++;
}
/**
* Simple listener for receiving notifications when a Project is modified.
*
* @author Kyle Mullins
*/
public abstract static class ProjectListener {
public abstract void notify(Project modifiedProj);
}
}