/*==========================================================================*\
| $Id: GradingPlugin.java,v 1.14 2012/05/09 16:32:42 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.eocontrol.*;
import com.webobjects.foundation.*;
import er.extensions.foundation.ERXValueUtilities;
import java.io.*;
import java.util.*;
import net.sf.webcat.FeatureDescriptor;
import net.sf.webcat.FeatureProvider;
import org.webcat.core.MutableDictionary;
import org.apache.log4j.Logger;
import org.webcat.core.*;
import org.webcat.woextensions.ECAction;
import static org.webcat.woextensions.ECAction.run;
// -------------------------------------------------------------------------
/**
* Represents an uploaded grading plug-in.
*
* @author Stephen Edwards
* @author Last changed by $Author: stedwar2 $
* @version $Revision: 1.14 $, $Date: 2012/05/09 16:32:42 $
*/
public class GradingPlugin
extends _GradingPlugin
{
//~ Constructors ..........................................................
// ----------------------------------------------------------
/**
* Creates a new ScriptFile object.
*/
public GradingPlugin()
{
super();
}
//~ Constants .............................................................
public static final String NO_AUTO_UPDATE_KEY =
"grader.willNotAutoUpdatePlugins";
public static final String NO_AUTO_INSTALL_KEY =
"grader.willNotAutoInstallPlugins";
public static final String AUTO_PUBLISH_KEY =
"autoPublish";
//~ Public Methods ........................................................
// ----------------------------------------------------------
/**
* Determine whether or not this script is stored in its own
* subdirectory, or as a single file.
* @return true if there is a subdirectory for this script
*/
public boolean hasSubdir()
{
return subdirName() != null;
}
// ----------------------------------------------------------
/**
* Retrieve the name of the directory where this script is stored.
* @return the directory name
*/
public String dirName()
{
StringBuffer dir = userScriptDirName( author(), isConfigFile() );
if ( hasSubdir() )
{
dir.append( '/' );
dir.append( subdirName() );
}
return dir.toString();
}
// ----------------------------------------------------------
/**
* Returns the directory where the plug-in's public resources are stored.
*
* @return the plug-in's public resources directory
*/
public File publicResourcesDir()
{
return new File(dirName(), "public");
}
// ----------------------------------------------------------
/**
* Retrieve the path name for this script's entry point--its main
* executable file.
* @return the path to the main file
*/
public String mainFilePath()
{
String myName = null;
@SuppressWarnings("unchecked")
NSDictionary<String, String> config = configDescription();
if (config != null)
{
myName = config.objectForKey("executable");
}
if (myName == null)
{
myName = mainFileName();
}
return dirName() + "/" + myName;
}
// ----------------------------------------------------------
/**
* Retrieve the path name for this script's configuration description.
* @return the path to the config.plist file
*/
public String configPlistFilePath()
{
return dirName() + "/config.plist";
}
// ----------------------------------------------------------
/**
* Retrieve the path to the public resource with the specified relative
* path in the public resources directory. If the path is invalid (for
* example, it tries to navigate to a parent directory), then null is
* returned.
*
* @param path the relative path to the resource
* @return the File object that represents the file, or null if the path
* was invalid
*/
public File fileForPublicResourceAtPath(String path)
{
if (path == null)
{
return publicResourcesDir();
}
else
{
File file = new File(publicResourcesDir(), path);
try
{
if (file.getCanonicalPath().startsWith(
publicResourcesDir().getCanonicalPath()))
{
return file;
}
}
catch (IOException e)
{
log.error("An error occurred while retrieving the canonical "
+ "path of the file " + file.toString());
}
return null;
}
}
// ----------------------------------------------------------
/**
* Execute this script with the given command line argument(s).
*
* @param args the arguments to pass to the script on the command line
* @param cwd the working directory to use
* @throws java.io.IOException if one occurs
* @throws InterruptedException if one occurs
*/
public void execute( String args, File cwd )
throws java.io.IOException, InterruptedException
{
if ( log.isDebugEnabled() )
{
log.debug( "execute(): args = '" + args + "', cwd = " + cwd );
}
String command = "";
if ( configDescription().containsKey( "interpreter.prefix" ) )
{
// Look up the associated value, perform property substitution
// on it, and add it before the main file name
command = command
+ Application.configurationProperties().
substitutePropertyReferences(
configDescription().valueForKey( "interpreter.prefix" )
.toString()
)
+ " ";
}
command += mainFilePath();
if ( args != null )
{
command = command + " " + args;
}
Application.wcApplication().executeExternalCommand( command, cwd );
}
// ----------------------------------------------------------
/**
* Get a short (no longer than 60 characters) description of this plug-in,
* which currently returns {@link #name()}.
* @return the description
*/
public String userPresentableDescription()
{
return name();
}
// ----------------------------------------------------------
public String displayableName()
{
Object nameObj = configDescription().valueForKey("displayableName");
String myName = (nameObj == null)
? name()
: nameObj.toString();
if (myName == null)
{
myName = uploadedFileName();
}
return myName;
}
// ----------------------------------------------------------
/**
* If this script's config.plist file has been modified, then
* reparse it and store its config information.
*/
public void reinitializeConfigAttributesIfNecessary()
{
if ( hasSubdir() )
{
File configPlist = new File( configPlistFilePath() );
NSTimestamp lastRead = lastModified();
NSTimestamp modified =
new NSTimestamp( configPlist.lastModified() );
// The check against fileConfigSettings that has been added is to
// migrate older plugins.
if (lastRead != null && modified.after(lastRead)
|| storedValueForKey("fileConfigSettings") == null)
{
// silently swallow returned message
initializeConfigAttributes( configPlist );
}
}
}
// ----------------------------------------------------------
/**
* Parse this script's config.plist file and store its config information.
* @return null on success, or an error message if parsing failed
*/
public String initializeConfigAttributes()
{
if ( hasSubdir() )
{
return initializeConfigAttributes(
new File( configPlistFilePath() ) );
}
return null;
}
// ----------------------------------------------------------
/**
* Parse this script's config.plist file and store its config information.
* @param configPlist the config.plist file to parse
* @return null on success, or an error message if parsing failed
*/
public String initializeConfigAttributes( File configPlist )
{
// reset the cached descriptor, if any
descriptor = null;
if ( configPlist.exists() )
{
try
{
log.debug( "reloading " + configPlist.getCanonicalPath() );
}
catch ( IOException e )
{
log.error( "error attempting to load confg.plist file for "
+ this, e );
}
try
{
MutableDictionary dict =
MutableDictionary.fromPropertyList( configPlist );
setConfigDescription( dict );
// log.debug( "script config.plist = " + dict );
String dictName = (String)dict.objectForKey( "name" );
setName( dictName );
MutableDictionary fileSettings = new MutableDictionary();
NSArray<?> globalOptions = (NSArray<?>)dict.objectForKey(
"globalOptions");
if (globalOptions != null)
{
addFilePropertiesToDictionary(fileSettings, globalOptions);
}
NSArray<?> assignmentOptions = (NSArray<?>)dict.objectForKey(
"assignmentOptions");
if (assignmentOptions != null)
{
addFilePropertiesToDictionary(fileSettings,
assignmentOptions);
}
NSArray<?> options = (NSArray<?>)dict.objectForKey("options");
if (options != null)
{
addFilePropertiesToDictionary(fileSettings, options);
MutableDictionary defaults = new MutableDictionary();
for ( int i = 0; i < options.count(); i++ )
{
@SuppressWarnings("unchecked")
NSDictionary<String, ?> thisOption =
(NSDictionary<String, ?>)options
.objectAtIndex( i );
// log.debug( "this option = " + thisOption );
if ( thisOption.objectForKey( "disable" ) == null )
{
String property = (String)thisOption
.objectForKey( "property" );
Object value = thisOption
.objectForKey( "default" );
if ( property != null && value != null )
{
defaults.setObjectForKey( value, property );
}
String type = (String) thisOption
.objectForKey("type");
if (type.equals("file"))
{
fileSettings.setObjectForKey(false, property);
}
else if (type.equals("fileOrDir"))
{
fileSettings.setObjectForKey(true, property);
}
}
}
setDefaultConfigSettings(defaults);
}
else
{
setDefaultConfigSettings(null);
}
setFileConfigSettings(fileSettings);
setLastModified(
new NSTimestamp( configPlist.lastModified() ) );
if ( ERXValueUtilities.booleanValue(
configDescription().get( AUTO_PUBLISH_KEY ) ) )
{
setIsPublished( true );
}
}
catch ( Exception e )
{
return e.getMessage()
+ " (error reading script's config.plist file)";
}
}
else
{
return "This script is missing its 'config.plist' file.";
}
return null;
}
// ----------------------------------------------------------
private void addFilePropertiesToDictionary(MutableDictionary fileProps,
NSArray<?> options)
{
@SuppressWarnings("unchecked")
NSArray<NSDictionary<String, ?>> optionsDict =
(NSArray<NSDictionary<String, ?>>) options;
for (NSDictionary<String, ?> option : optionsDict)
{
if (option.objectForKey("disable") == null)
{
String property = (String) option.objectForKey("property");
String type = (String) option.objectForKey("type");
if (type.equalsIgnoreCase("file"))
{
fileProps.setObjectForKey(false, property);
}
else if (type.equalsIgnoreCase("fileOrDir"))
{
fileProps.setObjectForKey(true, property);
}
}
}
}
// ----------------------------------------------------------
/**
* Retrieve the configured timeout multiplier for this script file.
* @return the timeout multiplier (scale factor)
*/
public int timeoutMultiplier()
{
return ERXValueUtilities.intValueWithDefault(
configDescription().valueForKey( "timeoutMultiplier" ), 1 );
}
// ----------------------------------------------------------
/**
* Retrieve the configured timeout internal padding for this script file.
* @return the timeout internal padding (in seconds)
*/
public int timeoutInternalPadding()
{
return ERXValueUtilities.intValueWithDefault(
configDescription().valueForKey( "timeoutInternalPadding" ), 0 );
}
// ----------------------------------------------------------
/**
* Get the FeatureDescriptor for this plugin.
* @return this plug-in's descriptor
*/
public FeatureDescriptor descriptor()
{
if ( descriptor == null )
{
descriptor = new PluginDescriptor( this );
}
return descriptor;
}
// ----------------------------------------------------------
/**
* Download this plug-in's latest file from its provider on-line
* and install it for the given user, overwriting any existing
* version.
* @return null on success, or an error message on failure
*/
public String installUpdate()
{
return installOrUpdate(
author(), descriptor().providerVersion(), true, this );
}
// ----------------------------------------------------------
/**
* Download the specified plug-in file and install it for the given
* user. If the download succeeds, a new ScriptFile object will be
* created under the specified user object's editing context. The
* new ScriptFile object is not returned, by can be retrieved after
* comitting the user object's editing context and refetching.
* @param installedBy the user
* @param plugin the plug-in to download
* @param overwrite true if the named plug-in already exists in the
* user's directory
* @return null on success, or an error message on failure
*/
public static String installOrUpdate(
User installedBy,
net.sf.webcat.FeatureDescriptor plugin,
boolean overwrite )
{
return installOrUpdate( installedBy, plugin, overwrite, null );
}
// ----------------------------------------------------------
/**
* Automatically update any installed scripts if auto-updates are
* enabled, and automatically install any new plug-ins, if
* auto-installation is enabled.
*/
public static void autoUpdateAndInstall()
{
run(new ECAction() { public void action() {
autoInstallNewPlugins(ec, autoUpdatePlugins(ec));
}});
}
// ----------------------------------------------------------
/**
* Retrieve the name of the directory where a user's scripts are stored.
* @param pluginAuthor the user
* @param isData true if this is the directory for a script data/config
* file, or false if this is the directory where scripts
* themselves are stored
* @return the directory name
*/
public static StringBuffer userScriptDirName(
User pluginAuthor, boolean isData)
{
StringBuffer dir = new StringBuffer(50);
dir.append(isData ? scriptDataRoot() : scriptRoot());
dir.append('/');
dir.append(pluginAuthor.authenticationDomain().subdirName());
dir.append('/');
dir.append(pluginAuthor.userName());
return dir;
}
// ----------------------------------------------------------
/**
* Retrieve the name of the directory where this script is stored.
* @param fileName the script's file name
* @return the subdirectory name based on the uploaded file name, with
* all dots replaced by underscores
*/
public static String convertToSubdirName( String fileName )
{
return fileName.replace( '.', '_' ).replace( ' ', '-' );
}
// ----------------------------------------------------------
/**
* Create a new script file object from uploaded file data.
* @param ec the editing context in which to add the new object
* @param pluginAuthor the user uploading the script
* @param uploadedName the script's file name
* @param uploadedData the file's data
* @param isData true if this is a script data/config file, or
* false if this is a script itself
* @param expand true if zip/jar files should be expanded, or false
* otherwise
* @param errors a dictionary in which to store any error messages
* for display to the user
* @return the new script file, if successful, or null if unsuccessful
*/
public static GradingPlugin createNewGradingPlugin(
EOEditingContext ec,
User pluginAuthor,
String uploadedName,
NSData uploadedData,
boolean isData,
boolean expand,
NSMutableDictionary<String, Object> errors
)
{
String userScriptDir = userScriptDirName( pluginAuthor, isData ).toString();
String newSubdirName = null;
uploadedName = ( new File( uploadedName ) ).getName();
String uploadedNameLC = uploadedName.toLowerCase();
File toLookFor;
if ( expand && ( uploadedNameLC.endsWith( ".zip" ) ||
uploadedNameLC.endsWith( ".jar" ) ) )
{
newSubdirName = GradingPlugin.convertToSubdirName( uploadedName );
toLookFor = new File( userScriptDir + "/" + newSubdirName );
}
else
{
toLookFor = new File( userScriptDir + "/" + uploadedName );
}
if ( toLookFor.exists() )
{
String msg = "You already have an uploaded script with this "
+ "name. If you want to change that script's "
+ "files, then edit its configuration. "
+ "Otherwise, please use a different file name "
+ "for this new script.";
errors.setObjectForKey( msg, msg );
return null;
}
GradingPlugin gradingPlugin = new GradingPlugin();
ec.insertObject( gradingPlugin );
gradingPlugin.setUploadedFileName( uploadedName );
gradingPlugin.setMainFileName( uploadedName );
gradingPlugin.setLastModified( new NSTimestamp() );
gradingPlugin.setAuthorRelationship( pluginAuthor );
// Save the file to disk
log.debug( "saving to file " + gradingPlugin.mainFilePath() );
File pluginPath = new File( gradingPlugin.mainFilePath() );
try
{
pluginPath.getParentFile().mkdirs();
FileOutputStream out = new FileOutputStream( pluginPath );
uploadedData.writeToStream( out );
out.close();
}
catch ( java.io.IOException e )
{
String msg = e.getMessage();
errors.setObjectForKey( msg, msg );
ec.deleteObject( gradingPlugin );
pluginPath.delete();
return null;
}
if ( expand && ( uploadedNameLC.endsWith( ".zip" ) ||
uploadedNameLC.endsWith( ".jar" ) ) )
{
try
{
//ZipFile zip = new ZipFile( script.mainFilePath() );
gradingPlugin.setSubdirName( newSubdirName );
log.debug( "unzipping to " + gradingPlugin.dirName() );
org.webcat.archives.ArchiveManager.getInstance()
.unpack( new File( gradingPlugin.dirName() ), pluginPath );
//Grader.unZip( zip, new File( script.dirName() ) );
//zip.close();
pluginPath.delete();
}
catch ( java.io.IOException e )
{
String msg = e.getMessage();
errors.setObjectForKey( msg, msg );
gradingPlugin.setSubdirName( newSubdirName );
org.webcat.core.FileUtilities
.deleteDirectory( gradingPlugin.dirName() );
pluginPath.delete();
log.warn( "error unzipping:", e );
// throw new NSForwardException( e );
ec.deleteObject( gradingPlugin );
return null;
}
gradingPlugin.setMainFileName( null );
String msg = gradingPlugin.initializeConfigAttributes();
if ( msg != null )
{
errors.setObjectForKey( msg, msg );
}
}
return gradingPlugin;
}
// ----------------------------------------------------------
/**
* Retrieve the name of the directory where all user scripts are stored.
* @return the directory name
*/
public static String scriptRoot()
{
if ( scriptRoot == null )
{
scriptRoot = org.webcat.core.Application
.configurationProperties().getProperty( "grader.scriptsroot" );
if ( scriptRoot == null )
{
scriptRoot = org.webcat.core.Application
.configurationProperties()
.getProperty( "grader.submissiondir" )
+ "/UserScripts";
}
}
return scriptRoot;
}
// ----------------------------------------------------------
/**
* Retrieve the name of the directory where all user script config/data
* files are stored.
* @return the directory name
*/
public static String scriptDataRoot()
{
if ( scriptDataRoot == null )
{
scriptDataRoot = org.webcat.core.Application
.configurationProperties()
.getProperty( "grader.scriptsdataroot" );
if ( scriptDataRoot == null )
{
scriptDataRoot = scriptRoot() + "Data";
}
}
return scriptDataRoot;
}
//~ Private Methods .......................................................
// ----------------------------------------------------------
/**
* Download the specified plug-in file and install it for the given
* user. If the download succeeds, the given ScriptFile object
* will be updated appropriately. If none is provided, a new ScriptFile
* object will be created under the specified user object's editing
* context. The new ScriptFile object is not returned, by can be
* retrieved after comitting the user object's editing context and
* refetching.
* @param installedBy the user
* @param plugin the plug-in to download
* @param overwrite true if the named plug-in already exists in the
* user's directory
* @param scriptFile the ScriptFile object to update (or null to force
* creation of a new one)
* @return null on success, or an error message on failure
*/
private static String installOrUpdate(
User installedBy,
net.sf.webcat.FeatureDescriptor plugin,
boolean overwrite,
GradingPlugin scriptFile )
{
if ( scriptFile != null && !scriptFile.hasSubdir() )
{
return "Installed plug-in does not support downloads!";
}
GradingPlugin newScriptFile = null;
String pluginSubdirName = convertToSubdirName( plugin.name() );
File newScriptPath = null;
if ( scriptFile == null )
{
newScriptFile = new GradingPlugin();
installedBy.editingContext().insertObject( newScriptFile );
newScriptFile.setLastModified( new NSTimestamp() );
newScriptFile.setAuthorRelationship( installedBy );
newScriptFile.setSubdirName( pluginSubdirName );
scriptFile = newScriptFile;
}
else if ( !pluginSubdirName.equals( scriptFile.subdirName() ) )
{
newScriptPath = new File (
userScriptDirName( installedBy, false ).toString(),
pluginSubdirName );
if ( newScriptPath.exists() )
{
return "The plug-in you are updating has changed names, but "
+ "you already have an installed plug-in with the new "
+ "name, so there is a conflict. The original plug-in "
+ "cannot be updated until the name conflict is resolved.";
}
}
File pluginSubdir = new File( scriptFile.dirName() );
if ( pluginSubdir.exists() )
{
log.debug(
"directory " + pluginSubdir.getAbsolutePath() + " exists" );
if ( overwrite )
{
org.webcat.core.FileUtilities
.deleteDirectory( pluginSubdir );
}
else
{
if ( newScriptFile != null )
{
newScriptFile.editingContext()
.deleteObject( newScriptFile );
}
return "You already have an installed plug-in with this name."
+ " If you want to change that script's files, then "
+ " use its browse/edit action icon instead.";
}
}
// Save the file to disk
log.debug( "downloading plug-in archive" );
if ( newScriptPath == null )
{
newScriptPath = new File( scriptFile.dirName() );
}
else
{
scriptFile.setSubdirName( pluginSubdirName );
}
File downloadPath = newScriptPath.getParentFile();
File archiveFile = new File( downloadPath.getAbsolutePath()
+ "/" + plugin.name() + "_" + plugin.currentVersion() + ".jar" );
downloadPath.mkdirs();
plugin.downloadTo( downloadPath );
try
{
org.webcat.archives.ArchiveManager.getInstance()
.unpack( newScriptPath, archiveFile );
}
catch ( java.io.IOException e )
{
if ( newScriptFile != null )
{
newScriptFile.editingContext()
.deleteObject( newScriptFile );
}
return e.getMessage();
}
archiveFile.delete();
String msg = scriptFile.initializeConfigAttributes();
if ( msg != null )
{
if ( newScriptFile != null )
{
newScriptFile.editingContext()
.deleteObject( newScriptFile );
}
}
return msg;
}
// ----------------------------------------------------------
private static NSArray<GradingPlugin> autoUpdatePlugins(
EOEditingContext ec)
{
NSArray<GradingPlugin> pluginList = allObjects(ec);
if (!Application.configurationProperties()
.booleanForKey(NO_AUTO_UPDATE_KEY))
{
for (GradingPlugin plugin : pluginList)
{
try
{
if (plugin.descriptor().updateIsAvailable())
{
log.info("Updating plug-in: \"" + plugin.name() + "\"");
String msg = plugin.installUpdate();
if (msg != null)
{
log.error("Error updating plug-in \""
+ plugin.name() + "\": " + msg);
}
ec.saveChanges();
}
else
{
log.debug("Plug-in \"" + plugin.name()
+ "\" is up to date.");
}
}
catch (IOException e)
{
log.error("Error checking for updates to plug-in \""
+ plugin.name() + "\": " + e);
}
}
}
return pluginList;
}
// ----------------------------------------------------------
private static void autoInstallNewPlugins(
EOEditingContext ec, NSArray<GradingPlugin> pluginList)
{
if (Application.configurationProperties()
.booleanForKey(NO_AUTO_INSTALL_KEY))
{
return;
}
String adminUserName = Application.configurationProperties()
.getProperty("AdminUsername");
if (adminUserName == null)
{
log.error("No definition for 'AdminUsername' config property!\n"
+ "Cannot install new plug-ins without admin user name.");
return;
}
User admin = null;
NSArray<User> candidates = User.objectsMatchingQualifier(ec,
User.userName.eq(adminUserName));
for (User user : candidates)
{
if (user.hasAdminPrivileges())
{
if (admin == null)
{
admin = user;
}
else
{
log.warn( "Duplicate admin accounts with user name \""
+ adminUserName + "\" found. Using " + admin
+ ", ignoring " + user);
}
}
}
if (admin == null)
{
log.error("Cannot find admin account with user name \""
+ adminUserName + "\"!");
return;
}
Collection<FeatureDescriptor> availablePlugins =
new HashSet<FeatureDescriptor>();
for (FeatureProvider provider : FeatureProvider.providers())
{
if (provider != null)
{
availablePlugins.addAll(provider.plugins());
}
}
if (pluginList != null)
{
for (GradingPlugin s : pluginList)
{
FeatureDescriptor fd = s.descriptor().providerVersion();
if (fd != null)
{
if (availablePlugins.size() > 0
&& !availablePlugins.remove(fd))
{
Iterator<FeatureDescriptor> available =
availablePlugins.iterator();
while (available.hasNext())
{
FeatureDescriptor candidate = available.next();
if (candidate.name() == null
|| candidate.name().equals(fd.name()))
{
available.remove();
}
}
}
}
}
}
for (FeatureDescriptor plugin : availablePlugins)
{
if (plugin.getProperty("batchEntity") == null)
{
log.info("Installing new plug-in: \"" + plugin.name() + "\"");
String msg = installOrUpdate(admin, plugin, false, null);
if (msg != null)
{
log.error("Error installing new plug-in \""
+ plugin.name() + "\": " + msg);
}
ec.saveChanges();
}
}
}
//~ Instance/static variables .............................................
private PluginDescriptor descriptor;
static private String scriptRoot = null;
static private String scriptDataRoot = null;
static Logger log = Logger.getLogger(GradingPlugin.class);
}