mirror of
https://github.com/processing/processing4.git
synced 2026-02-14 10:55:38 +01:00
618 lines
19 KiB
Java
618 lines
19 KiB
Java
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
|
|
|
|
/*
|
|
Part of the Processing project - http://processing.org
|
|
|
|
Copyright (c) 2013-15 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.text.Normalizer;
|
|
import java.util.*;
|
|
import java.util.concurrent.locks.ReentrantLock;
|
|
import java.util.regex.Pattern;
|
|
|
|
import processing.app.Base;
|
|
import processing.app.Library;
|
|
import processing.app.Util;
|
|
import processing.core.PApplet;
|
|
import processing.data.StringDict;
|
|
|
|
|
|
public class ContributionListing {
|
|
static volatile ContributionListing singleInstance;
|
|
|
|
/** Stable URL that will redirect to wherever the file is hosted */
|
|
static final String LISTING_URL = "http://download.processing.org/contribs";
|
|
static final String LOCAL_FILENAME = "contribs.txt";
|
|
|
|
/** Location of the listing file on disk, will be read and written. */
|
|
File listingFile;
|
|
|
|
List<ChangeListener> listeners;
|
|
List<AvailableContribution> advertisedContributions;
|
|
Map<String, List<Contribution>> librariesByCategory;
|
|
Map<String, Contribution> librariesByImportHeader;
|
|
// TODO: Every contribution is getting added twice
|
|
// and nothing is replaced ever.
|
|
List<Contribution> allContributions;
|
|
boolean listDownloaded;
|
|
boolean listDownloadFailed;
|
|
ReentrantLock downloadingListingLock;
|
|
|
|
|
|
private ContributionListing() {
|
|
listeners = new ArrayList<ChangeListener>();
|
|
advertisedContributions = new ArrayList<AvailableContribution>();
|
|
librariesByCategory = new HashMap<String, List<Contribution>>();
|
|
librariesByImportHeader = new HashMap<String, Contribution>();
|
|
allContributions = new ArrayList<Contribution>();
|
|
downloadingListingLock = new ReentrantLock();
|
|
|
|
//listingFile = Base.getSettingsFile("contributions.txt");
|
|
listingFile = Base.getSettingsFile(LOCAL_FILENAME);
|
|
listingFile.setWritable(true);
|
|
if (listingFile.exists()) {
|
|
setAdvertisedList(listingFile);
|
|
}
|
|
}
|
|
|
|
|
|
static public ContributionListing getInstance() {
|
|
if (singleInstance == null) {
|
|
synchronized (ContributionListing.class) {
|
|
if (singleInstance == null) {
|
|
singleInstance = new ContributionListing();
|
|
}
|
|
}
|
|
}
|
|
return singleInstance;
|
|
}
|
|
|
|
|
|
private void setAdvertisedList(File file) {
|
|
listingFile = file;
|
|
|
|
advertisedContributions.clear();
|
|
advertisedContributions.addAll(parseContribList(listingFile));
|
|
for (Contribution contribution : advertisedContributions) {
|
|
addContribution(contribution);
|
|
}
|
|
Collections.sort(allContributions, COMPARATOR);
|
|
}
|
|
|
|
|
|
/**
|
|
* Adds the installed libraries to the listing of libraries, replacing any
|
|
* pre-existing libraries by the same name as one in the list.
|
|
*/
|
|
protected void updateInstalledList(List<Contribution> installed) {
|
|
for (Contribution contribution : installed) {
|
|
Contribution existingContribution = getContribution(contribution);
|
|
if (existingContribution != null) {
|
|
replaceContribution(existingContribution, contribution);
|
|
//} else if (contribution != null) { // 130925 why would this be necessary?
|
|
} else {
|
|
addContribution(contribution);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
protected void replaceContribution(Contribution oldLib, Contribution newLib) {
|
|
if (oldLib != null && newLib != null) {
|
|
for (String category : oldLib.getCategories()) {
|
|
if (librariesByCategory.containsKey(category)) {
|
|
List<Contribution> list = librariesByCategory.get(category);
|
|
|
|
for (int i = 0; i < list.size(); i++) {
|
|
if (list.get(i) == oldLib) {
|
|
list.set(i, newLib);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (oldLib.getImports() != null) {
|
|
for (String importName : oldLib.getImports()) {
|
|
if (getLibrariesByImportHeader().containsKey(importName)) {
|
|
getLibrariesByImportHeader().put(importName, newLib);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < allContributions.size(); i++) {
|
|
if (allContributions.get(i) == oldLib) {
|
|
allContributions.set(i, newLib);
|
|
}
|
|
}
|
|
|
|
notifyChange(oldLib, newLib);
|
|
}
|
|
}
|
|
|
|
|
|
private void addContribution(Contribution contribution) {
|
|
if (contribution.getImports() != null) {
|
|
for (String importName : contribution.getImports()) {
|
|
getLibrariesByImportHeader().put(importName, contribution);
|
|
}
|
|
}
|
|
for (String category : contribution.getCategories()) {
|
|
if (librariesByCategory.containsKey(category)) {
|
|
List<Contribution> list = librariesByCategory.get(category);
|
|
list.add(contribution);
|
|
Collections.sort(list, COMPARATOR);
|
|
|
|
} else {
|
|
ArrayList<Contribution> list = new ArrayList<Contribution>();
|
|
list.add(contribution);
|
|
librariesByCategory.put(category, list);
|
|
}
|
|
allContributions.add(contribution);
|
|
notifyAdd(contribution);
|
|
Collections.sort(allContributions, COMPARATOR);
|
|
}
|
|
}
|
|
|
|
|
|
protected void removeContribution(Contribution contribution) {
|
|
for (String category : contribution.getCategories()) {
|
|
if (librariesByCategory.containsKey(category)) {
|
|
librariesByCategory.get(category).remove(contribution);
|
|
}
|
|
}
|
|
if (contribution.getImports() != null) {
|
|
for (String importName : contribution.getImports()) {
|
|
getLibrariesByImportHeader().remove(importName);
|
|
}
|
|
}
|
|
allContributions.remove(contribution);
|
|
notifyRemove(contribution);
|
|
}
|
|
|
|
|
|
private Contribution getContribution(Contribution contribution) {
|
|
for (Contribution c : allContributions) {
|
|
if (c.getName().equals(contribution.getName()) &&
|
|
c.getType() == contribution.getType()) {
|
|
return c;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
protected AvailableContribution getAvailableContribution(Contribution info) {
|
|
synchronized (advertisedContributions) {
|
|
for (AvailableContribution advertised : advertisedContributions) {
|
|
if (advertised.getType() == info.getType() &&
|
|
advertised.getName().equals(info.getName())) {
|
|
return advertised;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
protected Set<String> getCategories(Contribution.Filter filter) {
|
|
Set<String> outgoing = new HashSet<String>();
|
|
|
|
Set<String> categorySet = librariesByCategory.keySet();
|
|
for (String categoryName : categorySet) {
|
|
for (Contribution contrib : librariesByCategory.get(categoryName)) {
|
|
if (filter.matches(contrib)) {
|
|
// TODO still not sure why category would be coming back null [fry]
|
|
// http://code.google.com/p/processing/issues/detail?id=1387
|
|
if (categoryName != null && !categoryName.trim().isEmpty()) {
|
|
outgoing.add(categoryName);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return outgoing;
|
|
}
|
|
|
|
|
|
// public List<Contribution> getAllContributions() {
|
|
// return new ArrayList<Contribution>(allContributions);
|
|
// }
|
|
|
|
|
|
// public List<Contribution> getLibararies(String category) {
|
|
// ArrayList<Contribution> libinfos =
|
|
// new ArrayList<Contribution>(librariesByCategory.get(category));
|
|
// Collections.sort(libinfos, nameComparator);
|
|
// return libinfos;
|
|
// }
|
|
|
|
|
|
protected List<Contribution> getFilteredLibraryList(String category, List<String> filters) {
|
|
ArrayList<Contribution> filteredList =
|
|
new ArrayList<Contribution>(allContributions);
|
|
|
|
Iterator<Contribution> it = filteredList.iterator();
|
|
while (it.hasNext()) {
|
|
Contribution libInfo = it.next();
|
|
//if (category != null && !category.equals(libInfo.getCategory())) {
|
|
if (category != null && !libInfo.hasCategory(category)) {
|
|
it.remove();
|
|
} else {
|
|
for (String filter : filters) {
|
|
if (!matches(libInfo, filter)) {
|
|
it.remove();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return filteredList;
|
|
}
|
|
|
|
|
|
private boolean matches(Contribution contrib, String typed) {
|
|
int colon = typed.indexOf(":");
|
|
if (colon != -1) {
|
|
String isText = typed.substring(0, colon);
|
|
String property = typed.substring(colon + 1);
|
|
|
|
// Chances are the person is still typing the property, so rather than
|
|
// make the list flash empty (because nothing contains "is:" or "has:",
|
|
// just return true.
|
|
if (!isProperty(property)) {
|
|
return true;
|
|
}
|
|
|
|
if ("is".equals(isText) || "has".equals(isText)) {
|
|
return hasProperty(contrib, typed.substring(colon + 1));
|
|
} else if ("not".equals(isText)) {
|
|
return !hasProperty(contrib, typed.substring(colon + 1));
|
|
}
|
|
}
|
|
|
|
typed = ".*" + typed.toLowerCase() + ".*";
|
|
|
|
return (matchField(contrib.getName(), typed) ||
|
|
matchField(contrib.getAuthorList(), typed) ||
|
|
matchField(contrib.getSentence(), typed) ||
|
|
matchField(contrib.getParagraph(), typed) ||
|
|
contrib.hasCategory(typed));
|
|
}
|
|
|
|
|
|
static private boolean matchField(String field, String typed) {
|
|
return (field != null) &&
|
|
removeAccents(field.toLowerCase()).matches(typed);
|
|
}
|
|
|
|
|
|
// TODO is this removing characters with accents, not ascii normalizing them? [fry]
|
|
static private String removeAccents(String str) {
|
|
String nfdNormalizedString = Normalizer.normalize(str, Normalizer.Form.NFD);
|
|
Pattern pattern = Pattern.compile("\\p{InCombiningDiacriticalMarks}+");
|
|
return pattern.matcher(nfdNormalizedString).replaceAll("");
|
|
}
|
|
|
|
|
|
static private boolean isProperty(String property) {
|
|
return property.startsWith("updat") || property.startsWith("upgrad")
|
|
|| property.startsWith("instal") && !property.startsWith("installabl")
|
|
|| property.equals("tool") || property.startsWith("lib")
|
|
|| property.equals("mode") || property.equals("compilation");
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns true if the contribution fits the given property, false otherwise.
|
|
* If the property is invalid, returns false.
|
|
*/
|
|
private boolean hasProperty(Contribution contrib, String property) {
|
|
// update, updates, updatable, upgrade
|
|
if (property.startsWith("updat") || property.startsWith("upgrad")) {
|
|
return hasUpdates(contrib);
|
|
}
|
|
if (property.startsWith("instal") && !property.startsWith("installabl")) {
|
|
return contrib.isInstalled();
|
|
}
|
|
if (property.equals("tool")) {
|
|
return contrib.getType() == ContributionType.TOOL;
|
|
}
|
|
if (property.startsWith("lib")) {
|
|
return contrib.getType() == ContributionType.LIBRARY;
|
|
}
|
|
if (property.equals("mode")) {
|
|
return contrib.getType() == ContributionType.MODE;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/*
|
|
protected List<Contribution> listCompatible(List<Contribution> contribs, boolean filter) {
|
|
List<Contribution> filteredList =
|
|
new ArrayList<Contribution>(contribs);
|
|
|
|
if (filter) {
|
|
Iterator<Contribution> it = filteredList.iterator();
|
|
while (it.hasNext()) {
|
|
Contribution libInfo = it.next();
|
|
if (!libInfo.isCompatible(Base.getRevision())) {
|
|
it.remove();
|
|
}
|
|
}
|
|
}
|
|
return filteredList;
|
|
}
|
|
*/
|
|
|
|
|
|
private void notifyRemove(Contribution contribution) {
|
|
for (ChangeListener listener : listeners) {
|
|
listener.contributionRemoved(contribution);
|
|
}
|
|
}
|
|
|
|
|
|
private void notifyAdd(Contribution contribution) {
|
|
for (ChangeListener listener : listeners) {
|
|
listener.contributionAdded(contribution);
|
|
}
|
|
}
|
|
|
|
|
|
private void notifyChange(Contribution oldLib, Contribution newLib) {
|
|
for (ChangeListener listener : listeners) {
|
|
listener.contributionChanged(oldLib, newLib);
|
|
}
|
|
}
|
|
|
|
|
|
protected void addListener(ChangeListener listener) {
|
|
for (Contribution contrib : allContributions) {
|
|
listener.contributionAdded(contrib);
|
|
}
|
|
listeners.add(listener);
|
|
}
|
|
|
|
|
|
/**
|
|
* Starts a new thread to download the advertised list of contributions.
|
|
* Only one instance will run at a time.
|
|
*/
|
|
public void downloadAvailableList(final Base base,
|
|
final ContribProgressMonitor progress) {
|
|
|
|
// TODO: replace with SwingWorker [jv]
|
|
new Thread(new Runnable() {
|
|
public void run() {
|
|
downloadingListingLock.lock();
|
|
|
|
try {
|
|
URL url = new URL(LISTING_URL);
|
|
// testing port
|
|
// url = new URL("http", "download.processing.org", 8989, "/contribs");
|
|
|
|
// "http://download.processing.org/contribs";
|
|
// System.out.println(url);
|
|
// final String contribInfo =
|
|
// base.getInstalledContribsInfo();
|
|
// "?id=" + Preferences.get("update.id") +
|
|
// "&" + base.getInstalledContribsInfo();
|
|
// url = new URL(LISTING_URL + "?" + contribInfo);
|
|
// System.out.println(contribInfo.length() + " " + contribInfo);
|
|
|
|
File tempContribFile = Base.getSettingsFile("contribs.tmp");
|
|
tempContribFile.setWritable(true);
|
|
ContributionManager.download(url, base.getInstalledContribsInfo(),
|
|
tempContribFile, progress);
|
|
if (!progress.isCanceled() && !progress.isError()) {
|
|
if (listingFile.exists()) {
|
|
listingFile.delete(); // may silently fail, but below may still work
|
|
}
|
|
if (tempContribFile.renameTo(listingFile)) {
|
|
listDownloaded = true;
|
|
listDownloadFailed = false;
|
|
try {
|
|
// TODO: run this in SwingWorker done() [jv]
|
|
EventQueue.invokeAndWait(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
setAdvertisedList(listingFile);
|
|
base.setUpdatesAvailable(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 {
|
|
listDownloadFailed = true;
|
|
}
|
|
}
|
|
|
|
} catch (MalformedURLException e) {
|
|
progress.error(e);
|
|
progress.finished();
|
|
} finally {
|
|
downloadingListingLock.unlock();
|
|
}
|
|
}
|
|
}, "Contribution List Downloader").start();
|
|
}
|
|
|
|
|
|
/*
|
|
boolean hasUpdates(Base base) {
|
|
for (ModeContribution mc : base.getModeContribs()) {
|
|
if (hasUpdates(mc)) {
|
|
return true;
|
|
}
|
|
}
|
|
for (Library lib : base.getActiveEditor().getMode().contribLibraries) {
|
|
if (hasUpdates(lib)) {
|
|
return true;
|
|
}
|
|
}
|
|
for (ToolContribution tc : base.getToolContribs()) {
|
|
if (hasUpdates(tc)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
*/
|
|
|
|
|
|
protected boolean hasUpdates(Contribution contribution) {
|
|
if (contribution.isInstalled()) {
|
|
Contribution advertised = getAvailableContribution(contribution);
|
|
if (advertised == null) {
|
|
return false;
|
|
}
|
|
return advertised.getVersion() > contribution.getVersion()
|
|
&& advertised.isCompatible(Base.getRevision());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
protected String getLatestVersion(Contribution contribution) {
|
|
Contribution newestContrib = getAvailableContribution(contribution);
|
|
if (newestContrib == null) {
|
|
return null;
|
|
}
|
|
return newestContrib.getPrettyVersion();
|
|
}
|
|
|
|
|
|
|
|
protected boolean hasDownloadedLatestList() {
|
|
return listDownloaded;
|
|
}
|
|
|
|
|
|
protected boolean hasListDownloadFailed() {
|
|
return listDownloadFailed;
|
|
}
|
|
|
|
|
|
private List<AvailableContribution> parseContribList(File file) {
|
|
List<AvailableContribution> outgoing =
|
|
new ArrayList<AvailableContribution>();
|
|
|
|
if (file != null && file.exists()) {
|
|
String[] lines = PApplet.loadStrings(file);
|
|
|
|
int start = 0;
|
|
while (start < lines.length) {
|
|
String type = lines[start];
|
|
ContributionType contribType = ContributionType.fromName(type);
|
|
if (contribType == null) {
|
|
System.err.println("Error in contribution listing file on line " + (start+1));
|
|
// Scan forward for the next blank line
|
|
int end = ++start;
|
|
while (end < lines.length && !lines[end].trim().isEmpty()) {
|
|
end++;
|
|
}
|
|
start = end + 1;
|
|
|
|
} else {
|
|
// Scan forward for the next blank line
|
|
int end = ++start;
|
|
while (end < lines.length && !lines[end].trim().isEmpty()) {
|
|
end++;
|
|
}
|
|
|
|
String[] contribLines = PApplet.subset(lines, start, end-start);
|
|
StringDict contribParams = Util.readSettings(file.getName(), contribLines);
|
|
outgoing.add(new AvailableContribution(contribType, contribParams));
|
|
start = end + 1;
|
|
}
|
|
}
|
|
}
|
|
return outgoing;
|
|
}
|
|
|
|
|
|
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
|
|
|
|
|
|
/**
|
|
* TODO This needs to be called when the listing loads, and also whenever
|
|
* the contribs list has been updated (for whatever reason). In addition,
|
|
* the caller (presumably Base) should update all Editor windows with the
|
|
* correct information on the number of items available.
|
|
* @return The number of contributions that have available updates.
|
|
*/
|
|
public int countUpdates(Base base) {
|
|
int count = 0;
|
|
for (ModeContribution mc : base.getModeContribs()) {
|
|
if (hasUpdates(mc)) {
|
|
count++;
|
|
}
|
|
}
|
|
for (Library lib : base.getActiveEditor().getMode().contribLibraries) {
|
|
if (hasUpdates(lib)) {
|
|
count++;
|
|
}
|
|
}
|
|
for (ToolContribution tc : base.getToolContribs()) {
|
|
if (hasUpdates(tc)) {
|
|
count++;
|
|
}
|
|
}
|
|
for (ExamplesContribution ec : base.getExampleContribs()) {
|
|
if (hasUpdates(ec)) {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
|
|
/** Used by JavaEditor to auto-import */
|
|
public Map<String, Contribution> getLibrariesByImportHeader() {
|
|
return librariesByImportHeader;
|
|
}
|
|
|
|
|
|
static public Comparator<Contribution> COMPARATOR = new Comparator<Contribution>() {
|
|
public int compare(Contribution o1, Contribution o2) {
|
|
return o1.getName().toLowerCase().compareTo(o2.getName().toLowerCase());
|
|
}
|
|
};
|
|
|
|
|
|
public interface ChangeListener {
|
|
public void contributionAdded(Contribution Contribution);
|
|
public void contributionRemoved(Contribution Contribution);
|
|
public void contributionChanged(Contribution oldLib, Contribution newLib);
|
|
}
|
|
}
|