/*
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.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandlerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EventListener;
import java.util.EventObject;
import java.util.LinkedList;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.lobobrowser.clientlet.Clientlet;
import org.lobobrowser.clientlet.ClientletRequest;
import org.lobobrowser.clientlet.ClientletResponse;
import org.lobobrowser.clientlet.ClientletSelector;
import org.lobobrowser.ua.ConnectionProcessor;
import org.lobobrowser.ua.NavigationEvent;
import org.lobobrowser.ua.NavigationListener;
import org.lobobrowser.ua.NavigationVetoException;
import org.lobobrowser.ua.NavigatorErrorListener;
import org.lobobrowser.ua.NavigatorExceptionEvent;
import org.lobobrowser.ua.NavigatorExtension;
import org.lobobrowser.ua.NavigatorExtensionContext;
import org.lobobrowser.ua.NavigatorWindow;
import org.lobobrowser.ua.UserAgent;
import org.lobobrowser.util.ArrayUtilities;
import org.lobobrowser.util.EventDispatch2;
/**
* Encapsulates a browser extension or plugin.
*/
public class Extension implements Comparable<Object>, NavigatorExtensionContext {
private static final String ATTRIBUTE_EXTENSION_CLASS = "extension.class";
private static final String ATTRIBUTE_EXTENSION_PRIORITY = "extension.priority";
private static final String EXTENSION_PROPERTIES_FILE = "gngr-extension.properties";
private static final String PRIMARY_EXTENSION_FILE_NAME = "primary.jar";
/**
* The minimum priority.
*/
public static final int LOW_PRIORITY = 0;
/**
* The highest priority, only available to the default platform extension.
*/
public static final int PRIMARY_EXTENSION_PRIORITY = 10;
/**
* The highest priority allowed for non-primary platform extensions.
*/
public static final int HIGH_PRIORITY = 9;
/**
* The default priority.
*/
public static final int NORMAL_PRIORITY = 5;
private final int priority;
private final File extRoot;
private final String extClassName;
private final String extId;
private final boolean isPrimary;
// TODO: Move these collections to ExtensionManager.
// More efficient. Consider removal of extensions.
private final Collection<ClientletSelector> clientletSelectors;
private final Collection<ConnectionProcessor> connectionProcessors;
private final Collection<NavigationListener> navigationListeners;
private final EventDispatch2 EVENT = new NavigatorErrorEventDispatch();
public static boolean isExtension(final File root) {
if (root.isDirectory()) {
final File propsFile = new File(root, EXTENSION_PROPERTIES_FILE);
return propsFile.exists();
} else {
try (
final JarFile jarFile = new JarFile(root)) {
final JarEntry jarEntry = jarFile.getJarEntry(EXTENSION_PROPERTIES_FILE);
return jarEntry != null;
} catch (final IOException e) {
return false;
}
}
}
public Extension(final Properties mattribs, final ClassLoader parentClassLoader) {
this.extRoot = null;
this.extClassName = mattribs.getProperty(ATTRIBUTE_EXTENSION_CLASS);
this.extId = extClassName;
this.clientletSelectors = new LinkedList<>();
this.connectionProcessors = new ArrayList<>();
this.navigationListeners = new ArrayList<>();
this.priority = PRIMARY_EXTENSION_PRIORITY;
this.isPrimary = true;
}
public Extension(final File extRoot) throws IOException {
this.clientletSelectors = new LinkedList<>();
this.connectionProcessors = new ArrayList<>();
this.navigationListeners = new ArrayList<>();
this.extRoot = extRoot;
JarFile jarFile;
InputStream propsInputStream;
if (extRoot.isDirectory()) {
this.isPrimary = false;
jarFile = null;
this.extId = extRoot.getName();
final File propsFile = new File(extRoot, EXTENSION_PROPERTIES_FILE);
propsInputStream = propsFile.exists() ? new FileInputStream(propsFile) : null;
} else {
jarFile = new JarFile(extRoot);
this.isPrimary = extRoot.getName().toLowerCase().equals(PRIMARY_EXTENSION_FILE_NAME);
final String name = extRoot.getName();
final int dotIdx = name.lastIndexOf('.');
this.extId = dotIdx == -1 ? name : name.substring(0, dotIdx);
final JarEntry jarEntry = jarFile.getJarEntry(EXTENSION_PROPERTIES_FILE);
propsInputStream = jarEntry == null ? null : jarFile.getInputStream(jarEntry);
}
final boolean isLibrary = propsInputStream == null;
if (!isLibrary) {
final Properties mattribs = new Properties();
try {
mattribs.load(propsInputStream);
} finally {
propsInputStream.close();
}
final String extClassName = mattribs.getProperty(ATTRIBUTE_EXTENSION_CLASS);
if (extClassName == null) {
throw new IOException("Property " + ATTRIBUTE_EXTENSION_CLASS + " missing in " + EXTENSION_PROPERTIES_FILE + ", part of " + extRoot
+ ".");
}
this.extClassName = extClassName;
final String priorityText = mattribs.getProperty(ATTRIBUTE_EXTENSION_PRIORITY);
if (priorityText != null) {
final int tp = Integer.parseInt(priorityText.trim());
this.priority = Math.min(HIGH_PRIORITY, Math.max(LOW_PRIORITY, tp));
} else {
this.priority = NORMAL_PRIORITY;
}
} else {
this.extClassName = null;
this.priority = PRIMARY_EXTENSION_PRIORITY;
}
if (jarFile != null) {
jarFile.close();
}
}
public String getId() {
return this.extId;
}
public URL getCodeSource() throws java.net.MalformedURLException {
return this.extRoot.toURI().toURL();
}
public boolean isPrimaryExtension() {
return this.isPrimary;
}
private ClassLoader classLoader;
private NavigatorExtension platformExtension;
public void initClassLoader(final ClassLoader parentClassLoader) throws java.net.MalformedURLException, ClassNotFoundException,
IllegalAccessException, InstantiationException {
ClassLoader classLoader;
if (extRoot != null) {
final URL url = this.extRoot.toURI().toURL();
final URL[] urls = new URL[] { url };
classLoader = new ExtensionClassLoader(urls, parentClassLoader);
} else {
classLoader = parentClassLoader;
}
NavigatorExtension pe = null;
if (extClassName != null) {
final Class<?> extClass = classLoader.loadClass(extClassName);
pe = (NavigatorExtension) extClass.newInstance();
}
synchronized (this) {
this.classLoader = classLoader;
this.platformExtension = pe;
}
}
public ClassLoader getClassLoader() {
synchronized (this) {
return this.classLoader;
}
}
/**
* Gets the {@link org.lobobrowser.ua.NavigatorExtension} implementation. It
* may return <code>null</code> in the case of a library.
*/
public NavigatorExtension getNavigatorExtension() {
synchronized (this) {
return this.platformExtension;
}
}
public void initExtension() {
doWithClassLoader(() -> {
final NavigatorExtension pe = this.platformExtension;
if (pe != null) {
pe.init(this);
}
return null;
});
}
public void initExtensionWindow(final NavigatorWindow wcontext) {
doWithClassLoader(() -> {
final NavigatorExtension pe = this.platformExtension;
if (pe != null) {
pe.windowOpening(wcontext);
}
return null;
});
}
public void shutdownExtensionWindow(final NavigatorWindow wcontext) {
doWithClassLoader(() -> {
final NavigatorExtension pe = this.platformExtension;
if (pe != null) {
pe.windowClosing(wcontext);
}
return null;
});
}
public void close() throws java.io.IOException {
}
public void addClientletSelector(final ClientletSelector cs) {
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(org.lobobrowser.security.GenericLocalPermission.EXT_GENERIC);
}
synchronized (this) {
this.clientletSelectors.add(cs);
}
}
protected <V> V doWithClassLoader(final Callable<V> r) {
// Need to set the class loader in thread context, otherwise
// some library classes may not be found.
final Thread currentThread = Thread.currentThread();
final ClassLoader prevClassLoader = currentThread.getContextClassLoader();
final ClassLoader loader = this.classLoader;
if (loader != null) {
currentThread.setContextClassLoader(loader);
}
try {
return r.call();
} catch (final Exception e) {
throw new Error(e);
} finally {
currentThread.setContextClassLoader(prevClassLoader);
}
}
public Clientlet getClientlet(final ClientletRequest request, final ClientletResponse response) {
return doWithClassLoader(() -> {
synchronized (this) {
for (final ClientletSelector cs : this.clientletSelectors) {
final Clientlet c = cs.select(request, response);
if (c != null) {
return c;
}
}
}
return null;
});
}
public Clientlet getLastResortClientlet(final ClientletRequest request, final ClientletResponse response) {
return doWithClassLoader(() -> {
synchronized (this) {
for (final ClientletSelector cs : this.clientletSelectors) {
final Clientlet c = cs.lastResortSelect(request, response);
if (c != null) {
return c;
}
}
}
return null;
});
}
public void addNavigatorErrorListener(final NavigatorErrorListener listener) {
EVENT.addListener(listener);
}
public void removeNavigatorErrorListener(final NavigatorErrorListener listener) {
EVENT.removeListener(listener);
}
/**
* @param event
* @return True only if the event was dispatched to at least one listener.
*/
public boolean handleError(final NavigatorExceptionEvent event) {
// Expected in GUI thread.
return EVENT.fireEvent(event);
}
public void addURLStreamHandlerFactory(final URLStreamHandlerFactory factory) {
// TODO: Since extensions are intialized in parallel,
// this is not necessarily done in order of priority.
org.lobobrowser.main.PlatformStreamHandlerFactory.getInstance().addFactory(factory);
}
public UserAgent getUserAgent() {
return org.lobobrowser.request.UserAgentImpl.getInstance();
}
public int compareTo(final Object o) {
// Reverse order based on priority.
final Extension other = (Extension) o;
final int diff = other.priority - this.priority;
if (diff != 0) {
return diff;
}
if (extRoot != null) {
return this.extRoot.compareTo(other.extRoot);
} else {
return -1;
}
}
@Override
public int hashCode() {
return this.priority | this.extRoot.hashCode();
}
@Override
public boolean equals(final Object other) {
if (!(other instanceof Extension)) {
return false;
}
return ((Extension) other).extRoot.equals(this.extRoot);
}
@Override
public String toString() {
return "ExtensionInfo[extRoot=" + this.extRoot + "]";
}
public void addConnectionProcessor(final ConnectionProcessor processor) {
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(org.lobobrowser.security.GenericLocalPermission.EXT_GENERIC);
}
synchronized (this) {
this.connectionProcessors.add(processor);
}
}
public void addNavigationListener(final NavigationListener listener) {
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(org.lobobrowser.security.GenericLocalPermission.EXT_GENERIC);
}
synchronized (this) {
this.navigationListeners.add(listener);
}
}
public void removeClientletSelector(final ClientletSelector selector) {
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(org.lobobrowser.security.GenericLocalPermission.EXT_GENERIC);
}
synchronized (this) {
this.clientletSelectors.remove(selector);
}
}
public void removeConnectionProcessor(final ConnectionProcessor processor) {
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(org.lobobrowser.security.GenericLocalPermission.EXT_GENERIC);
}
synchronized (this) {
this.connectionProcessors.remove(processor);
}
}
public void removeNavigationListener(final NavigationListener listener) {
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(org.lobobrowser.security.GenericLocalPermission.EXT_GENERIC);
}
synchronized (this) {
this.navigationListeners.remove(listener);
}
}
void dispatchBeforeNavigate(final NavigationEvent event) throws NavigationVetoException {
// Should not be public
doWithClassLoader(() -> {
final NavigationListener[] listeners = ArrayUtilities.copySynched(navigationListeners, this, NavigationListener.EMPTY_ARRAY);
for (final NavigationListener l : listeners) {
l.beforeNavigate(event);
}
return null;
});
}
void dispatchBeforeLocalNavigate(final NavigationEvent event) throws NavigationVetoException {
// Should not be public
doWithClassLoader(() -> {
final NavigationListener[] listeners = ArrayUtilities.copySynched(navigationListeners, this, NavigationListener.EMPTY_ARRAY);
for (final NavigationListener l : listeners) {
l.beforeLocalNavigate(event);
}
return null;
});
}
void dispatchBeforeWindowOpen(final NavigationEvent event) throws NavigationVetoException {
// Should not be public
doWithClassLoader(() -> {
final NavigationListener[] listeners = ArrayUtilities.copySynched(navigationListeners, this, NavigationListener.EMPTY_ARRAY);
for (final NavigationListener l : listeners) {
l.beforeWindowOpen(event);
}
return null;
});
}
URLConnection dispatchPreConnection(final URLConnection connection) {
// Should not be public
return doWithClassLoader(() -> {
final ConnectionProcessor[] processors = ArrayUtilities.copySynched(connectionProcessors, this, ConnectionProcessor.EMPTY_ARRAY);
URLConnection result = connection;
for (final ConnectionProcessor processor : processors) {
result = processor.processPreConnection(connection);
}
return result;
});
}
URLConnection dispatchPostConnection(final URLConnection connection) {
// Should not be public
return doWithClassLoader(() -> {
final ConnectionProcessor[] processors = ArrayUtilities.copySynched(connectionProcessors, this, ConnectionProcessor.EMPTY_ARRAY);
URLConnection result = connection;
for (final ConnectionProcessor processor : processors) {
result = processor.processPostConnection(connection);
}
return result;
});
}
private static class NavigatorErrorEventDispatch extends EventDispatch2 {
@Override
protected void dispatchEvent(final EventListener listener, final EventObject event) {
((NavigatorErrorListener) listener).errorOcurred((NavigatorExceptionEvent) event);
}
}
}