package textbender.a.r.page; // Copyright 2006-2007, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Textbender Software"), to deal in the Textbender Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Textbender Software, and to permit persons to whom the Textbender Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Textbender Software. THE TEXTBENDER SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE TEXTBENDER SOFTWARE OR THE USE OR OTHER DEALINGS IN THE TEXTBENDER SOFTWARE. import java.io.*; import java.net.*; import java.rmi.*; import java.security.*; import java.util.*; import java.util.concurrent.atomic.*; import java.util.regex.*; import netscape.javascript.*; import org.w3c.dom.*; import org.w3c.dom.events.*; import org.w3c.dom.html.*; import textbender._.*; import textbender.a.r.page.navdo.*; import textbender.d.gene.*; import textbender.d.gene.xhtml.*; import textbender.g.hold.*; import textbender.g.lang.*; import textbender.g.net.*; import textbender.g.util.*; import textbender.g.util.logging.*; import textbender.o.*; import textbender.o.awt.*; import textbender.o.rhinohide.*; import textbender.o.rhinohide.events.*; import static textbender._.Textbender.TEXTBENDER_NAMESPACE; /** A single page visit (or refresh) in a browser. */ public final @ThreadSafe class PageVisit { /** Waits for page to become ready, and creates a PageVisit. *

* Launches a desk daemon (from the browser's own VM) * if none was already running. * It will be left running after the page is released. * (Avoid clearing its classloader during testing. * Use a separate browser to load the desk daemon, initially. * And before clearing the cache in any other browser, * back out of the recombinant text page first, * so the page daemon's listeners can deregister.) *

* * @param _w browser window, per {@linkplain #window() window}() * @param _f file, per {@linkplain #file() file}() * @param _fV versioned file, per {@linkplain #versionedFile() versionedFile}() * @param _a toolbar applet, per {@linkplain #applet() applet}() * * @throws IOException if serializedPage cannot be created, * or RemoteException if an in-page tool cannot connect * with desk daemon * @throws TextbenderException per findMetaData */ PageVisit( RhiWindow _w, File _f, VersionedFile _fV, ToolbarApplet _a ) throws IOException, TextbenderException { window = _w; file = _f; versionedFile = _fV; applet = _a; // Wait for page to become ready. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - document = (HTMLDocument)window.getDocument(); // may block, waiting for it become ready (Firefox, if in-memory caching enabled, which it no longer is, per run.js) // LoggerX.i(getClass()).fine( "page ready, apparently" ); // Process page. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // if( !Browser.isXML( document )) throw new UnsupportedOperationException( "browser did not parse document as XML" ); // avoid complication of coding for documents both with and without namespaces. An HTML parse has no namespaces. ///// Needs DOM 3. Prior guards in boot script may suffice... isPageSilent = file == null; // remote pages are "silent" { final StringBuilder b = new StringBuilder(); // final Element metaData = DocumentRT.findMetaData( document ); // if( metaData == null ) throw new TextbenderException( DocumentRT.FIND_META_DATA_FAILURE_MESSAGE ); //// but allow new, blank texts to be loaded, without meta-data: final Element metaData = RecombinantXHTML.ensureMetaData( document, b ); // final Element gg = Gene.findGG( metaData ); final Element gg = Gene.ensureGG( metaData, b ); final ArrayList _gList = // a faster list implementation (in the case of a Web page) than would be the rhinohide NodeList returned by gg.getElementsByTagNameNS(TEXTBENDER_NAMESPACE,"g") new ArrayList( /*initial capacity*/MEDIAN_GENE_COUNT * 2 ); for( Node child = gg.getFirstChild(); child != null; child = child.getNextSibling() ) { if( !( child instanceof Element )) continue; final Element element = (Element)child; if( "g".equals( element.getLocalName() ) && TEXTBENDER_NAMESPACE.equals( element.getNamespaceURI() )) _gList.add( element ); } // Element[] gArray = gList.toArray( new Element[gList.size()] ); //// no, give memory to gain contruction speed: gList = Collections.unmodifiableList( _gList ); } { geneList = Arrays.asList( new Element[gList.size()] ); MultiMapRM _locusToGeneMap = new MultiMapRM(); initRecursively( document, geneList, _locusToGeneMap ); locusToGeneMap = MultiMap.U.unmodifiableMultiMap( _locusToGeneMap ); } EventQueueX.invokeNowOrWait( new Runnable() { public void run() // in AWT event dispatch thread { synchronized( PageVisit.this ) { edt = new PageVisitEDT( PageVisit.this ); } } }); // Create serialized page. Done eagerly (rather than lazilly) // to get the cleanest copy, excluding later dynamic markup. // (Eager serialization apparently slows start-up for remote pages, // but actually start-up would entail serialization regardless, // because start-up is caused by selection, which leads to creation // of a transferand, which always refers to the serializedPage.) // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` if( file != null ) serializedPage = file; else { final String documentString = (String)window.eval ( "(new XMLSerializer()).serializeToString( document )" ); String encoding = document.getXmlEncoding(); // as declared in source if( encoding == null ) encoding = "UTF-8"; // default for XML serializedPage = File.createTempFile ( /*prefix*/"textbender.a.r.page.PageVisit.serializedPage", /*suffix*/".xml" ); // serializedPage.deleteOnExit(); //// sooner, rather: spool().add( new Hold() { public @ThreadSafe void release() { serializedPage.delete(); } }); final Writer out = new OutputStreamWriter ( new BufferedOutputStream( new FileOutputStream( serializedPage )), encoding ); try { out.write( documentString ); } finally{ out.close(); } } // Install tools in the page. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // transfer // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` transfer_inPageFactory = new textbender.a.u.transfer.InPageFactory( PageVisit.this ); // locusPoint // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` if( !isPageSilent ) new textbender.a.u.locusPoint.InPagePointer( PageVisit.this ); new textbender.a.u.locusPoint.InPageHighlighter( PageVisit.this ); // chromography // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final textbender.a.u.chromography.InPageFactory chromography_inPageFactory = new textbender.a.u.chromography.InPageFactory( PageVisit.this ); new textbender.a.u.chromography.InPageStainer( PageVisit.this, chromography_inPageFactory ); // - - - EventQueueX.invokeNowOrWait( new Runnable() { public void run() // in AWT event dispatch thread { new LeafGeneObjectV( PageVisit.this ); try{ new PopupMenu( PageVisit.this ); } catch( RemoteException x ) { user().logAndShow( x ); } } }); } private void initRecursively( final Node node, final List geneList, final MultiMap locusToGeneMap ) { if( node instanceof Element ) { final Element element = (Element)node; // List and map. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - int gIndex = Gene.gIndexOf( element ); if( gIndex >= 0 ) { geneList.set( gIndex, element ); locusToGeneMap.put( gList.get(gIndex).getAttribute("locus"), element ); } } for( Node child = node.getFirstChild(); child != null; child = child.getNextSibling() ) { initRecursively( child, geneList, locusToGeneMap ); } } // ------------------------------------------------------------------------------------ /** The toolbar applet. */ public ToolbarApplet applet() { return applet; } private final ToolbarApplet applet; /** The page as a DOM document. The return value is equivalent * to (HTMLDocument){@linkplain #window() window}().getDocument(). * It is cached here for efficiency. */ public HTMLDocument document() { return document; } private final HTMLDocument document; /** Facilities restricted to the AWT event dispatch thread. */ public PageVisitEDT edt() { return edt; } private PageVisitEDT edt; // final when initialized /** Returns the source of the page as a local file, * or null if the source is not a local file. * * @see #serializedPage() * @see #versionedFile() */ public File file() { return file; } private final File file; /** Sparse list of genes, indexed by g-index. */ public List geneList() { return geneList; } private final List geneList; /** Returns the reason why the page vist is coming to an end. * Specific reasons are defined by processes that cause page exit; * none are defined here. * * @return the reason, or null if there is no special reason */ public synchronized Object getReleaseReason() { return releaseReason; } private Object releaseReason; /** Sets the releaseReason. * * @see #getReleaseReason * * @throws IllegalStateException if reason already set */ public synchronized void setReleaseReason( Object newReleaseReason ) { if( releaseReason != null ) throw new IllegalStateException( "at most one reason for page visit release" ); releaseReason = newReleaseReason; } /** List of gene meta-data ('g') elements, ordered by g-index. */ public List gList() { return gList; } private final List gList; /** Returns true if the page is remote, and therefore blocks LiveConnect * JavaScript messages to the page daemon. Messages (method calls etc.) * are blocked because of same-origin restrictions on JavaScript, * Remote-origin JavaScript cannot message the local-origin page daemon * without getting an exception like this: *
* sun.plugin.liveconnect.OriginNotAllowedException: JavaScript is not from the same origin as the Java code, caller=http://zelea.com, callee=file:/home/mike/var/tmp-public/textbender-demo/textbender.jar *
*

* The main casualty is event dispatch; * remote pages cannot forward events to the page daemon. * They are effectively silent. *

*/ public boolean isPageSilent() { return isPageSilent; } private final boolean isPageSilent; /** Map of genes, keyed by locus. * Where there are multiple genes per locus, * the map stores them in document order. */ public MultiMap locusToGeneMap() { return locusToGeneMap; } private final MultiMap locusToGeneMap; /** An estimate of the median gene count, per page. */ public static final int MEDIAN_GENE_COUNT = 50; /** Spool unwound before this instance is released. * Useful for any hold whose release depends on other holds * (but which is depended on, in turn, by no other hold). * This spool is nothing but a fudge, * to work around order dependencies during release. * * @see #spool() */ public Spool preSpool() { return applet.preSpool; } /** Returns a serialized copy of the page. * If the source of the page is a local file, * returns the same as {@linkplain #file() file}(); * otherwise returns a file containing a serialization of the page. * In the latter case, the file is automatically deleted at page exit. *

* Note: In the latter case, the serialization will include * any markup added by content and user scripts during boot; * such as injected applets. *

*/ public File serializedPage() { return serializedPage; } private final File serializedPage; /** Spool unwound as this instance is released. * * @see #preSpool() * @see PageVisitEDT#spool() */ public Spool spool() { return applet.spool; } /** This page as a region-transfer source. */ public textbender.a.u.transfer.InPageFactory transfer_inPageFactory() { return transfer_inPageFactory; } private final textbender.a.u.transfer.InPageFactory transfer_inPageFactory; /** The user. */ public User user() { return applet.user; } /** Returns the source of the page as a versioned file, * or null if the source is not a versioned file. * * @see #file() */ public VersionedFile versionedFile() { return versionedFile; } private final VersionedFile versionedFile; /** Browser window visiting the page. */ public RhiWindow window() { return window; } private final RhiWindow window; // /** Returns the local working file of the page, // * or null if the page has no local working file. // * // * @return {@linkplain #versionedFile() versionedFile}().{@linkplain VersionedFile#originalFile() originalFile}() // * if the source is a versioned file; // * else {@linkplain #file file}() // */ // public File workingFile() // { // if( versionedFile == null ) return file; // else return versionedFile.originalFile(); // } }