Skip to content
Snippets Groups Projects
CPOfflineReadableManager.java 11.3 KiB
Newer Older
Alan Moran's avatar
Alan Moran committed
/**
 * 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.
 */
Alan Moran's avatar
Alan Moran committed

package org.olat.modules.cp;

import java.io.File;
import java.io.FileInputStream;
Alan Moran's avatar
Alan Moran committed
import java.util.HashSet;
Alan Moran's avatar
Alan Moran committed
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
Alan Moran's avatar
Alan Moran committed

import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.Logger;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
Alan Moran's avatar
Alan Moran committed
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;
Alan Moran's avatar
Alan Moran committed
import org.olat.core.id.OLATResourceable;
Alan Moran's avatar
Alan Moran committed
import org.olat.core.util.ExportUtil;
import org.olat.core.util.FileUtils;
srosse's avatar
srosse committed
import org.olat.core.util.StringHelper;
Alan Moran's avatar
Alan Moran committed
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 Logger log = Tracing.createLoggerFor(CPOfflineReadableManager.class);
Alan Moran's avatar
Alan Moran committed
	
	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";
Alan Moran's avatar
Alan Moran committed
	private static final String FRAME_NAME_CONTENT = "content";

	private String rootTitle;

Alan Moran's avatar
Alan Moran committed
	private CPOfflineReadableManager() {
		// private since singleton

		// init velocity engine
		Properties p = new Properties();
			p.setProperty(RuntimeConstants.RESOURCE_MANAGER_CACHE_CLASS, "org.olat.core.gui.render.velocity.InfinispanResourceCache");
			p.setProperty(RuntimeConstants.INPUT_ENCODING, VelocityModule.getInputEncoding());
			p.setProperty("resource.loader.classpath.cache", Settings.isDebuging() ? "false" : "true");
			velocityEngine.init(p);
		} catch (Exception e) {
			throw new RuntimeException("config error " + p);
Alan Moran's avatar
Alan Moran committed
	}

	/**
	 * @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
Alan Moran's avatar
Alan Moran committed
	 * 
Alan Moran's avatar
Alan Moran committed
	 * @param targetZip
Alan Moran's avatar
Alan Moran committed
	 */
	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);
		}
Alan Moran's avatar
Alan Moran committed
	}
	
	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, "UTF-8");
			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
Alan Moran's avatar
Alan Moran committed
	 * 
	 * @param ores
	 *            the OLATResourceable (expected to be a CP)
Alan Moran's avatar
Alan Moran committed
	 * @param zipName
Alan Moran's avatar
Alan Moran committed
	 */
	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.
	 * 
Alan Moran's avatar
Alan Moran committed
	 * 
	 * @param unzippedDir
	 *            the directory that contains the unzipped CP
Alan Moran's avatar
Alan Moran committed
	 */
	private void writeOfflineCPStartHTMLFile(File unzippedDir) throws IOException {
		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");
			log.error("Error while evaluating velovity template for CP Export",e);
		File f = new File(unzippedDir, FILENAME_START);
Alan Moran's avatar
Alan Moran committed
		if (f.exists()) {
			FileUtils.deleteDirsAndFiles(f, false, true);
		}
		ExportUtil.writeContentToFile(FILENAME_START, sw.toString(), unzippedDir, "utf-8");
Alan Moran's avatar
Alan Moran committed
	}
	
	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();
	}
Alan Moran's avatar
Alan Moran committed


	/**
	 * @param node
	 * @param sb
	 * @param indent
	 */
	private void renderMenuTreeNodeRecursively(TreeNode node, StringBuilder sb, int level) {
Alan Moran's avatar
Alan Moran committed
		// 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=\"");
srosse's avatar
srosse committed
			sb.append(StringHelper.escapeHtml(altText));
Alan Moran's avatar
Alan Moran committed
			sb.append("\" title=\"");
srosse's avatar
srosse committed
			sb.append(StringHelper.escapeHtml(altText));
Alan Moran's avatar
Alan Moran committed
			sb.append("\">");
			sb.append(title);
			sb.append("</a>\n");
		} else {
			sb.append("<span title=\"");
srosse's avatar
srosse committed
			sb.append(StringHelper.escapeHtml(altText));
Alan Moran's avatar
Alan Moran committed
			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);
Alan Moran's avatar
Alan Moran committed
			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
Alan Moran's avatar
Alan Moran committed
	 * 
	 * @param unzippedDir
	 * @param targetZip
	 * @param cpOfflineMat
	 */
	private void zipOfflineReadableCP(File unzippedDir, File targetZip, File cpOfflineMat) {
		// copy the offlineMat to unzippedDir
Alan Moran's avatar
Alan Moran committed
		FileUtils.copyDirToDir(cpOfflineMat, unzippedDir, "copy for offline readable cp");
Alan Moran's avatar
Alan Moran committed
		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>();
Alan Moran's avatar
Alan Moran committed
		String[] cpFiles = unzippedDir.list();
		for (int i = 0; i < cpFiles.length; i++) {
			allFilesInUnzippedDir.add(cpFiles[i]);
Alan Moran's avatar
Alan Moran committed
		}
		boolean zipResult = ZipUtil.zip(allFilesInUnzippedDir, unzippedDir, targetZip, true);
			log.warn("targetZip does not exists after zipping. zip-result is: "+ zipResult);