/* -*- 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.*;
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, false);
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, false);
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 getLatestPrettyVersion(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);
}
}