/* * File : ProjectFileUtils.java * Created : 10-aug-2015 09:00 * By : fbusquets * * JClic - Authoring and playing system for educational activities * * Copyright (C) 2000 - 2005 Francesc Busquets & Departament * d'Educacio de la Generalitat de Catalunya * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details (see the LICENSE file). */ package edu.xtec.jclic.project; import edu.xtec.jclic.bags.ActivityBagElement; import edu.xtec.jclic.bags.ActivitySequenceElement; import edu.xtec.jclic.bags.JumpInfo; import edu.xtec.jclic.bags.MediaBagElement; import edu.xtec.jclic.fileSystem.FileSystem; import edu.xtec.jclic.fileSystem.FileZip; import edu.xtec.util.JDomUtility; import edu.xtec.util.ResourceBridge; import edu.xtec.util.StreamIO; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.io.PrintWriter; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import org.json.JSONObject; /** * Miscellaneous utilities to process ".jclic.zip" files, normalizing media file * names, avoiding links to other "zip" files and extracting contents to a given * folder. * * @author fbusquet */ public class ProjectFileUtils implements ResourceBridge { FileZip zipFS; String zipFilePath; String zipFileName; String projectName; String jclicFileName; String basePath; String relativePath; String[] entries; JClicProject project; // Interruption flag public static boolean interrupt = false; /** * Builds a ProjectFileUtils object, initializing a @link{JClicProject} * * @param fileName - Relative or absolute path to the ".jclic.zip" file to be * processed * @param basePath - Base path of this project. Relative paths are based on * this one. When null, the parent folder of fileName will be used. * @throws Exception */ public ProjectFileUtils(String fileName, String basePath) throws Exception { zipFilePath = new File(fileName).getCanonicalPath(); if (!zipFilePath.endsWith(".jclic.zip")) { throw new Exception("File " + fileName + " is not a jclic.zip file!"); } String zipBase = (new File(zipFilePath)).getParent(); this.basePath = (basePath == null ? zipBase : basePath); relativePath = this.basePath.equals(zipBase) ? "" : zipBase.substring(this.basePath.length()+1); zipFS = (FileZip) FileSystem.createFileSystem(zipFilePath, this); zipFileName = zipFS.getZipName(); jclicFileName = zipFileName.substring(0, zipFileName.lastIndexOf(".")); entries = zipFS.getEntries(null); String[] projects = zipFS.getEntries(".jclic"); if (projects == null) { throw new Exception("File " + zipFilePath + " does not contain any jclic project"); } projectName = projects[0]; org.jdom.Document doc = zipFS.getXMLDocument(projectName); project = JClicProject.getJClicProject(doc.getRootElement(), this, zipFS, zipFileName); } /** * Normalizes the file names of the media bag, restricting it to URL-safe * characters. * * @param ps - The @link{PrintStream} where progress messages will be * outputed. Can be null. * @throws java.lang.InterruptedException */ public void normalizeFileNames(PrintStream ps) throws InterruptedException { HashSet<String> currentNames = new HashSet<String>(); Iterator<MediaBagElement> it = project.mediaBag.getElements().iterator(); while (it.hasNext()) { if (interrupt) { interrupt = false; throw new InterruptedException(); } MediaBagElement mbe = it.next(); String fn = mbe.getFileName(); mbe.setMetaData(fn); String fnv = FileSystem.getValidFileName(fn); // Avoid filenames starting with a dot if (fnv.charAt(0) == '.') { fnv = "_" + fnv; } if (!fnv.equals(fn)) { String fn0 = fnv; int n = 0; while (currentNames.contains(fnv)) { fnv = Integer.toString(n++) + fn0; } if (ps != null) { ps.println("Renaming \"" + fn + "\" as \"" + fnv + "\""); } mbe.setFileName(fnv); } currentNames.add(fnv); } } /** * Searchs for links to ".jclic.zip" files in @link{ActiveBox} * and @link{JumpInfo} objects, and redirects it to ".jclic" files * * @param ps - The @link{PrintStream} where progress messages will be * outputed. Can be null. * @throws java.lang.InterruptedException */ public void avoidZipLinks(PrintStream ps) throws InterruptedException { // Scan Activity elements for (ActivityBagElement ab : project.activityBag.getElements()) { if (interrupt) { interrupt = false; throw new InterruptedException(); } avoidZipLinksInElement(ab.getData(), ps); } for (ActivitySequenceElement ase : project.activitySequence.getElements()) { if (interrupt) { interrupt = false; throw new InterruptedException(); } if (ase.fwdJump != null) { avoidZipLinksInJumpInfo(ase.fwdJump, ps); avoidZipLinksInJumpInfo(ase.fwdJump.upperJump, ps); avoidZipLinksInJumpInfo(ase.fwdJump.lowerJump, ps); } if (ase.backJump != null) { avoidZipLinksInJumpInfo(ase.backJump, ps); avoidZipLinksInJumpInfo(ase.backJump.upperJump, ps); avoidZipLinksInJumpInfo(ase.backJump.lowerJump, ps); } } } /** * Searchs for ".jclic.zip" links in JumpInfo elements, changing it to links * to plain ".jclic" files. * * @param ji - The JumpInfo to scan for links * @param ps - The @link{PrintStream} where progress messages will be * outputed. Can be null. * @throws java.lang.InterruptedException */ public void avoidZipLinksInJumpInfo(JumpInfo ji, PrintStream ps) throws InterruptedException { if (ji != null && ji.projectPath != null && ji.projectPath.endsWith(".jclic.zip")) { String p = ji.projectPath; String pv = p.substring(0, p.length() - 4); ji.projectPath = pv; if (ps != null) { ps.println("Changing sequence link from \"" + p + "\" to \"" + pv + "\""); } } } /** * * Searchs for links to ".jclic.zip" files in the given JDOM element. This * method makes recursive calls on all the child elements of the provided * starting point. * * @param el - The org.jdom.Element to scan for links * @param ps - The @link{PrintStream} where progress messages will be * outputed. Can be null. * @throws java.lang.InterruptedException */ public void avoidZipLinksInElement(org.jdom.Element el, PrintStream ps) throws InterruptedException { if (el.getAttribute("params") != null) { String p = el.getAttributeValue("params"); if (p != null && p.endsWith(".jclic.zip")) { String pv = p.substring(0, p.length() - 4); if (ps != null) { ps.println("Changing media link from \"" + p + "\" to \"" + pv + "\""); } el.setAttribute("params", pv); } } Iterator it = el.getChildren().iterator(); while (it.hasNext()) { if (interrupt) { interrupt = false; throw new InterruptedException(); } avoidZipLinksInElement((org.jdom.Element) it.next(), ps); } } public String getRelativeFn(String fName){ return relativePath.length()>0 ? relativePath + '/' + fName : fName; } /** * Saves the JClic project and all its contents in plain format (not zipped) * into the specified path * * @param path - The path where the project will be saved * @param ps - The @link{PrintStream} where progress messages will be * outputed. Can be null. * @throws Exception * @throws java.lang.InterruptedException */ public void saveTo(String path, Collection<String> fileList, PrintStream ps) throws Exception, InterruptedException { File outPath = new File(path); path = outPath.getCanonicalPath(); // Check outPath exists and is writtable if (!outPath.exists()) { outPath.mkdirs(); } if (!outPath.isDirectory() || !outPath.canWrite()) { throw new Exception("Unable to write to: \"" + path + "\""); } // Export media fileList Iterator<MediaBagElement> it = project.mediaBag.getElements().iterator(); while (it.hasNext()) { if (interrupt) { interrupt = false; throw new InterruptedException(); } MediaBagElement mbe = it.next(); String fn = mbe.getMetaData(); if (fn == null) { fn = mbe.getFileName(); } InputStream is = zipFS.getInputStream(fn); File outFile = new File(outPath, mbe.getFileName()); FileOutputStream fos = new FileOutputStream(outFile); if (ps != null) { ps.println("Extracting " + fn + " to " + outFile.getCanonicalPath()); } StreamIO.writeStreamTo(is, fos); if(fileList!=null){ fileList.add(getRelativeFn(outFile.getName())); } } // Save ".jclic" file org.jdom.Document doc = project.getDocument(); File outFile = new File(outPath, jclicFileName); FileOutputStream fos = new FileOutputStream(outFile); if (ps != null) { ps.println("Saving project to: " + outFile.getCanonicalPath()); } JDomUtility.saveDocument(fos, doc); fos.close(); // Save ".jclic.js" file String jsFileName = jclicFileName + ".js"; outFile = new File(outPath, jsFileName); fos = new FileOutputStream(outFile); PrintWriter pw = new PrintWriter(new OutputStreamWriter(fos, "UTF-8")); if (ps != null) { ps.println("Saving project to: " + outFile.getCanonicalPath()); } org.jdom.output.XMLOutputter xmlOutputter = new org.jdom.output.XMLOutputter(); ByteArrayOutputStream bas = new ByteArrayOutputStream(); xmlOutputter.output(doc, bas); JSONObject json = new JSONObject(); json.put("xml", bas.toString("UTF-8")); String sequence = json.toString(); sequence = sequence.substring(8, sequence.length() - 2); pw.println("if(JClicObject){JClicObject.projectFiles[\"" + getRelativeFn(jclicFileName) + "\"]=\"" + sequence + "\";}"); pw.flush(); pw.close(); if(fileList!=null){ fileList.add(getRelativeFn(jclicFileName)); fileList.add(getRelativeFn(jsFileName)); } if (ps != null) { ps.println("Done processing: " + zipFilePath); } } public static void processSingleFile(String sourceFile, String destPath, Collection<String> fileList, PrintStream ps) throws Exception, InterruptedException { processSingleFile(sourceFile, destPath, null, fileList, ps); } public static void processSingleFile(String sourceFile, String destPath, String basePath, Collection<String> fileList, PrintStream ps) throws Exception, InterruptedException { ProjectFileUtils prjFU = new ProjectFileUtils(sourceFile, basePath); prjFU.normalizeFileNames(ps); prjFU.avoidZipLinks(ps); prjFU.saveTo(destPath, fileList, ps); } public static void processRootFolder(String sourcePath, String destPath, Collection<String> fileList, PrintStream ps) throws Exception, InterruptedException { String basePath = (new File(sourcePath)).getCanonicalPath(); processFolder(sourcePath, destPath, basePath, fileList, ps); } public static void processFolder(String sourcePath, String destPath, String basePath, Collection<String> fileList, PrintStream ps) throws Exception, InterruptedException { File src = new File(sourcePath); if (!src.isDirectory() || !src.canRead()) { throw new Exception("Source directory \"" + sourcePath + "\" does not exist, not a directory or not readable"); } if (ps != null) { ps.println("Exporting all jclic.zip files in \"" + src.getCanonicalPath() + "\" to \"" + destPath + "\""); } File dest = new File(destPath); File[] jclicZipFiles = src.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { return name.toLowerCase().endsWith(".jclic.zip"); } }); for (File f : jclicZipFiles) { if (interrupt) { interrupt = false; throw new InterruptedException(); } if (ps != null) { ps.println("\nProcessing file: " + f.getAbsolutePath()); } processSingleFile(f.getAbsolutePath(), dest.getAbsolutePath(), basePath, fileList, ps); } // Force garbage collection jclicZipFiles = null; System.gc(); // Process subdirectories File[] subDirs = src.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { return new File(dir, name).isDirectory(); } }); for (File f : subDirs) { if (interrupt) { interrupt = false; throw new InterruptedException(); } ProjectFileUtils.processFolder( new File(src, f.getName()).getCanonicalPath(), new File(dest, f.getName()).getCanonicalPath(), basePath, fileList, ps); } // Force garbage collection subDirs = null; System.gc(); } // Void implementation of "ResourceBridge" methods: // public java.io.InputStream getProgressInputStream(java.io.InputStream is, int expectedLength, String name) { return is; } public edu.xtec.util.Options getOptions() { return null; } public String getMsg(String key) { return key; } public javax.swing.JComponent getComponent() { return null; } public void displayUrl(String url, boolean inFrame) { throw new UnsupportedOperationException("Not supported"); } }