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}