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 2013 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.gwt.event.shared.GwtEvent;
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;
import com.google.web.bindery.event.shared.EventBus;
import com.google.web.bindery.event.shared.HandlerRegistration;
import com.gwtplatform.mvp.shared.proxy.PlaceRequest;
import com.gwtplatform.mvp.shared.proxy.TokenFormatException;
import com.gwtplatform.mvp.shared.proxy.TokenFormatter;

/**
 * Default implementation to be extended of the {@link PlaceManager}.
 *
 * @see DefaultPlaceManager
 */
public abstract class PlaceManagerImpl implements PlaceManager, ValueChangeHandler, ClosingHandler {
    private final EventBus eventBus;
    private final TokenFormatter tokenFormatter;

    private String currentHistoryToken = "";
    private boolean internalError;
    private String onLeaveQuestion;
    private Command defferedNavigation;

    private HandlerRegistration windowClosingHandlerRegistration;
    private boolean locked;

    private List placeHierarchy = new ArrayList();

    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.Builder().build();
        }
    }

    @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