Package com.google.api.explorer.client.history

Source Code of com.google.api.explorer.client.history.JsonPrettifier$JsObjectIterable

/*
* Copyright (C) 2011 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.api.explorer.client.history;

import com.google.api.explorer.client.Resources;
import com.google.api.explorer.client.Resources.Css;
import com.google.api.explorer.client.base.ApiMethod;
import com.google.api.explorer.client.base.ApiMethod.HttpMethod;
import com.google.api.explorer.client.base.ApiService;
import com.google.api.explorer.client.base.Config;
import com.google.api.explorer.client.base.Schema;
import com.google.api.explorer.client.base.dynamicjso.DynamicJsArray;
import com.google.api.explorer.client.base.dynamicjso.DynamicJso;
import com.google.api.explorer.client.base.dynamicjso.JsType;
import com.google.api.explorer.client.routing.HistoryWrapper;
import com.google.api.explorer.client.routing.HistoryWrapperImpl;
import com.google.api.explorer.client.routing.URLFragment;
import com.google.api.explorer.client.routing.UrlBuilder;
import com.google.api.explorer.client.routing.UrlBuilder.RootNavigationItem;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsonUtils;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.FocusPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.InlineHyperlink;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
import com.google.gwt.user.client.ui.PushButton;
import com.google.gwt.user.client.ui.Widget;

import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

import javax.annotation.Nullable;

/**
* A simple syntax highlighter for JSON data.
*
*/
public class JsonPrettifier {
  /**
   * Class that we can use to re-write runtime Json exceptions to checked.
   */
  public static class JsonFormatException extends Exception {
    private JsonFormatException(String message, Throwable cause) {
      super(message, cause);
    }
  }

  private static final String PLACEHOLDER_TEXT = "...";
  private static final String SEPARATOR_TEXT = ",";
  private static final String OPEN_IN_NEW_WINDOW = "_blank";
  private static final HistoryWrapper history = new HistoryWrapperImpl();

  private static Css style;
  private static Resources resources;

  /**
   * Factory that can be used to manufacture link information that can vary between the full and
   * embedded explorer.
   */
  public interface PrettifierLinkFactory {
    /**
     * Generate a click handler that will redirect to the fragment specified when invoked.
     */
    ClickHandler generateMenuHandler(String fragment);

    /**
     * Generate an anchor widget which will redirect to the fragment specified when clicked.
     */
    Widget generateAnchor(String embeddingText, String fragment);
  }

  /**
   * Link factory which generates links which manipulate the current browser view. This should be
   * used for the explorer full-view context.
   */
  public static final PrettifierLinkFactory LOCAL_LINK_FACTORY = new PrettifierLinkFactory() {
    @Override
    public ClickHandler generateMenuHandler(final String fragment) {
      return new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
          history.newItem(fragment);
        }
      };
    }

    @Override
    public Widget generateAnchor(String embeddingText, String fragment) {
      return new InlineHyperlink(embeddingText, fragment);
    }
  };

  /**
   * Link factory which generates links which either open a new tab, or switch the entire URL to a
   * new location. This should be used with the embedded explorer context.
   */
  public static final PrettifierLinkFactory EXTERNAL_LINK_FACTORY = new PrettifierLinkFactory() {
    @Override
    public ClickHandler generateMenuHandler(final String fragment) {
      return new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
          Window.open(createFullLink(fragment), "_blank", null);
        }
      };
    }

    @Override
    public Widget generateAnchor(String embeddingText, String fragment) {
      return new Anchor(embeddingText, createFullLink(fragment));
    }

    private String createFullLink(String fragment) {
      return Config.EXPLORER_URL + "#" + fragment;
    }
  };


  private static class Collapser implements ClickHandler {
    private final Widget toHide;
    private final Widget placeHolder;
    private final Widget clicker;

    public Collapser(Widget toHide, Widget placeHolder, Widget clicker) {
      this.toHide = toHide;
      this.placeHolder = placeHolder;
      this.clicker = clicker;
    }

    @Override
    public void onClick(ClickEvent arg0) {
      boolean makeVisible = !toHide.isVisible();
      decorateCollapserControl(clicker, makeVisible);
      toHide.setVisible(makeVisible);
      placeHolder.setVisible(!makeVisible);
    }

    public static void decorateCollapserControl(Widget collapser, boolean visible) {
      if (visible) {
        collapser.addStyleName(style.jsonExpanded());
        collapser.removeStyleName(style.jsonCollapsed());
      } else {
        collapser.addStyleName(style.jsonCollapsed());
        collapser.removeStyleName(style.jsonExpanded());
      }
    }
  }

  /**
   * This abstraction of an array creates formatted widgets from all children.
   */
  private static class JsArrayIterable implements Iterable<Widget> {
    private final DynamicJsArray backingObj;
    private final int depth;
    private final ApiService service;
    private final PrettifierLinkFactory linkFactory;

    public JsArrayIterable(
        ApiService service, DynamicJsArray array, int depth, PrettifierLinkFactory linkFactory) {
      this.backingObj = array;
      this.depth = depth;
      this.service = service;
      this.linkFactory = linkFactory;
    }

    @Override
    public Iterator<Widget> iterator() {
      return new Iterator<Widget>() {
        private int nextOffset = 0;

        @Override
        public boolean hasNext() {
          return nextOffset < backingObj.length();
        }

        @Override
        public Widget next() {
          if (!hasNext()) {
            throw new NoSuchElementException();
          }
          Widget next = formatArrayValue(service,
              backingObj,
              nextOffset,
              depth,
              nextOffset + 1 < backingObj.length(),
              linkFactory);
          nextOffset++;
          return next;
        }

        @Override
        public void remove() {
          throw new UnsupportedOperationException();
        }
      };
    }
  }

  /**
   * This abstraction of an object creates formatted widgets from all children.
   */
  private static class JsObjectIterable implements Iterable<Widget> {
    private final DynamicJso backingObj;
    private final int depth;
    private final ApiService service;
    private final PrettifierLinkFactory linkFactory;

    public JsObjectIterable(
        ApiService service, DynamicJso obj, int depth, PrettifierLinkFactory linkFactory) {

      this.backingObj = obj;
      this.depth = depth;
      this.service = service;
      this.linkFactory = linkFactory;
    }

    @Override
    public Iterator<Widget> iterator() {
      return new Iterator<Widget>() {
         int nextOffset = 0;

        @Override
        public boolean hasNext() {
          return nextOffset < backingObj.keys().length();
        }

        @Override
        public Widget next() {
          if (!hasNext()) {
            throw new NoSuchElementException();
          }
          Widget next =
              formatValue(service, backingObj, backingObj.keys().get(nextOffset), depth,
                  nextOffset + 1 < backingObj.keys().length(), linkFactory);
          nextOffset++;
          return next;
        }

        @Override
        public void remove() {
          throw new UnsupportedOperationException();
        }
      };
    }
  }

  /**
   * Must be called before calling prettify to set the resources file to be used. Makes it possible
   * to test this class under JUnit.
   *
   * @param resources Resources (images and style) to use when prettifying.
   */
  public static void setResources(Resources resources) {
    JsonPrettifier.resources = resources;
    JsonPrettifier.style = resources.style();
  }

  /**
   * Entry point for the formatter.
   *
   * @param destination Destination GWT object where the results will be placed
   * @param jsonString String to format
   * @param linkFactory Which links factory should be used when generating links and navigation
   *        menus.
   * @throws JsonFormatException when parsing the Json causes an error
   */
  public static void prettify(
      ApiService service, Panel destination, String jsonString, PrettifierLinkFactory linkFactory)
      throws JsonFormatException {

    // Make sure the user set a style before invoking prettify.
    Preconditions.checkState(style != null, "Must call setStyle before using.");

    Preconditions.checkNotNull(service);
    Preconditions.checkNotNull(destination);

    // Don't bother syntax highlighting empty text.
    boolean empty = Strings.isNullOrEmpty(jsonString);
    destination.setVisible(!empty);
    if (empty) {
      return;
    }

    if (!GWT.isScript()) {
      // Syntax highlighting is *very* slow in Development Mode (~30s for large
      // responses), but very fast when compiled and run as JS (~30ms). For the
      // sake of my sanity, syntax highlighting is disabled in Development
      destination.add(new InlineLabel(jsonString));
    } else {

      try {
        DynamicJso root = JsonUtils.<DynamicJso>safeEval(jsonString);
        Collection<ApiMethod> compatibleMethods = computeCompatibleMethods(root, service);
        Widget menuForMethods = createRequestMenu(compatibleMethods, service, root, linkFactory);
        JsObjectIterable rootObject = new JsObjectIterable(service, root, 1, linkFactory);
        Widget object = formatGroup(rootObject, "", 0, "{", "}", false, menuForMethods);
        destination.add(object);
      } catch (IllegalArgumentException e) {
        // JsonUtils will throw an IllegalArgumentException when it gets invalid
        // Json data. Rewrite as a checked exception and throw.
        throw new JsonFormatException("Invalid json.", e);
      }
    }
  }

  /**
   * Check the provided javascript object for a "kind" key and, and find all methods from the
   * provided service that accept the specified type for the request body.
   *
   * @param object Object which is checked against other methods.
   * @param service Service for which we want to find compatible methods.
   * @return Matching methods that accept the object type as an input, or an empty collection.
   */
  private static Collection<ApiMethod> computeCompatibleMethods(
      DynamicJso object, ApiService service) {

    String kind = object.getString(Schema.KIND_KEY);
    if (kind != null) {
      return service.usagesOfKind(kind);
    } else {
      return Collections.emptyList();
    }
  }

  /**
   * Iterate through an object or array adding the widgets generated for all children
   */
  private static FlowPanel formatGroup(Iterable<Widget> objIterable,
      String title,
      int depth,
      String openGroup,
      String closeGroup,
      boolean hasSeparator,
      @Nullable Widget menuButtonForReuse) {

    FlowPanel object = new FlowPanel();

    FlowPanel titlePanel = new FlowPanel();
    Label paddingSpaces = new InlineLabel(indentation(depth));
    titlePanel.add(paddingSpaces);

    Label titleLabel = new InlineLabel(title + openGroup);
    titleLabel.addStyleName(style.jsonKey());
    Collapser.decorateCollapserControl(titleLabel, true);
    titlePanel.add(titleLabel);

    object.add(titlePanel);

    FlowPanel objectContents = new FlowPanel();

    if (menuButtonForReuse != null) {
      objectContents.addStyleName(style.reusableResource());
      objectContents.add(menuButtonForReuse);
    }

    for (Widget child : objIterable) {
      objectContents.add(child);
    }
    object.add(objectContents);

    InlineLabel placeholder = new InlineLabel(indentation(depth + 1) + PLACEHOLDER_TEXT);
    ClickHandler collapsingHandler = new Collapser(objectContents, placeholder, titleLabel);
    placeholder.setVisible(false);
    placeholder.addClickHandler(collapsingHandler);
    object.add(placeholder);

    titleLabel.addClickHandler(collapsingHandler);

    StringBuilder closingLabelText = new StringBuilder(indentation(depth)).append(closeGroup);
    if (hasSeparator) {
      closingLabelText.append(SEPARATOR_TEXT);
    }

    object.add(new Label(closingLabelText.toString()));

    return object;
  }

  private static Widget formatArrayValue(ApiService service,
      DynamicJsArray obj,
      int index,
      int depth,
      boolean hasSeparator,
      PrettifierLinkFactory linkFactory) {

    JsType type = obj.typeofIndex(index);
    if (type == null) {
      return simpleInline("", "null", style.jsonNull(), depth, hasSeparator);
    }
    String title = "";
    switch (type) {
      case NUMBER:
        return simpleInline(
            title, String.valueOf(obj.getDouble(index)), style.jsonNumber(), depth, hasSeparator);

      case INTEGER:
        return simpleInline(
            title, String.valueOf(obj.getInteger(index)), style.jsonNumber(), depth, hasSeparator);

      case BOOLEAN:
        return simpleInline(
            title, String.valueOf(obj.getBoolean(index)), style.jsonBoolean(), depth, hasSeparator);

      case STRING:
        return inlineWidget(
            title, formatString(service, obj.getString(index), linkFactory), depth, hasSeparator);

      case ARRAY:
        return formatGroup(
            new JsArrayIterable(service, obj.<DynamicJsArray>get(index), depth + 1, linkFactory),
            title, depth, "[", "]", hasSeparator, null);

      case OBJECT:
        DynamicJso subObject = obj.<DynamicJso>get(index);

        // Determine if this object can be used as the request parameter for another method.
        Collection<ApiMethod> compatibleMethods = computeCompatibleMethods(subObject, service);
        Widget menuFromMethods =
            createRequestMenu(compatibleMethods, service, subObject, linkFactory);
        JsObjectIterable objIter = new JsObjectIterable(service, subObject, depth + 1, linkFactory);
        return formatGroup(objIter, title, depth, "{", "}", hasSeparator, menuFromMethods);
    }
    return new FlowPanel();
  }

  private static Widget formatValue(ApiService service,
      DynamicJso obj,
      String key,
      int depth,
      boolean hasSeparator,
      PrettifierLinkFactory linkFactory) {

    JsType type = obj.typeofKey(key);
    if (type == null) {
      return simpleInline(titleString(key), "null", style.jsonNull(), depth, hasSeparator);
    }
    String title = titleString(key);
    switch (type) {
      case NUMBER:
        return simpleInline(
            title, String.valueOf(obj.getDouble(key)), style.jsonNumber(), depth, hasSeparator);

      case INTEGER:
        return simpleInline(
            title, String.valueOf(obj.getInteger(key)), style.jsonNumber(), depth, hasSeparator);

      case BOOLEAN:
        return simpleInline(
            title, String.valueOf(obj.getBoolean(key)), style.jsonBoolean(), depth, hasSeparator);

      case STRING:
        return inlineWidget(
            title, formatString(service, obj.getString(key), linkFactory), depth, hasSeparator);

      case ARRAY:
        return formatGroup(
            new JsArrayIterable(service, obj.<DynamicJsArray>get(key), depth + 1, linkFactory),
            title, depth, "[", "]", hasSeparator, null);

      case OBJECT:
        DynamicJso subObject = obj.<DynamicJso>get(key);

        // Determine if this object can be used as the request parameter for another method.
        Collection<ApiMethod> compatibleMethods = computeCompatibleMethods(subObject, service);
        JsObjectIterable objIter = new JsObjectIterable(service, subObject, depth + 1, linkFactory);
        return formatGroup(objIter, title, depth, "{", "}", hasSeparator, null);
    }
    return new FlowPanel();
  }

  private static Widget simpleInline(
      String title, String inlineText, String style, int depth, boolean hasSeparator) {
    Widget valueLabel = new InlineLabel(inlineText);
    valueLabel.addStyleName(style);
    return inlineWidget(title, Lists.newArrayList(valueLabel), depth, hasSeparator);
  }

  private static Widget inlineWidget(
      String title, List<Widget> inlineWidgets, int depth, boolean hasSeparator) {

    FlowPanel inlinePanel = new FlowPanel();

    StringBuilder keyText = new StringBuilder(indentation(depth)).append(title);
    InlineLabel keyLabel = new InlineLabel(keyText.toString());
    keyLabel.addStyleName(style.jsonKey());
    inlinePanel.add(keyLabel);

    for (Widget child : inlineWidgets) {
      inlinePanel.add(child);
    }

    if (hasSeparator) {
      inlinePanel.add(new InlineLabel(SEPARATOR_TEXT));
    }

    return inlinePanel;
  }

  private static String indentation(int depth) {
    return Strings.repeat(" ", depth);
  }

  private static List<Widget> formatString(
      ApiService service, String rawText, PrettifierLinkFactory linkFactory) {

    if (isLink(rawText)) {
      List<Widget> response = Lists.newArrayList();
      response.add(new InlineLabel("\""));

      boolean createdExplorerLink = false;
      try {
        ApiMethod method = getMethodForUrl(service, rawText);
        if (method != null) {
          String explorerLink = createExplorerLink(service, rawText, method);
          Widget linkObject = linkFactory.generateAnchor(rawText, explorerLink);
          linkObject.addStyleName(style.jsonStringExplorerLink());
          response.add(linkObject);
          createdExplorerLink = true;
        }
      } catch (IndexOutOfBoundsException e) {
        // Intentionally blank - this will only happen when iterating the method
        // url template in parallel with the url components and you run out of
        // components
      }

      if (!createdExplorerLink) {
        Anchor linkObject = new Anchor(rawText, rawText, OPEN_IN_NEW_WINDOW);
        linkObject.addStyleName(style.jsonStringLink());
        response.add(linkObject);
      }

      response.add(new InlineLabel("\""));
      return response;
    } else {
      JSONString encoded = new JSONString(rawText);
      Widget stringText = new InlineLabel(encoded.toString());
      stringText.addStyleName(style.jsonString());
      return Lists.newArrayList(stringText);
    }
  }

  private static String titleString(String name) {
    return "\"" + name + "\": ";
  }

  /**
   * Attempts to identify an {@link ApiMethod} corresponding to the given url.
   * If one is found, a {@link java.util.Map.Entry} will be returned where the key is the
   * name of the method, and the value is the {@link ApiMethod} itself. If no
   * method is found, this will return {@code null}.
   */
  @VisibleForTesting
  static ApiMethod getMethodForUrl(ApiService service, String url) {
    String apiLinkPrefix = Config.getBaseUrl() + service.basePath();
    if (!url.startsWith(apiLinkPrefix)) {
      return null;
    }

    // Only check GET methods since those are the only ones that can be returned
    // in the response.
    Iterable<ApiMethod> getMethods =
        Iterables.filter(service.allMethods().values(), new Predicate<ApiMethod>() {
          @Override
          public boolean apply(ApiMethod input) {
            return input.getHttpMethod() == HttpMethod.GET;
          }
        });

    int paramIndex = url.indexOf("?");
    String path = url.substring(0, paramIndex > 0 ? paramIndex : url.length());
    for (ApiMethod method : getMethods) {
      // Try to match the request URL with its method by comparing it to the
      // method's rest base path URI template. To do this we have to remove the
      // {...} placeholders.
      String regex =
          apiLinkPrefix + method.getPath().replaceAll("\\{[^\\/]+\\}", "[^\\/]+");
      if (path.matches(regex)) {
        return method;
      }
    }
    return null;
  }

  /**
   * Creates an Explorer link token (e.g.,
   * #s/<service>/<version>/<method>) corresponding to the given request
   * URL, given the method name and method definition returned by
   * {@link #getMethodForUrl(ApiService, String)}.
   */
  @VisibleForTesting
  static String createExplorerLink(ApiService service, String url, ApiMethod method) {
    UrlBuilder builder = new UrlBuilder();

    // Add the basic information to the
    builder.addRootNavigationItem(RootNavigationItem.ALL_VERSIONS)
        .addService(service.getName(), service.getVersion())
        .addMethodName(method.getId());

    // Calculate the params from the path template and url.
    URLFragment parsed = URLFragment.parseFragment(url);
    Multimap<String, String> params = HashMultimap.create();
    String pathTemplate = method.getPath();
    if (pathTemplate.contains("{")) {
      String urlPath = parsed.getPath().replaceFirst(Config.getBaseUrl() + service.basePath(), "");
      String[] templateSections = pathTemplate.split("/");
      String[] urlSections = urlPath.split("/");
      for (int i = 0; i < templateSections.length; i++) {
        if (templateSections[i].contains("{")) {
          String paramName = templateSections[i].substring(1, templateSections[i].length() - 1);
          params.put(paramName, urlSections[i]);
        }
      }
    }

    // Apply the params.
    String fullUrl = builder.addQueryParams(params).toString();

    // Check if the url had query parameters to add.
    if (!parsed.getQueryString().isEmpty()) {
      fullUrl = fullUrl + parsed.getQueryString();
    }

    return fullUrl;
  }

  private static boolean isLink(String value) {
    return (value.startsWith("http://") || value.startsWith("https://")) && !value.contains("\n")
        && !value.contains("\t");
  }

  /**
   * Create a drop down menu that allows the user to navigate to compatible methods for the
   * specified resource.
   *
   * @param methods Methods for which to build the menu.
   * @param service Service to which the methods correspond.
   * @param objectToPackage Object which should be passed to the destination menus.
   * @param linkFactory Factory that will be used to create links.
   * @return A button that will show the menu that was generated or {@code null} if there are no
   *         compatible methods.
   */
  private static PushButton createRequestMenu(final Collection<ApiMethod> methods,
      final ApiService service, DynamicJso objectToPackage, PrettifierLinkFactory linkFactory) {

    // Determine if a menu even needs to be generated.
    if (methods.isEmpty()) {
      return null;
    }

    // Create the parameters that will be passed to the destination menu.
    String resourceContents = new JSONObject(objectToPackage).toString();
    final Multimap<String, String> resourceParams =
        ImmutableMultimap.of(UrlBuilder.BODY_QUERY_PARAM_KEY, resourceContents);

    // Create the menu itself.
    FlowPanel menuContents = new FlowPanel();

    // Add a description of what the menu does.
    Label header = new Label("Use this resource in one of the following methods:");
    header.addStyleName(style.dropDownMenuItem());
    menuContents.add(header);

    // Add a menu item for each method.
    for (ApiMethod method : methods) {
      PushButton methodItem = new PushButton();
      methodItem.addStyleName(style.dropDownMenuItem());
      methodItem.addStyleName(style.selectableDropDownMenuItem());
      methodItem.setText(method.getId());
      menuContents.add(methodItem);

      // When clicked, Navigate to the menu item.
      UrlBuilder builder = new UrlBuilder();
      String newUrl = builder
          .addRootNavigationItem(RootNavigationItem.ALL_VERSIONS)
          .addService(service.getName(), service.getVersion())
          .addMethodName(method.getId())
          .addQueryParams(resourceParams)
          .toString();
      methodItem.addClickHandler(linkFactory.generateMenuHandler(newUrl));
    }

    // Create the panel which will be disclosed.
    final PopupPanel popupMenu = new PopupPanel(/* auto hide */ true);
    popupMenu.setStyleName(style.dropDownMenuPopup());

    FocusPanel focusContents = new FocusPanel();
    focusContents.addMouseOutHandler(new MouseOutHandler() {
      @Override
      public void onMouseOut(MouseOutEvent event) {
        popupMenu.hide();
      }
    });
    focusContents.setWidget(menuContents);

    popupMenu.setWidget(focusContents);

    // Create the button which will disclose the menu.
    final PushButton menuButton = new PushButton(new Image(resources.downArrow()));
    menuButton.addStyleName(style.reusableResourceButton());

    menuButton.addClickHandler(new ClickHandler() {
      @Override
      public void onClick(ClickEvent event) {
        popupMenu.setPopupPositionAndShow(new PositionCallback() {
          @Override
          public void setPosition(int offsetWidth, int offsetHeight) {
            popupMenu.setPopupPosition(
                menuButton.getAbsoluteLeft() + menuButton.getOffsetWidth() - offsetWidth,
                menuButton.getAbsoluteTop() + menuButton.getOffsetHeight());
          }
        });
      }
    });

    // Return only the button to the caller.
    return menuButton;
  }
}
TOP

Related Classes of com.google.api.explorer.client.history.JsonPrettifier$JsObjectIterable

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.