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}