/*==========================================================================*\
| $Id: BatchPlugin.java,v 1.8 2012/03/07 03:21:13 stedwar2 Exp $
|*-------------------------------------------------------------------------*|
| Copyright (C) 2010-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.batchprocessor;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import net.sf.webcat.FeatureDescriptor;
import net.sf.webcat.FeatureProvider;
import org.webcat.core.Application;
import org.webcat.core.MutableDictionary;
import org.webcat.core.User;
import org.webcat.woextensions.ECAction;
import static org.webcat.woextensions.ECAction.run;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSData;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation.NSTimestamp;
import er.extensions.foundation.ERXArrayUtilities;
import er.extensions.foundation.ERXValueUtilities;
// -------------------------------------------------------------------------
/**
* Represents an uploaded batch processing plug-in.
*
* @author Tony Allevato
* @author Last changed by $Author: stedwar2 $
* @version $Revision: 1.8 $, $Date: 2012/03/07 03:21:13 $
*/
public class BatchPlugin
extends _BatchPlugin
{
//~ Constructors ..........................................................
// ----------------------------------------------------------
/**
* Creates a new BatchPlugin object.
*/
public BatchPlugin()
{
super();
}
//~ Constants .............................................................
public static final String NO_AUTO_UPDATE_KEY =
"batchprocessor.willNotAutoUpdatePlugins";
public static final String NO_AUTO_INSTALL_KEY =
"batchprocessor.willNotAutoInstallPlugins";
public static final String AUTO_PUBLISH_KEY =
"autoPublish";
//~ Methods ...............................................................
// ----------------------------------------------------------
/**
* Determine whether or not this plug-in is stored in its own
* subdirectory, or as a single file.
* @return true if there is a subdirectory for this plug-in
*/
public boolean hasSubdir()
{
return subdirName() != null;
}
// ----------------------------------------------------------
/**
* Retrieve the name of the directory where this plug-in is stored.
* @return the directory name
*/
public String dirName()
{
StringBuffer dir = userPluginDirName(author(), false);
if (hasSubdir())
{
dir.append('/');
dir.append(subdirName());
}
return dir.toString();
}
// ----------------------------------------------------------
/**
* Retrieve the path name for this plug-in's entry point--its main
* executable file.
* @return the path to the main file
*/
public String mainFilePath()
{
String pluginName = null;
@SuppressWarnings("unchecked")
NSDictionary<String, Object> config = configDescription();
if (config != null)
{
pluginName = (String)config.objectForKey("executable");
}
if (pluginName == null)
{
pluginName = mainFileName();
}
return dirName() + "/" + pluginName;
}
// ----------------------------------------------------------
/**
* Retrieve the path name for this plug-in's configuration description.
* @return the path to the config.plist file
*/
public String configPlistFilePath()
{
return dirName() + "/config.plist";
}
// ----------------------------------------------------------
/**
* Execute this plug-in with the given command line argument(s).
*
* @param args the arguments to pass to the plug-in on the command line
* @param cwd the working directory to use
* @return the Java Process object representing the external process
*
* @throws java.io.IOException if one occurs
* @throws InterruptedException if one occurs
*/
public Process execute(String args, File cwd)
throws IOException
{
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;
}
return Application.wcApplication().executeExternalCommandAsync(
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 displayableName =
(nameObj == null) ? name() : nameObj.toString();
if (displayableName == null)
{
displayableName = uploadedFileName();
}
return displayableName;
}
// ----------------------------------------------------------
/**
* If this plug-in'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());
if (lastRead != null && modified.after(lastRead))
{
// silently swallow returned message
initializeConfigAttributes(configPlist);
}
}
}
// ----------------------------------------------------------
/**
* Parse this plug-in'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 plug-in'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);
String entity = (String)dict.objectForKey("batchEntity");
setBatchEntity(entity);
NSArray<?> options =
(NSArray<?>)dict.objectForKey( "options" );
// log.debug( "options = " + options );
if (options != null)
{
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);
}
}
}
setDefaultConfigSettings(defaults);
}
else
{
setDefaultConfigSettings(null);
}
setLastModified(
new NSTimestamp(configPlist.lastModified()));
if (ERXValueUtilities.booleanValue(
configDescription().get(AUTO_PUBLISH_KEY)))
{
setIsPublished(true);
}
}
catch (Exception e)
{
return e.getMessage()
+ " (error reading plug-in's config.plist file)";
}
}
else
{
return "This plug-in is missing its 'config.plist' file.";
}
return null;
}
// ----------------------------------------------------------
/**
* Get the FeatureDescriptor for this plug-in.
*
* @return this plug-in's descriptor
*/
public FeatureDescriptor descriptor()
{
if (descriptor == null)
{
descriptor = new BatchPluginDescriptor(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 BatchPlugin object will be
* created under the specified user object's editing context. The
* new BatchPlugin 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 plug-ins 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 plug-ins are stored.
* @param pluginAuthor the user
* @param isData true if this is the directory for a plug-in data/config
* file, or false if this is the directory where plug-ins
* themselves are stored
* @return the directory name
*/
public static StringBuffer userPluginDirName(
User pluginAuthor, boolean isData)
{
StringBuffer dir = new StringBuffer(50);
dir.append(isData ? pluginDataRoot() : pluginRoot());
dir.append('/');
dir.append(pluginAuthor.authenticationDomain().subdirName());
dir.append('/');
dir.append(pluginAuthor.userName());
return dir;
}
// ----------------------------------------------------------
/**
* Retrieve the name of the directory where this plug-in is stored.
* @param fileName the plug-in'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 plug-in file object from uploaded file data.
* @param ec the editing context in which to add the new object
* @param pluginAuthor the user uploading the plug-in
* @param uploadedName the plug-in's file name
* @param uploadedData the file's data
* @param isData true if this is a plug-in data/config file, or
* false if this is a plug-in 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 plug-in file, if successful, or null if unsuccessful
*/
public static BatchPlugin createNewBatchPlugin(
EOEditingContext ec,
User pluginAuthor,
String uploadedName,
NSData uploadedData,
boolean expand,
NSMutableDictionary<String, Object> errors)
{
String userPluginDir =
userPluginDirName(pluginAuthor, false).toString();
String newSubdirName = null;
uploadedName = (new File(uploadedName)).getName();
String uploadedNameLC = uploadedName.toLowerCase();
File toLookFor;
if (expand && (uploadedNameLC.endsWith(".zip") ||
uploadedNameLC.endsWith(".jar")))
{
newSubdirName = convertToSubdirName(uploadedName);
toLookFor = new File(userPluginDir + "/" + newSubdirName);
}
else
{
toLookFor = new File(userPluginDir + "/" + uploadedName);
}
if (toLookFor.exists())
{
String msg = "You already have an uploaded batch plug-in with "
+ "this name. If you want to change that plug-in's "
+ "files, then edit its configuration. Otherwise, please "
+ "use a different file name for this new plug-in.";
errors.setObjectForKey(msg, msg);
return null;
}
BatchPlugin batchPlugin = new BatchPlugin();
ec.insertObject(batchPlugin);
batchPlugin.setUploadedFileName(uploadedName);
batchPlugin.setMainFileName(uploadedName);
batchPlugin.setLastModified(new NSTimestamp());
batchPlugin.setAuthorRelationship(pluginAuthor);
// Save the file to disk
log.debug("saving to file " + batchPlugin.mainFilePath());
File pluginPath = new File(batchPlugin.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(batchPlugin);
pluginPath.delete();
return null;
}
if (expand && (uploadedNameLC.endsWith(".zip") ||
uploadedNameLC.endsWith(".jar")))
{
try
{
// ZipFile zip = new ZipFile(script.mainFilePath());
batchPlugin.setSubdirName(newSubdirName);
log.debug("unzipping to " + batchPlugin.dirName());
org.webcat.archives.ArchiveManager.getInstance()
.unpack(new File(batchPlugin.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);
batchPlugin.setSubdirName(newSubdirName);
org.webcat.core.FileUtilities
.deleteDirectory(batchPlugin.dirName());
pluginPath.delete();
log.warn("error unzipping:", e);
// throw new NSForwardException(e);
ec.deleteObject(batchPlugin);
return null;
}
batchPlugin.setMainFileName(null);
String msg = batchPlugin.initializeConfigAttributes();
if (msg != null)
{
errors.setObjectForKey(msg, msg);
}
}
return batchPlugin;
}
// ----------------------------------------------------------
/**
* Retrieve the name of the directory where all user plug-ins are stored.
* @return the directory name
*/
public static String pluginRoot()
{
if (pluginRoot == null)
{
pluginRoot = org.webcat.core.Application
.configurationProperties().getProperty("grader.scriptsroot");
if (pluginRoot == null)
{
pluginRoot = org.webcat.core.Application
.configurationProperties()
.getProperty("grader.submissiondir")
+ "/BatchPlugins";
}
}
return pluginRoot;
}
// ----------------------------------------------------------
/**
* Retrieve the name of the directory where all user plug-in config/data
* files are stored.
* @return the directory name
*/
public static String pluginDataRoot()
{
if (pluginDataRoot == null)
{
pluginDataRoot = org.webcat.core.Application
.configurationProperties()
.getProperty("grader.scriptsdataroot");
if (pluginDataRoot == null)
{
pluginDataRoot = pluginRoot() + "Data";
}
}
return pluginDataRoot;
}
// ----------------------------------------------------------
/**
* Gets the array of all report templates that are accessible by the
* specified user. This is the union of that user's own uploaded templates
* and all of the published templates.
*
* @param ec the editing context to load the templates into
* @param user the user
*
* @return the array of report templates accessible by the user
*/
public static NSArray<BatchPlugin> pluginsAccessibleByUser(
EOEditingContext ec, User user)
{
// Admins have access to everything, so just short-circuit this.
if (user.hasAdminPrivileges())
{
return allPluginsOrderedByName(ec);
}
NSMutableArray<BatchPlugin> allPlugins =
publishedPlugins(ec).mutableClone();
ERXArrayUtilities.addObjectsFromArrayWithoutDuplicates(
allPlugins, pluginsForUser(ec, user));
ERXArrayUtilities.sortArrayWithKey(allPlugins, BatchPlugin.NAME_KEY);
return allPlugins;
}
//~ Private Methods .......................................................
// ----------------------------------------------------------
/**
* Download the specified plug-in file and install it for the given
* user. If the download succeeds, the given BatchPlugin object
* will be updated appropriately. If none is provided, a new BatchPlugin
* object will be created under the specified user object's editing
* context. The new BatchPlugin 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 batchPlugin the BatchPlugin 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,
BatchPlugin batchPlugin)
{
if (batchPlugin != null && !batchPlugin.hasSubdir())
{
return "Installed plug-in does not support downloads!";
}
BatchPlugin newBatchPlugin = null;
String pluginSubdirName = convertToSubdirName(plugin.name());
File newBatchPluginPath = null;
if (batchPlugin == null)
{
newBatchPlugin = new BatchPlugin();
installedBy.editingContext().insertObject(newBatchPlugin);
newBatchPlugin.setLastModified(new NSTimestamp());
newBatchPlugin.setAuthorRelationship(installedBy);
newBatchPlugin.setSubdirName(pluginSubdirName);
batchPlugin = newBatchPlugin;
}
else if (!pluginSubdirName.equals(batchPlugin.subdirName()))
{
newBatchPluginPath = new File (
userPluginDirName(installedBy, false).toString(),
pluginSubdirName);
if (newBatchPluginPath.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(batchPlugin.dirName());
if (pluginSubdir.exists())
{
log.debug(
"directory " + pluginSubdir.getAbsolutePath() + " exists");
if (overwrite)
{
org.webcat.core.FileUtilities.deleteDirectory(pluginSubdir);
}
else
{
if (newBatchPlugin != null)
{
newBatchPlugin.editingContext()
.deleteObject(newBatchPlugin);
}
return "You already have an installed plug-in with this name."
+ " If you want to change that plug-in's files, then "
+ " use its browse/edit action icon instead.";
}
}
// Save the file to disk
log.debug("downloading plug-in archive");
if (newBatchPluginPath == null)
{
newBatchPluginPath = new File(batchPlugin.dirName());
}
else
{
batchPlugin.setSubdirName(pluginSubdirName);
}
File downloadPath = newBatchPluginPath.getParentFile();
File archiveFile = new File(downloadPath.getAbsolutePath()
+ "/" + plugin.name() + "_" + plugin.currentVersion() + ".jar");
downloadPath.mkdirs();
plugin.downloadTo(downloadPath);
try
{
org.webcat.archives.ArchiveManager.getInstance()
.unpack(newBatchPluginPath, archiveFile);
}
catch (java.io.IOException e)
{
if (newBatchPlugin != null)
{
newBatchPlugin.editingContext()
.deleteObject(newBatchPlugin);
}
return e.getMessage();
}
archiveFile.delete();
String msg = batchPlugin.initializeConfigAttributes();
if (msg != null)
{
if (newBatchPlugin != null)
{
newBatchPlugin.editingContext()
.deleteObject(newBatchPlugin);
}
}
return msg;
}
// ----------------------------------------------------------
private static NSArray<BatchPlugin> autoUpdatePlugins(EOEditingContext ec)
{
NSArray<BatchPlugin> pluginList = allObjects(ec);
if (!Application.configurationProperties()
.booleanForKey(NO_AUTO_UPDATE_KEY))
{
for (BatchPlugin 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<BatchPlugin> 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 (BatchPlugin 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();
}
}
}
//~ Static/instance variables .............................................
static private String pluginRoot = null;
static private String pluginDataRoot = null;
private BatchPluginDescriptor descriptor;
}