BBPackage.groovy

/**
 * 
 */
package edu.odu.cs.cowem;

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.Sync
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream;
import java.io.StringReader
import java.io.StringWriter;
import java.net.MalformedURLException
import java.nio.file.Files
import java.nio.file.FileSystem
import java.nio.file.FileSystems
import java.nio.file.Path
import java.nio.file.Paths
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipOutputStream

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;

import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import edu.odu.cs.cowem.documents.MarkdownDocument
import edu.odu.cs.cowem.documents.Utils;


/**
 * Prepares a package suitable for importation into Blackbaord.
 * This is an extension of the IMS CC standard.
 * 
 * @author zeil
 *
 */
class BBPackage {

    private static Logger logger =
    LoggerFactory.getLogger(BBPackage.class);



    Project project;

    Course course;

    File buildDir;


    File tempAreaAbs;
    File webcontentAbs;
    String webcontentRel;

    /**
     * Initialize the package builder
     * @param theProject    the website project 
     * @param theCourse     the course website descriptor
     * @param theBuildDirectory  the project build directory
     */
    BBPackage (Project theProject, Course theCourse,
    File theBuildDirectory)
    {
        project = theProject
        course = theCourse
        buildDir = theBuildDirectory
    }

    /**
     * Generate the package suitable for import
     * @param destination  where to store the resulting package
     * @param thin if true, package will contain a modules outline and
     *     calendar entries but the outline will link to an external website
     *     for actual content. If false, the full content is included in the
     *     package. 
     */
    void generate (File destination, boolean thin)
    {
        destination.getParentFile().mkdirs()
        tempAreaAbs = new File(new File(buildDir, "temp"), "bb")
        if (tempAreaAbs.exists()) {
            tempAreaAbs.deleteDir()  // Groovy extension to java.io.File
        }
        tempAreaAbs.mkdirs()
        copyFiles (thin)
        buildManifest (thin)
        addHiddenFiles ()
        zipItAllUp (destination)
    }



    void copyFiles (boolean isThin)
    {
        if (isThin) {
            webcontentRel = 'webcontent'
            webcontentAbs = new File(tempAreaAbs, webcontentRel)
            webcontentAbs.mkdirs()
            File placeHolder = new File(webcontentAbs, 'placeHolder.txt')
            placeHolder.withWriter('UTF-8') {
                it.writeLine('foo')
            }
        } else {
            DateTimeFormatter format = DateTimeFormatter.ofPattern(
                "yyyy-MM-dd'T'HH-mm-ss")
            webcontentRel = 'webcontent-' + 
                LocalDateTime.now().format(format)
            webcontentAbs = tempAreaAbs.toPath().resolve(
                    'csfiles/home_dir/' + webcontentRel).toFile()
            webcontentAbs.mkdirs()
            def websiteFiles = project.fileTree(
                    dir: 'build/website/', include: '**/*')

            project.copy {
                from websiteFiles
                into webcontentAbs
            }
        }
    }

    void buildManifest (boolean isThin)
    {
        Properties docProperties = new Properties()
        docProperties.put('_' + project.rootProject.course.delivery, '1')
        project.rootProject.course.properties.each { prop, value ->
            docProperties.put(prop, value.toString())
        }

        String primaryName = 'outline';
        docProperties.put ("_bb", "1")
        MarkdownDocument doc =
                new MarkdownDocument(project.file('Directory/outline/outline.md'),
                project.rootProject.website, docProperties);
        doc.setDebugMode(true);
        String result = doc.transform("modules")

        int bodyPos = result.indexOf("<body")
        int lastTagPos = result.indexOf("</html")
        result = result.substring(bodyPos, lastTagPos)

        StringBuilder outlineDoc = new StringBuilder("<imscc>\n")

        outlineDoc.append("<courseName>");
        outlineDoc.append(project.rootProject.course.courseName)
        outlineDoc.append("</courseName>\n");

        outlineDoc.append("<outline>\n")
        outlineDoc.append(result)
        outlineDoc.append("\n</outline>\n<navigation>\n")

        MarkdownDocument navDoc =
                new MarkdownDocument(project.file('Directory/navigation/navigation.md'),
                project.rootProject.website,
                docProperties);

        navDoc.setDebugMode(true);
        String navResult = navDoc.transform("navigation")
        int start = navResult.indexOf("<body");
        start = navResult.indexOf('>', start);
        navResult = navResult.substring(start+1);
        int stop = navResult.indexOf("</body");
        navResult = navResult.substring(0, stop);

        outlineDoc.append (navResult)
        outlineDoc.append("\n</navigation>\n")

        outlineDoc.append("\n<files>\n")
        if (!isThin) {
            listFiles (outlineDoc)
        }
        outlineDoc.append("\n</files>\n")
        outlineDoc.append("\n</imscc>\n")

        transformManifest (outlineDoc.toString(), isThin)



    }


    org.w3c.dom.Document parseXML (String xml)
    {
        org.w3c.dom.Document result = null;
        try {
            DocumentBuilder b =
                    DocumentBuilderFactory.newInstance().newDocumentBuilder();
            result = b.parse(new InputSource(new StringReader(xml)));
        } catch (ParserConfigurationException e) {
            logger.error ("Could not set up XML parser: " + e);
        } catch (SAXParseException e) {
            logger.error("Parsing error from outline: "
                    + e);
            if (e.toString().contains("lineNumber:")) {
                Pattern p = Pattern.compile(
                        "lineNumber: (\\d+); columnNumber: (\\d+);");
                Matcher m  = p.matcher(e.toString());
                if (m.find()) {
                    String lNum = m.group(1);
                    int ln = Integer.parseInt(lNum);
                    String cNum = m.group(2);
                    int cn = Integer.parseInt(cNum);
                    String context = Utils.extractContext(xml, ln-1, cn-1);
                    logger.error("Generated output was:\n" + context);
                } else {
                    logger.error("Text was:\n" + xml);
                }
            } else {
                logger.error("Text was:\n" + xml);
            }
        } catch (SAXException e) {
            logger.error("Unable to parse outline: ", e);
            logger.error("Text was:\n" + xml);
        } catch (IOException e) {
            logger.error("Unable to parse outline: ", e);
            logger.error("Text was:\n" + xml);
        }
        return result;
    }


    void transformManifest (String outlineXML, boolean isThin)
    {
        String transformName = (isThin) ? "bbthinManifest.xsl": "bbManifest.xsl"

        org.w3c.dom.Document outlineDoc = parseXML(outlineXML);
        
        final String xsltLocation  = "/edu/odu/cs/cowem/templates/";
        final InputStream outlineConversionSheet =
                MarkdownDocument.class.getResourceAsStream(xsltLocation + transformName);

        if (outlineConversionSheet == null) {
            logger.error("Could not load stylesheet: " + transformName);
            return;
        }

        final InputStream oCSheet =
                MarkdownDocument.class.getResourceAsStream(xsltLocation + transformName);
        

        System.setProperty("javax.xml.transform.TransformerFactory",
                "net.sf.saxon.TransformerFactoryImpl");
        TransformerFactory transFact = TransformerFactory.newInstance();

        DocumentBuilder dBuilder = null;
        try {
            DocumentBuilderFactory dbFactory =
                    DocumentBuilderFactory.newInstance();
            dBuilder = dbFactory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            logger.error ("Problem creating new XML document ", e);
            return;
        }

        // Transform basic HTML into the selected format
        Properties projProperties = new Properties()
        projProperties.put('_' + project.rootProject.course.delivery, '1')
        project.rootProject.course.properties.each { prop, value ->
            projProperties.put(prop, value.toString())
        }

        String manifestContent = "";
        try {
            Source xslSource = new StreamSource(outlineConversionSheet);
            xslSource.setSystemId("http://www.cs.odu.edu/~zeil");
            Templates template = transFact.newTemplates(xslSource);
            Transformer xform = template.newTransformer();
            xform.setParameter("workDir", tempAreaAbs.toString());
            xform.setParameter("webcontentURL", webcontentRel);
            for (Object okey: projProperties.keySet()) {
                String key = okey.toString();
                String value = projProperties.getProperty(key).toString();
                xform.setParameter(key, value);
            }
            Source xmlIn = new DOMSource(outlineDoc.getDocumentElement());

            StringWriter xmlString = new StringWriter();
            StreamResult xmlOut = new StreamResult(xmlString);

            xform.transform(xmlIn, xmlOut);
            manifestContent = xmlString.toString()
        } catch (TransformerConfigurationException e) {
            logger.error ("Problem parsing XSLT2 stylesheet "
                    + outlineConversionSheet, e);
            return;
        } catch (TransformerException e) {
            logger.error ("Problem applying stylesheet "
                    + outlineConversionSheet, e);
            return;
        }

        File resultFile = new File(tempAreaAbs, "imsmanifest.xml")
        resultFile.getParentFile().mkdirs()
        resultFile.withWriter('UTF-8') {
            it.writeLine(manifestContent)
        }

    }


    void listFiles (StringBuilder buf)
    {
        // ToDo
    }

    void addHiddenFiles()
    {
        // Currently handled as a side effect of the bb*manifest.xsl
    }

    void zipItAllUp (File destination)
    {
        File bbDir = project.file("build/temp/bb")
        Path bbBase = bbDir.toPath();
        ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(destination));
        zout.close();
        Path zipfile = destination.toPath();
        Queue<File> q = new LinkedList<File>();
        q.push(bbDir);
        FileSystem zipfs = FileSystems.newFileSystem(zipfile, null);
        if (zipfs == null) {
            logger.error ("Could not create zip file system at " + zipfile)
        }
        while (!q.isEmpty()) {
            File dir = q.remove()
            for (File child : dir.listFiles()) {
                Path relChild = bbBase.relativize(child.toPath());
                if (child.isDirectory()) {
                    q.add(child);
                    Path directory = zipfs.getPath("/", relChild.toString())
                    Files.createDirectories(directory)
                } else {
                    Path childLoc = zipfs.getPath("/", relChild.toString())
                    Files.copy(child.toPath(), childLoc)
                }
            }
        }
        zipfs.close()
    }
}