All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gwtplatform.mvp.client.proxy.PlaceManagerImpl Maven / Gradle / Ivy

There is a newer version: 1.6
Show newest version
/**
 * Copyright 2011 ArcBees 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.gwtplatform.mvp.client.proxy;

import java.util.ArrayList;
import java.util.List;

import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.web.bindery.event.shared.EventBus;
import com.google.gwt.event.shared.GwtEvent;
import com.google.web.bindery.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.Window.ClosingEvent;
import com.google.gwt.user.client.Window.ClosingHandler;

/**
 * This is the default implementation of the {@link PlaceManager}.
 *
 * @author Philippe Beaudoin
 * @author Christian Goudreau
 */
public abstract class PlaceManagerImpl implements PlaceManager,
    ValueChangeHandler, ClosingHandler {

  private final EventBus eventBus;
  private String currentHistoryToken = "";

  private boolean internalError;
  private String onLeaveQuestion;
  private List placeHierarchy = new ArrayList();

  private final TokenFormatter tokenFormatter;

  private HandlerRegistration windowClosingHandlerRegistration;
  private boolean locked;
  private Command defferedNavigation;

  public PlaceManagerImpl(EventBus eventBus, TokenFormatter tokenFormatter) {
    this.eventBus = eventBus;
    this.tokenFormatter = tokenFormatter;
    registerTowardsHistory();
  }

  @Override
  public String buildHistoryToken(PlaceRequest request) {
    return tokenFormatter.toPlaceToken(request);
  }

  @Override
  public String buildRelativeHistoryToken(int level) {
    List placeHierarchyCopy = truncatePlaceHierarchy(level);
    if (placeHierarchyCopy.size() == 0) {
      return "";
    }
    return tokenFormatter.toHistoryToken(placeHierarchyCopy);
  }

  @Override
  public String buildRelativeHistoryToken(PlaceRequest request) {
    return buildRelativeHistoryToken(request, 0);
  }

  @Override
  public String buildRelativeHistoryToken(PlaceRequest request, int level) {
    List placeHierarchyCopy = truncatePlaceHierarchy(level);
    placeHierarchyCopy.add(request);
    return tokenFormatter.toHistoryToken(placeHierarchyCopy);
  }

  /**
   * If a confirmation question is set (see
   * {@link #setOnLeaveConfirmation(String)}), this asks the user if he wants to
   * leave the current page.
   *
   * @return true if the user accepts to leave. false if he refuses.
   */
  private boolean confirmLeaveState() {
    if (onLeaveQuestion == null) {
      return true;
    }
    boolean confirmed = Window.confirm(onLeaveQuestion);
    if (confirmed) {
      // User has confirmed, don't ask any more question.
      setOnLeaveConfirmation(null);
    } else {
      NavigationRefusedEvent.fire(this);
      setBrowserHistoryToken(currentHistoryToken, false);
    }
    return confirmed;
  }

  /**
   * Fires the {@link PlaceRequestInternalEvent} for the given
   * {@link PlaceRequest}. Do not call this method directly,
   * instead call {@link #revealPlace(PlaceRequest)} or a related method.
   *
   * @param request The {@link PlaceRequest} to fire.
   * @param updateBrowserUrl {@code true} If the browser URL should be updated, {@code false}
   *          otherwise.
   */
  protected void doRevealPlace(PlaceRequest request, boolean updateBrowserUrl) {
    PlaceRequestInternalEvent requestEvent = new PlaceRequestInternalEvent(request,
        updateBrowserUrl);
    fireEvent(requestEvent);
    if (!requestEvent.isHandled()) {
      unlock();
      error(tokenFormatter.toHistoryToken(placeHierarchy));
    } else if (!requestEvent.isAuthorized()) {
      unlock();
      illegalAccess(tokenFormatter.toHistoryToken(placeHierarchy));
    }
  }

  /**
   * Called whenever an error occurred that requires the error page to be shown
   * to the user. This method will detect infinite reveal loops and throw an
   * {@link RuntimeException} in that case.
   *
   * @param invalidHistoryToken The history token that was not recognised.
   */
  private void error(String invalidHistoryToken) {
    startError();
    revealErrorPlace(invalidHistoryToken);
    stopError();
  }

  @Override
  public void fireEvent(GwtEvent event) {
    getEventBus().fireEventFromSource(event, this);
  }

  String getBrowserHistoryToken() {
    return History.getToken();
  }

  @Override
  public List getCurrentPlaceHierarchy() {
    return placeHierarchy;
  }

  @Override
  public PlaceRequest getCurrentPlaceRequest() {
    if (placeHierarchy.size() > 0) {
      return placeHierarchy.get(placeHierarchy.size() - 1);
    } else {
      return new PlaceRequest();
    }
  }

  @Override
  public void getCurrentTitle(SetPlaceTitleHandler handler) {
    getTitle(placeHierarchy.size() - 1, handler);
  }

  @Override
  public EventBus getEventBus() {
    return eventBus;
  }

  @Override
  public int getHierarchyDepth() {
    return placeHierarchy.size();
  }

  /**
   * Checks that the place manager is not locked and that the user allows the
   * application to navigate (see {@link #confirmLeaveState()}. If the
   * application is allowed to navigate, this method locks navigation.
   *
   * @return true if the place manager can get the lock false otherwise.
   */
  private boolean getLock() {
    if (locked) {
      return false;
    }
    if (!confirmLeaveState()) {
      return false;
    }
    lock();
    return true;
  }

  @Override
  public void getTitle(int index, SetPlaceTitleHandler handler)
      throws IndexOutOfBoundsException {
    GetPlaceTitleEvent event = new GetPlaceTitleEvent(
        placeHierarchy.get(index), handler);
    fireEvent(event);
    // If nobody took care of the title, indicate it's null
    if (!event.isHandled()) {
      handler.onSetPlaceTitle(null);
    }
  }

  @Override
  public boolean hasPendingNavigation() {
    return defferedNavigation != null;
  }

  /**
   * Called whenever the user tries to access an page to which he doesn't have
   * access, and we need to reveal the user-defined unauthorized place. This
   * method will detect infinite reveal loops and throw an
   * {@link RuntimeException} in that case.
   *
   * @param historyToken The history token that was not recognised.
   */
  private void illegalAccess(String historyToken) {
    startError();
    revealUnauthorizedPlace(historyToken);
    stopError();
  }

  private void lock() {
    if (!locked) {
      locked = true;
      LockInteractionEvent.fire(this, true);
    }
  }

  @Override
  public void navigateBack() {
    History.back();
  }

  /**
   * Handles change events from {@link History}.
   */
  @Override
  public void onValueChange(final ValueChangeEvent event) {
    if (locked) {
      defferedNavigation = new Command() {
        @Override
        public void execute() {
          onValueChange(event);
        }
      };
      return;
    }
    if (!getLock()) {
      return;
    }
    String historyToken = event.getValue();
    try {
      if (historyToken.trim().equals("")) {
        unlock();
        revealDefaultPlace();
      } else {
        placeHierarchy = tokenFormatter.toPlaceRequestHierarchy(historyToken);
        doRevealPlace(getCurrentPlaceRequest(), true);
      }
    } catch (TokenFormatException e) {
      unlock();
      error(historyToken);
      NavigationEvent.fire(this, null);
    }
  }

  @Override
  public void onWindowClosing(ClosingEvent event) {
    // The current implementation has a few bugs described below. However these are browser
    // bugs, and the workarounds we've experimented with gave worst results than the bug itself.
    //
    // Here are the current behaviours of different browsers after cancelling navigation
    // * Chrome
    //    - URL bar shows new website (FAIL)
    //    - Bookmarking uses the title of the webapp, but url of new website (FAIL)
    //    - Navigating away and then back goes back to the correct webapp page (WORKS)
    // * Firefox
    //    - URL bar shows new website (FAIL)
    //    - Bookmarking uses the title of the webapp, and url of webapp (WORKS)
    //    - Navigating away and then back goes back to the correct webapp page (WORKS)
    // * IE
    //    - Untested
    //
    // Options are to report that upstream in the browsers or to go back to our workarounds in a
    // browser-dependent fashion using deferred binding. The workarounds we've experimented with
    // consisted of adding a deferred command that used Window.Location.replace to reset the URL
    // to the current page. However, this caused infinite loops in some browsers.
    //
    // See this issue:
    //   http://code.google.com/p/gwt-platform/issues/detail?id=315

    event.setMessage(onLeaveQuestion);
  }

  void registerTowardsHistory() {
    History.addValueChangeHandler(this);
  }

  @Override
  public void revealCurrentPlace() {
    History.fireCurrentHistoryState();
  }

  @Override
  public void revealErrorPlace(String invalidHistoryToken) {
    revealDefaultPlace();
  }

  @Override
  public void revealPlace(final PlaceRequest request) {
    revealPlace(request, true);
  }

  @Override
  public void revealPlace(final PlaceRequest request, final boolean updateBrowserUrl) {
    if (locked) {
      defferedNavigation = new Command() {
        @Override
        public void execute() {
          revealPlace(request, updateBrowserUrl);
        }
      };
      return;
    }
    if (!getLock()) {
      return;
    }
    placeHierarchy.clear();
    placeHierarchy.add(request);
    doRevealPlace(request, updateBrowserUrl);
  }

  @Override
  public void revealPlaceHierarchy(
      final List placeRequestHierarchy) {
    if (locked) {
      defferedNavigation = new Command() {
        @Override
        public void execute() {
          revealPlaceHierarchy(placeRequestHierarchy);
        }
      };
      return;
    }
    if (!getLock()) {
      return;
    }
    if (placeRequestHierarchy.size() == 0) {
      unlock();
      revealDefaultPlace();
    } else {
      placeHierarchy = placeRequestHierarchy;
      doRevealPlace(getCurrentPlaceRequest(), true);
    }
  }

  @Override
  public void revealRelativePlace(final int level) {
    if (locked) {
      defferedNavigation = new Command() {
        @Override
        public void execute() {
          revealRelativePlace(level);
        }
      };
      return;
    }
    if (!getLock()) {
      return;
    }
    placeHierarchy = truncatePlaceHierarchy(level);
    int hierarchySize = placeHierarchy.size();
    if (hierarchySize == 0) {
      unlock();
      revealDefaultPlace();
    } else {
      PlaceRequest request = placeHierarchy.get(hierarchySize - 1);
      doRevealPlace(request, true);
    }
  }

  @Override
  public void revealRelativePlace(PlaceRequest request) {
    revealRelativePlace(request, 0);
  }

  @Override
  public void revealRelativePlace(final PlaceRequest request, final int level) {
    if (locked) {
      defferedNavigation = new Command() {
        @Override
        public void execute() {
          revealRelativePlace(request, level);
        }
      };
      return;
    }
    if (!getLock()) {
      return;
    }
    placeHierarchy = truncatePlaceHierarchy(level);
    placeHierarchy.add(request);
    doRevealPlace(request, true);
  }

  @Override
  public void revealUnauthorizedPlace(String unauthorizedHistoryToken) {
    revealErrorPlace(unauthorizedHistoryToken);
  }

  /**
   * This method saves the history token, making it possible to correctly restore the browser's
   * URL if the user refuses to navigate. (See {@link #onWindowClosing(ClosingEvent)})
   *
   * @param historyToken The current history token, a string.
   */
  private void saveHistoryToken(String historyToken) {
    currentHistoryToken = historyToken;
  }

  void setBrowserHistoryToken(String historyToken, boolean issueEvent) {
    History.newItem(historyToken, issueEvent);
  }

  @Override
  public void setOnLeaveConfirmation(String question) {
    if (question == null && onLeaveQuestion == null) {
      return;
    }
    if (question != null && onLeaveQuestion == null) {
      windowClosingHandlerRegistration = Window.addWindowClosingHandler(this);
    }
    if (question == null && onLeaveQuestion != null) {
      windowClosingHandlerRegistration.removeHandler();
    }
    onLeaveQuestion = question;
  }

  /**
   * Start revealing an error or unauthorized page. This method will throw an
   * exception if an infinite loop is detected.
   *
   * @see #stopError()
   */
  private void startError() {
    if (this.internalError) {
      throw new RuntimeException(
          "Encountered repeated errors resulting in an infinite loop. Make sure all users have access "
              + "to the pages revealed by revealErrorPlace and revealUnauthorizedPlace. (Note that the default "
              + "implementations call revealDefaultPlace)");
    }
    internalError = true;
  }

  /**
   * Indicates that an error page has successfully been revealed. Makes it
   * possible to detect infinite loops.
   *
   * @see #startError()
   */
  private void stopError() {
    internalError = false;
  }

  @Override
  public void unlock() {
    if (locked) {
      locked = false;
      LockInteractionEvent.fire(this, false);
      if (hasPendingNavigation()) {
        Command navigation = defferedNavigation;
        defferedNavigation = null;
        navigation.execute();
      }
    }
  }

  @Override
  public void updateHistory(PlaceRequest request, boolean updateBrowserUrl) {
    try {
      // Make sure the request match
      assert request.hasSameNameToken(getCurrentPlaceRequest()) : "Internal error, PlaceRequest passed to updateHistory doesn't match the tail of the place hierarchy.";
      placeHierarchy.set(placeHierarchy.size() - 1, request);
      if (updateBrowserUrl) {
        String historyToken = tokenFormatter.toHistoryToken(placeHierarchy);
        String browserHistoryToken = getBrowserHistoryToken();
        if (browserHistoryToken == null
            || !browserHistoryToken.equals(historyToken)) {
          setBrowserHistoryToken(historyToken, false);
        }
        saveHistoryToken(historyToken);
      }
    } catch (TokenFormatException e) {
      // Do nothing.
    }
  }

  /**
   * Returns a modified copy of the place hierarchy based on the specified
   * {@code level}.
   *
   * @param level If negative, take back that many elements from the tail of the
   *          hierarchy. If positive, keep only that many elements from the head
   *          of the hierarchy. Passing {@code 0} leaves the hierarchy
   *          untouched.
   */
  private List truncatePlaceHierarchy(int level) {
    int size = placeHierarchy.size();
    if (level < 0) {
      if (-level >= size) {
        return new ArrayList();
      } else {
        return new ArrayList(placeHierarchy.subList(0, size
            + level));
      }
    } else if (level > 0) {
      if (level >= size) {
        return new ArrayList(placeHierarchy);
      } else {
        return new ArrayList(placeHierarchy.subList(0, level));
      }
    }
    return new ArrayList(placeHierarchy);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy