/** * 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); } } }