/*==========================================================================*\
| $Id: Grader.java,v 1.12 2012/05/09 16:28:33 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 java.io.File;
import org.apache.log4j.Logger;
import org.webcat.core.Application;
import org.webcat.core.Course;
import org.webcat.core.CourseOffering;
import org.webcat.core.EntityResourceRequestHandler;
import org.webcat.core.Session;
import org.webcat.core.Subsystem;
import org.webcat.core.User;
import org.webcat.core.messaging.UnexpectedExceptionMessage;
import org.webcat.grader.messaging.AdminReportsForSubmissionMessage;
import org.webcat.grader.messaging.GraderKilledMessage;
import org.webcat.grader.messaging.GraderMarkupParseError;
import org.webcat.grader.messaging.GradingResultsAvailableMessage;
import org.webcat.grader.messaging.SubmissionSuspendedMessage;
import org.webcat.woextensions.ECAction;
import org.webcat.woextensions.WCFetchSpecification;
import static org.webcat.woextensions.ECAction.run;
import com.webobjects.appserver.WOActionResults;
import com.webobjects.appserver.WOContext;
import com.webobjects.appserver.WORequest;
import com.webobjects.eoaccess.EOUtilities;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOQualifier;
import com.webobjects.eocontrol.EOSortOrdering;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSData;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSNumberFormatter;
import com.webobjects.foundation.NSTimestamp;
import er.extensions.qualifiers.ERXKeyValueQualifier;
//-------------------------------------------------------------------------
/**
* The subsystem defining Web-CAT administrative tasks.
*
* @author Stephen Edwards
* @author Last changed by $Author: stedwar2 $
* @version $Revision: 1.12 $, $Date: 2012/05/09 16:28:33 $
*/
public class Grader
extends Subsystem
{
//~ Constructors ..........................................................
// ----------------------------------------------------------
/**
* Creates a new Grader subsystem object.
*/
public Grader()
{
super();
instance = this;
}
// ----------------------------------------------------------
/**
* Returns the current subsystem object. In principle, only one instance
* of this class exists. However, we're not using the singleton pattern
* exactly, since the instance is created using a normal constructor
* via reflection. However, this class has a private static data member
* that keeps track of the most recently created instance, and this
* method provides access to it. The result is much like a singleton,
* but without the guarantees provided by a hidden constructor.
* @return the current Grader subsystem instance
*/
public static Grader getInstance()
{
return instance;
}
//~ Methods ...............................................................
// ----------------------------------------------------------
/**
* Performs all initialization actions for this subsystem.
*/
public void init()
{
super.init();
// Register notification messages.
GradingResultsAvailableMessage.register();
AdminReportsForSubmissionMessage.register();
SubmissionSuspendedMessage.register();
GraderKilledMessage.register();
GraderMarkupParseError.register();
EntityResourceRequestHandler.registerHandler(GradingPlugin.class,
new GradingPluginResourceHandler());
// Install or update any plug-ins that need it
GradingPlugin.autoUpdateAndInstall();
}
// ----------------------------------------------------------
/**
* Performs all startup actions for this subsystem.
*/
public void start()
{
// Create the queue and the queueprocessor
graderQueue = new GraderQueue();
graderQueueProcessor = new GraderQueueProcessor( graderQueue );
// Kick off the processor thread
graderQueueProcessor.start();
if ( Application.configurationProperties().booleanForKey(
"grader.resumeSuspendedJobs" ) )
{
// Resume any enqueued jobs (if grader is coming back up
// after an application restart)
run(new ECAction() { public void action() {
for (EnqueuedJob job : EnqueuedJob.allObjects(ec))
{
if (!job.paused())
{
// Only need to trigger the queue processor once,
// and it will slurp up all the jobs that are ready.
graderQueue.enqueue(null);
break;
}
}
}});
}
}
// ----------------------------------------------------------
/**
* Initialize the subsystem-specific session data in a newly created
* session object. This method is called once by the core for
* each newly created session object.
*
* @param s The new session object
*/
public void initializeSessionData( Session s )
{
super.initializeSessionData(s);
try
{
EOUtilities.objectsForEntityNamed( s.sessionContext(),
Assignment.ENTITY_NAME );
}
catch ( Exception e )
{
// Swallow the exception--we want to force a failure on
// the first cross-model search in this session, so that
// later searches will work OK.
}
}
// ----------------------------------------------------------
/**
* Generate the component definitions and bindings for a given
* pre-defined information fragment, so that the result can be
* plugged into other pages defined elsewhere in the system.
* @param fragmentKey the identifier for the fragment to generate
* (see the keys defined in {@link SubsystemFragmentCollector}
* @param htmlBuffer add the html template for the subsystem's fragment
* to this buffer
* @param wodBuffer add the binding definitions (the .wod file contents)
* for the subsystem's fragment to this buffer
*/
/* public void collectSubsystemFragments(
String fragmentKey, StringBuffer htmlBuffer, StringBuffer wodBuffer )
{
if ( fragmentKey.equals(
SubsystemFragmentCollector.SYSTEM_STATUS_ROWS_KEY ) )
{
htmlBuffer.append( "<webobject name=\"GraderSystemStatusRows\"/>" );
wodBuffer.append(
"GraderSystemStatusRows: "
+ GraderSystemStatusRows.class.getName()
+ "{ index = ^index; }\n"
);
}
else if ( fragmentKey.equals(
SubsystemFragmentCollector.HOME_STATUS_KEY ) )
{
htmlBuffer.append( "<webobject name=\"GraderHomeStatus\"/>" );
wodBuffer.append(
"GraderHomeStatus: "
+ GraderHomeStatus.class.getName()
+ "{}\n"
);
}
}*/
// ----------------------------------------------------------
/**
* Access the grader job queue.
*
* @return the grader job queue associated with this subsystem
*/
public GraderQueue graderQueue()
{
return graderQueue;
}
// ----------------------------------------------------------
/**
* Find out how many grading jobs have been processed so far.
*
* @return the number of jobs process so far
*/
public int processedJobCount()
{
return graderQueueProcessor.processedJobCount();
}
// ----------------------------------------------------------
/**
* Find out the processing delay for the most recently completed job.
*
* @return the time in milliseconds
*/
public long mostRecentJobWait()
{
return graderQueueProcessor.mostRecentJobWait();
}
// ----------------------------------------------------------
/**
* Find out the processing delay for the most recently completed job.
*
* @return the time in milliseconds
*/
public long estimatedJobTime()
{
return graderQueueProcessor.estimatedJobTime();
}
// ----------------------------------------------------------
/**
* Handle a direct action request. The user's login session will be
* passed in as well.
*
* @param request the request to respond to
* @param session the user's session
* @param context the context for the request
* @return The response page or contents
*/
public WOActionResults handleDirectAction(
WORequest request,
Session session,
WOContext context )
{
// Wait until this subsystem has actually started
while (!hasStarted())
{
try
{
// sleep for 2 seconds
Thread.sleep(2000);
}
catch (InterruptedException e)
{
// silently repeat the loop
}
}
// log.debug( "handleDirectAction(): session = " + session );
// log.debug( "handleDirectAction(): context = " + context );
WOActionResults results = null;
log.debug( "path = " + request.requestHandlerPath() );
if ( "cmsRequest".equals( request.requestHandlerPath() ) )
{
return handleCmsRequest( request, session, context );
}
if ( session == null )
{
log.error( "handleDirectAction(): null session" );
log.error( Application.extraInfoForContext( context ) );
}
else if ( !context.hasSession() )
{
log.error( "handleDirectAction(): no session on context!" );
log.error( Application.extraInfoForContext( context ) );
}
else if ( session != context.session() )
{
log.error( "handleDirectAction(): session mismatch with context!" );
log.error( "session = " + session );
log.error( "context session = " + context.session() );
log.error( Application.extraInfoForContext( context ) );
}
if ( "submit".equals( request.requestHandlerPath() ) )
{
results = handleSubmission( request, session, context );
}
else
{
results = handleReport( request, session, context );
}
// log.debug( "handleDirectAction() returning" );
return results;
}
// ----------------------------------------------------------
/**
* Handle a direct action request. The user's login session will be
* passed in as well.
*
* @param request the request to respond to
* @param session the user's session
* @param context the context for the request
* @return The response page or contents
*/
public WOActionResults handleCmsRequest(
WORequest request,
Session session,
WOContext context )
{
CmsResponse result = (CmsResponse)Application.application()
.pageWithName( CmsResponse.class.getName(), context );
return result;
}
// ----------------------------------------------------------
/**
* Handle a direct action request. The user's login session will be
* passed in as well.
*
* @param request the request to respond to
* @param session the user's session
* @param context the context for the request
* @return The response page or contents
*/
public WOActionResults handleSubmission(
WORequest request,
Session session,
WOContext context )
{
log.debug("handleSubmission()");
String scheme = request.stringFormValueForKey("a");
log.debug("scheme = " + scheme);
String crn = request.stringFormValueForKey("crn");
log.debug("crn = " + crn);
Integer courseNo = null;
try
{
Number num = request.numericFormValueForKey("course",
new NSNumberFormatter("0"));
courseNo = (num instanceof Integer)
? (Integer)num
: new Integer(num.intValue());
}
catch (Exception e)
{
// Ignore it, and treat it as an undefined course number
}
log.debug("courseNo = " + courseNo);
String partnerList = request.stringFormValueForKey("partners");
log.debug("partners = " + partnerList);
NSData file = (NSData)request.formValueForKey("file1");
String fileName = request.stringFormValueForKey("file1.filename");
log.debug("fileName = " + fileName);
SubmitResponse result = Application.wcApplication().pageWithName(
SubmitResponse.class, context);
EOEditingContext ec = result.localContext();
result.sessionID = session.sessionID();
log.debug("handleSubmission(): sessionID = " + result.sessionID);
NSTimestamp currentTime = new NSTimestamp();
log.debug("user = " + session.user()
+ "(prime = " + session.primeUser() + ")");
NSArray<EOSortOrdering> orderings = AssignmentOffering.dueDate.descs();
ERXKeyValueQualifier matchesScheme = AssignmentOffering.assignment
.dot(Assignment.name).eq(scheme);
EOQualifier qualifier = matchesScheme;
NSArray<AssignmentOffering> assignments = null;
if (crn != null)
{
qualifier = matchesScheme.and(AssignmentOffering.courseOffering
.dot(CourseOffering.crn).eq(crn));
}
else if (courseNo != null)
{
qualifier = matchesScheme.and(AssignmentOffering.courseOffering
.dot(CourseOffering.course).dot(Course.number).eq(courseNo));
}
try
{
assignments = AssignmentOffering.objectsMatchingQualifier(
ec, qualifier, orderings);
}
catch (Exception e)
{
assignments = AssignmentOffering.objectsMatchingQualifier(
ec, qualifier, orderings);
}
AssignmentOffering assignment = null;
User localizedUser = result.user();
if (assignments != null && assignments.count() > 0)
{
String msg = null;
for (AssignmentOffering thisAssignment : assignments)
{
log.debug("assignment = " + thisAssignment.assignment().name());
CourseOffering co = thisAssignment.courseOffering();
if (co.isInstructor(localizedUser)
|| co.isGrader(localizedUser)
|| (co.students().contains(localizedUser)
&& thisAssignment.publish()
&& currentTime.after(thisAssignment.availableFrom())
&& currentTime.before(thisAssignment.lateDeadline())))
{
log.debug("found matching assignment that is open.");
if (assignment == null)
{
assignment = thisAssignment;
}
else
{
if (msg == null)
{
msg = "Warning: multiple matching assignments "
+ "found.<br/>"
+ "Submitting to: " + assignment;
}
msg += "<br/>Ignoring: " + thisAssignment;
}
}
}
if (msg != null)
{
result.errorMessages.add(msg);
msg = msg.replaceAll("<br/>", "\n\t");
log.warn(msg + "\n\tUser = " + session.user());
}
}
if (assignment == null)
{
log.debug("no assignments are open.");
String msg = "The requested assignment is not accepting "
+ "submissions at this time or it could not be found. "
+ "The deadline may have passed.";
result.errorMessages.add(msg);
result.assignmentClosed = true;
log.warn(msg + " User = " + session.user() + ", a = " + scheme
+ ", crn = " + crn + ", courseNo = " + courseNo);
return result.generateResponse();
}
result.coreSelections().setCourseOfferingRelationship(
assignment.courseOffering());
result.prefs().setAssignmentOfferingRelationship(assignment);
NSArray<Submission> submissions =
Submission.submissionsForAssignmentOfferingAndUser(
ec, assignment, result.user());
int currentSubNo = submissions.count() + 1;
for (int i = 0; i < submissions.count(); i++)
{
int sno = submissions.objectAtIndex(i).submitNumber();
if (sno >= currentSubNo)
{
currentSubNo = sno + 1;
}
}
// TODO: This max submission check doesn't take partners into account
Number maxSubmissions = assignment.assignment().submissionProfile()
.maxSubmissionsRaw();
if (maxSubmissions != null
&& currentSubNo > maxSubmissions.intValue()
&& !assignment.courseOffering().isStaff(session.user()))
{
String msg = "You have exceeded the allowable number "
+ "of submissions for this assignment.";
result.errorMessages.add(msg);
log.warn(msg + " User = " + session.user()
+ "\n\t" + assignment);
return result.generateResponse();
}
// Parse the partner list and get the User objects.
NSMutableArray<User> partners = new NSMutableArray<User>();
NSMutableArray<String> partnersNotFound = new NSMutableArray<String>();
if (partnerList != null)
{
String[] usernames = partnerList.split("[,\\s]+");
for (String username : usernames)
{
username = username.trim();
if (username.length() > 0)
{
User partner = User.userWithDomainAndName(
ec, session.user().authenticationDomain(), username);
if (partner != null)
{
partners.addObject(partner);
}
else
{
partnersNotFound.addObject(username);
}
}
}
}
result.partnersNotFound = partnersNotFound;
result.startSubmission(currentSubNo, result.user());
result.submissionInProcess().setPartners(partners);
result.submissionInProcess().setUploadedFile(file);
result.submissionInProcess().setUploadedFileName(fileName);
int len = 0;
try
{
len = file.length();
}
catch (Exception e)
{
// Ignore it: length() could produce an NPE on a bad POST request
}
if (len == 0)
{
result.clearSubmission();
result.submissionInProcess().clearUpload();
String msg = "Your file submission is empty. "
+ "Please choose an appropriate file.";
result.errorMessages.add(msg);
log.warn(msg + " User = " + session.user()
+ "\n\t" + assignment);
return result.generateResponse();
}
else if (len > assignment.assignment().submissionProfile()
.effectiveMaxFileUploadSize())
{
result.clearSubmission();
result.submissionInProcess().clearUpload();
String msg = "Your file exceeds the file size limit for "
+ "this assignment ("
+ assignment.assignment().submissionProfile()
.effectiveMaxFileUploadSize()
+ "). Please choose a smaller file.";
result.errorMessages.add(msg);
log.warn(msg + " User = " + session.user()
+ "\n\t" + assignment);
return result.generateResponse();
}
try
{
String msg = result.commitSubmission(context, currentTime);
if (msg != null)
{
log.warn(msg + " User = " + session.user()
+ "\n\t" + assignment);
result.errorMessages.add(msg);
}
}
catch (Exception e)
{
new UnexpectedExceptionMessage(e, context, null, null)
.send();
result.clearSubmission();
result.submissionInProcess().clearUpload();
result.cancelLocalChanges();
String msg =
"An unexpected exception occurred while trying to commit "
+ "your submission. The error has been reported to the "
+ "Web-CAT administrator. Please try your submission again.";
result.errorMessages.add(msg);
log.error(msg + " User = " + session.user()
+ "\n\t" + assignment, e);
result.criticalError = true;
}
log.debug("handleSubmission() returning");
return result.generateResponse();
}
// ----------------------------------------------------------
/**
* Handle a direct action request. The user's login session will be
* passed in as well.
*
* @param request the request to respond to
* @param session the user's session
* @param context the context for the request
* @return The response page or contents
*/
public WOActionResults handleReport(
WORequest request,
Session session,
WOContext context )
{
log.debug( "handleReport()" );
WOActionResults result = null;
GraderComponent genericGComp =
(GraderComponent)Application.application().pageWithName(
PickCourseEnrolledPage.class.getName(), context);
if ( genericGComp.wcSession().primeUser() == null
|| genericGComp.prefs().submission() == null )
{
result = Application.wcApplication().gotoLoginPage(context);
}
else
{
// result = Application.application().pageWithName(
// session.tabs.selectById( "MostRecent" ).pageName(),
// context ).generateResponse();
result = genericGComp.pageWithName(
session.tabs.selectById( "MostRecent" ).pageName())
.generateResponse();
}
log.debug( "handleReport() returning" );
return result;
}
// ----------------------------------------------------------
/**
* A class filled with information designed for use with KVC,
* generated by {@link #storageStatus()}.
*/
public static class StorageStatus
{
// ----------------------------------------------------------
public static StorageStatus instance()
{
if (instance == null)
{
instance = new StorageStatus();
}
return instance;
}
// ----------------------------------------------------------
public String store()
{
return storageRoot == null
? null
: storageRoot.toString();
}
// ----------------------------------------------------------
public long totalSpace()
{
return storageRoot == null
? 0L
: storageRoot.getTotalSpace();
}
// ----------------------------------------------------------
public long usableSpace()
{
return storageRoot == null
? 0L
: storageRoot.getUsableSpace();
}
// ----------------------------------------------------------
public int usedSpacePct()
{
return 100 - usableSpacePct();
}
// ----------------------------------------------------------
public int usableSpacePct()
{
if (storageRoot == null)
{
return 0;
}
else
{
int result = (int)(
((double)storageRoot.getUsableSpace())
/ ((double)storageRoot.getTotalSpace())
* 100.0 + 0.5);
if (result > 100)
{
result = 100;
}
return result;
}
}
// ----------------------------------------------------------
public long burnRatePerHour()
{
if (storageRoot == null)
{
return 0L;
}
nowRemaining = storageRoot.getUsableSpace();
nowTime = new NSTimestamp();
long deltaSize = nowRemaining - firstRemaining;
if (deltaSize < 0L)
{
deltaSize = 0L;
}
long deltaT = nowTime.getTime() - firstTime.getTime();
if (deltaT <= 0L)
{
return 0L;
}
double rate = deltaSize / (deltaT/3600000.0);
return (long)(rate + 0.5);
}
// ----------------------------------------------------------
public double predictedHoursRemaining()
{
long rate = burnRatePerHour();
if (rate <= 0L)
{
return 8760.0; // one year :-)
}
return nowRemaining / (double)rate;
}
// ----------------------------------------------------------
public boolean alert()
{
return predictedHoursRemaining() < 48.0
|| nowRemaining < MIN_SPACE;
}
// ----------------------------------------------------------
private StorageStatus()
{
String fileName = Application.configurationProperties()
.getProperty("grader.submissiondir");
if (fileName == null)
{
log.error("Cannot find Grader storage area "
+ "(property grader.submissiondir)");
}
else
{
storageRoot = new File(fileName);
try
{
while (storageRoot != null
&& storageRoot.getTotalSpace() == 0L)
{
storageRoot = storageRoot.getParentFile();
}
if (storageRoot != null
&& storageRoot.getTotalSpace() == 0L)
{
log.error(
"Cannot get device space on file " + storageRoot);
storageRoot = null;
}
}
catch (SecurityException e)
{
log.error("Encountered security exception trying to "
+ "identify storage partition using " + storageRoot,
e);
storageRoot = null;
}
}
if (storageRoot != null)
{
firstRemaining = storageRoot.getUsableSpace();
firstTime = new NSTimestamp();
log.info("Total space on " + storageRoot + ": "
+ storageRoot.getTotalSpace());
log.info("Free space on " + storageRoot + ": "
+ storageRoot.getFreeSpace());
log.info("Usable space on " + storageRoot + ": "
+ firstRemaining);
}
}
// ----------------------------------------------------------
private static StorageStatus instance;
private static final long MIN_SPACE = 1000000000; // approx 1 GB
private File storageRoot;
private NSTimestamp firstTime;
private long firstRemaining;
private NSTimestamp nowTime;
private long nowRemaining;
// private boolean alertSent;
}
// ----------------------------------------------------------
@Override
protected void performPeriodicMaintenance()
{
run(new ECAction() {
// ----------------------------------------------------------
@Override
public void action()
{
WCFetchSpecification<Submission> needsMigration =
new WCFetchSpecification<Submission>(
Submission.ENTITY_NAME,
Submission.isSubmissionForGrading.isNull(),
Submission.submitTime.descs());
needsMigration.setRefreshesRefetchedObjects(false);
needsMigration.setFetchLimit(500);
NSArray<Submission> migrated = Submission
.objectsWithFetchSpecification(ec, needsMigration);
while (migrated.size() > 0)
{
log.info("performPeriodicMaintenance(): migrated "
+ migrated.size() + " submissions");
try
{
// Sleep for 2 seconds
Thread.sleep(2000);
}
catch (InterruptedException e)
{
// ignore
}
migrated = Submission
.objectsWithFetchSpecification(ec, needsMigration);
}
}
});
}
//~ Instance/static variables .............................................
/**
* This is a reference to the single instance of this class, representing
* this subsystem. It is initialized by the constructor.
*/
private static Grader instance;
/** this is the main single grader queue */
private static GraderQueue graderQueue;
/** this is the queue processor for processing grader jobs */
private static GraderQueueProcessor graderQueueProcessor;
static Logger log = Logger.getLogger( Grader.class );
}