package votorola.g.web.gwt; // Copyright 2010-2012, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE.
import com.google.gwt.core.client.*;
import com.google.gwt.event.logical.shared.*;
import com.google.gwt.http.client.UrlBuilder;
import com.google.gwt.regexp.shared.*;
import com.google.gwt.user.client.*;
import com.google.web.bindery.event.shared.*;
import java.util.*;
import votorola.g.hold.*;
import votorola.g.web.gwt.event.*;
import static votorola.g.web.gwt.Switch.SWITCH_SPEC_PATTERN;
import static votorola.g.web.gwt.Switch.SWITCH_SPEC_SEPARATOR_PATTERN;
/** An extended implementation of the URL history stacker with support for {@linkplain
* Switch switches}. Typical usage is by one of these two equivalent methods, each of
* which alters the fragment and therefore stacks a new URL in the browser history:
*
* - history.{@linkplain #setSwitchValue(String,String) setSwitchValue}( "NAME", "VALUE" )
* - switch.{@linkplain Switch#set(String)}( "VALUE" )
*
*
Note: with Internet Explorer, manual modification of the history token from
* within the address bar goes unrecorded in the stack (IE 8). As a result, a subsequent
* press of the back button takes you farther back than expected.
*
* @see Coding basics
* @see Making AJAX applications crawlable
*/
public final class HistoryX extends History
{
/** Constructs a permanent instance of HistoryX. Do not create and discard these on
* the fly; at present, they do not clean up after themselves.
*
* @see #isFragmentShared()
* @see #bus()
*/
public HistoryX( boolean _isFragmentShared, EventBus _bus )
{
isFragmentShared = _isFragmentShared;
bus = _bus;
synchronizer = new Synchronizer();
}
// ------------------------------------------------------------------------------------
/** Registers a handler to receive change events fired from this history stack. The
* event dispatch for switch changes proceeds as follows:
*
* - A switch value is changed via {@linkplain #setSwitchValue(String,String)
* setSwitchValue} or {@linkplain #clearSwitchValue(String) clearSwitchValue}.
*
* - A property change event with the name "switch-NAME" is
* immediately fired. [NOT YET IMPLIMENTED]
*
* - In the "{@linkplain Scheduler#scheduleFinally(Scheduler.ScheduledCommand)
* finally}" phase of the event dispatch loop, all switch values are compiled
* into a new token. If the new token differs from the old then it is added to
* this history stack as a {@linkplain #newItem(String) new item}, resulting in a
* new URL fragment on the document and a token change as described
* below.
*
* And for token changes:
*
* - The document's URL fragment is changed either programatically, for example
* via {@linkplain #newItem(String) newItem}(token) or {@linkplain
* com.google.gwt.user.client.Window.Location Window.Location}, or manually via
* the address bar.
*
* - A change event is created and fired from the {@linkplain #previewSource()
* preview source}.
*
* - The same change event is fired from this history stack. Handlers may
* retrieve switch values (which may or may not have changed) via {@linkplain
* #getSwitchValue(String) getSwitchValue}.
*/
public HandlerRegistration addHandler( final ValueChangeHandler handler )
{
return bus.addHandlerToSource( ValueChangeEvent.getType(), /*source*/HistoryX.this,
handler );
}
/** Registers a handler to receive change events fired from the {@linkplain
* #previewSource() preview source}.
*
* @see #addHandler(ValueChangeHandler)
*/
public HandlerRegistration addPreviewHandler( final ValueChangeHandler handler )
{
// Unfortunately I can think of no more elegant way to implement
// votorola.s.gwt.scene.Scenes.cCompositionSwitch than with a preview.
return bus.addHandlerToSource( ValueChangeEvent.getType(), previewSource, handler );
}
private final Object previewSource = new Object();
/** The source of all preview events.
*
* @see #addPreviewHandler(ValueChangeHandler)
*/
public Object previewSource() { return previewSource; }
/** The bus through which events are fired.
*/
public EventBus bus() { return bus; }
private final EventBus bus;
/** Returns the value of a switch, or null if the switch has no value.
*
* @see Switch#get()
*/
public String getSwitchValue( final String name ) { return switchMap.get( name ); }
/** Removes the switch from the history token and returns its value.
*
* @see Switch#clear()
*/
public String clearSwitchValue( final String name )
{
final String old = switchMap.remove( name );
synchronizer.syncTokenLater(); // after all switch changes
return old;
}
/** Sets the value of a switch and returns the old value. Setting an empty value
* has the same effect as clearing the switch.
*
* @see Switch#set(String)
*/
public String setSwitchValue( final String name, final String value )
{
if( value.length() == 0 ) return clearSwitchValue( name );
final String old = switchMap.put( name, value );
synchronizer.syncTokenLater(); // after all switch changes
return old;
}
/** Answers whether the URL fragment of the browser window may be used for purposes
* other than storing the history token. For example, if the GWT application is
* embedded in a page that provides internal link targets (id
* attributes), then set this true in the constructor in order to suppress malformed
* switch alerts.
*
* @see votorola/a/web/gwt/gwt.js
* @return true if the fragment is shared, false if it is used exclusively for
* the history token.
*/
public boolean isFragmentShared() { return isFragmentShared; };
private final boolean isFragmentShared;
/** Schedules the construction of a new token to replace the current item in the
* browser's history stack, as opposed to adding a new one. The replacement will
* occur in the "{@linkplain Scheduler#scheduleFinally(Scheduler.ScheduledCommand)
* finally}" phase of the event dispatch loop, based on the switch values at that
* time, and a change event will then be fired. "All GWT state will be lost", as in
* a reload.
*
* @see com.google.gwt.user.client.Window.Location#replace(String)
*/
public void replace()
{
toReplace = true;
synchronizer.syncTokenLater(); // atomic with any pending sync
}
private boolean toReplace;
/** The map of switches in the current history token, indexed by switch name.
*/
public Map switchMap() { return switchMap; }
private final Map switchMap = new HashMap();
// - H i s t o r y --------------------------------------------------------------------
/** Throws {@linkplain UnsupportedOperationException UnsupportedOperationException}.
*/
public static com.google.gwt.event.shared.HandlerRegistration addValueChangeHandler(
ValueChangeHandler handler )
{
throw new UnsupportedOperationException( "use the HistoryX methods instead" );
}
// /** Throws {@linkplain UnsupportedOperationException UnsupportedOperationException}.
// */
// @SuppressWarnings( "deprecation" )
// public static void addHistoryListener( HistoryListener listener )
// {
// throw new UnsupportedOperationException( "use the HistoryX methods instead" );
// }
///// Why does that suppression fail? Fails when moved to class, too.
//// P r i v a t e ///////////////////////////////////////////////////////////////////////
/** Constructs a token from the current values of the switch map and appends it to
* the specified string builder. Does not affect the history stack.
*
* @param b the string builder to use.
*/
private StringBuilder appendToken( final StringBuilder b )
{
final Iterator> entryIterator = switchMap.entrySet().iterator();
if( entryIterator.hasNext() )
{
for( ;; )
{
final Map.Entry entry = entryIterator.next();
b.append( entry.getKey() );
b.append( '=' );
final String value = entry.getValue();
for( int v = 0, vN = value.length(); v < vN; ++v )
{
char ch = value.charAt( v );
if( ch == ' ' ) ch = '+'; // encode ' ' as '+', as in query parameters
else if( ch == '+' ) ch = ' '; // encode '+' as ' ', which becomes %20 in URL
// In a query parameter, '+' would instead become %2B in the URL, so
// this is an imperfect simulation of its encoding behaviour. But
// it would be difficult to perfect because the encoding code is
// wrapped up too tightly in the History base class.
b.append( ch );
}
if( !entryIterator.hasNext() ) break;
b.append( '&' );
}
}
return b;
}
private final Synchronizer synchronizer;
// ====================================================================================
private final class Synchronizer implements Scheduler.ScheduledCommand,
ValueChangeHandler
{
private Synchronizer()
{
History.addValueChangeHandler( Synchronizer.this ); // no need to unregister, registry does not outlive the handler
syncSwitches( getToken() ); // init state
}
private void syncSwitches( final String fromToken )
{
if( fromToken.equals( tokenLast )) return; // redundant, probably echo of syncToken()
tokenLast = fromToken;
switchMap.clear();
if( fromToken.length() == 0 ) return;
final SplitResult split = SWITCH_SPEC_SEPARATOR_PATTERN.split( fromToken );
for( int s = split.length() - 1; s >= 0; --s )
{
final String switchSpec = split.get( s );
final MatchResult m = SWITCH_SPEC_PATTERN.exec( switchSpec );
if( m == null )
{
if( isFragmentShared ) break; // probably an internal link target, ignore
Window.alert( "Malformed switch specification: " + switchSpec );
continue;
}
final String encodedValue = m.getGroup( 2 );
final StringBuilder b = GWTX.stringBuilderClear();
for( int v = 0, vN = encodedValue.length(); v < vN; ++v )
{
char ch = encodedValue.charAt( v );
if( ch == ' ' ) ch = '+'; // cf. appendToken()
else if( ch == '+' ) ch = ' ';
b.append( ch );
}
switchMap.put( /*key*/m.getGroup( 1 ), /*value*/b.toString() );
}
}
private void syncToken() // from switches
{
tokenLast = appendToken(GWTX.stringBuilderClear()).toString();
if( toReplace )
{
toReplace = false;
final UrlBuilder u = Window.Location.createUrlBuilder();
u.setHash( tokenLast );
Window.Location.replace( u.buildString() ); // "all GWT state will be lost" according to API docs
}
else newItem( tokenLast ); // will fire change event, if this actually changes token
}
private void syncTokenLater() { syncTokenScheduler.schedule(); }
private final CoalescingSchedulerS syncTokenScheduler =
new CoalescingSchedulerS( CoalescingSchedulerS.FINALLY, Synchronizer.this );
private String tokenLast; // token of last sync in either direction
// - S c h e d u l e r . S c h e d u l e d - C o m m a n d ------------------------
public void execute() { syncToken(); }
// - V a l u e - C h a n g e - H a n d l e r --------------------------------------
public void onValueChange( final ValueChangeEvent eTrigger )
{
// We do not want to expose clients directly to eTrigger, because the switch
// map is not yet synchronized. The base History implementation (GWT 2.1)
// that generates eTrigger is wrapped too tightly to be modified, so we modify
// it after the fact and cascade a new event.
// CoalescingScheduler.Tester.i(bus).run(); // TEST
final String token = eTrigger.getValue();
syncSwitches( token );
final ValueChangeEvent e = new ValueChange( token );
bus.fireEventFromSource( e, previewSource );
bus.fireEventFromSource( e, HistoryX.this );
}
}
}