/*
GNU GENERAL PUBLIC LICENSE
Copyright (C) 2006 The Lobo Project
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public
License as published by the Free Software Foundation; either
verion 2 of the License, or (at your option) any later version.
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 library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Contact info: lobochief@users.sourceforge.net
*/
package org.lobobrowser.main;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.SortedSet;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipInputStream;
import javax.swing.SwingUtilities;
import org.lobobrowser.clientlet.Clientlet;
import org.lobobrowser.clientlet.ClientletRequest;
import org.lobobrowser.clientlet.ClientletResponse;
import org.lobobrowser.ua.NavigationEvent;
import org.lobobrowser.ua.NavigationVetoException;
import org.lobobrowser.ua.NavigatorEventType;
import org.lobobrowser.ua.NavigatorExceptionEvent;
import org.lobobrowser.ua.NavigatorFrame;
import org.lobobrowser.ua.NavigatorWindow;
import org.lobobrowser.ua.RequestType;
import org.lobobrowser.util.JoinableTask;
/**
* Manages platform extensions.
*/
public class ExtensionManager {
public static final String ZIPENTRY_PROTOCOL = "zipentry";
private static final Logger logger = Logger.getLogger(ExtensionManager.class.getName());
private static final ExtensionManager instance = new ExtensionManager();
private static final String EXT_DIR_NAME = "ext";
// Note: We do not synchronize around the extensions collection,
// given that it is fully built in the constructor.
private final Map<String, Extension> extensionById = new HashMap<>();
private final SortedSet<Extension> extensions = new TreeSet<>();
private final ArrayList<URL> libraryURLs = new ArrayList<>();
private ExtensionManager() {
this.createExtensionsAndLibraries(getExtDirs(), getExtFiles());
}
public static ExtensionManager getInstance() {
// This security check should be enough, provided
// ExtensionManager instances are not retained.
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(org.lobobrowser.security.GenericLocalPermission.EXT_GENERIC);
}
return instance;
}
public static File[] getExtDirs() {
File[] extDirs;
final String extDirsProperty = System.getProperty("ext.dirs");
if (extDirsProperty == null) {
final Optional<File> appDirOpt = PlatformInit.getInstance().getApplicationDirectory();
if (appDirOpt.isPresent()) {
extDirs = new File[] { new File(appDirOpt.get(), EXT_DIR_NAME) };
} else {
extDirs = new File[0];
}
} else {
final StringTokenizer tok = new StringTokenizer(extDirsProperty, ",");
final ArrayList<File> extDirsList = new ArrayList<>();
while (tok.hasMoreTokens()) {
final String token = tok.nextToken();
extDirsList.add(new File(token.trim()));
}
extDirs = extDirsList.toArray(new File[0]);
}
return extDirs;
}
public static File[] getExtFiles() {
File[] extFiles;
final String extFilesPropertySystem = System.getProperty("ext.files");
final String extFilesProperty = extFilesPropertySystem == null ? System.getProperty("jnlp.ext.files") : extFilesPropertySystem;
if (extFilesProperty == null) {
extFiles = new File[0];
} else {
final StringTokenizer tok = new StringTokenizer(extFilesProperty, ",");
final ArrayList<File> extFilesList = new ArrayList<>();
while (tok.hasMoreTokens()) {
final String token = tok.nextToken();
extFilesList.add(new File(token.trim()));
}
extFiles = extFilesList.toArray(new File[0]);
}
return extFiles;
}
private void addExtension(final File file) throws java.io.IOException {
if (!file.exists()) {
logger.warning("addExtension(): File " + file + " does not exist.");
return;
}
if (Extension.isExtension(file)) {
addExtension(new Extension(file));
} else {
libraryURLs.add(file.toURI().toURL());
}
}
private void addExtension(final Extension ei) {
this.extensionById.put(ei.getId(), ei);
if (logger.isLoggable(Level.FINE)) {
logger.fine("createExtensions(): Loaded extension: " + ei);
}
extensions.add(ei);
}
private void createExtensionsAndLibraries(final File[] extDirs, final File[] extFiles) {
final Collection<Extension> extensions = this.extensions;
final Map<String, Extension> extensionById = this.extensionById;
extensions.clear();
extensionById.clear();
final List<URL> libraryEntryURLs = new LinkedList<>();
addFlatExtensions();
for (final File extDir : extDirs) {
if (!extDir.exists()) {
logger.warning("createExtensions(): Directory '" + extDir + "' not found.");
if (PlatformInit.getInstance().isCodeLocationDirectory()) {
logger
.warning("createExtensions(): The application code location is a directory, which means the application is probably being run from an IDE. Additional setup is required. Please refer to README.txt file.");
}
continue;
}
if (extDir.isFile()) {
// Check if it is a jar. We will load jars from inside this jar.
try (
final JarFile jf = new JarFile(extDir);) {
// We can't close jf, because the class loader will load files lazily.
for (final JarEntry jarEntry : (Iterable<JarEntry>) jf.stream()::iterator) {
if (!jarEntry.isDirectory() && jarEntry.getName().endsWith(".jar")) {
System.out.println("Found entry: " + jarEntry.getName());
final InputStream jfIS = jf.getInputStream(jarEntry);
final URL libURL = makeZipEntryURL(extDir.getName(), jfIS, jarEntry.getName());
libraryEntryURLs.add(libURL);
}
}
} catch (final IOException e) {
logger.warning("Couldn't open: " + extDir);
e.printStackTrace();
}
} else {
final File[] extRoots = extDir.listFiles(new ExtFileFilter());
if ((extRoots == null) || (extRoots.length == 0)) {
logger.warning("createExtensions(): No potential extensions found in " + extDir + " directory.");
continue;
}
addAllFileExtensions(extRoots);
}
}
addAllFileExtensions(extFiles);
if (this.extensionById.size() == 0) {
logger.warning("createExtensions(): No extensions found. This is indicative of a setup error. Extension directories scanned are: "
+ Arrays.asList(extDirs) + ".");
}
loadExtensions(extensions, libraryURLs);
}
private void addFlatExtensions() {
// TODO: in future, avoid using the flat-extensions file. All resources matching
// a standard name like "lobo-extension.properties" can be automatically fetched
// using ClassLoader.getResources() method. Uno needs to implement it though (needs URL magic).
final ClassLoader loader = getClass().getClassLoader();
final InputStream indexStream = getClass().getResourceAsStream("/flat-extensions");
if (indexStream != null) {
final BufferedReader indexReader = new BufferedReader(new InputStreamReader(indexStream));
try {
String propertyFileName;
while ((propertyFileName = indexReader.readLine()) != null) {
final InputStream propertyStream = loader.getResourceAsStream(propertyFileName);
final Properties extensionAttributes = new Properties();
extensionAttributes.load(propertyStream);
addExtension(new Extension(extensionAttributes, loader));
}
} catch (final IOException e) {
logger.log(Level.SEVERE, "Error while reading embedded resources", e);
}
}
}
/*
private void addEmbeddedJars(final List<URL> libraryEntryURLs) {
final ClassLoader loader = getClass().getClassLoader();
final InputStream indexStream = getClass().getResourceAsStream("/jar-index");
System.out.println("jar-index: " + indexStream);
if (indexStream != null) {
final BufferedReader indexReader = new BufferedReader(new InputStreamReader(indexStream));
try {
String fileName;
while ((fileName = indexReader.readLine()) != null) {
System.out.println("Filename: " + fileName);
final InputStream entryStream = loader.getResourceAsStream(fileName);
final URL entryURL = makeZipEntryURL("embedded-jar", entryStream, fileName);
if (fileName.endsWith("Primary_Extension.jar")) {
final Properties extensionAttributes = new Properties();
extensionAttributes.put("extension.class", "org.lobobrowser.primary.ext.ExtensionImpl");
addExtension(new Extension(entryURL, extensionAttributes, loader));
} else {
libraryEntryURLs.add(entryURL);
}
}
} catch (IOException e) {
logger.log(Level.SEVERE, "Error while reading embedded resources", e);
}
}
}*/
private static URL makeZipEntryURL(final String dirName, final InputStream jfIS, final String entryName) throws IOException,
MalformedURLException {
final String urlSpec = ZIPENTRY_PROTOCOL + "://" + dirName + "/" + entryName + "!/";
final URL libURL = new URL(null, urlSpec, new ZipEntryHandler(new ZipInputStream(jfIS)));
return libURL;
}
private void loadExtensions(final Collection<Extension> extensions,
final Collection<URL> libraryURLCollection) {
// Get the system class loader
final ClassLoader rootClassLoader = this.getClass().getClassLoader();
final URLClassLoader librariesCL = new URLClassLoader(libraryURLCollection.toArray(new URL[0]), rootClassLoader);
// Initialize class loader in each extension, using librariesCL as
// the parent class loader. Extensions are initialized in parallel.
final Collection<JoinableTask> tasks = new ArrayList<>();
final PlatformInit pm = PlatformInit.getInstance();
for (final Extension ei : extensions) {
final Extension fei = ei;
// Initialize rest of them in parallel.
final JoinableTask task = new JoinableTask() {
@Override
public void execute() {
try {
fei.initClassLoader(librariesCL);
} catch (final Exception err) {
logger.log(Level.WARNING, "Unable to create class loader for " + fei + ".", err);
}
}
@Override
public String toString() {
return "createExtensions:" + fei;
}
};
tasks.add(task);
pm.scheduleTask(task);
}
// Join tasks to make sure all extensions are initialized at this point.
for (final JoinableTask task : tasks) {
try {
task.join();
} catch (final InterruptedException ie) {
// TODO
// ignore
}
}
}
private void addAllFileExtensions(final File[] extRoots) {
for (final File file : extRoots) {
try {
this.addExtension(file);
} catch (final IOException ioe) {
logger.log(Level.WARNING, "createExtensions(): Unable to load '" + file + "'.", ioe);
}
}
}
/*
public ClassLoader getClassLoader(final String extensionId) {
final Extension ei = this.extensionById.get(extensionId);
if (ei != null) {
return ei.getClassLoader();
} else {
return null;
}
}*/
public void initExtensions() {
final Collection<JoinableTask> tasks = new ArrayList<>();
final PlatformInit pm = PlatformInit.getInstance();
for (final Extension ei : this.extensions) {
final JoinableTask task = new JoinableTask() {
@Override
public void execute() {
ei.initExtension();
}
@Override
public String toString() {
return "initExtensions:" + ei;
}
};
tasks.add(task);
pm.scheduleTask(task);
}
// Join all tasks before returning
for (final JoinableTask task : tasks) {
try {
task.join();
} catch (final InterruptedException ie) {
// ignore
}
}
}
public void initExtensionsWindow(final NavigatorWindow context) {
// This must be done sequentially due to menu lookup infrastructure.
for (final Extension ei : this.extensions) {
try {
ei.initExtensionWindow(context);
} catch (final Exception err) {
logger.log(Level.SEVERE, "initExtensionsWindow(): Extension could not properly initialize a new window.", err);
}
}
}
public void shutdownExtensionsWindow(final NavigatorWindow context) {
// This must be done sequentially due to menu lookup infrastructure.
for (final Extension ei : this.extensions) {
try {
ei.shutdownExtensionWindow(context);
} catch (final Exception err) {
logger.log(Level.SEVERE, "initExtensionsWindow(): Extension could not properly process window shutdown.", err);
}
}
}
public Clientlet getClientlet(final ClientletRequest request, final ClientletResponse response) {
final Collection<Extension> extensions = this.extensions;
// Call all plugins once to see if they can select the response.
for (final Extension ei : extensions) {
try {
final Clientlet clientlet = ei.getClientlet(request, response);
if (clientlet != null) {
return clientlet;
}
} catch (final Exception thrown) {
logger.log(Level.SEVERE, "getClientlet(): Extension " + ei + " threw exception.", thrown);
}
}
// None handled it. Call the last resort handlers in reverse order.
for (final Extension ei : org.lobobrowser.util.CollectionUtilities.reverse(extensions)) {
try {
final Clientlet clientlet = ei.getLastResortClientlet(request, response);
if (clientlet != null) {
return clientlet;
}
} catch (final Exception thrown) {
logger.log(Level.SEVERE, "getClientlet(): Extension " + ei + " threw exception.", thrown);
}
}
return null;
}
public void handleError(final NavigatorFrame frame, final ClientletResponse response, final Throwable exception,
final RequestType requestType) {
final NavigatorExceptionEvent event = new NavigatorExceptionEvent(this, NavigatorEventType.ERROR_OCCURRED, frame, response, exception,
requestType);
SwingUtilities.invokeLater(() -> {
final Collection<Extension> ext = extensions;
// Call all plugins once to see if they can select the response.
boolean dispatched = false;
for (final Extension ei : ext) {
if (ei.handleError(event)) {
dispatched = true;
}
}
if (!dispatched && logger.isLoggable(Level.INFO)) {
logger.log(Level.WARNING, "No error handlers found for error that occurred while processing response=[" + response + "].",
exception);
}
});
}
public void dispatchBeforeNavigate(final NavigationEvent event) throws NavigationVetoException {
for (final Extension ei : extensions) {
try {
ei.dispatchBeforeLocalNavigate(event);
} catch (final NavigationVetoException nve) {
throw nve;
} catch (final Exception other) {
logger.log(Level.SEVERE, "dispatchBeforeNavigate(): Extension threw an unexpected exception.", other);
}
}
}
public void dispatchBeforeLocalNavigate(final NavigationEvent event) throws NavigationVetoException {
for (final Extension ei : extensions) {
try {
ei.dispatchBeforeLocalNavigate(event);
} catch (final NavigationVetoException nve) {
throw nve;
} catch (final Exception other) {
logger.log(Level.SEVERE, "dispatchBeforeLocalNavigate(): Extension threw an unexpected exception.", other);
}
}
}
public void dispatchBeforeWindowOpen(final NavigationEvent event) throws NavigationVetoException {
for (final Extension ei : extensions) {
try {
ei.dispatchBeforeWindowOpen(event);
} catch (final NavigationVetoException nve) {
throw nve;
} catch (final Exception other) {
logger.log(Level.SEVERE, "dispatchBeforeWindowOpen(): Extension threw an unexpected exception.", other);
}
}
}
public URLConnection dispatchPreConnection(URLConnection connection) {
for (final Extension ei : extensions) {
try {
connection = ei.dispatchPreConnection(connection);
} catch (final Exception other) {
logger.log(Level.SEVERE, "dispatchPreConnection(): Extension threw an unexpected exception.", other);
}
}
return connection;
}
public URLConnection dispatchPostConnection(URLConnection connection) {
for (final Extension ei : extensions) {
try {
connection = ei.dispatchPostConnection(connection);
} catch (final Exception other) {
logger.log(Level.SEVERE, "dispatchPostConnection(): Extension threw an unexpected exception.", other);
}
}
return connection;
}
private static class ExtFileFilter implements FileFilter {
public boolean accept(final File file) {
return file.isDirectory() || file.getName().toLowerCase().endsWith(".jar");
}
}
}