/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. */ package org.olat.modules.cp; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; import java.util.HashSet; import java.util.Properties; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringEscapeUtils; import org.apache.velocity.VelocityContext; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.runtime.RuntimeConstants; import org.olat.core.commons.modules.bc.FolderConfig; import org.olat.core.gui.components.tree.TreeNode; import org.olat.core.gui.render.velocity.VelocityModule; import org.olat.core.helpers.Settings; import org.olat.core.id.OLATResourceable; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.ExportUtil; import org.olat.core.util.FileUtils; import org.olat.core.util.WebappHelper; import org.olat.core.util.ZipUtil; import org.olat.core.util.vfs.LocalFileImpl; import org.olat.fileresource.FileResourceManager; /** * Description: <br> * Provides functionality to make a IMS-Content-Packaging offline readable with * menu just using a browser. The menu, an ordinary * <ul> * list, is turned into a dynamic, expandable, collapsible tree structure with * support from www.mattkruse.com/javascript. The Menu resides in the frame * FRAME_NAME_MENU and the Content in FRAME_NAME_CONTENT. * * @author alex */ public class CPOfflineReadableManager { private static CPOfflineReadableManager instance = new CPOfflineReadableManager(); private static final OLog log = Tracing.createLoggerFor(CPOfflineReadableManager.class); private static final String DIRNAME_CPOFFLINEMENUMAT = "cp_offline_menu_mat"; private static final String FILENAME_START = "_START_.html"; private static final String FILENAME_IMSMANIFEST = "imsmanifest.xml"; private static final String FRAME_NAME_CONTENT = "content"; private String rootTitle; private VelocityEngine velocityEngine; private CPOfflineReadableManager() { // private since singleton // init velocity engine Properties p = null; try { velocityEngine = new VelocityEngine(); p = new Properties(); p.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, "org.apache.velocity.runtime.log.SimpleLog4JLogSystem"); p.setProperty(RuntimeConstants.RESOURCE_MANAGER_CACHE_CLASS, "org.olat.core.gui.render.velocity.InfinispanResourceCache"); p.setProperty("runtime.log.logsystem.log4j.category", "syslog"); p.setProperty(RuntimeConstants.INPUT_ENCODING, VelocityModule.getInputEncoding()); p.setProperty(RuntimeConstants.OUTPUT_ENCODING, VelocityModule.getOutputEncoding()); p.setProperty("classpath.resource.loader.cache", Settings.isDebuging() ? "false" : "true"); velocityEngine.init(p); } catch (Exception e) { throw new RuntimeException("config error " + p.toString()); } } /** * @return instance of CPOfflineReadableManager */ public static CPOfflineReadableManager getInstance() { return instance; } /** * "exports" the the given CP (specified by its containing _unzipped_ directory) to a * zipFile.<br /> * The resulting zip contains a "offline-readable" version of the CP. * including style-sheets, menu-Tree and OpenOLAT branding * * @param ores * the containing directory * @param targetZip * the resulting zip-filename */ public void makeCPOfflineReadable(File unzippedDir, File targetZip) { try { writeOfflineCPStartHTMLFile(unzippedDir); File cpOfflineMat = new File(WebappHelper.getContextRealPath("/static/" + DIRNAME_CPOFFLINEMENUMAT)); zipOfflineReadableCP(unzippedDir, targetZip, cpOfflineMat); } catch (IOException e) { log.error("", e); } } public void makeCPOfflineReadable(String manifest, String indexSrc, ZipOutputStream exportStream) { try { //start page String startPage = getOfflineCPStartHTMLFile(manifest, indexSrc); exportStream.putNextEntry(new ZipEntry("_START_.html")); IOUtils.write(startPage, exportStream); exportStream.closeEntry(); File cpOfflineMat = new File(WebappHelper.getContextRealPath("/static/"), DIRNAME_CPOFFLINEMENUMAT); for(File content:cpOfflineMat.listFiles()) { exportStream.putNextEntry(new ZipEntry(DIRNAME_CPOFFLINEMENUMAT + "/" + content.getName())); InputStream in = new FileInputStream(content); FileUtils.cpio(in, exportStream, ""); exportStream.closeEntry(); in.close(); } } catch (IOException e) { log.error("", e); } } /** * "exports" the the given CP (specified by its OLATResourceable) to a * zipFile.<br /> * The resulting zip contains a "offline-readable" version of the CP. * including style-sheets, menu-Tree and OpenOLAT branding * * @param ores * the OLATResourceable (expected to be a CP) * @param zipName * the resulting zip-filename */ public void makeCPOfflineReadable(OLATResourceable ores, String zipName) { try { String repositoryHome = FolderConfig.getCanonicalRepositoryHome(); FileResourceManager fm = FileResourceManager.getInstance(); String relPath = fm.getUnzippedDirRel(ores); String resId = ores.getResourceableId().toString(); File unzippedDir = new File(repositoryHome + "/" + relPath); File targetZip = new File(repositoryHome + "/" + resId + "/" + zipName); File cpOfflineMat = new File(WebappHelper.getContextRealPath("/static/" + DIRNAME_CPOFFLINEMENUMAT)); writeOfflineCPStartHTMLFile(unzippedDir); zipOfflineReadableCP(unzippedDir, targetZip, cpOfflineMat); } catch (IOException e) { log.error("", e); } } /** * generates a html-file (_START_.html) that presents the given cp-content * (specified by its "_unzipped_"-dir). The resulting file is suitable for * offline reading of the cp. * * * @param unzippedDir * the directory that contains the unzipped CP */ private void writeOfflineCPStartHTMLFile(File unzippedDir) throws IOException { /* first, we do the menu-tree */ File mani = new File(unzippedDir, FILENAME_IMSMANIFEST); LocalFileImpl vfsMani = new LocalFileImpl(mani); CPManifestTreeModel ctm = new CPManifestTreeModel(vfsMani, ""); TreeNode root = ctm.getRootNode(); // let's take the rootnode title as page title this.rootTitle = root.getTitle(); StringBuilder menuTreeSB = new StringBuilder(); renderMenuTreeNodeRecursively(root, menuTreeSB, 0); // now put values to velocityContext VelocityContext ctx = new VelocityContext(); ctx.put("menutree", menuTreeSB.toString()); ctx.put("rootTitle", this.rootTitle); ctx.put("cpoff",DIRNAME_CPOFFLINEMENUMAT); StringWriter sw = new StringWriter(); try { String template = FileUtils.load(CPOfflineReadableManager.class.getResourceAsStream("_content/cpofflinereadable.html"), "utf-8"); boolean evalResult = velocityEngine.evaluate(ctx, sw, "cpexport", template); if (!evalResult) log.error("Could not evaluate velocity template for CP Export"); } catch (Exception e) { log.error("Error while evaluating velovity template for CP Export",e); } File f = new File(unzippedDir, FILENAME_START); if (f.exists()) { FileUtils.deleteDirsAndFiles(f, false, true); } ExportUtil.writeContentToFile(FILENAME_START, sw.toString(), unzippedDir, "utf-8"); } public String getOfflineCPStartHTMLFile(String manifest, String indexSrc) throws IOException { CPManifestTreeModel ctm = new CPManifestTreeModel(manifest, ""); TreeNode root = ctm.getRootNode(); // let's take the rootnode title as page title StringBuilder menuTreeSB = new StringBuilder(); renderMenuTreeNodeRecursively(root, menuTreeSB, 0); // now put values to velocityContext VelocityContext ctx = new VelocityContext(); ctx.put("menutree", menuTreeSB.toString()); ctx.put("rootTitle", root.getTitle()); ctx.put("cpoff",DIRNAME_CPOFFLINEMENUMAT); ctx.put("index", indexSrc); StringWriter sw = new StringWriter(); try { String template = FileUtils.load(CPOfflineReadableManager.class.getResourceAsStream("_content/cpofflinereadable.html"), "utf-8"); boolean evalResult = velocityEngine.evaluate(ctx, sw, "cpexport", template); if (!evalResult) log.error("Could not evaluate velocity template for CP Export"); } catch (Exception e) { log.error("Error while evaluating velovity template for CP Export",e); } return sw.toString(); } /** * @param node * @param sb * @param indent */ private void renderMenuTreeNodeRecursively(TreeNode node, StringBuilder sb, int level) { // set content to first accessible child or root node if no children // available // render current node String nodeUri = (String) node.getUserObject(); String title = node.getTitle(); String altText = node.getAltText(); sb.append("<li>\n"); if (node.isAccessible()) { sb.append("<a href=\""); sb.append(nodeUri); sb.append("\" target=\""); sb.append(FRAME_NAME_CONTENT); sb.append("\" alt=\""); sb.append(StringEscapeUtils.escapeHtml(altText)); sb.append("\" title=\""); sb.append(StringEscapeUtils.escapeHtml(altText)); sb.append("\">"); sb.append(title); sb.append("</a>\n"); } else { sb.append("<span title=\""); sb.append(StringEscapeUtils.escapeHtml(altText)); sb.append("\">"); sb.append(title); sb.append("</span>"); } // render all children boolean b = true; for (int i = 0; i < node.getChildCount(); i++) { if (b) { sb.append("<ul>\n"); } TreeNode child = (TreeNode) node.getChildAt(i); renderMenuTreeNodeRecursively(child, sb, level + 1); b = false; } if (!b) { sb.append("</ul>\n"); } sb.append("</li>\n"); } /** * copy the whole CPOFFLINEMENUMAT-Folder (mktree.js, mktree.css and gifs) * to the _unzipped_-Folder and zip everything that is in the * _unzipped_-Folder * * @param unzippedDir * @param targetZip * @param cpOfflineMat */ private void zipOfflineReadableCP(File unzippedDir, File targetZip, File cpOfflineMat) { // copy the offlineMat to unzippedDir FileUtils.copyDirToDir(cpOfflineMat, unzippedDir, "copy for offline readable cp"); if (targetZip.exists()) { FileUtils.deleteDirsAndFiles(targetZip, false, true); } else { // ZipUtil.zip expects the target-file's parent directory to exist! targetZip.getParentFile().mkdirs(); } Set<String> allFilesInUnzippedDir = new HashSet<String>(); String[] cpFiles = unzippedDir.list(); for (int i = 0; i < cpFiles.length; i++) { allFilesInUnzippedDir.add(cpFiles[i]); } boolean zipResult = ZipUtil.zip(allFilesInUnzippedDir, unzippedDir, targetZip, true); if(!targetZip.exists()){ log.warn("targetZip does not exists after zipping. zip-result is: "+ zipResult); } } }