/**
* BrowserPane.java
* (c) Peter Bielik and Radek Burget, 2011-2012
*
* SwingBox is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* SwingBox 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with SwingBox. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.fit.cssbox.swingbox;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.net.ssl.HttpsURLConnection;
import javax.swing.JEditorPane;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.ChangedCharSetException;
import javax.swing.text.DefaultCaret;
import javax.swing.text.Document;
import javax.swing.text.EditorKit;
import javax.swing.text.Element;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;
import org.fit.cssbox.swingbox.util.Anchor;
import org.fit.cssbox.swingbox.util.CSSBoxAnalyzer;
import org.fit.cssbox.swingbox.util.Constants;
import org.fit.cssbox.swingbox.util.GeneralEvent;
import org.fit.cssbox.swingbox.util.GeneralEvent.EventType;
import org.fit.cssbox.swingbox.util.GeneralEventListener;
import org.fit.net.DataURLHandler;
/**
* The Class BrowserPane - JEditorPane based component capable to render HTML +
* CSS. This is alternative to HTMLEditorKit.
*
* @author Peter Bielik
* @version 1.0
* @since 1.0 - 28.9.2010
*/
public class BrowserPane extends JEditorPane
{
private static final long serialVersionUID = 7303652028812084960L;
private InputStream loadingStream;
private Hashtable<String, Object> pageProperties;
private Document document;
private static EditorKit swingBoxEditorKit = null;
// "org.fit.cssbox.swingbox.SwingBoxEditorKit"
protected String HtmlEditorKitClass = "org.fit.cssbox.swingbox.SwingBoxEditorKit";
/**
* Instantiates a new browser pane.
*/
public BrowserPane()
{
super();
init();
}
/**
* Initial settings
*/
protected void init()
{
// "support for SSL"
String handlerPkgs = System.getProperty("java.protocol.handler.pkgs");
if ((handlerPkgs != null) && !(handlerPkgs.isEmpty())) {
handlerPkgs = handlerPkgs + "|com.sun.net.ssl.internal.www.protocol";
} else {
handlerPkgs = "com.sun.net.ssl.internal.www.protocol";
}
System.setProperty("java.protocol.handler.pkgs", handlerPkgs);
java.security.Security
.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
// Create custom EditorKit if needed
if (swingBoxEditorKit == null) {
swingBoxEditorKit = new SwingBoxEditorKit();
}
setEditable(false);
setContentType("text/html");
activateTooltip(true);
Caret caret = getCaret();
if (caret instanceof DefaultCaret)
((DefaultCaret) caret).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
}
@Override
public Document getDocument()
{
return document;
}
@Override
public void setDocument(Document document)
{
this.document = document;
super.setDocument(document);
}
/**
* Activates tooltips.
*
* @param show
* if true, shows tooltips.
*/
public void activateTooltip(boolean show)
{
if (show)
{
ToolTipManager.sharedInstance().registerComponent(this);
}
else
{
ToolTipManager.sharedInstance().unregisterComponent(this);
}
ToolTipManager.sharedInstance().setEnabled(show);
}
/**
* Checks if tooltips are activated.
*
* @return true, if is activated
*/
public boolean isTooltipActivated()
{
return ToolTipManager.sharedInstance().isEnabled();
}
/**
* Adds the general event listener.
*
* @param listener
* the listener
*/
public synchronized void addGeneralEventListener(
GeneralEventListener listener)
{
if (listener == null) return;
listenerList.add(GeneralEventListener.class, listener);
}
/**
* Removes the general event listener.
*
* @param listener
* the listener
*/
public synchronized void removeGeneralEventListener(
GeneralEventListener listener)
{
if (listener == null) return;
listenerList.remove(GeneralEventListener.class, listener);
}
/**
* Gets registered general event listeners.
*
* @return the array of general event listeners
*/
public synchronized GeneralEventListener[] getGeneralEventListeners()
{
return (GeneralEventListener[]) listenerList
.getListeners(GeneralEventListener.class);
}
/**
* Fires general event. All registered listeners will be notified.
*
* @param e
* the event
*/
public void fireGeneralEvent(GeneralEvent e)
{
Object[] listeners = listenerList.getListenerList();
// notify those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2)
{
if (listeners[i] == GeneralEventListener.class)
{
((GeneralEventListener) listeners[i + 1]).generalEventUpdate(e);
}
}
}
/**
* Renders current content to graphic context, which is returned. May return
* null;
*
* @return the Graphics2D context
* @see Graphics2D
*/
public Graphics2D renderContent()
{
View view = null;
ViewFactory factory = getEditorKit().getViewFactory();
if (factory instanceof SwingBoxViewFactory)
{
view = ((SwingBoxViewFactory) factory).getViewport();
}
if (view != null)
{
int w = (int) view.getPreferredSpan(View.X_AXIS);
int h = (int) view.getPreferredSpan(View.Y_AXIS);
Rectangle rec = new Rectangle(w, h);
BufferedImage img = new BufferedImage(w, h,
BufferedImage.TYPE_INT_RGB);
Graphics2D g = img.createGraphics();
g.setClip(rec);
view.paint(g, rec);
return g;
}
return null;
}
/**
* Renders current content to given graphic context, which is updated and
* returned. Context must have set the clip, otherwise NullPointerException
* is thrown.
*
* @param g
* the context to be rendered to.
* @return the Graphics2D context
* @see Graphics2D
*/
public Graphics2D renderContent(Graphics2D g)
{
if (g.getClip() == null)
throw new NullPointerException(
"Clip is not set on graphics context");
ViewFactory factory = getEditorKit().getViewFactory();
if (factory instanceof SwingBoxViewFactory)
{
View view = ((SwingBoxViewFactory) factory).getViewport();
if (view != null) view.paint(g, g.getClip());
}
return g;
}
@Override
public void setText(String t)
{
//fireGeneralEvent(new GeneralEvent(this, EventType.page_loading_begin, null, null));
//super.setText(t);
try
{
URL url = DataURLHandler.createURL(null, "data:text/html," + t);
setPage(url);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Sets the css box analyzer.
*
* @param cba
* the analyzer to be set
* @return true, if successful
*/
public boolean setCSSBoxAnalyzer(CSSBoxAnalyzer cba)
{
EditorKit kit = getEditorKit();
if (kit instanceof SwingBoxEditorKit)
{
((SwingBoxEditorKit) kit).setCSSBoxAnalyzer(cba);
return true;
}
return false;
}
@Override
public void scrollToReference(String reference)
{
tryScrollToReference(reference);
}
/**
* This method has the same purpose as {@link BrowserPane#scrollToReference(String)}.
* However, it allows checking whether the reference exists in the document.
* @param reference the named location to scroll to
* @return <code>true</code> when the location exists in the document, <code>false</code> when not found.
*/
public boolean tryScrollToReference(String reference)
{
Element dst = findElementToScroll(reference, getDocument().getDefaultRootElement());
if (dst != null)
{
try
{
Rectangle bottom = new Rectangle(0, getHeight() - 1, 1, 1);
Rectangle rec = modelToView(dst.getStartOffset());
if (rec != null)
{
scrollRectToVisible(bottom); //move to the bottom and back in order to put the reference to the window top
scrollRectToVisible(rec);
}
return true;
} catch (BadLocationException e)
{
UIManager.getLookAndFeel().provideErrorFeedback(this);
return false;
}
}
else
return false;
}
private Element findElementToScroll(String ref, Element root)
{
String name = (String) root.getAttributes().getAttribute(SwingBoxDocument.ElementNameAttribute);
if (!Constants.BACKGROUND.equals(name)) //do not consider backgrounds
{
//try the id attribute
String eid = (String) root.getAttributes().getAttribute(Constants.ATTRIBUTE_ELEMENT_ID);
if (eid != null && ref.equalsIgnoreCase(eid))
{
return root;
}
//or try the name attribute of <a>
else
{
Anchor anchor = (Anchor) root.getAttributes().getAttribute(Constants.ATTRIBUTE_ANCHOR_REFERENCE);
if (anchor != null && anchor.isActive())
{
if (anchor.getProperties().get(Constants.ELEMENT_A_ATTRIBUTE_NAME).equals(ref))
return root;
}
}
}
int n = root.getElementCount();
Element child;
for (int i = 0; i < n; i++)
{
if ((child = findElementToScroll(ref, root.getElement(i))) != null)
return child;
}
return null;
}
@Override
public EditorKit getEditorKitForContentType(String type) {
if (type.equalsIgnoreCase("text/html") || type.equalsIgnoreCase("application/xhtml+xml")
|| type.equalsIgnoreCase("text/xhtml")) {
return swingBoxEditorKit;
} else {
return super.getEditorKitForContentType(type);
}
}
@Override
protected InputStream getStream(URL page) throws IOException
{
final URLConnection conn = setConnectionProperties(page.openConnection());
// http://stackoverflow.com/questions/875467/java-client-certificates-over-https-ssl
if (conn instanceof HttpsURLConnection)
{
// XXX toto moc nefunguje
System.out.println("$ Connection is HTTPS !!");
}
else if (conn instanceof HttpURLConnection)
{
HttpURLConnection hconn = (HttpURLConnection) conn;
hconn.setInstanceFollowRedirects(false);
Object postData = getPostData();
if (postData != null)
{
handlePostData(hconn, postData);
}
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);
}
}
// Connection properties handler should be forced to run on EDT,
// as it instantiates the EditorKit.
if (SwingUtilities.isEventDispatchThread())
{
handleConnectionProperties(conn);
}
else
{
try
{
SwingUtilities.invokeAndWait(new Runnable()
{
@Override
public void run()
{
handleConnectionProperties(conn);
}
});
} catch (InterruptedException e)
{
throw new RuntimeException(e);
} catch (InvocationTargetException e)
{
throw new RuntimeException(e);
}
}
return conn.getInputStream();
}
@Override
public void setPage(final URL newPage) throws IOException
{
fireGeneralEvent(new GeneralEvent(this, EventType.page_loading_begin,
newPage, null));
if (newPage == null)
{
// TODO fire general event here
throw new IOException("invalid url");
}
final URL oldPage = getPage();
Object postData = getPostData();
if ((oldPage == null) || !oldPage.sameFile(newPage) || (postData != null))
{
// different url or POST method, load the new content
final InputStream in = getStream(newPage);
// editor kit is set according to content type
EditorKit kit = getEditorKit();
if (kit == null)
{
UIManager.getLookAndFeel().provideErrorFeedback(this);
}
else
{
document = createDocument(kit, newPage);
int p = getAsynchronousLoadPriority(document);
if (p < 0)
{
// load synchro
loadPage(newPage, oldPage, in, document);
}
else
{
// load asynchro
Thread t = new Thread(new Runnable()
{
@Override
public void run()
{
loadPage(newPage, oldPage, in, document);
}
});
t.setDaemon(true);
t.start();
}
}
}
else if (oldPage.sameFile(newPage))
{
if (newPage.getRef() != null)
{
final String reference = newPage.getRef();
SwingUtilities.invokeLater(new Runnable()
{
@Override
public void run()
{
scrollToReference(reference);
}
});
}
}
}
private void loadPage(final URL newPage, final URL oldPage, final InputStream in, final Document doc)
{
boolean done = false;
try
{
synchronized (this)
{
if (loadingStream != null)
{
// we are loading asynchronously, so we need to cancel
// the old stream.
loadingStream.close();
loadingStream = null;
}
loadingStream = in;
}
// read the content
read(loadingStream, doc);
// set the document to the component
setDocument(doc);
final String reference = newPage.getRef();
// Have to scroll after painted.
SwingUtilities.invokeLater(new Runnable()
{
@Override
public void run()
{
scrollRectToVisible(new Rectangle(0, 0, 1, 1)); // top of the pane
if (reference != null)
scrollToReference(reference);
}
});
done = true;
} catch (IOException ioe)
{
UIManager.getLookAndFeel().provideErrorFeedback(this);
} finally
{
synchronized (this)
{
if (loadingStream != null)
{
try
{
loadingStream.close();
} catch (IOException ignored)
{
}
}
loadingStream = null;
}
if (done)
{
SwingUtilities.invokeLater(new Runnable()
{
@Override
public void run()
{
firePropertyChange("page", oldPage, newPage);
}
});
}
}
}
private Document createDocument(EditorKit kit, URL page)
{
// we have pageProperties, because we can be in situation that
// old page is being removed & new page is not yet created...
// we need somewhere store important data.
Document doc = kit.createDefaultDocument();
if (pageProperties != null)
{
// transfer properties discovered in stream to the
// document property collection.
for (Enumeration<String> e = pageProperties.keys(); e
.hasMoreElements();)
{
Object key = e.nextElement();
doc.putProperty(key, pageProperties.get(key));
}
}
if (doc.getProperty(Document.StreamDescriptionProperty) == null)
{
doc.putProperty(Document.StreamDescriptionProperty, page);
}
return doc;
}
private int getAsynchronousLoadPriority(Document doc)
{
return (doc instanceof AbstractDocument ? ((AbstractDocument) doc)
.getAsynchronousLoadPriority() : -1);
}
private Object getPostData()
{
return getDocument().getProperty(Constants.PostDataProperty);
}
/**
* Handle URL connection properties (most notably, content type).
*/
private void handleConnectionProperties(URLConnection conn)
{
if (pageProperties == null)
{
pageProperties = new Hashtable<String, Object>(22);
}
String type = conn.getContentType();
if (type != null)
{
// XXX mozno prepisat podla seba, setContentType, len pre text/****
setContentType(type); // >> XXX putClientProperty("charset",
// charset); !!!
// charset\s*=[\s'"]*([\-_a-zA-Z0-9]+)[\s'",;]*
// pageProperties.put("content-type", type);
}
pageProperties.put(Document.StreamDescriptionProperty, conn.getURL());
// String enc = conn.getContentEncoding();
// if (enc != null) {
// pageProperties.put("content-encoding", enc);
// }
Map<String, List<String>> header = conn.getHeaderFields();
Set<String> keys = header.keySet();
Object obj;
for (String key : keys)
{
obj = header.get(key);
if (key != null && obj != null)
{
pageProperties.put(key, obj);
}
}
System.out.println("# pageProperties #");
for (String k : pageProperties.keySet())
{
System.out.println(k + " : " + pageProperties.get(k));
}
}
private URLConnection setConnectionProperties(URLConnection conn)
{
// http://www.useragentstring.com/index.php
// http://tools.ietf.org/html/rfc1945
// Opera 11.50 : Opera/9.80 (X11; Linux i686; U; sk) Presto/2.9.168
// Version/11.50
// CSSBox : Mozilla/5.0 (compatible; BoxBrowserTest/2.x; Linux)
// CSSBox/2.x (like Gecko)
// FireFox : Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9b5)
// Gecko/2008032620 Firefox/3.0b5
// IE8 : Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0;
// .NET CLR 2.0.50727; .NET CLR 1.1.4322; .NET CLR 3.0.04506.30; .NET
// CLR 3.0.04506.648)
// SwingBox : Mozilla/5.0 (compatible; SwingBox/1.x; Linux; U)
// CSSBox/2.x (like Gecko)
/*
* An unofficial format, based on the above, used by Web browsers is as
* follows: Mozilla/[version] ([system and browser information])
* [platform] ([platform details]) [extensions]. For example, Safari on
* the iPad has used the following: Mozilla/5.0 (iPad; U; CPU OS 3_2_1
* like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko)
* Mobile/7B405.
*/
conn.setRequestProperty("User-Agent",
"Mozilla/5.0 (compatible; SwingBox/1.x; Linux; U) CSSBox/4.x (like Gecko)");
conn.setRequestProperty("Accept-Charset", "utf-8");
return conn;
}
private void handlePostData(HttpURLConnection conn, Object postData)
throws IOException
{
conn.setDoOutput(true);
DataOutputStream os = null;
try
{
conn.setRequestProperty("Content-Type",
"application/x-www-form-urlencoded");
os = new DataOutputStream(conn.getOutputStream());
os.writeBytes((String) postData);
} finally
{
if (os != null)
{
os.close();
}
}
}
private void setCharsetFromContentTypeParameters(String paramlist)
{
String charset = null;
try
{
// paramlist is handed to us with a leading ';', strip it.
int semi = paramlist.indexOf(';');
if (semi > -1 && semi < paramlist.length() - 1)
{
paramlist = paramlist.substring(semi + 1);
}
if (paramlist.length() > 0)
{
// parse the paramlist into attr-value pairs & get the
// charset pair's value
// TODO error here
// HeaderParser hdrParser = new HeaderParser(paramlist);
// charset = hdrParser.findValue("charset");
if (charset != null)
{
putClientProperty("charset", charset);
}
}
} catch (IndexOutOfBoundsException e)
{
// malformed parameter list, use charset we have
} catch (NullPointerException e)
{
// malformed parameter list, use charset we have
} catch (Exception e)
{
// malformed parameter list, use charset we have; but complain
System.err
.println("JEditorPane.getCharsetFromContentTypeParameters failed on: "
+ paramlist);
e.printStackTrace();
}
}
@Override
public void read(InputStream in, Object desc) throws IOException
{
super.read(in, desc); // !!! na toto sa tiez pozriet
}
void read(InputStream in, Document doc) throws IOException
{
EditorKit kit = getEditorKit();
try
{
kit.read(in, doc, 0);
} catch (ChangedCharSetException ccse)
{
// ignored, may be in the future will be processed
throw ccse;
} catch (BadLocationException ble)
{
throw new IOException(ble);
}
}
}