001package votorola.a.diff; // Copyright 2011-2013, 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 java.io.*;
004import java.util.*;
005import votorola.g.*;
006import votorola.g.lang.*;
007
008import static votorola.a.position.DraftRevision.MAX_PATH_LENGTH;
009
010
011/** A {@linkplain DiffKey difference key} in parsed form.
012  */
013public @ThreadSafe final class DiffKeyParse
014{
015
016
017    /** Constructs a DiffKeyParse from obsolete, named revision variables.  This form of
018      * difference key dates from when revision paths had a maximum length of two, the
019      * second revision (if present) always designating a remote draft.  The {@linkplain
020      * #revisionSeries() revision series} is zero in this case.
021      *
022      *     @param a the aPath revision number at index zero.
023      *     @param aR the aPath revision number at index one, or -1 if there is none.
024      *     @param b the bPath revision number at index zero.
025      *     @param bR the bPath revision number at index one, or -1 if there is none.
026      */
027    public DiffKeyParse( final int a, final int aR, final int b, final int bR )
028    {
029        final List<Integer> aPathW = new ArrayList<>( /*initial capacity*/2 );
030        final List<Integer> bPathW = new ArrayList<>( /*initial capacity*/2 );
031        aPathW.add( a );
032        bPathW.add( b );
033        if( aR > 0 ) aPathW.add( aR );
034        if( bR > 0 ) bPathW.add( bR );
035        revisionSeries = 0;
036        key = append( aPathW, bPathW, revisionSeries, new StringBuilder() ).toString();
037        aPath = Collections.unmodifiableList( aPathW );
038        bPath = Collections.unmodifiableList( bPathW );
039    }
040
041
042
043    /** Constructs a DiffKeyParse by taking ownership of two path lists.  API integrity
044      * and thread safety depend on the caller not subsequently modifying either list
045      * (unchecked constraint).
046      *
047      *     @see #aPath()
048      *     @see #bPath()
049      *     @see #revisionSeries()
050      */
051    public DiffKeyParse( final List<Integer> aPathW, final List<Integer> bPathW,
052      int _revisionSeries ) throws MalformedKey
053    {
054        ensurePathLength( aPathW );
055        ensurePathLength( bPathW );
056        revisionSeries = _revisionSeries;
057        key = append( aPathW, bPathW, revisionSeries, new StringBuilder() ).toString();
058        aPath = Collections.unmodifiableList( aPathW ); // take ownership
059        bPath = Collections.unmodifiableList( bPathW ); // save copy time
060    }
061
062
063
064    /** Parses a difference key and constructs a DiffKeyParse.
065      *
066      *     @see #key()
067      */
068    public DiffKeyParse( String _key ) throws MalformedKey
069    {
070        key = _key;
071        final List<Integer> aPathW = new ArrayList<>( MAX_PATH_LENGTH );
072        final List<Integer> bPathW = new ArrayList<>( MAX_PATH_LENGTH );
073        revisionSeries = readSeries( key, readPath(key,bPathW,readPath(key,aPathW,0)) );
074        ensurePathLength( aPathW );
075        ensurePathLength( bPathW );
076        aPath = Collections.unmodifiableList( aPathW );
077        bPath = Collections.unmodifiableList( bPathW );
078    }
079
080
081
082    private DiffKeyParse( String _key, List<Integer> _aPath, List<Integer> _bPath,
083      int _revisionSeries )
084    {
085        key = _key;
086        aPath = _aPath;
087        bPath = _bPath;
088        revisionSeries = _revisionSeries;
089    }
090
091
092
093   // ------------------------------------------------------------------------------------
094
095
096    /** The revision path for the first (a) draft revision.
097      */
098    public final List<Integer> aPath() { return aPath; }
099
100
101        private final List<Integer> aPath;
102
103
104
105    /** The revision path for the second (b) draft revision.
106      */
107    public final List<Integer> bPath() { return bPath; }
108
109
110        private final List<Integer> bPath;
111
112
113
114    /** The difference key in string form.
115      */
116    public final String key() { return key; }
117
118
119        private final String key;
120
121
122
123    /** Constructs the reverse of this parse, in which all components of a-draft and
124      * b-draft are interchanged.
125      */
126    public DiffKeyParse newReverseParse()
127    {
128        return new DiffKeyParse( DiffKey.newReverseKey(key), bPath, aPath, revisionSeries );
129    }
130
131
132
133    /** The pollwiki revision series.
134      *
135      *     @see votorola.a.PollwikiVS#revisionSeries()
136      */
137    public final int revisionSeries() { return revisionSeries; }
138
139
140        private final int revisionSeries;
141
142
143
144   // - O b j e c t ----------------------------------------------------------------------
145
146
147    /** Returns the {@linkplain #key() difference key}.
148      */
149    public @Override final String toString() { return key; }
150
151
152
153   // ====================================================================================
154
155
156    /** Thrown when a malformed difference key is detected.
157      */
158    public static @ThreadSafe final class MalformedKey extends IOException implements UserInformative
159    {
160        MalformedKey( String _message ) { super( _message ); }
161
162        MalformedKey( String _message, Throwable _cause ) { super( _message, _cause ); }
163    }
164
165
166
167//// P r i v a t e ///////////////////////////////////////////////////////////////////////
168
169
170    /** Appends to the string builder the difference key for the specified revision paths
171      * and series, and returns the same string builder.
172      */
173    private static StringBuilder append( final List<Integer> aPath, final List<Integer> bPath,
174      final int revisionSeries, final StringBuilder s )
175    {
176        append( aPath, s );
177        s.append( '-' );
178        append( bPath, s );
179        if( revisionSeries > 0 )
180        {
181            s.append( '!' );
182            s.append( revisionSeries );
183        }
184        return s;
185    }
186
187
188
189    private static StringBuilder append( final List<Integer> path, final StringBuilder s )
190    {
191        s.append( path.get( 0 ));
192        for( int r = 1, rN = path.size(); r < rN; ++r )
193        {
194            s.append( '.' );
195            s.append( path.get(r) );
196        }
197        return s;
198    }
199
200
201
202    private static void ensurePathLength( final List<Integer> path ) throws MalformedKey
203    {
204        final int pN = path.size();
205        if( pN < 1 || pN > MAX_PATH_LENGTH ) throw new MalformedKey( "path length " + pN );
206    }
207
208
209
210    private static int readPath( final String key, final List<Integer> path, int c )
211      throws MalformedKey
212    {
213        final int cN = key.length();
214        if( cN == 0 ) return c;
215
216        final int cPath = c; // start of path
217        int cRev = c; // start of rev
218        for( ;; )
219        {
220            final char ch = key.charAt( c );
221            if( ch == '.' )
222            {
223                readRev( key, path, cRev, c );
224                ++c; // swallow it
225                if( c == cN ) throw new MalformedKey( "ends in dot: " + key );
226
227                cRev = c; // next rev
228                continue;
229            }
230
231            if( ch == '-' )
232            {
233                if( cPath != 0 ) throw new MalformedKey( "too many dashes: " + key );
234                  // only the first path should end with a dash
235
236                readRev( key, path, cRev, c );
237                ++c; // swallow it
238                break; // end of path
239            }
240
241            if( ch == '!' )
242            {
243                if( cPath == 0 ) throw new MalformedKey( "missing dash after first path: " + key );
244                  // only the second path may end with an exclamation mark
245
246                readRev( key, path, cRev, c );
247                ++c; // swallow it
248                break; // end of paths
249            }
250
251
252            if( c == cRev ) // disallow any superfluous character that parseInt would accept:
253            {
254                if( ch == '0' ) throw new MalformedKey( "leading zero: " + key );
255
256                if( ch == '+' ) throw new MalformedKey( "unexpected character '" + ch + "':" + key );
257            }
258
259            ++c;
260            if( c == cN )
261            {
262                readRev( key, path, cRev, c );
263                break;
264            }
265        }
266        return c;
267    }
268
269
270
271    private static void readRev( final String key, final List<Integer> path, final int cRev,
272      final int cRevEndBound ) throws MalformedKey
273    {
274        final String revString = key.substring( cRev, cRevEndBound );
275        final int rev;
276        try{ rev = Integer.parseInt( revString ); }
277        catch( final NumberFormatException x )
278        {
279            throw new MalformedKey( "rev string \"" + revString + "\"", x );
280        }
281
282        path.add( rev );
283    }
284
285
286
287    private static int readSeries( final String key, final int cSeries ) throws MalformedKey
288    {
289        final int cN = key.length();
290        if( cSeries >= cN ) return 0;
291
292        final char ch = key.charAt( cSeries );
293        if( ch == '0' ) throw new MalformedKey( "explicit zero revision series: " + key );
294          // or leading zero, either of which is superfluous
295
296        if( ch == '-' || ch == '+' ) // non-digit characters that parseInt would accept
297        {
298            throw new MalformedKey( "unexpected character in revision series '" + ch + "':" + key );
299        }
300
301        final String seriesString = key.substring( cSeries );
302        try{ return Integer.parseInt( seriesString ); }
303        catch( final NumberFormatException x )
304        {
305            throw new MalformedKey( "revision series string \"" + seriesString + "\"", x );
306        }
307    }
308
309
310}