package votorola.a.register; // Copyright 2008, 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.sql.*; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import votorola._.*; import votorola.a.manline.*; import votorola.g.lang.*; import votorola.g.sql.*; /** A neighbourhood in a compiled voter list. Neighbourhoods divide the overall electoral * region into a hierarchical patchwork of areas. They serve primarily for indexing * and structuring of the list. * * @see VoterList */ public class Neighbourhood { /** Creates a Neighbourhood with default values. * * @param table per {@linkplain #table() table}() * @param path per {@linkplain #path() path}() * @param isLeaf per {@linkplain #isLeaf() isLeaf}() */ Neighbourhood( Table table, String path, boolean isLeaf ) { this( table, new Key( path, isLeaf )); isChanged = true; } /** Constructs an existing Neighbourhood. * * @param table per {@linkplain #table() table}() * @param path per {@linkplain #path() path}() * @param isLeaf per {@linkplain #isLeaf() isLeaf}() * @param doubtedVoterCount per {@linkplain #getDoubtedVoterCount() getDoubtedVoterCount}() * @param doubted2VoterCount per {@linkplain #getDoubted2VoterCount() getDoubted2VoterCount}() * @param doubted4VoterCount per {@linkplain #getDoubted4VoterCount() getDoubted4VoterCount}() * @param trustedVoterCount per {@linkplain #getTrustedVoterCount() getTrustedVoterCount}() */ Neighbourhood( Table table, String path, boolean isLeaf, String childPath, boolean isChildLeaf, long doubtedVoterCount, long doubted2VoterCount, long doubted4VoterCount, long trustedVoterCount ) { this( table, new Key( path, isLeaf )); // Adding fields here? Add to copy(). Increment VoterList.serialVersionUID. this.childPath = childPath; this.isChildLeaf = isChildLeaf; this.doubtedVoterCount = doubtedVoterCount; this.doubted2VoterCount = doubted2VoterCount; this.doubted4VoterCount = doubted4VoterCount; this.trustedVoterCount = trustedVoterCount; } private Neighbourhood( Table table, Key key ) { if( table == null ) throw new NullPointerException(); // fail fast this.table = table; this.key = key; } // ------------------------------------------------------------------------------------ /** @see #getChildPath() * @see #isChildLeaf() */ final void clearChild() { if( childPath == null ) return; childPath = null; isChanged = true; } /** Writes this neighbourhood to the table, if it has uncommitted changes. */ final void commit() throws SQLException { if( !isChanged ) return; table.put( Neighbourhood.this ); isChanged = false; } /** Copy all the fieelds of another neighbourhood. */ final void copy( Neighbourhood other ) { setChild( other.getChildPath(), other.isChildLeaf() ); setDoubtedVoterCount( other.getDoubtedVoterCount() ); setDoubted2VoterCount( other.getDoubted2VoterCount() ); setDoubted4VoterCount( other.getDoubted4VoterCount() ); setTrustedVoterCount( other.getTrustedVoterCount() ); } /** The path to this neighbourhood's single child, if it has only one. If the child * itself has only a single child, then the path is that of the deeper child; and so * on. The single child itself is not physically stored in the table, as its * counts would be redundant with those of this neighbourhood. *

* This facility for "abstract" child nodes is an optimization, to dispense with * creation and storage of redundant intermediate neighbourhoods. But it has * complicated the code. *

* * @return the path to the single child; or null, if there is no child, or * if there are multiple children * * @see #clearChild() * @see #setChild(String,boolean) * @see #isChildLeaf() */ final String getChildPath() { return childPath; } private String childPath; /** Returns the number of doubted voters in the neighbourhood, * those receiving 1 or more doubt signals. * * @see #setDoubtedVoterCount(long) */ final long getDoubtedVoterCount() { return doubtedVoterCount; } private long doubtedVoterCount; /** Sets the number of doubted voters. * * @see #getDoubtedVoterCount() */ final void setDoubtedVoterCount( long newVoterCount ) { if( newVoterCount == doubtedVoterCount ) return; doubtedVoterCount = newVoterCount; isChanged = true; } /** Returns the number of 2X doubted voters in the neighbourhood, * those receiving 2 or more doubt signals. * * @see #setDoubted2VoterCount(long) */ final long getDoubted2VoterCount() { return doubted2VoterCount; } private long doubted2VoterCount; /** Sets the number of doubted voters. * * @see #getDoubted2VoterCount() */ final void setDoubted2VoterCount( long new2VoterCount ) { if( new2VoterCount == doubted2VoterCount ) return; doubted2VoterCount = new2VoterCount; isChanged = true; } /** Returns the number of 4X doubted voters in the neighbourhood, * those receiving 4 or more doubt signals. * * @see #setDoubted4VoterCount(long) */ final long getDoubted4VoterCount() { return doubted4VoterCount; } private long doubted4VoterCount; /** Sets the number of doubted voters. * * @see #getDoubted4VoterCount() */ final void setDoubted4VoterCount( long new4VoterCount ) { if( new4VoterCount == doubted4VoterCount ) return; doubted4VoterCount = new4VoterCount; isChanged = true; } /** Returns the number of trusted voters in the neighbourhood. * * @see #setTrustedVoterCount(long) */ final long getTrustedVoterCount() { return trustedVoterCount; } private long trustedVoterCount; /** Sets the number of trusted voters. * * @see #getTrustedVoterCount() */ final void setTrustedVoterCount( long newVoterCount ) { if( newVoterCount == trustedVoterCount ) return; trustedVoterCount = newVoterCount; isChanged = true; } /** Returns true if the single child is a leaf; false if it is not. If there is no * single child, the value is meaningless. * * @see #getChildPath() * @see #clearChild() * @see #setChild(String,boolean) */ final boolean isChildLeaf() { return isChildLeaf; } private boolean isChildLeaf; /** Returns true if this is a leaf neighbourhood. A leaf neighbourhood comprises * voters whose {@linkplain ListNode#getNeighbourhoodPath() paths} exactly match the * neighbourhood path. For example, if the paths are based on postal codes, and a * voter V had registered with an erroneously truncated postal code of "M5S 1K" (and * somehow became trusted at that address) then V would reside in the leaf * neighbourhood at path "M5S1K". This false neighbourhood would stand out in the * voter list, where the other voters would be likely to notice it, and cast doubt at * V's registration. *

* A non-leaf neighbourhood, by contrast, comprises voters whose paths * begin with the neighbourhood path, but are longer than it. So the * non-leaf neighbourhood at the path "M5S1K" would exclude V (above), * but would include voters at paths "M5S1K1" and "M5S1K2" (who would also be * included in the two leaf neighbourhoods at those paths). *

*/ final boolean isLeaf() { return key.isLeaf(); } /** The unique identifier of this neighbourhood. */ public final Key key() { return key; } private final Key key; /** The path to this neighbourhood in the tree of neighbourhoods. The final character * of the path string identifies this neighbourhood within the context of its parent * neighbourhood; the preceding character identifies the parent neighbourhood within * the context of its parent; and so on. *

* Root neighbourhoods are identified by an empty string, and correspond to the * entire electoral region. Leaves correspond to neighbourhoods at the smallest * scale. *

*

* The exact format of the path depends on the format of addresses in the * region. Street names might be used; or even entire address strings; but * postal codes are probably best. In a Canadian region, for example, a * neighbourhood with a postal code of "M5S IK3" might be assigned the path * "M5SIK3"; its parent neighbourhood, "M5SIK"; and so on. *

*

* The path, in combination with {@linkplain #isLeaf() isLeaf}, forms a unique * key to the neighbourhood. *

* * @return a non-null string, possibly empty */ final String path() { return key.path(); } /** Answers whether or not the root-leaf neighbourhood (at path "") is guaranteed to * be empty. */ final static boolean ROOT_LEAF_ALWAYS_EMPTY = true; /** @see #getChildPath() * @see #isChildLeaf() */ final void setChild( String newChildPath, boolean newChildLeaf ) { if( ObjectX.nullEquals(newChildPath,childPath) && newChildLeaf == isChildLeaf ) return; childPath = newChildPath; isChildLeaf = newChildLeaf; isChanged = true; } /** The table in which this neighbourhood is stored. */ final Table table() { return table; } private final Table table; // - O b j e c t ---------------------------------------------------------------------- /** Returns true iff the o is a neighbourhood, and its key "equals" this * neighbourhood's key. */ public @Override boolean equals( Object o ) { if( o == null || !Neighbourhood.class.equals( o.getClass() )) return false; Neighbourhood other = (Neighbourhood)o; return key.equals( other.key ); } /** Returns a description of the neighbourhood, including the values of its unique * key. */ public @Override final String toString() { return key.toString(); } // ==================================================================================== /** A cached neighbourhood table, with cache controls. */ static @ThreadRestricted final class CachedTable extends Table implements CacheControlledTable { CachedTable( ReadyDirectory rD, Database d ) throws IOException { super( rD, d ); isCachingAndCommitting = true; } private final LinkedHashMap cache; { final float loadFactor = 0.75f; final int maximumOvergrow = 100; // in capacity, guess cache = new LinkedHashMap( /*initial capacity*/(int)((skimmedSize + maximumOvergrow) / loadFactor), loadFactor, /*access order*/true ); } private static final int skimmedSize = 1000; // to which it returns after skimming // -------------------------------------------------------------------------------- /** Adds a voter to the table, updating the counts for the voter's neighbourhood, * and its ancestors in the tree. */ void addVoter( final ListNode node ) throws SQLException { if( node.getBar() != null ) return; // barred from the list if( node.getNeighbourhoodPath().length() == 0 ) throw new IllegalStateException( "voters in the root-leaf neighbourhood must be barred, per ROOT_LEAF_ALWAYS_EMPTY" ); final String leafPath = node.getNeighbourhoodPath(); for( int p = 0, pN = leafPath.length(); p <= pN; ++p ) { final String path = leafPath.substring( 0, p ); final Neighbourhood neighbourhood = getOrCreate( path, /*isLeaf*/p == pN ); try { // If it's a new parent, then set it a single child. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // if( neighbourhood instanceof NeighbourhoodIC && !neighbourhood.isLeaf() ) //// best be stricter, since root is pre-created (maybe others too, later) if( neighbourhood.getTrustedVoterCount() == 0L && !neighbourhood.isLeaf() ) { neighbourhood.setChild( leafPath, /*isLeaf*/true ); break; // because that's effectively the leaf } // Else, if it's a single-child parent... // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final String existingChildPath = neighbourhood.getChildPath(); if( existingChildPath != null ) { // (A) If new leaf belongs to existing child, skip ahead. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( neighbourhood.isChildLeaf() && existingChildPath.equals(leafPath) // existing is same as new || !neighbourhood.isChildLeaf() && pN > existingChildPath.length() && leafPath.startsWith(existingChildPath) ) // existing is ancestor of new { p = existingChildPath.length(); // skip ahead to child, and beyond (on next pass) continue; } // (B) Else, find lowest common ancestor of both children. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - assert p < pN : "parent cannot be a leaf"; assert existingChildPath.startsWith( path ): "child is child of parent"; int pLowCommonAncestor = p; // till proven otherwise for( int q = p + 1;; ++q ) // q is end-bound, like p; not character index { if( existingChildPath.charAt(pLowCommonAncestor) != leafPath.charAt(pLowCommonAncestor) ) break; // found divergence if( q >= pN ) break; // disallow full length of leafPath, leaf cannot be an ancestor if( q >= existingChildPath.length() ) break; // existing child cannot be the common ancestor, or we would have skipped ahead to it in (A) pLowCommonAncestor = q; } final int pNew = pLowCommonAncestor + 1; // create new neighbourhoods for existing and new children // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` { final Neighbourhood newForExisting; if( existingChildPath.length() == pNew ) // new *is* existing child { newForExisting = new Neighbourhood( CachedTable.this, existingChildPath, /*isLeaf*/neighbourhood.isChildLeaf() ); newForExisting.copy( neighbourhood ); newForExisting.clearChild(); } else // new is ancestor of existing child { newForExisting = new Neighbourhood( CachedTable.this, existingChildPath.substring( 0, pNew ), /*isLeaf*/false ); newForExisting.copy( neighbourhood ); } cache.put( newForExisting.key(), newForExisting ); } // ` ` ` { final Neighbourhood newForNew; if( leafPath.length() == pNew ) // new *is* new child { newForNew = new Neighbourhood( CachedTable.this, leafPath, /*isLeaf*/true ); } else // new is ancestor of new child { newForNew = new Neighbourhood( CachedTable.this, leafPath.substring(0,pNew), /*isLeaf*/false ); newForNew.setChild( leafPath, /*isLeaf*/true ); } newForNew.setTrustedVoterCount( 1 ); cache.put( newForNew.key(), newForNew ); } // adjust original ancestor neighbourhood // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` if( p == pLowCommonAncestor ) neighbourhood.clearChild(); // lowest common *is* original ancestor else // lowest common is single child of original ancestor { neighbourhood.setChild( leafPath.substring( 0, pLowCommonAncestor ), /*isLeaf*/false ); } break; // because newForNew is the effective leaf } } finally { neighbourhood.setTrustedVoterCount( neighbourhood.getTrustedVoterCount() + 1 ); if( node.doubterCount() >= 1 ) { neighbourhood.setDoubtedVoterCount( neighbourhood.getDoubtedVoterCount() + 1 ); if( node.doubterCount() >= 2 ) { neighbourhood.setDoubted2VoterCount( neighbourhood.getDoubted2VoterCount() + 1 ); if( node.doubterCount() >= 4 ) { neighbourhood.setDoubted4VoterCount( neighbourhood.getDoubted4VoterCount() + 1 ); } } } skimFlush(); } } } @Override Neighbourhood get( final String path, final boolean isLeaf ) throws SQLException { Neighbourhood neighbourhood = cache.get( new Key( path, isLeaf )); // if( neighbourhood == null && !cache.containsKey( etc )) //// no need of null values if( neighbourhood == null ) { neighbourhood = super.get( path, isLeaf ); if( neighbourhood != null ) cache.put( neighbourhood.key(), neighbourhood ); } return neighbourhood; } @Override Neighbourhood getOrCreate( final String path, final boolean isLeaf ) throws SQLException { final Neighbourhood neighbourhood = super.getOrCreate( path, isLeaf ); if( neighbourhood instanceof NeighbourhoodIC ) { cache.put( neighbourhood.key(), neighbourhood ); } return neighbourhood; } // - C a c h e - C o n t r o l l e d - T a b l e ---------------------------------- public void commitAll() throws SQLException { for( Neighbourhood neighbourhood: cache.values() ) { if( neighbourhood != null ) neighbourhood.commit(); // null guard probably redundant } } public void skimFlush() throws SQLException { if( cache.size() <= skimmedSize ) return; final Iterator neighbourhoodIterator = cache.values().iterator(); for( ;; ) { Neighbourhood neighbourhood = neighbourhoodIterator.next(); if( neighbourhood != null ) neighbourhood.commit(); // null guard probably redundant neighbourhoodIterator.remove(); if( cache.size() <= skimmedSize ) break; } } } // ==================================================================================== /** A unique identifier for a particular neighbourhood. */ static @ThreadSafe final class Key implements Serializable { private static final long serialVersionUID = 0L; /** Constructs a Key. * * @param path per {@linkplain Neighbourhood#path() path}() * @param isLeaf per {@linkplain Neighbourhood#isLeaf() isLeaf}() */ Key( String path, boolean isLeaf ) { if( path == null ) throw new NullPointerException(); // fail fast this.path = path; this.isLeaf = isLeaf; } // -------------------------------------------------------------------------------- /** @see Neighbourhood#isLeaf() */ boolean isLeaf() { return isLeaf; } private final boolean isLeaf; /** @see Neighbourhood#path() */ String path() { return path; } private final String path; // - O b j e c t ------------------------------------------------------------------ public @Override boolean equals( Object o ) { if( o == null || !Key.class.equals( o.getClass() )) return false; Key other = (Key)o; return isLeaf == other.isLeaf && path.equals( other.path ); } public @Override int hashCode() { return path.hashCode() * 31 + (isLeaf? 0:1); } // multiplier reduces overlap due to addition public @Override String toString() { return "Neighbourhood[path=\"" + path + "\", " + (isLeaf?"leaf":"non-leaf") + "]"; } } // ==================================================================================== /** The relational store of trust neighbourhoods that (in part) backs * a {@linkplain VoterList compiled voter list}. * * @see ListNodeC.Table */ static @ThreadSafe class Table { /** Constructs a Table. * * @param readyDirectory per {@linkplain #readyDirectory() readyDirectory}() * @param database per {@linkplain #database() database}() */ Table( final ReadyDirectory readyDirectory, final Database database ) throws IOException { this.readyDirectory = readyDirectory; this.database = database; final String snapDateSuffix = VOSnap.dateSuffix( readyDirectory.snapDirectory().getName() ); if( !VOSnap.isDateSuffix( snapDateSuffix )) throw new VotorolaRuntimeException( "improperly date-suffixed snap directory parent of ready directory: " + readyDirectory ); tableName = snapDateSuffix.substring(1) + VOSnap.dateSuffixDelimiter + "neighbourhood" + VOSnap.dateSuffix(readyDirectory.getCanonicalFile().getName()); statementKeyBase = getClass().getName() + ":" + schemaName() + "/" + tableName + "."; } protected boolean isCachingAndCommitting; // try to prevent future bugs - FIX these caching tricks are too dangerous private String schemaName() { return readyDirectory.name(); } private final String statementKeyBase; private final String tableName; // -------------------------------------------------------------------------------- /** Creates this table in the database. */ final void create() throws SQLException { final String key = statementKeyBase + "create"; synchronized( database ) { PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "CREATE TABLE \"" + schemaName() + "\".\"" + tableName + "\"" + " (path character varying," + " isLeaf boolean," + " childPath character varying," + " isChildLeaf boolean NOT NULL," + " doubtedVoterCount bigint NOT NULL," + " doubted2VoterCount bigint NOT NULL," + " doubted4VoterCount bigint NOT NULL," + " trustedVoterCount bigint NOT NULL," + " PRIMARY KEY (path, isLeaf))" ); // + " UNIQUE (childPath, isChildLeaf))" ); //// except that writing of altered rows cannot easily be ordered to prevent violations // Changing table structure? Then also increment VoterList.serialVersionUID. database.statementCache().put( key, s ); } s.execute(); } } /** Returns the database in which this table is stored. */ final Database database() { return database; } private final Database database; /** Drops this table from the database. */ final void drop() throws SQLException { final String key = statementKeyBase + "drop"; synchronized( database ) { PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "DROP TABLE \"" + schemaName() + "\".\"" + tableName + "\"" ); database.statementCache().put( key, s ); } s.execute(); } } /** Returns true if this table exists in the database; false otherwise. */ final boolean exists() throws SQLException { final String key = statementKeyBase + "exists"; synchronized( database ) { PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "SELECT * FROM \"" + schemaName() + "\".\"" + tableName + "\"" ); s.setMaxRows( 1 ); database.statementCache().put( key, s ); } try { s.execute(); } catch( SQLException x ) { if( "42P01".equals( x.getSQLState() )) return false; // UNDEFINED TABLE throw x; } } return true; } /** Retrieves a neighbourhood from this table. * * @return neighbourhood as stored in the table; or null, if none is stored */ Neighbourhood get( final String path, final boolean isLeaf ) throws SQLException { if( path == null ) throw new NullPointerException(); // fail fast final String key = statementKeyBase + "get"; synchronized( database ) { PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "SELECT childPath, isChildLeaf," + " doubtedVoterCount, doubted2VoterCount, doubted4VoterCount," + " trustedVoterCount" + " FROM \"" + schemaName() + "\".\"" + tableName + "\"" + " WHERE path = ? AND isLeaf = ?" ); database.statementCache().put( key, s ); } s.setString( 1, path ); s.setBoolean( 2, isLeaf ); final ResultSet r = s.executeQuery(); try { if( !r.next() ) return null; return new Neighbourhood( Table.this, path, isLeaf, r.getString(1), r.getBoolean(2), r.getLong(3), r.getLong(4), r.getLong(5), r.getLong(6) ); } finally{ r.close(); } } } /** Retrieves the ancestor neighbourhood of a single child. * * @return ancestor neighbourhood; or null, if there is none */ final Neighbourhood getAncestorOfSingleChild( final String childPath, final boolean isChildLeaf ) throws SQLException { if( childPath == null ) throw new NullPointerException(); // fail fast if( isCachingAndCommitting ) throw new IllegalStateException(); final String key = statementKeyBase + "getAncestorOfSingleChild"; synchronized( database ) { PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "SELECT path, isLeaf," + " doubtedVoterCount, doubted2VoterCount, doubted4VoterCount," + " trustedVoterCount" + " FROM \"" + schemaName() + "\".\"" + tableName + "\"" + " WHERE childPath = ? AND isChildLeaf = ?" ); database.statementCache().put( key, s ); } s.setString( 1, childPath ); s.setBoolean( 2, isChildLeaf ); final ResultSet r = s.executeQuery(); try { if( !r.next() ) return null; return new Neighbourhood( Table.this, r.getString(1), r.getBoolean(2), childPath, isChildLeaf, r.getLong(3), r.getLong(4), r.getLong(5), r.getLong(6) ); } finally{ r.close(); } } } /** Retrieves a list of child neighbourhoods for a given parent. The list is * pre-ordered by path and isLeaf. */ final java.util.List getChildren( final String parentPath ) throws SQLException { if( isCachingAndCommitting ) throw new IllegalStateException(); final String key = statementKeyBase + "getChildren"; synchronized( database ) { PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "SELECT * FROM \"" + schemaName() + "\".\"" + tableName + "\"" + " WHERE path LIKE ? ORDER BY path, isLeaf" ); database.statementCache().put( key, s ); } s.setString( 1, parentPath + "_" ); // child pattern, exactly one character longer final ResultSet r = s.executeQuery(); final ArrayList childList = new ArrayList(); try { while( r.next() ) { childList.add( new Neighbourhood( Table.this, r.getString(1), r.getBoolean(2), r.getString(3), r.getBoolean(4), r.getLong(5), r.getLong(6), r.getLong(7), r.getLong(8) )); } } finally{ r.close(); } return childList; } } /** Retrieves a neighbourhood from this table; or, if none is stored, a default * neighbourhood. * * @return Neighbourhood as stored in the table; or, if none is stored, * a {@linkplain NeighbourhoodIC NeighbourhoodIC} with default values */ Neighbourhood getOrCreate( final String path, final boolean isLeaf ) throws SQLException { Neighbourhood neighbourhood = get( path, isLeaf ); if( neighbourhood == null ) { neighbourhood = new NeighbourhoodIC( Table.this, path, isLeaf ); } return neighbourhood; } /** Stores a neighbourhood. */ final void put( final Neighbourhood neighbourhood ) throws SQLException { synchronized( database ) { { final String key = statementKeyBase + "putU"; PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "UPDATE \"" + schemaName() + "\".\"" + tableName + "\"" + " SET childPath = ?, isChildLeaf = ?," + " doubtedVoterCount = ?, doubted2VoterCount = ?," + " doubted4VoterCount = ?, trustedVoterCount = ?" + " WHERE path = ? AND isLeaf = ?" ); database.statementCache().put( key, s ); } s.setString( 1, neighbourhood.getChildPath() ); s.setBoolean( 2, neighbourhood.isChildLeaf() ); s.setLong( 3, neighbourhood.getDoubtedVoterCount() ); s.setLong( 4, neighbourhood.getDoubted2VoterCount() ); s.setLong( 5, neighbourhood.getDoubted4VoterCount() ); s.setLong( 6, neighbourhood.getTrustedVoterCount() ); s.setString( 7, neighbourhood.path() ); s.setBoolean( 8, neighbourhood.isLeaf() ); final int updatedRows = s.executeUpdate(); if( updatedRows > 0 ) { assert updatedRows == 1; return; } } { final String key = statementKeyBase + "putI"; PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "INSERT INTO \"" + schemaName() + "\".\"" + tableName + "\"" + " (path, isLeaf, childPath, isChildLeaf," + " doubtedVoterCount, doubted2VoterCount, doubted4VoterCount," + " trustedVoterCount)" + " VALUES (?, ?, ?, ?, ?, ?, ?, ?)" ); database.statementCache().put( key, s ); } s.setString( 1, neighbourhood.path() ); s.setBoolean( 2, neighbourhood.isLeaf() ); s.setString( 3, neighbourhood.getChildPath() ); s.setBoolean( 4, neighbourhood.isChildLeaf() ); s.setLong( 5, neighbourhood.getDoubtedVoterCount() ); s.setLong( 6, neighbourhood.getDoubted2VoterCount() ); s.setLong( 7, neighbourhood.getDoubted4VoterCount() ); s.setLong( 8, neighbourhood.getTrustedVoterCount() ); s.executeUpdate(); } } } /** The file-based counterpart to this table. */ final ReadyDirectory readyDirectory() { return readyDirectory; } private final ReadyDirectory readyDirectory; // final after init } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private boolean isChanged; }