001package votorola.s.gwt.scene.vote; // Copyright 2011-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.event.logical.shared.*; 004import com.google.gwt.regexp.shared.*; 005import com.google.gwt.user.client.*; 006import com.google.web.bindery.event.shared.HandlerRegistration; 007import votorola.a.count.gwt.*; 008import votorola.a.web.gwt.*; 009import votorola.g.hold.*; 010import votorola.g.lang.*; 011import votorola.g.web.gwt.*; 012import votorola.g.web.gwt.event.*; 013import votorola.s.gwt.scene.*; 014import votorola.s.gwt.stage.*; 015 016 017/** Position scoping based on dart sectors. A user's stance in regard to a particular 018 * issue is specified by reference to a poll name and a votepath of dart sectors. The 019 * scoping state is exposed in the {@linkplain Scenes#sScopingSwitch() 's' switch}: 020 * 021 * <table class='definition' style='margin-left:1em'> 022 * <tr> 023 * <th class='key'>Switch</th> 024 * <th>Controlled state</th> 025 * <th>Default</th> 026 * </tr> 027 * <tr><td class='key'>s</td> 028 * 029 * <td>The scoping state. Its form is s={@linkplain #pollName() 030 * poll-name}*{@linkplain #votepath() votepath}. The vote path is optional. 031 * Examples are "s=Sys/p/sandbox" and "s=Sys/p/sandbox*9kc2".</td> 032 * 033 * <td>None, which means all positions in all polls.</td> 034 * 035 * </tr> 036 * </table> 037 * 038 * <p>When the stage is ready to be initialized, this scoping model sets the {@linkplain 039 * Stage#setActorName(String) actor} and {@linkplain Stage#setPollName(String) poll} 040 * according to the 's' switch. It thereafter keeps itself and the stage in synchrony. 041 * At present it cannot synchonize to a change of actor on the stage, however, nor can it 042 * honour the configuration of a {@linkplain Stage#getDefaultActorName() default actor}; 043 * when these become necessary, please refer to the implementation plan in the code.</p> 044 * 045 * @see Scoping 046 * @see <a href='http://reluk.ca/w/Category:Position' target='_top' 047 * >Category:Position</a> 048 * @see votorola.a.count.CountNode#dartSector() 049 */ 050final class DartScoping implements Scoping 051{ 052 053 // cf. my archived ._/PositionScoping.javas based on username rather than dart sector 054 055 056 /** Constructs a DartScoping. 057 * 058 * @param _spool the spool for the release of associated holds. When unwound it 059 * releases the holds of the instance and thereby disables it. 060 * @throws IllegalStateException if the stage is configured with a {@linkplain 061 * Stage#getDefaultActorName() default actor}, which is not currently 062 * supported. 063 */ 064 public DartScoping( Spool _spool ) 065 { 066 spool = _spool; 067 Stage.i().addInitializer( new TheatreInitializer0() // auto-removed 068 { 069 public @Override void initFrom( Stage _s, boolean _isReferencePending ) { init(); } 070 // Does the reverse (init to) as part and parcel of model initialization, 071 // but this should be entirely without effect on the stage, otherwise the 072 // stage is wrong and needs correcting. The purpose of initFrom (aside from 073 // triggering init) is to convey state that was persisted by the stage. But 074 // this is unecessary for the state variables that are synchronized between 075 // the stage and dart scoping (actor and poll) because both of these 076 // variables are persisted and controlled by the 's' scoping switch in the 077 // URL. This model passively obeys that switch on initialization and 078 // enforces the same obedience on the stage. 079 public @Override void initTo( Stage _s ) { init(); } 080 public @Override void initTo( Stage _s, TheatrePage _referrer ) { init(); } 081 }); 082 } 083 084 085 private void init() 086 { 087 spool.add( new Hold() // sync from page URL: 088 { 089 final HandlerRegistration hR = Scenes.i().sScopingSwitch().addHandler( 090 new ValueChangeHandler<String>() 091 { 092 public void onValueChange( final ValueChangeEvent<String> e ) 093 { 094 rescope( e.getValue() ); 095 } 096 }); 097 public void release() { hR.removeHandler(); } 098 }); 099 spool.add( new Hold() // sync from stage: 100 { 101 final HandlerRegistration hR = GWTX.i().bus().addHandlerToSource( 102 PropertyChange.TYPE, /*source*/Stage.i(), new PropertyChangeHandler() 103 { 104 public void onPropertyChange( final PropertyChange e ) 105 { 106 if( "pollName".equals( e.propertyName() )) 107 { 108 final String newPollName = Stage.i().getPollName(); 109 if( ObjectX.nullEquals( pollName, newPollName )) return; 110 111 Scenes.i().sScopingSwitch().set( appendSwitch( GWTX.stringBuilderClear(), 112 newPollName, votepath ).toString() ); 113 } 114 // else if( "actorName".equals( e.propertyName() )) 115 // 116 // Never happens because the actor is changed only from this scoping 117 // model and never externally. Support for external changes would 118 // require a map in the CountCache to look up the count node by the new 119 // actor's name, or to request it from the server. Then once the node is 120 // obtained and verified to still match the actor on stage, the dart 121 // sector would be set here in the scoping model. syncActorToStage would 122 // also have to guard against setting the actor after such such an 123 // external change so as not to clobber it. 124 } 125 }); 126 public void release() { hR.removeHandler(); } 127 }); 128 rescope( Scenes.i().sScopingSwitch().get() ); // init state 129 } 130 131 132 133 // ------------------------------------------------------------------------------------ 134 135 136 /** Appends a value for the 's' scoping switch to accord with the specified 137 * parameters. 138 * 139 * @return the same string builder. 140 * 141 * @see #pollName() 142 * @see #votepath() 143 */ 144 static StringBuilder appendSwitch( final StringBuilder b, final String pollName, 145 final String votepath ) 146 { 147 if( pollName != null && 148 (votepath.length() > 0 || !pollName.equals(Stage.i().getDefaultPollName())) ) 149 { 150 // b.append( pollName.replace( '/', '!' )); 151 /// no need, slash not reserved in fragment, http://tools.ietf.org/html/rfc2396#section-4.1 152 b.append( pollName ); 153 if( votepath.length() > 0 ) 154 { 155 b.append( '*' ); 156 b.append( votepath ); 157 } 158 } 159 return b; 160 } 161 162 163 164 /** Decodes the dart sector for the specified offset along the votepath. 165 * 166 * @param v the character offset along the votepath. 167 * @return a dart sector of 1 to {@value 168 * votorola.a.count.CountNode#DART_SECTOR_MAX}. 169 * 170 * @see #votepath() 171 * @see votorola.a.count.CountNode#dartSector() 172 */ 173 byte dartSector( final int v ) { return votepathDecoded[v]; } 174 175 176 177 /** A string of encoded digits for use in translation from radix 10 to 21. 178 * Usage:<pre> 179 * 180 * char radix21Digit = DIGIT_ENCODER[radix10DartSector];</pre> 181 */ 182 static final String DIGIT_ENCODER = "0123456789bcdfghjkmnp"; // radix 21 digit 183 // ^0 ^10 ^20 radix 10 value 184 // ^30 ^62 ^70 hex character 185 186 187 /** An array of bytes for use in translating from radix 21 to 10. Usage:<pre> 188 * 189 * byte radix10DartSector = DIGIT_DECODER[radix21Digit - '0'];</pre> 190 */ 191 private static final byte[] DIGIT_DECODER = new byte[] 192 { 193 // 0 1 2 3 4 5 6 7 8 9 radix 21 digit 194 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, // radix 10 value 195 // 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f hex character 196 197 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 198 // 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 199 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 200 // 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f 201 202 // b c d f g h j k m n 203 0, 0, 10, 11, 12, 0, 13, 14, 15, 0, 16, 17, 0, 18, 19, 0, 204 // 60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 205 206 // p 207 20 208 // 70 209 }; 210 211 212 213 /** The name of the poll in which the user takes a position, or null if no poll is 214 * specified. If no poll is specifed, then the votepath will be empty. 215 * 216 * @see votorola.a.count.Poll#name() 217 */ 218 final String pollName() { return pollName; } 219 220 221 private String pollName; 222 223 224 225 /** Prompts this scoping model to set the stage actor if that is wanted. This is a 226 * temporary workaround for the difficult task of mapping votepaths to usernames. 227 * Instead we depend on VotespaceV to discover the information for its own purposes 228 * and then to send this prompt. 229 * 230 * @param nodeOnVotepath a node that is on the current votepath, or null if a 231 * missing node is detected. 232 * @param votepathFromNode the votepath from the node to the terminal candidate, 233 * which may be null if the node is null. 234 */ 235 void syncActorToStage( final CountNodeJS nodeOnVotepath, final String votepathFromNode ) 236 { 237 if( !syncActorToStage_isPending ) return; 238 239 if( nodeOnVotepath == null ) Stage.setActorName( null ); 240 else 241 { 242 if( !votepath.equals( votepathFromNode )) return; 243 244 Stage.setActorName( nodeOnVotepath.name() ); 245 } 246 syncActorToStage_isPending = false; 247 } 248 249 250 private boolean syncActorToStage_isPending; // guard against busy testing/setting 251 252 253 254 /** The path of a vote expressed as a sequence of count nodes, each node being 255 * identified by an encoded dart sector. The path extends from the voter node (index 256 * 0) through each candidate node that receives the vote. The dart sector of each 257 * node is encoded in radix 21 using the following ASCII characters as digits: 258 * 259 * <pre>{@value #DIGIT_ENCODER}</pre> 260 * 261 * <p>Dart sectors are therefore encoded as single digits in the range 1 to p. For 262 * example, the path of encoded digits "9kc2" decodes to sector numbers (9, 17, 11, 263 * 2).</p> 264 * 265 * @return the votepath which may be an empty string. 266 * 267 * @see #dartSector(int) 268 * @see votorola.a.count.CountNode#dartSector() 269 */ 270 String votepath() { return votepath; } 271 272 273 private String votepath; 274 275 276 private byte[] votepathDecoded; // contains no zeros 277 278 279 280 // - O b j e c t ---------------------------------------------------------------------- 281 282 283 public @Override String toString() 284 { 285 return "dart scope = poll(" + pollName + ") votepath(" + votepath + ")"; 286 } 287 288 289 290 // - S c o p i n g -------------------------------------------------------------------- 291 292 293 public HandlerRegistration addHandler( final ScopeChangeHandler handler ) 294 { 295 return GWTX.i().bus().addHandlerToSource( ScopeChangeEvent.TYPE, /*source*/DartScoping.this, 296 handler ); 297 } 298 299 300 301//// P r i v a t e /////////////////////////////////////////////////////////////////////// 302 303 304 private @Warning("init call") void rescope( final String s ) 305 { 306 try 307 { 308 if( s == null ) 309 { 310 rescopeToDefault(); 311 return; 312 } 313 314 final MatchResult m = S_PATTERN.exec( s ); 315 if( m == null ) 316 { 317 Window.alert( "Malformed scoping switch: s=" + s ); 318 rescopeToDefault(); 319 return; 320 } 321 322 pollName = m.getGroup( 1 ).replace( '!', '/' ); 323 // we used to encode '/' thus, so we decode it for sake of old URLs 324 final Stage st = Stage.i(); 325 if( pollName == null ) pollName = st.getDefaultPollName(); 326 votepath = m.getGroup( 2 ); // if any 327 if( votepath == null ) votepath = ""; 328 final int vN = votepath.length(); 329 votepathDecoded = new byte[vN]; 330 for( int v = 0; v < vN; ++v ) 331 { 332 final byte dartSector = DIGIT_DECODER[votepath.charAt(v) - '0']; 333 if( dartSector == 0 ) 334 { 335 Window.alert( "Malformed scoping switch, votepath contains a zero: s=" + s ); 336 rescopeToDefault(); 337 return; 338 } 339 340 votepathDecoded[v] = dartSector; 341 } 342 st.setPollName( pollName ); // sync to stage 343 if( vN == 0 ) Stage.setActorName( null ); 344 else syncActorToStage_isPending = true; 345 } 346 finally{ GWTX.i().bus().fireEventFromSource( new ScopeChangeEvent(), DartScoping.this ); } 347 } 348 349 350 351 private void rescopeToDefault() 352 { 353 final Stage st = Stage.i(); 354 pollName = st.getDefaultPollName(); 355 if( st.getDefaultActorName() != null ) throw new IllegalStateException( "stage configured with default actor" ); // not supported, per API doc 356 357 votepath = ""; 358 votepathDecoded = new byte[0]; 359 st.setPollName( pollName ); // sync to stage 360 Stage.setActorName( null ); 361 } 362 363 364 365 /** The pattern of the 's' scoping switch. Sets group (1) to the poll name and (2) 366 * to the encoded votepath, if any. The asterisk '*' separator was chosen because 367 * the following were rejected for the reasons specified:<ul> 368 * 369 * <li>'!' used to be allowed for encoding '/' in the poll name, and we still 370 * decode it</li> 371 * <li>apostrope ''' and left bracket '(' are escape-encoded by Firefox 5.0 when 372 * copying from its address bar, which results in an ugly URL</li> 373 * 374 * 375 * </ul>A left bracket '(' is allowed in place of the '*' separator for backward 376 * compatibility with early prototypes (0.2.3), URLs of which are in the archives of 377 * the Metagov and Votorola lists. This usage is deprecated. 378 * 379 * @see <a href='http://tools.ietf.org/html/rfc2396#section-2.3' target='_top'> 380 * ietf.org/html/rfc2396#section-2.3</a> 381 */ 382 private static final RegExp S_PATTERN = RegExp.compile( 383 "^([A-Z][^*(]*)(?:[*(]([" + DIGIT_ENCODER + "]+))?$" ); 384 // POLL * VOTEPATH 385 386 387 388 private final Spool spool; 389 390 391 392}