/*
* Copyright (C) 2010 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;
import com.google.api.explorer.client.FullViewPresenter.NavigationItem;
import com.google.api.explorer.client.analytics.AnalyticsManager;
import com.google.api.explorer.client.auth.AuthView;
import com.google.api.explorer.client.base.ApiDirectory.ServiceDefinition;
import com.google.api.explorer.client.base.ApiMethod;
import com.google.api.explorer.client.base.ApiRequest;
import com.google.api.explorer.client.base.ApiResponse;
import com.google.api.explorer.client.base.ApiService;
import com.google.api.explorer.client.base.NameHelper;
import com.google.api.explorer.client.context.ExplorerContext;
import com.google.api.explorer.client.context.ListServiceContext.TagProcessor;
import com.google.api.explorer.client.embedded.EmbeddedParameterFormPresenter.RequestFinishedCallback;
import com.google.api.explorer.client.embedded.EmbeddedView;
import com.google.api.explorer.client.history.EmbeddedHistoryItemView;
import com.google.api.explorer.client.history.HistoryItem;
import com.google.api.explorer.client.history.JsonPrettifier;
import com.google.api.explorer.client.navigation.EntryAggregatorView;
import com.google.api.explorer.client.navigation.HistoryEntry;
import com.google.api.explorer.client.navigation.MethodEntry;
import com.google.api.explorer.client.navigation.SectionedAggregator;
import com.google.api.explorer.client.navigation.ServiceEntry;
import com.google.api.explorer.client.navigation.ServiceEntry.DescriptionTag;
import com.google.api.explorer.client.routing.TitleSupplier.Title;
import com.google.api.explorer.client.routing.URLManipulator;
import com.google.api.explorer.client.routing.UrlBuilder.RootNavigationItem;
import com.google.api.explorer.client.routing.handler.HistoryManager.HistoryManagerDelegate;
import com.google.api.explorer.client.search.SearchManager.SearchReadyCallback;
import com.google.api.explorer.client.search.SearchResult;
import com.google.api.explorer.client.search.SearchResult.MethodBundle;
import com.google.api.explorer.client.widgets.PlaceholderTextBox;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.DockLayoutPanel;
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.MenuBar;
import com.google.gwt.user.client.ui.MenuItem;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.PushButton;
import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.SuggestBox.DefaultSuggestionDisplay;
import com.google.gwt.user.client.ui.SuggestBox.SuggestionDisplay;
import com.google.gwt.user.client.ui.SuggestOracle;
import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
import com.google.gwt.user.client.ui.Widget;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* View of the whole app.
*
* @author jasonhall@google.com (Jason Hall)
*/
public class FullView extends Composite
implements FullViewPresenter.Display, HistoryManagerDelegate, SearchReadyCallback {
private static FullViewUiBinder uiBinder = GWT.create(FullViewUiBinder.class);
private static final String REPORT_ERROR_URL = "http://code.google.com/p/google-apis-explorer/"
+ "issues/entry?template=Defect%20report%20from%20user";
private static final String EXPLORER_HELP_URL = "http://code.google.com/apis/explorer-help";
private static final String EXPLORER_FORUM_URL =
"http://code.google.com/apis/explorer-help/forum.html";
private static final String NEW_TAB_TARGET = "_blank";
private static final String SETTINGS_MENU_CSS_RULE = "settingsMenu";
private static final boolean HIDE_AUTH = false;
interface FullViewUiBinder extends UiBinder<Widget, FullView> {
}
interface FullViewStyle extends CssResource {
String selectedNavigation();
String searchPlaceholderText();
String methodSubtitle();
}
@UiField FullViewStyle style;
@UiField DockLayoutPanel dockLayoutPanel;
@UiField Image logo;
@UiField PushButton backButton;
@UiField Widget searchLoadingIndicator;
@UiField(provided = true) SuggestBox searchBox;
@UiField Panel searchErrorPanel;
@UiField SectionedAggregator searchResults;
@UiField EntryAggregatorView drillDownNav;
@UiField Panel detailHeader;
@UiField Panel detailTitleContainer;
@UiField Panel authViewPlaceholder;
@UiField Panel docsContainer;
@UiField Panel detailPane;
@UiField Panel preferredServicesMenuItem;
@UiField Panel requestHistoryMenuItem;
@UiField Panel allServicesMenuItem;
@UiField MenuBar settingsMenu;
@UiField MenuItem helpItem;
@UiField MenuItem forumItem;
@UiField MenuItem bugReportItem;
private final FullViewPresenter presenter;
private final AuthManager authManager;
private final AnalyticsManager analytics;
public FullView(URLManipulator urlManipulator, AuthManager authManager,
AnalyticsManager analytics, SuggestOracle searchKeywords) {
this.analytics = analytics;
this.presenter = new FullViewPresenter(urlManipulator, this);
this.authManager = authManager;
PlaceholderTextBox searchBackingTextBox =
new PlaceholderTextBox("Search for services, methods, and recent requests...");
this.searchBox = new SuggestBox(searchKeywords, searchBackingTextBox);
searchBox.setAutoSelectEnabled(false);
initWidget(uiBinder.createAndBindUi(this));
setMenuActions();
// Add a fixed css class name that I can use to be able to style the menu.
settingsMenu.setStyleName(SETTINGS_MENU_CSS_RULE + " " + settingsMenu.getStyleName());
// Set the style of the search box.
searchBackingTextBox.setPlaceholderTextStyleName(style.searchPlaceholderText());
}
/**
* Assign the actions to the settings menu items.
*/
private void setMenuActions() {
bugReportItem.setCommand(getOpenUrlAction(REPORT_ERROR_URL));
helpItem.setCommand(getOpenUrlAction(EXPLORER_HELP_URL));
forumItem.setCommand(getOpenUrlAction(EXPLORER_FORUM_URL));
}
/**
* Create a command that can be bound to a menu item that will open a url in a new tab.
*/
private Command getOpenUrlAction(final String url) {
return new Command() {
@Override
public void execute() {
Window.open(url, NEW_TAB_TARGET, "");
}
};
}
@UiHandler("preferredServicesMenuItem")
void clickPreferred(ClickEvent event) {
presenter.clickNavigationItem(NavigationItem.PREFERRED_SERVICES);
}
@UiHandler("requestHistoryMenuItem")
void clickHistory(ClickEvent event) {
presenter.clickNavigationItem(NavigationItem.REQUEST_HISTORY);
}
@UiHandler("allServicesMenuItem")
void clickAllVersions(ClickEvent event) {
presenter.clickNavigationItem(NavigationItem.ALL_VERSIONS);
}
@UiHandler("logo")
void clickLogo(ClickEvent event) {
// Go back to the "home" state of the app when the logo is clicked.
presenter.handleClickLogo();
}
@UiHandler("backButton")
void clickBack(ClickEvent event) {
presenter.handleClickBack();
}
@UiHandler("searchButton")
void clickSearch(ClickEvent event) {
presenter.handleSearch(searchBox.getText());
}
@UiHandler("searchBox")
void searchBoxEnter(KeyDownEvent event) {
if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
SuggestionDisplay suggestionDisplay = searchBox.getSuggestionDisplay();
// This should always be true unless GWT changes the type of the suggestion generated by the
// SuggestBox. It is too complicated and nasty to switch out the SuggestBox suggestion display
// factory, so we're left with this type safety check and broken functionality if GWT changes.
Preconditions.checkState(suggestionDisplay instanceof DefaultSuggestionDisplay);
// At this point this should always be true.
if (suggestionDisplay instanceof DefaultSuggestionDisplay) {
DefaultSuggestionDisplay suggestions = (DefaultSuggestionDisplay) suggestionDisplay;
if (!suggestions.isSuggestionListShowing()) {
presenter.handleSearch(searchBox.getValue());
}
}
}
}
@UiHandler("searchBox")
void suggestionSelected(SelectionEvent<Suggestion> event) {
presenter.handleSearch(event.getSelectedItem().getReplacementString());
}
@Override
public void setContext(ExplorerContext context) {
presenter.setContext(context);
// Fill in the entry list widget, only the collections that have entries will be shown
drillDownNav.setVisible(context.isEntryListVisible());
drillDownNav.clear();
if (context.isEntryListVisible()) {
populateHistoryItems("", context.getHistoryItems(), drillDownNav);
populateServiceEntries(
sortServices(context.getServicesList()), drillDownNav, context.getServiceTagProcessor());
populateServiceMethods(context.getService(), context.getMethods(), drillDownNav);
}
// Fill in the detail pane.
detailPane.setVisible(context.isHistoryItemVisible() || context.isMethodFormVisible());
detailPane.clear();
if (context.isHistoryItemVisible()) {
HistoryItem item = Iterables.getOnlyElement(context.getHistoryItems());
EmbeddedHistoryItemView view = generateHistoryItemView(item);
detailPane.add(view);
} else if (context.isMethodFormVisible()) {
ApiMethod method = context.getMethod();
// Wrap the callback given by the context so that we may also be notified when a request is
// finished. Pass through events to the original callback.
CallbackWrapper cbWrapper = new CallbackWrapper();
cbWrapper.delegate = context.getRequestFinishedCallback();
cbWrapper.methodName = method.getId();
// Create the view of the request editor and the single history item.
EmbeddedView view = new EmbeddedView(authManager,
context.getService(),
method,
context.getMethodParameters(),
cbWrapper,
HIDE_AUTH,
analytics);
cbWrapper.localView = view;
// If this context came bundled with a history item, that means the navigation references a
// previous executed request, and we should show the result.
List<HistoryItem> historyItems = context.getHistoryItems();
if (!historyItems.isEmpty()) {
view.showHistoryItem(generateHistoryItemView(Iterables.getLast(historyItems)));
}
detailPane.add(view);
}
// Show the search results.
searchResults.setVisible(context.isSearchResultsVisible());
searchResults.clear();
searchErrorPanel.setVisible(false);
if (context.isSearchResultsVisible()) {
populateSearchResults(context.getSearchResults(), context.getServiceTagProcessor());
}
// Show the auth panel.
authViewPlaceholder.setVisible(context.isAuthVisible());
authViewPlaceholder.clear();
if (context.isAuthVisible()) {
showAuth(context.getService(), context.getMethod());
}
// Show the documentation link.
docsContainer.setVisible(context.isDocsLinkVisible());
docsContainer.clear();
if (context.isDocsLinkVisible()) {
showDocumentationLink("the " + context.getService().displayTitle(),
context.getService().getDocumentationLink());
}
// Show the title.
boolean showContentTitle = context.getContentTitles() != null;
if (showContentTitle) {
generateBreadcrumbs(detailTitleContainer, context.getContentTitles());
}
// Show the detail header.
detailHeader.setVisible(showContentTitle || context.isAuthVisible());
// Show the back button.
backButton.setVisible(context.getParentUrl() != null);
// Highlight the navigation item which was the root of our navigation.
highlightNavigationItem(context.getRootNavigationItem());
}
/**
* Generate a view of the provided history item.
*/
private EmbeddedHistoryItemView generateHistoryItemView(HistoryItem item) {
EmbeddedHistoryItemView view = new EmbeddedHistoryItemView(item.getRequest());
view.complete(item.getResponse(), item.getEndTime() - item.getStartTime(),
JsonPrettifier.LOCAL_LINK_FACTORY);
return view;
}
/**
* Generate breadcrumbs into the specified container using the format link > link > text where the
* last breadcrumb is always plain text.
*/
private void generateBreadcrumbs(Panel container, List<Title> titles) {
container.clear();
// For all of the titles previous to the last, add a link and a separator.
for (Title notLast : titles.subList(0, titles.size() - 1)) {
container.add(new InlineHyperlink(notLast.getTitle(), notLast.getFragment()));
container.add(new InlineLabel(" > "));
}
// Append only the text for the last title.
Title lastTitle = Iterables.getLast(titles);
container.add(new InlineLabel(lastTitle.getTitle()));
if (lastTitle.getSubtitle() != null) {
Label subtitle = new InlineLabel(" - " + lastTitle.getSubtitle());
subtitle.addStyleName(style.methodSubtitle());
container.add(subtitle);
}
}
private void showAuth(ApiService service, ApiMethod method) {
AuthView auth = new AuthView(authManager, service, analytics);
if (method != null) {
auth.getPresenter().setStateForMethod(method);
}
authViewPlaceholder.add(auth);
}
private void showDocumentationLink(String componentName, String href) {
docsContainer.add(
new InlineLabel("Learn more about using " + componentName + " by reading the "));
docsContainer.add(new Anchor("documentation", href, NEW_TAB_TARGET));
docsContainer.add(new InlineLabel("."));
}
/**
* Display the specified service entries in the container provided, while applying the tags
* generated by the tag processor.
*/
private void populateServiceEntries(Iterable<ServiceDefinition> services,
EntryAggregatorView toPopulate,
Set<TagProcessor> tagProcessors) {
for (final ServiceDefinition service : services) {
String iconUrl = service.getIcons().getIcon16Url();
String displayName = NameHelper.generateDisplayTitle(service.getTitle(), service.getName());
Set<DescriptionTag> tags = Sets.newHashSet();
for (TagProcessor processor : tagProcessors) {
tags.addAll(processor.process(service));
}
HasClickHandlers rowHandle = toPopulate.addEntry(new ServiceEntry(
iconUrl, displayName, service.getVersion(), service.getDescription(), tags));
rowHandle.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
presenter.handleClickService(service);
}
});
}
}
/**
* Display the spcified history items in the aggregator specified.
*
* @param prefix Prefix that should be prepended to the history item URL when an item is clicked,
* changes based on whether this was a search result or the history item list.
* @param historyItems Items which to render and display in the aggregator,
* @param aggregator Aggregator that will display rendered history items.
*/
private void populateHistoryItems(
final String prefix, Iterable<HistoryItem> historyItems, EntryAggregatorView aggregator) {
for (final HistoryItem item : historyItems) {
ApiRequest request = item.getRequest();
HasClickHandlers rowHandler = aggregator.addEntry(new HistoryEntry(request.getMethod()
.getId(), request.getHttpMethod().toString() + " " + request.getRequestPath(), item
.getEndTime()));
rowHandler.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
presenter.handleClickHistoryItem(prefix, item);
}
});
}
}
/**
* Display all of the methods for the specified service in the aggregator provided.
*/
private void populateServiceMethods(
ApiService service, Iterable<ApiMethod> methods, EntryAggregatorView view) {
for (final ApiMethod method : methods) {
populateMethodEntry(method, null, "", view);
}
}
/**
* Add an aggregator line for the particular method specified. When clicked, append the prefix
* specified and then the method identifier to the current URL.
*/
private void populateMethodEntry(final ApiMethod method, @Nullable String serviceTitle,
final String prefix, EntryAggregatorView aggregator) {
HasClickHandlers rowHandler = aggregator.addEntry(
new MethodEntry(method.getId(), serviceTitle, method.getDescription()));
rowHandler.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent arg0) {
presenter.handleClickMethod(prefix, method);
}
});
}
/**
* Take the list of search results and split them into appropriate aggregators hidden under
* disclosure panels.
*/
private void populateSearchResults(
Iterable<SearchResult> results, Set<TagProcessor> serviceTagProcessors) {
List<MethodBundle> methodResults = Lists.newArrayList();
List<ServiceDefinition> serviceResults = Lists.newArrayList();
List<HistoryItem> historyResults = Lists.newArrayList();
for (SearchResult result : results) {
switch(result.getKind()) {
case HISTORY_ITEM:
historyResults.add(result.getHistoryItem());
break;
case METHOD:
methodResults.add(result.getMethodBundle());
break;
case SERVICE:
serviceResults.add(result.getService());
break;
default:
throw new RuntimeException("Unknown search result type: " + result.toString());
}
}
if (!serviceResults.isEmpty()) {
EntryAggregatorView serviceAggregator = new EntryAggregatorView();
populateServiceEntries(serviceResults, serviceAggregator, serviceTagProcessors);
searchResults.addSection("Services", serviceAggregator);
}
if (!methodResults.isEmpty()) {
EntryAggregatorView methodAggregator = new EntryAggregatorView();
for (MethodBundle bundle : methodResults) {
String prefix =
"m/" + bundle.getService().getName() + "/" + bundle.getService().getVersion() + "/";
String serviceTitle =
bundle.getService().displayTitle() + " " + bundle.getService().getVersion();
populateMethodEntry(bundle.getMethod(), serviceTitle, prefix, methodAggregator);
}
searchResults.addSection("Methods", methodAggregator);
}
if (!historyResults.isEmpty()) {
EntryAggregatorView historyAggregator = new EntryAggregatorView();
populateHistoryItems("h/", historyResults, historyAggregator);
searchResults.addSection("History", historyAggregator);
}
if (serviceResults.isEmpty() && methodResults.isEmpty() && historyResults.isEmpty()) {
// There are no results, show the message
searchResults.setVisible(false);
searchErrorPanel.setVisible(true);
}
}
/**
* Highlight the navigation item for the root navigation item specified.
*/
private void highlightNavigationItem(RootNavigationItem navItem) {
preferredServicesMenuItem.removeStyleName(style.selectedNavigation());
requestHistoryMenuItem.removeStyleName(style.selectedNavigation());
allServicesMenuItem.removeStyleName(style.selectedNavigation());
switch(navItem) {
case ALL_VERSIONS:
allServicesMenuItem.addStyleName(style.selectedNavigation());
break;
case PREFERRED_SERVICES:
preferredServicesMenuItem.addStyleName(style.selectedNavigation());
break;
case REQUEST_HISTORY:
requestHistoryMenuItem.addStyleName(style.selectedNavigation());
break;
}
}
@Override
public void hideSearchLoadingIndicator() {
searchLoadingIndicator.setVisible(false);
}
@Override
public void searchReady() {
// Delegate to the presenter
presenter.searchReady();
}
private List<ServiceDefinition> sortServices(Set<ServiceDefinition> services) {
List<ServiceDefinition> serviceList = Lists.newArrayList(services);
Collections.sort(serviceList, new Comparator<ServiceDefinition>() {
@Override
public int compare(ServiceDefinition s1, ServiceDefinition s2) {
String s1Title = NameHelper.generateDisplayTitle(s1.getTitle(), s1.getName());
String s2Title = NameHelper.generateDisplayTitle(s2.getTitle(), s2.getName());
return s1Title.toLowerCase().compareTo(s2Title.toLowerCase());
}
});
return Collections.unmodifiableList(serviceList);
}
/**
* Wrapper class that is used to siphon off request complete events, while still passing the
* original events through to the wrapped delegate class.
*/
private static class CallbackWrapper implements RequestFinishedCallback {
public RequestFinishedCallback delegate;
public EmbeddedView localView;
public String methodName;
private Map<ApiRequest, EmbeddedHistoryItemView> incompleteRequests = Maps.newHashMap();
@Override
public void finished(ApiRequest request, ApiResponse response, long startTime, long endTime) {
EmbeddedHistoryItemView toComplete = incompleteRequests.get(request);
toComplete.complete(response, endTime - startTime, JsonPrettifier.LOCAL_LINK_FACTORY);
incompleteRequests.remove(request);
delegate.finished(request, response, startTime, endTime);
}
@Override
public void starting(ApiRequest request) {
EmbeddedHistoryItemView incomplete = new EmbeddedHistoryItemView(request);
incompleteRequests.put(request, incomplete);
localView.showHistoryItem(incomplete);
delegate.starting(request);
}
}
}