001package 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.
002
003import com.google.gwt.core.client.*;
004import com.google.gwt.event.logical.shared.*;
005import com.google.gwt.http.client.UrlBuilder;
006import com.google.gwt.regexp.shared.*;
007import com.google.gwt.user.client.*;
008import com.google.web.bindery.event.shared.*;
009import java.util.*;
010import votorola.g.hold.*;
011import votorola.g.web.gwt.event.*;
012
013import static votorola.g.web.gwt.Switch.SWITCH_SPEC_PATTERN;
014import static votorola.g.web.gwt.Switch.SWITCH_SPEC_SEPARATOR_PATTERN;
015
016
017/** An extended implementation of the URL history stacker with support for {@linkplain
018  * Switch switches}.  Typical usage is by one of these two equivalent methods, each of
019  * which alters the fragment and therefore stacks a new URL in the browser history:<ul>
020  *
021  *     <li>history.{@linkplain #setSwitchValue(String,String) setSwitchValue}( "NAME", "VALUE" )</li>
022  *     <li>switch.{@linkplain Switch#set(String)}( "VALUE" )</li>
023  *
024  * </ul><p>Note: with Internet Explorer, manual modification of the history token from
025  * within the address bar goes unrecorded in the stack (IE 8).  As a result, a subsequent
026  * press of the back button takes you farther back than expected.</p>
027  *
028  *     @see <a href='http://code.google.com/webtoolkit/doc/latest/DevGuideCodingBasicsHistory.html'>Coding basics</a>
029  *     @see <a href='http://code.google.com/web/ajaxcrawling/docs/specification.html'>Making AJAX applications crawlable</a>
030  */
031public final class HistoryX extends History
032{
033
034    /** Constructs a permanent instance of HistoryX.  Do not create and discard these on
035      * the fly; at present, they do not clean up after themselves.
036      *
037      *     @see #isFragmentShared()
038      *     @see #bus()
039      */
040    public HistoryX( boolean _isFragmentShared, EventBus _bus )
041    {
042        isFragmentShared = _isFragmentShared;
043        bus = _bus;
044
045        synchronizer = new Synchronizer();
046    }
047
048
049
050   // ------------------------------------------------------------------------------------
051
052
053    /** Registers a handler to receive change events fired from this history stack.  The
054      * event dispatch for <em>switch</em> changes proceeds as follows:<ol>
055      *
056      *     <li>A switch value is changed via {@linkplain #setSwitchValue(String,String)
057      *     setSwitchValue} or {@linkplain #clearSwitchValue(String) clearSwitchValue}.</li>
058      *
059      *     <li>A property change event with the name "switch-<var>NAME</var>" is
060      *     immediately fired.  [NOT YET IMPLIMENTED]</li>
061      *
062      *     <li>In the "{@linkplain Scheduler#scheduleFinally(Scheduler.ScheduledCommand)
063      *     finally}" phase of the event dispatch loop, all switch values are compiled
064      *     into a new token.  If the new token differs from the old then it is added to
065      *     this history stack as a {@linkplain #newItem(String) new item}, resulting in a
066      *     new URL fragment on the document and a token change as described
067      *     below.</li></ol>
068      *
069      * <p>And for <em>token</em> changes:</p><ol>
070      *
071      *     <li>The document's URL fragment is changed either programatically, for example
072      *     via {@linkplain #newItem(String) newItem}(token) or {@linkplain
073      *     com.google.gwt.user.client.Window.Location Window.Location}, or manually via
074      *     the address bar.</li>
075      *
076      *     <li>A change event is created and fired from the {@linkplain #previewSource()
077      *     preview source}.</li>
078      *
079      *     <li>The same change event is fired from this history stack.  Handlers may
080      *     retrieve switch values (which may or may not have changed) via {@linkplain
081      *     #getSwitchValue(String) getSwitchValue}.</li></ol>
082      */
083    public HandlerRegistration addHandler( final ValueChangeHandler<String> handler )
084    {
085        return bus.addHandlerToSource( ValueChangeEvent.getType(), /*source*/HistoryX.this,
086          handler );
087    }
088
089
090
091    /** Registers a handler to receive change events fired from the {@linkplain
092      * #previewSource() preview source}.
093      *
094      *     @see #addHandler(ValueChangeHandler)
095      */
096    public HandlerRegistration addPreviewHandler( final ValueChangeHandler<String> handler )
097    {
098        // Unfortunately I can think of no more elegant way to implement
099        // votorola.s.gwt.scene.Scenes.cCompositionSwitch than with a preview.
100        return bus.addHandlerToSource( ValueChangeEvent.getType(), previewSource, handler );
101    }
102
103        private final Object previewSource = new Object();
104
105
106        /** The source of all preview events.
107          *
108          *     @see #addPreviewHandler(ValueChangeHandler)
109          */
110        public Object previewSource() { return previewSource; }
111
112
113
114    /** The bus through which events are fired.
115      */
116    public EventBus bus() { return bus; }
117
118
119        private final EventBus bus;
120
121
122
123    /** Returns the value of a switch, or null if the switch has no value.
124      *
125      *     @see Switch#get()
126      */
127    public String getSwitchValue( final String name ) { return switchMap.get( name ); }
128
129
130        /** Removes the switch from the history token and returns its value.
131          *
132          *     @see Switch#clear()
133          */
134        public String clearSwitchValue( final String name )
135        {
136            final String old = switchMap.remove( name );
137            synchronizer.syncTokenLater(); // after all switch changes
138            return old;
139        }
140
141
142        /** Sets the value of a switch and returns the old value.  Setting an empty value
143          * has the same effect as clearing the switch.
144          *
145          *     @see Switch#set(String)
146          */
147        public String setSwitchValue( final String name, final String value )
148        {
149            if( value.length() == 0 ) return clearSwitchValue( name );
150
151            final String old = switchMap.put( name, value );
152            synchronizer.syncTokenLater(); // after all switch changes
153            return old;
154        }
155
156
157
158    /** Answers whether the URL fragment of the browser window may be used for purposes
159      * other than storing the history token.  For example, if the GWT application is
160      * embedded in a page that provides internal link targets (<code>id</code>
161      * attributes), then set this true in the constructor in order to suppress malformed
162      * switch alerts.
163      *
164      *     @see <a href='../../../../../../a/web/gwt/gwt.js' target='_top'
165      *                           >votorola/a/web/gwt/gwt.js</a>
166      *     @return true if the fragment is shared, false if it is used exclusively for
167      *       the history token.
168      */
169    public boolean isFragmentShared() { return isFragmentShared; };
170
171
172        private final boolean isFragmentShared;
173
174
175
176    /** Schedules the construction of a new token to replace the current item in the
177      * browser's history stack, as opposed to adding a new one.  The replacement will
178      * occur in the "{@linkplain Scheduler#scheduleFinally(Scheduler.ScheduledCommand)
179      * finally}" phase of the event dispatch loop, based on the switch values at that
180      * time, and a change event will then be fired.  "All GWT state will be lost", as in
181      * a reload.
182      *
183      *     @see com.google.gwt.user.client.Window.Location#replace(String)
184      */
185    public void replace()
186    {
187        toReplace = true;
188        synchronizer.syncTokenLater(); // atomic with any pending sync
189    }
190
191
192        private boolean toReplace;
193
194
195
196    /** The map of switches in the current history token, indexed by switch name.
197      */
198    public Map<String,String> switchMap() { return switchMap; }
199
200
201        private final Map<String,String> switchMap = new HashMap<String,String>();
202
203
204
205   // - H i s t o r y --------------------------------------------------------------------
206
207
208    /** Throws {@linkplain UnsupportedOperationException UnsupportedOperationException}.
209      */
210    public static com.google.gwt.event.shared.HandlerRegistration addValueChangeHandler(
211      ValueChangeHandler<String> handler )
212    {
213        throw new UnsupportedOperationException( "use the HistoryX methods instead" );
214    }
215
216
217
218 // /** Throws {@linkplain UnsupportedOperationException UnsupportedOperationException}.
219 //   */
220 //   @SuppressWarnings( "deprecation" )
221 // public static void addHistoryListener( HistoryListener listener )
222 // {
223 //     throw new UnsupportedOperationException( "use the HistoryX methods instead" );
224 // }
225 ///// Why does that suppression fail?  Fails when moved to class, too.
226
227
228
229//// P r i v a t e ///////////////////////////////////////////////////////////////////////
230
231
232    /** Constructs a token from the current values of the switch map and appends it to
233      * the specified string builder.  Does not affect the history stack.
234      *
235      *     @param b the string builder to use.
236      */
237    private StringBuilder appendToken( final StringBuilder b )
238    {
239        final Iterator<Map.Entry<String,String>> entryIterator = switchMap.entrySet().iterator();
240        if( entryIterator.hasNext() )
241        {
242            for( ;; )
243            {
244                final Map.Entry<String,String> entry = entryIterator.next();
245                b.append( entry.getKey() );
246                b.append( '=' );
247                final String value = entry.getValue();
248                for( int v = 0, vN = value.length(); v < vN; ++v )
249                {
250                    char ch = value.charAt( v );
251                    if( ch == ' ' ) ch = '+'; // encode ' ' as '+', as in query parameters
252                    else if( ch == '+' ) ch = ' '; // encode '+' as ' ', which becomes %20 in URL
253                      // In a query parameter, '+' would instead become %2B in the URL, so
254                      // this is an imperfect simulation of its encoding behaviour.  But
255                      // it would be difficult to perfect because the encoding code is
256                      // wrapped up too tightly in the History base class.
257                    b.append( ch );
258                }
259
260                if( !entryIterator.hasNext() ) break;
261
262                b.append( '&' );
263            }
264        }
265        return b;
266    }
267
268
269
270    private final Synchronizer synchronizer;
271
272
273
274   // ====================================================================================
275
276
277    private final class Synchronizer implements Scheduler.ScheduledCommand,
278       ValueChangeHandler<String>
279    {
280
281        private Synchronizer()
282        {
283            History.addValueChangeHandler( Synchronizer.this ); // no need to unregister, registry does not outlive the handler
284            syncSwitches( getToken() ); // init state
285        }
286
287
288        private void syncSwitches( final String fromToken )
289        {
290            if( fromToken.equals( tokenLast )) return; // redundant, probably echo of syncToken()
291
292            tokenLast = fromToken;
293            switchMap.clear();
294            if( fromToken.length() == 0 ) return;
295
296            final SplitResult split = SWITCH_SPEC_SEPARATOR_PATTERN.split( fromToken );
297            for( int s = split.length() - 1; s >= 0; --s )
298            {
299                final String switchSpec = split.get( s );
300                final MatchResult m = SWITCH_SPEC_PATTERN.exec( switchSpec );
301                if( m == null )
302                {
303                    if( isFragmentShared ) break; // probably an internal link target, ignore
304
305                    Window.alert( "Malformed switch specification: " + switchSpec );
306                    continue;
307                }
308
309                final String encodedValue = m.getGroup( 2 );
310                final StringBuilder b = GWTX.stringBuilderClear();
311                for( int v = 0, vN = encodedValue.length(); v < vN; ++v )
312                {
313                    char ch = encodedValue.charAt( v );
314                    if( ch == ' ' ) ch = '+'; // cf. appendToken()
315                    else if( ch == '+' ) ch = ' ';
316                    b.append( ch );
317                }
318                switchMap.put( /*key*/m.getGroup( 1 ), /*value*/b.toString() );
319            }
320        }
321
322
323        private void syncToken() // from switches
324        {
325            tokenLast = appendToken(GWTX.stringBuilderClear()).toString();
326            if( toReplace )
327            {
328                toReplace = false;
329                final UrlBuilder u = Window.Location.createUrlBuilder();
330                u.setHash( tokenLast );
331                Window.Location.replace( u.buildString() ); // "all GWT state will be lost" according to API docs
332            }
333            else newItem( tokenLast ); // will fire change event, if this actually changes token
334        }
335
336
337        private void syncTokenLater() { syncTokenScheduler.schedule(); }
338
339
340        private final CoalescingSchedulerS syncTokenScheduler =
341          new CoalescingSchedulerS( CoalescingSchedulerS.FINALLY, Synchronizer.this );
342
343
344        private String tokenLast; // token of last sync in either direction
345
346
347       // - 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 ------------------------
348
349
350        public void execute() { syncToken(); }
351
352
353       // - V a l u e - C h a n g e - H a n d l e r --------------------------------------
354
355
356        public void onValueChange( final ValueChangeEvent<String> eTrigger )
357        {
358            // We do not want to expose clients directly to eTrigger, because the switch
359            // map is not yet synchronized.  The base History implementation (GWT 2.1)
360            // that generates eTrigger is wrapped too tightly to be modified, so we modify
361            // it after the fact and cascade a new event.
362
363         // CoalescingScheduler.Tester.i(bus).run(); // TEST
364            final String token = eTrigger.getValue();
365            syncSwitches( token );
366
367            final ValueChangeEvent<String> e = new ValueChange<String>( token );
368            bus.fireEventFromSource( e, previewSource );
369            bus.fireEventFromSource( e, HistoryX.this );
370        }
371
372
373    }
374
375
376
377}