package org.wookie.qti; import r2q2.util.streams.*; import r2q2.router.ws.*; import java.net.*; import javax.activation.*; import javax.xml.soap.*; import java.rmi.RemoteException; import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; import java.io.*; import org.apache.axis.AxisFault; import org.apache.commons.codec.binary.Base64; import org.apache.log4j.Logger; import org.jsoup.Jsoup; import org.jsoup.safety.Whitelist; import org.jsoup.select.Elements; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.wookie.qti.util.DocumentBuilderHelper; import org.wookie.qti.util.PropertiesHelper; import r2q2.rendering.RendererOpts; /** * @author Raido * */ public class R2Q2_Interface { private RouterSoapBindingStub stub; private int renderOpts =RendererOpts.RENDERWRAPPER | RendererOpts.RENDERTITLE | RendererOpts.RENDERBODY | RendererOpts.RENDERFEEDBACK | RendererOpts.RENDERFORM | RendererOpts.RENDERINCLUDES; private String assessmentTitle; private Logger logger = Logger.getLogger(R2Q2_Interface.class.getName()); /* * Init R2Q2 Interface class */ public R2Q2_Interface() { PropertiesHelper p = new PropertiesHelper(); String router = p.getR2Q2WsPath(); try { this.stub = new RouterSoapBindingStub( new URL(router), new RouterServiceLocator()); } catch (AxisFault e) { logger.fatal("AxisFault "+e.getFaultReason()); } catch (MalformedURLException e) { logger.fatal("URL MALFORMED"); } } /* * Get R2Q2 RouterSoapBindingStub; * @return RouterSoapBindingStub */ private RouterSoapBindingStub getSoapStub() { return this.stub; } /* * Get R2Q2 renderOpts attribute * @return renderOpts */ private int getRenderOpts() { return this.renderOpts; } /** Return current assessment title * @return Assessment title */ public String getAssessmentTitle() { return this.assessmentTitle; } //TODO fix this crap, add param byte zip (for future waramu) public ArrayList getHtml(byte[] zip) { ArrayList out = new ArrayList(); String main_test_xml = this.getXmlFromZip(zip, null); //fileTitle param null, for test_###.xml search NodeList qXmlList; try { Document doc = new DocumentBuilderHelper().createDocument(main_test_xml); NodeList assessmentTitleTag = doc.getElementsByTagName("assessmentTest"); Element title = (Element) assessmentTitleTag.item(0); this.assessmentTitle = title.getAttribute("title"); qXmlList = doc.getElementsByTagName("assessmentItemRef"); for (int i = 0; i < qXmlList.getLength(); i++) { try { Element xmlRef = (Element) qXmlList.item(i); String fileTitle = xmlRef.getAttribute("href"); String questionIdentifier = xmlRef.getAttribute("identifier"); String guid = this.newSession(zip, fileTitle); String xmlString = this.getXmlFromZip(zip, fileTitle); ArrayList responseIds = this.getResponseIdentifiers(xmlString); String html = this.doFirstStage(guid); org.jsoup.nodes.Document docOut = Jsoup.parse(html); Elements questionForm = docOut.getElementsByTag("form"); org.jsoup.nodes.Element form = questionForm.get(0); String itemHtmlFragment = form.html(); Item newItem = new Item(itemHtmlFragment, guid, responseIds, questionIdentifier); out.add(newItem); } catch (Exception e) { logger.error("Parsing QTI item HTML failed"); } } } catch (Exception e) { logger.error("Parsing main assessment XML file failed"); } return out; } /* * Create new content package session * @param byte[] zip, String fileTitle (in Zip) * @return session GUID, or null if session failed */ public String newSession(byte[] zip, String fileTitle) throws IOException { if (zip != null){ ByteArrayDataSource bads = new ByteArrayDataSource("package.zip", zip, "application/zip"); this.getSoapStub().addAttachment(new DataHandler(bads)); String guid = this.getSoapStub().newContentPackageSession(fileTitle, "r2q2.rendering.xhtml.XHTMLRenderer",this.getRenderOpts()); return guid; } return null; } /* * Do firstStage of QTI item parsing * @param String guid * @return retrieved HTML from R2Q2 */ public String doFirstStage(String guid) { if(guid != null) { try { this.getSoapStub().firstStage(guid); } catch (Exception e) { logger.fatal("Error doing firstStage"); } } //return question HTML return this.getHtmlAttachment(); } /* * Do nextStage of QTI item parsing * @param String guid, HashMap responseVars * @return HashMap = new HashMap(); * if AxisFault No guid exception occurs, @return HashMap = new HashMap<"axisFault", "noGuid">(); */ //TODO koodi mudida @SuppressWarnings("unchecked") public HashMap doNextStage(String guid, HashMap> responseVars) { HashMap returnMap = new HashMap(); float quesScore = 0; if(guid != null) { try { if(!responseVars.containsKey("")) { HashMap[] resp = new HashMap[2]; resp = this.getSoapStub().nextStage(guid, responseVars); logger.debug("ResponseVars: \n"+responseVars+"\n"+resp[1]+"\n"+resp[0]); HashMap>> outcome = resp[1]; Iterator iter = outcome.keySet().iterator(); //iterate through score maps while(iter.hasNext()){ String next = iter.next(); HashMap> temp = outcome.get(next); //TODO check this code one more time try { //try parsing number, completionStatus and Feedback will //give NumberFormatException or NullPointerException quesScore += Float.parseFloat((String)temp.get(temp.get("identifier")).toString().replace("[", "").replace("]", "")); } catch (NumberFormatException e) { //TODO implement some error handling, if needed } catch (NullPointerException e) { quesScore += 0; } } //put outcome info to returnMap HashMap returnMap.put("score", ""+quesScore); returnMap.put("feedback", this.getFeedback()); } } catch (AxisFault e) { if(e.getFaultReason().contains("No processor for GUID")) { returnMap.put("axisFault", "noGuid"); } } catch (RemoteException e) { logger.fatal("NextStage communication to R2Q2 failed"); } catch (NullPointerException e) { logger.fatal("NextStage FAILURE, check R2Q2 URL, probably missing/malformed"); } } return returnMap; } /* * Get SOAP HTML attachment, * must be called after doFirstStage / doNextStage * @return String */ private String getHtmlAttachment() { Object[] attachments = this.getSoapStub().getAttachments(); AttachmentPart ap = (AttachmentPart) attachments[0]; byte[] file = null; try { file = StreamCopier.copyToByteArray(ap.getDataHandler().getInputStream()); } catch (SOAPException e) { logger.error("Error retriving HTML "+e.getMessage()); } catch (IOException e) { logger.error("I/O error "+e.getMessage()); } try { return new String(file, "UTF-8"); } catch (UnsupportedEncodingException e) { logger.fatal("Unsupported encoding, must be UTF-8"); } return ""; } /* Parse HTML to get feedback info * @calls this.getHtmlAttachment(); * @return String feedback */ //TODO fix feedback parsing private String getFeedback() { String outcome = ""; try { //TODO find better implementation, but this works String feedbackHtml = this.getHtmlAttachment(); org.jsoup.nodes.Document doc = Jsoup.parse(feedbackHtml, "UTF-8"); org.jsoup.nodes.Element feedbackDiv = doc.select("div.r2q2ModalFeedback").first(); feedbackDiv.child(0).text(""); outcome = Jsoup.clean(feedbackDiv.html(), Whitelist.none()); } catch (Exception e) { //no error handling needed } return outcome; } /* * Get test_someNumbers.xml file from ZipFile, * this file contains assessmentItemRef nodes * which we need to iterate trough and generate HTML from * each QTI item. * * @param byte[] zipFile - from repository * @return String, empty if can't read ZipFile */ public String getXmlFromZip(byte[] zipIn, String fileTitle) { String xmlString = ""; try { File tmp = File.createTempFile("test", "zip"); FileOutputStream out = new FileOutputStream(tmp); out.write(zipIn); out.close(); // open tmp zip file ZipFile zip = new ZipFile(tmp); //if file title is null, then we are supposed to search for test_###.xml file if(fileTitle == null) { for (Enumeration e = zip.entries(); e.hasMoreElements();) { ZipEntry entry = (ZipEntry) e.nextElement(); if(entry.getName().toLowerCase().startsWith("test") && entry.getName().toLowerCase().endsWith(".xml")) { xmlString = this.readInputStream(zip.getInputStream(entry)); //test file found, break the cycle break; } } } else { //Get file by filename xmlString = this.readInputStream(zip.getInputStream(zip.getEntry(fileTitle))); } //clean up, delete temp file tmp.delete(); } catch (ZipException e1) { logger.fatal("Problem with temporary Zip file. "+e1.getMessage()); } catch (IOException e1) { logger.error("I/O error "+e1.getMessage()); } return xmlString.toString(); } /** Read inputstream * * @param InputStream in * @return String read file contents * @throws IOException */ private String readInputStream(InputStream in) throws IOException { String str = ""; InputStreamReader isr = new InputStreamReader(in); BufferedReader bufr = new BufferedReader(isr); String xmlStringTmp = bufr.readLine(); while (xmlStringTmp != null) { str += xmlStringTmp; xmlStringTmp = bufr.readLine(); } return str; } /* Get response identifier from responseDeclaration node * * @param byte[] zip, String fileTitle * * @return responseIdentifier */ private ArrayList getResponseIdentifiers(String xmlString) { ArrayList respIdents = new ArrayList(); //try to parse question XML file and read responseDeclaration node Document doc = null; try { doc = new DocumentBuilderHelper().createDocument(xmlString); //TODO clean up doc.getDocumentElement().normalize(); NodeList respDeclNode = doc.getElementsByTagName("responseDeclaration"); for(int i=0; i -1) { outStream.write(buffer, 0, bytesRead); } in.close(); return outStream.toByteArray(); } catch (MalformedURLException e) { logger.fatal("ZIP file URL malformed"); } catch (IOException e) { logger.error("I/O error "+e.getMessage()); } return null; } }