Java tutorial
/** * Copyright (c) 2008-2015 Regents of the University of California (Regents). * Created by WISE, Graduate School of Education, University of California, Berkeley. * * This software is distributed under the GNU General Public License, v3, * or (at your option) any later version. * * Permission is hereby granted, without written agreement and without license * or royalty fees, to use, copy, modify, and distribute this software and its * documentation for any purpose, provided that the above copyright notice and * the following two paragraphs appear in all copies of this software. * * REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED * HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE * MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. * * IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, * SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF * REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.wise.vle.utils; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.ServletContext; import javax.servlet.ServletException; import org.apache.commons.io.FileUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.wise.portal.domain.module.impl.CurnitGetCurnitUrlVisitor; import org.wise.portal.domain.project.Project; /** * Servlet implementation class for Servlet: FileManager * * @author Patrick Lawler */ public class FileManager { static final long serialVersionUID = 1L; private final static String COMMAND = "command"; private final static String PARAM1 = "param1"; @SuppressWarnings("unused") private final static String PARAM2 = "param2"; @SuppressWarnings("unused") private final static String PARAM3 = "param3"; @SuppressWarnings("unused") private final static String PARAM4 = "param4"; private final static String PROJECT_PATHS = "projectPaths"; private static boolean standAlone = true; private boolean modeRetrieved = false; private static Properties wiseProperties = null; static { try { // Read properties file. wiseProperties = new Properties(); wiseProperties.load(FileManager.class.getClassLoader().getResourceAsStream("wise.properties")); } catch (Exception e) { System.err.println("FileManager could not read in wiseProperties file"); e.printStackTrace(); } } /** * Returns a '|' delimited String of all projects, returns an empty String if no projects exist * * @return String */ public static String getProjectList(String projectPaths, String projectExt) throws IOException { String[] paths = projectPaths.split("~"); List<String> visited = new ArrayList<String>(); List<String> projects = new ArrayList<String>(); String projectList = ""; if (paths != null && paths.length > 0) { for (int p = 0; p < paths.length; p++) { getProjectFiles(new File(paths[p]), projects, visited, projectExt); } CompareByLastModified compareByLastModified = new CompareByLastModified(); Collections.sort(projects, compareByLastModified); for (int q = 0; q < projects.size(); q++) { projectList += projects.get(q); if (q != projects.size() - 1) { projectList += "|"; } } return projectList; } else { return ""; } } /** * Given a file, a List of projects, and a list of visited directories, recursively adds * any project files to the list of projects that are in any subdirectories (n-deep). * * @param file * @param projects * @param visited * @throws IOException */ public static void getProjectFiles(File f, List<String> projects, List<String> visited, String projectExt) throws IOException { if (f.exists() && !visited.contains(f.getCanonicalPath())) { if (f.isFile()) { if (f.getName().endsWith(projectExt)) { projects.add(f.getAbsolutePath()); } else { return; } } else if (f.isDirectory()) { visited.add(f.getCanonicalPath()); if (!f.getCanonicalPath().contains(".svn")) { String children[] = f.list(); for (int y = 0; y < children.length; y++) { getProjectFiles(new File(f, children[y]), projects, visited, projectExt); } } } else { throw new IOException("Not a file and not a directory. I don't know what it is."); } } else { return; } } /** * A Comparator that compares two <code>String</code> paths by the date it was last modified. */ public static class CompareByLastModified implements Comparator<String> { public int compare(String arg0, String arg1) { File file1 = new File(arg0); File file2 = new File(arg1); if (file1.lastModified() == file2.lastModified()) { return 0; } else if (file1.lastModified() > file2.lastModified()) { return -1; } else { return 1; } } } /** * Returns true if given directory exists, if not returns whether the * creation of the given directory was successful * * @return boolean */ public static boolean ensureDir(File file) { if (file.isDirectory()) { return true; } else { return file.mkdir(); } } /** * Given a <code>File</code> the file to write to and the <code>String</code> data to write * to that file and a <code>boolean</code> overwrite, indicating whether the file should be * overwritten if it exists, writes the data to the file specified. * * @param <code>File</code> file * @param <code>String</code> data * @param <code>boolean</code> overwrite * @throws <code>IOException</code> */ public static void writeFile(File file, String data, boolean overwrite) throws IOException { if (!file.exists() || overwrite) { /* create a new file if it doesn't exist */ if (!file.exists()) { file.createNewFile(); } /* write the data to the file */ Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8")); writer.write(data); writer.close(); } else { throw new IOException("File already exists and forced overwrite not set. Could not write file. " + file.getAbsolutePath()); } } /** * Given a <code>String</code> path to the file to write to and the <code>String</code> data * to write to that file and a <code>boolean</code> overwrite, indicating whether the file * should be overwritten if it exists, writes the data to the file specified. * * @param <code>File</code> file * @param <code>String</code> data * @param <code>boolean</code> overwrite * @throws <code>IOException</code> */ public static void writeFile(String path, String data, boolean overwrite) throws IOException { File file = new File(path); writeFile(file, data, overwrite); } /** * Given a <code>File</code>, reads the text from the file as a <code>String</code> * and returns the string. * * @param <code>File</code> file * @return <code>String</code> text * @throws <code>IOException</code> */ public static String getFileText(File file) throws IOException { String result = "error"; if (file.exists()) { BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF8")); String current = br.readLine(); String fullText = ""; while (current != null) { fullText += current + System.getProperty("line.separator"); current = br.readLine(); } br.close(); result = fullText; } else { throw new IOException("Could not find specified file " + file.getAbsolutePath()); } return result; } /** * Get the text in the file * @param filePath the file path * @return the text in the file * @throws IOException */ public static String retrieveFile(String filePath) throws IOException { return getFileText(new File(filePath)); } /** * Given a request, extracts the project name, file name and the data * to be written to the file and writes it to the specified file * * @param request * @return String * @throws IOException */ /** * Update the contents of a file * @param projectFolderPath the project folder path * @param fileName the file name * @param data the text content to put into the file * @return the text that specifies whether we were successful or not * @throws IOException */ public static String updateFile(String projectFolderPath, String fileName, String data) throws IOException { String result = "not authorized"; File dir = new File(projectFolderPath); if (dir.exists()) { File file = new File(dir, fileName); writeFile(file, data, true); result = "success"; } return result; } /** * Creates a project in the curriculum directory * * @param curriculumBaseDir the curriculum base * e.g. * /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum * * @param projectName the project name * * @return int * @throws IOException * @throws JSONException * @throws ServletException */ public static String createProject(String curriculumBaseDir, String projectName) throws IOException, ServletException { String result = ""; File parent = new File(curriculumBaseDir); ensureDir(parent); //all project json files will be given the filename of wise4.project.json File newProjectPath = createNewprojectPath(parent); // also make assets directory File newProjectAssetsDir = new File(newProjectPath, "assets"); newProjectAssetsDir.mkdir(); File newFile = new File(newProjectPath, "wise4.project.json"); try { //write the empty project json to the file writeFile(newFile, Template.getProjectTemplate(projectName).toString(3), false); //get the folder name e.g. 513 String folder = newFile.getParentFile().getName(); //get the file name e.g. wise4.project.json String fileName = newFile.getName(); //return the path to the file result = "/" + folder + "/" + fileName; } catch (JSONException e) { e.printStackTrace(); } return result; } /** * Creates a WISE5 project in the curriculum directory * * @param curriculumBaseDir the curriculum base * e.g. * /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum * * @param projectName the project name * * @return int * @throws IOException * @throws JSONException * @throws ServletException */ public static String createWISE5Project(String curriculumBaseDir) { String result = ""; File parent = new File(curriculumBaseDir); ensureDir(parent); // get the path to the new project File newProjectPath = createNewprojectPath(parent); // also make assets directory File newProjectAssetsDir = new File(newProjectPath, "assets"); newProjectAssetsDir.mkdir(); File newFile = new File(newProjectPath, "project.json"); try { //write the empty project json to the file writeFile(newFile, "", false); //get the folder name e.g. 513 String folder = newFile.getParentFile().getName(); //get the file name e.g. project.json String fileName = newFile.getName(); //return the path to the file e.g. /513/project.json result = "/" + folder + "/" + fileName; } catch (IOException e) { e.printStackTrace(); } return result; } /** * Given a parent directory, attempts to generate and return * a unique project directory * * @param parent * @return */ public static File createNewprojectPath(File parent) { Integer counter = 1; while (true) { File tryMe = new File(parent, String.valueOf(counter)); if (!tryMe.exists()) { tryMe.mkdir(); return tryMe; } counter++; } } /** * Given a request, extracts the project name, the file name and the node type to * be created and creates the node and adds it to the associated project. * * @param request * @return String */ /** * Creates a node and adds it to the project * @param projectPath the project path * @param nodeClass the class for the node * @param title the title of the step * @param type the type of step * @param nodeTemplateParams a JSONArray string that specifies what files to create * @return the node name * @throws IOException * @throws ServletException */ public static String createNode(String projectPath, String nodeClass, String title, String type, String nodeTemplateParams) throws IOException, ServletException { /* * the node name, "nodeNotProject" is the default value to return * which means there was an error creating the file. this variable * will be changed below once the file is created. */ String nodeName = "nodeNotProject"; File dir = new File(projectPath).getParentFile(); if (dir.exists()) { try { //create the JSONArray of files to create JSONArray filesToCreate = new JSONArray(nodeTemplateParams); if (filesToCreate != null) { /* * get the root of the file name for the files we are about to make * e.g. 'node_1' */ String fileNamePrefix = getUniqueFileNamePrefix(dir); //loop through each file to create for (int x = 0; x < filesToCreate.length(); x++) { //get the a file to create JSONObject fileToCreate = filesToCreate.getJSONObject(x); //get the extension for the file type String nodeExtension = fileToCreate.getString("nodeExtension"); //get the content to put in the file String nodeTemplateContent = fileToCreate.getString("nodeTemplateContent"); if (nodeExtension != null && !nodeExtension.equals("") && nodeTemplateContent != null && !nodeTemplateContent.equals("")) { /* * whether this is the main file for this step. */ boolean mainNodeFile = false; if (filesToCreate.length() == 1) { //in most cases there is only one file to create so it will be the main file mainNodeFile = true; } else { /* * in rare cases there may be multiple files to create such * as with HtmlNode in which case one of the files is the * mainNodeFile (.ht) and the other is a supporting file (.html). * for these cases the param for each file must specify * whether it is the mainNodeFile or not */ mainNodeFile = fileToCreate.getBoolean("mainNodeFile"); } //create the file handle e.g. 'node_1.or' File file = new File(dir, fileNamePrefix + "." + nodeExtension); //write the contents to the file writeFile(file, nodeTemplateContent, false); //add the node to the project if (mainNodeFile && addNodeToProject(new File(projectPath), Template.getProjectNodeTemplate(type, file.getName(), title, nodeClass))) { nodeName = file.getName(); } } } } } catch (JSONException e) { e.printStackTrace(); } } else { throw new IOException("Unable to find project"); } return nodeName; } /** * Given a node type, returns the associated file extension, if the * node type is unknown, throws Servlet Exception * * @param type * @return String * @throws ServletException */ public static String getExtension(String type) throws ServletException { if (type.equals("BrainstormNode")) { return ".bs"; } else if (type.equals("FillinNode")) { return ".fi"; } else if (type.equals("HtmlNode") || type.equals("DrawNode")) { return ".ht"; } else if (type.equals("MySystemNode")) { return ".my"; } else if (type.equals("MatchSequenceNode")) { return ".ms"; } else if (type.equals("MultipleChoiceNode")) { return ".mc"; } else if (type.equals("NoteNode") || type.equals("OpenResponseNode")) { return ".or"; } else if (type.equals("OutsideUrlNode")) { return ".ou"; } else if (type.equals("DataGraphNode")) { return ".dg"; } else if (type.equals("SVGDrawNode")) { return ".sd"; } else if (type.equals("AnnotatorNode")) { return ".an"; } else if (type.equals("MWNode")) { return ".mw"; } else if (type.equals("AssessmentListNode")) { return ".al"; } else if (type.equals("ChallengeNode")) { return ".ch"; } else if (type.equals("BranchNode")) { return ".br"; } else if (type.equals("SensorNode")) { return ".se"; } else if (type.equals("ExplanationBuilderNode")) { return ".eb"; } else { //throw new ServletException("I don't know how to handle nodes of type: " + type); return ".txt"; } } /** * Given a <code>String</code> filename, returns the <code>String</code> extension. * If the filename has no extension, returns the filename. * * @param filename * @return String - extension */ public String getFileExtension(String filename) { if (filename.lastIndexOf(".") == -1) { return filename; } else { return filename.substring(filename.lastIndexOf("."), filename.length()); } } /** * Given a parent directory <code>File</code> and a file extension <code>String</code> * generates and returns a <code>File</code> with a unique filename. * * @param parent * @param ext * @return File */ public static File generateUniqueFile(File parent, String ext) { String name = "node_"; int count = 0; while (true) { File file = new File(parent, name + count + ext); if (!file.exists() && !duplicateName(parent, name + count)) { return file; } count++; } } /** * Get a file name prefix that has not been used yet * @param parent the directory where we want to search files * @return a String containing a file name prefix that is not * being used by any other files. e.g. 'node_2' */ public static String getUniqueFileNamePrefix(File parent) { String name = "node_"; int count = 0; while (true) { //check if the file name prefix has been used yet if (!duplicateName(parent, name + count)) { //it has not been used yet so we can return it return name + count; } count++; } } /** * Returns true if any of the children files in the directory of the given parent * <code>File</code> have the same root name as the given name <code>String</code>, * otherwise, returns false. * * @param parent * @param name * @return boolean */ public static boolean duplicateName(File parent, String name) { String[] children = parent.list(); for (int i = 0; i < children.length; i++) { File childFile = new File(parent, children[i]); if (!childFile.isDirectory()) { //find the last index of dot int lastIndexOfDot = children[i].lastIndexOf("."); if (lastIndexOfDot != -1) { //the filename contains a dot String childName = children[i].substring(0, children[i].lastIndexOf(".")); if (childName.equals(name)) { return true; } } } } return false; } /** * Given a <code>File</code> project file and a <code>JSONObject</code> node, * inserts the node into the project file and writes the project file to the * file system. Returns true if operation is successful, otherwise throws * <code>IOException</code> * * @param parent the project file * @param node the node to add to the project * @return boolean * @throws IOException */ public static boolean addNodeToProject(File parent, JSONObject node) throws IOException { try { JSONObject project = new JSONObject(getFileText(parent)); project.getJSONArray("nodes").put(node); writeFile(parent, project.toString(3), true); return true; } catch (JSONException e) { e.printStackTrace(); throw new IOException("Unable to add node to project."); } } /** * Given a request, extracts the project name and sequence name and creates * a new sequence with the given name in the project. Throws IOException if * the file could not be found and if the servlet is unable to insert it into * the project file. * * @param request * @return String * @throws IOException */ /** * Create a sequence in the project * @param projectFileName the project file name * @param name the name of the sequence to create * @param id the id of the sequence to create * @param projectFolderPath the path to the project folder * @return the id of the sequence * @throws IOException */ public static String createSequence(String projectFileName, String name, String id, String projectFolderPath) throws IOException { String result = ""; /* * get the full project file path * e.g. * /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/667/wise4.project.json */ String fullProjectFilePath = projectFolderPath + projectFileName; File file = new File(fullProjectFilePath); try { addSequenceToProject(file, Template.getSequenceTemplate(id, name)); result = id; } catch (JSONException e) { e.printStackTrace(); } return result; } /** * Given a <code>File</code> project file and a <code>JSONObject</code> sequence, * inserts the sequence into the project and saves the project file. * * @param <code>projectFile</code> project file * @param <code>JSONObject</code> sequence * @throws <code>IOException</code> */ public static void addSequenceToProject(File projectFile, JSONObject sequence) throws IOException { try { JSONObject project = new JSONObject(getFileText(projectFile)); project.getJSONArray("sequences").put(sequence); writeFile(projectFile, project.toString(3), true); } catch (JSONException e) { e.printStackTrace(); throw new IOException("Could not insert new sequence in project file."); } } /** * Create a file in the specified folder and put data in the file * @param projectFolderPath the project folder path * @param fileName the file name * @param data the data to put in the file */ public static String createFile(String projectFolderPath, String fileName, String data) throws ServletException, IOException { String result = ""; /* * get the full file path * e.g. * /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/667/node_1.or */ String fullFilePath = projectFolderPath + fileName; writeFile(fullFilePath, data, false); return result; } /** * Remove a file from a project folder * * @param projectFolderPath the project folder path * @param fileName the file name to remove * @return whether it was a success or failure * @throws IOException */ public static String removeFile(String projectFolderPath, String fileName) throws IOException { File child = new File(new File(projectFolderPath), fileName); if (child.exists() && child.delete()) { return "success"; } else { return "failure"; } } /** * Given a <code>HttpServletRequest</code> request with parameters containing * path - path to project directory that we wish to copy and projectPath - the * default project directory location (to create projects), copies the directory * and returns <code>String</code> the path to the freshly copied directory. * * @param curriculumBaseDir the path to the curriculum directory * @param projectFolderPath the project folder path of the project we are copying * @return the path to the new project * @throws IOException */ public static String copyProject(String curriculumBaseDir, String projectFolderPath) throws IOException { String result = ""; File srcDir = new File(projectFolderPath); if (srcDir.exists() && srcDir.isDirectory()) { File destDir; if (curriculumBaseDir != null && curriculumBaseDir != "") { destDir = createNewprojectPath(new File(curriculumBaseDir)); } else { destDir = createNewprojectPath(srcDir.getParentFile()); } copy(srcDir, destDir); result = destDir.getName(); } else { throw new IOException("Provided path is not found or is not a directory. Path: " + projectFolderPath); } return result; } /** * Copies the given <code>File</code> src to the given <code>File</code> dest. If they * are directories, recursively copies the contents of the directories. * * @param File src * @param File dest * @throws FileNotFoundException * @throws IOException */ public static void copy(File src, File dest) throws FileNotFoundException, IOException { if (src.isDirectory()) { if (!dest.exists()) { dest.mkdir(); } String[] files = src.list(); for (int a = 0; a < files.length; a++) { copy(new File(src, files[a]), new File(dest, files[a])); } } else { InputStream in = new FileInputStream(src); FileOutputStream out = new FileOutputStream(dest); byte[] buffer = new byte[2048]; int len; while ((len = in.read(buffer)) > 0) { out.write(buffer, 0, len); } in.close(); out.close(); } } /** * Copy a node in a project * @param projectFolderPath the project folder path * @param projectFileName the project file name * @param data the data to put in the new node * @param type the node type * @param title the node title * @param nodeClass the node class * @param contentFile the file name of the node we are copying * @return the new file name * @throws IOException * @throws ServletException */ public static String copyNode(String projectFolderPath, String projectFileName, String data, String type, String title, String nodeClass, String contentFile) throws IOException, ServletException { String result = ""; /* * get the full path to the file * /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/667/node_1.or */ String fullProjectFilePath = projectFolderPath + projectFileName; File dir = new File(fullProjectFilePath).getParentFile(); if (dir.exists()) { File file = generateUniqueFile(dir, getExtension(type)); /* if this is an html type, change the src filename */ if (type.equals("HtmlNode") || type.equals("DrawNode") || type.equals("MySystemNode")) { try { File content = new File(dir, contentFile); if (content.exists()) { writeFile(new File(dir, file.getName() + "ml"), getFileText(content), false); } JSONObject node = new JSONObject(data); node.put("src", file.getName() + "ml"); writeFile(file, node.toString(3), false); } catch (JSONException e) { throw new ServletException(e); } } else { writeFile(file, data, false); } File parent = new File(fullProjectFilePath); try { if (addNodeToProject(parent, Template.getProjectNodeTemplate(type, file.getName(), title, nodeClass))) { result = file.getName(); } else { throw new IOException( "New node file created: " + file.getName() + " but could not update project file."); } } catch (JSONException e) { e.printStackTrace(); throw new IOException( "New node file created: " + file.getName() + " but could not update project file."); } } else { throw new IOException("Cannot find provided path, aborting operation."); } return result; } /** * Create a sequence in a project * @param projectFolderPath the project folder path * @param projectFileName the project file name * @param data the data for the sequence * @return whether it was a success or not * @throws IOException * @throws ServletException */ public static String createSequenceFromJSON(String projectFolderPath, String projectFileName, String data) throws IOException, ServletException { String result = ""; String fullProjectFilePath = projectFolderPath + projectFileName; File projectFile = new File(fullProjectFilePath); try { JSONObject sequence = new JSONObject(data); JSONObject project = new JSONObject(getFileText(projectFile)); project.getJSONArray("sequences").put(sequence); writeFile(projectFile, project.toString(3), true); result = "success"; } catch (JSONException e) { throw new ServletException(e); } return result; } /** * Retrieves all of the scripts in the scripts array and writes them out in the <code>HttpServletResponse</code> * @param context the context to retrieve the files from * @param data the script file names * @return the contents of the scripts * @throws IOException */ public static String getScripts(ServletContext context, String data) throws IOException { String[] scripts = data.split("~"); StringBuffer scriptsText = new StringBuffer(); String out = ""; for (String script : scripts) { InputStream is = context.getResourceAsStream("/" + script); if (is != null) { BufferedReader reader = new BufferedReader(new InputStreamReader(is)); while ((out = reader.readLine()) != null) { scriptsText.append(out); scriptsText.append("\n"); } } } scriptsText.append( "scriptloader.scriptAvailable(scriptloader.baseUrl + \"vle/filemanager.html?command=getScripts¶m1=" + data + "\");"); scriptsText.append("\n"); return scriptsText.toString(); } /** * Compare the parent and child projects to find differences. * These differences include whether a node was added, deleted, * moved, or not moved and whether the content for the node * was modified. * @param curriculumBaseDir the curriculum directory * @param parentProjectUrl the parent project url e.g. 236/wise4.project.json * @param projectUrl the child project url e.g. 235/wise4.project.json * @return the results of the analysis of the difference between the * parent and child project * @throws IOException */ public static String reviewUpdateProject(String curriculumBaseDir, String parentProjectUrl, String projectUrl) throws IOException { //stores node id to the node or sequence JSONObject for child project nodes HashMap<String, JSONObject> childNodeIdToNodeOrSequence = new HashMap<String, JSONObject>(); //stores the filename to the node id for child project nodes HashMap<String, String> childFileNameToId = new HashMap<String, String>(); //stores the node id to the node step number for child project nodes HashMap<String, String> childNodeIdToStepNumber = new HashMap<String, String>(); //stores node id to the node or sequence JSONObject for parent project nodes HashMap<String, JSONObject> parentNodeIdToNodeOrSequence = new HashMap<String, JSONObject>(); //stores the filename to the node id for parent project nodes HashMap<String, String> parentFileNameToNodeId = new HashMap<String, String>(); //stores the node id to the node step number for parent project nodes HashMap<String, String> parentNodeIdToStepNumber = new HashMap<String, String>(); //stores the .html file name to the .ht node id HashMap<String, String> htmlToHt = new HashMap<String, String>(); /* * stores the node id to status of the node. status can be * "added" * "deleted" * "moved" * "not moved" */ HashMap<String, String> nodeIdToStatus = new HashMap<String, String>(); /* * stores the node id to whether that node was modified or not. modified can be * "true" * "false" * (note these are String values) */ HashMap<String, String> nodeIdToModified = new HashMap<String, String>(); String fileSeparator = System.getProperty("file.separator"); //get the child project folder e.g. /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/236 String fullProjectFolderUrl = curriculumBaseDir + projectUrl.substring(0, projectUrl.lastIndexOf(fileSeparator)); //get the child project file e.g. /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/236/wise4.project.json String fullProjectFileUrl = curriculumBaseDir + projectUrl; //get the parent project folder e.g. /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/235 String fullParentProjectFolderUrl = curriculumBaseDir + parentProjectUrl.substring(0, parentProjectUrl.lastIndexOf(fileSeparator)); //get the parent project file e.g. /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/235/wise4.project.json String fullParentProjectFileUrl = curriculumBaseDir + parentProjectUrl; //get the project JSONObject for parent and child projects JSONObject childProject = getProjectJSONObject(fullProjectFileUrl); JSONObject parentProject = getProjectJSONObject(fullParentProjectFileUrl); //parse the parent and child projects to obtain mappings that we will use later parseProjectJSONObject(childProject, childNodeIdToNodeOrSequence, childFileNameToId, childNodeIdToStepNumber); parseProjectJSONObject(parentProject, parentNodeIdToNodeOrSequence, parentFileNameToNodeId, parentNodeIdToStepNumber); /* * compare the parent and child folders to determine if node * content files have been modified */ compareFolder(new File(fullParentProjectFolderUrl), new File(fullProjectFolderUrl), parentFileNameToNodeId, htmlToHt, nodeIdToModified); /* * compare the sequences in the parent and child projects * to determine if sequences have been added, deleted, moved, * or modified and if nodes have been added, deleted, or * moved (node modification detection is handled in the * compareFolder() call above) */ compareSequences(parentProject, childProject, parentNodeIdToNodeOrSequence, childNodeIdToNodeOrSequence, parentNodeIdToStepNumber, childNodeIdToStepNumber, nodeIdToStatus, nodeIdToModified); /* * a collection of NodeInfo objects for nodes in the child and * parent project */ TreeSet<NodeInfo> childAndParentNodes = new TreeSet<NodeInfo>(new NodeInfoComparator()); /* * a collection to keep track of all the node ids we have * added to the childAndParentNodes collection for quicker * lookup */ TreeSet<String> nodeIdsAdded = new TreeSet<String>(); /* * we must add nodes from the parent project first because we want * to show the author the structure of the parent project and * then add any additional nodes from the child project. we will * show them the parent project structure and how the nodes in the * parent project are different from the child project. any nodes * that are in the child project and not the parent project will * be also added to show that those nodes will be deleted. */ //get all the node ids from the parent project Set<String> parentKeySet = parentNodeIdToStepNumber.keySet(); //loop through all the sequences and nodes in the parent project Iterator<String> parentIdIterator = parentKeySet.iterator(); while (parentIdIterator.hasNext()) { //get a node id String parentId = parentIdIterator.next(); //get the step number for this node id String stepNumber = parentNodeIdToStepNumber.get(parentId); String title = ""; String nodeType = ""; try { //get the JSONObject for the node JSONObject parentNode = parentNodeIdToNodeOrSequence.get(parentId); //get the title and node type title = parentNode.getString("title"); nodeType = parentNode.getString("type"); } catch (JSONException e) { e.printStackTrace(); } //create a NodeInfo object with the info from the node NodeInfo parentNodeInfo = new NodeInfo(stepNumber, parentId, title, nodeType, "parent"); //add the NodeInfo to the collection childAndParentNodes.add(parentNodeInfo); //add the node id to the collection nodeIdsAdded.add(parentId); } //get all the nod ids from the child project Set<String> childKeySet = childNodeIdToStepNumber.keySet(); //loop through all the sequences and nodes in the child project Iterator<String> childIdIterator = childKeySet.iterator(); while (childIdIterator.hasNext()) { //get a node id String childId = childIdIterator.next(); //get the step number for this node id String stepNumber = childNodeIdToStepNumber.get(childId); String title = ""; String nodeType = ""; try { //get the JSONObject for the node JSONObject childNode = childNodeIdToNodeOrSequence.get(childId); //get the title and node type title = childNode.getString("title"); nodeType = childNode.getString("type"); } catch (JSONException e) { e.printStackTrace(); } //check if we have already added a node with this node id if (!nodeIdsAdded.contains(childId)) { //we have not added it before //create a NodeInfo object with the info from the node NodeInfo childNodeInfo = new NodeInfo(stepNumber, childId, title, nodeType, "child"); //add the NodeInfo to the collection childAndParentNodes.add(childNodeInfo); //add the node id to the collection nodeIdsAdded.add(childId); } } /* * the JSONArray that will contain the status info for all the nodes * such as whether a node was added, deleted, moved, or modified */ JSONArray nodeStatuses = new JSONArray(); //loop through all the NodeInfo objects Iterator<NodeInfo> childAndParentNodesIterator = childAndParentNodes.iterator(); while (childAndParentNodesIterator.hasNext()) { //get a node NodeInfo node = childAndParentNodesIterator.next(); //get the info from the node String nodeId = node.getNodeId(); String stepNumber = node.getStepNumber(); String title = node.getTitle(); String nodeType = node.getNodeType(); //get the status of the node ("added", "deleted", "moved", "not moved") String status = nodeIdToStatus.get(nodeId); //get whether the node was modified ("true" or "false") String modified = nodeIdToModified.get(nodeId); if (status == null) { //if there is no status value it means it was not moved status = "not moved"; } if (modified == null) { //if there was no modified value it means it was not modified modified = "false"; } try { //put all the values for this node into a JSONObject JSONObject nodeStatus = new JSONObject(); nodeStatus.put("stepNumber", stepNumber); nodeStatus.put("title", title); nodeStatus.put("nodeId", nodeId); nodeStatus.put("status", status); nodeStatus.put("modified", modified); nodeStatus.put("nodeType", nodeType); //add the node to the array nodeStatuses.put(nodeStatus); } catch (JSONException e) { e.printStackTrace(); } } //return the status array to the client return nodeStatuses.toString(); } /** * Updates the child project by copying the parent project folder to the child project folder * @param curriculumBaseDir the curriculum directory * @param parentProjectUrl the parent project url e.g. 236/wise4.project.json * @param childProjectUrl the child project url e.g. 235/wise4.project.json * @return the result of the update * @throws IOException */ public static String updateProject(String curriculumBaseDir, String parentProjectUrl, String childProjectUrl) throws IOException { String result = ""; String fileSeparator = System.getProperty("file.separator"); //get the child project folder e.g. /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/236 String fullChildProjectFolderUrl = curriculumBaseDir + childProjectUrl.substring(0, childProjectUrl.lastIndexOf(fileSeparator)); //get the parent project folder e.g. /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/235 String fullParentProjectFolderUrl = curriculumBaseDir + parentProjectUrl.substring(0, parentProjectUrl.lastIndexOf(fileSeparator)); //create a backup of the project by renaming the folder renameFolder(fullChildProjectFolderUrl); //copy the parent project folder contents to this project's folder copyFile(new File(fullParentProjectFolderUrl), new File(fullChildProjectFolderUrl)); return result; } /** * Import the steps from one project to another project * @param curriculumBaseDir the curriculum directory * @param fromProjectUrl the project to import from * @param toProjectUrl the from to import to * @param nodeIds the node ids to import * @return the result of the import * @throws IOException */ public static String importSteps(String curriculumBaseDir, String fromProjectUrl, String toProjectUrl, String nodeIds) throws IOException { String result = ""; //the file separator for the OS e.g. / String fileSeparator = System.getProperty("file.separator"); //get the full project file url e.g. /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/236/wise4.project.json String fullToProjectFileUrl = curriculumBaseDir + toProjectUrl; //get the project folder e.g. /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/236 String fullToProjectFolderUrl = curriculumBaseDir + toProjectUrl.substring(0, toProjectUrl.lastIndexOf(fileSeparator)); File toProjectFolder = new File(fullToProjectFolderUrl); //get the project assets folder e.g. /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/236/assets String toProjectAssetsUrl = fullToProjectFolderUrl + "/assets"; File toProjectAssetsFolder = new File(toProjectAssetsUrl); //get the full project file url e.g. /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/172/wise4.project.json String fullFromProjectFileUrl = curriculumBaseDir + fromProjectUrl; //get the project folder e.g. /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/172 String fullFromProjectFolderUrl = curriculumBaseDir + fromProjectUrl.substring(0, fromProjectUrl.lastIndexOf(fileSeparator)); File fromProjectFolder = new File(fullFromProjectFolderUrl); //get the project assets folder e.g. /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/172/assets String fromProjectAssetsUrl = fullFromProjectFolderUrl + "/assets"; File fromProjectAssetsFolder = new File(fromProjectAssetsUrl); //get the project file in the "from" project String fromProjectFileContent = FileUtils.readFileToString(new File(fullFromProjectFileUrl)); JSONObject fromProjectJSON = null; try { fromProjectJSON = new JSONObject(fromProjectFileContent); } catch (JSONException e1) { e1.printStackTrace(); } JSONArray nodeIdsArray = null; try { //get all the file names in an array nodeIdsArray = new JSONArray(nodeIds); //loop through all the file names for (int x = 0; x < nodeIdsArray.length(); x++) { //get node id String nodeId = nodeIdsArray.getString(x); //get the node object in the "from" project JSONObject fromNode = getNodeById(fromProjectJSON, nodeId); if (fromNode != null) { //get the attributes of the node in the "from" project String type = fromNode.optString("type"); String id = nodeId; String title = fromNode.optString("title"); String nodeClass = fromNode.optString("class"); String fileName = fromNode.optString("ref"); //make sure the file exists in the fromProjectFolder File fileToImport = new File(fromProjectFolder, fileName); if (fileToImport.exists()) { //get the content from the step String fileContent = FileUtils.readFileToString(fileToImport); //the part of the file name before the . e.g. "node_5" String fileNamePrefix = getUniqueFileNamePrefix(toProjectFolder); //the part of the file name after the . (including the .) e.g. ".or" String fileNameExtension = fileName.substring(fileName.indexOf(".")); //make the name of the new file we want to make String newFileName = fileNamePrefix + fileNameExtension; /* * check if we are importing a .ht or .wa file since we also need to * import the associated .html file */ if (fileNameExtension.equals(".ht")) { //get the html file name String htmlFileName = fileName + "ml"; //get a handle on the html file in the "from" project File htmlFileToImport = new File(fromProjectFolder, htmlFileName); if (htmlFileToImport.exists()) { //get the contents of the html file String htmlString = FileUtils.readFileToString(htmlFileToImport); //import assets that are referenced in the html content htmlString = importAssetsInContent(htmlString, fromProjectAssetsFolder, toProjectAssetsFolder); //make the html file name for our "to" project String newHtmlFileName = newFileName + "ml"; //make the new html file in our "to" project File newHtmlFile = new File(toProjectFolder, newHtmlFileName); //write the content to the file in the "to" project FileUtils.writeStringToFile(newHtmlFile, htmlString); /* * replace all references to the .html file in the new .ht file * e.g. * * before * { * "src": "node_0.html", * "type": "Html" * } * * after * { * "src": "node_141.html", * "type": "Html" * } */ fileContent = fileContent.replaceAll(htmlFileName, newHtmlFileName); } } else if (fileNameExtension.equals(".wa")) { //we are importing a webapp step so we need to also import the associated .html file if (fileContent != null) { try { //get the step content JSONObject fileContentJSON = new JSONObject(fileContent); if (fileContentJSON != null) { //get the file name of the associated .html file String fromAssetFileName = fileContentJSON.getString("url"); //get a handle on the .html file File fromAssetFile = new File(fromProjectAssetsFolder, fromAssetFileName); //get the string content of the .html file String fromAssetFileContent = FileUtils.readFileToString(fromAssetFile); //import any assets that are referenced in the .html file String toAssetFileContent = importReferencedFilesInContent( fromAssetFileContent, fromProjectAssetsFolder, toProjectAssetsFolder); //import the .html file to the to project asset folder String toAssetFileName = importAssetInContent(fromAssetFileName, toAssetFileContent, fromProjectAssetsFolder, toProjectAssetsFolder); //replace references to the file name in the content if we changed the file name if (fromAssetFileName != null && toAssetFileName != null && !fromAssetFileName.equals(toAssetFileName)) { fileContent = fileContent.replaceAll(fromAssetFileName, toAssetFileName); } } } catch (JSONException e) { } } } //import assets that are referenced in the step content fileContent = importAssetsInContent(fileContent, fromProjectAssetsFolder, toProjectAssetsFolder); //create a new file in our project File newFile = new File(toProjectFolder, newFileName); //write the contents to the new file FileUtils.writeStringToFile(newFile, fileContent); //create the node object that we will put in our "to" project JSONObject newNode = Template.getProjectNodeTemplate(type, newFileName, title, nodeClass); //add the node to our "to" project addNodeToProject(new File(fullToProjectFileUrl), newNode); } } } } catch (JSONException e) { e.printStackTrace(); } return result; } /** * Search for any references to assets in the step content and copy the assets to our assets folder * @param content the step content * @param fromProjectAssetsFolder the asset folder in the project we are copying the asset from * @param toProjectAssetsFolder the asset folder in the project we are copying the asset to * @return the updated content string */ public static String importAssetsInContent(String content, File fromProjectAssetsFolder, File toProjectAssetsFolder) { /* * create a pattern that will match any of these below * "assets/myPicture.jpg" * 'assets/myPicture.jpg" * "./assets/myPicture.jpg" * './assets/myPicture.jpg' * \"assets/myPicture.jpg\" * \'assets/myPicture.jpg\' * * if the pattern matcher is run on the last example './assets/myPicture.jpg' * this is what the groups will look like * group(0)='./assets/myPicture.jpg' * group(1)=./ * group(2)=myPicture.jpg */ Pattern p = Pattern.compile("\\\\?[\\\"'](\\./)?assets/([^\\\"'\\\\]+)\\\\?[\\\"']"); //run the matcher Matcher m = p.matcher(content); //loop through all the matches while (m.find()) { if (m.groupCount() == 2) { //the file name e.g. myPicture.jpg String fromAssetFileName = m.group(2); if (fromAssetFileName != null && !fromAssetFileName.isEmpty()) { if (fromAssetFileName.contains("?")) { /* * the file name contains GET params e.g. sunlight.jpg?w=12&h=12 * so we will remove everything after the ? */ fromAssetFileName = fromAssetFileName.substring(0, fromAssetFileName.indexOf("?")); } //import the asset file into the project asset folder String toAssetFileName = importAssetInContent(fromAssetFileName, null, fromProjectAssetsFolder, toProjectAssetsFolder); //replace references to the file name in the content if we changed the file name if (fromAssetFileName != null && toAssetFileName != null && !fromAssetFileName.equals(toAssetFileName)) { content = content.replaceAll(fromAssetFileName, toAssetFileName); } } } } return content; } /** * Search for any references to a file in the content and copy the file to our assets folder * @param content the content * @param fromProjectAssetsFolder the asset folder in the project we are copying the asset from * @param toProjectAssetsFolder the asset folder in the project we are copying the asset to * @return the updated content string */ public static String importReferencedFilesInContent(String content, File fromProjectAssetsFolder, File toProjectAssetsFolder) { /* * create a pattern that will search for file references that use the src attribute * * the pattern will match any of these below and extract just the file name * src="Heat.png" * src='Heat.png' * src="Heat.png?w=15&h=18" * src='Heat.png?w=15&h=18' * * the pattern will not match any of these below * src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js" * src='https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js' * * if the pattern matcher is run on src="Heat.png?w=15&h=18" * this is what the groups will look like * group(0)=src="Heat.png?w=15&h=18" * group(1)=src * group(2)=Heat.png */ Pattern p = Pattern.compile("(src|href)=[\"'](?!http)([^\"'\\?]*)[^\"']*[\"']"); //run the matcher Matcher m = p.matcher(content); //loop through all the matches while (m.find()) { if (m.groupCount() == 2) { //get the captured group 2 String fromAssetFileName = m.group(2); if (fromAssetFileName != null && !fromAssetFileName.isEmpty()) { //import the asset file into the project asset folder String toAssetFileName = importAssetInContent(fromAssetFileName, null, fromProjectAssetsFolder, toProjectAssetsFolder); //replace references to the file name in the content if we changed the file name if (fromAssetFileName != null && toAssetFileName != null && !fromAssetFileName.equals(toAssetFileName)) { content = content.replaceAll(fromAssetFileName, toAssetFileName); } } } } return content; } /** * Import the asset from one project asset folder to another project asset folder * @param fromAssetFileName the name of the file in the asset folder * @param fromAssetFileContent (optional) the content that we want to save to the to asset. * if this is not provided we will obtain the content from the fromAssetFileName handle. * this parameter is used when the content in the fromAssetFileName needs to be modified * such as when file name references in the content need to be changed due to file name * conflicts. * @param fromProjectAssetsFolder the asset folder in the from project * @param toProjectAssetsFolder the asset folder in the to project * @return the name of the asset file that was created in the to project asset folder * or null if we were unable to create the asset in the to project asset folder */ public static String importAssetInContent(String fromAssetFileName, String fromAssetFileContent, File fromProjectAssetsFolder, File toProjectAssetsFolder) { String toAssetFileName = null; String toAssetFileContent = null; //create the file handle for the "from" file File fromAsset = new File(fromProjectAssetsFolder, fromAssetFileName); //make sure the file exists in the "from" project if (fromAsset.exists()) { toAssetFileName = fromAssetFileName; //create the file handle for the "to" file File toAsset = new File(toProjectAssetsFolder, toAssetFileName); boolean assetCompleted = false; int counter = 1; /* * this while loop will check if the file already exists. * * if the file already exists, we will check if the content in the "from" file is the same as in the "to" file. * if the content is the same, we do not need to do anything. * if the content is different, we will look for another file name to use. * if the file does not exist, we will make it. */ while (!assetCompleted) { if (toAsset.exists()) { //file already exists try { //get the to asset file content toAssetFileContent = FileUtils.readFileToString(toAsset); } catch (IOException e1) { e1.printStackTrace(); } try { boolean contentMatches = false; if (fromAssetFileContent != null) { /* * the from asset file content was passed in so we will compare it with * the to asset file content */ if (fromAssetFileContent.equals(toAssetFileContent)) { //the file content matches contentMatches = true; } } else if (FileUtils.contentEquals(fromAsset, toAsset)) { /* * the from asset file content was not passed in so we will compare * the contents from their file handles */ //the file content matches contentMatches = true; } if (contentMatches) { //files are the same so we do not need to do anything assetCompleted = true; } else { //files are not the same so we need to try a different file name //get new file name e.g. myPicture-1.jpg toAssetFileName = createNewFileName(fromAssetFileName, counter); //create the handle for the next file we will try to use toAsset = new File(toProjectAssetsFolder, toAssetFileName); counter++; } } catch (IOException e) { e.printStackTrace(); break; } } else { //file does not exist so we will copy the file to the "to" assets folder try { if (fromAssetFileContent != null) { //the content was passed in so we will use it FileUtils.write(toAsset, fromAssetFileContent); } else { /* * the content was not passed in so we will use the content * obtained from the file handle */ //copy the file into our new asset file FileUtils.copyFile(fromAsset, toAsset); } assetCompleted = true; } catch (IOException e) { e.printStackTrace(); break; } } } } return toAssetFileName; } /** * Create a new file name by adding '-' and a number to the end of * the file name. * * before * myPicture.jpg * after * myPicture-1.jpg * * @param fileName the current file name * @param counter the number to add to the file name * @return a new file name with '-' and a number added to the end */ public static String createNewFileName(String fileName, int counter) { String newFileName = ""; int lastDot = fileName.lastIndexOf("."); //get the beginning of the file e.g. myPicture String fileNameBeginning = fileName.substring(0, lastDot); //get the end of the file e.g. .jpg String fileNameEnding = fileName.substring(lastDot); //create the new file name e.g. myPicture-1.jpg newFileName = fileNameBeginning + "-" + counter + fileNameEnding; return newFileName; } /** * Get the node JSONObject from the project JSON * @param projectJSON the project JSON object * @param nodeId the node id * @return the JSONObject for the node in the project */ public static JSONObject getNodeById(JSONObject projectJSON, String nodeId) { JSONObject node = null; if (nodeId != null && !nodeId.equals("")) { try { //get the array of nodes in the project JSONArray fromProjectNodes = projectJSON.getJSONArray("nodes"); //loop through all the nodes in the project for (int x = 0; x < fromProjectNodes.length(); x++) { //get a node JSONObject tempNode = fromProjectNodes.getJSONObject(x); if (tempNode != null) { //get the node id String id = tempNode.getString("identifier"); if (nodeId.equals(id)) { //the node id matches the one we want so we are done searching node = tempNode; break; } } } } catch (JSONException e) { e.printStackTrace(); } } return node; } /** * Renames the folder by changing the folder name to * <original folder name>-<time in milliseconds> * e.g. * 236 * to * 236-1290473717307 * @param projectUrl */ public static void renameFolder(String projectUrl) { //get a handle on the folder File originalFolder = new File(projectUrl); //get the current time Date date = new Date(); //create a handle to the new folder name File backupFolder = new File(projectUrl + "-" + date.getTime()); //rename the original folder to the new folder name originalFolder.renameTo(backupFolder); } /** * Copy the contents of a folder to another folder. This is a recursive function * that deep copies folders. * @param sourceLocation the folder that contains the files we want to copy * @param targetLocation the folder we want to copy the files to * @throws IOException */ public static void copyFile(File sourceLocation, File targetLocation) throws IOException { if (sourceLocation.isDirectory()) { //current file is a folder if (!targetLocation.exists()) { //make the folder in the target folder if it does not already exist targetLocation.mkdir(); } //get the files in the folder String[] children = sourceLocation.list(); //loop through all the files for (int i = 0; i < children.length; i++) { //copy the file copyFile(new File(sourceLocation, children[i]), new File(targetLocation, children[i])); } } else { //current file is a file //create stream to read from the source and write to the target InputStream in = new FileInputStream(sourceLocation); OutputStream out = new FileOutputStream(targetLocation); //buffer to hold the bytes to copy byte[] buf = new byte[1024]; int len; //loop through all the bytes in the source file while ((len = in.read(buf)) > 0) { //write the byte to the target file out.write(buf, 0, len); } //close the streams in.close(); out.close(); } } /** * An object to hold information for a node. A node can be a sequence * or a node. */ public static class NodeInfo { //the step number as seen in the vle private String stepNumber; //the id of the node private String nodeId; //the title of the node private String title; //the type of the node e.g. 'HtmlNode', 'OpenResponseNode', 'sequence', etc. private String nodeType; /* * whether this node is from the parent project or the child project. this * field will be set to "parent" or "child" */ private String parentOrChild; public NodeInfo(String stepNumber, String nodeId, String title, String nodeType, String parentOrChild) { this.stepNumber = stepNumber; this.nodeId = nodeId; this.title = title; this.setNodeType(nodeType); this.setParentOrChild(parentOrChild); } public void setStepNumber(String stepNumber) { this.stepNumber = stepNumber; } public String getStepNumber() { return stepNumber; } public void setNodeId(String nodeId) { this.nodeId = nodeId; } public String getNodeId() { return nodeId; } public void setTitle(String title) { this.title = title; } public String getTitle() { return title; } public void setNodeType(String nodeType) { this.nodeType = nodeType; } public String getNodeType() { return nodeType; } public void setParentOrChild(String parentOrChild) { this.parentOrChild = parentOrChild; } public String getParentOrChild() { return parentOrChild; } } /** * Comparator that compares NodeInfo objects. It orders the NodeInfo objects * by their step numbers from smaller to bigger. * e.g. * 1.1, 1.2, 1.3, 2.1, 3.1, 3.2, etc. */ public static class NodeInfoComparator implements Comparator<NodeInfo> { /** * Compares the NodeInfo objects such that they will be * ordered by step number from smallest to biggest. We only want * one of each node id in our TreeSet so if the node ids are the * same for node1 and node2 we will say the NodeInfo objects are * the same. Otherwise we will compare their step numbers. * @param node1 a NodeInfo object * @param node2 a NodeInfo object * @return * -1 if node1 step number is smaller than node2 step number * 0 if the node ids are the same * 1 if the node1 step number is bigger than the node2 step number */ public int compare(NodeInfo node1, NodeInfo node2) { //the default return value int result = 0; //get the step numbers String node1StepNumber = node1.getStepNumber(); String node2StepNumber = node2.getStepNumber(); //get the node ids String nodeId1 = node1.getNodeId(); String nodeId2 = node2.getNodeId(); if (nodeId1 != null && nodeId2 != null) { //compare the node ids if (nodeId1.equals(nodeId2)) { /* * the node ids are the same so we will return 0 to * specify that these two NodeInfo objects are the same */ return 0; } } if (node1StepNumber != null && node2StepNumber != null) { //compare the step numbers /* * split the step numbers by '.' * step numbers will look like 1.1, 2.3, 3.1, etc. * so we will obtain an array with the parts separated * 1.1 will become [1, 1] * 2.3 will become [2, 3] * 3.1 will become [3, 1] * 4.1.2 will become [4, 1, 2] * etc. */ String[] node1Split = node1StepNumber.split("\\."); String[] node2Split = node2StepNumber.split("\\."); /* * get the longer of the lengths between the two arrays. * this is in case one of the arrays is longer than the * other. */ int maxLength = Math.max(node1Split.length, node2Split.length); //loop through all the parts in the arrays for (int x = 0; x < maxLength; x++) { if (node1Split.length - 1 < x) { /* * node1 has run out of parts while node 2 still has parts. * this will only happen if the node1 array is shorter than * the node2 array. */ result = -1; break; } else if (node2Split.length - 1 < x) { /* * node2 has run out of parts while node 1 still has parts. * this will only happen if the node2 array is shorter than * the node1 array. */ result = 1; break; } else { //both nodes still have parts //get the xth element in each array String node1Part = node1Split[x]; String node2Part = node2Split[x]; //get the int value int node1PartNum = Integer.parseInt(node1Part); int node2PartNum = Integer.parseInt(node2Part); if (node1PartNum > node2PartNum) { //node1 part is larger than node2 part result = 1; break; } else if (node1PartNum < node2PartNum) { //node2 part is larger than node1 part result = -1; break; } else { /* * the parts are the same value so we will continue * on with the for loop to look at the next xth * element */ } } } } if (result == 0) { //step numbers are the same so we will now compare the node ids if (nodeId1 != null && nodeId2 != null) { if (!nodeId1.equals(nodeId2)) { //node ids are not the same which means these are different nodes. if (node1.getParentOrChild() == null) { //just return a non 0 value to specify that the nodes are different result = 1; } else if (node1.getParentOrChild().equals("parent")) { /* * the nodes are different so we will return a non 0 value. * in this case we will try to be consistent by putting parent * nodes after child nodes */ result = 1; } else if (node1.getParentOrChild().equals("child")) { /* * the nodes are different so we will return a non 0 value. * in this case we will try to be consistent by putting child * nodes before parent nodes */ result = -1; } } } } return result; } } /** * Get the project JSONObject from the project url * @param projectUrl the url to the project * e.g. * /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/236/wise4.project.json * @return the JSONObject for the project */ public static JSONObject getProjectJSONObject(String projectUrl) { JSONObject projectJSONObject = null; //get the project file File projectFile = new File(projectUrl); try { //get the contents of the file as a string String projectJSONString = FileUtils.readFileToString(projectFile); //create a JSONObject from the string projectJSONObject = new JSONObject(projectJSONString); } catch (IOException e) { e.printStackTrace(); } catch (JSONException e) { e.printStackTrace(); } return projectJSONObject; } /** * Parse the project JSONObject to get all the nodes and sequences and * put them into HashMaps so that we can quickly reference them by id * later. * @param projectJSON the project JSONObject * @param nodeIdToNodeOrSequence a HashMap that stores node id to * node or sequence JSONObject * @param fileNameToNodeId a HashMap that stores filename to node id * @param nodeIdToStepNumber a HashMap that stores node id to step number */ public static void parseProjectJSONObject(JSONObject projectJSON, HashMap<String, JSONObject> nodeIdToNodeOrSequence, HashMap<String, String> fileNameToNodeId, HashMap<String, String> nodeIdToStepNumber) { try { //get the nodes in the project JSONArray projectNodes = projectJSON.getJSONArray("nodes"); //loop through all the nodes for (int x = 0; x < projectNodes.length(); x++) { //get a node JSONObject node = projectNodes.getJSONObject(x); //get the node id String identifier = node.getString("identifier"); //get the filename String ref = node.getString("ref"); //add the entries into the HashMaps fileNameToNodeId.put(ref, identifier); nodeIdToNodeOrSequence.put(identifier, node); } //get the sequences in the project JSONArray projectSequences = projectJSON.getJSONArray("sequences"); //loop through all the sequences for (int y = 0; y < projectSequences.length(); y++) { //get a sequence JSONObject sequence = projectSequences.getJSONObject(y); //get the node id String identifier = sequence.getString("identifier"); //add an entry into the HashMap nodeIdToNodeOrSequence.put(identifier, sequence); } //get the start point for the project String startPoint = projectJSON.getString("startPoint"); JSONObject startPointSequence = nodeIdToNodeOrSequence.get(startPoint); //parse the project by traversing through the project from start to finish parseNodeStepNumbers("", startPointSequence, nodeIdToNodeOrSequence, nodeIdToStepNumber); } catch (JSONException e) { e.printStackTrace(); } } /** * Parse the project to calculate step numbers by traversing the project from start * to finish. * @param stepNumber the current step number, this will hold the activity numbers * so that when we get to a step we can put the activity number together with the * step number such as 1.1 * @param node the current node * @param nodeIdToNodeOrSequence the HashMap that store node id to node JSONObject * @param nodeIdToStepNumber the HashMap that we will fill with node id to * step number */ public static void parseNodeStepNumbers(String stepNumber, JSONObject node, HashMap<String, JSONObject> nodeIdToNodeOrSequence, HashMap<String, String> nodeIdToStepNumber) { try { //get the node type String nodeType = node.getString("type"); if (node.getString("type") != null && nodeType.equals("sequence")) { //node is a sequence //get the nodes in the sequence JSONArray refs = node.getJSONArray("refs"); /* * check if stepNumber is "", if it is "" it means we are on the * start sequence and we do not need to add an entry for that * but we need to add an entry for all activities and steps */ if (!stepNumber.equals("")) { //this is an activity or step //get the node id String identifier = node.getString("identifier"); //add an entry into the HashMap nodeIdToStepNumber.put(identifier, stepNumber); } if (refs != null) { //loop through all the nodes in the sequence for (int x = 0; x < refs.length(); x++) { //get a child id String childRef = refs.getString(x); //get the JSONObject for the child JSONObject childNode = nodeIdToNodeOrSequence.get(childRef); /* * make the step number, if we are on activity 1, * stepNumber would be 1 and childStepNumber would * be set to 1 at the moment */ String childStepNumber = stepNumber; if (!childStepNumber.equals("")) { //add a "." between each level childStepNumber += "."; } /* * add the step number, if we are on activity 1, * step 2, childStepNumber would be set to * 1.2 */ childStepNumber += (x + 1); //recursively parse the children's children parseNodeStepNumbers(childStepNumber, childNode, nodeIdToNodeOrSequence, nodeIdToStepNumber); } } } else { //node is a leaf node //get the node id String identifier = node.getString("identifier"); //add an entry into the HashMap nodeIdToStepNumber.put(identifier, stepNumber); } } catch (JSONException e) { e.printStackTrace(); } } /** * Compare all the sequences in the parent project and the child project. * We will determine whether an activity or a step was added, deleted, * or moved. * @param parentProjectNode the JSONObject for the parent project * @param childProjectNode the JSONObject for the child project */ public static void compareSequences(JSONObject parentProjectNode, JSONObject childProjectNode, HashMap<String, JSONObject> parentNodeIdToNodeOrSequence, HashMap<String, JSONObject> childNodeIdToNodeOrSequence, HashMap<String, String> parentNodeIdToStepNumber, HashMap<String, String> childNodeIdToStepNumber, HashMap<String, String> nodeIdToStatus, HashMap<String, String> nodeIdToModified) { //a TreeSet to gather all the unique sequence ids from the parent and child projects TreeSet<String> sequenceIds = new TreeSet<String>(); try { //get the sequences in the parent project JSONArray parentProjectSequences = parentProjectNode.getJSONArray("sequences"); //get the sequences in the child project JSONArray childProjectSequences = childProjectNode.getJSONArray("sequences"); //retrieve all the sequence ids from the parent and child projects extractNodeIdsFromSequenceNodeJSONArray(sequenceIds, parentProjectSequences); extractNodeIdsFromSequenceNodeJSONArray(sequenceIds, childProjectSequences); //loop through all the sequence ids we have just collected Iterator<String> sequenceIdsIterator = sequenceIds.iterator(); while (sequenceIdsIterator.hasNext()) { //get a sequence id String sequenceId = sequenceIdsIterator.next(); /* * try to retrieve the sequence JSONObject from the child and parent project. * the sequence id may be in only one of the projects if one of the projects * has been changed. */ JSONObject childSequence = childNodeIdToNodeOrSequence.get(sequenceId); JSONObject parentSequence = parentNodeIdToNodeOrSequence.get(sequenceId); if (childSequence != null && parentSequence != null) { /* * both parent and child projects have this sequence so we will compare * the nodes within them */ //get the array of child ids from both sequences JSONArray parentRefs = parentSequence.getJSONArray("refs"); JSONArray childRefs = childSequence.getJSONArray("refs"); //a TreeSet to collect all the node ids for the current sequence TreeSet<String> nodeIds = new TreeSet<String>(); //retrieve all the sequence ids from the current parent and child sequence extractNodeIdsFromJSONArray(nodeIds, parentRefs); extractNodeIdsFromJSONArray(nodeIds, childRefs); /* * flag to be set if there is a difference between the parent and * child sequence in terms of node existence and node comparison. * this will not take into consideration the modification of a * node's content. node modification is handled somewhere else. */ boolean sequenceModified = false; //loop through all the node ids we found Iterator<String> nodeIdsIterator = nodeIds.iterator(); while (nodeIdsIterator.hasNext()) { //get a node id String nodeId = nodeIdsIterator.next(); //get the node from the parent and child project JSONObject parentNode = parentNodeIdToNodeOrSequence.get(nodeId); JSONObject childNode = childNodeIdToNodeOrSequence.get(nodeId); if (childNode != null && parentNode != null) { /* * node exists in both parent and child project so we will * check if the node is in the same position or if it was moved */ //get the step number for the node in the parent and child project String parentStepNumber = parentNodeIdToStepNumber.get(nodeId); String childStepNumber = childNodeIdToStepNumber.get(nodeId); if (childStepNumber != null && parentStepNumber != null) { if (!childStepNumber.equals(parentStepNumber)) { //step numbers are different so the step was moved nodeIdToStatus.put(nodeId, "moved"); //the sequence is different between parent and child project sequenceModified = true; } } } else if (childNode != null && parentNode == null) { /* * node was only found in the child project which means * the node will be deleted from child project */ nodeIdToStatus.put(nodeId, "deleted"); //the sequence is different between parent and child project sequenceModified = true; } else if (childNode == null && parentNode != null) { /* * node was only found in the parent project which means * the node will be added to child project */ nodeIdToStatus.put(nodeId, "added"); //the sequence is different between parent and child project sequenceModified = true; } } if (sequenceModified) { //sequence was modified nodeIdToModified.put(sequenceId, "true"); } else { //sequence was not modified nodeIdToModified.put(sequenceId, "false"); } } else if (childSequence != null && parentSequence == null) { /* * child project has this sequence but parent project does not so * we will check if the nodes in the child project sequence are * new to the parent project because it is possible the child * project had the nodes moved to a new sequence. */ /* * set the status of this sequence to be deleted since the parent * project does not have this sequence */ nodeIdToStatus.put(sequenceId, "deleted"); //get the array of node ids in the sequence from the child project JSONArray childRefs = childSequence.getJSONArray("refs"); //loop through all the node ids for (int x = 0; x < childRefs.length(); x++) { //get a node id String nodeId = childRefs.getString(x); //try to retrieve the node with the given node id from the parent project JSONObject parentNode = parentNodeIdToNodeOrSequence.get(nodeId); if (parentNode == null) { //parent does not have this node so it will be deleted nodeIdToStatus.put(nodeId, "deleted"); } else { //parent does have this node so it was just moved to another sequence nodeIdToStatus.put(nodeId, "moved"); } } } else if (childSequence == null && parentSequence != null) { /* * parent project has this sequence but child project does not so * we will check if the nodes in the parent project sequence are * new to the child project */ /* * set the status of this sequence to be added since the child * project does not have this sequence */ nodeIdToStatus.put(sequenceId, "added"); //get the array of node ids in the sequence from the child project JSONArray parentRefs = parentSequence.getJSONArray("refs"); //loop through all the node is for (int x = 0; x < parentRefs.length(); x++) { //get a node id String nodeId = parentRefs.getString(x); //try to retrieve the node with the given node id from the child project JSONObject childNode = childNodeIdToNodeOrSequence.get(nodeId); if (childNode == null) { //child does not have this node so it will be added nodeIdToStatus.put(nodeId, "added"); } else { //child does have this node so it was just moved to another sequence nodeIdToStatus.put(nodeId, "moved"); } } } } } catch (JSONException e) { e.printStackTrace(); } } /** * Extract the node ids from the JSONArray of node ids and add them to * the given TreeSet. The TreeSet only contains unique values so we * will not have any duplicates. * @param nodeIds a TreeSet to store the node ids in * @param nodeIdsArray a JSONArray of node id strings */ public static void extractNodeIdsFromJSONArray(TreeSet<String> nodeIds, JSONArray nodeIdsArray) { //loop through all the node ids in the array for (int x = 0; x < nodeIdsArray.length(); x++) { try { //get a node id String ref = nodeIdsArray.getString(x); //add the node id to the TreeSet nodeIds.add(ref); } catch (JSONException e) { e.printStackTrace(); } } } /** * Extract sequence ids from an array of sequences and add them to * the given TreeSet. The TreeSet only contains unique values so we * will not have any duplicates. * @param nodeIds a TreeSet to store the node ids in * @param sequenceNodes a JSONArray of sequence JSONObjects */ public static void extractNodeIdsFromSequenceNodeJSONArray(TreeSet<String> nodeIds, JSONArray sequenceNodes) { //loop through all the sequences for (int x = 0; x < sequenceNodes.length(); x++) { try { //get a sequence JSONObject sequence = sequenceNodes.getJSONObject(x); //get the sequence id String identifier = sequence.getString("identifier"); //add the sequence id to the TreeSet nodeIds.add(identifier); } catch (JSONException e) { e.printStackTrace(); } } } /** * Compares the files in the folders and checks whether they have been modified * or not. This is a recursive function that implements deep traversal so that * contents in child folders are also compared. This comparison is determining * what changes, if any, have been made to the files in the child project. * @param sourceLocation the folder of the parent project * @param targetLocation the folder of the child project * @throws IOException */ public static void compareFolder(File sourceLocation, File targetLocation, HashMap<String, String> parentFileNameToNodeId, HashMap<String, String> htmlToHt, HashMap<String, String> nodeIdToModified) throws IOException { if (sourceLocation.exists() && targetLocation.exists()) { //file or folder exists in parent and child project if (sourceLocation.isDirectory() && targetLocation.isDirectory()) { //compare the contents of the folders compareFolderHelper(sourceLocation, targetLocation, parentFileNameToNodeId, htmlToHt, nodeIdToModified); } else if (sourceLocation.isFile() && targetLocation.isFile()) { /* * file exists in parent and child project so we will now compare the * file from the parent project and the child project */ //get the file name String fileName = sourceLocation.getName(); /* * get the node id of the node that this file is for. if the * filename is a .html file, it will not have an associated * node id but we will handle that below */ String nodeId = parentFileNameToNodeId.get(fileName); //get the contents from both files String sourceFile = FileUtils.readFileToString(sourceLocation); String targetFile = FileUtils.readFileToString(targetLocation); if (sourceFile != null && targetFile != null) { if (fileName.toLowerCase().endsWith(".ht")) { //this is a .ht file try { //retrieve the content of the .ht file JSONObject sourceFileContent = new JSONObject(sourceFile); //retrieve the .html file name associated with this .ht file String referencedHtmlFileName = sourceFileContent.getString("src"); //add an entry into the HashMap that stores .html file name to node id htmlToHt.put(referencedHtmlFileName, nodeId); } catch (JSONException e) { e.printStackTrace(); } } else if (fileName.toLowerCase().endsWith(".html")) { //this is a .html file /* * obtain the node id associated with this .html file. we are * assuming the .ht file is analyzed before the .html file is. * this will be true as long as the traversal of the files in * the folder are alphabetical such that .ht always comes before * .html. * * note: this will not be true if the file names to not * share the same prefix but in our case they always do. */ String htNodeId = htmlToHt.get(fileName); /* * we will use the node id associated with the associated .ht file * for this .html file. this will cause the entry in nodeIdToModified * to be overridden with the modified value for the .html file * because we care about whether the .html file was modified and * not the .ht because the .ht never really changes. */ nodeId = htNodeId; } //check if there is any difference between the the content of the files if (!sourceFile.equals(targetFile)) { //content in the files are not the same so the file was modified nodeIdToModified.put(nodeId, "true"); } else { //content in the files are the same so the file was not modified nodeIdToModified.put(nodeId, "false"); } } } else if (sourceLocation.isDirectory() && targetLocation.isFile()) { } else if (sourceLocation.isFile() && targetLocation.isDirectory()) { } } else if (sourceLocation.exists() && !targetLocation.exists()) { if (sourceLocation.isDirectory()) { //compare the contents of the folders compareFolderHelper(sourceLocation, targetLocation, parentFileNameToNodeId, htmlToHt, nodeIdToModified); } else if (sourceLocation.isFile()) { /* * file does not exist in the child project so it is new in the * parent project or was deleted in the child project. from the * author's point of view, the file will be added to the child * project. this will be handled in compareSequences(). */ } } else if (!sourceLocation.exists() && targetLocation.exists()) { if (targetLocation.isDirectory()) { //compare the contents of the folders compareFolderHelper(sourceLocation, targetLocation, parentFileNameToNodeId, htmlToHt, nodeIdToModified); } else if (targetLocation.isFile()) { /* * file does not exist in the parent project so it was either * deleted in the parent project or is new in the child project. * from the author's point of view, the file will be deleted * from the child project. this will be handled in compareSequences(). */ } } } /** * Retrieves the files in the folders and calls compareFolder on all * of those files. * @param sourceLocation the file or folder from the parent project * @param targetLocation the file or folder from the child project * @throws IOException */ public static void compareFolderHelper(File sourceLocation, File targetLocation, HashMap<String, String> parentFileNameToNodeId, HashMap<String, String> htmlToHt, HashMap<String, String> nodeIdToModified) throws IOException { //used to retrieve all the file names TreeSet<String> files = new TreeSet<String>(); if (sourceLocation.isDirectory()) { //get all the files in the child project folder String[] sourceChildren = sourceLocation.list(); //add the file names to the files collection addFileNamesToCollection(files, sourceChildren); } if (targetLocation.isDirectory()) { //get all the files in the parent project folder String[] targetChildren = targetLocation.list(); //add the file names to the files collection addFileNamesToCollection(files, targetChildren); } //loop through all the file names Iterator<String> iterator = files.iterator(); while (iterator.hasNext()) { //get a file name String file = iterator.next(); /* * call compareFolder on the file which compares the file in the * parent and child project folders even if the file exists or not */ compareFolder(new File(sourceLocation, file), new File(targetLocation, file), parentFileNameToNodeId, htmlToHt, nodeIdToModified); } } /** * Adds the file names from the array to the collection * @param fileNameCollection collection that holds all the file names * @param fileNames an array of file names */ public static void addFileNamesToCollection(TreeSet<String> fileNameCollection, String[] fileNames) { //loop through all the file names for (int i = 0; i < fileNames.length; i++) { //add the file name to the collection fileNameCollection.add(fileNames[i]); } } /** * Get the amount of disk space this project uses and the max project size * @param path the path to the project * @param projectMaxTotalAssetsSizeLong the max allowable project folder size * @return a string specifying how much space the project is using and what * the max allowable project folder size is as a fraction e.g. * 100kb/10mb */ public static String getProjectUsageAndMax(String path, Long projectMaxTotalAssetsSizeLong) { String result = ""; //get the amount of disk space the project folder uses String sizeUsed = getProjectSize(path); String projectMaxTotalAssetsSizeString = null; if (projectMaxTotalAssetsSizeLong != null) { //get the max project size as a string projectMaxTotalAssetsSizeString = projectMaxTotalAssetsSizeLong.toString(); } else { //get the global max project size value, we will default to 15MB if none is provided in the wise.properties file projectMaxTotalAssetsSizeString = wiseProperties.getProperty("project_max_total_assets_size", "15728640"); } //get the project folder size usage as a fraction String usageString = sizeUsed + "/" + projectMaxTotalAssetsSizeString; result = usageString; return result; } /** * Returns the size in bytes of all of the files in the specified path/dirname * * @param path the path to the project folder * @return the size of the folder in bytes as a string */ public static String getProjectSize(String path) { if (path == null) { return "No project path specified"; } else { File projectDir = new File(path); if (projectDir.exists()) { if (projectDir.isDirectory()) { long sizeOfDirectory = FileUtils.sizeOfDirectory(projectDir); return String.valueOf(sizeOfDirectory); } else { return "0"; } } else { return "Given project path does not exist."; } } } /** * Get the full project file path * @param project the project object * @return the full project file path * e.g. * /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/667/wise4.project.json */ public static String getProjectFilePath(Project project) { String projectFilePath = null; if (project != null) { String curriculumBaseDir = wiseProperties.getProperty("curriculum_base_dir"); String projectUrl = (String) project.getCurnit().accept(new CurnitGetCurnitUrlVisitor()); projectFilePath = curriculumBaseDir + projectUrl; } return projectFilePath; } /** * Get the full file path given the project object and a file name * @param project the project object * @param fileName the file name * @return the full file path * e.g. * /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/667/node_2.or */ public static String getFilePath(Project project, String fileName) { String filePath = null; if (project != null) { String projectFolderPath = getProjectFolderPath(project); filePath = projectFolderPath + fileName; } return filePath; } /** * Get the full project folder path given the project object * @param project the project object * @return the full project folder path * e.g. * /Users/geoffreykwan/dev/apache-tomcat-5.5.27/webapps/curriculum/667 */ public static String getProjectFolderPath(Project project) { String projectFolderPath = null; if (project != null) { String projectFilePath = getProjectFilePath(project); projectFolderPath = projectFilePath.substring(0, projectFilePath.lastIndexOf("/")); } return projectFolderPath; } }