package org.limewire.ui.swing.components;
import java.awt.EventQueue;
import java.awt.Insets;
import java.awt.Rectangle;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.JEditorPane;
import javax.swing.SwingUtilities;
import javax.swing.text.Document;
import javax.swing.text.EditorKit;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import org.limewire.concurrent.ExecutorsHelper;
import org.limewire.concurrent.ListeningExecutorService;
import org.limewire.concurrent.ListeningFuture;
import org.limewire.ui.swing.util.SwingUtils;
import org.limewire.util.ExceptionUtils;
/**
* An editor pane that forces synchronous page loading unless you use the
* {@link #setPageAsynchronous(String, String)} method.
* <p>
* Much of this comes from JEditorPane and had to be copied out because of package-private problems.
*/
public class HTMLPane extends JEditorPane {
private static final ListeningExecutorService QUEUE = ExecutorsHelper.newProcessingQueue("HTMLPane Queue");
private final SynchronousEditorKit kit = new SynchronousEditorKit();
private HashMap<Object, Object> pageProperties;
private volatile boolean pageLoaded;
private ListeningFuture<Void> currentLoad;
public HTMLPane() {
setEditorKit(kit);
setEditorKitForContentType(kit.getContentType(), kit);
setContentType("text/html");
setEditable(false);
kit.setAutoFormSubmission(false);
setMargin(new Insets(0, 0, 0, 0));
}
/** Loads the given URL, loading the backup page if it fails to load. */
public ListeningFuture<Void> setPageAsynchronous(final String url, final String backupPage) {
assert SwingUtilities.isEventDispatchThread();
if(currentLoad != null) {
currentLoad.cancel(true);
}
currentLoad = QUEUE.submit(new Callable<Void>() {
@Override
public Void call() {
try {
setPageImpl(new URL(url));
} catch(IOException iox) {
setBackup();
} catch(RuntimeInterruptedException rie) {
setBackup();
}
return null;
}
private void setBackup() {
SwingUtils.invokeLater(new Runnable() {
public void run() {
setContentType("text/html");
setText(backupPage);
setCaretPosition(0);
}
});
}
});
return currentLoad;
}
public boolean isLastRequestSuccessful() {
return pageLoaded;
}
@Override
public void setPage(final URL page) throws IOException {
setPageImpl(page);
}
// Note the use of SwingUtils vs SwingUtilities & also a custom invokeAndWait
// SwingUtils will invoke immediately if already in the dispatch thread,
// SwingUtilities will always force to the end of the queue --
// The uses here (of both) are very deliberate.
// invokeAndWait is special because we want to rethrow InterruptedException
// as IOException, because we're expecting to be interrupted occasionally
// and do not want to report the error.
private void setPageImpl(final URL page) throws IOException {
if (page == null) {
throw new IOException("invalid url");
}
pageLoaded = false;
final AtomicReference<URL> loaded = new AtomicReference<URL>();
invokeAndWait(new Runnable() {
public void run() {
loaded.set(getPage());
// reset scrollbar
if (!page.equals(loaded.get()) && page.getRef() == null) {
scrollRectToVisible(new Rectangle(0, 0, 1, 1));
}
}
});
final AtomicBoolean reloaded = new AtomicBoolean(false);
InputStream in = new InterruptableStream(getStream(page));
if (kit != null) {
final AtomicReference<Document> doc = new AtomicReference<Document>();
invokeAndWait(new Runnable() {
@Override
public void run() {
doc.set(initializeModel(kit, page));
setDocument(doc.get());
}
});
read(in, doc.get());
reloaded.set(true);
}
SwingUtils.invokeLater(new Runnable() {
public void run() {
// Start at the top, then move if necessary.
setCaretPosition(0);
final String reference = page.getRef();
if (reference != null) {
if (!reloaded.get()) {
scrollToReference(reference);
} else {
// Force this to the back of the queue.
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
scrollToReference(reference);
}
});
}
}
getDocument().putProperty(Document.StreamDescriptionProperty, page);
firePropertyChange("page", loaded.get(), page);
}
});
pageLoaded = true; // purposely not in a finally -- if we fail it didn't load
}
@Override
public void setDocument(final Document doc) {
// Ensure the document is set on the EDT thread,
// because it triggers a repaint (and it should be).
invokeAndWait(new Runnable() {
@Override
public void run() {
HTMLPane.super.setDocument(doc);
}
});
}
// Copied from {@link JEditorPane#getStream(URL)} because of package-private problems.
@Override
protected InputStream getStream(URL page) throws IOException {
final URLConnection conn = page.openConnection();
if (conn instanceof HttpURLConnection) {
HttpURLConnection hconn = (HttpURLConnection) conn;
hconn.setInstanceFollowRedirects(false);
int response = hconn.getResponseCode();
boolean redirect = (response >= 300 && response <= 399);
/*
* In the case of a redirect, we want to actually change the URL
* that was input to the new, redirected URL
*/
if (redirect) {
String loc = conn.getHeaderField("Location");
if (loc.startsWith("http", 0)) {
page = new URL(loc);
} else {
page = new URL(page, loc);
}
return getStream(page);
}
}
invokeAndWait(new Runnable() {
@Override
public void run() {
handleConnectionProperties(conn);
}
});
return conn.getInputStream();
}
// This rethrows InterruptedException as RuntimeInterruptedException,
// so that we can catch it in the setPageImpl call and respond to it.
private void invokeAndWait(final Runnable runnable) throws RuntimeInterruptedException {
if(EventQueue.isDispatchThread()) {
runnable.run();
} else {
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
runnable.run();
}
});
} catch (InterruptedException e) {
throw new RuntimeInterruptedException(e);
} catch (InvocationTargetException e) {
ExceptionUtils.rethrow(e);
}
}
}
private static class RuntimeInterruptedException extends RuntimeException {
public RuntimeInterruptedException(Throwable t) {
super(t);
}
}
// Copied from {@link JEditorPane#handleConnectionProperties(URLConnection)} because of package-private problems.
private void handleConnectionProperties(URLConnection conn) {
if (pageProperties == null) {
pageProperties = new HashMap<Object, Object>();
}
String type = conn.getContentType();
if (type != null) {
setContentType(type);
pageProperties.put("content-type", type);
}
pageProperties.put(Document.StreamDescriptionProperty, conn.getURL());
String enc = conn.getContentEncoding();
if (enc != null) {
pageProperties.put("content-encoding", enc);
}
}
// Copied from {@link JEditorPane#initializeModel(EditorKit, URL)} because of package-private problems.
private Document initializeModel(EditorKit kit, URL page) {
Document doc = kit.createDefaultDocument();
if (pageProperties != null) {
// transfer properties discovered in stream to the
// document property collection.
for (Iterator<Object> iter = pageProperties.keySet().iterator(); iter.hasNext() ;) {
Object key = iter.next();
doc.putProperty(key, pageProperties.get(key));
}
pageProperties.clear();
}
if (doc.getProperty(Document.StreamDescriptionProperty) == null) {
doc.putProperty(Document.StreamDescriptionProperty, page);
}
return doc;
}
/**
* Updated to set the asynchronous priority to -1, although it shouldn't
* matter because we explicitly load documents ourselves.
*/
private static class SynchronousEditorKit extends HTMLEditorKit {
@Override
public Document createDefaultDocument() {
Document doc = super.createDefaultDocument();
((HTMLDocument)doc).setAsynchronousLoadPriority(-1);
return doc;
}
}
/** An extension to {@link InputStream} that fails if the thread was interrupted. */
private static class InterruptableStream extends FilterInputStream {
public InterruptableStream(InputStream i) {
super(i);
}
protected void checkInterrupted() throws IOException {
if(Thread.interrupted()) {
throw new IOException("read interrupted");
}
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
checkInterrupted();
return super.read(b, off, len);
}
@Override
public int read() throws IOException {
checkInterrupted();
return super.read();
}
@Override
public long skip(long n) throws IOException {
checkInterrupted();
return super.skip(n);
}
@Override
public int available() throws IOException {
checkInterrupted();
return super.available();
}
@Override
public void reset() throws IOException {
checkInterrupted();
super.reset();
}
}
}