001package votorola.a.trust; // Copyright 2008, 2010-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 java.io.*; 004import java.sql.*; 005import java.util.*; 006import votorola.a.*; 007import votorola.a.voter.*; 008import votorola.g.*; 009import votorola.g.lang.*; 010import votorola.g.sql.*; 011 012 013/** A writable node in a network trace. 014 */ 015public @ThreadRestricted( "single writer, readers touch" ) class TraceNodeW implements TraceNode 016{ 017 018 // cf. a/count/CountNodeW 019 020 private static final long serialVersionUID = 0L; 021 022 023 024 /** Creates a TraceNodeW with all variables at default values. 025 */ 026 TraceNodeW( IDPair registrant ) { this( registrant, null, null, null, null ); } 027 028 029 030 /** Creates a TraceNodeW. 031 */ 032 TraceNodeW( IDPair registrant, String area, String countryCode, String geohandle, 033 String otherProperties ) 034 { 035 this( registrant, area, countryCode, geohandle, otherProperties, new int[0] ); 036 isChanged = true; 037 } 038 039 040 041 private TraceNodeW( IDPair _registrant, String _area, String _countryCode, String _geohandle, 042 String _otherProperties, int[] _trustEdgeCountArray ) 043 { 044 if( _registrant == null ) throw new NullPointerException(); // fail fast 045 046 // Adding fields here? Increment serialVersionUID and NetworkTrace.serialVersionUID. 047 registrant = _registrant; 048 area = _area; 049 countryCode = _countryCode; 050 geohandle = _geohandle; 051 otherProperties = _otherProperties; 052 trustEdgeCountArray = _trustEdgeCountArray; 053 } 054 055 056 057 // ------------------------------------------------------------------------------------ 058 059 060 /** Attaches a trust edge to this node. The change may affect the trust level of this 061 * node, but is not automatically propagated to other nodes. 062 * 063 * @param trustLevel0 trust level of source node, 064 * or Integer.MAX_VALUE for infinity 065 * 066 * @see #detachTrustEdge(int) 067 * @see #trustLevel() 068 */ 069 final void attachTrustEdge( int trustLevel0 ) 070 { 071 if( trustLevel0 == 0 ) { assert false: "attach only from traced source"; return; } 072 073 if( trustLevel0 == Integer.MAX_VALUE ) trustLevel0 = 0; 074 if( trustLevel0 + 1 > trustEdgeCountArray.length ) 075 { 076 trustEdgeCountArray = Arrays.copyOf( trustEdgeCountArray, 077 trustLevel0 + 1 + GROWTH_PADDING ); 078 } 079 ++trustEdgeCountArray[trustLevel0]; 080 isChanged = true; 081 } 082 083 084 /** Count of attached edges, by source trust level. Index 0 is reserved 085 * for the level of infinity (from the root node). 086 */ 087 private int[] trustEdgeCountArray; 088 089 090 private static final int GROWTH_PADDING = 0; // none, because it can grow once only per trace (because cycles are impossible), and is never subject to more than one trace (because nodes are not cached) 091 092 093 /** Detaches a trust edge from this node. The change may affect the {@linkplain 094 * #trustLevel() trust level} of this node, but is not automatically propagated 095 * to other nodes. 096 * 097 * @param trustLevel0 trust level of source node 098 * 099 * @see #attachTrustEdge(int) 100 */ 101 final void detachTrustEdge( int trustLevel0 ) 102 { 103 if( trustLevel0 == 0 ) { assert false: "detach only when attached"; return; } 104 105 if( trustLevel0 == Integer.MAX_VALUE ) trustLevel0 = 0; 106 if( trustEdgeCountArray[trustLevel0] <= 0 ) { assert false: "detach never causes negative trust"; return; } 107 108 --trustEdgeCountArray[trustLevel0]; // no need to shrink array, it's done in write() 109 isChanged = true; 110 } 111 112 113 114 /** Serializes this node in XML format. 115 * 116 * @throws BadInputException 117 */ 118 void toXML( final Set<String> divisions, final Appendable a ) throws IOException 119 { 120 a.append( "\t<reg" ); 121 final VoterInputTable.XMLColumnAppender aC = new VoterInputTable.XMLColumnAppender( a ); 122 Registration.toCommonXMLAttributes( TraceNodeW.this, aC ); 123 aC.appendAttribute( "trustLevel", Integer.toString(trustLevel()) ); 124 final int primaryTrustEdgeCount = primaryTrustEdgeCount(); 125 if( primaryTrustEdgeCount != 0 ) 126 { 127 aC.appendAttribute( "primaryTrustEdgeCount", 128 Integer.toString(primaryTrustEdgeCount) ); 129 } 130 a.append( ">\n" ); 131 if( otherProperties != null ) 132 { 133 a.append( "\t\t" ); 134 a.append( otherProperties ); 135 a.append( '\n' ); 136 } 137 Registration.toCommonXMLElements( divisions, aC ); 138 a.append( "\t\t</reg>\n" ); 139 } 140 141 142 143 /** Writes this node to the table if it has unwritten changes. 144 */ 145 final void write( final Table table ) throws SQLException 146 { 147 if( !isChanged ) return; 148 149 final int[] trustEdgeCountArrayTrimmed; 150 { 151 int length = trustEdgeCountArray.length; // same as before, till proven otherwise 152 for(; length > 0; --length ) 153 { 154 if( trustEdgeCountArray[length-1] > 0 ) break; // found a positive count 155 } 156 if( length == trustEdgeCountArray.length ) 157 { 158 trustEdgeCountArrayTrimmed = trustEdgeCountArray; 159 } 160 else trustEdgeCountArrayTrimmed = Arrays.copyOf( trustEdgeCountArray, length ); 161 } 162 163 table.put( TraceNodeW.this, trustEdgeCountArrayTrimmed ); 164 isChanged = false; 165 } 166 167 168 169 // - T r a c e - N o d e -------------------------------------------------------------- 170 171 172 public final String getArea() { return area; } 173 174 175 private final String area; 176 177 178 179 public final String getCountryCode() { return countryCode; } 180 181 182 private final String countryCode; 183 184 185 186 public final String getGeohandle() { return geohandle; } 187 188 189 private final String geohandle; 190 191 192 193 public final String getOtherProperties() { return otherProperties; } 194 195 196 private final String otherProperties; 197 198 199 200 public final int primaryTrustEdgeCount() 201 { 202 int count = 0; 203 if( trustEdgeCountArray.length > 0 ) count = trustEdgeCountArray[0]; 204 return count; 205 } 206 207 208 209 public IDPair registrant() { return registrant; } 210 211 212 private final IDPair registrant; 213 214 215 216 /** @see #attachTrustEdge(int) 217 */ 218 public final int trustLevel() 219 { 220 int level = primaryTrustEdgeCount(); // sources of infinite trust 221 for( int l = trustEdgeCountArray.length - 1; l > level; --l ) // only sources at levels greater than the current level can contribute an increase 222 { 223 level = Math.min( level + trustEdgeCountArray[l], l ); // cannot reduce level, because l > level (per loop guard) 224 } 225 return level; 226 } 227 228 229 230 // - O b j e c t ---------------------------------------------------------------------- 231 232 233 /** Returns the registrant's username. 234 */ 235 public @Override final String toString() { return registrant().username(); } 236 237 238 239 // ==================================================================================== 240 241 242 /** A routine that runs in the context of a trace node. 243 */ 244 public interface Runner 245 { 246 247 // - T r a c e - N o d e - C . R u n n e r ---------------------------------------- 248 249 250 /** Runs this routine in the context of the specified trace node. 251 */ 252 public void run( final TraceNodeW node ); 253 254 } 255 256 257 258 // ==================================================================================== 259 260 261 /** The relational store of nodes that (in part) backs a {@linkplain NetworkTrace 262 * network trace}. 263 * 264 * @see votorola.a.trust.TrustEdge.Table 265 * @see votorola.a.trust.Membership.Table 266 */ 267 public static @ThreadSafe final class Table 268 { 269 270 // cf. a/count/CountTable 271 272 273 /** Constructs a Table. 274 */ 275 public Table( final ReadyDirectory readyDirectory, final Database database ) 276 throws IOException, SQLException 277 { 278 this.readyDirectory = readyDirectory; 279 this.database = database; 280 281 synchronized( database ) { database.ensureSchema( SCHEMA_NAME ); } 282 final String snapSuffix = OutputStore.suffix( 283 readyDirectory.snapDirectory().getName() ); 284 if( !OutputStore.isY4MDS( snapSuffix )) throw new VotorolaRuntimeException( "improperly suffixed snap directory parent of ready directory: " + readyDirectory ); 285 286 tableName = snapSuffix.substring(1) + OutputStore.SUFFIX_DELIMITER 287 + "trace_node" + OutputStore.suffix(readyDirectory.getName()); 288 statementKeyBase = getClass().getName() + ":" + SCHEMA_NAME + "/" + tableName + "."; 289 } 290 291 292 293 private final String statementKeyBase; 294 295 296 297 // -------------------------------------------------------------------------------- 298 299 300 /** Creates this table in the database. 301 */ 302 void create() throws SQLException 303 { 304 final String sKey = statementKeyBase + "create"; 305 synchronized( database ) 306 { 307 PreparedStatement s = database.statementCache().get( sKey ); 308 if( s == null ) 309 { 310 s = database.connection().prepareStatement( 311 "CREATE TABLE \"" + SCHEMA_NAME + "\".\"" + tableName + "\"" 312 + " (registrantEmail character varying PRIMARY KEY," 313 + " area character varying," 314 + " countryCode character varying," 315 + " geohandle character varying," 316 + " otherProperties character varying," 317 + " trustEdgeCountArray character varying)" ); 318 319 // Changing table structure? Then also increment NetworkTrace.serialVersionUID. 320 321 database.statementCache().put( sKey, s ); 322 } 323 s.execute(); 324 } 325 } 326 327 328 329 /** The database in which this table is stored. 330 */ 331 @Warning("thread restricted object") Database database() { return database; } 332 333 334 private final Database database; 335 336 337 338 /** Drops this table from the database if it exists. 339 * 340 * @return true if any rows were actually removed as a result, false otherwise. 341 */ 342 final boolean drop() throws SQLException 343 { 344 final String sKey = statementKeyBase + "drop"; 345 synchronized( database ) 346 { 347 PreparedStatement s = database.statementCache().get( sKey ); 348 if( s == null ) 349 { 350 s = database.connection().prepareStatement( 351 "DROP TABLE IF EXISTS \"" + SCHEMA_NAME + "\".\"" + tableName + "\"" ); 352 database.statementCache().put( sKey, s ); 353 } 354 final int updatedRows = s.executeUpdate(); 355 return updatedRows > 0; 356 } 357 } 358 359 360 361 /** Retrieves a node from this table. 362 * 363 * @return node as stored in the table; or null, if none is stored 364 */ 365 public TraceNodeW get( final IDPair registrant ) throws SQLException 366 { 367 if( registrant == null ) throw new NullPointerException(); // fail fast 368 369 final String sKey = statementKeyBase + "get"; 370 synchronized( database ) 371 { 372 PreparedStatement s = database.statementCache().get( sKey ); 373 if( s == null ) 374 { 375 s = database.connection().prepareStatement( 376 "SELECT area,countryCode,geohandle,otherProperties,trustEdgeCountArray" 377 + " FROM \"" + SCHEMA_NAME + "\".\"" + tableName + "\"" 378 + " WHERE registrantEmail = ?" ); 379 database.statementCache().put( sKey, s ); 380 } 381 s.setString( 1, registrant.email() ); 382 final ResultSet r = s.executeQuery(); 383 try 384 { 385 if( !r.next() ) return null; 386 387 return new TraceNodeW( registrant, r.getString(1), r.getString(2), 388 r.getString(3), r.getString(4), ArrayX.stringToInts(r.getString(5)) ); 389 } 390 finally{ r.close(); } 391 } 392 } 393 394 395 396 /** Retrieves a node from this table; or if none is stored, a default node. 397 * 398 * @return TraceNodeW as stored in the table; or, if none is stored, 399 * a {@linkplain TraceNodeIC TraceNodeIC} with default values 400 */ 401 public TraceNodeW getOrCreate( final IDPair registrant ) throws SQLException 402 { 403 TraceNodeW traceNode = get( registrant ); 404 if( traceNode == null ) traceNode = new TraceNodeIC( registrant ); 405 406 return traceNode; 407 } 408 409 410 411 /** Stores a node. 412 */ 413 void put( final TraceNodeW node, final int[] trustEdgeCountArray ) throws SQLException 414 { 415 final String qTable = "\"" + SCHEMA_NAME + "\".\"" + tableName + "\""; 416 synchronized( database ) 417 { 418 // effect an "upsert" in PostgreSQL 419 // http://stackoverflow.com/questions/1109061/insert-on-duplicate-update-postgresql/6527838#6527838 420 final Connection c = database.connection(); 421 { 422 final String sKey = statementKeyBase + "putU"; 423 PreparedStatement s = database.statementCache().get( sKey ); 424 if( s == null ) 425 { 426 s = c.prepareStatement( "UPDATE " + qTable 427 + " SET area = ?, countryCode = ?, geohandle = ?, otherProperties = ?," 428 + " trustEdgeCountArray = ?" 429 + " WHERE registrantEmail = ?" ); 430 database.statementCache().put( sKey, s ); 431 } 432 s.setString( 1, node.getArea() ); 433 s.setString( 2, node.getCountryCode() ); 434 s.setString( 3, node.getGeohandle() ); 435 s.setString( 4, node.getOtherProperties() ); 436 s.setString( 5, ArrayX.intsToString( trustEdgeCountArray )); 437 s.setString( 6, node.registrant().email() ); 438 final int updatedRows = s.executeUpdate(); 439 if( updatedRows > 0 ) { assert updatedRows == 1; return; } 440 } 441 { 442 final String sKey = statementKeyBase + "putI"; 443 PreparedStatement s = database.statementCache().get( sKey ); 444 if( s == null ) 445 { 446 s = c.prepareStatement( "INSERT INTO " + qTable 447 + " (registrantEmail, area, countryCode, geohandle, otherProperties," 448 + " trustEdgeCountArray)" 449 + " SELECT ?, ?, ?, ?, ?, ? WHERE NOT EXISTS" 450 + " (SELECT 1 FROM " + qTable + " WHERE registrantEmail = ?)" ); 451 database.statementCache().put( sKey, s ); 452 } 453 s.setString( 1, node.registrant().email() ); 454 s.setString( 2, node.getArea() ); 455 s.setString( 3, node.getCountryCode() ); 456 s.setString( 4, node.getGeohandle() ); 457 s.setString( 5, node.getOtherProperties() ); 458 s.setString( 6, ArrayX.intsToString( trustEdgeCountArray )); 459 s.setString( 7, node.registrant().email() ); 460 s.executeUpdate(); 461 } 462 } 463 } 464 465 466 467 /** The file-based counterpart to this table. 468 */ 469 ReadyDirectory readyDirectory() { return readyDirectory; } 470 471 472 private final ReadyDirectory readyDirectory; // final after init 473 474 475 476 /** Pass through the specified runner all nodes of this table. The nodes are 477 * sorted by area. 478 */ 479 public void run( final TraceNodeW.Runner runner ) throws SQLException 480 { 481 final String sKey = statementKeyBase + "run"; 482 synchronized( database ) 483 { 484 PreparedStatement s = database.statementCache().get( sKey ); 485 if( s == null ) 486 { 487 s = database.connection().prepareStatement( 488 "SELECT registrantEmail, area, countryCode, geohandle, otherProperties," 489 + " trustEdgeCountArray" 490 + " FROM \"" + SCHEMA_NAME + "\".\"" + tableName + "\"" 491 + " ORDER BY area" ); 492 database.statementCache().put( sKey, s ); 493 } 494 final ResultSet r = s.executeQuery(); 495 try 496 { 497 while( r.next() ) 498 { 499 runner.run( new TraceNodeW( IDPair.fromEmail(r.getString(1)), 500 r.getString(2), r.getString(3), r.getString(4), r.getString(5), 501 ArrayX.stringToInts(r.getString(6)) )); 502 } 503 } 504 finally{ r.close(); } 505 } 506 } 507 508 509 510 /** The name of the table's schema. 511 */ 512 static final String SCHEMA_NAME = "out_trace"; 513 514 515 516 /** The name of this table. 517 */ 518 String tableName() { return tableName; } 519 520 521 private final String tableName; 522 523 524 } 525 526 527 528//// P r i v a t e /////////////////////////////////////////////////////////////////////// 529 530 531 private boolean isChanged; 532 533 534 535}