001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.openide.awt;
042:
043: import java.util.logging.Level;
044: import java.util.logging.Logger;
045: import org.openide.util.Mutex;
046: import org.openide.util.NbBundle;
047: import org.openide.util.RequestProcessor;
048:
049: import java.awt.Dimension;
050: import java.awt.Graphics;
051: import java.awt.Rectangle;
052:
053: import java.beans.PropertyChangeEvent;
054: import java.beans.PropertyChangeListener;
055: import java.beans.PropertyChangeSupport;
056:
057: import java.io.FilterInputStream;
058: import java.io.IOException;
059: import java.io.InputStream;
060:
061: import java.net.URL;
062: import java.net.URLConnection;
063:
064: import java.util.Vector;
065:
066: import javax.swing.AbstractAction;
067: import javax.swing.ActionMap;
068: import javax.swing.JEditorPane;
069: import javax.swing.SwingConstants;
070: import javax.swing.SwingUtilities;
071: import javax.swing.event.HyperlinkEvent;
072: import javax.swing.event.HyperlinkListener;
073: import javax.swing.text.AbstractDocument;
074: import javax.swing.text.DefaultEditorKit;
075: import javax.swing.text.Document;
076: import javax.swing.text.html.*;
077:
078: /**
079: * Implementation of BrowserImpl in Swing.
080: */
081: final class SwingBrowserImpl extends HtmlBrowser.Impl implements
082: Runnable {
083: /** state of history management */
084: private static final int NO_NAVIGATION = 1;
085: private static final int NAVIGATION_BACK = 2;
086: private static final int NAVIGATION_FWD = 3;
087: private static RequestProcessor rp = new RequestProcessor(
088: "Swing Browser"); //NOI18N
089:
090: /** Current URL. */
091: private URL url;
092:
093: /** URL loaded by JEditorPane */
094: private URL loadingURL;
095: private PropertyChangeSupport pcs;
096: private String statusMessage = ""; // NOI18N
097: private SwingBrowser swingBrowser;
098:
099: /** list of accessed URLs for back/fwd navigation */
100: private Vector<Object> historyList;
101:
102: /** current position in history */
103: private int historyIndex;
104:
105: /** navigation indication */
106: private int historyNavigating = NO_NAVIGATION;
107: private String title = null;
108: boolean fetchingTitle = false;
109:
110: private static Logger LOG = Logger.getLogger(SwingBrowserImpl.class
111: .getName());
112:
113: SwingBrowserImpl() {
114: pcs = new PropertyChangeSupport(this );
115: swingBrowser = new SwingBrowser();
116: historyList = new Vector<Object>(5, 3);
117: historyIndex = -1;
118: swingBrowser.addPropertyChangeListener("page", // NOI18N
119: new PropertyChangeListener() {
120: public void propertyChange(PropertyChangeEvent evt) {
121: if (evt.getNewValue() instanceof URL) {
122: URL old = SwingBrowserImpl.this .url;
123: SwingBrowserImpl.this .url = (URL) evt
124: .getNewValue();
125: SwingBrowserImpl.this .pcs
126: .firePropertyChange(PROP_URL, old,
127: url);
128:
129: if (((URL) evt.getNewValue())
130: .equals(loadingURL)) {
131: loadingURL = null;
132: }
133:
134: // update history
135: if (historyNavigating == NAVIGATION_BACK) {
136: int idx = historyList.lastIndexOf(evt
137: .getNewValue(),
138: historyIndex - 1);
139:
140: if (idx != -1) {
141: historyIndex = idx;
142: }
143: } else if (historyNavigating == NAVIGATION_FWD) {
144: int idx = historyList.indexOf(evt
145: .getNewValue(),
146: historyIndex + 1);
147:
148: if (idx != -1) {
149: historyIndex = idx;
150: }
151: } else {
152: while (historyList.size() > (historyIndex + 1))
153: historyList.remove(historyList
154: .size() - 1);
155:
156: historyList.add(evt.getNewValue());
157: historyIndex = historyList.size() - 1;
158: }
159:
160: historyNavigating = NO_NAVIGATION;
161: pcs.firePropertyChange(PROP_BACKWARD, null,
162: null);
163: pcs.firePropertyChange(PROP_FORWARD, null,
164: null);
165: SwingUtilities
166: .invokeLater(SwingBrowserImpl.this );
167: }
168: }
169: });
170: }
171:
172: /**
173: * Returns visual component of html browser.
174: *
175: * @return visual component of html browser.
176: */
177: public java.awt.Component getComponent() {
178: return swingBrowser;
179: }
180:
181: /**
182: * Reloads current html page.
183: */
184: public void reloadDocument() {
185: synchronized (rp) {
186: try {
187: if ((url == null) || (loadingURL != null)) {
188: return;
189: }
190:
191: Document doc = swingBrowser.getDocument();
192: loadingURL = url;
193:
194: if (doc instanceof AbstractDocument) {
195: String protocol = url.getProtocol();
196:
197: if ("ftp".equalsIgnoreCase(protocol) // NOI18N
198: || "http".equalsIgnoreCase(protocol) // NOI18N
199: ) {
200: ((AbstractDocument) doc)
201: .setAsynchronousLoadPriority(Thread.NORM_PRIORITY);
202: } else {
203: ((AbstractDocument) doc)
204: .setAsynchronousLoadPriority(-1);
205: }
206: }
207:
208: rp.post(this );
209: } catch (Exception e) {
210: LOG.log(Level.WARNING, null, e);
211: pcs.firePropertyChange(PROP_STATUS_MESSAGE, null,
212: statusMessage = "" + e); // NOI18N
213: }
214: }
215: }
216:
217: /**
218: * Stops loading of current html page.
219: */
220: public void stopLoading() {
221: }
222:
223: /**
224: * Sets current URL.
225: *
226: * @param url URL to show in the browser.
227: */
228: public void setURL(URL url) {
229: synchronized (rp) {
230: try {
231: if (url == null) {
232: return;
233: }
234:
235: loadingURL = url;
236:
237: rp.post(this );
238: } catch (Exception e) {
239: LOG.log(Level.WARNING, null, e);
240: pcs.firePropertyChange(PROP_STATUS_MESSAGE, null,
241: statusMessage = "" + e); // NOI18N
242: }
243: }
244: }
245:
246: /**
247: * Returns current URL.
248: *
249: * @return current URL.
250: */
251: public URL getURL() {
252: return url;
253: }
254:
255: /**
256: * Returns status message representing status of html browser.
257: *
258: * @return status message.
259: */
260: public String getStatusMessage() {
261: return statusMessage;
262: }
263:
264: /** Returns title of the displayed page.
265: * @return title
266: */
267: public String getTitle() {
268: if (title == null) {
269: Mutex.EVENT.readAccess(this );
270: }
271:
272: return (title == null) ? NbBundle.getMessage(
273: SwingBrowserImpl.class, "LBL_Loading") : title; //NOI18N
274: }
275:
276: void updateTitle() {
277: assert SwingUtilities.isEventDispatchThread();
278:
279: // System.err.println("Update title");
280: if (fetchingTitle) {
281: // System.err.println(" ...already updating");
282: return;
283: }
284:
285: fetchingTitle = true;
286:
287: String oldTitle = getTitle();
288:
289: try {
290: Document d = swingBrowser.getDocument();
291: title = (String) d.getProperty(HTMLDocument.TitleProperty);
292:
293: // System.err.println("Title from document is " + title);
294: if ((title == null) || (title.trim().length() == 0)) {
295: // System.err.println("No title from document, trying from url ");
296: URL url = getURL();
297:
298: if (url != null) {
299: title = url.getFile();
300:
301: if (title.length() == 0) {
302: title = NbBundle.getMessage(
303: SwingBrowserImpl.class, "LBL_Untitled"); //NOI18N
304: } else {
305: //Trim any extraneous path info
306: int i = title.lastIndexOf("/"); //NOI18N
307:
308: if ((i != -1) && (i != (title.length() - 1))) {
309: title = title.substring(i + 1);
310: }
311: }
312:
313: // System.err.println("Using from url: " + title);
314: }
315: }
316:
317: if (title != null) {
318: if (title.length() > 60) {
319: //Truncate to a reasonable tab length
320: title = NbBundle.getMessage(SwingBrowserImpl.class,
321: "LBL_Title", new Object[] { title
322: .substring(0, 57) });
323: }
324:
325: if (!oldTitle.equals(title)) {
326: // System.err.println("Firing prop change from " + oldTitle + " to " + title);
327: pcs.firePropertyChange(PROP_TITLE, oldTitle, title); //NOI18N
328: }
329: }
330: } finally {
331: fetchingTitle = false;
332: }
333: }
334:
335: public void run() {
336: if (SwingUtilities.isEventDispatchThread()) {
337: title = null;
338: updateTitle();
339: } else {
340: URL requestedURL;
341: synchronized (rp) {
342: if ((this .url != null) && this .url.sameFile(url)) {
343: Document doc = swingBrowser.getDocument();
344:
345: if (doc != null) {
346: //force reload
347: doc.putProperty(
348: Document.StreamDescriptionProperty,
349: null);
350: }
351: }
352: requestedURL = loadingURL;
353: loadingURL = null;
354: }
355: try {
356:
357: swingBrowser.setPage(requestedURL);
358: setStatusText(null);
359: } catch (java.net.UnknownHostException uhe) {
360: setStatusText(NbBundle.getMessage(
361: SwingBrowserImpl.class, "FMT_UnknownHost",
362: new Object[] { requestedURL })); // NOI18N
363: } catch (java.net.NoRouteToHostException nrthe) {
364: setStatusText(NbBundle.getMessage(
365: SwingBrowserImpl.class, "FMT_NoRouteToHost",
366: new Object[] { requestedURL })); // NOI18N
367: } catch (IOException ioe) {
368: setStatusText(NbBundle.getMessage(
369: SwingBrowserImpl.class, "FMT_InvalidURL",
370: new Object[] { requestedURL })); // NOI18N
371: }
372:
373: SwingUtilities.invokeLater(this );
374: }
375: }
376:
377: /**
378: * Accessor to allow a message about bad urls to be displayed - see
379: * HtmlBrowser.setURL().
380: */
381: void setStatusText(String s) {
382: pcs.firePropertyChange(PROP_STATUS_MESSAGE, null,
383: statusMessage = s); // NOI18N
384: }
385:
386: /** Is forward button enabled?
387: * @return true if it is
388: */
389: public boolean isForward() {
390: return (historyIndex >= 0)
391: && (historyIndex < (historyList.size() - 1))
392: && (historyNavigating == NO_NAVIGATION);
393: }
394:
395: /** Moves the browser forward. Failure is ignored.
396: */
397: public void forward() {
398: if (isForward()) {
399: historyNavigating = NAVIGATION_FWD;
400: setURL((URL) historyList.elementAt(historyIndex + 1));
401: }
402: }
403:
404: /** Is backward button enabled?
405: * @return true if it is
406: */
407: public boolean isBackward() {
408: return (historyIndex > 0)
409: && (historyIndex < historyList.size())
410: && (historyNavigating == NO_NAVIGATION);
411: }
412:
413: /** Moves the browser forward. Failure is ignored.
414: */
415: public void backward() {
416: if (isBackward()) {
417: historyNavigating = NAVIGATION_BACK;
418: setURL((URL) historyList.elementAt(historyIndex - 1));
419: }
420: }
421:
422: /** Is history button enabled?
423: * @return true if it is
424: */
425: public boolean isHistory() {
426: return false;
427: }
428:
429: /** Invoked when the history button is pressed.
430: */
431: public void showHistory() {
432: }
433:
434: /**
435: * Adds PropertyChangeListener to this browser.
436: *
437: * @param l Listener to add.
438: */
439: public void addPropertyChangeListener(PropertyChangeListener l) {
440: pcs.addPropertyChangeListener(l);
441: }
442:
443: /**
444: * Removes PropertyChangeListener from this browser.
445: *
446: * @param l Listener to remove.
447: */
448: public void removePropertyChangeListener(PropertyChangeListener l) {
449: pcs.removePropertyChangeListener(l);
450: }
451:
452: // encoding support; copied from html/HtmlEditorSupport
453: private static String findEncodingFromURL(InputStream stream) {
454: try {
455: byte[] arr = new byte[4096];
456: int len = stream.read(arr, 0, arr.length);
457: String txt = new String(arr, 0, (len >= 0) ? len : 0)
458: .toUpperCase();
459:
460: // encoding
461: return findEncoding(txt);
462: } catch (Exception x) {
463: x.printStackTrace();
464: }
465:
466: return null;
467: }
468:
469: /** Tries to guess the mime type from given input stream. Tries to find
470: * <em><meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"></em>
471: * @param txt the string to search in (should be in upper case)
472: * @return the encoding or null if no has been found
473: */
474: private static String findEncoding(String txt) {
475: int headLen = txt.indexOf("</HEAD>"); // NOI18N
476:
477: if (headLen == -1) {
478: headLen = txt.length();
479: }
480:
481: int content = txt.indexOf("CONTENT-TYPE"); // NOI18N
482:
483: if ((content == -1) || (content > headLen)) {
484: return null;
485: }
486:
487: int charset = txt.indexOf("CHARSET=", content); // NOI18N
488:
489: if (charset == -1) {
490: return null;
491: }
492:
493: int charend = txt.indexOf('"', charset);
494: int charend2 = txt.indexOf('\'', charset);
495:
496: if ((charend == -1) && (charend2 == -1)) {
497: return null;
498: }
499:
500: if (charend2 != -1) {
501: if ((charend == -1) || (charend > charend2)) {
502: charend = charend2;
503: }
504: }
505:
506: return txt.substring(charset + "CHARSET=".length(), charend); // NOI18N
507: }
508:
509: // innerclasses ..............................................................
510: private class SwingBrowser extends JEditorPane {
511: private boolean lastPaintException = false;
512:
513: private SwingBrowser() {
514: setEditable(false);
515: addHyperlinkListener(new HyperlinkListener() {
516: public void hyperlinkUpdate(HyperlinkEvent e) {
517: if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
518: if (e instanceof HTMLFrameHyperlinkEvent) {
519: HTMLFrameHyperlinkEvent evt = (HTMLFrameHyperlinkEvent) e;
520: HTMLDocument doc = (HTMLDocument) getDocument();
521: URL old = getURL();
522: doc.processHTMLFrameHyperlinkEvent(evt);
523: pcs.firePropertyChange(PROP_URL, old, e
524: .getURL());
525: } else {
526: try {
527: SwingBrowserImpl.this
528: .setURL(e.getURL());
529: } catch (Exception ex) {
530: LOG.log(Level.WARNING, null, ex);
531: }
532: }
533: }
534: }
535: });
536:
537: //when up/down arrow keys are pressed, ensure the whole browser content
538: //scrolls up/down instead of moving the caret position only
539: ActionMap actionMap = getActionMap();
540: actionMap.put(DefaultEditorKit.upAction, new ScrollAction(
541: -1));
542: actionMap.put(DefaultEditorKit.downAction,
543: new ScrollAction(1));
544: }
545:
546: /**
547: * Fetches a stream for the given URL, which is about to
548: * be loaded by the <code>setPage</code> method.
549: * This method is expected to have the the side effect of
550: * establishing the content type, and therefore setting the
551: * appropriate <code>EditorKit</code> to use for loading the stream.
552: * <p>
553: * If debugger is not running returns super implementation.
554: * <p>
555: * If debugger runs it will set content type to text/html.
556: * Forwarding is not supported is that case.
557: * <p>Control using sysprop org.openide.awt.SwingBrowserImpl.do-not-block-awt=true.
558: *
559: * @param page the URL of the page
560: */
561: protected InputStream getStream(URL page) throws IOException {
562: SwingUtilities.invokeLater(SwingBrowserImpl.this );
563:
564: // #53207: pre-read encoding from loaded URL
565: String charset = findEncodingFromURL(page.openStream());
566: LOG.log(Level.FINE, "Url " + page + " has charset "
567: + charset); // NOI18N
568:
569: if (charset != null) {
570: putClientProperty("charset", charset);
571: }
572:
573: // XXX debugger ought to set this temporarily
574: if (Boolean
575: .getBoolean("org.openide.awt.SwingBrowserImpl.do-not-block-awt")) {
576: // try to set contentType quickly and return (don't block AWT Thread)
577: setContentType("text/html"); // NOI18N
578:
579: return new FilteredInputStream(page.openConnection(),
580: SwingBrowserImpl.this );
581: } else {
582: return super .getStream(page);
583: }
584: }
585:
586: public Dimension getPreferredSize() {
587: try {
588: return super .getPreferredSize();
589: } catch (RuntimeException e) {
590: //Bug in javax.swing.text.html.BlockView
591: return new Dimension(400, 600);
592: }
593: }
594:
595: public void paint(Graphics g) {
596: try {
597: super .paint(g);
598: lastPaintException = false;
599: } catch (RuntimeException e) {
600: //Bug in javax.swing.text.html.BlockView
601: //do nothing
602: if (!lastPaintException) {
603: repaint();
604: }
605:
606: lastPaintException = true;
607: }
608: }
609:
610: /**
611: * An action to scroll the browser content up or down.
612: */
613: private class ScrollAction extends AbstractAction {
614: int direction;
615:
616: public ScrollAction(int direction) {
617: this .direction = direction;
618: }
619:
620: public void actionPerformed(java.awt.event.ActionEvent e) {
621: Rectangle r = getVisibleRect();
622: int increment = getScrollableUnitIncrement(r,
623: SwingConstants.VERTICAL, direction);
624: r.y += (increment * direction);
625: scrollRectToVisible(r);
626: }
627: }
628: }
629:
630: /**
631: * FilterInputStream that delays opening of stream.
632: * The purpose is not to initialize the stream when it is created in getStream()
633: * but to do it later when the content is asynchronously loaded in separate thread.
634: */
635: private static class FilteredInputStream extends FilterInputStream {
636: private final URLConnection conn;
637: private final SwingBrowserImpl browser;
638:
639: FilteredInputStream(URLConnection conn, SwingBrowserImpl browser) {
640: super ((FilterInputStream) null);
641: this .conn = conn;
642: this .browser = browser;
643: }
644:
645: private synchronized void openStream() throws IOException {
646: if (in == null) {
647: in = conn.getInputStream();
648: }
649: }
650:
651: public int available() throws IOException {
652: openStream();
653:
654: return super .available();
655: }
656:
657: public long skip(long n) throws IOException {
658: openStream();
659:
660: return super .skip(n);
661: }
662:
663: public void reset() throws IOException {
664: openStream();
665: super .reset();
666: }
667:
668: public void close() throws IOException {
669: openStream();
670: super .close();
671: Mutex.EVENT.readAccess(browser);
672: }
673:
674: public int read(byte[] b) throws IOException {
675: openStream();
676:
677: return super .read(b);
678: }
679:
680: public int read(byte[] b, int off, int len) throws IOException {
681: openStream();
682:
683: return super .read(b, off, len);
684: }
685:
686: public int read() throws IOException {
687: openStream();
688:
689: return super.read();
690: }
691: }
692: }
|