com.google.speedtracer.client.SourceViewer.java Source code

Java tutorial

Introduction

Here is the source code for com.google.speedtracer.client.SourceViewer.java

Source

/*
 * Copyright 2009 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.speedtracer.client;

import com.google.gwt.chrome.crx.client.Chrome;
import com.google.gwt.dom.client.AnchorElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.HeadElement;
import com.google.gwt.dom.client.IFrameElement;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.dom.client.SpanElement;
import com.google.gwt.dom.client.StyleElement;
import com.google.gwt.dom.client.TableCellElement;
import com.google.gwt.dom.client.TableElement;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.events.client.Event;
import com.google.gwt.events.client.EventListener;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.topspin.ui.client.ClickEvent;
import com.google.gwt.topspin.ui.client.ClickListener;
import com.google.speedtracer.client.util.Command;

/**
 * Class that exposes API for mapping a resource URL and line number to actual
 * source. This wraps an iFrame which contains the source contents formatted as
 * an HTML table. View and Model are coupled in that the presentation of the
 * source (rendered as a styled table) also encodes the line number and line
 * contents per row.
 * 
 * WebKit's inspector allows for iFrames to take on the semantics of a
 * view-source:// URL if you attach the appropriate attribute
 * <code>viewSource="true"</code> to the iFrame element. Since we are not
 * allowed to violate same origin, we rely on Cross Domain XHR functionality
 * provided by Chromium Extensions. We use a simply proxy script embedded in a
 * local "ResourceFetcher.html" file that fetches the resource, and formats it
 * appropriately in the table structure that view-source:// Urls expect.
 */
public class SourceViewer {
    /**
     * Things that contain a SourceViewer can implement this interface to have
     * external entities display source through them.
     * 
     * TODO (jaimeyap): Remove the absoluteFilePath param when GPE is able to
     * resolve classpath relative paths to files.
     */
    public interface SourcePresenter {
        void showSource(String resourceUrl, SourceViewerServer sourceViewerServer, int lineNumber, int column,
                String absoluteFilePath);
    }

    /**
     * Styles that get applied to the internals of the iFrame for highlighting
     * line numbers and other code styling.
     */
    public interface CodeCss extends CssResource {
        String columnMarker();

        String highlightedLine();

        /**
         * Some spacing between the top of the source viewer and the line that is
         * scrolled into view.
         */
        int scrollPadding();
    }

    /**
     * Styles for styling the outside of this Widget.
     */
    public interface Css extends CssResource {
        String base();

        String closeLink();

        String frameWrapper();

        String header();

        String titleText();
    }

    /**
     * Externalized ClientBundle Resource interface.
     */
    public interface Resources extends ClientBundle {
        @Source("resources/column-marker.png")
        ImageResource columnMarker();

        @Source("resources/SourceViewerCode.css")
        CodeCss sourceViewerCodeCss();

        @Source("resources/SourceViewer.css")
        Css sourceViewerCss();
    }

    /**
     * Callback invoked when the source that was requested has been loaded.
     */
    public interface SourceViewerLoadedCallback {
        void onSourceViewerLoaded(SourceViewer viewer);

        void onSourceFetchFail(int statusCode, SourceViewer viewer);
    }

    /**
     * Callback invoked when the SourceViewer instance is initialized and ready
     * for use.
     */
    public interface SourceViewerInitializedCallback {
        void onSourceViewerInitialized(SourceViewer viewer);
    }

    /**
     * Callback used internally to know when the SourceFetcher.html page has
     * fetched the source resource.
     */
    private interface SourceFetcherCallback {
        void onContentReady(int statusCode);
    }

    private static final String LINE_CONTENT = "webkit-line-content";

    /**
     * Creates an instance of the SourceViewer and invokes the passed in callback
     * when the iFrame is loaded with the target source.
     * 
     * We first load the proxy html page. This proxy page uses cross site XHR
     * enabled by Chrome extensions to fetch and format the target source. Once
     * the target source is loaded, we consider the viewer initialized.
     * 
     * @param parent the parent container element we will attach the SourceViewer
     *          to.
     * @param resources the ClientBundle instance for this class.
     * @param initializedCallback the {@link SourceViewerInitializedCallback} that
     *          we pass the loaded SourceViewer to.
     */
    public static void create(Element parent, final Resources resources,
            final SourceViewerInitializedCallback initializedCallback) {
        Document document = parent.getOwnerDocument();
        // Create the iframe within which we will load the source.
        final IFrameElement sourceFrame = document.createIFrameElement();
        Element frameWrapper = document.createDivElement();
        frameWrapper.setClassName(resources.sourceViewerCss().frameWrapper());
        frameWrapper.appendChild(sourceFrame);

        final Element baseElement = document.createDivElement();
        final Element headerElem = document.createDivElement();
        headerElem.setClassName(resources.sourceViewerCss().header());
        baseElement.appendChild(headerElem);

        // IFrame must be attached to fire onload.
        baseElement.appendChild(frameWrapper);
        parent.appendChild(baseElement);
        Event.addEventListener("load", sourceFrame, new EventListener() {
            public void handleEvent(Event event) {
                // The source fetcher should be loaded. Lets now point it at the source
                // we want to load.
                SourceViewer sourceViewer = new SourceViewer(baseElement, headerElem, sourceFrame, resources);
                initializedCallback.onSourceViewerInitialized(sourceViewer);
            }
        });

        sourceFrame.setSrc(Chrome.getExtension().getUrl("monitor/SourceFetcher.html"));
    }

    /**
     * Calls into the iframe's window context to call a method defined by
     * SourceFetcher.html. This method does a Cross Domain XHR to fetch the source
     * resource we want to view.
     */
    private static native void fetchSource(IFrameElement sourceFrame, String targetSource,
            SourceFetcherCallback callback) /*-{
                                            var frameWindow = sourceFrame.contentWindow;
                                            if (frameWindow && frameWindow._doFetchUrl) {
                                            frameWindow._doFetchUrl(targetSource, function(statusCode) {
                                            callback.@com.google.speedtracer.client.SourceViewer.SourceFetcherCallback::onContentReady(I)(statusCode);
                                            });
                                            }
                                            }-*/;

    private static void injectStyles(IFrameElement sourceFrame, String styleText) {
        Document iframeDocument = sourceFrame.getContentDocument();
        HeadElement head = iframeDocument.getElementsByTagName("head").getItem(0).cast();
        StyleElement styleTag = iframeDocument.createStyleElement();
        styleTag.setInnerText(styleText);
        head.appendChild(styleTag);
    }

    private final SpanElement columnMarker;

    private String currentResourceUrl;

    // The base Element container for the SourceViewer.
    private final Element element;

    private TableRowElement highlightedRow;

    private final IFrameElement sourceFrame;

    private final CodeCss styles;

    // The element where we display the URL of the currently loaded source.
    private final Element titleElement;

    protected SourceViewer(Element myElement, Element headerElem, IFrameElement sourceFrame, Resources resources) {
        this.element = myElement;
        this.sourceFrame = sourceFrame;
        this.styles = resources.sourceViewerCodeCss();
        this.element.setClassName(resources.sourceViewerCss().base());

        // Create the title element and the close link.
        Document document = myElement.getOwnerDocument();
        this.titleElement = document.createDivElement();
        titleElement.setClassName(resources.sourceViewerCss().titleText());
        AnchorElement closeLink = document.createAnchorElement();
        closeLink.setClassName(resources.sourceViewerCss().closeLink());
        closeLink.setHref("javascript:;");
        closeLink.setInnerText("Close");
        headerElem.appendChild(titleElement);
        headerElem.appendChild(closeLink);

        this.columnMarker = document.createSpanElement();

        // TODO(jaimeyap): I guess this listener is going to leak.
        ClickEvent.addClickListener(closeLink, closeLink, new ClickListener() {
            public void onClick(ClickEvent event) {
                hide();
            }
        });

        injectStyles(sourceFrame, this.styles.getText());
    }

    public String getCurrentResourceUrl() {
        return currentResourceUrl;
    }

    public Element getElement() {
        return element;
    }

    /**
     * Gets the source line contents for a specified line number.
     * 
     * @param lineNumber the 1 based line index.
     * @return the source line contents as a String.
     */
    public String getLineContents(int lineNumber) {
        TableRowElement row = getTableRowElement(lineNumber);
        TableCellElement contents = getRowContentCell(row);
        if (contents != null) {
            return contents.getInnerText();
        }

        return null;
    }

    public void hide() {
        getElement().getStyle().setDisplay(Display.NONE);
    }

    /**
     * Highlights a specified line of source. Only one line of source will be
     * highlighted at a time. Subsequent calls to this method will remove
     * highlighting of other lines before highlighting the new line.
     * 
     * @param lineNumber the 1 based line index this method will highlight.
     */
    public void highlightLine(int lineNumber) {
        if (highlightedRow != null) {
            highlightedRow.removeClassName(styles.highlightedLine());
        }
        TableRowElement row = getTableRowElement(lineNumber);
        if (row != null) {
            highlightedRow = row;
            highlightedRow.addClassName(styles.highlightedLine());
        }
    }

    /**
     * Points the SourceViewer at a particular resource URL and calls back once it
     * has loaded. This method is asynchronous and will call you back some time
     * later.
     * 
     * @param resource the resource URL you wish to load.
     * @param callback the {@link SourceViewerLoadedCallback} that gets invoked
     *          once the resource has been fetched.
     */
    public void loadResource(final String resource, final SourceViewerLoadedCallback callback) {
        // Defense against empty urls.
        if (resource == null || "".equals(resource)) {
            Command.defer(new Command.Method() {
                public void execute() {
                    callback.onSourceFetchFail(-1, SourceViewer.this);
                }
            });
            return;
        }

        // Early out if this frame already points at the requested resource.
        if (resource.equals(currentResourceUrl)) {
            // This method is expected to be asynchronous.
            Command.defer(new Command.Method() {
                public void execute() {
                    callback.onSourceViewerLoaded(SourceViewer.this);
                }
            });
            return;
        }

        fetchSource(sourceFrame, resource, new SourceFetcherCallback() {
            public void onContentReady(int statusCode) {
                if (statusCode == 200) {
                    // This target source resource should now be fetched and the table
                    // constructed.
                    sourceFrame.setAttribute("viewSource", "true");
                    currentResourceUrl = resource;

                    // Display the name of the resource URL and a link to close it.
                    titleElement.setInnerText("Viewing: " + currentResourceUrl);
                    callback.onSourceViewerLoaded(SourceViewer.this);
                } else {
                    callback.onSourceFetchFail(statusCode, SourceViewer.this);
                }
            }
        });
    }

    /**
     * Places a marker in the source for the specified line number at the
     * specified character offset.
     * 
     * @param lineNumber the line which we will use for marking the column.
     * @param columnNumber the offset from the start of the line to mark.
     */
    public void markColumn(int lineNumber, int columnNumber) {
        if (columnNumber <= 0) {
            return;
        }

        TableCellElement contentCell = getRowContentCell(getTableRowElement(lineNumber));
        columnMarker.removeFromParent();

        int zeroIndexCol = columnNumber - 1;
        String textBeforeMark = contentCell.getInnerText().substring(0, zeroIndexCol);
        String textAfterMark = contentCell.getInnerText().substring(zeroIndexCol);

        Document document = contentCell.getOwnerDocument();
        contentCell.setInnerText("");
        contentCell.appendChild(document.createTextNode(textBeforeMark));
        contentCell.appendChild(columnMarker);
        contentCell.appendChild(document.createTextNode(textAfterMark));
        columnMarker.setClassName(styles.columnMarker());
    }

    /**
     * We scroll to the highlighted node to the top of the source viewer frame.
     */
    public void scrollColumnMarkerIntoView() {
        sourceFrame.getContentDocument().setScrollTop(columnMarker.getOffsetTop() - styles.scrollPadding());
    }

    /**
     * We scroll to the highlighted line to the top of the source viewer frame.
     */
    public void scrollHighlightedLineIntoView() {
        sourceFrame.getContentDocument().setScrollTop(highlightedRow.getOffsetTop() - styles.scrollPadding());
    }

    public void show() {
        getElement().getStyle().setDisplay(Display.BLOCK);
    }

    /**
     * Getter for the <code>tr</code> element wrapping the line of code.
     * 
     * @param lineNumber the 1 based index for the row.
     * @return the {@link TableRowElement} wrapping the line of code.
     */
    TableRowElement getTableRowElement(int lineNumber) {
        NodeList<TableElement> tables = sourceFrame.getContentDocument().getElementsByTagName("table").cast();
        TableElement sourceTable = tables.getItem(0);

        assert (lineNumber > 0);
        assert (sourceTable != null) : "No table loaded in source frame.";

        return sourceTable.getRows().getItem(lineNumber - 1);
    }

    /**
     * Returns the cell that contains the line contents for a row.
     */
    private TableCellElement getRowContentCell(TableRowElement row) {
        NodeList<TableCellElement> cells = row.getElementsByTagName("td").cast();

        for (int i = 0, n = cells.getLength(); i < n; i++) {
            TableCellElement cell = cells.getItem(i);
            if (cell.getClassName().indexOf(LINE_CONTENT) >= 0) {
                return cell;
            }
        }

        return null;
    }
}