/* * 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.auth; import com.google.api.explorer.client.AuthManager; import com.google.api.explorer.client.analytics.AnalyticsManager; import com.google.api.explorer.client.base.ApiService; import com.google.api.explorer.client.base.ApiService.AuthScope; import com.google.common.base.Function; import com.google.common.base.Joiner; 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.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.event.dom.client.MouseOutEvent; import com.google.gwt.event.dom.client.MouseOverEvent; import com.google.gwt.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.resources.client.CssResource; import com.google.gwt.safehtml.shared.SafeHtmlBuilder; 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.ui.Button; import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.FocusWidget; import com.google.gwt.user.client.ui.Image; 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.TextBox; import com.google.gwt.user.client.ui.ToggleButton; import com.google.gwt.user.client.ui.Widget; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; /** * View for Authentication status and authentication link. * * @author jasonhall@google.com (Jason Hall) */ public class AuthView extends Composite implements AuthPresenter.Display { private static AuthUiBinder uiBinder = GWT.create(AuthUiBinder.class); interface AuthUiBinder extends UiBinder<Widget, AuthView> { } interface AuthViewStyle extends CssResource { String clickable(); String discoveryScopeSelector(); } @UiField AuthViewStyle style; @UiField Panel scopeSelector; @UiField ToggleButton authToggle; @UiField Image authInfoIcon; @UiField Image authWarningIcon; @UiField Image authErrorIcon; @UiField Panel discloseScopeInfo; @UiField PopupPanel scopeInfoPopup; @UiField Label authMessage; @UiField Label scopeList; @UiField PopupPanel scopePopup; @UiField Panel scopePanel; @UiField Panel additionalScopePanel; @UiField Label hasScopesText; @UiField Label noScopesText; @UiField Label optionalAdditionalScopes; @UiField Label serviceName; @UiField Button authorizeButton; @UiField Button cancelAuthButton; private final AuthPresenter presenter; private Map<String, AuthScope> scopesFromDiscovery = Maps.newHashMap(); private Set<String> selectedScopes = Sets.newLinkedHashSet(); private List<TextBox> freeFormEditors = Lists.newLinkedList(); /** * Variable to track when it was a click that was used to disclose the scope info rather than a * hover. This will prevent the widget from closing when the user moves the mouse away. */ private boolean clickedToDiscloseScopeInfo = false; public AuthView(AuthManager authManager, ApiService service, AnalyticsManager analytics) { initWidget(uiBinder.createAndBindUi(this)); this.presenter = new AuthPresenter(service, authManager, analytics, this); serviceName.setText(service.displayTitle()); // Unless you show then hide popup windows they do not initialize properly. scopePopup.show(); scopePopup.hide(); scopeInfoPopup.show(); scopeInfoPopup.hide(); scopeInfoPopup.addCloseHandler(new CloseHandler<PopupPanel>() { @Override public void onClose(CloseEvent<PopupPanel> event) { GWT.log("Handler for closing popup."); clickedToDiscloseScopeInfo = false; } }); } public AuthPresenter getPresenter() { return presenter; } @UiHandler("authToggle") void authToggled(ValueChangeEvent<Boolean> event) { if (event.getValue()) { presenter.clickEnableAuth(); } else { presenter.clickDisableAuth(); } } @UiHandler("authorizeButton") void authorize(ClickEvent event) { presenter.clickExecuteAuth(); } @UiHandler("cancelAuthButton") void cancelAuth(ClickEvent event) { presenter.clickCancelAuth(); } @UiHandler("discloseScopeInfo") void discloseScopeInfo(ClickEvent event) { clickedToDiscloseScopeInfo = true; showScopeInfoPopup(); } @UiHandler("discloseScopeInfo") void scopeInfoHover(MouseOverEvent event) { showScopeInfoPopup(); } @UiHandler("discloseScopeInfo") void scopeInfoMouseOut(MouseOutEvent event) { if (!clickedToDiscloseScopeInfo) { scopeInfoPopup.hide(); } } private void showScopeInfoPopup() { scopeInfoPopup.setPopupPositionAndShow(new PositionCallback() { @Override public void setPosition(int offsetWidth, int offsetHeight) { int left = discloseScopeInfo.getAbsoluteLeft() - offsetWidth + discloseScopeInfo.getOffsetWidth(); int top = discloseScopeInfo.getAbsoluteTop() + discloseScopeInfo.getOffsetHeight(); scopeInfoPopup.setPopupPosition(left, top); } }); } @Override public void setState(State state, Set<String> requiredScopes, Set<String> heldScopes) { scopeSelector.setVisible(state != State.ONLY_PUBLIC); authToggle.setValue(state == State.PRIVATE); Set<String> missingScopes = Sets.difference(requiredScopes, heldScopes); Set<String> scopesToShow; String message; AuthIconState iconState; if (missingScopes.isEmpty()) { iconState = AuthIconState.INFO; scopesToShow = heldScopes; if (heldScopes.isEmpty()) { message = "No auth required."; } else { message = "Auth scopes held: "; } } else if (missingScopes.equals(requiredScopes)) { iconState = AuthIconState.ERROR; scopesToShow = Collections.emptySet(); message = "Method requires authorized requests."; } else { iconState = AuthIconState.WARNING; scopesToShow = missingScopes; message = "Method may require additional auth scopes: "; } authInfoIcon.setVisible(iconState == AuthIconState.INFO); authWarningIcon.setVisible(iconState == AuthIconState.WARNING); authErrorIcon.setVisible(iconState == AuthIconState.ERROR); authMessage.setText(message); Iterable<String> shortNames = Iterables.transform(scopesToShow, new Function<String, String>() { @Override public String apply(String scopeUrl) { return AuthPresenter.scopeName(scopeUrl); } }); String delimitedNames = Joiner.on(", ").join(shortNames); scopeList.setText(delimitedNames); } @Override public void setScopes(Map<String, AuthScope> scopes) { selectedScopes.clear(); scopesFromDiscovery = scopes; } /** * Rebuild the popup from scratch with all of the scopes that we have from the last time we were * presented. */ private void buildScopePopup() { scopePanel.clear(); additionalScopePanel.clear(); Set<TextBox> oldEditors = Sets.newLinkedHashSet(freeFormEditors); freeFormEditors.clear(); // Hide the service scopes label if there aren't any. hasScopesText.setVisible(!scopesFromDiscovery.isEmpty()); noScopesText.setVisible(scopesFromDiscovery.isEmpty()); // Show different text on the free-form scopes section when there are discovery scopes. optionalAdditionalScopes.setVisible(!scopesFromDiscovery.isEmpty()); for (final Map.Entry<String, AuthScope> scope : scopesFromDiscovery.entrySet()) { // Add the check box to the table. CheckBox scopeToggle = new CheckBox(); SafeHtmlBuilder safeHtml = new SafeHtmlBuilder(); safeHtml.appendEscaped(scope.getKey()).appendHtmlConstant("<br><span>") .appendEscaped(scope.getValue().getDescription()).appendHtmlConstant("</span>"); scopeToggle.setHTML(safeHtml.toSafeHtml()); scopeToggle.addStyleName(style.discoveryScopeSelector()); scopePanel.add(scopeToggle); // When the box is checked, add our scope to the selected list. scopeToggle.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { CheckBox checkBox = (CheckBox) event.getSource(); if (checkBox.getValue()) { selectedScopes.add(scope.getKey()); } else { selectedScopes.remove(scope.getKey()); } } }); // Enable the check box if the scope is selected. scopeToggle.setValue(selectedScopes.contains(scope.getKey())); } // Process any scopes that are extra. for (TextBox editor : oldEditors) { if (!editor.getValue().trim().isEmpty()) { addFreeFormEditorRow(editor.getValue(), true); } } // There should always be one empty editor. addFreeFormEditorRow("", false); } /** * Add an editor row in the form of a textbox, that will allow an arbitrary scope to be added. */ private FocusWidget addFreeFormEditorRow(String name, boolean showRemoveLink) { final FlowPanel newRow = new FlowPanel(); // Create the new editor and do the appropriate bookkeeping. final TextBox scopeText = new TextBox(); scopeText.setValue(name); newRow.add(scopeText); freeFormEditors.add(scopeText); final Label removeLink = new InlineLabel("X"); removeLink.addStyleName(style.clickable()); removeLink.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { freeFormEditors.remove(scopeText); additionalScopePanel.remove(newRow); if (freeFormEditors.isEmpty()) { addFreeFormEditorRow("", false); } } }); newRow.add(removeLink); removeLink.setVisible(showRemoveLink); // Add a handler to add a new editor when there is text in the existing editor. scopeText.addKeyDownHandler(new KeyDownHandler() { @Override public void onKeyDown(KeyDownEvent event) { TextBox editor = (TextBox) event.getSource(); boolean isLastEditor = editor.equals(Iterables.getLast(freeFormEditors)); if (isLastEditor && !editor.getValue().isEmpty()) { presenter.addNewScope(); removeLink.setVisible(true); } } }); additionalScopePanel.add(newRow); return scopeText; } @Override public Set<String> getSelectedScopes() { // Concatenate the structured scopes with the free-form scopes. Set<String> allScopes = Sets.newHashSet(selectedScopes); for (TextBox editor : freeFormEditors) { if (!editor.getValue().trim().isEmpty()) { allScopes.add(editor.getValue()); } } return allScopes; } @Override public void showScopeDialog() { buildScopePopup(); scopePopup.show(); scopePopup.center(); } @Override public void addScopeEditor() { addFreeFormEditorRow("", false); } @Override public void hideScopeDialog() { scopePopup.hide(); } @Override public void preSelectScopes(Set<String> scopes) { selectedScopes.removeAll(scopesFromDiscovery.keySet()); selectedScopes.addAll(scopes); } /** Possible states of the auth icon indicator. */ private enum AuthIconState { WARNING, ERROR, INFO, } }