/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2013-16 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.URL;
import java.net.URLClassLoader;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.zip.*;
import javax.swing.JOptionPane;
import processing.app.*;
import processing.app.ui.Editor;
import processing.core.PApplet;
import processing.data.StringDict;
import processing.data.StringList;
/**
* A contribution that has been downloaded to the disk, and may or may not
* be installed.
*/
public abstract class LocalContribution extends Contribution {
static public final String DELETION_FLAG = "marked_for_deletion";
static public final String UPDATE_FLAGGED = "marked_for_update";
static public final String RESTART_FLAG = "requires_restart";
protected String id; // 1 (unique id for this library)
protected int latestVersion; // 103
protected File folder;
protected StringDict properties;
protected ClassLoader loader;
public LocalContribution(File folder) {
this.folder = folder;
// required for contributed modes, but not for built-in core modes
File propertiesFile = new File(folder, getTypeName() + ".properties");
if (propertiesFile.exists()) {
properties = Util.readSettings(propertiesFile);
name = properties.get("name");
id = properties.get("id");
categories = parseCategories(properties);
imports = parseImports(properties);
if (name == null) {
name = folder.getName();
}
// changing to 'authors' in 3.0a11
authors = properties.get(AUTHORS_PROPERTY);
// if (authors == null) {
// authors = properties.get("authorList");
// }
url = properties.get("url");
sentence = properties.get("sentence");
paragraph = properties.get("paragraph");
try {
version = Integer.parseInt(properties.get("version"));
} catch (NumberFormatException e) {
System.err.println("The version number for the “" + name + "” library is not a number.");
System.err.println("Please contact the library author to fix it according to the guidelines.");
}
setPrettyVersion(properties.get("prettyVersion"));
try {
lastUpdated = Long.parseLong(properties.get("lastUpdated"));
} catch (NumberFormatException e) {
lastUpdated = 0;
// Better comment these out till all contribs have a lastUpdated
// System.err.println("The last updated timestamp for the “" + name + "” library is not set properly.");
// System.err.println("Please contact the library author to fix it according to the guidelines.");
}
String minRev = properties.get("minRevision");
if (minRev != null) {
minRevision = PApplet.parseInt(minRev, 0);
}
String maxRev = properties.get("maxRevision");
if (maxRev != null) {
maxRevision = PApplet.parseInt(maxRev, 0);
}
} else {
Messages.log("No properties file at " + propertiesFile.getAbsolutePath());
// We'll need this to be set at a minimum.
name = folder.getName();
categories = unknownCategoryList();
}
if (categories.hasValue(SPECIAL_CATEGORY)) {
validateSpecial();
}
}
private void validateSpecial() {
for (AvailableContribution available : ContributionListing.getInstance().advertisedContributions) {
if (available.getName().equals(name)) {
if (!available.isSpecial()) {
categories.removeValue(SPECIAL_CATEGORY);
}
}
break;
}
}
public String initLoader(String className) throws Exception {
File modeDirectory = new File(folder, getTypeName());
if (modeDirectory.exists()) {
Messages.log("checking mode folder regarding " + className);
// If no class name specified, search the main <modename>.jar for the
// full name package and mode name.
if (className == null) {
String shortName = folder.getName();
File mainJar = new File(modeDirectory, shortName + ".jar");
if (mainJar.exists()) {
className = findClassInZipFile(shortName, mainJar);
} else {
throw new IgnorableException(mainJar.getAbsolutePath() + " does not exist.");
}
if (className == null) {
throw new IgnorableException("Could not find " + shortName +
" class inside " + mainJar.getAbsolutePath());
}
}
// Add .jar and .zip files from the "mode" folder into the classpath
File[] archives = Util.listJarFiles(modeDirectory);
if (archives != null && archives.length > 0) {
URL[] urlList = new URL[archives.length];
for (int j = 0; j < urlList.length; j++) {
Messages.log("Found archive " + archives[j] + " for " + getName());
urlList[j] = archives[j].toURI().toURL();
}
// loader = new URLClassLoader(urlList, Thread.currentThread().getContextClassLoader());
loader = new URLClassLoader(urlList);
Messages.log("loading above JARs with loader " + loader);
// System.out.println("listing classes for loader " + loader);
// listClasses(loader);
}
}
// If no archives were found, just use the regular ClassLoader
if (loader == null) {
loader = Thread.currentThread().getContextClassLoader();
}
return className;
}
/*
// doesn't work with URLClassLoader, but works with the system CL
static void listClasses(ClassLoader loader) {
// loader = Thread.currentThread().getContextClassLoader();
try {
Field f = ClassLoader.class.getDeclaredField("classes");
f.setAccessible(true);
Vector<Class> classes = (Vector<Class>) f.get(loader);
for (Class c : classes) {
System.out.println(c.getName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
*/
// static protected boolean isCandidate(File potential, final ContributionType type) {
// return (potential.isDirectory() &&
// new File(potential, type.getFolderName()).exists());
// }
//
//
// /**
// * Return a list of directories that have the necessary subfolder for this
// * contribution type. For instance, a list of folders that have a 'mode'
// * subfolder if this is a ModeContribution.
// */
// static protected File[] listCandidates(File folder, final ContributionType type) {
// return folder.listFiles(new FileFilter() {
// public boolean accept(File potential) {
// return isCandidate(potential, type);
// }
// });
// }
//
//
// /**
// * Return the first directory that has the necessary subfolder for this
// * contribution type. For instance, the first folder that has a 'mode'
// * subfolder if this is a ModeContribution.
// */
// static protected File findCandidate(File folder, final ContributionType type) {
// File[] folders = listCandidates(folder, type);
//
// if (folders.length == 0) {
// return null;
//
// } else if (folders.length > 1) {
// Base.log("More than one " + type.toString() + " found inside " + folder.getAbsolutePath());
// }
// return folders[0];
// }
LocalContribution copyAndLoad(Base base,
boolean confirmReplace,
StatusPanel status) {
// NOTE: null status => function is called on startup when Editor objects, et al. aren't ready
String contribFolderName = getFolder().getName();
File contribTypeFolder = getType().getSketchbookFolder();
File contribFolder = new File(contribTypeFolder, contribFolderName);
if (status != null) { // when status != null, install is not occurring on startup
Editor editor = base.getActiveEditor();
ArrayList<LocalContribution> oldContribs =
getType().listContributions(editor);
// In case an update marker exists, and the user wants to install, delete the update marker
if (contribFolder.exists() && !contribFolder.isDirectory()) {
contribFolder.delete();
contribFolder = new File(contribTypeFolder, contribFolderName);
}
for (LocalContribution oldContrib : oldContribs) {
if ((oldContrib.getFolder().exists() && oldContrib.getFolder().equals(contribFolder)) ||
(oldContrib.getId() != null && oldContrib.getId().equals(getId()))) {
if (oldContrib.getType().requiresRestart()) {
// XXX: We can't replace stuff, soooooo.... do something different
if (!oldContrib.backup(false, status)) {
return null;
}
} else {
int result = 0;
boolean doBackup = Preferences.getBoolean("contribution.backup.on_install");
if (confirmReplace) {
if (doBackup) {
result = Messages.showYesNoQuestion(editor, "Replace",
"Replace pre-existing \"" + oldContrib.getName() + "\" library?",
"A pre-existing copy of the \"" + oldContrib.getName() + "\" library<br>"+
"has been found in your sketchbook. Clicking “Yes”<br>"+
"will move the existing library to a backup folder<br>" +
"in <i>libraries/old</i> before replacing it.");
if (result != JOptionPane.YES_OPTION || !oldContrib.backup(true, status)) {
return null;
}
} else {
result = Messages.showYesNoQuestion(editor, "Replace",
"Replace pre-existing \"" + oldContrib.getName() + "\" library?",
"A pre-existing copy of the \"" + oldContrib.getName() + "\" library<br>"+
"has been found in your sketchbook. Clicking “Yes”<br>"+
"will permanently delete this library and all of its contents<br>"+
"before replacing it.");
if (result != JOptionPane.YES_OPTION || !oldContrib.getFolder().delete()) {
return null;
}
}
} else {
if ((doBackup && !oldContrib.backup(true, status)) ||
(!doBackup && !oldContrib.getFolder().delete())) {
return null;
}
}
}
}
}
// At this point it should be safe to replace this fella
if (contribFolder.exists()) {
Util.removeDir(contribFolder);
}
}
else {
// This if should ideally never happen, since this function
// is to be called only when restarting on update
if (contribFolder.exists() && contribFolder.isDirectory()) {
Util.removeDir(contribFolder);
}
else if (contribFolder.exists()) {
contribFolder.delete();
contribFolder = new File(contribTypeFolder, contribFolderName);
}
}
File oldFolder = getFolder();
try {
Util.copyDir(oldFolder, contribFolder);
} catch (IOException e) {
status.setErrorMessage("Could not copy " + getTypeName() +
" \"" + getName() + "\" to the sketchbook.");
e.printStackTrace();
return null;
}
/*
if (!getFolder().renameTo(contribFolder)) {
status.setErrorMessage("Could not move " + getTypeName() +
" \"" + getName() + "\" to the sketchbook.");
return null;
}
*/
return getType().load(base, contribFolder);
}
/**
* Moves the given contribution to a backup folder.
* @param deleteOriginal
* true if the file should be moved to the directory, false if it
* should instead be copied, leaving the original in place
*/
boolean backup(boolean deleteOriginal, StatusPanel status) {
File backupFolder = getType().createBackupFolder(status);
boolean success = false;
if (backupFolder != null) {
String libFolderName = getFolder().getName();
String prefix = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
final String backupName = prefix + " " + libFolderName;
File backupSubFolder =
ContributionManager.getUniqueName(backupFolder, backupName);
if (deleteOriginal) {
success = getFolder().renameTo(backupSubFolder);
} else {
try {
Util.copyDir(getFolder(), backupSubFolder);
success = true;
} catch (IOException e) { }
}
if (!success) {
status.setErrorMessage("Could not move contribution to backup folder.");
}
}
return success;
}
/**
* Non-blocking call to remove a contribution in a new thread.
*/
void removeContribution(final Base base,
final ContribProgressMonitor pm,
final StatusPanel status) {
// TODO: replace with SwingWorker [jv]
new Thread(new Runnable() {
public void run() {
remove(base,
pm,
status,
ContributionListing.getInstance());
}
}, "Contribution Uninstaller").start();
}
void remove(final Base base,
final ContribProgressMonitor pm,
final StatusPanel status,
final ContributionListing contribListing) {
pm.startTask("Removing", ContribProgressMonitor.UNKNOWN);
boolean doBackup = Preferences.getBoolean("contribution.backup.on_remove");
// if (getType().requiresRestart()) {
// if (!doBackup || (doBackup && backup(editor, false, status))) {
// if (setDeletionFlag(true)) {
// contribListing.replaceContribution(this, this);
// }
// }
// } else {
boolean success = false;
if (getType() == ContributionType.MODE) {
boolean isModeActive = false;
ModeContribution m = (ModeContribution) this;
Iterator<Editor> iter = base.getEditors().iterator();
while (iter.hasNext()) {
Editor e = iter.next();
if (e.getMode().equals(m.getMode())) {
isModeActive = true;
break;
}
}
if (!isModeActive) {
m.clearClassLoader(base);
} else {
pm.cancel();
Messages.showMessage("Mode Manager",
"Please save your Sketch and change the Mode of all Editor\n" +
"windows that have " + name + " as the active Mode.");
return;
}
}
if (getType() == ContributionType.TOOL) {
/*
ToolContribution t = (ToolContribution) this;
Iterator<Editor> iter = editor.getBase().getEditors().iterator();
while (iter.hasNext()) {
Editor ed = iter.next();
ed.clearToolMenu();
}
t.clearClassLoader(editor.getBase());
*/
// menu will be rebuilt below with the refreshContribs() call
base.clearToolMenus();
((ToolContribution) this).clearClassLoader();
}
if (doBackup) {
success = backup(true, status);
} else {
success = Util.removeDir(getFolder(), false);
}
if (success) {
// this was just rebuilding the tool menu in one editor, which happens
// yet again down below with the call to refreshInstalled() [fry 150828]
// if (getType() == ContributionType.TOOL) {
// editor.removeTool();
// }
try {
// TODO: run this in SwingWorker done() [jv]
EventQueue.invokeAndWait(new Runnable() {
@Override
public void run() {
Contribution advertisedVersion =
contribListing.getAvailableContribution(LocalContribution.this);
if (advertisedVersion == null) {
contribListing.removeContribution(LocalContribution.this);
} else {
contribListing.replaceContribution(LocalContribution.this, advertisedVersion);
}
base.refreshContribs(LocalContribution.this.getType());
base.setUpdatesAvailable(contribListing.countUpdates(base));
}
});
} catch (InterruptedException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else {
cause.printStackTrace();
}
}
} else {
// There was a failure backing up the folder
if (!doBackup || (doBackup && backup(false, status))) {
if (setDeletionFlag(true)) {
try {
// TODO: run this in SwingWorker done() [jv]
EventQueue.invokeAndWait(new Runnable() {
@Override
public void run() {
contribListing.replaceContribution(LocalContribution.this,
LocalContribution.this);
base.refreshContribs(LocalContribution.this.getType());
base.setUpdatesAvailable(contribListing.countUpdates(base));
}
});
} catch (InterruptedException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else {
cause.printStackTrace();
}
}
}
} else {
status.setErrorMessage("Could not delete the contribution's files");
}
}
if (success) {
pm.finished();
} else {
pm.cancel();
}
}
public File getFolder() {
return folder;
}
public boolean isInstalled() {
return folder != null;
}
// public String getCategory() {
// return category;
// }
//
//
// public String getName() {
// return name;
// }
public String getId() {
return id;
}
// public String getAuthorList() {
// return authorList;
// }
//
//
// public String getUrl() {
// return url;
// }
//
//
// public String getSentence() {
// return sentence;
// }
//
//
// public String getParagraph() {
// return paragraph;
// }
//
//
// public int getVersion() {
// return version;
// }
public int getLatestVersion() {
return latestVersion;
}
// public String getPrettyVersion() {
// return prettyVersion;
// }
//
//
// public String getTypeName() {
// return getType().toString();
// }
/*
static protected String findClassInZipFileList(String base, File[] fileList) {
for (File file : fileList) {
String found = findClassInZipFile(base, file);
if (found != null) {
return found;
}
}
return null;
}
*/
/**
* Returns the imports (package-names) for a library, as specified in its library.properties
* (e.g., imports=libname.*,libname.support.*)
*
* @return String[] packageNames (without wildcards) or null if none are specified
*/
public StringList getImports() {
//return imports != null ? imports.toArray(new String[0]) : null;
return imports;
}
// this duplicates code found in Contribution (though that version doesn't check for .* at the end)
// /**
// * @return the list of Java imports to be added to the sketch when the library is imported
// * or null if none are specified
// */
// static StringList parseImports(String importsStr) {
// StringList outgoing = new StringList();
//
// if (importsStr != null) {
// String[] listing = PApplet.trim(PApplet.split(importsStr, ','));
// for (String imp : listing) {
//
// // In case the wildcard is specified, strip it, as it gets added later)
// if (imp.endsWith(".*")) {
//
// imp = imp.substring(0, imp.length() - 2);
// }
//
// outgoing.add(imp);
// }
// }
//// return (outgoing.size() > 0) ? outgoing : null;
// return outgoing;
// }
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
boolean setDeletionFlag(boolean flag) {
return setFlag(DELETION_FLAG, flag);
}
boolean isDeletionFlagged() {
return isDeletionFlagged(getFolder());
}
static boolean isDeletionFlagged(File folder) {
return isFlagged(folder, DELETION_FLAG);
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
boolean setUpdateFlag(boolean flag) {
return setFlag(UPDATE_FLAGGED, flag);
}
boolean isUpdateFlagged() {
return isUpdateFlagged(getFolder());
}
static boolean isUpdateFlagged(File folder) {
return isFlagged(folder, UPDATE_FLAGGED);
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
boolean setRestartFlag() {
//System.out.println("setting restart flag for " + folder);
return setFlag(RESTART_FLAG, true);
}
@Override
boolean isRestartFlagged() {
//System.out.println("checking for restart inside LocalContribution for " + getName());
return isFlagged(getFolder(), RESTART_FLAG);
}
static void clearRestartFlags(File folder) {
File restartFlag = new File(folder, RESTART_FLAG);
if (restartFlag.exists()) {
restartFlag.delete();
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
private boolean setFlag(String flagFilename, boolean flag) {
if (flag) {
// Only returns false if the file already exists, so we can
// ignore the return value.
try {
new File(getFolder(), flagFilename).createNewFile();
return true;
} catch (IOException e) {
return false;
}
} else {
return new File(getFolder(), flagFilename).delete();
}
}
static private boolean isFlagged(File folder, String flagFilename) {
return new File(folder, flagFilename).exists();
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
/**
*
* @param base name of the class, with or without the package
* @param file
* @return name of class (with full package name) or null if not found
*/
static protected String findClassInZipFile(String base, File file) {
// Class file to search for
String classFileName = "/" + base + ".class";
try {
ZipFile zipFile = new ZipFile(file);
Enumeration<?> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = (ZipEntry) entries.nextElement();
if (!entry.isDirectory()) {
String name = entry.getName();
// System.out.println("entry: " + name);
if (name.endsWith(classFileName)) {
//int slash = name.lastIndexOf('/');
//String packageName = (slash == -1) ? "" : name.substring(0, slash);
// Remove .class and convert slashes to periods.
zipFile.close();
return name.substring(0, name.length() - 6).replace('/', '.');
}
}
}
zipFile.close();
} catch (IOException e) {
//System.err.println("Ignoring " + filename + " (" + e.getMessage() + ")");
e.printStackTrace();
}
return null;
}
}