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}