/* * Copyright 2010 Gal Dolber. * * 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.guit.client.place; import com.google.gwt.core.client.GWT; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.EventBus; import com.google.gwt.inject.client.AsyncProvider; import com.google.gwt.json.client.JSONParser; import com.google.gwt.logging.client.LogConfiguration; 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.inject.Inject; import com.google.inject.Provider; import com.guit.client.async.AbstractAsyncCallback; import com.guit.client.jsorm.TypeJsonSerializer; import com.guit.client.place.event.PlaceChangeEvent; import java.util.HashMap; import java.util.logging.Logger; public class PlaceManagerImpl implements PlaceManager, ValueChangeHandler<String>, ClosingHandler { private static interface PlaceCallback<D> { void onSuccess(Place<D> place); } private static final String PLACENAMESEPARATOR = "/"; private final String defaultTitle; protected Place<?> currentPlace; protected String currentPlaceName; protected Object currentData; protected String defaultPlace; private final Crypter crypter; protected final HashMap<Class<?>, String> placesNames = new HashMap<Class<?>, String>(); protected final HashMap<String, String> placesTitles = new HashMap<String, String>(); protected final HashMap<String, Place<?>> places = new HashMap<String, Place<?>>(); protected final HashMap<String, Provider<? extends Place<?>>> providedPlaces = new HashMap<String, Provider<? extends Place<?>>>(); protected final HashMap<String, AsyncProvider<? extends Place<?>>> asyncProvidedPlaces = new HashMap<String, AsyncProvider<? extends Place<?>>>(); protected final HashMap<String, TypeJsonSerializer<?>> serializers = new HashMap<String, TypeJsonSerializer<?>>(); protected final HashMap<String, Boolean> serializersEncrypted = new HashMap<String, Boolean>(); private final Logger logger = Logger.getLogger("Place Manager"); private final EventBus eventBus; private int placeChangeCount = 0; @Inject public PlaceManagerImpl(final PlaceManagerInitializer initializer, Crypter crypter, EventBus eventBus) { this.eventBus = eventBus; this.crypter = crypter; this.defaultTitle = Window.getTitle(); initializer.initialize(this); } public <P extends Place<D>, D> void addPlace(Class<P> clazz, String token, String title, AsyncProvider<P> placeProvider, TypeJsonSerializer<D> dataSerializer, boolean encrypted) { placesNames.put(clazz, token); serializersEncrypted.put(token, encrypted); serializers.put(token, dataSerializer); asyncProvidedPlaces.put(token, placeProvider); placesTitles.put(token, title); } public <P extends Place<D>, D> void addPlace(Class<P> clazz, String token, String title, Provider<P> placeProvider, TypeJsonSerializer<D> dataSerializer, boolean encrypted) { placesNames.put(clazz, token); serializersEncrypted.put(token, encrypted); serializers.put(token, dataSerializer); providedPlaces.put(token, placeProvider); placesTitles.put(token, title); } @Override public String getCurrentToken() { return History.getToken(); } @SuppressWarnings("unchecked") private <D> void getPlace(final String placeName, final PlaceCallback<D> callback) { if (places.containsKey(placeName)) { callback.onSuccess((Place<D>) places.get(placeName)); } else if (providedPlaces.containsKey(placeName)) { Provider<? extends Place<?>> placeProvider = providedPlaces.get(placeName); Place<?> place = placeProvider.get(); places.put(placeName, place); callback.onSuccess((Place<D>) place); } else if (asyncProvidedPlaces.containsKey(placeName)) { AsyncProvider<Place<D>> asyncProvider = (AsyncProvider<Place<D>>) asyncProvidedPlaces.get(placeName); asyncProvider.get(new AbstractAsyncCallback<Place<D>>() { @Override public void success(Place<D> place) { places.put(placeName, place); callback.onSuccess(place); } }); } else { // The exception is only for development mode assert false : "Error on history manager. The place " + placeName + " is not registered. It should be binded as Singleton."; // In production we just go to the default place if (defaultPlace != null) { getPlace(defaultPlace, callback); } } } @Override public <D> String getToken(Class<? extends Place<D>> placeClass) { return getToken(placeClass, null); } @Override public <D> String getToken(Class<? extends Place<D>> placeClass, D placeData) { String placeName = placesNames.get(placeClass); assert placeName != null : "Error on history manager. The place " + placeClass.getName() + " is not registered. It should be binded as Singleton."; return getToken(placeName, placeData); } @SuppressWarnings("unchecked") protected <D> String getToken(String placeName, D placeData) { TypeJsonSerializer<D> s = (TypeJsonSerializer<D>) serializers.get(placeName); boolean encrypted = serializersEncrypted.get(placeName); if (s != null) { if (placeData != null) { String json = s.serialize(placeData).toString(); return "!" + placeName + PLACENAMESEPARATOR + (encrypted ? crypter.encode(json) : json); } } else { assert false : "Error on history manager. The place " + placeName + " is not registered. It should be binded as Singleton."; } return "!" + placeName; } @Override public <D> void go(Class<? extends Place<D>> placeClass) { go(placeClass, null); } @Override public <D> void go(Class<? extends Place<D>> placeClass, final D placeData) { if (LogConfiguration.loggingIsEnabled()) { logger.info("PlaceGo=[" + getToken(placeClass, placeData) + "]"); } History.newItem(getToken(placeClass, placeData), false); goToPlace(placesNames.get(placeClass), placeData); } @Override public void go(String token) { History.newItem(token, false); onTokenChange(token); } @Override public void goBack() { if (placeChangeCount == 1 && defaultPlace != null) { History.newItem(getToken(defaultPlace, null)); goToDefaultPlace(); if (LogConfiguration.loggingIsEnabled()) { logger.info("goBack to default place"); } } else { History.back(); if (LogConfiguration.loggingIsEnabled()) { logger.info("goBack"); } } } @Override public void goForward() { History.forward(); } @Override public void goDefault() { if (defaultPlace != null) { History.newItem(getToken(defaultPlace, null), false); goToDefaultPlace(); } } private void goToDefaultPlace() { if (defaultPlace != null) { goToPlace(defaultPlace, null); } } private <D> void goToPlace(final String placeName, final D placeData) { if (currentPlace != null && currentPlace instanceof StayPlace) { StayPlace<?> stayPlace = (StayPlace<?>) currentPlace; String warning = stayPlace.mayLeave(); if (warning != null) { if (!Window.confirm(warning)) { // Restore the token History.newItem(getToken(currentPlaceName, currentData), false); if (currentPlace instanceof StayPlaceWithCallback) { ((StayPlaceWithCallback<?>) stayPlace).stay(); } return; } else if (currentPlace instanceof StayPlaceWithCallback) { ((StayPlaceWithCallback<?>) stayPlace).leave(); } } } else if (currentPlace != null && currentPlace instanceof LeavePlace) { ((LeavePlace<?>) currentPlace).leave(); } placeChangeCount++; getPlace(placeName, new PlaceCallback<D>() { @Override public void onSuccess(Place<D> place) { eventBus.fireEvent(new PlaceChangeEvent(place.getClass(), placeData)); String title = placesTitles.get(placeName); if (!title.isEmpty()) { Window.setTitle(defaultTitle + " - " + title); } else { Window.setTitle(defaultTitle); } currentPlace = place; currentPlaceName = placeName; currentData = placeData; place.go(placeData); } }); } @Override public <D> void newItem(Class<? extends Place<D>> placeClass) { newItem(placeClass, null); } @Override public <D> void newItem(Class<? extends Place<D>> placeClass, D placeData) { assert placesNames.containsKey(placeClass) : "The place " + placeClass.getName() + " is not registered. It should be binded as Singleton."; assert placesNames.get(placeClass).equals(currentPlaceName) : "You only can call newItem() for the current place. Otherwise call go()."; if (LogConfiguration.loggingIsEnabled()) { logger.info("Place NewItem=[" + getToken(placeClass, placeData)); } History.newItem(getToken(placeClass, placeData), false); eventBus.fireEvent(new PlaceChangeEvent(placeClass, placeData)); } @Override public void newItem(String token) { // TODO Fire change event History.newItem(token, false); } /** * Simulate browser token change event (public for testing). */ public void onTokenChange(String token) { if (LogConfiguration.loggingIsEnabled()) { logger.info("Place TokenChange=[" + token + "]"); } if (token.isEmpty()) { goToDefaultPlace(); return; } if (!token.startsWith("!")) { assert false : "Error on place manager. The token doesn't start with '!'. Found: " + token; goToDefaultPlace(); return; } int nameSeparator = token.indexOf(PLACENAMESEPARATOR); if (nameSeparator != -1) { String placeName = token.substring(1, nameSeparator); String json = token.substring(nameSeparator + 1); Object data = null; if (!json.isEmpty()) { TypeJsonSerializer<?> typeJsonSerializer = serializers.get(placeName); boolean encrypted = serializersEncrypted.get(placeName); if (typeJsonSerializer == null) { // The exception is only for development mode assert false : "Error on history manager. The place " + placeName + " is not registered. It must be binded as Singleton."; goToDefaultPlace(); } try { data = typeJsonSerializer.deserialize(JSONParser.parseStrict((encrypted ? crypter .decode(json) : json))); } catch (Exception e) { if (GWT.isScript()) { goToDefaultPlace(); return; } else { throw new IllegalStateException("Malformed place data", e); } } } goToPlace(placeName, data); } else { goToPlace(token.substring(1), null); } } /** * Token changed handler. */ @Override public void onValueChange(ValueChangeEvent<String> event) { onTokenChange(event.getValue()); } @Override public void onWindowClosing(ClosingEvent event) { if (currentPlace != null && currentPlace instanceof StayPlace) { String warning = ((StayPlace<?>) currentPlace).mayLeave(); if (warning != null) { event.setMessage(warning); } } } /** * Set default place. */ protected void setDefaultPlace(String defaultPlace) { assert providedPlaces.containsKey(defaultPlace) || asyncProvidedPlaces.containsKey(defaultPlace) : "The default place must implement " + Place.class.getName() + ". Found: " + defaultPlace; this.defaultPlace = defaultPlace; } @Override public String toString() { return placesNames.toString(); } @Override public <D> void newItem(Class<? extends Place<D>> placeClass, D placeData, D defaultPlaceData) { String token = getToken(placeClass, placeData); String currentToken = getCurrentToken(); // If there is any change if (!currentToken.equals(token)) { if (placeData == null) { // If default place if (defaultPlace != null && placesNames.get(placeClass).equals(defaultPlace)) { if (!currentToken.isEmpty() && !currentToken.equals(getToken(placeClass))) { History.newItem(""); } } else if (!currentToken.equals(getToken(placeClass))) { newItem(placeClass); } } else { // If the place have his default data if (placeData.equals(defaultPlaceData)) { // If the place is the default place if (defaultPlace != null && placesNames.get(placeClass).equals(defaultPlace)) { // And we are in the default place we use empty place data if (!currentToken.isEmpty() && !currentToken.equals(getToken(placeClass))) { History.newItem(""); } } else { newItem(placeClass, null); } } else { newItem(placeClass, placeData); } } } } }