package processing.app;
import processing.app.contrib.ContributionType;
import processing.app.contrib.LocalContribution;
import processing.core.PApplet;
import processing.data.StringDict;
import processing.data.StringList;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.*;
import java.util.zip.ZipFile;
public class Library extends LocalContribution {
// static final String[] platformNames = PConstants.platformNames;
static StringDict newToOld = new StringDict(new String[][] {
{ "macos-x86_64", "macosx" },
{ "windows-amd64", "windows64" },
{ "linux-amd64", "linux64" },
{ "linux-arm", "linux-armv6hf" },
{ "linux-aarch64", "linux-arm64" }
});
protected File libraryFolder; // shortname/library
protected File examplesFolder; // shortname/examples
protected File referenceFile; // shortname/reference/index.html
/**
* Subfolder for grouping libraries in a menu. Basic subfolder support
* is provided so that some organization can be done in the import menu.
* (This is the replacement for the "library compilation" type.)
*/
protected String group;
/** Packages provided by this library. */
StringList packageList;
/** Per-platform exports for this library. */
Map exportList;
/** List of default exports */
String[] baseList;
/** Android exports (single platform for now, may not exist). */
String[] androidExportList;
// /** True if there are separate 32/64 bit for the specified platform index. */
// boolean[] multipleArch = new boolean[platformNames.length];
// Map multipleArch = new HashMap();
/**
* For runtime, the native library path for this platform. e.g. on Windows 64,
* this might be the windows64 subfolder with the library.
*/
String nativeLibraryPath;
// /** True if */
// boolean variants;
static public final String propertiesFileName = "library.properties";
/**
* Filter to pull out just files and none of the platform-specific
* directories, and to skip export.txt.
*
* As of 2.0a2, other directories are included, because we need
* things like the 'plugins' subfolder w/ video.
*
* As of 4.0b4, only checking whether macos, windows, linux, or
* android are the prefix of the folder name, so that we can avoid
* explicitly listing all possible architectures, and so that
* macos-blah as well as and macosx will be handled properly.
*/
static FilenameFilter libraryFolderFilter = (dir, name) -> {
// skip .DS_Store files, .svn folders, etc
if (name.charAt(0) == '.') return false;
// ha, the sftp library still has one [fry 220121]
if (name.equals("CVS")) return false;
if (name.equals("export.txt")) return false;
File file = new File(dir, name);
if (file.isDirectory()) {
//noinspection RedundantIfStatement
if (name.startsWith("macos") ||
name.startsWith("windows") ||
name.startsWith("linux")) {
//name.startsWith("android")) { // no libraries use this
return false;
}
}
return true;
};
static FilenameFilter jarFilter = (dir, name) -> {
if (name.charAt(0) == '.') return false; // skip ._blah.jar crap on OS X
if (new File(dir, name).isDirectory()) return false;
String lc = name.toLowerCase();
return lc.endsWith(".jar") || lc.endsWith(".zip");
};
static public Library load(File folder) {
try {
return new Library(folder);
// } catch (IgnorableException ig) {
// Base.log(ig.getMessage());
} catch (Error err) {
// Handles UnsupportedClassVersionError and others
err.printStackTrace();
}
return null;
}
public Library(File folder) {
this(folder, null);
}
private Library(File folder, String groupName) {
super(folder);
this.group = groupName;
libraryFolder = new File(folder, "library");
examplesFolder = new File(folder, "examples");
referenceFile = new File(folder, "reference/index.html");
handle();
}
/**
* Handles all the Java-specific parsing for library handling.
*/
protected void handle() {
handleNative();
handleExports();
}
/**
* Identify nativeLibraryFolder location for the current platform.
*/
private void handleNative() {
String variant = Platform.getVariant();
// use the root of the library folder as the default
File nativeLibraryFolder = libraryFolder;
/*
String hostPlatform = Platform.getName();
// see if there's a 'windows', 'macosx', or 'linux' folder
File hostLibrary = new File(libraryFolder, hostPlatform);
if (hostLibrary.exists()) {
nativeLibraryFolder = hostLibrary;
}
*/
// see if there's a {platform}-{arch} folder
File hostLibrary = new File(libraryFolder, variant);
if (hostLibrary.exists()) {
nativeLibraryFolder = hostLibrary;
} else {
// if not found, try the old-style naming
String oldName = newToOld.get(variant);
if (oldName != null) {
hostLibrary = new File(libraryFolder, oldName);
if (hostLibrary.exists()) {
nativeLibraryFolder = hostLibrary;
}
}
}
// save that folder for later use
nativeLibraryPath = nativeLibraryFolder.getAbsolutePath();
}
private void handleExports() {
/*
File exportSettings = new File(libraryFolder, "export.txt");
StringDict exportTable = exportSettings.exists() ?
Util.readSettings(exportSettings) : new StringDict();
*/
exportList = new HashMap();
// get the list of files just in the library root
baseList = libraryFolder.list(libraryFolderFilter);
for (String variant : Platform.getSupportedVariants().keys()) {
File variantFolder = new File(libraryFolder, variant);
if (!variantFolder.exists()) {
// check to see if old naming is in use
String oldName = newToOld.get(variant, null);
if (oldName != null) {
variantFolder = new File(libraryFolder, variant);
if (variantFolder.exists()) {
Messages.log("Please update " + getName() + " for Processing 4. " +
variantFolder + " is the older naming scheme.");
}
}
}
if (variantFolder.exists()) {
String[] entries = listPlatformEntries(libraryFolder, variant, baseList);
if (entries != null) {
exportList.put(variant, entries);
}
}
}
/*
// not actually used in any libraries
String androidExportStr = exportTable.get("android");
if (androidExportStr != null) {
androidExportList = PApplet.splitTokens(androidExportStr, ", ");
} else {
androidExportList = baseList;
}
*/
/*
// for each individual platform that this library supports, figure out what's around
for (int i = 1; i < platformNames.length; i++) {
String platformName = platformNames[i];
String platformName32 = platformName + "32";
String platformName64 = platformName + "64";
String platformNameArmv6hf = platformName + "-armv6hf";
String platformNameArm64 = platformName + "-arm64";
// First check for things like 'application.macosx=' or 'application.windows32' in the export.txt file.
// These will override anything in the platform-specific subfolders.
String platformAll = exportTable.get("application." + platformName);
String[] platformList = platformAll == null ? null : PApplet.splitTokens(platformAll, ", ");
String platform32 = exportTable.get("application." + platformName + "32");
String[] platformList32 = platform32 == null ? null : PApplet.splitTokens(platform32, ", ");
String platform64 = exportTable.get("application." + platformName + "64");
String[] platformList64 = platform64 == null ? null : PApplet.splitTokens(platform64, ", ");
String platformArmv6hf = exportTable.get("application." + platformName + "-armv6hf");
String[] platformListArmv6hf = platformArmv6hf == null ? null : PApplet.splitTokens(platformArmv6hf, ", ");
String platformArm64 = exportTable.get("application." + platformName + "-arm64");
String[] platformListArm64 = platformArm64 == null ? null : PApplet.splitTokens(platformArm64, ", ");
// If nothing specified in the export.txt entries, look for the platform-specific folders.
if (platformAll == null) {
platformList = listPlatformEntries(libraryFolder, platformName, baseList);
}
if (platform32 == null) {
platformList32 = listPlatformEntries(libraryFolder, platformName32, baseList);
}
if (platform64 == null) {
platformList64 = listPlatformEntries(libraryFolder, platformName64, baseList);
}
if (platformListArmv6hf == null) {
platformListArmv6hf = listPlatformEntries(libraryFolder, platformNameArmv6hf, baseList);
}
if (platformListArm64 == null) {
platformListArm64 = listPlatformEntries(libraryFolder, platformNameArm64, baseList);
}
if (platformList32 != null || platformList64 != null || platformListArmv6hf != null || platformListArm64 != null) {
//multipleArch[i] = true;
multipleArch.put(platformName, true);
}
// if there aren't any relevant imports specified or in their own folders,
// then use the baseList (root of the library folder) as the default.
if (platformList == null && platformList32 == null && platformList64 == null && platformListArmv6hf == null && platformListArm64 == null) {
exportList.put(platformName, baseList);
} else {
// once we've figured out which side our bread is buttered on, save it.
// (also concatenate the list of files in the root folder as well
if (platformList != null) {
exportList.put(platformName, platformList);
}
if (platformList32 != null) {
exportList.put(platformName32, platformList32);
}
if (platformList64 != null) {
exportList.put(platformName64, platformList64);
}
if (platformListArmv6hf != null) {
exportList.put(platformNameArmv6hf, platformListArmv6hf);
}
if (platformListArm64 != null) {
exportList.put(platformNameArm64, platformListArm64);
}
}
}
*/
// for (String p : exportList.keySet()) {
// System.out.println(p + " -> ");
// PApplet.println(exportList.get(p));
// }
// get the path for all .jar files in this code folder
packageList = Util.packageListFromClassPath(getClassPath());
}
/**
* List who's inside a windows64, macosx, linux32, etc folder.
*/
static String[] listPlatformEntries(File libraryFolder, String folderName, String[] baseList) {
File folder = new File(libraryFolder, folderName);
if (folder.exists()) {
String[] entries = folder.list((dir, name) -> name.charAt(0) != '.');
if (entries != null) {
String[] outgoing = new String[entries.length + baseList.length];
for (int i = 0; i < entries.length; i++) {
outgoing[i] = folderName + "/" + entries[i];
}
// Copy the base libraries in there as well
System.arraycopy(baseList, 0, outgoing, entries.length, baseList.length);
return outgoing;
}
}
return null;
}
//static protected HashMap packageWarningMap = new HashMap();
/**
* Add the packages provided by this library to the master list that maps
* imports to specific libraries.
* @param importToLibraryTable mapping from package names to Library objects
*/
static boolean instructed = false;
// public void addPackageList(HashMap importToLibraryTable) {
public void addPackageList(Map importToLibraryTable) {
// PApplet.println(packages);
for (String pkg : packageList) {
// pw.println(pkg + "\t" + libraryFolder.getAbsolutePath());
// PApplet.println(pkg + "\t" + getName());
// Library library = importToLibraryTable.get(pkg);
List libraries = importToLibraryTable.get(pkg);
if (libraries == null) {
libraries = new ArrayList();
importToLibraryTable.put(pkg, libraries);
} else {
if(!instructed) {
instructed = true;
Messages.err("The library found in");
Messages.err(getPath());
Messages.err("conflicts with");
for (Library library : libraries) {
Messages.err(library.getPath());
}
Messages.err("which already define(s) the package " + pkg);
Messages.err("If you have a line in your sketch that reads");
Messages.err("import " + pkg + ".*;");
Messages.err("Then you'll need to first remove one of those libraries.");
}else{
Messages.err("\tPackage ("+pkg+")\t conflict found in [" + name + "] with libraries: " + libraries.stream().map(Library::getName).reduce((a, b) -> a + ", " + b).orElse(""));
}
}
libraries.add(this);
}
}
public boolean hasExamples() {
return examplesFolder.exists();
}
public File getExamplesFolder() {
return examplesFolder;
}
public String getGroup() {
return group;
}
public String getPath() {
return folder.getAbsolutePath();
}
public String getLibraryPath() {
return libraryFolder.getAbsolutePath();
}
public String getJarPath() {
//return new File(folder, "library/" + name + ".jar").getAbsolutePath();
return new File(libraryFolder, folder.getName() + ".jar").getAbsolutePath();
}
// the returned value begins with File.pathSeparatorChar
// so that it can be appended to other paths safely
public String getClassPath() {
StringBuilder cp = new StringBuilder();
addJars(libraryFolder, cp);
File nativeLibraryFolder = new File(nativeLibraryPath);
if (!libraryFolder.equals(nativeLibraryFolder)) {
addJars(nativeLibraryFolder, cp);
addJars(new File(nativeLibraryFolder, "modules"), cp);
}
return cp.toString();
}
private void addJars(File folder, StringBuilder cp) {
String[] jarHeads = folder.list(jarFilter);
if (jarHeads != null) {
for (String jar : jarHeads) {
cp.append(File.pathSeparatorChar);
cp.append(new File(folder, jar).getAbsolutePath());
}
}
}
public String getNativePath() {
return nativeLibraryPath;
}
protected File[] wrapFiles(String[] list) {
File[] outgoing = new File[list.length];
for (int i = 0; i < list.length; i++) {
outgoing[i] = new File(libraryFolder, list[i]);
}
return outgoing;
}
public File[] getApplicationExports(String variant) {
String[] list = getApplicationExportList(variant);
return wrapFiles(list);
}
/*
public File[] getApplicationExports(int platform, String variant) {
String[] list = getApplicationExportList(platform, variant);
return wrapFiles(list);
}
// * Returns the necessary exports for the specified platform.
// * If no 32 or 64-bit version of the exports exists, it returns the version
// * that doesn't specify bit depth.
public String[] getApplicationExportList(int platform, String variant) {
String platformName = PConstants.platformNames[platform];
if (variant.equals("32")) {
String[] pieces = exportList.get(platformName + "32");
if (pieces != null) return pieces;
} else if (variant.equals("64")) {
String[] pieces = exportList.get(platformName + "64");
if (pieces != null) return pieces;
} else if (variant.equals("armv6hf")) {
String[] pieces = exportList.get(platformName + "-armv6hf");
if (pieces != null) return pieces;
} else if (variant.equals("arm64")) {
String[] pieces = exportList.get(platformName + "-arm64");
if (pieces != null) return pieces;
}
return exportList.get(platformName);
}
*/
public String[] getApplicationExportList(String variant) {
if (exportList.isEmpty()) {
return baseList;
}
return exportList.get(variant);
}
@SuppressWarnings("unused")
public File[] getAndroidExports() {
return wrapFiles(androidExportList);
}
/*
public boolean hasMultipleArch(int platform) {
//return multipleArch[platform];
return multipleArch.getOrDefault(platform, false);
}
*/
/*
public boolean supportsArch(int platform, String variant) {
// If this is a universal library, or has no natives, then we're good.
if (multipleArch[platform] == false) {
return true;
}
return getApplicationExportList(platform, variant) != null;
}
public boolean isUnsupported(int platform, String variant) {
// If this is a universal library, or has no natives, then we're good.
if (!multipleArch.containsKey(platformNames[platform])) {
return false;
}
return getApplicationExportList(platform, variant) == null;
}
*/
public boolean isUnsupported(String variant) {
if (exportList.isEmpty()) {
// if no per-platform exports, then nothing to worry about
return false;
}
return getApplicationExportList(variant) == null;
}
/*
static public boolean hasMultipleArch(int platform, List libraries) {
return libraries.stream().anyMatch(library -> library.hasMultipleArch(platform));
}
*/
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
static protected FilenameFilter junkFolderFilter = (dir, name) -> {
// skip .DS_Store files, .svn and .git folders, etc
if (name.charAt(0) == '.') return false;
if (name.equals("CVS")) return false; // old skool
return new File(dir, name).isDirectory();
};
static public String findCollision(File folder) {
File[] jars = PApplet.listFiles(folder, "recursive", "extension=jar");
if (jars != null) {
for (File file : jars) {
try {
ZipFile zf = new ZipFile(file);
if (zf.getEntry("processing/core/PApplet.class") != null) {
return "processing.core";
}
if (zf.getEntry("processing/app/Base.class") != null) {
return "processing.app";
}
} catch (IOException e) {
// ignored
}
}
}
return null;
}
static public List discover(File folder) {
List libraries = new ArrayList();
String[] folderNames = folder.list(junkFolderFilter);
// if a bad folder or unreadable, folderNames might be null
if (folderNames != null) {
// alphabetize list, since it's not always alpha order
// replaced hella slow bubble sort with this feller for 0093
Arrays.sort(folderNames, String.CASE_INSENSITIVE_ORDER);
// TODO a little odd because ContributionType.LIBRARY.isCandidate()
// handles some, but not all, of this; and the rules of selection
// should probably be consolidated in a sensible way [fry 200116]
for (String potentialName : folderNames) {
File baseFolder = new File(folder, potentialName);
File libraryFolder = new File(baseFolder, "library");
File libraryJar = new File(libraryFolder, potentialName + ".jar");
// If a .jar file of the same prefix as the folder exists
// inside the 'library' subfolder of the sketch
if (libraryJar.exists()) {
String sanityCheck = Sketch.sanitizeName(potentialName);
if (!sanityCheck.equals(potentialName)) {
final String mess =
"The library \"" + potentialName + "\" cannot be used.\n" +
"Library names must contain only basic letters and numbers.\n" +
"(ASCII only and no spaces, and it cannot start with a number)";
Messages.showMessage("Ignoring bad library name", mess);
} else {
String pkg = findCollision(libraryFolder);
if (pkg != null) {
final String mess =
"The library \"" + potentialName + "\" cannot be used\n" +
"because it contains the " + pkg + " libraries.\n" +
"Please contact the library author for an update.";
Messages.showMessage("Ignoring bad library", mess);
// Move the folder out of the way
File badFolder = new File(baseFolder.getParentFile(), "disabled");
boolean success = true;
if (!badFolder.exists()) {
success = badFolder.mkdirs();
}
if (success) {
File hideFolder = new File(badFolder, baseFolder.getName());
success = baseFolder.renameTo(hideFolder);
if (success) {
System.out.println("Moved " + baseFolder + " to " + hideFolder);
} else {
System.err.println("Could not move " + baseFolder + " to " + hideFolder);
}
}
} else {
libraries.add(baseFolder);
}
}
}
}
}
return libraries;
}
static Set duplicateLibraries = new HashSet();
static public List list(File folder) {
List libraries = new ArrayList();
List librariesFolders = new ArrayList(discover(folder));
Map seen = new HashMap();
for (File baseFolder : librariesFolders) {
Library lib = new Library(baseFolder);
String name = lib.getName();
File foundEarlier = seen.get(name);
if (foundEarlier != null) {
// Warn the user about this duplication (later, on the EDT)
if (!duplicateLibraries.contains(name)) {
Messages.showWarningTiered("Duplicate Library Found",
"There are multiple libraries named " + name + "",
"Please remove either " + foundEarlier.getName() +
" or " + lib.getName() + "\n" +
"from " + folder.getPath() + "\n" +
"and restart Processing.", null);
}
duplicateLibraries.add(name);
} else {
seen.put(name, lib.getFolder());
libraries.add(lib);
}
}
return libraries;
}
public ContributionType getType() {
return ContributionType.LIBRARY;
}
/**
* Returns the object stored in the referenceFile field, which contains an
* instance of the file object representing the index file of the reference
*
* @return referenceFile
*/
public File getReferenceIndexFile() {
return referenceFile;
}
/**
* Tests whether the reference's index file indicated by referenceFile exists.
*
* @return true if and only if the file denoted by referenceFile exists; false
* otherwise.
*/
public boolean hasReference() {
return referenceFile.exists();
}
}