package 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. import java.io.*; import java.util.*; import votorola.g.*; import votorola.g.lang.*; import static votorola.a.position.DraftRevision.MAX_PATH_LENGTH; /** A {@linkplain DiffKey difference key} in parsed form. */ public @ThreadSafe final class DiffKeyParse { /** Constructs a DiffKeyParse from obsolete, named revision variables. This form of * difference key dates from when revision paths had a maximum length of two, the * second revision (if present) always designating a remote draft. The {@linkplain * #revisionSeries() revision series} is zero in this case. * * @param a the aPath revision number at index zero. * @param aR the aPath revision number at index one, or -1 if there is none. * @param b the bPath revision number at index zero. * @param bR the bPath revision number at index one, or -1 if there is none. */ public DiffKeyParse( final int a, final int aR, final int b, final int bR ) { final List aPathW = new ArrayList<>( /*initial capacity*/2 ); final List bPathW = new ArrayList<>( /*initial capacity*/2 ); aPathW.add( a ); bPathW.add( b ); if( aR > 0 ) aPathW.add( aR ); if( bR > 0 ) bPathW.add( bR ); revisionSeries = 0; key = append( aPathW, bPathW, revisionSeries, new StringBuilder() ).toString(); aPath = Collections.unmodifiableList( aPathW ); bPath = Collections.unmodifiableList( bPathW ); } /** Constructs a DiffKeyParse by taking ownership of two path lists. API integrity * and thread safety depend on the caller not subsequently modifying either list * (unchecked constraint). * * @see #aPath() * @see #bPath() * @see #revisionSeries() */ public DiffKeyParse( final List aPathW, final List bPathW, int _revisionSeries ) throws MalformedKey { ensurePathLength( aPathW ); ensurePathLength( bPathW ); revisionSeries = _revisionSeries; key = append( aPathW, bPathW, revisionSeries, new StringBuilder() ).toString(); aPath = Collections.unmodifiableList( aPathW ); // take ownership bPath = Collections.unmodifiableList( bPathW ); // save copy time } /** Parses a difference key and constructs a DiffKeyParse. * * @see #key() */ public DiffKeyParse( String _key ) throws MalformedKey { key = _key; final List aPathW = new ArrayList<>( MAX_PATH_LENGTH ); final List bPathW = new ArrayList<>( MAX_PATH_LENGTH ); revisionSeries = readSeries( key, readPath(key,bPathW,readPath(key,aPathW,0)) ); ensurePathLength( aPathW ); ensurePathLength( bPathW ); aPath = Collections.unmodifiableList( aPathW ); bPath = Collections.unmodifiableList( bPathW ); } private DiffKeyParse( String _key, List _aPath, List _bPath, int _revisionSeries ) { key = _key; aPath = _aPath; bPath = _bPath; revisionSeries = _revisionSeries; } // ------------------------------------------------------------------------------------ /** The revision path for the first (a) draft revision. */ public final List aPath() { return aPath; } private final List aPath; /** The revision path for the second (b) draft revision. */ public final List bPath() { return bPath; } private final List bPath; /** The difference key in string form. */ public final String key() { return key; } private final String key; /** Constructs the reverse of this parse, in which all components of a-draft and * b-draft are interchanged. */ public DiffKeyParse newReverseParse() { return new DiffKeyParse( DiffKey.newReverseKey(key), bPath, aPath, revisionSeries ); } /** The pollwiki revision series. * * @see votorola.a.PollwikiVS#revisionSeries() */ public final int revisionSeries() { return revisionSeries; } private final int revisionSeries; // - O b j e c t ---------------------------------------------------------------------- /** Returns the {@linkplain #key() difference key}. */ public @Override final String toString() { return key; } // ==================================================================================== /** Thrown when a malformed difference key is detected. */ public static @ThreadSafe final class MalformedKey extends IOException implements UserInformative { MalformedKey( String _message ) { super( _message ); } MalformedKey( String _message, Throwable _cause ) { super( _message, _cause ); } } //// P r i v a t e /////////////////////////////////////////////////////////////////////// /** Appends to the string builder the difference key for the specified revision paths * and series, and returns the same string builder. */ private static StringBuilder append( final List aPath, final List bPath, final int revisionSeries, final StringBuilder s ) { append( aPath, s ); s.append( '-' ); append( bPath, s ); if( revisionSeries > 0 ) { s.append( '!' ); s.append( revisionSeries ); } return s; } private static StringBuilder append( final List path, final StringBuilder s ) { s.append( path.get( 0 )); for( int r = 1, rN = path.size(); r < rN; ++r ) { s.append( '.' ); s.append( path.get(r) ); } return s; } private static void ensurePathLength( final List path ) throws MalformedKey { final int pN = path.size(); if( pN < 1 || pN > MAX_PATH_LENGTH ) throw new MalformedKey( "path length " + pN ); } private static int readPath( final String key, final List path, int c ) throws MalformedKey { final int cN = key.length(); if( cN == 0 ) return c; final int cPath = c; // start of path int cRev = c; // start of rev for( ;; ) { final char ch = key.charAt( c ); if( ch == '.' ) { readRev( key, path, cRev, c ); ++c; // swallow it if( c == cN ) throw new MalformedKey( "ends in dot: " + key ); cRev = c; // next rev continue; } if( ch == '-' ) { if( cPath != 0 ) throw new MalformedKey( "too many dashes: " + key ); // only the first path should end with a dash readRev( key, path, cRev, c ); ++c; // swallow it break; // end of path } if( ch == '!' ) { if( cPath == 0 ) throw new MalformedKey( "missing dash after first path: " + key ); // only the second path may end with an exclamation mark readRev( key, path, cRev, c ); ++c; // swallow it break; // end of paths } if( c == cRev ) // disallow any superfluous character that parseInt would accept: { if( ch == '0' ) throw new MalformedKey( "leading zero: " + key ); if( ch == '+' ) throw new MalformedKey( "unexpected character '" + ch + "':" + key ); } ++c; if( c == cN ) { readRev( key, path, cRev, c ); break; } } return c; } private static void readRev( final String key, final List path, final int cRev, final int cRevEndBound ) throws MalformedKey { final String revString = key.substring( cRev, cRevEndBound ); final int rev; try{ rev = Integer.parseInt( revString ); } catch( final NumberFormatException x ) { throw new MalformedKey( "rev string \"" + revString + "\"", x ); } path.add( rev ); } private static int readSeries( final String key, final int cSeries ) throws MalformedKey { final int cN = key.length(); if( cSeries >= cN ) return 0; final char ch = key.charAt( cSeries ); if( ch == '0' ) throw new MalformedKey( "explicit zero revision series: " + key ); // or leading zero, either of which is superfluous if( ch == '-' || ch == '+' ) // non-digit characters that parseInt would accept { throw new MalformedKey( "unexpected character in revision series '" + ch + "':" + key ); } final String seriesString = key.substring( cSeries ); try{ return Integer.parseInt( seriesString ); } catch( final NumberFormatException x ) { throw new MalformedKey( "revision series string \"" + seriesString + "\"", x ); } } }