/*==========================================================================*\ | $Id: SubmissionFileStats.java,v 1.9 2012/06/06 18:43:56 stedwar2 Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2012 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU Affero General Public License as published | by the Free Software Foundation; either version 3 of the License, or | (at your option) any later version. | | Web-CAT is distributed in the hope that it will be useful, | but WITHOUT ANY WARRANTY; without even the implied warranty of | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | GNU General Public License for more details. | | You should have received a copy of the GNU Affero General Public License | along with Web-CAT; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package org.webcat.grader; import com.webobjects.foundation.*; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileReader; import java.io.PrintWriter; import java.io.StringReader; import java.util.*; import org.apache.log4j.Logger; import org.jdom.*; import org.jdom.input.SAXBuilder; import org.jdom.output.XMLOutputter; import org.webcat.core.*; import org.webcat.grader.messaging.GraderMarkupParseError; import org.webcat.woextensions.WCResourceManager; // ------------------------------------------------------------------------- /** * Represents test coverage metrics for one file/class in a submission. * * @author Stephen Edwards * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.9 $, $Date: 2012/06/06 18:43:56 $ */ public class SubmissionFileStats extends _SubmissionFileStats { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Creates a new SubmissionFileStats object. */ public SubmissionFileStats() { super(); } //~ Constants (for key names) ............................................. // Attributes --- public static final String SOURCE_FILE_NAME_KEY = "sourceFileName"; //~ Methods ............................................................... // ---------------------------------------------------------- public double gradedElementsCoverage() { int num = elements(); int numCovered = elementsCovered(); if ( num != 0 ) { return ( (double)numCovered ) / ( (double)num ); } else { return 1.0; } } // ---------------------------------------------------------- public double gradedElementsCoveragePercent() { return gradedElementsCoverage() * 100.0; } // ---------------------------------------------------------- public int totalElements() { return statements() + conditionals() + methods(); } // ---------------------------------------------------------- public int totalElementsCovered() { return statementsCovered() + conditionalsCovered() + methodsCovered(); } // ---------------------------------------------------------- public double totalElementsCoverage() { int num = totalElements(); int numCovered = totalElementsCovered(); if ( num != 0 ) { return ( (double)numCovered ) / ( (double)num ); } else { return 1.0; } } // ---------------------------------------------------------- public String fullyQualifiedClassName() { String pkg = pkgName(); if ( pkg != null ) { return pkg + "." + className(); } else { return className(); } } // ---------------------------------------------------------- public String sourceFileName() { String result = sourceFileNameRaw(); if ( result == null ) { result = fullyQualifiedClassName().replace( '.', '/' ) + ".java"; setSourceFileNameRaw( result ); } return result; } // ---------------------------------------------------------- public String markupFileName() { String result = markupFileNameRaw(); if ( result == null ) { result = "clover/" + fullyQualifiedClassName().replace( '.', '/' ) + ".html"; } return result; } // ---------------------------------------------------------- public java.io.File markupFile() { return new java.io.File( submissionResult().submission().resultDirName(), markupFileName() ); } // ---------------------------------------------------------- /** * Get the corresponding icon URL for this file's grading status. * * @return The image URL as a string */ public String statusURL() { return Status.statusURL( status() ); } // ---------------------------------------------------------- /** * Retrieve the number of TA/instructor remarks made on this file. * @return a count of manual comments entered for this file */ public int staffRemarks() { NSArray<SubmissionFileComment> myComments = comments(); return myComments == null ? 0 : myComments.count(); } // ---------------------------------------------------------- /** * Retrieve the total number of remarks on this file: auto-graded + * manual. * @return the value of the attribute */ public int totalRemarks() { return remarks() + staffRemarks(); } // ---------------------------------------------------------- /** * Retrieve the total number of point deductions on this file visible * to the student. * @return the value of the attribute */ public double deductionsForStudent() { if (submissionResult().status() == Status.CHECK) { return deductions(); } else { return deductions() - staffDeductions(); } } // ---------------------------------------------------------- /** * Retrieve the total number of point deductions on this file visible * to the given user. * @return the value of the attribute */ public double deductionsVisibleTo(User user) { if (user.hasAdminPrivileges() || submissionResult().submission().assignmentOffering() .courseOffering().isStaff(user)) { return deductions(); } else { return deductionsForStudent(); } } // ---------------------------------------------------------- /** * Retrieve the total number of manual point deductions on this file. * @return the value of the attribute */ public double staffDeductions() { if (!staffDeductionsIsValid) { staffDeductions = 0.0; for (SubmissionFileComment thisComment : comments()) { staffDeductions += thisComment.deduction(); } staffDeductionsIsValid = true; } return staffDeductions; } // ---------------------------------------------------------- /** * Retrieve the total number of automated grading point deductions * on this file. * @return the value of the attribute */ public double toolDeductions() { return deductions() - staffDeductions(); } // ---------------------------------------------------------- /** * Add a new entity to the <code>comments</code> * relationship (DO NOT USE--instead, use * <code>addToCommentsRelationship()</code>. * This method is provided for WebObjects use. * * @param value The new entity to relate to */ public void addToComments( org.webcat.grader.SubmissionFileComment value ) { staffDeductionsIsValid = false; super.addToComments(value); } // ---------------------------------------------------------- /** * Remove a specific entity from the <code>comments</code> * relationship (DO NOT USE--instead, use * <code>removeFromCommentsRelationship()</code>. * This method is provided for WebObjects use. * * @param value The entity to remove from the relationship */ public void removeFromComments( org.webcat.grader.SubmissionFileComment value ) { staffDeductionsIsValid = false; super.removeFromComments(value); } // ---------------------------------------------------------- /** * Add a new entity to the <code>comments</code> * relationship. * * @param value The new entity to relate to */ public void addToCommentsRelationship( org.webcat.grader.SubmissionFileComment value ) { staffDeductionsIsValid = false; super.addToCommentsRelationship(value); } // ---------------------------------------------------------- /** * Remove a specific entity from the <code>comments</code> * relationship. * * @param value The entity to remove from the relationship */ public void removeFromCommentsRelationship( org.webcat.grader.SubmissionFileComment value ) { staffDeductionsIsValid = false; super.removeFromCommentsRelationship(value); } // ---------------------------------------------------------- /** * Create a brand new object that is a member of the * <code>comments</code> relationship. * * @return The new entity */ public org.webcat.grader.SubmissionFileComment createCommentsRelationship() { staffDeductionsIsValid = false; return super.createCommentsRelationship(); } // ---------------------------------------------------------- /** * Remove a specific entity that is a member of the * <code>comments</code> relationship. * * @param value The entity to remove from the relationship */ public void deleteCommentsRelationship( org.webcat.grader.SubmissionFileComment value ) { staffDeductionsIsValid = false; super.deleteCommentsRelationship(value); } // ---------------------------------------------------------- /** * Remove (and then delete, if owned) all entities that are members of the * <code>comments</code> relationship. */ public void deleteAllCommentsRelationships() { staffDeductionsIsValid = false; super.deleteAllCommentsRelationships(); } // ---------------------------------------------------------- /** * Change the value of this object's <code>deductions</code> * property. * * @param value The new value for this property */ public void setDeductionsRaw( Double value ) { staffDeductionsIsValid = false; super.setDeductionsRaw(value); } // ---------------------------------------------------------- /** * A convenience method that returns true if this file has been tagged with * the specified tag. * * @param tag the tag to search for * @return true if the file has been tagged with the specified tag */ public boolean hasTag(String tag) { // As noted in GraderQueueProcessor, we always search for the tag with // surrounding spaces so that tags that are infixes of other tags do // not return false positives. The tags() attribute is guaranteed to // have leading and trailing spaces as well so that this always works. if (tags() == null) { return false; } else { return tags().contains(" " + tag + " "); } } // ---------------------------------------------------------- // To fix up incorrect markup generated between 1/21/2011 and 2/9/2011. // SF bug 3174285. private static final long PERIOD_START = new NSTimestamp(2011, 1, 21, 0, 0, 0, TimeZone.getTimeZone("UTC")) .getTime(); private static final long PERIOD_END = new NSTimestamp(2011, 2, 10, 0, 0, 0, TimeZone.getTimeZone("UTC")) .getTime(); private void rewriteMarkupIfNecessary(File markupFile) { if (!markupFile.exists()) return; long lastModified = markupFile.lastModified(); if (lastModified > PERIOD_START && lastModified < PERIOD_END) { // Attempt to correct problems in markup File revised = new File(markupFile.getParentFile(), markupFile.getName() + ".rev"); if (markupFile.exists() && !revised.exists()) { try { BufferedReader in = new BufferedReader(new FileReader(markupFile)); PrintWriter out = new PrintWriter(revised); String line = in.readLine(); while (line != null) { line = line.replace(" ", "\r"); out.println(line); line = in.readLine(); } in.close(); out.close(); if (markupFile.delete()) { if (!revised.renameTo(markupFile)) { log.error("cannot rename " + revised + " to " + markupFile); } } } catch (Exception e) { log.error("error patching HTML file " + markupFile, e); } } } } // ---------------------------------------------------------- public String codeWithComments( User user, boolean isGrading, com.webobjects.appserver.WORequest request ) throws Exception { File file = markupFile(); rewriteMarkupIfNecessary(file); //make the html file StringBuffer contents = new StringBuffer( (int)file.length() ); if (isGrading) { contents.append( "<link rel=\"stylesheet\" type=\"text/css\" href=\"" ); contents.append(WCResourceManager.versionlessResourceURLFor( "theme/base/code.css", "Core", null, request )); contents.append( "\"/>\n" ); } //get the array of file comments from the database NSArray<SubmissionFileComment> myComments = comments() .sortedArrayUsingComparator( SubmissionFileComment.STANDARD_ORDERING); /* StringBuffer fileoutput = new StringBuffer( (int)file.length() ); try { FileReader in = new FileReader( file ); final int BUFFER_SIZE = 8192; char[] b = new char[BUFFER_SIZE]; int count = in.read( b, 0, BUFFER_SIZE ); while ( count > -1 ) { fileoutput.append( b, 0, count ); count = in.read( b, 0, BUFFER_SIZE ); } java.io.FileWriter out = new java.io.FileWriter( new File( "C:/comments.out" ) ); out.write( fileoutput.toString() ); out.close(); } catch ( Exception e ) { log.error( "error loading file contents for " + file.getPath(), e ); } */ // parse the HTML text into a DOM structure FileInputStream inStream = null; try { SAXBuilder parser = new SAXBuilder(); inStream = new FileInputStream( file ); Document doc = parser.build( inStream ); Element root = doc.getRootElement(); @SuppressWarnings("unchecked") List<Element> children = root.getChild( "TBODY" ).getChildren(); ListIterator<Element> iterator = children.listIterator(); int index = 0; int box_number = 1; int reference = 0; String prefixToId = ""; boolean showPts = false; boolean isEditable = false; while ( iterator.hasNext() ) { Element child = iterator.next(); // get the id attribute from the row String id = child.getAttributeValue( "id" ); if ( ( id.charAt( 0 ) == '\"' ) && ( id.charAt( id.length() - 1 ) == '\"' ) ) { // if quotes around it id = id.substring( 1, id.length() - 1 ); } String [] idarr = id.split( ":" ); if ( idarr[0].charAt( 0 ) == 'O' ) // outside { // check to see if this is the row where the comment // needs to be inserted int rownum = Integer.parseInt( idarr[1] ); while (index != myComments.count() && myComments.objectAtIndex(index).lineNo() == rownum) { log.debug( "index = " + index + " count = " + myComments.count() ); // make a new comment with the properties and // insert it after the line SubmissionFileComment thisComment = myComments.objectAtIndex( index ); if ( thisComment.readableByUser( user ) ) { log.debug( "Inserting comment at line number " + thisComment.lineNo() ); box_number++; reference = box_number; showPts = true; // should be false, later ... isEditable = false; // if the comment is of the current users, if ( thisComment.author() == user ) { prefixToId = "I"; isEditable = isGrading; } else { // this comment is by someone else, not the // current user, so make it uneditable prefixToId = "F"; isEditable = false; } // Also need to check user's relationship with // course and enable/disable showPts appropriately String idnum = prefixToId + box_number + ":" + rownum + ":" + reference; // ---- first row ---- String firstrow = "<tr id=\"" + idnum + "\"><td id=\"" + idnum + "\" colspan=\"3\">" + "<img id=\"" + idnum + "\" src=\"" + WCResourceManager.resourceURLFor( "images/blank.gif", "Core", null, null) + "\" width=\"1\" height=\"2\"/>" + "</td></tr>"; // pass it through the XML parser before putting // it in JDOM Document doc1 = parser.build( new StringReader( firstrow ) ); // ---- second row ---- String pts = null; // basically, check here if it is a code review // assignment or a TA grading page, and // suppress score if it is // log.debug( "deduction = " // + thisComment.deduction() ); if ( thisComment.deduction() != 0.0 && showPts ) { pts = "" + thisComment.deduction(); } // log.debug( "pts = \"" + pts + "\"" ); // replace the current value of the // contentEditable tag with the new value String newmes = thisComment.message(); log.debug( "newmes is = " + newmes ); newmes = newmes.replaceAll( "&&&&", idnum ); newmes = newmes.replaceAll( "content[e|E]ditable=\"[false|true]\"", "contentEditable=\"" + isEditable + "\"" ); log.debug( newmes ); String vals = "<table id=\"" + idnum + ":X\" border=\"0\" cellpadding=\"0\"><tbody " + "id=\"" + idnum + ":B\"><tr id=\"" + idnum + ":R\"><td id=\"" + idnum + ":D\" class=\"messageBox\"><img id=\"" + idnum + ":I\" src=\"" + WCResourceManager.resourceURLFor( thisComment.categoryIcon(), "Core", null, null) + "\" border=\"0\"/><option id=\"" + idnum + ":T\" value=\"" + thisComment.to() + "\"/><b id=\"" + idnum + "\"> <span id=\"" + idnum + ":C\">" + thisComment.category() + "</span> <span id=\"" + idnum + ":N\">[" + thisComment.author().name() + "]" + ( ( pts != null ) ? " : </span><span id=\"" + idnum + ":P\" contentEditable=\"" + isEditable + "\">" + pts + "</span>" : "</span>" ) + "</b><br id=\"" + idnum + "\"/><i id=\"" + idnum + "\">" + newmes + "</i></td></tr></tbody></table>"; // log.debug( "vals = " + vals ); String secondrow = "<tr id=\"" + idnum + "\"><td id=\"" + idnum + "\"><div id=\"" + idnum + "\"> </div></td><td id=\"" + idnum + "\"><div id=\"" + idnum + "\"> </div></td>" + "<td id=\"" + idnum + "\" align=\"left\"><div id=\"" + idnum + "\">" + vals + "</div></td></tr>"; // pass it through the XML parser before putting // it in JDOM Document doc2 = parser.build( new StringReader( secondrow ) ); // ---- third row ---- String thirdrow = "<tr id=\"" + idnum + "\"><td id=\"" + idnum + "\" colspan=\"3\"><img id=\"" + idnum + "\" src=\"" + WCResourceManager.resourceURLFor( "images/blank.gif", "Core", null, null) + "\" width=\"1\" height=\"2\"/>" + "</td></tr>"; // pass it through the XML parser before putting // it in JDOM Document doc3 = parser.build( new StringReader( thirdrow ) ); int newcat = thisComment.categoryNo(); // check to see if it has any attributes if ( !child.getAttributes().isEmpty() ) { String classname = child.getAttributeValue( "class" ); if ( classname != null ) { if ( classname.charAt( 0 ) == '\"' && classname.charAt( classname.length() - 1 ) == '\"' ) { // if quotes around it classname = classname.substring( 1, classname.length() - 1 ); } int thisCategory = SubmissionFileComment .categoryIntFromString( classname ); if ( thisCategory < newcat ) newcat = thisCategory; child.removeAttribute( "class" ); } } child.setAttribute("class", SubmissionFileComment.categoryName(newcat) .replaceAll("\\s", "_")); // inserting the comment box iterator.add( doc1.detachRootElement() ); iterator.add( doc2.detachRootElement() ); iterator.add( doc3.detachRootElement() ); } index++; // go to next comment } //while ends here } // big if ends here } // big while ends here // Now render the DOM tree in string form at append it // to contents XMLOutputter outputter = new XMLOutputter(); outputter.setOmitDeclaration( true ); contents.append( outputter.outputString( doc ) ); } catch ( Exception e ) { log.error( "exception parsing raw HTML file " + markupFile().getPath(), e ); new GraderMarkupParseError(submissionResult().submission(), GraderMarkupParseError.LOCATION_SUBMISSION_FILE_STATS, e, null, markupFile(), null).send(); /* Application.sendAdminEmail( null, submissionResult().submission().assignmentOffering() .courseOffering().instructors(), true, "Exception in SubmissionFileStats", "This is an automatic message from the Web-CAT server. An " + "exception was caught while\nattempting to read " + "the raw HTML stored in the file:\n\n" + markupFile().getPath() + "\n\nThis error may be due to errors in the HTML " + "generated by the grading script.\n" + ( (Application)Application.application() ) .informationForExceptionInContext( e, null, null ), null );*/ throw e; } finally { // Ensure the stream is closed, to prevent exceeding the // max # of file handles if ( inStream != null ) { try { inStream.close(); } catch ( Exception e ) { // Just swallow it } } } /* try { FileReader in = new FileReader( file ); final int BUFFER_SIZE = 8192; char[] b = new char[BUFFER_SIZE]; int count = in.read( b, 0, BUFFER_SIZE ); while ( count > -1 ) { contents.append( b, 0, count ); count = in.read( b, 0, BUFFER_SIZE ); } } catch ( Exception e ) { log.error( "error loading file contents for " + file.getPath(), e ); } */ return contents.toString(); } //~ Instance/static variables ............................................. private double staffDeductions; private boolean staffDeductionsIsValid; static Logger log = Logger.getLogger(SubmissionFileStats.class); }