package org.sakaiproject.lessonbuildertool.ccexport; /*********** * * Copyright (c) 2013 Rutgers, the State University of New Jersey * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.OutputStream; import java.io.File; import java.io.FileInputStream; import java.io.PrintWriter; import java.io.PrintStream; import java.io.FileOutputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Map; import java.util.HashMap; import java.util.List; import java.util.ArrayList; import java.util.Set; import java.util.HashSet; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import java.util.regex.Pattern; import java.util.regex.Matcher; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringEscapeUtils; import org.sakaiproject.content.api.ContentCollection; import org.sakaiproject.content.api.ContentEntity; import org.sakaiproject.content.api.ContentHostingService; import org.sakaiproject.content.api.ContentResource; import org.sakaiproject.entity.api.ResourceProperties; import org.sakaiproject.component.cover.ServerConfigurationService; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.cover.SiteService; import org.sakaiproject.lessonbuildertool.SimplePageItem; import org.sakaiproject.lessonbuildertool.ccexport.SamigoExport; import org.sakaiproject.lessonbuildertool.ccexport.AssignmentExport; import org.sakaiproject.lessonbuildertool.ccexport.ForumsExport; import org.sakaiproject.lessonbuildertool.ccexport.ZipPrintStream; import org.sakaiproject.lessonbuildertool.model.SimplePageToolDao; import org.sakaiproject.lessonbuildertool.tool.view.ExportCCViewParameters; import org.sakaiproject.lessonbuildertool.tool.beans.SimplePageBean; import org.sakaiproject.tool.cover.SessionManager; import org.sakaiproject.tool.api.ToolSession; import org.sakaiproject.tool.api.Placement; import org.sakaiproject.tool.cover.SessionManager; import org.sakaiproject.tool.cover.ToolManager; import uk.org.ponder.messageutil.MessageLocator; import uk.org.ponder.rsf.viewstate.ViewParameters; public class CCExport { private static Log log = LogFactory.getLog(CCExport.class); private File root; private String rootPath; long nextid = 1; static ContentHostingService contentHostingService; public void setContentHostingService(ContentHostingService chs) { contentHostingService = chs; } static SamigoExport samigoExport; public void setSamigoExport(SamigoExport se) { samigoExport = se; } static AssignmentExport assignmentExport; public void setAssignmentExport(AssignmentExport se) { assignmentExport = se; } static ForumsExport forumsExport; public void setForumsExport(ForumsExport se) { forumsExport = se; } static BltiExport bltiExport; public void setBltiExport(BltiExport se) { bltiExport = se; } static MessageLocator messageLocator; public void setMessageLocator(MessageLocator x) { messageLocator = x; } static SimplePageToolDao simplePageToolDao; public void setSimplePageToolDao(Object dao) { simplePageToolDao = (SimplePageToolDao) dao; } HttpServletResponse response; File errFile = null; PrintStream errStream = null; String siteId = null; static String server = ServerConfigurationService.getServerName(); int version = V12; boolean doBank = false; class Resource { String sakaiId; String resourceId; String location; String use; String title; String url; boolean islink; List<String> dependencies; } // map of all file resource to be included in cartridge Map<String, Resource> fileMap = new HashMap<String, Resource>(); // map of all Samigo tests Map<String, Resource> samigoMap = new HashMap<String, Resource>(); Resource samigoBank = null; // map of all Assignments Map<String, Resource> assignmentMap = new HashMap<String, Resource>(); // map of all Forums Map<String, Resource> forumsMap = new HashMap<String, Resource>(); // map of all Blti instances Map<String, Resource> bltiMap = new HashMap(); // to prevent pages from being output more than once Set<Long> pagesDone = new HashSet(); // list of item ID's that use embed code. Need to output // an HTML page with the embed code. The string is the embed code, // with fixups done Map<Long, String> embedMap = new HashMap<Long,String>(); // itemID's that are links. Need to output the link XML file Set<Resource> linkSet = new HashSet<Resource>(); // the error messages are a problem. They won't show until the next page display // however errrors at this level are unusual, and we interrupt the download, so the // user should never see an incomplete one. Most common errors have to do with // problems converting for CC format. Those go into a log file that's included in // the ZIP, so the user will see those errors (if he knows the look) public static void setErrMessage(String s) { ToolSession toolSession = SessionManager.getCurrentToolSession(); if (toolSession == null) { log.error("Lesson Builder error not in tool: " + s); return; } List<String> errors = (List<String>)toolSession.getAttribute("lessonbuilder.errors"); if (errors == null) errors = new ArrayList<String>(); errors.add(s); toolSession.setAttribute("lessonbuilder.errors", errors); } public static void setErrKey(String key, String text ) { if (text == null) text = ""; setErrMessage(messageLocator.getMessage(key).replace("{}", text)); } // current we don't support 1.0 public static final int V11 = 1; public static final int V12 = 2; /* * maintain global lists of resources, adding as they are referenced on a page or * adding all resources of a kind, depending. Each type of resource has a map * indexed by sakai ID, with a generated ID for the cartridge and the name of the * file or XML file. * * the overall flow will be to load all resources and tests into the temp directory * and the maps, the walk the lesson hierarchy building imsmanifest.xml. Any resources * not used will get some kind of dummy entries in imsmanifest.xml, so that the whole * contents of the site is brought over. */ public void doExport(String sid, HttpServletResponse httpServletResponse, ExportCCViewParameters params) { response = httpServletResponse; siteId = sid; if ("1.1".equals(params.getVersion())) version = V11; if ("1".equals(params.getBank())) doBank = true; if (! startExport()) return; if (! addAllFiles(siteId)) return; if (! addAllSamigo(siteId)) return; if (! addAllAssignments(siteId)) return; if (! addAllForums(siteId)) return; if (!addAllBlti(siteId)) return; download(); } /* * create temp dir and start writing */ public boolean startExport() { try { root = File.createTempFile("ccexport", "root"); if (root.exists()) root.delete(); root.mkdir(); errFile = new File(root, "export-errors"); errStream = new PrintStream(errFile); } catch (Exception e) { log.error("Lessons export error outputting file, startExport " + e); setErrKey("simplepage.exportcc-fileerr", e.getMessage()); return false; } return true; } String getResourceId () { return "res" + (nextid++); } String getResourceIdPeek () { return "res" + nextid; } public void setIntendeduse (String sakaiId, String intendeduse) { Resource ref = fileMap.get(sakaiId); if (ref == null) return; ref.use = intendeduse; } String getLocation(String sakaiId) { Resource ref = fileMap.get(sakaiId); if (ref == null) return null; return ref.location; } public Resource addFile(String sakaiId, String location) { return addFile(sakaiId, location, null); } public Resource addFile(String sakaiId, String location, String use) { Resource res = new Resource(); res.sakaiId = sakaiId; res.resourceId = getResourceId(); res.location = location; res.dependencies = new ArrayList<String>(); res.use = use; res.islink = false; fileMap.put(sakaiId, res); return res; } public boolean addAllFiles(String siteId) { try { String base = contentHostingService.getSiteCollection(siteId); ContentCollection baseCol = contentHostingService.getCollection(base); return addAllFiles(baseCol, base.length()); } catch (org.sakaiproject.exception.IdUnusedException e) { setErrKey("simplepage.exportcc-noresource", e.getMessage()); return false; } catch (Exception e) { log.error("Lessons export error outputting file, addAllFiles " + e); setErrKey("simplepage.exportcc-fileerr", e.getMessage()); return false; } } public boolean addAllFiles(ContentCollection baseCol, int baselen) { try { List<ContentEntity> members = baseCol.getMemberResources(); for (ContentEntity e: members) { // don't export things we generate. Can lead to collisions String filename = e.getId().substring(baselen); if (filename.equals("cc-objects/export-errors") || filename.equals("cc-objects")) continue; if (e instanceof ContentResource) { boolean islink = ((ContentResource)e).getContentType().equals("text/url"); String location = null; if (islink) { location = "attachments/" + getResourceIdPeek() + ".xml"; String url = new String(((ContentResource)e).getContent()); // see if Youtube. If so, use the current recommended URL String youtubeKey = SimplePageBean.getYoutubeKeyFromUrl(url); if (youtubeKey != null) // code is also in ShowPageProducer. keep in sync url = SimplePageBean.getYoutubeUrlFromKey(youtubeKey); Resource res = addFile(e.getId(), location); res.islink = true; res.url = url; // try to get title from resource. Will normally be the URL res.title = ((ContentResource)e).getProperties().getProperty(ResourceProperties.PROP_DISPLAY_NAME); if (res.title == null) res.title = url; // queue this so we output the XML file linkSet.add(res); } else { location = e.getId().substring(baselen); Resource res = addFile(e.getId(), location); } } else addAllFiles((ContentCollection)e, baselen); } } catch (Exception e) { log.error("Lessons export error outputting file, addAllFiles 2 " + e); setErrKey("simplepage.exportcc-fileerr", e.getMessage()); return false; } return true; } public boolean outputAllFiles (ZipPrintStream out) { try { for (Map.Entry<String, Resource> entry: fileMap.entrySet()) { // normally this is a file ID for contenthosting. // But jforum gives us an actual filesystem filename. We stick /// on // the front to make that clear. inSakai is contenthosting. boolean inSakai = !entry.getKey().startsWith("///"); ZipEntry zipEntry = new ZipEntry(entry.getValue().location); // for contenthosting ContentResource resource = null; // for raw file File infile = null; InputStream instream = null; if (inSakai) { resource = contentHostingService.getResource(entry.getKey()); // if URL there's no file to output. The link XML file will // be done at the end, since some links are discovered while outputting manifest if (((Resource)entry.getValue()).islink) { continue; } else zipEntry.setSize(resource.getContentLength()); } else { infile = new File(entry.getKey().substring(3)); instream = new FileInputStream(infile); } out.putNextEntry(zipEntry); InputStream contentStream = null; // see if this is HTML. If so, we need to scan it. String filename = entry.getKey(); int lastdot = filename.lastIndexOf("."); int lastslash = filename.lastIndexOf("/"); String extension = ""; if (lastdot >= 0 && lastdot > lastslash) extension = filename.substring(lastdot+1); String mimeType = null; if (inSakai) mimeType = resource.getContentType(); boolean isHtml = false; if (mimeType != null && (mimeType.startsWith("http") || mimeType.equals(""))) mimeType = null; if (mimeType != null && (mimeType.equals("text/html") || mimeType.equals("application/xhtml+xml")) || mimeType == null && (extension.equals("html") || extension.equals("htm"))) { isHtml = true; } try { if (isHtml) { // treat html separately. Need to convert urls to relative String content = null; if (inSakai) content = new String(resource.getContent()); else { byte[] b = new byte[(int) infile.length()]; instream.read(b); content = new String(b); } content = relFixup(content, entry.getValue()); out.print(content); } else { if (inSakai) contentStream = resource.streamContent(); else contentStream = instream; IOUtils.copy(contentStream, out); } } catch (Exception e) { log.error("Lessons export error outputting file " + e); } finally { if (contentStream != null) { contentStream.close(); } } } } catch (Exception e) { log.error("Lessons export error outputting file, outputAllFiles " + e); setErrKey("simplepage.exportcc-fileerr", e.getMessage()); return false; } return true; } public boolean addAllSamigo(String siteId) { List<String> tests = samigoExport.getEntitiesInSite(siteId); if (tests == null) return true; // These are going to be loaded into the final file system. I considered // putting them in a separate directory to avoid conflicting with real files. // However this would force all URLs to be written with ../ at the start, // which is probably more dangerous, as it depends upon loaders making the // same interpretation of a somewhat ambiguous specification. for (String sakaiId: tests) { Resource res = new Resource(); res.resourceId = getResourceId(); res.location = "cc-objects/" + res.resourceId + ".xml"; res.sakaiId = sakaiId; res.dependencies = new ArrayList<String>(); res.use = null; res.islink = false; samigoMap.put(res.sakaiId, res); } if (doBank && samigoExport.havePoolItems()) { Resource res = new Resource(); res.resourceId = getResourceId(); res.location = "cc-objects/" + res.resourceId + ".xml"; res.sakaiId = null; res.dependencies = new ArrayList<String>(); res.use = null; res.islink = false; samigoBank = res; } return true; } public boolean outputAllSamigo(ZipPrintStream out) { try { for (Map.Entry<String, Resource> entry: samigoMap.entrySet()) { ZipEntry zipEntry = new ZipEntry(entry.getValue().location); out.putNextEntry(zipEntry); boolean ok = samigoExport.outputEntity(entry.getValue().sakaiId, out, errStream, this, entry.getValue(), version); if (!ok) return false; } if (samigoBank != null) { ZipEntry zipEntry = new ZipEntry(samigoBank.location); out.putNextEntry(zipEntry); boolean ok = samigoExport.outputBank(out, errStream, this, samigoBank, version); if (!ok) return false; } } catch (Exception e) { log.error("output sam " + e); setErrKey("simplepage.exportcc-fileerr", e.getMessage()); e.printStackTrace(); return false; } return true; } public boolean addAllAssignments(String siteId) { List<String> assignments = assignmentExport.getEntitiesInSite(siteId, this); if (assignments == null) return true; for (String sakaiId: assignments) { Resource res = new Resource(); res.resourceId = getResourceId(); int slash = sakaiId.indexOf("/"); res.location = "attachments/" + sakaiId.substring(slash+1) + "/assignmentpage.html"; res.sakaiId = sakaiId; res.dependencies = new ArrayList<String>(); res.use = null; res.islink = false; assignmentMap.put(res.sakaiId, res); } return true; } public boolean outputAllAssignments(ZipPrintStream out) { try { for (Map.Entry<String, Resource> entry: assignmentMap.entrySet()) { ZipEntry zipEntry = new ZipEntry(entry.getValue().location); out.putNextEntry(zipEntry); boolean ok = assignmentExport.outputEntity(entry.getValue().sakaiId, out, errStream, this, entry.getValue()); if (!ok) return false; } } catch (Exception e) { log.error("Lessons export error outputting file, outputAllAssignments " + e); setErrKey("simplepage.exportcc-fileerr", e.getMessage()); return false; } return true; } public boolean addAllForums(String siteId) { List<String> forums = forumsExport.getEntitiesInSite(siteId, this); if (forums == null) return true; for (String sakaiId: forums) { Resource res = new Resource(); res.resourceId = getResourceId(); res.location = "cc-objects/" + res.resourceId + ".xml"; res.sakaiId = sakaiId; res.dependencies = new ArrayList<String>(); res.use = null; res.islink = false; forumsMap.put(res.sakaiId, res); } return true; } public boolean outputAllForums(ZipPrintStream out) { try { for (Map.Entry<String, Resource> entry: forumsMap.entrySet()) { ZipEntry zipEntry = new ZipEntry(entry.getValue().location); out.putNextEntry(zipEntry); boolean ok = forumsExport.outputEntity(entry.getValue().sakaiId, out, errStream, this, entry.getValue(), version); if (!ok) return false; } } catch (Exception e) { log.error("problem in outputallforums, outputAllForums " + e); setErrKey("simplepage.exportcc-fileerr", e.getMessage()); return false; } return true; } public boolean addAllBlti(String siteId) { List<String> bltis = bltiExport.getEntitiesInSite(siteId, this); if (bltis == null) return true; for (String sakaiId : bltis) { Resource res = new Resource(); res.resourceId = getResourceId(); res.location = ("cc-objects/" + res.resourceId + ".xml"); res.sakaiId = sakaiId; res.dependencies = new ArrayList(); res.use = null; res.islink = false; bltiMap.put(res.sakaiId, res); } return true; } public boolean outputAllBlti(ZipPrintStream out) { try { for (Map.Entry entry : bltiMap.entrySet()) { ZipEntry zipEntry = new ZipEntry(((Resource)entry.getValue()).location); out.putNextEntry(zipEntry); boolean ok = bltiExport.outputEntity(((Resource)entry.getValue()).sakaiId, out, this.errStream, this, (Resource)entry.getValue(), version); if (!ok) return false; } } catch (Exception e) { log.error("problem in outputallforums, outputAllBlti " + e); setErrKey("simplepage.exportcc-fileerr", e.getMessage()); return false; } return true; } public boolean outputAllTexts(ZipPrintStream out) { try { List<SimplePageItem> items = simplePageToolDao.findTextItemsInSite(this.siteId); for (SimplePageItem item : items) { String location = "attachments/item-" + item.getId() + ".html"; ZipEntry zipEntry = new ZipEntry(location); out.putNextEntry(zipEntry); out.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"); out.println("<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">"); out.println("<body>"); out.print(item.getHtml()); out.println("</body>"); out.println("</html>"); Resource res = new Resource(); res.sakaiId = ("/text/" + item.getId()); res.resourceId = getResourceId(); res.location = location; res.dependencies = new ArrayList(); res.use = null; res.islink = false; fileMap.put(res.sakaiId, res); } } catch (Exception e) { log.error("Lessons export error outputting file, outputAllTexts " + e); setErrKey("simplepage.exportcc-fileerr", e.getMessage()); return false; } return true; } public boolean outputLessons(ZipPrintStream out) { out.println(" <organization identifier=\"page\" structure=\"rooted-hierarchy\">"); out.println(" <item identifier=\"I_1\">"); List<SimplePageItem> sitePages = simplePageToolDao.findItemsInSite(ToolManager.getCurrentPlacement().getContext()); for (SimplePageItem i : sitePages) pagesDone.add(Long.valueOf(i.getSakaiId())); for (SimplePageItem i : sitePages) { outputLessonPage(out, Long.valueOf(i.getSakaiId()), i.getName(), 6, true); } out.println(" </item>"); out.println(" </organization>"); return true; } public void outputIndent(ZipPrintStream out, int indent) { for (int i = 0; i < indent; i++) out.print(" "); } public SimplePageItem outputLessonPage(ZipPrintStream out, Long pageId, String title, int indent, boolean shownext) { SimplePageItem next = null; boolean multiplenext = false; pagesDone.add(pageId); outputIndent(out, indent); out.println("<item identifier=\"page_" + pageId + "\">"); outputIndent(out, indent + 2); out.println("<title>" + StringEscapeUtils.escapeXml(title) + "</title>"); List<SimplePageItem> items = simplePageToolDao.findItemsOnPage(pageId.longValue()); for (SimplePageItem item : items) { if (item.getNextPage()) { if (next == null) { next = item; } else if (!multiplenext) { next = null; multiplenext = true; } } } for (SimplePageItem item : items) { Resource res = null; String sakaiId = null; String itemString = null; String urlTitle = null; switch (item.getType()) { case SimplePageItem.PAGE: Long pId = Long.valueOf(item.getSakaiId()); if (this.pagesDone.contains(pId)) { this.errStream.println(messageLocator.getMessage("simplepage.exportcc-pagealreadydone").replace("{1}", title).replace("{2}", item.getName())); } else if ((next != null) && (item.getId() == next.getId())) { if (shownext) { SimplePageItem n = outputLessonPage(out, pId, item.getName(), indent + 2, false); while ((n != null) && (!this.pagesDone.contains(pId = Long.valueOf(n.getSakaiId())))) { n = outputLessonPage(out, pId, n.getName(), indent + 2, false); } if ((n != null) && (this.pagesDone.contains(pId))) { errStream.println(messageLocator.getMessage("simplepage.exportcc-pagealreadydone").replace("{1}", title).replace("{2}", item.getName())); } } } else { outputLessonPage(out, pId, item.getName(), indent + 2, true); } break; case SimplePageItem.MULTIMEDIA: String embedCode = item.getAttribute("multimediaEmbedCode"); if (embedCode != null && embedCode.length() > 0) { String location = "attachments/item-" + item.getId() + ".html"; res = new Resource(); res.sakaiId = ("/text/" + item.getId()); res.resourceId = getResourceId(); res.location = location; res.dependencies = new ArrayList(); res.use = null; res.islink = false; fileMap.put(res.sakaiId, res); embedMap.put(item.getId(), relFixup(embedCode, res)); // item won't have a title, so we have to specify one. But with an embed code // there's no useful title. So just use generic text. urlTitle = messageLocator.getMessage("simplepage.importcc-embedtitle"); break; } String oembed = item.getAttribute("multimediaUrl"); if (oembed != null && oembed.length() > 0) { // we've already done outputAllFiles, so this code is simply // to get the <resource> output. String location = "attachments/item-" + item.getId() + ".xml"; // first argument is dummy in this case, since it's not in the file system res = addFile(oembed, location); res.islink = true; res.url = oembed; res.title = item.getName(); if (res.title == null || res.title.length() == 0) res.title = oembed; // queue this to output the XML link file linkSet.add(res); // no actual item, so we need to supply a title. Use the URL urlTitle = oembed; break; } case SimplePageItem.RESOURCE: res = (Resource)this.fileMap.get(item.getSakaiId()); break; case SimplePageItem.ASSIGNMENT: sakaiId = item.getSakaiId(); if (sakaiId.indexOf("/", 1) < 0) sakaiId = "assignment/" + sakaiId; else sakaiId = sakaiId.substring(1); res = (Resource)this.assignmentMap.get(sakaiId); break; case SimplePageItem.ASSESSMENT: sakaiId = item.getSakaiId(); if (sakaiId.indexOf("/", 1) < 0) sakaiId = "sam_pub/" + sakaiId; else sakaiId = sakaiId.substring(1); res = (Resource)samigoMap.get(sakaiId); break; case SimplePageItem.TEXT: res = (Resource)fileMap.get("/text/" + item.getId()); break; case SimplePageItem.FORUM: res = (Resource)forumsMap.get(item.getSakaiId().substring(1)); break; case SimplePageItem.BLTI: res = (Resource)bltiMap.get(item.getSakaiId().substring(1)); break; case SimplePageItem.COMMENTS: case SimplePageItem.STUDENT_CONTENT: case SimplePageItem.QUESTION: case SimplePageItem.PEEREVAL: switch (item.getType()) { case SimplePageItem.COMMENTS: itemString = messageLocator.getMessage("simplepage.comments-section"); break; case SimplePageItem.STUDENT_CONTENT: itemString = messageLocator.getMessage("simplepage.student-content"); break; case SimplePageItem.QUESTION: itemString = messageLocator.getMessage("simplepage.questionName"); break; case SimplePageItem.PEEREVAL: itemString = messageLocator.getMessage("simplepage.peerEval-secotion"); break; } errStream.println(messageLocator.getMessage("simplepage.exportcc-bad-type").replace("{1}", title).replace("{2}", item.getName()).replace("{3}", itemString)); break; } if (res != null) { outputIndent(out, indent + 2); out.println("<item identifier=\"item_" + item.getId() + "\" identifierref=\"" + res.resourceId + "\">"); String ititle = item.getName(); if ((ititle == null) || (ititle.equals(""))) { if (urlTitle != null) ititle = urlTitle; else ititle = messageLocator.getMessage("simplepage.importcc-texttitle"); } outputIndent(out, indent + 4); out.println("<title>" + StringEscapeUtils.escapeXml(ititle) + "</title>"); outputIndent(out, indent + 2); out.println("</item>"); } } outputIndent(out, indent); out.println("</item>"); if (shownext) { return null; } return next; } public boolean outputManifest(ZipPrintStream out) { String title = "Sakai"; // should never be used try { Site site = null; site = SiteService.getSite(siteId); title = site.getTitle(); } catch (Exception impossible) { // impossible, one hopes } try { ZipEntry zipEntry = new ZipEntry("imsmanifest.xml"); out.putNextEntry(zipEntry); switch (version) { case V11: out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); out.println("<manifest identifier=\"cctd0001\""); out.println(" xmlns=\"http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1\""); out.println(" xmlns:lom=\"http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource\""); out.println(" xmlns:lomimscc=\"http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest\""); out.println(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "); out.println(" xsi:schemaLocation=\""); out.println(" http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource http://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lomresource_v1p0.xsd "); out.println(" http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1 http://www.imsglobal.org/profile/cc/ccv1p1/ccv1p1_imscp_v1p2_v1p0.xsd "); out.println(" http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest http://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lommanifest_v1p0.xsd\">"); out.println(" <metadata>"); out.println(" <schema>IMS Common Cartridge</schema>"); out.println(" <schemaversion>1.1.0</schemaversion>"); out.println(" <lomimscc:lom>"); out.println(" <lomimscc:general>"); out.println(" <lomimscc:title>"); out.println(" <lomimscc:string>" + StringEscapeUtils.escapeXml(title) + "</lomimscc:string>"); out.println(" </lomimscc:title>"); // out.println(" <lomimscc:description>"); // out.println(" <lomimscc:string language=\"en-US\">Sakai Export, including only files from site</lomimscc:string>"); // out.println(" </lomimscc:description>"); // out.println(" <lomimscc:keyword>"); // out.println(" <lomimscc:string language=\"en-US\">Export</lomimscc:string>"); // out.println(" </lomimscc:keyword>"); out.println(" </lomimscc:general>"); out.println(" </lomimscc:lom>"); out.println(" </metadata>"); break; default: out.print( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<manifest identifier=\"sakai1\"\n xmlns=\"http://www.imsglobal.org/xsd/imsccv1p2/imscp_v1p1\"\nxmlns:lom=\"http://ltsc.ieee.org/xsd/imsccv1p2/LOM/resource\"\nxmlns:lomimscc=\"http://ltsc.ieee.org/xsd/imsccv1p2/LOM/manifest\"\nxmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\nxsi:schemaLocation=\" \n http://ltsc.ieee.org/xsd/imsccv1p2/LOM/resource http://www.imsglobal.org/profile/cc/ccv1p2/LOM/ccv1p2_lomresource_v1p0.xsd \n http://www.imsglobal.org/xsd/imsccv1p2/imscp_v1p1 http://www.imsglobal.org/profile/cc/ccv1p2/ccv1p2_imscp_v1p2_v1p0.xsd \n http://ltsc.ieee.org/xsd/imsccv1p2/LOM/manifest http://www.imsglobal.org/profile/cc/ccv1p2/LOM/ccv1p2_lommanifest_v1p0.xsd\">\n <metadata>\n <schema>IMS Common Cartridge</schema>\n <schemaversion>1.2.0</schemaversion>\n <lomimscc:lom>\n <lomimscc:general>\n <lomimscc:title>\n <lomimscc:string>" + StringEscapeUtils.escapeXml(title) + "</lomimscc:string>\n </lomimscc:title>\n </lomimscc:general>\n </lomimscc:lom>\n </metadata>\n "); } out.println(" <organizations>"); outputLessons(out); out.println(" </organizations>"); String qtiid = null; String bankid = null; String topicid = null; String linkid = null; String usestr = ""; switch (version) { case V11: qtiid = "imsqti_xmlv1p2/imscc_xmlv1p1/assessment"; bankid = "imsqti_xmlv1p2/imscc_xmlv1p1/question-bank"; topicid = "imsdt_xmlv1p1"; linkid = "imswl_xmlv1p1"; usestr = ""; break; default: qtiid = "imsqti_xmlv1p2/imscc_xmlv1p2/assessment"; bankid = "imsqti_xmlv1p2/imscc_xmlv1p2/question-bank"; topicid = "imsdt_xmlv1p2"; linkid = "imswl_xmlv1p2"; usestr = " intendeduse=\"assignment\""; } out.println(" <resources>"); for (Map.Entry<String, Resource> entry: fileMap.entrySet()) { String use = ""; if (version >= V12) { if (entry.getValue().use != null) use = " intendeduse=\"" + entry.getValue().use + "\""; } String type = "webcontent"; if (((Resource)entry.getValue()).islink) type = linkid; out.println(" <resource href=\"" + StringEscapeUtils.escapeXml(entry.getValue().location) + "\" identifier=\"" + entry.getValue().resourceId + "\" type=\"" + type + "\"" + use + ">"); out.println(" <file href=\"" + StringEscapeUtils.escapeXml(entry.getValue().location) + "\"/>"); out.println(" </resource>"); } for (Map.Entry<String, Resource> entry: samigoMap.entrySet()) { out.println(" <resource href=\"" + StringEscapeUtils.escapeXml(entry.getValue().location) + "\" identifier=\"" + entry.getValue().resourceId + "\" type=\"" + qtiid + "\">"); out.println(" <file href=\"" + StringEscapeUtils.escapeXml(entry.getValue().location) + "\"/>"); for (String d: entry.getValue().dependencies) out.println(" <dependency identifierref=\"" + d + "\"/>"); out.println(" </resource>"); } // question bank if (samigoBank != null) { out.println(" <resource href=\"" + StringEscapeUtils.escapeXml(samigoBank.location) + "\" identifier=\"" + samigoBank.resourceId + "\" type=\"" + bankid + "\">"); out.println(" <file href=\"" + StringEscapeUtils.escapeXml(samigoBank.location) + "\"/>"); for (String d: samigoBank.dependencies) out.println(" <dependency identifierref=\"" + d + "\"/>"); out.println(" </resource>"); } for (Map.Entry<String, Resource> entry: assignmentMap.entrySet()) { out.println(" <resource href=\"" + StringEscapeUtils.escapeXml(entry.getValue().location) + "\" identifier=\"" + entry.getValue().resourceId + "\" type=\"webcontent\"" + usestr + ">"); out.println(" <file href=\"" + StringEscapeUtils.escapeXml(entry.getValue().location) + "\"/>"); for (String d: entry.getValue().dependencies) out.println(" <dependency identifierref=\"" + d + "\"/>"); out.println(" </resource>"); } for (Map.Entry<String, Resource> entry: forumsMap.entrySet()) { out.println(" <resource href=\"" + StringEscapeUtils.escapeXml(entry.getValue().location) + "\" identifier=\"" + entry.getValue().resourceId + "\" type=\"" + topicid + "\">"); out.println(" <file href=\"" + StringEscapeUtils.escapeXml(entry.getValue().location) + "\"/>"); for (String d: entry.getValue().dependencies) out.println(" <dependency identifierref=\"" + d + "\"/>"); out.println(" </resource>"); } for (Map.Entry entry : this.bltiMap.entrySet()) { out.println(" <resource href=\"" + StringEscapeUtils.escapeXml(((Resource)entry.getValue()).location) + "\" identifier=\"" + ((Resource)entry.getValue()).resourceId + "\" type=\"imsbasiclti_xmlv1p0\">"); out.println(" <file href=\"" + StringEscapeUtils.escapeXml(((Resource)entry.getValue()).location) + "\"/>"); for (String d : ((Resource)entry.getValue()).dependencies) out.println(" <dependency identifierref=\"" + d + "\"/>"); out.println(" </resource>"); } // add error log at the very end String errId = getResourceId(); out.println((" <resource href=\"cc-objects/export-errors\" identifier=\"" + errId + "\" type=\"webcontent\">\n <file href=\"cc-objects/export-errors\"/>\n </resource>")); out.println(" </resources>\n</manifest>"); // items with embed code. need to put out the HTML page for (Map.Entry entry : this.embedMap.entrySet()) { Long itemId = (Long)entry.getKey(); String location = "attachments/item-" + itemId + ".html"; ZipEntry ze = new ZipEntry(location); out.putNextEntry(ze); out.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"); out.println("<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">"); out.println("<body>"); out.print((String)entry.getValue()); out.println("</body>"); out.println("</html>"); } // links. need to put out the XML file defining the link for (Resource res: linkSet) { ZipEntry ze = new ZipEntry(res.location); out.putNextEntry(ze); switch (version) { case V11: out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); out.println("<webLink xmlns=\"http://www.imsglobal.org/xsd/imsccv1p1/imswl_v1p1\""); out.println(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""); out.println(" xsi:schemaLocation=\"http://www.imsglobal.org/xsd/imsccv1p1/imswl_v1p1 http://www.imsglobal.org/profile/cc/ccv1p1/ccv1p1_imswl_v1p1.xsd\">"); out.println(" <title>" + StringEscapeUtils.escapeXml(res.title) + "</title>"); out.println(" <url href=\"" + StringEscapeUtils.escapeXml(res.url) + "\"/>"); out.println("</webLink>"); break; default: out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); out.println("<webLink xmlns=\"http://www.imsglobal.org/xsd/imsccv1p2/imswl_v1p2\""); out.println(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""); out.println(" xsi:schemaLocation=\"http://www.imsglobal.org/xsd/imsccv1p2/imswl_v1p2 http://www.imsglobal.org/profile/cc/ccv1p2/ccv1p2_imswl_v1p2.xsd\">"); out.println(" <title>" + StringEscapeUtils.escapeXml(res.title) + "</title>"); out.println(" <url href=\"" + StringEscapeUtils.escapeXml(res.url) + "\"/>"); out.println("</webLink>"); } } errStream.close(); zipEntry = new ZipEntry("cc-objects/export-errors"); out.putNextEntry(zipEntry); InputStream contentStream = null; try { contentStream = new FileInputStream(errFile); IOUtils.copy(contentStream, out); } finally { if (contentStream != null) { contentStream.close(); } } } catch (Exception e) { log.error("Lessons export error outputting file, outputManifest " + e); setErrKey("simplepage.exportcc-fileerr", e.getMessage()); return false; } return true; } public boolean download() { OutputStream htmlOut = null; ZipPrintStream out = null; try { htmlOut = response.getOutputStream(); out = new ZipPrintStream(htmlOut); response.setHeader("Content-disposition", "inline; filename=sakai-export.imscc"); response.setContentType("application/zip"); outputAllFiles (out); outputAllSamigo (out); outputAllAssignments (out); outputAllForums (out); outputAllBlti(out); outputAllTexts(out); outputManifest (out); if (out != null) out.close(); } catch (Exception ioe) { if (out != null) { try { out.close(); } catch (Exception ignore) { } } log.error("Lessons export error outputting file, download " + ioe); setErrKey("simplepage.exportcc-fileerr", ioe.getMessage()); return false; } return true; } public void addDependency(Resource resource, String sakaiId) { Resource ref = fileMap.get(sakaiId); if (ref != null) resource.dependencies.add(ref.resourceId); } public String fixup (String s, Resource resource) { // http://lessonbuilder.sakaiproject.org/53605/ StringBuilder ret = new StringBuilder(); String sakaiIdBase = "/group/" + siteId; Pattern target = Pattern.compile("/access/content/group/" + siteId + "|http://lessonbuilder.sakaiproject.org/", Pattern. CASE_INSENSITIVE); Matcher matcher = target.matcher(s); // technically / isn't allowed in an unquoted attribute, but sometimes people // use sloppy HTML Pattern wordend = Pattern.compile("[^-a-zA-Z0-9._:/]"); int index = 0; while (true) { if (!matcher.find()) { ret.append(s.substring(index)); break; } String sakaiId = null; int start = matcher.start(); if (s.regionMatches(false, start, "/access", 0, 7)) { // matched /access/content... int sakaistart = start + "/access/content".length(); //start of sakaiid, can't find end until we figure out quoting int last = start + "/access/content/group/".length() + siteId.length(); if (s.regionMatches(true, (start - server.length()), server, 0, server.length())) { // servername before it start -= server.length(); if (s.regionMatches(true, start - 7, "http://", 0, 7)) { // http:// or https:// before that start -= 7; } else if (s.regionMatches(true, start - 8, "https://", 0, 8)) { start -= 8; } } // need to find sakaiend. To do that we need to find the close quote int sakaiend = 0; char quote = s.charAt(start-1); if (quote == '\'' || quote == '"') // quoted, this is easy sakaiend = s.indexOf(quote, sakaistart); else { // not quoted. find first char not legal in unquoted attribute Matcher wordendMatch = wordend.matcher(s); if (wordendMatch.find(sakaistart)) { sakaiend = wordendMatch.start(); } else sakaiend = s.length(); } sakaiId = s.substring(sakaistart, sakaiend); ret.append(s.substring(index, start)); ret.append("$IMS-CC-FILEBASE$.."); index = last; // start here next time } else { // matched http://lessonbuilder.sakaiproject.org/ int last = matcher.end(); // should be start of an integer int endnum = s.length(); // end of the integer for (int i = last; i < s.length(); i++) { if ("0123456789".indexOf(s.charAt(i)) < 0) { endnum = i; break; } } String numString = s.substring(last, endnum); if (numString.length() >= 1) { Long itemId = new Long(numString); SimplePageItem item = simplePageToolDao.findItem(itemId); sakaiId = item.getSakaiId(); int itemType = item.getType(); if ((itemType == SimplePageItem.RESOURCE || itemType == SimplePageItem.MULTIMEDIA) && sakaiId.startsWith(sakaiIdBase)) { ret.append(s.substring(index, start)); ret.append("$IMS-CC-FILEBASE$.." + sakaiId.substring(sakaiIdBase.length())); index = endnum; } } } if (sakaiId != null) { Resource r = fileMap.get(sakaiId); if (r != null) { resource.dependencies.add(r.resourceId); } } } return StringEscapeUtils.escapeXml(ret.toString()); } // turns the links into relative links public String relFixup (String s, Resource resource) { // http://lessonbuilder.sakaiproject.org/53605/ StringBuilder ret = new StringBuilder(); String sakaiIdBase = "/group/" + siteId; Pattern target = Pattern.compile("/access/content/group/" + siteId + "|http://lessonbuilder.sakaiproject.org/", Pattern. CASE_INSENSITIVE); Matcher matcher = target.matcher(s); // technically / isn't allowed in an unquoted attribute, but sometimes people // use sloppy HTML Pattern wordend = Pattern.compile("[^-a-zA-Z0-9._:/]"); int index = 0; while (true) { if (!matcher.find()) { ret.append(s.substring(index)); break; } String sakaiId = null; int start = matcher.start(); if (s.regionMatches(false, start, "/access", 0, 7)) { // matched /access/content... int sakaistart = start + "/access/content".length(); //start of sakaiid, can't find end until we figure out quoting int last = start + "/access/content/group/".length() + siteId.length(); if (s.regionMatches(true, (start - server.length()), server, 0, server.length())) { // servername before it start -= server.length(); if (s.regionMatches(true, start - 7, "http://", 0, 7)) { // http:// or https:// before that start -= 7; } else if (s.regionMatches(true, start - 8, "https://", 0, 8)) { start -= 8; } } // need to find sakaiend. To do that we need to find the close quote int sakaiend = 0; char quote = s.charAt(start-1); if (quote == '\'' || quote == '"') // quoted, this is easy sakaiend = s.indexOf(quote, sakaistart); else { // not quoted. find first char not legal in unquoted attribute Matcher wordendMatch = wordend.matcher(s); if (wordendMatch.find(sakaistart)) { sakaiend = wordendMatch.start(); } else sakaiend = s.length(); } last = sakaiend; sakaiId = s.substring(sakaistart, sakaiend); ret.append(s.substring(index, start)); // do the mapping. resource.location is a relative URL of the page we're looking at // sakaiid is the URL of the object, starting /group/ String base = getParent(resource.location); String thisref = sakaiId.substring(sakaiIdBase.length()+1); String relative = relativize(thisref, base); ret.append(relative.toString()); index = last; // start here next time } else { // matched http://lessonbuilder.sakaiproject.org/ int last = matcher.end(); // should be start of an integer int endnum = s.length(); // end of the integer for (int i = last; i < s.length(); i++) { if ("0123456789".indexOf(s.charAt(i)) < 0) { endnum = i; break; } } String numString = s.substring(last, endnum); if (numString.length() >= 1) { Long itemId = new Long(numString); SimplePageItem item = simplePageToolDao.findItem(itemId); sakaiId = item.getSakaiId(); int itemType = item.getType(); if ((itemType == SimplePageItem.RESOURCE || itemType == SimplePageItem.MULTIMEDIA) && sakaiId.startsWith(sakaiIdBase)) { ret.append(s.substring(index, start)); String base = getParent(resource.location); String thisref = sakaiId.substring(sakaiIdBase.length()+1); String relative = relativize(thisref, base); ret.append(relative); index = endnum; } } } if (sakaiId != null) { Resource r = fileMap.get(sakaiId); if (r != null) { resource.dependencies.add(r.resourceId); } } } return ret.toString(); } // return base directory of file, including trailing / // "" if it is in home directory public String getParent(String s) { int i = s.lastIndexOf("/"); if (i < 0) return ""; else return s.substring(0, i+1); } // return relative path to target from base // base is assumed to be "" or ends in / public String relativize(String target, String base) { if (base.equals("")) return target; if (target.startsWith(base)) return target.substring(base.length()); else { // get parent directory of base directory. // base directory ends in / int i = base.lastIndexOf("/", base.length()-2); if (i < 0) base = ""; else base = base.substring(0, i+1); // include / return "../" + relativize(target, base); } } }