/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2013 The Processing Foundation
Copyright (c) 2011-12 Ben Fry and Casey Reas
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.
This program 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 General Public License along
with this program; if not, write to the Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.app.contrib;
import java.awt.EventQueue;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.*;
import java.util.*;
import javax.swing.SwingWorker;
import processing.app.Base;
import processing.app.Language;
import processing.app.Messages;
import processing.app.Util;
import processing.app.ui.Editor;
import processing.core.PApplet;
import processing.data.StringDict;
public class ContributionManager {
static ContributionListing listing;
/**
* Blocks until the file is downloaded or an error occurs.
*
* @param source the URL of the file to download
* @param post Binary blob of POST data if a payload should be sent.
* Must already be URL-encoded and will be Gzipped for upload.
* @param dest The file on the local system where the file will be written.
* This must be a file (not a directory), and must already exist.
* @param progress null if progress is irrelevant, such as when downloading
* for an install during startup, when the ProgressMonitor
* is useless since UI isn't setup yet.
*
* @return true if the file was successfully downloaded, false otherwise.
*/
static boolean download(URL source, byte[] post,
File dest, ContribProgressMonitor progress) {
boolean success = false;
try {
HttpURLConnection conn = (HttpURLConnection) source.openConnection();
HttpURLConnection.setFollowRedirects(true);
conn.setConnectTimeout(15 * 1000);
conn.setReadTimeout(60 * 1000);
if (post == null) {
conn.setRequestMethod("GET");
conn.connect();
} else {
post = Util.gzipEncode(post);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setRequestProperty("Content-Encoding", "gzip");
conn.setRequestProperty("Content-Length", String.valueOf(post.length));
conn.setUseCaches(false);
conn.setDoInput(true);
conn.setDoOutput(true);
conn.getOutputStream().write(post);
}
if (progress != null) {
// TODO this is often -1, may need to set progress to indeterminate
int fileSize = conn.getContentLength();
progress.max = fileSize;
// System.out.println("file size is " + fileSize);
progress.startTask(Language.text("contrib.progress.downloading"), fileSize);
}
InputStream in = conn.getInputStream();
FileOutputStream out = new FileOutputStream(dest);
byte[] b = new byte[8192];
int amount;
if (progress != null) {
int total = 0;
while (!progress.isCanceled() && (amount = in.read(b)) != -1) {
out.write(b, 0, amount);
total += amount;
progress.setProgress(total);
}
} else {
while ((amount = in.read(b)) != -1) {
out.write(b, 0, amount);
}
}
out.flush();
out.close();
success = true;
} catch (SocketTimeoutException ste) {
if (progress != null) {
progress.error(ste);
progress.cancel();
}
} catch (IOException ioe) {
if (progress != null) {
progress.error(ioe);
progress.cancel();
}
// Hiding stack trace. An error has been shown where needed.
// ioe.printStackTrace();
}
if (progress != null) {
progress.finished();
}
return success;
}
/**
* Non-blocking call to download and install a contribution in a new thread.
*
* @param url
* Direct link to the contribution.
* @param toBeReplaced
* The Contribution that will be replaced by this library being
* installed (e.g. an advertised version of a contribution, or the
* old version of a contribution that is being updated). Must not be
* null.
*/
static void downloadAndInstall(final Base base,
final URL url,
final AvailableContribution ad,
final ContribProgressBar downloadProgress,
final ContribProgressBar installProgress,
final StatusPanel status) {
// TODO: replace with SwingWorker [jv]
new Thread(new Runnable() {
public void run() {
String filename = url.getFile();
filename = filename.substring(filename.lastIndexOf('/') + 1);
try {
File contribZip = File.createTempFile("download", filename);
contribZip.setWritable(true); // necessary?
try {
download(url, null, contribZip, downloadProgress);
if (!downloadProgress.isCanceled() && !downloadProgress.isError()) {
installProgress.startTask(Language.text("contrib.progress.installing"), ContribProgressMonitor.UNKNOWN);
final LocalContribution contribution =
ad.install(base, contribZip, false, status);
if (contribution != null) {
try {
// TODO: run this in SwingWorker done() [jv]
EventQueue.invokeAndWait(new Runnable() {
@Override
public void run() {
listing.replaceContribution(ad, contribution);
/*
if (contribution.getType() == ContributionType.MODE) {
List<ModeContribution> contribModes = editor.getBase().getModeContribs();
if (!contribModes.contains(contribution)) {
contribModes.add((ModeContribution) contribution);
}
}
*/
base.refreshContribs(contribution.getType());
base.setUpdatesAvailable(listing.countUpdates(base));
}
});
} catch (InterruptedException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
throw (Exception) e.getCause();
}
}
installProgress.finished();
}
else {
if (downloadProgress.exception instanceof SocketTimeoutException) {
status.setErrorMessage(Language
.interpolate("contrib.errors.contrib_download.timeout",
ad.getName()));
} else {
status.setErrorMessage(Language
.interpolate("contrib.errors.download_and_install",
ad.getName()));
}
}
contribZip.delete();
//} catch (NoClassDefFoundError ncdfe) {
} catch (Exception e) {
String msg = null;
if (e instanceof RuntimeException) {
Throwable cause = ((RuntimeException) e).getCause();
if (cause instanceof NoClassDefFoundError ||
cause instanceof NoSuchMethodError) {
msg = "This item is not compatible with this version of Processing";
} else if (cause instanceof UnsupportedClassVersionError) {
msg = "This item needs to be recompiled for Java " +
PApplet.javaPlatform;
}
}
if (msg == null) {
msg = Language.interpolate("contrib.errors.download_and_install", ad.getName());
}
status.setErrorMessage(msg);
downloadProgress.cancel();
installProgress.cancel();
}
} catch (IOException e) {
status.setErrorMessage(Language.text("contrib.errors.temporary_directory"));
downloadProgress.cancel();
installProgress.cancel();
}
}
}, "Contribution Installer").start();
}
/**
* Non-blocking call to download and install a contribution in a new thread.
* Used when information about the progress of the download and install
* procedure is not of importance, such as if a contribution has to be
* installed at startup time.
*
* @param url Direct link to the contribution.
* @param ad The AvailableContribution to be downloaded and installed.
*/
static void downloadAndInstallOnStartup(final Base base, final URL url,
final AvailableContribution ad) {
// TODO: replace with SwingWorker [jv]
new Thread(new Runnable() {
public void run() {
String filename = url.getFile();
filename = filename.substring(filename.lastIndexOf('/') + 1);
try {
File contribZip = File.createTempFile("download", filename);
contribZip.setWritable(true); // necessary?
try {
download(url, null, contribZip, null);
final LocalContribution contribution = ad.install(base, contribZip,
false, null);
if (contribution != null) {
try {
// TODO: run this in SwingWorker done() [jv]
EventQueue.invokeAndWait(new Runnable() {
@Override
public void run() {
listing.replaceContribution(ad, contribution);
base.refreshContribs(contribution.getType());
base.setUpdatesAvailable(listing.countUpdates(base));
}
});
} catch (InterruptedException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else {
cause.printStackTrace();
}
}
}
contribZip.delete();
handleUpdateFailedMarkers(ad, filename.substring(0, filename.lastIndexOf('.')));
} catch (Exception e) {
String arg = "contrib.startup.errors.download_install";
System.err.println(Language.interpolate(arg, ad.getName()));
}
} catch (IOException e) {
String arg = "contrib.startup.errors.temp_dir";
System.err.println(Language.interpolate(arg, ad.getName()));
}
}
}, "Contribution Installer").start();
}
/**
* After install, this function checks whether everything went properly.
* If not, it adds a marker file so that the next time Processing is started,
* installPreviouslyFailed() can install the contribution.
* @param c the contribution just installed
* @param filename name of the folder for the contribution
*/
static private void handleUpdateFailedMarkers(final AvailableContribution c,
String filename) {
File typeFolder = c.getType().getSketchbookFolder();
for (File contribDir : typeFolder.listFiles()) {
if (contribDir.isDirectory()) {
/*
File[] contents = contribDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String file) {
return file.equals(c.getType() + ".properties");
}
});
if (contents.length > 0 && Util.readSettings(contents[0]).get("name").equals(c.getName())) {
return;
}
*/
File propsFile = new File(contribDir, c.getType() + ".properties");
if (propsFile.exists()) {
StringDict props = Util.readSettings(propsFile);
if (c.getName().equals(props.get("name"))) {
return;
}
}
}
}
try {
new File(typeFolder, c.getName()).createNewFile();
} catch (IOException e) {
String arg = "contrib.startup.errors.new_marker";
System.err.println(Language.interpolate(arg, c.getName()));
}
}
/**
* Blocking call to download and install a set of libraries. Used when a list
* of libraries have to be installed while forcing the user to not modify
* anything and providing feedback via the console status area, such as when
* the user tries to run a sketch that imports uninstaled libraries.
*
* @param list The list of AvailableContributions to be downloaded and installed.
*/
static public void downloadAndInstallOnImport(final Base base,
final List<AvailableContribution> list) {
// To avoid the user from modifying stuff, since this function is only
// called during pre-processing
Editor editor = base.getActiveEditor();
editor.getTextArea().setEditable(false);
// base.getActiveEditor().getConsole().clear();
List<String> installedLibList = new ArrayList<String>();
// boolean variable to check if previous lib was installed successfully,
// to give the user an idea about progress being made.
boolean isPrevDone = false;
for (final AvailableContribution contrib : list) {
if (contrib.getType() != ContributionType.LIBRARY) {
continue;
}
try {
URL url = new URL(contrib.link);
String filename = url.getFile();
filename = filename.substring(filename.lastIndexOf('/') + 1);
try {
File contribZip = File.createTempFile("download", filename);
contribZip.setWritable(true);
try {
// Use the console to let the user know what's happening
// The slightly complex if-else is required to let the user know when
// one install is completed and the next download has begun without
// interfering with other status messages that may arise in the meanwhile
String statusMsg = editor.getStatusMessage();
if (isPrevDone) {
String status = statusMsg + " "
+ Language.interpolate("contrib.import.progress.download", contrib.name);
editor.statusNotice(status);
} else {
String arg = "contrib.import.progress.download";
String status = Language.interpolate(arg, contrib.name);
editor.statusNotice(status);
}
isPrevDone = false;
download(url, null, contribZip, null);
String arg = "contrib.import.progress.install";
editor.statusNotice(Language.interpolate(arg,contrib.name));
final LocalContribution contribution =
contrib.install(base, contribZip, false, null);
if (contribution != null) {
try {
EventQueue.invokeAndWait(new Runnable() {
@Override
public void run() {
listing.replaceContribution(contrib, contribution);
base.refreshContribs(contribution.getType());
base.setUpdatesAvailable(listing.countUpdates(base));
}
});
} catch (InterruptedException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else {
cause.printStackTrace();
}
}
}
contribZip.delete();
installedLibList.add(contrib.name);
isPrevDone = true;
arg = "contrib.import.progress.done";
editor.statusNotice(Language.interpolate(arg,contrib.name));
} catch (Exception e) {
String arg = "contrib.startup.errors.download_install";
System.err.println(Language.interpolate(arg, contrib.getName()));
}
} catch (IOException e) {
String arg = "contrib.startup.errors.temp_dir";
System.err.println(Language.interpolate(arg,contrib.getName()));
}
} catch (MalformedURLException e1) {
System.err.println(Language.interpolate("contrib.import.errors.link",
contrib.getName()));
}
}
editor.getTextArea().setEditable(true);
editor.statusEmpty();
System.out.println(Language.text("contrib.import.progress.final_list"));
for (String l : installedLibList) {
System.out.println(" * " + l);
}
}
/*
static void refreshInstalled(Editor e) {
for (Editor ed : e.getBase().getEditors()) {
ed.getMode().rebuildImportMenu();
ed.getMode().rebuildExamplesFrame();
ed.rebuildToolMenu();
ed.rebuildModeMenu();
}
}
*/
/**
* Returns a file in the parent folder that does not exist yet. If
* parent/fileName already exists, this will look for parent/fileName(2)
* then parent/fileName(3) and so forth.
*
* @return a file that does not exist yet
*/
public static File getUniqueName(File parentFolder, String fileName) {
File backupFolderForLib;
int i = 1;
do {
String folderName = fileName;
if (i >= 2) {
folderName += "(" + i + ")";
}
i++;
backupFolderForLib = new File(parentFolder, folderName);
} while (backupFolderForLib.exists());
return backupFolderForLib;
}
/**
* Returns the name of a file without its path or extension.
*
* For example,
* "/path/to/helpfullib.zip" returns "helpfullib"
* "helpfullib-0.1.1.plb" returns "helpfullib-0.1.1"
*/
static public String getFileName(File libFile) {
String path = libFile.getPath();
int lastSeparator = path.lastIndexOf(File.separatorChar);
String fileName;
if (lastSeparator != -1) {
fileName = path.substring(lastSeparator + 1);
} else {
fileName = path;
}
int lastDot = fileName.lastIndexOf('.');
if (lastDot != -1) {
return fileName.substring(0, lastDot);
}
return fileName;
}
/**
* Called by Base to clean up entries previously marked for deletion
* and remove any "requires restart" flags.
* Also updates all entries previously marked for update.
*/
static private void cleanup(final Base base) throws Exception {
deleteTemp(Base.getSketchbookModesFolder());
deleteTemp(Base.getSketchbookToolsFolder());
deleteFlagged(Base.getSketchbookLibrariesFolder());
deleteFlagged(Base.getSketchbookModesFolder());
deleteFlagged(Base.getSketchbookToolsFolder());
installPreviouslyFailed(base, Base.getSketchbookModesFolder());
updateFlagged(base, Base.getSketchbookModesFolder());
updateFlagged(base, Base.getSketchbookToolsFolder());
SwingWorker s = new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
try {
// TODO: pls explain the sleep and why this runs on a worker thread,
// but a couple of lines above on EDT [jv]
Thread.sleep(1000);
installPreviouslyFailed(base, Base.getSketchbookToolsFolder());
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
};
s.execute();
clearRestartFlags(Base.getSketchbookModesFolder());
clearRestartFlags(Base.getSketchbookToolsFolder());
}
/**
* Deletes the icky tmp folders that were left over from installs and updates
* in the previous run of Processing. Needed to be called only on the tools
* and modes sketchbook folders.
*
* @param root
*/
static private void deleteTemp(File root) {
String pattern = root.getName().substring(0, 4) + "\\d*" + "tmp";
File[] possible = root.listFiles();
if (possible != null) {
for (File f : possible) {
if (f.getName().matches(pattern)) {
Util.removeDir(f);
}
}
}
}
/**
* Deletes all the modes/tools/libs that are flagged for removal.
*/
static private void deleteFlagged(File root) throws Exception {
File[] markedForDeletion = root.listFiles(new FileFilter() {
public boolean accept(File folder) {
return (folder.isDirectory() &&
LocalContribution.isDeletionFlagged(folder));
}
});
for (File folder : markedForDeletion) {
Util.removeDir(folder);
}
}
/**
* Installs all the modes/tools whose installation failed during an
* auto-update the previous time Processing was started up.
*/
static private void installPreviouslyFailed(Base base, File root) throws Exception {
File[] installList = root.listFiles(new FileFilter() {
public boolean accept(File folder) {
return folder.isFile();
}
});
for (File file : installList) {
for (AvailableContribution contrib : listing.advertisedContributions) {
if (file.getName().equals(contrib.getName())) {
file.delete();
installOnStartUp(base, contrib);
EventQueue.invokeAndWait(() -> {
listing.replaceContribution(contrib, contrib);
});
}
}
}
}
/**
* Updates all the flagged modes/tools.
*/
static private void updateFlagged(Base base, File root) throws Exception {
File[] markedForUpdate = root.listFiles(new FileFilter() {
public boolean accept(File folder) {
return (folder.isDirectory() &&
LocalContribution.isUpdateFlagged(folder));
}
});
ArrayList<String> updateContribsNames = new ArrayList<String>();
LinkedList<AvailableContribution> updateContribsList = new LinkedList<AvailableContribution>();
String type = root.getName().substring(root.getName().lastIndexOf('/') + 1);
String propFileName = null;
if (type.equalsIgnoreCase("tools"))
propFileName = "tool.properties";
else if (type.equalsIgnoreCase("modes"))
propFileName = "mode.properties";
else if (type.equalsIgnoreCase("libraries")) //putting this here, just in case
propFileName = "libraries.properties";
for (File folder : markedForUpdate) {
StringDict props = Util.readSettings(new File(folder, propFileName));
updateContribsNames.add(props.get("name"));
Util.removeDir(folder);
}
Iterator<AvailableContribution> iter = listing.advertisedContributions.iterator();
while (iter.hasNext()) {
AvailableContribution availableContribs = iter.next();
if (updateContribsNames.contains(availableContribs.getName())) {
updateContribsList.add(availableContribs);
}
}
Iterator<AvailableContribution> iter2 = updateContribsList.iterator();
while (iter2.hasNext()) {
AvailableContribution contribToUpdate = iter2.next();
installOnStartUp(base, contribToUpdate);
listing.replaceContribution(contribToUpdate, contribToUpdate);
}
}
static private void installOnStartUp(final Base base, final AvailableContribution availableContrib) {
if (availableContrib.link == null) {
Messages.showWarning(Language.interpolate("contrib.errors.update_on_restart_failed", availableContrib.getName()),
Language.text("contrib.unsupported_operating_system"));
} else {
try {
URL downloadUrl = new URL(availableContrib.link);
ContributionManager.downloadAndInstallOnStartup(base, downloadUrl, availableContrib);
} catch (MalformedURLException e) {
Messages.showWarning(Language.interpolate("contrib.errors.update_on_restart_failed", availableContrib.getName()),
Language.text("contrib.errors.malformed_url"), e);
}
}
}
static private void clearRestartFlags(File root) throws Exception {
File[] folderList = root.listFiles(new FileFilter() {
public boolean accept(File folder) {
return folder.isDirectory();
}
});
for (File folder : folderList) {
LocalContribution.clearRestartFlags(folder);
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
static ManagerFrame managerDialog;
static public void init(Base base) throws Exception {
listing = ContributionListing.getInstance(); // Moved here to make sure it runs on EDT [jv 170121]
managerDialog = new ManagerFrame(base);
cleanup(base);
}
/**
* Show the Library installer window.
*/
static public void openLibraries() {
managerDialog.showFrame(ContributionType.LIBRARY);
}
/**
* Show the Mode installer window.
*/
static public void openModes() {
managerDialog.showFrame(ContributionType.MODE);
}
/**
* Show the Tool installer window.
*/
static public void openTools() {
managerDialog.showFrame(ContributionType.TOOL);
}
/**
* Show the Examples installer window.
*/
static public void openExamples() {
managerDialog.showFrame(ContributionType.EXAMPLES);
}
/**
* Open the updates panel.
*/
static public void openUpdates() {
managerDialog.showFrame(null);
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
static int getTypeIndex(ContributionType contributionType) {
int index;
if (contributionType == ContributionType.LIBRARY) {
index = 0;
} else if (contributionType == ContributionType.MODE) {
index = 1;
} else if (contributionType == ContributionType.TOOL) {
index = 2;
} else if (contributionType == ContributionType.EXAMPLES) {
index = 3;
} else {
index = 4;
}
return index;
}
}