org.etudes.mneme.impl.ExportQtiServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.etudes.mneme.impl.ExportQtiServiceImpl.java

Source

/**********************************************************************************
 * $URL: https://source.etudes.org/svn/apps/mneme/trunk/mneme-impl/impl/src/java/org/etudes/mneme/impl/ExportQtiServiceImpl.java $
 * $Id: ExportQtiServiceImpl.java 8562 2014-08-30 06:07:15Z mallikamt $
 ***********************************************************************************
 *
 * Copyright (c) 2013, 2014 Etudes, Inc.
 *
 * Licensed under the Apache 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.apache.org/licenses/LICENSE-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.
 *
 **********************************************************************************/

package org.etudes.mneme.impl;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.etudes.mneme.api.Assessment;
import org.etudes.mneme.api.AssessmentParts;
import org.etudes.mneme.api.AssessmentPermissionException;
import org.etudes.mneme.api.AssessmentService;
import org.etudes.mneme.api.ExportQtiService;
import org.etudes.mneme.api.Part;
import org.etudes.mneme.api.PartDetail;
import org.etudes.mneme.api.Pool;
import org.etudes.mneme.api.Question;
import org.etudes.mneme.api.QuestionService;
import org.etudes.mneme.impl.LikertScaleQuestionImpl.LikertScaleQuestionChoice;
import org.etudes.mneme.impl.MatchQuestionImpl.MatchQuestionPair;
import org.etudes.mneme.impl.MultipleChoiceQuestionImpl.MultipleChoiceQuestionChoice;
import org.sakaiproject.authz.api.SecurityAdvisor;
import org.sakaiproject.content.api.ContentHostingService;
import org.sakaiproject.content.api.ContentResource;
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.i18n.InternationalizedMessages;
import org.sakaiproject.id.cover.IdManager;
import org.sakaiproject.util.FormattedText;
import org.sakaiproject.util.ResourceLoader;
import org.sakaiproject.util.Validator;
import org.sakaiproject.util.Xml;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * <p>
 * ImportQtiServiceImpl implements ImportQtiService
 * </p>
 */
public class ExportQtiServiceImpl implements ExportQtiService {
    /** Our logger. */
    private static Log M_log = LogFactory.getLog(ExportQtiServiceImpl.class);

    /** Dependency: AssessmentService */
    protected AssessmentService assessmentService = null;

    /** Messages bundle name. */
    protected String bundle = null;

    /** Messages. */
    protected transient InternationalizedMessages messages = null;

    /** Dependency: QuestionService */
    protected QuestionService questionService = null;

    /** Dependency: SecurityService */
    protected org.sakaiproject.authz.api.SecurityService securityServiceSakai = null;

    protected ContentHostingService contentHostingService = null;

    private class PoolDetails {
        String pool_id;
        String pool_title;

        public PoolDetails(String pool_id, String pool_title) {
            this.pool_id = pool_id;
            this.pool_title = pool_title;
        }

        public String getPool_id() {
            return pool_id;
        }

        public String getPool_title() {
            return pool_title;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + getOuterType().hashCode();
            result = prime * result + ((pool_id == null) ? 0 : pool_id.hashCode());
            result = prime * result + ((pool_title == null) ? 0 : pool_title.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            PoolDetails other = (PoolDetails) obj;
            if (!getOuterType().equals(other.getOuterType()))
                return false;
            if (pool_id == null) {
                if (other.pool_id != null)
                    return false;
            } else if (!pool_id.equals(other.pool_id))
                return false;
            if (pool_title == null) {
                if (other.pool_title != null)
                    return false;
            } else if (!pool_title.equals(other.pool_title))
                return false;
            return true;
        }

        private ExportQtiServiceImpl getOuterType() {
            return ExportQtiServiceImpl.this;
        }

    }

    /**
     * Returns to uninitialized state.
     */
    public void destroy() {
        M_log.info("destroy()");
    }

    /**
     * Get the random id
     * 
     * @return
     */
    String getUUID() {
        return IdManager.createUuid();
    }

    /**
     * Create Manifest file root element
     * 
     * @param doc
     * @return
     */
    public Element createManifest(Document doc) {
        Element root = doc.createElementNS("http://www.imsglobal.org/xsd/imscp_v1p1", "manifest");
        root.setAttribute("xmlns:imsqti", "http://www.imsglobal.org/xsd/imsqti_v2p1");
        root.setAttribute("xmlns:imsmd", "http://www.imsglobal.org/xsd/imsmd_v1p2");
        //      root.setAttribute("xmlns", "http://www.imsglobal.org/xsd/imscp_v1p1");
        root.setAttribute("identifier", "Manifest-" + getUUID());
        return root;
    }

    /**
     * Creates the Metadata element for the manifest file which mainly includes the title of the pool
     * 
     * @param doc
     * @return
     */
    public Element createManifestMetadata(Document doc, String title) {
        Element metadata = doc.createElementNS("http://www.imsglobal.org/xsd/imscp_v1p1", "metadata");

        // schema element
        Element schema = doc.createElementNS("http://www.imsglobal.org/xsd/imscp_v1p1", "schema");

        schema.setTextContent("IMS Content");
        metadata.appendChild(schema);

        // schema version element
        Element schemaVersion = doc.createElementNS("http://www.imsglobal.org/xsd/imscp_v1p1", "schemaversion");
        schemaVersion.setTextContent("2.1");
        metadata.appendChild(schemaVersion);
        Element lom = createLomElement(doc, null, title, null, null, "", null);
        metadata.appendChild(lom);

        return metadata;
    }

    /**
     * Create resource element for each pool and its corresponding file element. resource element also lists all embedded data files. <resource identifier="RES-B38DF83F-A291-86DA-4EC3-B2CEBD1515A4" type="imsqti_test_xmlv2p1" href="choice.xml">
     * 
     * @param doc
     * @param count
     * @param poolFile
     * @return
     */
    public Element createResourceElementforAssessment(ZipOutputStream zip, Document doc, int count,
            Assessment test) {
        String titleFile = test.getId() + "/assessment" + count + ".xml";

        Element resource = doc.createElement("resource");
        resource.setAttribute("identifier", "Resource" + count);
        resource.setAttribute("type", "imsqti_test_xmlv2p1");
        resource.setAttribute("href", titleFile);

        ArrayList<String> mediaFiles = new ArrayList<String>();
        Element metadata = doc.createElement("metadata");
        String subFolder = test.getId() + "/Resources/";
        metadata.appendChild(createLomElement(doc, test.getType().toString(), test.getTitle(),
                (test.getPresentation() != null) ? test.getPresentation().getText() : null, zip, subFolder,
                mediaFiles));

        resource.appendChild(metadata);

        Element file = doc.createElement("file");
        file.setAttribute("href", titleFile);
        resource.appendChild(file);

        if (mediaFiles.size() > 0) {
            for (String m : mediaFiles) {
                Element mediafile = doc.createElement("file");
                mediafile.setAttribute("href", m);
                resource.appendChild(mediafile);
            }
        }

        return resource;
    }

    /**
     * Creates ims manifest resource element for a question.
     * 
     * @param doc
     *        Manifest Document
     * @param question
     *        Question
     * @return resource Element
     */
    public Element createResourceElementforQuestion(Document doc, String testId, Question question,
            List<String> embedMedia) {
        String titleFile = testId + "/question" + question.getId() + ".xml";

        Element resource = doc.createElement("resource");
        resource.setAttribute("identifier", question.getPool().getId() + ":" + question.getId());
        resource.setAttribute("type", "imsqti_item_xmlv2p1");
        resource.setAttribute("href", titleFile);

        Element metadata = doc.createElement("metadata");

        // if question is a survey add imsmd:identifier similar to test type
        Boolean survey = question.getIsSurvey();
        if (Boolean.TRUE.equals(survey))
            metadata.appendChild(createLomElement(doc, "survey", "", null, null, "", null));

        metadata.appendChild(createQTIMetadataElement(doc, question.getType()));
        resource.appendChild(metadata);

        Element file = doc.createElement("file");
        file.setAttribute("href", titleFile);
        resource.appendChild(file);

        // add embeds in resource element as file elements
        for (String media : embedMedia) {
            Element fileElement = doc.createElement("file");
            fileElement.setAttribute("href", media);
            resource.appendChild(fileElement);
        }
        return resource;
    }

    /**
     * 
     * @param doc
     * @param type
     * @param title
     * @param description
     * @param zip
     * @param mediaFiles
     * @return
     */
    public Element createLomElement(Document doc, String type, String title, String description,
            ZipOutputStream zip, String subFolder, List<String> mediaFiles) {
        Element lom = doc.createElementNS("http://www.imsglobal.org/xsd/imsmd_v1p2", "imsmd:lom");
        Element general = doc.createElementNS("http://www.imsglobal.org/xsd/imsmd_v1p2", "imsmd:general");

        // <imsmd:identifier>QUE_1106</imsmd:identifier>
        if (type != null && type.length() > 0) {
            Element typeElement = doc.createElementNS("http://www.imsglobal.org/xsd/imsmd_v1p2",
                    "imsmd:identifier");
            typeElement.setTextContent(type);
            general.appendChild(typeElement);
        }
        if (title != null && title.length() > 0) {
            Element titleElement = doc.createElementNS("http://www.imsglobal.org/xsd/imsmd_v1p2", "imsmd:title");
            Element langstring = doc.createElementNS("http://www.imsglobal.org/xsd/imsmd_v1p2", "imsmd:langstring");
            title = FormattedText.unEscapeHtml(title);
            langstring.setTextContent(title);
            titleElement.appendChild(langstring);
            general.appendChild(titleElement);
        }
        if (description != null && description.length() > 0) {
            Element descElement = doc.createElementNS("http://www.imsglobal.org/xsd/imsmd_v1p2",
                    "imsmd:description");
            Element langstring = doc.createElementNS("http://www.imsglobal.org/xsd/imsmd_v1p2", "imsmd:langstring");
            description = FormattedText.unEscapeHtml(description);
            if (zip != null && mediaFiles != null)
                description = translateEmbedData(zip, subFolder, subFolder, description, mediaFiles);
            langstring.setTextContent(description);
            descElement.appendChild(langstring);
            general.appendChild(descElement);
        }
        lom.appendChild(general);
        return lom;
    }

    /**
     * create outcomeDeclaration element which contains the score
     * 
     * @param document
     * @param points
     * @return
     */
    private Element createOutcomeElement(Document document, String identifier, String baseType, String points) {
        Element outcomeDeclarationElement = document.createElement("outcomeDeclaration");
        outcomeDeclarationElement.setAttribute("baseType", baseType);
        outcomeDeclarationElement.setAttribute("cardinality", "single");
        outcomeDeclarationElement.setAttribute("identifier", identifier);
        if (points != null && points.length() > 0) {
            Element defaultValueElement = document.createElement("defaultValue");
            Element valueElement = document.createElement("value");
            valueElement.setTextContent(points);
            defaultValueElement.appendChild(valueElement);
            outcomeDeclarationElement.appendChild(defaultValueElement);
        }
        return outcomeDeclarationElement;
    }

    /**
     * Creates response declaration element.
     * 
     * @param questionDocument
     * @param indentifier
     * @param cardinality
     * @param basetype
     * @return
     */
    private Element createResponseDeclaration(Document questionDocument, String indentifier, String cardinality,
            String basetype) {
        Element responseDeclaration = questionDocument.createElement("responseDeclaration");
        responseDeclaration.setAttribute("identifier", indentifier);
        responseDeclaration.setAttribute("cardinality", cardinality);
        responseDeclaration.setAttribute("baseType", basetype);
        return responseDeclaration;
    }

    /**
     * 
     * @param assessmentTestDocument
     * @param tries
     * @param reviewOptions
     * @param showHints
     * @param showModelAnswer
     * @return
     */
    private Element createItemSessionElement(Document assessmentTestDocument, String tries, String reviewOptions,
            String showHints, boolean showModelAnswer) {
        // <itemSessionControl maxAttempts="0"/>
        Element itemSessionControlElement = assessmentTestDocument.createElement("itemSessionControl");
        if (!tries.equals(""))
            itemSessionControlElement.setAttribute("maxAttempts", tries);

        // review options
        if (reviewOptions.equalsIgnoreCase("Never"))
            itemSessionControlElement.setAttribute("allowReview", "false");
        else
            itemSessionControlElement.setAttribute("allowReview", "true");

        // show hints
        itemSessionControlElement.setAttribute("showFeedback", showHints);

        // show model answer
        if (showModelAnswer)
            itemSessionControlElement.setAttribute("showSolution", "true");

        return itemSessionControlElement;
    }

    /**
     * 
     * @param assessmentTestDocument
     * @param partTitle
     * @param partsCount
     * @param partRandomize
     * @return
     */
    private Element createSectionElement(Document assessmentTestDocument, String partTitle, int partsCount,
            boolean partRandomize) {
        String sectionTitle = (partTitle != null && partTitle.length() > 0) ? partTitle : "";
        Element assessmentSectionElement = assessmentTestDocument.createElement("assessmentSection");
        assessmentSectionElement.setAttribute("identifier", "Section" + partsCount);
        assessmentSectionElement.setAttribute("title", sectionTitle);
        assessmentSectionElement.setAttribute("visible", "true");

        // randomize <ordering shuffle="true"/>
        if (partRandomize) {
            Element orderingElement = assessmentTestDocument.createElement("ordering");
            orderingElement.setAttribute("shuffle", "true");
            assessmentSectionElement.appendChild(orderingElement);
        }
        return assessmentSectionElement;
    }

    /**
     * create final message element.
     * 
     * @param assessmentTestDocument
     * @param text
     * @return
     */
    private Element createFinalFeedbackElement(Document assessmentTestDocument, String text) {
        Element testFeedbackElement = assessmentTestDocument.createElement("testFeedback");
        testFeedbackElement.setAttribute("access", "atEnd");
        testFeedbackElement.setAttribute("showHide", "hide");
        testFeedbackElement.setAttribute("identifier", "FB_Total");

        Element feedbackContentElement = assessmentTestDocument.createElement("div");
        feedbackContentElement.appendChild(assessmentTestDocument.createCDATASection(text));
        testFeedbackElement.appendChild(feedbackContentElement);
        return testFeedbackElement;
    }

    /**
     * Create QTI meta data element
     * 
     * @param doc
     * @param questionType
     * @return
     */
    private Element createQTIMetadataElement(Document doc, String questionType) {
        Element qtiMetaData = doc.createElementNS("http://www.imsglobal.org/xsd/imsqti_v2p1", "imsqti:qtiMetadata");

        Element itemTemplate = doc.createElementNS("http://www.imsglobal.org/xsd/imsqti_v2p1",
                "imsqti:itemTemplate");
        itemTemplate.setTextContent("false");
        qtiMetaData.appendChild(itemTemplate);

        Element composite = doc.createElementNS("http://www.imsglobal.org/xsd/imsqti_v2p1", "imsqti:composite");
        composite.setTextContent("false");
        qtiMetaData.appendChild(composite);

        Element interactionType = doc.createElementNS("http://www.imsglobal.org/xsd/imsqti_v2p1",
                "imsqti:interactionType");
        String interaction = "";
        if (("mneme:MultipleChoice").equals(questionType) || ("mneme:TrueFalse").equals(questionType)
                || ("mneme:LikertScale").equals(questionType))
            interaction = "choiceInteraction";
        if (("mneme:Essay").equals(questionType))
            interaction = "extendedTextInteraction";
        if (("mneme:Match").equals(questionType))
            interaction = "matchInteraction";
        if (("mneme:FillBlanks").equals(questionType))
            interaction = "textEntryInteraction";

        interactionType.setTextContent(interaction);
        qtiMetaData.appendChild(interactionType);

        Element feedbackType = doc.createElementNS("http://www.imsglobal.org/xsd/imsqti_v2p1",
                "imsqti:feedbackType");
        feedbackType.setTextContent("none");
        qtiMetaData.appendChild(feedbackType);

        return qtiMetaData;
    }

    /**
     * {@inheritDoc}
     */
    public void exportAssessments(String context, String[] ids, ZipOutputStream zip)
            throws AssessmentPermissionException, IOException {
        // create manifest element
        Document doc = Xml.createDocument();
        Element manifestRoot = createManifest(doc);
        doc.appendChild(manifestRoot);
        String poolTitle = "";
        ArrayList<PoolDetails> pools = new ArrayList<PoolDetails>();

        // create organization element
        if (ids == null || ids.length == 0)
            return;
        Element organizations = doc.createElement("organizations");
        manifestRoot.appendChild(organizations);

        // create resources parent element
        Element resources = doc.createElement("resources");
        manifestRoot.appendChild(resources);

        int count = 0;
        for (String pId : ids) {
            Assessment test = this.assessmentService.getAssessment(pId);
            if (test == null)
                continue;

            // 2. add the resource element entry in manifest file
            Element resourceAssessment = createResourceElementforAssessment(zip, doc, ++count, test);

            // 3. create assessmentTest document
            String resourceAssessmentIdent = resourceAssessment.getAttribute("identifier");
            String assessmentFileName = resourceAssessment.getAttribute("href");
            HashMap<String, List<Element>> miscellaneousItems = createAssessmentDocument(context, test, doc,
                    resourceAssessmentIdent, assessmentFileName, zip, pools);

            List<Element> items = (miscellaneousItems.containsKey("questionItems"))
                    ? miscellaneousItems.get("questionItems")
                    : null;
            List<String> questionIds = new ArrayList<String>();
            // 4. add dependency to test element and question as a resource
            // <dependency identifierref="RES-BCA84FC0-53F9-ABBD-C3FE-BDB5B825CA9E"/>
            if (items != null) {
                for (Element item : items) {
                    Element dependecyElement = doc.createElement("dependency");
                    dependecyElement.setAttribute("identifierref", item.getAttribute("identifier"));
                    questionIds.add(item.getAttribute("identifier"));
                    resourceAssessment.appendChild(dependecyElement);
                    resources.appendChild(item);
                }
            }
            resources.appendChild(resourceAssessment);
        }

        // write pools information
        for (PoolDetails p : pools) {
            poolTitle = poolTitle.concat(" " + p.getPool_title());
            Element poolElement = doc.createElement("resource");
            poolElement.setAttribute("title", p.getPool_title());
            poolElement.setAttribute("identifier", "POOL" + p.getPool_id());
            resources.appendChild(poolElement);
        }

        // write manifest file
        if ("".equals(poolTitle))
            poolTitle = "defaultPool";
        manifestRoot.appendChild(createManifestMetadata(doc, poolTitle));
        manifestRoot.appendChild(resources);
        writeDocumentToZip(zip, null, "imsmanifest.xml", doc);

    }

    /**
     * 
     * @param details
     * @param assessmentTestDocument
     * @param assessmentSectionElement
     * @param drawPools
     * @param count
     * @return
     */
    private Element findDrawPools(List<PartDetail> details, Document assessmentTestDocument,
            Element assessmentSectionElement, List<PartDetail> drawPools, Integer count) {
        boolean randomDraw = false;
        for (PartDetail detail : details) {
            if (detail.getType().contains("Draw")) {
                randomDraw = true;
                drawPools.add(detail);
            }
        }

        if (randomDraw) {
            // <selection select="2"/>
            Element selection = assessmentTestDocument.createElement("selection");
            selection.setAttribute("select", count.toString());
            assessmentSectionElement.appendChild(selection);
        }
        return assessmentSectionElement;
    }

    /**
     * Creates the assessment.xml file and writes to the zip package
     * 
     * @param test
     * @param resourceAssessmentIdent
     * @param assessmentFileName
     * @param zip
     * @return
     */
    public HashMap<String, List<Element>> createAssessmentDocument(String context, Assessment test, Document doc,
            String resourceAssessmentIdent, String assessmentFileName, ZipOutputStream zip,
            ArrayList<PoolDetails> pools) {
        Document assessmentTestDocument = Xml.createDocument();
        Element assessmentTestElement = assessmentTestDocument.createElement("assessmentTest");

        assessmentTestElement.setAttribute("xmlns", "http://www.imsglobal.org/xsd/imsqti_v2p1");
        HashMap<String, List<Element>> miscellaneousItems = new HashMap<String, List<Element>>();
        HashMap<String, Element> questionsList = new HashMap<String, Element>();

        assessmentTestElement.setAttribute("identifier", resourceAssessmentIdent);
        assessmentTestElement.setAttribute("title", test.getTitle());
        float pointsValue = 0;
        String points = "10";
        String tries = "";
        String navigationMode = (test.getRandomAccess()) ? "nonlinear" : "linear";
        String submissionMode = "simultaneous";
        String reviewOptions = "Never";
        String showHints = "false";
        boolean randomDraw = false;

        if (test.getHasTriesLimit())
            tries = test.getTries().toString();
        if (test.getReview() != null && test.getReview().getTiming() != null)
            reviewOptions = test.getReview().getTiming().toString();
        if (test.getShowHints() != null)
            showHints = test.getShowHints().toString();

        // <timeLimits maxTime="" allowLateSubmission="0 or 1"/>
        if (test.getHasTimeLimit()) {
            Element limitElement = assessmentTestDocument.createElement("timeLimits");
            limitElement.setAttribute("maxTime", test.getTimeLimit().toString());
            limitElement.setAttribute("allowLateSubmission", "0");
            assessmentTestElement.appendChild(limitElement);
        }

        // create outcomeDeclaration
        Element outcomeDeclarationElement = createOutcomeElement(assessmentTestDocument, "SCORE", "float", points);
        if (outcomeDeclarationElement != null)
            assessmentTestElement.appendChild(outcomeDeclarationElement);

        assessmentTestElement = createOtherSettingsOutcomeElements(assessmentTestDocument, assessmentTestElement,
                test);

        // testPart
        AssessmentParts assessmentParts = test.getParts();

        // part numbering
        if (!assessmentParts.getContinuousNumbering()) {
            Element outcomeDeclarationPartNumberingElement = createOutcomeElement(assessmentTestDocument,
                    "PartNumbering", "string", assessmentParts.getContinuousNumbering().toString());
            if (outcomeDeclarationPartNumberingElement != null)
                assessmentTestElement.appendChild(outcomeDeclarationPartNumberingElement);
        }

        List<Part> parts = assessmentParts.getParts();

        int partsCount = 1;

        for (Part p : parts) {
            //get the master pool and questions id. 
            for (Iterator<PartDetail> i = p.getDetails().iterator(); i.hasNext();) {
                PartDetail detail = i.next();
                if (!detail.restoreToOriginal(null, null)) {
                    i.remove();
                }
            }

            pointsValue = pointsValue + p.getTotalPoints();
            // <testPart identifier="part01" navigationMode="nonlinear" submissionMode="simultaneous">
            Element testPartElement = assessmentTestDocument.createElement("testPart");
            testPartElement.setAttribute("identifier", p.getId());
            testPartElement.setAttribute("navigationMode", navigationMode);
            testPartElement.setAttribute("submissionMode", submissionMode);

            Element itemSession = createItemSessionElement(assessmentTestDocument, tries, reviewOptions, showHints,
                    test.getShowModelAnswer());
            testPartElement.appendChild(itemSession);

            // <assessmentSection identifier="sectionA" title="Section A" visible="true">
            Element assessmentSectionElement = createSectionElement(assessmentTestDocument, p.getTitle(),
                    partsCount++, p.getRandomize());

            // rubricBlock
            Element rubricElement = assessmentTestDocument.createElement("rubricBlock");
            if (p.getPresentation() != null && p.getPresentation().getText() != null) {
                String partDescription = p.getPresentation().getText();
                ArrayList<String> mediaFiles = new ArrayList<String>();
                String subFolder = test.getId() + "/Resources/";
                if (zip != null && mediaFiles != null)
                    partDescription = translateEmbedData(zip, subFolder, "Resources/", partDescription, mediaFiles);
                rubricElement.setTextContent(partDescription);
            }
            assessmentSectionElement.appendChild(rubricElement);

            // create AssessmentItem document for the question
            int question_count = 1;
            List<Question> questions = p.getQuestions();
            List<PartDetail> details = p.getDetails();
            List<PartDetail> drawPools = new ArrayList<PartDetail>();

            assessmentSectionElement = findDrawPools(details, assessmentTestDocument, assessmentSectionElement,
                    drawPools, p.getNumQuestions());
            if (drawPools.size() > 0)
                randomDraw = true;

            for (Question question : questions) {
                try {
                    String questionPoolId = question.getPool().getId();
                    PoolDetails questionPool = new PoolDetails(questionPoolId, question.getPool().getTitle());
                    if (!pools.contains(questionPool))
                        pools.add(questionPool);

                    // create question.xml file 
                    ArrayList<String> mediaFiles = new ArrayList<String>();
                    Element assessmentItem = createAssessmentItemforQuestion(zip, context, test.getId(), mediaFiles,
                            question, question.getPartDetail().getQuestionPoints(), question_count++);
                    if (assessmentItem == null)
                        continue;
                    Element itemResourceElement = createResourceElementforQuestion(doc, test.getId(), question,
                            mediaFiles);
                    itemResourceElement = getAttachments(zip, test.getId(), doc, itemResourceElement, question);
                    questionsList.put(question.getId(), itemResourceElement);

                    // add assessmentItemRef -- <assessmentItemRef identifier="item034" href="adaptive.xml">
                    Element assessmentItemRefElement = assessmentTestDocument.createElement("assessmentItemRef");
                    assessmentItemRefElement.setAttribute("identifier", questionPoolId + ":" + question.getId());
                    assessmentItemRefElement.setAttribute("href", "question" + question.getId() + ".xml");

                    if (randomDraw && !question.getPartDetail().getType().contains("Draw"))
                        assessmentItemRefElement.setAttribute("required", "true");
                    else if (randomDraw && question.getPartDetail().getType().contains("Draw"))
                        assessmentItemRefElement.setAttribute("required", "false");

                    assessmentSectionElement.appendChild(assessmentItemRefElement);
                } catch (Exception e) {
                    M_log.debug("Export qti2:" + e.getMessage());
                }
            } // questions for

            // if random add other questions from the pool to draw from
            for (PartDetail pd : drawPools) {
                Float pdQuestionPoints = pd.getEffectivePoints() / pd.getNumQuestions();

                List<Element> otherResources = addOtherPoolQuestions(doc, assessmentTestDocument, questionsList,
                        pd.getPool(), zip, context, test.getId(), pdQuestionPoints, question_count++);
                for (Element r : otherResources)
                    assessmentSectionElement.appendChild(r);
                question_count = question_count + otherResources.size();
            }

            ArrayList<Element> items = new ArrayList<Element>();
            for (Element e : questionsList.values())
                items.add(e);

            miscellaneousItems.put("questionItems", items);
            testPartElement.appendChild(assessmentSectionElement);
            assessmentTestElement.appendChild(testPartElement);
        } // parts end

        if (pointsValue > 0) {
            NodeList value = outcomeDeclarationElement.getElementsByTagName("value");
            Element valueElement = (Element) value.item(0);
            valueElement.setTextContent(new Float(pointsValue).toString());
        }

        // outcomeProcessing
        Element outcomeProcessingElement = assessmentTestDocument.createElement("outcomeProcessing");
        assessmentTestElement.appendChild(outcomeProcessingElement);

        // Final Message
        if (test.getSubmitPresentation() != null && test.getSubmitPresentation().getText() != null) {
            Element testFeedbackElement = createFinalFeedbackElement(assessmentTestDocument,
                    test.getSubmitPresentation().getText());
            assessmentTestElement.appendChild(testFeedbackElement);
        }

        assessmentTestDocument.appendChild(assessmentTestElement);
        // 4. write assessment.xml to the zip file
        writeDocumentToZip(zip, test.getId() + "/", assessmentFileName, assessmentTestDocument);

        return miscellaneousItems;
    }

    /**
     * Creates the question.xml file and writes to the zip package
     * 
     * @param zip
     * @param question
     * @param count
     * @return
     * @throws Exception
     */
    public Element createAssessmentItemforQuestion(ZipOutputStream zip, String context, String testId,
            List<String> mediaFiles, Question question, float points, int count) throws Exception {
        if (question == null)
            return null;

        String fileTitle = testId + "/question" + question.getId() + ".xml";
        String title = "question" + count;

        Document assessmentItemDocument = Xml.createDocument();
        Element item = assessmentItemDocument.createElement("assessmentItem");
        item.setAttribute("xmlns", "http://www.imsglobal.org/xsd/imsqti_v2p1");
        item.setAttribute("identifier", question.getPool().getId() + ":" + question.getId());
        item.setAttribute("title", title);
        item.setAttribute("adaptive", "false");
        item.setAttribute("timeDependent", "false");

        Map<String, Element> map_question = getallQuestionElements(zip, assessmentItemDocument, testId, question,
                context, mediaFiles);

        // <responseDeclaration identifier="RESPONSE" cardinality="single" baseType="identifier">
        if (map_question.containsKey("responseDeclarationCount")) {
            // fill blanks have multiple response declare
            int responseCount = Integer.parseInt(map_question.get("responseDeclarationCount").getTextContent());
            for (int i = 1; i < responseCount; i++) {
                if (map_question.containsKey("responseDeclaration" + i))
                    item.appendChild(map_question.get("responseDeclaration" + i));
            }
        } else if (map_question.containsKey("responseDeclaration"))
            item.appendChild(map_question.get("responseDeclaration"));

        // create outcomeDeclaration
        item.appendChild(
                createOutcomeElement(assessmentItemDocument, "SCORE", "float", new Float(points).toString()));

        // <itemBody>
        if (map_question.containsKey("itemBody"))
            item.appendChild(map_question.get("itemBody"));

        // responseProcessing
        if (map_question.containsKey("responseProcessing"))
            item.appendChild(map_question.get("responseProcessing"));

        // modal feedback
        if (map_question.containsKey("modalFeedback"))
            item.appendChild(map_question.get("modalFeedback"));

        assessmentItemDocument.appendChild(item);

        writeDocumentToZip(zip, null, fileTitle, assessmentItemDocument);

        return item;
    }

    /**
     * create outcome declarations for other settings
     * @param assessmentTestDocument
     * @param test
     * @return
     */
    private Element createOtherSettingsOutcomeElements(Document assessmentTestDocument,
            Element assessmentTestElement, Assessment test) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

        // pass percentage
        if (test.getMinScoreSet()) {
            Element outcomeDeclarationPassedElement = createOutcomeElement(assessmentTestDocument, "PASSFACTOR",
                    "float", test.getMinScore().toString());
            if (outcomeDeclarationPassedElement != null)
                assessmentTestElement.appendChild(outcomeDeclarationPassedElement);
        }

        //review show summary
        if (test.getReview().getShowSummary()) {
            Element outcomeDeclarationShowSummaryElement = createOutcomeElement(assessmentTestDocument,
                    "ReviewShowSummary", "string", test.getReview().getShowSummary().toString());
            if (outcomeDeclarationShowSummaryElement != null)
                assessmentTestElement.appendChild(outcomeDeclarationShowSummaryElement);
        }

        //review show feedback
        if (test.getReview().getShowFeedback()) {
            Element outcomeDeclarationShowFeedbackElement = createOutcomeElement(assessmentTestDocument,
                    "ReviewShowFeedback", "string", test.getReview().getShowFeedback().toString());
            if (outcomeDeclarationShowFeedbackElement != null)
                assessmentTestElement.appendChild(outcomeDeclarationShowFeedbackElement);
        }

        // show correct answer
        Element outcomeDeclarationShowAnswerElement = createOutcomeElement(assessmentTestDocument,
                "ReviewCorrectAnswer", "string", test.getReview().getShowCorrectAnswer().toString());
        if (outcomeDeclarationShowAnswerElement != null)
            assessmentTestElement.appendChild(outcomeDeclarationShowAnswerElement);

        // anonymous grading
        if (test.getAnonymous()) {
            Element outcomeDeclarationAnonymousElement = createOutcomeElement(assessmentTestDocument,
                    "AnonymousGrading", "string", test.getAnonymous().toString());
            if (outcomeDeclarationAnonymousElement != null)
                assessmentTestElement.appendChild(outcomeDeclarationAnonymousElement);
        }

        // question grouping
        Element outcomeDeclarationGroupingElement = createOutcomeElement(assessmentTestDocument, "QuestionLayout",
                "string", test.getQuestionGrouping().toString());
        if (outcomeDeclarationGroupingElement != null)
            assessmentTestElement.appendChild(outcomeDeclarationGroupingElement);

        // honor pledge
        if (test.getRequireHonorPledge()) {
            Element outcomeDeclarationPledgeElement = createOutcomeElement(assessmentTestDocument, "HonorPledge",
                    "string", test.getRequireHonorPledge().toString());
            if (outcomeDeclarationPledgeElement != null)
                assessmentTestElement.appendChild(outcomeDeclarationPledgeElement);
        }

        // shuffle choices for all MCs
        if (test.getShuffleChoicesOverride()) {
            Element outcomeDeclarationShuffleElement = createOutcomeElement(assessmentTestDocument,
                    "ShuffleChoicesOverride", "string", test.getShuffleChoicesOverride().toString());
            if (outcomeDeclarationShuffleElement != null)
                assessmentTestElement.appendChild(outcomeDeclarationShuffleElement);
        }

        // dates
        if (test.getDates() != null) {
            if (test.getDates().getOpenDate() != null) {
                Date open = test.getDates().getOpenDate();

                Element outcomeDeclarationOpenElement = createOutcomeElement(assessmentTestDocument, "OpenDate",
                        "string", sdf.format(open));
                if (outcomeDeclarationOpenElement != null)
                    assessmentTestElement.appendChild(outcomeDeclarationOpenElement);
            }

            if (test.getDates().getHideUntilOpen() != null) {
                Element outcomeDeclarationHideOpenElement = createOutcomeElement(assessmentTestDocument,
                        "HideUntilOpen", "string", test.getDates().getHideUntilOpen().toString());
                if (outcomeDeclarationHideOpenElement != null)
                    assessmentTestElement.appendChild(outcomeDeclarationHideOpenElement);
            }

            if (test.getDates().getDueDate() != null) {
                Date due = test.getDates().getDueDate();

                Element outcomeDeclarationDueElement = createOutcomeElement(assessmentTestDocument, "DueDate",
                        "string", sdf.format(due));
                if (outcomeDeclarationDueElement != null)
                    assessmentTestElement.appendChild(outcomeDeclarationDueElement);
            }

            if (test.getDates().getAcceptUntilDate() != null) {
                Date acceptUntil = test.getDates().getAcceptUntilDate();

                Element outcomeDeclarationSubmitUntilElement = createOutcomeElement(assessmentTestDocument,
                        "AcceptUntil", "string", sdf.format(acceptUntil));
                if (outcomeDeclarationSubmitUntilElement != null)
                    assessmentTestElement.appendChild(outcomeDeclarationSubmitUntilElement);
            }
        }

        return assessmentTestElement;
    }

    /**
     * 
     * @param resourceDocument
     * @param assessmentDocument
     * @param questionsList
     * @param pool
     * @return 
     *    List of AssessmentItemRef elements
     */
    private List<Element> addOtherPoolQuestions(Document resourceDocument, Document assessmentDocument,
            HashMap<String, Element> questionsList, Pool pool, ZipOutputStream zip, String context, String testId,
            float detailPoints, int questionCount) {
        // get all pool questions
        List<String> poolQuestions = pool.getAllQuestionIds(null, true);
        List<Element> otherRandomQuestions = new ArrayList<Element>();

        for (String chkId : poolQuestions) {
            try {
                // if not in questionsList
                if (questionsList.containsKey(chkId))
                    continue;

                // create question.xml file
                Question question = questionService.getQuestion(chkId);

                ArrayList<String> mediaFiles = new ArrayList<String>();
                Element assessmentItem = createAssessmentItemforQuestion(zip, context, testId, mediaFiles, question,
                        detailPoints, questionCount++);
                if (assessmentItem == null)
                    continue;

                // add to resourceDocument
                Element itemResourceElement = createResourceElementforQuestion(resourceDocument, testId, question,
                        mediaFiles);
                itemResourceElement = getAttachments(zip, testId, resourceDocument, itemResourceElement, question);
                questionsList.put(question.getId(), itemResourceElement);

                // add to assessmentDocument and to arraylist
                Element assessmentItemRefElement = assessmentDocument.createElement("assessmentItemRef");
                assessmentItemRefElement.setAttribute("identifier", pool.getId() + ":" + question.getId());
                assessmentItemRefElement.setAttribute("href", "question" + question.getId() + ".xml");
                otherRandomQuestions.add(assessmentItemRefElement);

            } catch (Exception e) {
            }
        }

        return otherRandomQuestions;
    }

    /**
     * 
     * @param zip
     * @param questionDocument
     * @param questionResourceElement
     * @param question
     * @return
     */
    public Element getAttachments(ZipOutputStream zip, String testId, Document document,
            Element questionResourceElement, Question question) {
        List<Reference> attachments = question.getPresentation().getAttachments();

        if (attachments != null && attachments.size() > 0) {
            // security advisor
            pushAdvisor();

            String subFolder = testId + "/Resources/";
            for (Reference attachment : attachments) {
                try {
                    // get a usable file name for the attachment               
                    String fileName = Validator.getFileName(attachment.getId().replaceAll("%20", " "));
                    fileName = fileName.replaceAll("%20", " ");
                    fileName = Validator.escapeResourceName(fileName);

                    writeContentResourceToZip(zip, subFolder, attachment.getId(), fileName);

                    Element file = document.createElement("file");
                    file.setAttribute("href", subFolder + fileName);
                    questionResourceElement.appendChild(file);
                } catch (Exception e) {
                    M_log.warn("ExportQtiService: zipping attachments: " + e.toString());
                }
            }
            popAdvisor();
        }
        return questionResourceElement;
    }

    /**
     * 
     * @param poolDocument
     * @param question
     * @param text
     * @return
     */
    public Map<String, Element> getallQuestionElements(ZipOutputStream zip, Document questionDocument,
            String testId, Question question, String context, List<String> mediaFiles) throws Exception {
        Map<String, Element> questionParts = new HashMap<String, Element>();

        String text = question.getPresentation().getText();
        Element itemBody = questionDocument.createElement("itemBody");

        // for fill blanks text will be chopped
        if (!"mneme:FillBlanks".equals(question.getType())) {
            if (text == null)
                return questionParts;
            // process embed media and change path as Resources/xxxx.jpg

            // security advisor
            pushAdvisor();
            itemBody = translateEmbedData(zip, testId + "/Resources/", text, itemBody, mediaFiles,
                    questionDocument);

            popAdvisor();

            if (mediaFiles.isEmpty()) {
                Element div = questionDocument.createElement("div");
                div.setTextContent(text);
                itemBody.appendChild(div);
            }
        }

        // split the text based on <br/>
        if ("mneme:FillBlanks".equals(question.getType())) {
            FillBlanksQuestionImpl f = (FillBlanksQuestionImpl) (question.getTypeSpecificQuestion());
            text = f.getText();
            itemBody = getFillBlanksResponseChoices(questionDocument, itemBody, f, questionParts);
        } else if ("mneme:TrueFalse".equals(question.getType())) {
            getTFResponseChoices(questionDocument, question, questionParts);
            if (questionParts.containsKey("choiceInteraction"))
                itemBody.appendChild(questionParts.get("choiceInteraction"));
        } else if ("mneme:MultipleChoice".equals(question.getType())) {
            getMCResponseChoices(questionDocument, question, questionParts);
            if (questionParts.containsKey("choiceInteraction"))
                itemBody.appendChild(questionParts.get("choiceInteraction"));
        } else if ("mneme:Match".equals(question.getType())) {
            getMatchResponseChoices(questionDocument, question, questionParts);
            if (questionParts.containsKey("matchInteraction"))
                itemBody.appendChild(questionParts.get("matchInteraction"));
        } else if ("mneme:Essay".equals(question.getType())) {
            getEssayResponseChoices(questionDocument, question, questionParts);
            if (questionParts.containsKey("extendedTextInteraction"))
                itemBody.appendChild(questionParts.get("extendedTextInteraction"));
            if (questionParts.containsKey("uploadInteraction"))
                itemBody.appendChild(questionParts.get("uploadInteraction"));
        } else if ("mneme:Task".equals(question.getType())) {
            // no model answer and no submission type and no basetype
            Element responseDeclaration = questionDocument.createElement("responseDeclaration");
            responseDeclaration.setAttribute("identifier", "RESPONSE");
            responseDeclaration.setAttribute("cardinality", "single");
            questionParts.put("responseDeclaration", responseDeclaration);
        } else if ("mneme:LikertScale".equals(question.getType())) {
            // <itemBody class="likert">
            itemBody.setAttribute("class", "likert");
            getLikertScaleResponseChoices(questionDocument, question, questionParts);
            if (questionParts.containsKey("choiceInteraction"))
                itemBody.appendChild(questionParts.get("choiceInteraction"));
        }

        // Hints are part of item body
        if (question.getHints() != null && question.getHints().length() != 0) {
            Element feedbackInlineElement = questionDocument.createElement("feedbackInline");
            feedbackInlineElement.setAttribute("showHide", "hide");
            feedbackInlineElement.setAttribute("identifier", "FB_Hints");
            feedbackInlineElement.appendChild(questionDocument.createCDATASection(question.getHints()));
            itemBody.appendChild(feedbackInlineElement);
        }

        // question feedback
        if (question.getFeedback() != null && question.getFeedback().length() != 0) {
            Element feedbackElement = questionDocument.createElement("modalFeedback");
            feedbackElement.setAttribute("showHide", "hide");
            feedbackElement.setAttribute("identifier", "FB_Question");
            feedbackElement.appendChild(questionDocument.createCDATASection(question.getFeedback()));
            questionParts.put("modalFeedback", feedbackElement);
        }
        questionParts.put("itemBody", itemBody);
        return questionParts;
    }

    /**
     * 
     * @param questionDocument
     * @param question
     * @param questionParts
     */
    public void getEssayResponseChoices(Document questionDocument, Question question,
            Map<String, Element> questionParts) {
        if (question == null)
            return;

        EssayQuestionImpl essay = (EssayQuestionImpl) (question.getTypeSpecificQuestion());
        String basetype = ("attachments".equals(essay.getSubmissionType().toString())) ? "file" : "string";

        if ("string".equals(basetype)) {
            // <extendedTextInteraction responseIdentifier="RESPONSE" expectedLength="200">
            Element interaction = questionDocument.createElement("extendedTextInteraction");
            interaction.setAttribute("responseIdentifier", "RESPONSE");
            interaction.setAttribute("expectedLength", "200");
            questionParts.put("extendedTextInteraction", interaction);
        } else if ("file".equals(basetype)) {
            // <uploadInteraction responseIdentifier="RESPONSE">
            Element interaction = questionDocument.createElement("uploadInteraction");
            interaction.setAttribute("responseIdentifier", "RESPONSE");
            questionParts.put("uploadInteraction", interaction);
        }

        // <responseDeclaration identifier="RESPONSE" cardinality="single" baseType="string"/>
        Element responseDeclaration = createResponseDeclaration(questionDocument, "RESPONSE", "single", basetype);

        // Modal answer
        String answer = essay.getModelAnswer();
        Element correctResponse = questionDocument.createElement("correctResponse");
        responseDeclaration.appendChild(correctResponse);
        if (answer != null && answer.length() > 0) {
            answer = FormattedText.unEscapeHtml(answer);
            Element correctResponseValue = questionDocument.createElement("value");
            correctResponseValue.setTextContent(answer);
            correctResponse.appendChild(correctResponseValue);
        }

        questionParts.put("responseDeclaration", responseDeclaration);
        return;
    }

    /**
     * 
     * @param questionDocument
     * @param itemBody
     * @param question
     * @param questionParts
     * @return
     */
    public Element getFillBlanksResponseChoices(Document questionDocument, Element itemBody,
            FillBlanksQuestionImpl question, Map<String, Element> questionParts) {
        if (question == null)
            return itemBody;

        // itemBody
        String text = question.getText();

        Pattern p_fillBlanks_curly = Pattern.compile("([^{]*.?)(\\{)([^}]*.?)(\\})",
                Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE | Pattern.DOTALL);

        Matcher m_fillBlanks = p_fillBlanks_curly.matcher(text);
        StringBuffer sb = new StringBuffer();

        // in each part look for {} fill in the blank symbol and create render_fib tag
        int count = 1;
        while (m_fillBlanks.find()) {
            String fib = m_fillBlanks.group(1);
            Element textDiv = questionDocument.createElement("div");
            textDiv.setTextContent(fib);
            itemBody.appendChild(textDiv);

            String fib_curly = m_fillBlanks.group(2);
            Element fbInteraction = questionDocument.createElement("textEntryInteraction");
            fbInteraction.setAttribute("responseIdentifier", "RESPONSE" + (count++));
            fbInteraction.setAttribute("expectedLength", Integer.toString(fib_curly.length()));
            itemBody.appendChild(fbInteraction);
            m_fillBlanks.appendReplacement(sb, "");
        }
        m_fillBlanks.appendTail(sb);

        if (sb.length() > 0) {
            Element textDiv = questionDocument.createElement("div");
            textDiv.setTextContent(sb.toString());
            itemBody.appendChild(textDiv);
        }

        // answer
        List<String> correctAnswers = new ArrayList<String>();
        question.parseCorrectAnswers(correctAnswers);

        int responseCount = 1;
        for (String answer : correctAnswers) {
            Element responseDeclaration = createResponseDeclaration(questionDocument, "RESPONSE" + responseCount,
                    "single", "string");
            Element correctResponse = questionDocument.createElement("correctResponse");
            Element correctResponseValue = questionDocument.createElement("value");
            answer = FormattedText.unEscapeHtml(answer);
            correctResponseValue.setTextContent(answer);
            correctResponse.appendChild(correctResponseValue);
            responseDeclaration.appendChild(correctResponse);
            questionParts.put("responseDeclaration" + responseCount, responseDeclaration);
            responseCount++;
        }

        Element countDiv = questionDocument.createElement("div");
        countDiv.setTextContent(Integer.toString(responseCount));
        questionParts.put("responseDeclarationCount", countDiv);
        questionParts.put("itemBody", itemBody);
        return itemBody;
    }

    /**
     * 
     * @param questionDocument
     * @param question
     * @param questionParts
     */
    public void getLikertScaleResponseChoices(Document questionDocument, Question question,
            Map<String, Element> questionParts) {
        if (question == null)
            return;

        LikertScaleQuestionImpl likert = (LikertScaleQuestionImpl) (question.getTypeSpecificQuestion());
        List<LikertScaleQuestionChoice> choices = likert.getChoices();

        // <choiceInteraction responseIdentifier="RESPONSE" shuffle="false" maxChoices="1">
        Element choiceInteraction = questionDocument.createElement("choiceInteraction");
        choiceInteraction.setAttribute("responseIdentifier", "RESPONSE");
        choiceInteraction.setAttribute("shuffle", "false");
        choiceInteraction.setAttribute("maxChoices", "1");

        // <responseDeclaration identifier="RESPONSE" cardinality="single" baseType="identifier">
        Element responseDeclaration = createResponseDeclaration(questionDocument, "RESPONSE", "single",
                "identifier");

        // add scale as the correct answer
        Element correctResponse = questionDocument.createElement("correctResponse");
        Element value = questionDocument.createElement("value");
        value.setTextContent(likert.getScale());
        correctResponse.appendChild(value);
        responseDeclaration.appendChild(correctResponse);

        // response choices
        int count = 1;
        for (LikertScaleQuestionChoice c : choices) {
            // <simpleChoice identifier="ChoiceA">You must stay with your luggage at all times.</simpleChoice>
            Element simpleChoice = questionDocument.createElement("simpleChoice");
            simpleChoice.setAttribute("identifier", "Choice" + Integer.toString(count++) + "_" + question.getId());
            String choiceText = c.getText();
            choiceText = FormattedText.unEscapeHtml(choiceText);
            simpleChoice.setTextContent(choiceText);
            choiceInteraction.appendChild(simpleChoice);
        }

        questionParts.put("choiceInteraction", choiceInteraction);
        questionParts.put("responseDeclaration", responseDeclaration);
        return;
    }

    /**
     * Get the Elements needed for question.xml
     * 
     * @param questionDocument
     *        The AssessmentItem Document
     * @param question
     *        The Question
     * @param questionParts
     *        Map containing different w3c dom elements
     */
    public void getMCResponseChoices(Document questionDocument, Question question,
            Map<String, Element> questionParts) {
        if (question == null)
            return;

        MultipleChoiceQuestionImpl mc = (MultipleChoiceQuestionImpl) (question.getTypeSpecificQuestion());
        //      String cardinality = ("False".equalsIgnoreCase(mc.getSingleCorrect())) ? "multiple" : "single";
        boolean shuffle = mc.shuffleChoicesSetting();
        List<MultipleChoiceQuestionChoice> choices = mc.getChoices();
        Set<Integer> correctAnswers = mc.getCorrectAnswerSet();
        int maxChoice = (correctAnswers != null) ? correctAnswers.size() : 0;

        // <choiceInteraction responseIdentifier="RESPONSE" shuffle="false" maxChoices="1">
        Element choiceInteraction = questionDocument.createElement("choiceInteraction");
        choiceInteraction.setAttribute("responseIdentifier", "RESPONSE");
        choiceInteraction.setAttribute("shuffle", new Boolean(shuffle).toString());
        choiceInteraction.setAttribute("maxChoices", new Integer(maxChoice).toString());

        // <responseDeclaration identifier="RESPONSE" cardinality="single" baseType="identifier">
        Element responseDeclaration = createResponseDeclaration(questionDocument, "RESPONSE", "single",
                "identifier");
        Element correctResponse = questionDocument.createElement("correctResponse");
        responseDeclaration.appendChild(correctResponse);

        // response choices
        int count = 1;
        for (MultipleChoiceQuestionChoice c : choices) {
            // <simpleChoice identifier="ChoiceA">You must stay with your luggage at all times.</simpleChoice>
            Element simpleChoice = questionDocument.createElement("simpleChoice");
            simpleChoice.setAttribute("identifier", "Choice" + Integer.toString(count++) + "_" + question.getId());
            String choiceText = c.getText();
            choiceText = FormattedText.unEscapeHtml(choiceText);
            simpleChoice.setTextContent(choiceText);
            choiceInteraction.appendChild(simpleChoice);
        }

        // correct answers
        for (Integer correct : correctAnswers) {
            Element correctResponseValue = questionDocument.createElement("value");
            correctResponseValue.setTextContent("Choice" + (correct.intValue() + 1) + "_" + question.getId());
            correctResponse.appendChild(correctResponseValue);
        }
        questionParts.put("choiceInteraction", choiceInteraction);
        questionParts.put("responseDeclaration", responseDeclaration);
        return;
    }

    /**
     * Get different components of a match type question
     * 
     * @param questionDocument
     * @param question
     * @param questionParts
     */
    public void getMatchResponseChoices(Document questionDocument, Question question,
            Map<String, Element> questionParts) {
        if (question == null)
            return;

        MatchQuestionImpl mc = (MatchQuestionImpl) (question.getTypeSpecificQuestion());
        String cardinality = "multiple";
        boolean shuffle = true;
        List<MatchQuestionPair> choicePairs = mc.getPairs();

        int maxChoice = (choicePairs != null) ? choicePairs.size() : 0;

        // <matchInteraction responseIdentifier="RESPONSE" shuffle="true" maxAssociations="4">
        Element matchInteraction = questionDocument.createElement("matchInteraction");
        matchInteraction.setAttribute("responseIdentifier", "RESPONSE");
        matchInteraction.setAttribute("shuffle", new Boolean(shuffle).toString());
        matchInteraction.setAttribute("maxAssociations", new Integer(maxChoice).toString());

        // <responseDeclaration identifier="RESPONSE" cardinality="single" baseType="identifier">
        Element responseDeclaration = createResponseDeclaration(questionDocument, "RESPONSE", cardinality,
                "directedPair");
        Element correctResponse = questionDocument.createElement("correctResponse");
        responseDeclaration.appendChild(correctResponse);

        // response choices
        int count = 0;
        Element simpleMatchSet1 = questionDocument.createElement("simpleMatchSet");
        Element simpleMatchSet2 = questionDocument.createElement("simpleMatchSet");

        for (MatchQuestionPair c : choicePairs) {
            // <simpleAssociableChoice identifier="C" matchMax="1">Capulet</simpleAssociableChoice>
            Element simpleAssociableChoice = questionDocument.createElement("simpleAssociableChoice");
            String id1 = "Choice" + Integer.toString(++count) + "_" + question.getId();
            simpleAssociableChoice.setAttribute("identifier", id1);
            simpleAssociableChoice.setAttribute("matchMax", "1");
            String choiceText = c.getChoice();
            choiceText = FormattedText.unEscapeHtml(choiceText);
            simpleAssociableChoice.setTextContent(choiceText);
            simpleMatchSet1.appendChild(simpleAssociableChoice);

            // match
            Element simpleAssociableMatch = questionDocument.createElement("simpleAssociableChoice");
            String id2 = "Choice" + Integer.toString(++count) + "_" + question.getId();
            simpleAssociableMatch.setAttribute("identifier", id2);
            simpleAssociableMatch.setAttribute("matchMax", "1");
            String matchText = c.getMatch();
            matchText = FormattedText.unEscapeHtml(matchText);
            simpleAssociableMatch.setTextContent(matchText);
            simpleMatchSet2.appendChild(simpleAssociableMatch);

            // correct pair
            Element correctResponseValue = questionDocument.createElement("value");
            correctResponseValue.setTextContent(id1 + " " + id2);
            correctResponse.appendChild(correctResponseValue);
        }
        matchInteraction.appendChild(simpleMatchSet1);
        matchInteraction.appendChild(simpleMatchSet2);

        questionParts.put("matchInteraction", matchInteraction);
        questionParts.put("responseDeclaration", responseDeclaration);
        return;
    }

    /**
     * Get True-false question elements
     * 
     * @param questionDocument
     * @param question
     * @param questionParts
     */
    public void getTFResponseChoices(Document questionDocument, Question question,
            Map<String, Element> questionParts) {
        if (question == null)
            return;

        TrueFalseQuestionImpl tf = (TrueFalseQuestionImpl) (question.getTypeSpecificQuestion());
        String cardinality = "single";

        // <choiceInteraction responseIdentifier="RESPONSE" shuffle="false" maxChoices="1">
        Element choiceInteraction = questionDocument.createElement("choiceInteraction");
        choiceInteraction.setAttribute("responseIdentifier", "RESPONSE");
        choiceInteraction.setAttribute("shuffle", "false");
        choiceInteraction.setAttribute("maxChoices", "1");

        // <responseDeclaration identifier="RESPONSE" cardinality="single" baseType="identifier">
        Element responseDeclaration = createResponseDeclaration(questionDocument, "RESPONSE", cardinality,
                "identifier");
        Element correctResponse = questionDocument.createElement("correctResponse");
        responseDeclaration.appendChild(correctResponse);
        Element correctResponseValue = questionDocument.createElement("value");

        // response choices
        // <simpleChoice identifier="ChoiceA">True</simpleChoice>
        Element simpleChoice = questionDocument.createElement("simpleChoice");
        simpleChoice.setAttribute("identifier", "ChoiceA" + "_" + question.getId());
        simpleChoice.setTextContent("True");
        choiceInteraction.appendChild(simpleChoice);

        Element simpleChoiceFalse = questionDocument.createElement("simpleChoice");
        simpleChoiceFalse.setAttribute("identifier", "ChoiceB" + "_" + question.getId());
        simpleChoiceFalse.setTextContent("False");
        choiceInteraction.appendChild(simpleChoiceFalse);

        if (tf.correctAnswer)
            correctResponseValue.setTextContent("ChoiceA" + "_" + question.getId());
        else
            correctResponseValue.setTextContent("ChoiceB" + "_" + question.getId());
        correctResponse.appendChild(correctResponseValue);

        questionParts.put("choiceInteraction", choiceInteraction);
        questionParts.put("responseDeclaration", responseDeclaration);
        return;
    }

    /**
     * Parses the text and if embed media found then translates the path and adds the file to the zip package.
     * 
     * @param zip
     *        The zip package
     * @param text
     *        Text
     * @param mediaFiles
     *        List of embedded files found
     * @return The translated Text
     */
    protected String translateEmbedData(ZipOutputStream zip, String subFolder, String writeSubFolder, String text,
            List<String> mediaFiles) {
        Pattern p = Pattern.compile("(src|href)[\\s]*=[\\s]*\"([^#\"]*)([#\"])",
                Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
        StringBuffer sb = new StringBuffer();
        subFolder = (subFolder == null || subFolder.length() == 0) ? "Resources/" : subFolder;
        Matcher m = p.matcher(text);

        // security advisor
        pushAdvisor();

        while (m.find()) {
            if (m.groupCount() != 3)
                continue;
            String ref = m.group(2);
            if (!ref.contains("/access/mneme/content/"))
                continue;

            String resource_id = ref.replace("/access/mneme", "");
            String resource_name = ref.substring(ref.lastIndexOf("/") + 1);
            resource_name = resource_name.replaceAll("%20", " ");
            resource_name = Validator.escapeResourceName(resource_name);
            mediaFiles.add(subFolder + resource_name);
            m.appendReplacement(sb, m.group(1) + "= \"" + writeSubFolder + resource_name + "\"");

            writeContentResourceToZip(zip, subFolder, resource_id, resource_name);
        }

        popAdvisor();
        m.appendTail(sb);
        return sb.toString();
    }

    /**
     * Creates elements for all embed media with in itembody element. Use this for question text
     * 
     * @param zip
     * @param subFolder
     * @param text
     * @param itemBody
     * @param mediaFiles
     * @return
     */
    private Element translateEmbedData(ZipOutputStream zip, String subFolder, String text, Element itemBody,
            List<String> mediaFiles, Document questionDocument) {
        if (text == null || text.length() == 0)
            return itemBody;

        Element media = null;
        try {
            Pattern pa = Pattern.compile("<(img|a|embed)\\s+.*?/*>",
                    Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE | Pattern.DOTALL);

            // TODO: write all attributes
            Pattern p_srcAttribute = Pattern.compile("(src|href)[\\s]*=[\\s]*\"([^#\"]*)([#\"])",
                    Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
            Matcher m = pa.matcher(text);
            StringBuffer sb = new StringBuffer();
            subFolder = (subFolder == null || subFolder.length() == 0) ? "Resources/" : subFolder;
            String embedSubFolder = "Resources/";
            int start = 0;

            while (m.find()) {
                int startIdx = m.start();

                String img_content = m.group(0);
                Matcher m_src = p_srcAttribute.matcher(img_content);
                if (m_src.find()) {
                    String ref = m_src.group(2);
                    if (!ref.contains("/access/mneme/content/"))
                        continue;

                    Element div = questionDocument.createElement("div");
                    if (startIdx <= text.length()) {
                        String divText = text.substring(start, startIdx);
                        div.setTextContent(divText);
                        start = m.end();
                    }
                    ref = ref.replaceAll("%20", " ");
                    String resource_id = ref.replace("/access/mneme", "");
                    String embedFileName = ref.substring(ref.lastIndexOf("/") + 1);
                    ref = subFolder + embedFileName;
                    mediaFiles.add(ref);

                    media = questionDocument.createElement(m.group(1));
                    if ("a".equalsIgnoreCase(m.group(1)))
                        media.setAttribute("target", "_blank");
                    media.setAttribute(m_src.group(1), embedSubFolder + embedFileName);
                    m.appendReplacement(sb, "");

                    String fileName = Validator.getFileName(resource_id);
                    fileName = fileName.replaceAll("%20", " ");
                    fileName = Validator.escapeResourceName(fileName);
                    writeContentResourceToZip(zip, subFolder, resource_id, fileName);
                    itemBody.appendChild(div);
                    itemBody.appendChild(media);
                }
            }
            m.appendTail(sb);
            if (start > 0 && start < text.length()) {
                Element div = questionDocument.createElement("div");
                div.setTextContent(text.substring(start));
                itemBody.appendChild(div);
            }
            return itemBody;
        } catch (Exception e) {
            M_log.debug("error in translating embed up blank img tags:" + e.getMessage());
        }
        return itemBody;
    }

    /**
     * Writes the media file from content resource to the zip package. Its important to push the security advisor before calling this method.
     * 
     * @param zip
     *        The zip package
     * @param id
     *        resource id
     * @param fileName
     *        file name
     */
    protected void writeContentResourceToZip(ZipOutputStream zip, String subFolder, String id, String fileName) {
        try {
            // Reference does not know how to make the id from a private docs reference.
            if (id.startsWith("/content/")) {
                id = id.substring("/content".length());
            }

            if (subFolder != null)
                fileName = subFolder + fileName;

            // write attachment to the zip
            id = id.replaceAll("%20", " ");
            ContentResource resource = this.contentHostingService.getResource(id);
            zip.putNextEntry(new ZipEntry(fileName));
            zip.write(resource.getContent());
            zip.closeEntry();
        } catch (Exception e) {
            M_log.warn("ExportQtiService: zipping embed or attachments: " + e.toString());
        }
    }

    /**
     * Write Xml document to zip package
     * 
     * @param zip
     * @param fileTitle
     * @param document
     */
    protected void writeDocumentToZip(ZipOutputStream zip, String subFolder, String fileTitle, Document document) {
        try {
            if (subFolder != null)
                zip.putNextEntry(new ZipEntry(subFolder));
            zip.putNextEntry(new ZipEntry(fileTitle));
            zip.write(Xml.writeDocumentToString(document).getBytes("UTF-8"));
            zip.closeEntry();
            zip.flush();
        } catch (IOException e) {
            M_log.warn("zipSubmissionsQuestion: zipping question: " + e.toString());
        }
    }

    /**
     * Final initialization, once all dependencies are set.
     */
    public void init() {
        // messages
        if (this.bundle != null)
            this.messages = new ResourceLoader(this.bundle);

        M_log.info("init()");
    }

    /**
     * Remove our security advisor.
     */
    protected void popAdvisor() {
        securityServiceSakai.popAdvisor();
    }

    /**
     * Setup a security advisor.
     */
    protected void pushAdvisor() {
        // setup a security advisor
        securityServiceSakai.pushAdvisor(new SecurityAdvisor() {
            public SecurityAdvice isAllowed(String userId, String function, String reference) {
                return SecurityAdvice.ALLOWED;
            }
        });
    }

    /**
     * Dependency: AssessmentService.
     * 
     * @param service
     *        The AssessmentService.
     */
    public void setAssessmentService(AssessmentService service) {
        this.assessmentService = service;
    }

    /**
     * Set the message bundle.
     * 
     * @param bundle
     *        The message bundle.
     */
    public void setBundle(String name) {
        this.bundle = name;
    }

    /**
     * Dependency: QuestionService.
     * 
     * @param service
     *        The QuestionService.
     */
    public void setQuestionService(QuestionService service) {
        this.questionService = service;
    }

    public void setSecurityServiceSakai(org.sakaiproject.authz.api.SecurityService securityServiceSakai) {
        this.securityServiceSakai = securityServiceSakai;
    }

    public void setContentHostingService(ContentHostingService contentHostingService) {
        this.contentHostingService = contentHostingService;
    }

}