/*==========================================================================*\
| $Id: WCUpdater.java,v 1.2 2011/05/27 15:30:56 stedwar2 Exp $
|*-------------------------------------------------------------------------*|
| Copyright (C) 2006-2011 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 net.sf.webcat;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
//-------------------------------------------------------------------------
/**
* This class runs and handles the update creation for webcat. It runs
* a background process which will continually check for updates.
*
* @author Travis Bale
* @author Last changed by $Author: stedwar2 $
* @version $Revision: 1.2 $, $Date: 2011/05/27 15:30:56 $
*/
public class WCUpdater
{
//~ Instance/static variables .............................................
private static WCUpdater instance = null;
private File downloadDir;
private File stagingDir;
private File updateDir;
private File frameworkDir;
private File mainBundle;
private Map<File, SubsystemUpdater> subsystems =
new HashMap<File, SubsystemUpdater>();
private Map<String, SubsystemUpdater> subsystemsByName =
new HashMap<String, SubsystemUpdater>();
private Map<String, Condition> updateFileConditions =
new HashMap<String, Condition>();
private static final String FRAMEWORK_SUBDIR1 =
"/Contents/Frameworks/Library/Frameworks";
private static final String FRAMEWORK_SUBDIR2 =
"/Contents/Library/Frameworks";
private static final String DOWNLOAD_SUBDIR = "pending-downloads";
private static final String STAGING_SUBDIR = "complete-downloads";
private static final String UPDATE_SUBDIR = "pending-updates";
//~ Public Constants ......................................................
/**
* The possible states for an update that is to be downloaded.
*/
public enum Condition {
DOWNLOAD_PENDING,
DOWNLOAD_COMPLETE,
UPDATE_PENDING,
UP_TO_DATE,
UPDATE_IS_AVAILABLE,
UNAVAILABLE
}
//~ Constructors ..........................................................
// ----------------------------------------------------------
/**
* Constructor.
*
* Cannot be called directly. Must use getInstance().
*/
private WCUpdater()
{
//Exists to prevent instantiation
}
// ----------------------------------------------------------
/**
* Gets an instance of this updater.
* @return An instance of the updater
*/
public static WCUpdater getInstance()
{
if(instance == null)
{
instance = new WCUpdater();
}
return instance;
}
//~ Public Methods ........................................................
// ----------------------------------------------------------
/**
* Accessor for the download directory
* @return the download directory
*/
public File getDownloadDir()
{
return downloadDir;
}
// ----------------------------------------------------------
/**
* Accessor for the staging directory
* @return the staging directory
*/
public File getStagingDir()
{
return stagingDir;
}
// ----------------------------------------------------------
/**
* Accessor for the update directory
* @return the update directory
*/
public File getUpdateDir()
{
return updateDir;
}
// ----------------------------------------------------------
/**
* Accessor for the framework directory
* @return the framework directory
*/
public File getFrameworkDir()
{
return frameworkDir;
}
// ----------------------------------------------------------
/**
* Accessor for the main bundle
* @return the main bundle
*/
public File getMainBundle()
{
return mainBundle;
}
// ----------------------------------------------------------
/**
* Schedules and starts the update.
*/
public void startBackgroundUpdaterThread(long delay, long period)
{
TimerTask updateTask = new TimerTask()
{
public void run()
{
getInstance().createUpdate();
FeatureProvider.refreshProviderRegistry();
getInstance().refreshSubsystemUpdaters();
}
};
Timer timer = new Timer();
timer.schedule(updateTask, delay, period);
}
// ----------------------------------------------------------
/**
* Sets up the update directories and sets the current file conditions.
* @param webInfDir The WEB-INF directory file
*/
public void setup(File webInfDir)
{
//Create the Directories needed for the Updater
downloadDir = new File(webInfDir, DOWNLOAD_SUBDIR);
stagingDir = new File(webInfDir, STAGING_SUBDIR);
updateDir = new File(webInfDir, UPDATE_SUBDIR);
if (webInfDir.isDirectory())
{
for (File bundleSearchDir : webInfDir.listFiles())
{
if (bundleSearchDir.isDirectory()
&& bundleSearchDir.getName().endsWith(".woa"))
{
mainBundle = new File(bundleSearchDir, "Contents");
frameworkDir = new File(
bundleSearchDir.getAbsolutePath()
+ FRAMEWORK_SUBDIR1);
if (!frameworkDir.exists())
{
frameworkDir = new File(
bundleSearchDir.getAbsolutePath()
+ FRAMEWORK_SUBDIR2);
}
break;
}
}
}
if (!downloadDir.exists())
{
downloadDir.mkdirs();
}
if(!stagingDir.exists())
{
stagingDir.mkdirs();
}
if (!updateDir.exists())
{
updateDir.mkdirs();
}
//Load the Conditions
for(File jar : downloadDir.listFiles())
{
updateFileConditions.put(
jar.getName(), Condition.DOWNLOAD_PENDING);
}
for(File jar : stagingDir.listFiles())
{
updateFileConditions.put(
jar.getName(), Condition.DOWNLOAD_COMPLETE);
}
for(File jar : updateDir.listFiles())
{
updateFileConditions.put(
jar.getName(), Condition.UPDATE_PENDING);
}
}
// ----------------------------------------------------------
/**
* Access the collection of subsystems in this application.
* @return a collection of {@link SubsystemUpdater} objects representing
* the available subsystems
*/
public Collection<SubsystemUpdater> subsystems()
{
return subsystems.values();
}
// ----------------------------------------------------------
/**
* Get the {@link SubsystemUpdater} for the specified subsystem location.
* Creates a new updater on demand, if necessary.
* @param dir the subsystem location to look up
* @return the corresponding updater
*/
public SubsystemUpdater getUpdaterFor(File dir)
{
SubsystemUpdater updater = null;
if (dir != null)
{
updater = subsystems.get(dir);
if (updater == null)
{
updater = new SubsystemUpdater(dir);
subsystems.put(dir, updater);
if (updater.name() != null)
{
subsystemsByName.put(updater.name(), updater);
}
}
}
return updater;
}
// ----------------------------------------------------------
/**
* Returns the condition of the specified filename.
*
* @param filename The update file's name
* @return The condition of the specified file.
*/
public Condition getFileConditionFor(String filename)
{
return updateFileConditions.get(filename);
}
// ----------------------------------------------------------
/**
* Log an informational message. This implementation sends output
* to {@link System#out}.
* @param msg the message to log.
*/
public static void logInfo(String msg)
{
System.out.println( msg );
}
// ----------------------------------------------------------
/**
* Log an error message. This implementation sends output
* to {@link System#out}, but provides a hook so that subclasses
* can use Log4J (we don't use that here, so that the Log4J library
* can be dynamically updatable through subsystems).
* @param msg the message to log
*/
public static void logError(Class<?> reference, String msg)
{
String className = reference.getName();
int pos = className.lastIndexOf('.');
if (pos >= 0)
{
className = className.substring(pos + 1);
}
System.out.println(className + ": ERROR: " + msg);
}
// ----------------------------------------------------------
/**
* Log an error message. This implementation sends output
* to {@link System#out}, but provides a hook so that subclasses
* can use Log4J (we don't use that here, so that the Log4J library
* can be dynamically updatable through subsystems).
* @param msg the message to log
* @param exception an optional exception that goes with the message
*/
public static void logError(
Class<?> reference, String msg, Throwable exception)
{
logError(reference, msg);
System.out.println(exception);
}
//~ Private Methods .......................................................
// ----------------------------------------------------------
/**
* Creates an update by downloading the newest versions of the
* subsystems and preparing the update directory with an update
* that can be applied.
*/
private void createUpdate()
{
logInfo("Download all updates");
// Download the updates
boolean downloadSuccessful = downloadNewUpdates();
FileUtilities.deleteOlderFiles(stagingDir);
logInfo("\nMoving updates that can be applied to pending-updates "
+ "directory.");
// Prepare the update
if(downloadSuccessful)
{
prepareFullUpdate();
}
else
{
preparePartialUpdate();
}
FileUtilities.deleteOlderFiles(updateDir);
}
// ----------------------------------------------------------
/**
* If automatic updates are turned on, scan all current subsystems and
* download any new versions of update files that are available.
* @param aFrameworkDir The directory where all subsystems are located
* @param mainBundle The main bundle location
* @return true if no errors occurred while downloading
*/
private boolean downloadNewUpdates()
{
boolean successfulDownload = true;
for (File subdir : frameworkDir.listFiles())
{
if(!downloadUpdateIfNecessary(getUpdaterFor(subdir)))
{
successfulDownload = false;
}
}
// Now handle the application update, if available
if(!downloadUpdateIfNecessary( getUpdaterFor(mainBundle) ))
{
successfulDownload = false;
}
// Now check through existing subsystems and check for any required
// subsystems that are not yet installed
for (SubsystemUpdater thisUpdater : subsystems.values())
{
String requires = thisUpdater.getProperty("requires");
if (thisUpdater.providerVersion() != null)
{
requires =
thisUpdater.providerVersion().getProperty("requires");
}
else
{
logError(getClass(), "Unable to read from provider.");
}
if (requires != null)
{
for (String requiredSubsystem : requires.split( ",\\s*" ))
{
if (!subsystemsByName.containsKey(requiredSubsystem))
{
// A required subsystem is not present, so find it
// and download it
logInfo( "Installed subsystem "
+ thisUpdater.name() + " requires subsystem "
+ requiredSubsystem
+ ", which is not installed.");
// First, look in the subsystem's provider
FeatureDescriptor newSubsystem;
try
{
newSubsystem = thisUpdater.provider()
.subsystemDescriptor(requiredSubsystem);
}
catch (IOException e)
{
newSubsystem = null;
}
if (newSubsystem == null)
{
// OK, look in all providers for it
for (FeatureProvider fp :
FeatureProvider.providers())
{
newSubsystem = fp.subsystemDescriptor(
requiredSubsystem);
if (newSubsystem != null)
{
break;
}
}
}
if (newSubsystem == null)
{
logInfo("Cannot identify provider for subsystem "
+ requiredSubsystem);
successfulDownload = false;
}
else
{
try
{
String filename = newSubsystem.name + "_"
+ newSubsystem.currentVersion() + ".jar";
Condition fileCondition =
updateFileConditions.get(filename);
if (fileCondition !=
Condition.DOWNLOAD_COMPLETE
&& fileCondition !=
Condition.UPDATE_PENDING)
{
updateFileConditions.put(
filename, Condition.DOWNLOAD_PENDING);
newSubsystem.downloadTo(
downloadDir, stagingDir);
updateFileConditions.put(
filename, Condition.DOWNLOAD_COMPLETE);
}
else
{
logInfo(newSubsystem.name
+ " is already downloaded.");
}
}
catch (IOException c)
{
successfulDownload = false;
}
}
}
}
}
}
return successfulDownload;
}
// ----------------------------------------------------------
/**
* Check for any updates for the given subsystem, and download them.
* @param updater The {@link SubsystemUpdater} to download for
* @return true if download is successful
*/
private boolean downloadUpdateIfNecessary(SubsystemUpdater updater)
{
try
{
FeatureDescriptor latest = updater.providerVersion();
if (latest != null)
{
String filename =
latest.name + "_" + latest.currentVersion() + ".jar";
Condition fileCondition = updateFileConditions.get(filename);
if (fileCondition != Condition.DOWNLOAD_COMPLETE
&& fileCondition != Condition.UPDATE_PENDING)
{
updateFileConditions.put(
filename, Condition.DOWNLOAD_PENDING);
if (updater.downloadUpdateIfNecessary(
downloadDir, stagingDir))
{
updateFileConditions.put(
filename, Condition.DOWNLOAD_COMPLETE);
}
else
{
updateFileConditions.put(
filename, Condition.UP_TO_DATE);
}
}
else
{
logInfo(latest.name + " is already downloaded.");
}
}
}
catch (IOException e)
{
logError(getClass(), "Error occured during download." , e);
return false;
}
return true;
}
// ----------------------------------------------------------
/**
* Prepares for the update by moving all files from the staging
* directory to the update directory.
*/
private void prepareFullUpdate()
{
if (stagingDir.exists() && stagingDir.isDirectory())
{
for (File jar : stagingDir.listFiles())
{
for (String extension :
SubsystemUpdater.JAVA_ARCHIVE_EXTENSIONS)
{
if (jar.getName().endsWith(extension))
{
if(!jar.renameTo(new File(updateDir, jar.getName())))
{
logError(getClass(), "Unable to move "
+ jar.getName()
+ " from "
+ stagingDir.getAbsolutePath()
+ " to "
+ updateDir.getAbsolutePath());
}
else
{
updateFileConditions.put(
jar.getName(), Condition.UPDATE_PENDING);
logInfo("Moving "
+ jar.getName()
+ " from "
+ stagingDir.getAbsolutePath()
+ " to "
+ updateDir.getAbsolutePath());
}
}
}
}
}
}
// ----------------------------------------------------------
/**
* Prepares for the update by moving all files from the staging
* directory to the update directory. It only moves files that do
* not have requirements that have not been downloaded.
*/
private void preparePartialUpdate()
{
if (stagingDir.exists() && stagingDir.isDirectory())
{
File[] stagingFiles = stagingDir.listFiles();
for (int i = 0; i < stagingFiles.length; i++)
{
File jar = stagingFiles[i];
if (jar != null)
{
for (String extension :
SubsystemUpdater.JAVA_ARCHIVE_EXTENSIONS)
{
if (jar.getName().endsWith(extension))
{
String subsystemName = jar.getName();
subsystemName = subsystemName.substring(
0, subsystemName.indexOf("_"));
ArrayList<String> requires =
getRequirements(subsystemName);
ArrayList<Integer> positions =
new ArrayList<Integer>();
if (requires == null)
{
continue;
}
boolean canAddUpdate = true;
for (String requirement : requires)
{
Condition fileCondition =
updateFileConditions.get(requirement);
if (fileCondition ==
Condition.DOWNLOAD_COMPLETE)
{
for(int j = i + 1; j < stagingFiles.length;
j++)
{
String filename =
stagingFiles[j].getName();
if (requirement.equals(filename))
{
positions.add(j);
}
}
}
else if (
fileCondition != Condition.UPDATE_PENDING
&& fileCondition != Condition.UP_TO_DATE)
{
canAddUpdate = false;
break;
}
}
//Move file and requirements to updateDir
if (canAddUpdate)
{
if (!jar.renameTo(new File(
updateDir, jar.getName())))
{
logError(getClass(), "Unable to move "
+ jar.getName()
+ " from "
+ stagingDir.getAbsolutePath()
+ " to "
+ updateDir.getAbsolutePath());
}
else
{
updateFileConditions.put(
jar.getName(),
Condition.UPDATE_PENDING);
logInfo("Moving "
+ jar.getName()
+ " from "
+ stagingDir.getAbsolutePath()
+ " to "
+ updateDir.getAbsolutePath());
}
stagingFiles[i] = null;
for (int pos : positions)
{
if (stagingFiles[pos].renameTo(
new File(updateDir,
stagingFiles[pos].getName())))
{
logError(getClass(), "Unable to move "
+ stagingFiles[pos].getName()
+ " from "
+ stagingDir.getAbsolutePath()
+ " to "
+ updateDir.getAbsolutePath());
}
else
{
updateFileConditions.put(
stagingFiles[pos].getName(),
Condition.UPDATE_PENDING);
logInfo("Moving "
+ stagingFiles[pos].getName()
+ " from "
+ stagingDir.getAbsolutePath()
+ " to "
+ updateDir.getAbsolutePath());
}
stagingFiles[pos] = null;
}
}
}
}
}
}
}
}
// ----------------------------------------------------------
/**
* Refresh the subsystems collection so that it reflects the new
* updates (intended to be called after downloading/applying pending
* updates).
* @param aFrameworkDir The directory where all subsystems are located
* @param mainBundle The main bundle location
*/
private void refreshSubsystemUpdaters()
{
// Clear out old values
subsystems = new HashMap<File, SubsystemUpdater>();
subsystemsByName = new HashMap<String, SubsystemUpdater>();
updateFileConditions = new HashMap<String, Condition>();
// Look up the updater for each framework
for (File dir : frameworkDir.listFiles())
{
getUpdaterFor(dir);
}
// Now create the updater for the main bundle
getUpdaterFor(mainBundle);
// Load file conditions
for (File jar : downloadDir.listFiles())
{
updateFileConditions.put(
jar.getName(), Condition.DOWNLOAD_PENDING);
}
for (File jar : stagingDir.listFiles())
{
updateFileConditions.put(
jar.getName(), Condition.DOWNLOAD_COMPLETE);
}
for (File jar : updateDir.listFiles())
{
updateFileConditions.put(jar.getName(), Condition.UPDATE_PENDING);
}
}
// ----------------------------------------------------------
/**
* Gets a list of all required subsystems need by the specified subsystem.
*
* @param subsystem The name of the subsystem who's requirements we
* are checking.
* @return A list of a required subsystems. Returns null if no
* connection can be made with the provider.
*/
private ArrayList<String> getRequirements(String subsystem)
{
SubsystemUpdater updater = subsystemsByName.get(subsystem);
ArrayList<String> requires = new ArrayList<String>();
if (updater != null)
{
String required;
if (updater.providerVersion() != null)
{
required = updater.providerVersion().getProperty("requires");
}
else
{
return null;
}
if (required != null)
{
for (String requiredSubsystem : required.split( ",\\s*" ))
{
ArrayList<String> temp =
getRequirements(requiredSubsystem);
if (temp == null)
{
return null;
}
SubsystemUpdater requiredUpdater =
subsystemsByName.get(requiredSubsystem);
if (requiredUpdater == null)
{
return null;
}
requiredSubsystem +=
"_" + requiredUpdater.currentVersion() + ".jar";
requires.addAll(temp);
if (!requires.contains(requiredSubsystem))
{
requires.add(requiredSubsystem);
}
}
}
}
else
{
return null;
}
return requires;
}
}