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}