/*
* Copyright (c) 2010-2012 Lockheed Martin Corporation
*
* 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 org.eurekastreams.web.client.history;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.eurekastreams.server.domain.Page;
import org.eurekastreams.web.client.events.EventBus;
import org.eurekastreams.web.client.events.HistoryViewsChangedEvent;
import org.eurekastreams.web.client.events.Observer;
import org.eurekastreams.web.client.events.PreSwitchedHistoryViewEvent;
import org.eurekastreams.web.client.events.PreventHistoryChangeEvent;
import org.eurekastreams.web.client.events.SwitchedHistoryViewEvent;
import org.eurekastreams.web.client.events.UpdateHistoryEvent;
import org.eurekastreams.web.client.events.UpdateRawHistoryEvent;
import org.eurekastreams.web.client.events.UpdatedHistoryParametersEvent;
import org.eurekastreams.web.client.jsni.WidgetJSNIFacadeImpl;
import org.eurekastreams.web.client.ui.Session;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.History;
/**
* The HistoryHandler should be the ONLY history listener in the entire app. It should listen for history change events
* and fire off one of two events. An event to indicate the current page/view has changed (which would rerender the page
* but not in a reload) and/or an event to indicate the parameters of the page have changed (which will alert the
* current view of this, not change it. It also eats an UpdateHistoryEvent which allows a view to change the history w/o
* directly touching it, and involves helper methods to create history tokens so that manual Hyperlinks can be made
* without hardcoding URLs.
*
*/
public class HistoryHandler implements ValueChangeHandler<String>
{
/**
* The JSNI facade.
*/
private final WidgetJSNIFacadeImpl jsniFacade = new WidgetJSNIFacadeImpl();
/**
* The values.
*/
private Map<String, String> currentValues = new HashMap<String, String>();
/**
* The views.
*/
private List<String> currentViews = new ArrayList<String>();
/**
* The page.
*/
private Page currentPage = null;
/**
* Should we execute the value change.
*/
private boolean fireValueChange = true;
/**
* Should we stop the history change?.
*/
private boolean interruptHistoryChange = false;
/**
* The previous token.
*/
private String previousToken = "";
/**
* Default Constructor.
*/
public HistoryHandler()
{
EventBus eventBus = Session.getInstance().getEventBus();
eventBus.addObserver(UpdateHistoryEvent.class, new Observer<UpdateHistoryEvent>()
{
public void update(final UpdateHistoryEvent event)
{
updateHistory(getHistoryToken(event.getRequest()));
}
});
eventBus.addObserver(UpdateRawHistoryEvent.class, new Observer<UpdateRawHistoryEvent>()
{
public void update(final UpdateRawHistoryEvent event)
{
updateHistory(event.getHistoryToken());
}
});
eventBus.addObserver(PreventHistoryChangeEvent.class, new Observer<PreventHistoryChangeEvent>()
{
public void update(final PreventHistoryChangeEvent event)
{
interruptHistoryChange = true;
}
});
History.addValueChangeHandler(this);
}
/**
* Handles updating history when requested.
*
* @param historyToken
* The history token for the new location.
*/
private void updateHistory(final String historyToken)
{
previousToken = History.getToken();
onValueChange(historyToken);
fireValueChange = false;
jsniFacade.setHistoryToken(historyToken, true);
// in case setting the history token above doesn't cause onValueChange to be called, reset
// fireValueChange so it will work properly next time around
fireValueChange = true;
}
/**
* On Value Change.
*
* @param historyToken
* the history token.
*/
private void onValueChange(final String historyToken)
{
if (fireValueChange)
{
// save old state
List<String> originalViews = currentViews;
Page originalPage = currentPage;
Map<String, String> originalValues = currentValues;
// parse and store new state
CreateUrlRequest parsed = parseHistoryToken(historyToken);
currentPage = parsed.getPage();
currentViews = parsed.getViews();
currentValues = parsed.getParameters();
// check if the "views" part has changed
boolean viewUpdated = false;
if (originalViews.size() == currentViews.size())
{
for (int i = 0; i < currentViews.size(); i++)
{
if (!currentViews.get(i).equals(originalViews.get(i)))
{
viewUpdated = true;
break;
}
}
}
else
{
viewUpdated = true;
}
if (viewUpdated)
{
EventBus.getInstance().notifyObservers(new HistoryViewsChangedEvent(currentViews));
}
// check if the page has changed
if (originalPage != currentPage)
{
// Let developers know we're about to switch the view. Prep for it if necessary. If you want us
// to stop, throw a PreventHistoryChangeEvent and we'll set the boolean to stop it from going.
Session.getInstance().getEventBus()
.notifyObservers(new PreSwitchedHistoryViewEvent(currentPage, currentViews));
// We're all clear. Go ahead. These are the events you're looking for.
if (!interruptHistoryChange)
{
// Put the original events back into the event bus, wiping out all the events specific to the prior
// page.
Session.getInstance().getEventBus().restoreBufferedObservers();
// Clear all temporary timer jobs.
Session.getInstance().getTimer().clearTempJobs();
// Tell listeners the URL has indicated a page/view change.
Session.getInstance().getEventBus()
.notifyObservers(new SwitchedHistoryViewEvent(currentPage, currentViews));
}
// A developer has halted the process. He probably sees something he needs to alert the user
// of before they switch the page. Roll everything back.
else
{
currentViews = originalViews;
currentPage = originalPage;
currentValues = originalValues;
interruptHistoryChange = false;
fireValueChange = false;
jsniFacade.setHistoryToken(previousToken, true);
fireValueChange = true;
return;
}
}
Session.getInstance().getEventBus()
.notifyObservers(new UpdatedHistoryParametersEvent(currentValues, viewUpdated));
}
fireValueChange = true;
}
/**
* OnValueChange gets called when the history changes.
*
* @param event
* the event.
*/
public void onValueChange(final ValueChangeEvent<String> event)
{
onValueChange(event.getValue());
}
/**
* Parses a history token into its component parts.
*
* @param historyToken
* History token.
* @return Component parts as a CreateUrlRequest.
*/
public CreateUrlRequest parseHistoryToken(final String historyToken)
{
HashMap<String, String> parameters = new HashMap<String, String>();
String concatenatedViews = historyToken;
int questionMarkIndex = historyToken.indexOf("?");
if (questionMarkIndex >= 0)
{
concatenatedViews = historyToken.substring(0, questionMarkIndex);
// get the sub string of parameters var=1&var2=2&var3=3
String[] paramString = historyToken.substring(questionMarkIndex + 1).split("&");
for (int i = 0; i < paramString.length; i++)
{
String[] substr = paramString[i].split("=");
if (substr.length == 2)
{
parameters.put(jsniFacade.urlDecode(substr[0]), jsniFacade.urlDecode(substr[1]));
}
}
}
String[] tokens = concatenatedViews.split("/", 2);
Page page = Page.toEnum(tokens[0]);
List<String> views = tokens.length > 1 ? Arrays.asList(tokens[1].split("/")) : Collections.EMPTY_LIST;
return new CreateUrlRequest(page, views, parameters);
}
/**
* Gets a history token given the params.
*
* @param request
* the request.
* @return the token.
*/
public String getHistoryToken(final CreateUrlRequest request)
{
Page inPage = currentPage;
List<String> inViews = currentViews;
// determine page
if (request.getPage() != null)
{
inPage = request.getPage();
}
if (inPage == null)
{
inPage = Page.START;
}
// determine views
if (request.getViews() != null)
{
inViews = request.getViews();
}
// determine parameters
Map<String, String> parameters;
if (request.getReplacePrevious())
{
parameters = request.getParameters();
}
else
{
parameters = new HashMap<String, String>(currentValues);
for (Entry<String, String> entry : request.getParameters().entrySet())
{
if (entry.getValue() == null)
{
parameters.remove(entry.getKey());
}
else
{
parameters.put(entry.getKey(), entry.getValue());
}
}
}
// stringify page and views
StringBuilder sb = new StringBuilder(inPage.toString());
for (String view : inViews)
{
if (view != null && !view.isEmpty())
{
sb.append("/").append(view);
}
}
// stringify parameters
String prefix = "?";
for (Entry<String, String> entry : parameters.entrySet())
{
sb.append(prefix).append(jsniFacade.urlEncode(entry.getKey())).append("=")
.append(jsniFacade.urlEncode(entry.getValue()));
prefix = "&";
}
return sb.toString();
}
/**
* Get the value of a current parameter. NOTE: Do NOT use this to "monitor" the history param, only to grab a one
* time instance of it. Use the UpdatedHistoryParametersEvent to listen to a parameter.
*
* @param key
* the key.
* @return the value.
*/
public String getParameterValue(final String key)
{
return currentValues.get(key);
}
/**
* Gets the views.
*
* @return the views.
*/
public List<String> getViews()
{
return currentViews;
}
/**
* @return The current page.
*/
public Page getPage()
{
return currentPage;
}
/**
* @return A collection holding the current history parameters.
*/
public Map<String, String> getParameters()
{
return Collections.unmodifiableMap(currentValues);
}
}