Subversion Repositories DevTools

Rev

Rev 1295 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
1293 dpurdie 1
###############################################################################
2
# Codestriker: Copyright (c) 2001, 2002 David Sitsky.  All rights reserved.
3
# sits@users.sourceforge.net
4
#
5
# This program is free software; you can redistribute it and modify it under
6
# the terms of the GPL.
7
 
8
# Model object for handling topic data.
9
 
10
package Codestriker::Model::Topic;
11
 
12
use strict;
13
use Encode qw(decode_utf8);
14
 
15
use Codestriker::DB::DBI;
16
use Codestriker::Model::File;
17
use Codestriker::Model::Metrics;
18
 
19
sub new {
20
    my ($class, $topicid) = @_;
21
    my $self = {};
22
 
23
    $self->{topicid} = 0;
24
    $self->{author} = "";
25
    $self->{title} = "";
26
    $self->{bug_ids} = "";
27
    $self->{reviewers} = "";
28
    $self->{cc} = "";
29
    $self->{description} = "";
30
    $self->{document} = "";
31
    $self->{creation_ts} = "";
32
    $self->{modified_ts} = "";
33
    $self->{topic_state} = "";
34
    $self->{topic_state_id} = 0;
35
    $self->{version} = 0;
36
    $self->{start_tag} = "";
37
    $self->{end_tag} = "";
38
    $self->{module} = "";
39
    $self->{repository} = "";
40
    $self->{project_id} = "";
41
    $self->{project_name} = "";
42
    $self->{obsoleted_topics} = [];
43
    $self->{obsoleted_by} = [];
44
    $self->{comments} = [];
45
    $self->{metrics} = Codestriker::Model::Metrics->new($topicid);
46
    #
47
    # 29-Aug-07 SJK: Create a class variable to hold an optional URL that is
48
    # often included in the description, for linking to Release Manager etc.
49
    #
50
    $self->{desc_url} = "";
51
 
52
    bless $self, $class;
53
 
54
    if (defined($topicid)) {
55
	$self->read($topicid);
56
    }
57
 
58
    return $self;
59
}
60
 
61
# Delete the specified participant type from the topic.
62
sub _delete_participants($$$) {
63
    my ($self, $dbh, $type) = @_;
64
 
65
    my $delete_participants =
66
	$dbh->prepare_cached('DELETE FROM participant ' .
67
			     'WHERE topicid = ? AND type = ?');
68
    my $success = defined $delete_participants;
69
 
70
    $success &&= $delete_participants->execute($self->{topicid}, $type);
71
    return $success;
72
}
73
 
74
# Insert the specified participants into the topic.
75
sub _insert_participants($$$$$) {
76
    my ($self, $dbh, $type, $participants, $timestamp) = @_;
77
 
78
    my $insert_participant =
79
	$dbh->prepare_cached('INSERT INTO participant (email, topicid, type,' .
80
			     'state, modified_ts, version) ' .
81
			     'VALUES (?, ?, ?, ?, ?, ?)');
82
    my $success = defined $insert_participant;
83
 
84
    my @participants = split /, /, $participants;
85
    for (my $i = 0; $i <= $#participants; $i++) {
86
	$success &&= $insert_participant->execute($participants[$i],
87
						  $self->{topicid}, $type, 0,
88
						  $timestamp, 0);
89
    }
90
 
91
    return $success;
92
}
93
 
94
# Delete the bugids associated with a particular topic.
95
sub _delete_bug_ids($$) {
96
    my ($self, $dbh) = @_;
97
 
98
    my $delete_topicbug =
99
	$dbh->prepare_cached('DELETE FROM topicbug WHERE topicid = ?');
100
    my $success = defined $delete_topicbug;
101
 
102
    $success &&= $delete_topicbug->execute($self->{topicid});
103
    return $success;
104
}
105
 
106
# Insert the comma-separated list of bug_ids into the topic.
107
sub _insert_bug_ids($$$) {
108
    my ($self, $dbh, $bug_ids) = @_;
109
 
110
    my $insert_bugs =
111
	$dbh->prepare_cached('INSERT INTO topicbug (topicid, bugid) ' .
112
			     'VALUES (?, ?)');
113
    my $success = defined $insert_bugs;
114
 
115
    my @bug_ids = split /, /, $bug_ids;
116
    for (my $i = 0; $i <= $#bug_ids; $i++) {
117
	$success &&= $insert_bugs->execute($self->{topicid}, $bug_ids[$i]);
118
    }
119
 
120
    return $success;
121
}
122
 
123
# Create a new topic with all of the specified properties.
124
sub create($$$$$$$$$$$$) {
125
    my ($self, $topicid, $author, $title, $bug_ids, $reviewers, $cc,
126
	$description, $document, $start_tag, $end_tag, $module,
127
	$repository, $projectid, $deltas_ref, $obsoleted_topics) = @_;
128
 
129
    my $timestamp = Codestriker->get_timestamp(time);        
130
 
131
    $self->{topicid} = $topicid;
132
    $self->{author} = $author;
133
    $self->{title} = $title;
134
    $self->{bug_ids} = $bug_ids;
135
    $self->{reviewers} = $reviewers;
136
    $self->{cc} = $cc;
137
    $self->{description} = $description;
138
    $self->{document} = $document;
139
    $self->{creation_ts} = $timestamp;
140
    $self->{modified_ts} = $timestamp;
141
    $self->{topic_state} = 0;
142
    $self->{topic_state_id} = 0;
143
    $self->{project_id} = $projectid;
144
    $self->{version} = 0;
145
    $self->{start_tag} = $start_tag;
146
    $self->{end_tag} = $end_tag;
147
    $self->{module} = $module;
148
    $self->{repository} = $repository;
149
    $self->{metrics} = Codestriker::Model::Metrics->new($topicid);
150
    $self->{obsoleted_topics} = [];
151
    $self->{obsoleted_by} = [];
152
 
153
    # Obtain a database connection.
154
    my $dbh = Codestriker::DB::DBI->get_connection();
155
 
156
    # Create the prepared statements.
157
    my $insert_topic =
158
	$dbh->prepare_cached('INSERT INTO topic (id, author, title, ' .
159
			     'description, document, state, creation_ts, ' .
160
			     'modified_ts, version, start_tag, end_tag, ' .
161
			     'module, repository, projectid) ' .
162
			     'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
163
    my $success = defined $insert_topic;
164
 
165
    # Create all of the necessary rows.  It is assumed state 0 is the initial
166
    # state.
167
    $success &&= $insert_topic->execute($topicid, $author, $title,
168
					$description, $document, 0,
169
					$timestamp, $timestamp, 0,
170
					$start_tag, $end_tag, $module,
171
					$repository, $projectid);
172
 
173
    # Insert the associated bug records.
174
    $success &&= $self->_insert_bug_ids($dbh, $bug_ids);
175
 
176
    # Insert the reviewers and cc participants.
177
    $success &&=
178
	$self->_insert_participants($dbh,
179
				    $Codestriker::PARTICIPANT_REVIEWER,
180
				    $reviewers, $timestamp);
181
    $success &&=
182
	$self->_insert_participants($dbh,
183
				    $Codestriker::PARTICIPANT_CC,
184
				    $cc, $timestamp);
185
 
186
    # Create the appropriate delta rows.
187
    $success &&= Codestriker::Model::File->create($dbh, $topicid, $deltas_ref);
188
 
189
    # Create any obsolete records, if any.
190
    if (defined $obsoleted_topics && $obsoleted_topics ne '') {
191
	my $insert_obsolete_topic =
192
	    $dbh->prepare_cached('INSERT INTO topicobsolete ' .
193
				 '(topicid, obsoleted_by) ' .
194
				 'VALUES (?, ?)');
195
	my $success = defined $insert_obsolete_topic;
196
	my @data = split ',', $obsoleted_topics;
197
	my @obsoleted = ();
198
	for (my $i = 0; $success && $i <= $#data; $i+=2) {
199
	    my $obsolete_topic_id = $data[$i];
200
	    my $obsolete_topic_version = $data[$i+1];
201
	    $success &&=
202
		$insert_obsolete_topic->execute($obsolete_topic_id,
203
						$topicid);
204
	    push @obsoleted, $obsolete_topic_id if $success;
205
	}
206
	$self->{obsoleted_topics} = \@obsoleted;
207
    }
208
 
209
    Codestriker::DB::DBI->release_connection($dbh, $success);
210
 
211
    die $dbh->errstr unless $success;
212
}
213
 
214
# Read the contents of a specific topic, and return the results in the
215
# provided reference variables.
216
sub read($$) {
217
    my ($self, $topicid) = @_;
218
 
219
    $self->{topicid} = $topicid;    
220
 
221
    # Obtain a database connection.
222
    my $dbh = Codestriker::DB::DBI->get_connection();
223
 
224
    # Setup the prepared statements.
225
    my $select_topic = $dbh->prepare_cached('SELECT topic.id, topic.author, ' .
226
					    'topic.title, ' .
227
					    'topic.description, ' .
228
					    'topic.document, topic.state, ' .
229
					    'topic.creation_ts, ' .
230
					    'topic.modified_ts, ' .
231
					    'topic.version, ' .
232
					    'topic.start_tag, ' .
233
					    'topic.end_tag, ' .
234
					    'topic.module, ' .
235
					    'topic.repository, ' .
236
					    'project.id, project.name ' .
237
					    'FROM topic, project ' .
238
					    'WHERE topic.id = ? AND ' .
239
					    'topic.projectid = project.id');
240
    my $select_bugs =
241
	$dbh->prepare_cached('SELECT bugid FROM topicbug WHERE topicid = ?');
242
    my $select_participants =
243
	$dbh->prepare_cached('SELECT type, email FROM participant ' .
244
			     'WHERE topicid = ?');
245
    my $select_obsoleted_by =
246
	$dbh->prepare_cached('SELECT obsoleted_by FROM topicobsolete ' .
247
			     'WHERE topicid = ?');
248
    my $select_topics_obsoleted =
249
	$dbh->prepare_cached('SELECT topicid FROM topicobsolete ' .
250
			     'WHERE obsoleted_by = ?');
251
 
252
    my $success = defined $select_topic && defined $select_bugs &&
253
	defined $select_participants && defined $select_obsoleted_by &&
254
	defined $select_topics_obsoleted;
255
    my $rc = $Codestriker::OK;
256
 
257
    # Retrieve the topic information.
258
    $success &&= $select_topic->execute($topicid);
259
 
260
    my ($id, $author, $title, $description, $document, $state,
261
	$creationtime, $modifiedtime, $version, $start_tag, $end_tag,
262
	$module, $repository, $projectid, $projectname);
263
 
264
    if ($success) {
265
	($id, $author, $title, $description, $document, $state,
266
	 $creationtime, $modifiedtime, $version, $start_tag, $end_tag,
267
	 $module, $repository, $projectid, $projectname)
268
	    = $select_topic->fetchrow_array();
269
	$select_topic->finish();
270
 
271
	if (!defined $id) {
272
	    $success = 0;
273
	    $rc = $Codestriker::INVALID_TOPIC;
274
	}
275
    }
276
 
277
    # Retrieve the bug ids relating to this topic.
278
    my @bugs = ();
279
    $success &&= $select_bugs->execute($topicid);
280
    if ($success) {
281
	my @data;
282
	while (@data = $select_bugs->fetchrow_array()) {
283
	    push @bugs, $data[0];
284
	}
285
	$select_bugs->finish();
286
    }
287
 
288
    # Retrieve the participants in this review.
289
    my @reviewers = ();
290
    my @cc = ();
291
    $success &&= $select_participants->execute($topicid);
292
    if ($success) {
293
	while (my @data = $select_participants->fetchrow_array()) {
294
	    if ($data[0] == 0) {
295
		push @reviewers, $data[1];
296
	    } else {
297
		push @cc, $data[1];
298
	    }
299
	}
300
	$select_participants->finish();
301
    }
302
 
303
    # Retrieve the topics obsoleted by this topic.
304
    $success &&= $select_topics_obsoleted->execute($topicid);
305
    my @obsoleted_topics = ();
306
    if ($success) {
307
	while (my ($id) = $select_topics_obsoleted->fetchrow_array()) {
308
	    push @obsoleted_topics, $id;
309
	}
310
	$select_topics_obsoleted->finish();
311
    }
312
 
313
    # Retrieve the topics that have obsoleted this topic.
314
    $success &&= $select_obsoleted_by->execute($topicid);
315
    my @obsoleted_by = ();
316
    if ($success) {
317
	while (my ($id) = $select_obsoleted_by->fetchrow_array()) {
318
	    push @obsoleted_by, $id;
319
	}
320
	$select_obsoleted_by->finish();
321
    }
322
 
323
    # Close the connection, and check for any database errors.
324
    Codestriker::DB::DBI->release_connection($dbh, $success);
325
 
326
    # Store the data into the referenced variables if the operation was
327
    # successful.
328
    if ($success) {
329
	$self->{author} = $author;
330
	$self->{title} = decode_utf8($title);
331
	$self->{bug_ids} = join ', ', @bugs;
332
	$self->{reviewers} = join ', ', @reviewers;
333
	$self->{cc} = join ', ', @cc;
334
	$self->{description} = decode_utf8($description);
335
	$self->{document} = decode_utf8($document);
336
	$self->{creation_ts} = $creationtime;
337
	$self->{modified_ts} = $modifiedtime;
338
	$self->{topic_state} = $Codestriker::topic_states[$state];
339
	$self->{topic_state_id} = $state;
340
	$self->{project_id} = $projectid;
341
	$self->{project_name} = decode_utf8($projectname);
342
	$self->{start_tag} = $start_tag;
343
	$self->{end_tag} = $end_tag;
344
	$self->{module} = $module;
345
	$self->{version} = $version;
346
        $self->{metrics} = Codestriker::Model::Metrics->new($topicid);
347
	$self->{obsoleted_topics} = \@obsoleted_topics;
348
	$self->{obsoleted_by} = \@obsoleted_by;
349
 
350
	# Set the repository to the default system value if it is not defined.
351
	if (!defined $repository || $repository eq "") {
352
	    $self->{repository} = $Codestriker::default_repository;
353
	} else {
354
	    $self->{repository} = $repository;
355
	}
356
    #
357
    # 29-Aug-07 SJK: Detect & store optional URL that is
358
    # often included in the description, for linking to Release Manager etc.
359
    #
360
    if ( $self->{description} =~ m/(http:\/\/[^ \t\n]*)/i ) {
361
	    $self->{desc_url} = $1;
362
	}
363
 
364
    }
365
 
366
    return $success ? $Codestriker::OK : $rc;
367
}
368
 
369
# Reads from the db if needed, and returns the list of comments for
370
# this topic. If the list of comments have already been returned, the
371
# function will skip the db call, and just return the list from
372
# memory.
373
sub read_comments {
374
    my ($self) = shift;
375
 
376
    if (scalar(@{$self->{comments}}) == 0) {
377
	my @comments = Codestriker::Model::Comment->read_all_comments_for_topic($self->{topicid});
378
 
379
	$self->{comments} = \@comments;
380
    }
381
 
382
    return @{$self->{comments}};
383
}
384
 
385
# returns a count of the comments that have the given comment
386
# metric (commentmetrictype) set to value. Used 
387
# on the search page.
388
sub get_comment_metric_count {
389
    my ($self,$commentmetrictype,$value) = @_;
390
 
391
    my $count = 0;
392
 
393
    if (scalar(@{$self->{comments}}) == 0) {
394
 
395
        # Obtain a database connection.
396
        my $dbh = Codestriker::DB::DBI->get_connection();
397
 
398
        # the comments have not been read into memory yet.
399
        $count = $dbh->selectrow_array('
400
            SELECT COUNT(commentstatemetric.value) 
401
            FROM commentstatemetric, commentstate 
402
            WHERE  commentstate.topicid = ? AND
403
                   commentstate.id = commentstatemetric.id AND
404
                   commentstatemetric.name = ? AND
405
                   commentstatemetric.value = ?',
406
                   {}, $self->{topicid},
407
                   $commentmetrictype, $value);
408
 
409
        Codestriker::DB::DBI->release_connection($dbh, 1);
410
 
411
    } else {
412
        # already read into memory, don't hit the database.
413
 
414
        foreach my $comment (@{$self->{comments}}) {
415
            if ( exists( $comment->{$commentmetrictype} ) && 
416
                 $comment->{$commentmetrictype} eq $value ) {
417
                ++$count;
418
            }
419
        }
420
    }
421
 
422
    return $count;
423
}
424
 
425
 
426
# Retrieve the changed files which are a part of this review. It will only pull them
427
# from the database once.
428
sub get_filestable
429
{
430
    my ($self,$filenames, $revisions, $offsets, $binary, $numchanges) = @_;
431
 
432
    if (exists ($self->{filetable})) {
433
 
434
    	( $filenames, $revisions, $offsets,$binary, $numchanges ) = @{$self->{filetable}};
435
    }
436
    else {
437
 
438
        Codestriker::Model::File->get_filetable($self->{topicid},
439
    		    $filenames,
440
                    $revisions,
441
                    $offsets,
442
                    $binary,
443
                    $numchanges);
444
 
445
        $self->{filetable} = [ 
446
    		    $filenames,
447
                    $revisions,
448
                    $offsets,
449
                    $binary,
450
                    $numchanges ];
451
 
452
    }
453
 
454
}
455
 
456
 
457
 
458
# Determine if the specified topic id exists in the table or not.
459
sub exists($) {
460
    my ($topicid) = @_;
461
 
462
    # Obtain a database connection.
463
    my $dbh = Codestriker::DB::DBI->get_connection();
464
 
465
    # Prepare the statement and execute it.
466
    my $select_topic = $dbh->prepare_cached('SELECT COUNT(*) FROM topic ' .
467
					    'WHERE id = ?');
468
    my $success = defined $select_topic;
469
    $success &&= $select_topic->execute($topicid);
470
 
471
    my $count;
472
    if ($success) {
473
	($count) = $select_topic->fetchrow_array();
474
	$select_topic->finish();
475
    }
476
 
477
    Codestriker::DB::DBI->release_connection($dbh, $success);
478
    die $dbh->errstr unless $success;
479
 
480
    return $count;
481
}
482
 
483
# This function returns the metrics objects that are part of the topic.
484
sub get_metrics {
485
    my ($self) = @_;
486
 
487
    return $self->{metrics};
488
}
489
 
490
# Returns the size of the topic text in lines. If the topic is a diff topic
491
# it attempts to only count the lines that have changed, and not count the
492
# context around the lines.
493
sub get_topic_size_in_lines {
494
 
495
    my ($self) = @_;
496
 
497
    my @deltas = Codestriker::Model::Delta->get_delta_set($self->{topicid}, -1);
498
 
499
    my $line_count = 0;
500
 
501
    foreach my $delta (@deltas)
502
    {
503
        my @document = split /\n/, $delta->{text};
504
 
505
        $line_count += scalar( grep /^[+-][^+-][^+-]/, @document );
506
    }
507
 
508
    return $line_count;
509
}
510
 
511
 
512
# This function is used to create a new topic id. The function insures 
513
# that the new topic id is difficult to guess, and is not taken in the 
514
# database already.
515
sub create_new_topicid {
516
    # For "hysterical" reasons, the topic id is randomly generated.  Seed the
517
    # generator based on the time and the pid.  Keep searching until we find
518
    # a free topicid.  In 99% of the time, we will get a new one first time.
519
    srand(time() ^ ($$ + ($$ << 15)));
520
    my $topicid;
521
    do {
522
	$topicid = int rand(10000000);
523
    } while (Codestriker::Model::Topic::exists($topicid));
524
 
525
    return $topicid;
526
}
527
 
528
# Everytime a topic is stored the version number is incremented. When
529
# a page is created it includes the version number of the topic used
530
# to create the page. The user posts information back to server to
531
# change, the version information needs to be checked to make sure
532
# somebody else has not modified the server.
533
sub check_for_stale($$) {
534
    my ($self, $version) = @_;
535
 
536
    return $self->{version} ne $version;
537
}
538
 
539
# Update the state of the specified topic. 
540
sub change_state($$) {
541
    my ($self, $new_state) = @_;
542
 
543
    my $modified_ts = Codestriker->get_timestamp(time);
544
 
545
    # Map the new state to its number.
546
    my $new_stateid;
547
    for ($new_stateid = 0; $new_stateid <= $#Codestriker::topic_states;
548
	 $new_stateid++) {
549
	last if ($Codestriker::topic_states[$new_stateid] eq $new_state);
550
    }
551
    if ($new_stateid > $#Codestriker::topic_states) {
552
	die "Unable to change topic to invalid state: \"$new_state\"";
553
    }
554
 
555
    # Obtain a database connection.
556
    my $dbh = Codestriker::DB::DBI->get_connection();
557
 
558
    # Check that the version reflects the current version in the DB.  
559
    my $select_topic =
560
	$dbh->prepare_cached('SELECT version ' .
561
			     'FROM topic WHERE id = ?');
562
    my $update_topic =
563
	$dbh->prepare_cached('UPDATE topic SET version = ?, state = ?, ' .
564
			     'modified_ts = ? WHERE id = ?');
565
    my $success = defined $select_topic && defined $update_topic;
566
    my $rc = $Codestriker::OK;
567
 
568
    # Retrieve the current topic data.
569
    $success &&= $select_topic->execute($self->{topicid});
570
 
571
    # Make sure that the topic still exists, and is therefore valid.
572
    my ($current_version);
573
    if ($success && ! (($current_version) =
574
		       $select_topic->fetchrow_array())) {
575
	# Invalid topic id.
576
	$success = 0;
577
	$rc = $Codestriker::INVALID_TOPIC;
578
    }
579
    $success &&= $select_topic->finish();
580
 
581
    # Check the version number.
582
    if ($self->{version} != $current_version) {
583
	$success = 0;
584
	$rc = $Codestriker::STALE_VERSION;
585
    }
586
 
587
    # If the state hasn't changed, don't do anything, otherwise update the
588
    # topic.
589
    if ($new_state ne $self->{topic_state}) {
590
    	$self->{version} = $self->{version} + 1;
591
	$success &&= $update_topic->execute($self->{version}, $new_stateid,
592
					    $modified_ts,
593
					    $self->{topicid});
594
    }
595
 
596
    $self->{modified_ts} = $modified_ts;
597
    $self->{topic_state} = $new_state;
598
 
599
    Codestriker::DB::DBI->release_connection($dbh, $success);
600
    return $rc;
601
}
602
 
603
# Update the properties of the specified topic. This is not implemented
604
# very efficiently, however it is not expected to be called very often.
605
sub update($$$$$$$$$$) {
606
    my ($self, $new_title, $new_author, $new_reviewers, $new_cc,
607
	$new_repository, $new_bug_ids, $new_projectid, $new_description,
608
	$new_state) = @_;
609
 
610
    # First check that the version matches the current topic version in the
611
    # database.
612
    my $dbh = Codestriker::DB::DBI->get_connection();
613
    my $select_topic =
614
	$dbh->prepare_cached('SELECT version ' .
615
			     'FROM topic WHERE id = ?');
616
    my $success = defined $select_topic;
617
    my $rc = $Codestriker::OK;
618
 
619
    # Make sure that the topic still exists, and is therefore valid.
620
    $success &&= $select_topic->execute($self->{topicid});
621
    my $current_version;
622
    if ($success && 
623
	! (($current_version) =
624
	   $select_topic->fetchrow_array())) {
625
	# Invalid topic id.
626
	$success = 0;
627
	$rc = $Codestriker::INVALID_TOPIC;
628
    }
629
    $success &&= $select_topic->finish();
630
 
631
    # Check the version number.
632
    if ($success && $self->{version} != $current_version) {
633
	$success = 0;
634
	$rc = $Codestriker::STALE_VERSION;
635
    }
636
 
637
    # Get the modified date to the current time.
638
    my $modified_ts = Codestriker->get_timestamp(time);
639
 
640
    # Map the new state to its number.
641
    my $new_stateid;
642
    for ($new_stateid = 0; $new_stateid <= $#Codestriker::topic_states;
643
	 $new_stateid++) {
644
	last if ($Codestriker::topic_states[$new_stateid] eq $new_state);
645
    }
646
    if ($new_stateid > $#Codestriker::topic_states) {
647
	die "Unable to change topic to invalid state: \"$new_state\"";
648
    }
649
 
650
    # Update the topic object's properties.
651
    $self->{title} = $new_title;
652
    $self->{author} = $new_author;
653
    $self->{repository} = $new_repository;
654
    $self->{project_id} = $new_projectid;
655
    $self->{description} = $new_description;
656
    $self->{modified_ts} = $modified_ts;
657
    $self->{topic_state} = $new_state;
658
    $self->{topic_state_id} = $new_stateid;
659
 
660
    # Now update the database with the new properties. 
661
    my $update_topic =
662
	$dbh->prepare_cached('UPDATE topic SET version = ?, state = ?, ' .
663
			     'modified_ts = ?, ' .
664
			     'title = ?, author = ?, ' .
665
			     'repository = ?, projectid = ?, ' .
666
			     'description = ? WHERE id = ?');
667
    $success &&= defined $update_topic;
668
 
669
    # If the state hasn't changed, don't do anything, otherwise update the
670
    # topic.
671
    if ($success) {
672
    	$self->{version} = $self->{version} + 1;
673
	$success &&= $update_topic->execute($self->{version}, $new_stateid,
674
					    $modified_ts,
675
					    $new_title, $new_author,
676
					    $new_repository, $new_projectid,
677
					    $new_description,
678
					    $self->{topicid});
679
    }
680
 
681
    # Now delete all bugs associated with this topic, and recreate them again
682
    # if they have changed.
683
    if ($success && $self->{bug_ids} ne $new_bug_ids) {
684
	$success &&= $self->_delete_bug_ids($dbh);
685
	$success &&= $self->_insert_bug_ids($dbh, $new_bug_ids);
686
	$self->{bug_ids} = $new_bug_ids;
687
    }
688
 
689
    # Now delete all reviewers associated with this topic, and recreate
690
    # them again, if they have changed.
691
    if ($success && $self->{reviewers} ne $new_reviewers) {
692
	$success &&=
693
	    $self->_delete_participants($dbh,
694
					$Codestriker::PARTICIPANT_REVIEWER);
695
	$success &&=
696
	    $self->_insert_participants($dbh,
697
					$Codestriker::PARTICIPANT_REVIEWER,
698
					$new_reviewers, $modified_ts);
699
	$self->{reviewers} = $new_reviewers;
700
    }
701
 
702
    # Now delete all CCs associated with this topic, and recreate
703
    # them again, if they have changed.
704
    if ($success && $self->{cc} ne $new_cc) {
705
	$success &&=
706
	    $self->_delete_participants($dbh, $Codestriker::PARTICIPANT_CC);
707
	$success &&=
708
	    $self->_insert_participants($dbh, $Codestriker::PARTICIPANT_CC,
709
					$new_cc, $modified_ts);
710
	$self->{cc} = $new_cc;
711
    }
712
 
713
    Codestriker::DB::DBI->release_connection($dbh, $success);
714
 
715
    if ($success == 0 && $rc == $Codestriker::OK) {
716
	# Unexpected DB error.
717
	die $dbh->errstr;
718
    }
719
 
720
    return $rc;
721
}
722
 
723
# Return back the list of topics which match the specified parameters.
724
sub query($$$$$$$$$$$$$$\@\@\@) {
725
    my ($type, $sauthor, $sreviewer, $scc, $sbugid, $sstate, $sproject, $stext,
726
	$stitle, $sdescription, $scomments, $sbody, $sfilename, $sort_order) = @_;
727
 
728
    # Obtain a database connection.
729
    my $database = Codestriker::DB::Database->get_database();
730
    my $dbh = $database->get_connection();
731
 
732
    # If there are wildcards in the author, reviewer, or CC fields,
733
    # replace them with the appropriate SQL wildcards.
734
    $sauthor =~ s/\*/%/g if $sauthor ne "";
735
    $sreviewer =~ s/\*/%/g if $sreviewer ne "";
736
    $scc =~ s/\*/%/g if $scc ne "";
737
 
738
    # Automatically surround the search term term in wildcards, and replace
739
    # any wildcards appropriately.
740
    if ($stext ne "") {
741
	$stext =~ s/\*/%/g;
742
	if (! ($stext =~ /^%/o) ) {
743
	    $stext = "%${stext}";
744
	}
745
	if (! ($stext =~ /%$/o) ) {
746
	    $stext = "${stext}%";
747
	}
748
    }
749
 
750
    # Build up the query conditions.
751
    my $author_part = $sauthor eq "" ? "" :
752
	$database->case_insensitive_like("topic.author", $sauthor);
753
    my $reviewer_part = $sreviewer eq "" ? "" :
754
	($database->case_insensitive_like("participant.email", $sreviewer) .
755
	 " AND type = $Codestriker::PARTICIPANT_REVIEWER");
756
    my $cc_part = $scc eq "" ? "" :
757
	($database->case_insensitive_like("participant.email", $scc) .
758
	 " AND type = $Codestriker::PARTICIPANT_CC");
759
    my $bugid_part = $sbugid eq "" ? "" :
760
	("topicbug.bugid = " . $dbh->quote($sbugid));
761
 
762
    # Build up the state condition.
763
    my $state_part = "";
764
    if ($sstate ne "") {
765
	$state_part = "topic.state IN ($sstate)";
766
    }
767
 
768
    # Build up the project condition.
769
    my $project_part = "";
770
    if ($sproject ne "") {
771
	$project_part = "topic.projectid IN ($sproject)";
772
    }
773
 
774
    my $text_title_part =
775
	$database->case_insensitive_like("topic.title", $stext);
776
    my $text_description_part =
777
	$database->case_insensitive_like("topic.description", $stext);
778
    my $text_body_part = 
779
	$database->case_insensitive_like("topic.document", $stext);
780
    my $text_filename_part =
781
	$database->case_insensitive_like("topicfile.filename", $stext);
782
    my $text_comment_part =
783
	$database->case_insensitive_like("commentdata.commentfield", $stext);
784
 
785
    # Build up the base query.
786
    my $query =
787
	"SELECT topic.id, topic.title, topic.description, " .
788
	"topic.author, topic.creation_ts, " .
789
	"topic.state, topicbug.bugid, participant.email, participant.type, " .
790
	"topic.version ";
791
 
792
    # Since Oracle < 9i can't handle LEFT OUTER JOIN, determine what tables
793
    # are required in this query and add them in.
794
    my $using_oracle = $Codestriker::db =~ /^DBI:Oracle/i;
795
    if ($using_oracle) {
796
	my @fromlist = ("topic", "topicbug", "participant");
797
	if ($stext ne "" && $scomments) {
798
	    push @fromlist, "commentstate";
799
	    push @fromlist, "commentdata";
800
	}
801
	if ($stext ne "" && $sfilename) {
802
	    push @fromlist, "topicfile";
803
	}
804
	$query .= "FROM " . (join ', ', @fromlist) . " WHERE ";
805
    }
806
    else {
807
	$query .= "FROM topic ";
808
    }
809
 
810
    # Add the join to topicbug and participant.
811
    if ($using_oracle) {
812
	$query .= "topic.id = topicbug.topicid(+) AND " .
813
	    "topic.id = participant.topicid(+) ";
814
    }
815
    else {
816
	$query .= "LEFT OUTER JOIN topicbug ON topic.id = topicbug.topicid " .
817
	"LEFT OUTER JOIN participant ON topic.id = participant.topicid ";
818
    }
819
 
820
    # Join with the comment table if required - GACK!
821
    if ($stext ne "" && $scomments) {
822
	if ($using_oracle) {
823
	    $query .=
824
		' AND topic.id = commentstate.topicid(+) AND '.
825
		'commentstate.id = commentdata.commentstateid(+) ';
826
	}
827
	else {
828
	    $query .= 
829
		'LEFT OUTER JOIN commentstate ON ' .
830
		'topic.id = commentstate.topicid '.
831
		'LEFT OUTER JOIN commentdata ON ' .
832
		'commentstate.id = commentdata.commentstateid ';
833
	}
834
    }
835
 
836
    # Join with the file table if required.
837
    if ($stext ne "" && $sfilename) {
838
	if ($using_oracle) {
839
	    $query .= ' AND topic.id = topicfile.topicid(+) ';
840
	}
841
	else {
842
	    $query .= 'LEFT OUTER JOIN topicfile ON ' .
843
		'topicfile.topicid = topic.id ';
844
	}
845
    }
846
 
847
    # Combine the "AND" conditions together.  Note for Oracle, the 'WHERE'
848
    # keyword has already been used.
849
    my $first_condition = $using_oracle ? 0 : 1;
850
    $query = _add_condition($query, $author_part, \$first_condition);
851
    $query = _add_condition($query, $reviewer_part, \$first_condition);
852
    $query = _add_condition($query, $cc_part, \$first_condition);
853
    $query = _add_condition($query, $bugid_part, \$first_condition);
854
 
855
    # Handle the state set.
856
    if ($state_part ne "") {
857
	$query = _add_condition($query, $state_part, \$first_condition);
858
    }
859
 
860
    # Handle the project set.
861
    if ($project_part ne "") {
862
	$query = _add_condition($query, $project_part, \$first_condition);
863
    }
864
 
865
    # Handle the text searching part, which is a series of ORs.
866
    if ($stext ne "") {
867
	my @text_cond = ();
868
 
869
	push @text_cond, $text_title_part if $stitle;
870
	push @text_cond, $text_description_part if $sdescription;
871
	push @text_cond, $text_body_part if $sbody;
872
	push @text_cond, $text_filename_part if $sfilename;
873
	push @text_cond, $text_comment_part if $scomments;
874
 
875
	if ($#text_cond >= 0) {
876
	    my $cond = join  ' OR ', @text_cond;
877
	    $query = _add_condition($query, $cond, \$first_condition);
878
	}
879
    }
880
 
881
    # Order the result by the creation date field.
882
    if (scalar( @$sort_order ) == 0) {
883
        # no sort order, defaults to topic creation.
884
    $query .= " ORDER BY topic.creation_ts ";
885
    }
886
    else {
887
 
888
        my @sort_terms;
889
 
890
        foreach my $sortItem (@$sort_order) {
891
 
892
            if ($sortItem eq "+title") {
893
                push @sort_terms, "topic.title";
894
            }
895
            elsif ($sortItem eq "-title") {
896
                push @sort_terms, "topic.title DESC";
897
            }
898
            elsif ($sortItem eq "+author") {
899
                push @sort_terms, "topic.author ";
900
            }
901
            elsif ($sortItem eq "-author") {
902
                push @sort_terms, "topic.author DESC";
903
            }
904
            elsif ($sortItem eq "+created") {
905
                push @sort_terms, "topic.creation_ts ";
906
            }
907
            elsif ($sortItem eq "-created") {
908
                push @sort_terms, "topic.creation_ts DESC";
909
            }
910
            elsif ($sortItem eq "+state") {
911
                push @sort_terms, "topic.state ";
912
            }
913
            elsif ($sortItem eq "-state") {
914
                push @sort_terms, "topic.state DESC";
915
            }
916
            else {
917
                die "unknown sort key $sortItem";
918
            }
919
        }
920
 
921
        $query .= " ORDER BY " . join(',',@sort_terms) . " ";
922
    }
923
 
924
    my $select_topic = $dbh->prepare_cached($query);
925
    my $success = defined $select_topic;
926
    $success &&= $select_topic->execute();
927
    my $lastid;
928
    my @topic_list;
929
    if ($success) {
930
	my ($id, $title, $author, $description, $creation_ts, $state, $bugid,
931
	    $email, $type, $version);
932
 
933
	while (($id, $title, $description, $author, $creation_ts, $state,
934
		$bugid, $email, $type, $version) =
935
	       $select_topic->fetchrow_array()) {
936
 
937
            # This is a bit heavy, but the search screen does need much 
938
            # of the information in the topic object, it is much cleaner
939
            # to just return a fully formed topic object, rather than a 
940
            # array tunned. If performace is an issue, then the topic
941
            # object should use lazy instatation to don't pull data from
942
            # the database unless it is needed.
943
            if ( !defined($lastid) || $id ne $lastid ) {
944
 
945
                my $new_topic = Codestriker::Model::Topic->new($id);
946
 
947
                push @topic_list,$new_topic;
948
            }
949
 
950
            $lastid = $id;
951
 
952
    }
953
	$select_topic->finish();
954
    }
955
 
956
    $database->release_connection();
957
    die $dbh->errstr unless $success;
958
 
959
    return @topic_list;
960
}
961
 
962
# Add the condition to the specified query string, returning the new query.
963
sub _add_condition($$\$) {
964
    my ($query, $condition, $first_cond_ref) = @_;
965
 
966
    return $query if ($condition eq ""); # Nothing to do.
967
    if ($$first_cond_ref) {
968
	$$first_cond_ref = 0;
969
	$query .= " WHERE (" . $condition . ") ";
970
    } else {
971
	$query .= " AND (" . $condition . ") ";
972
    }
973
    return $query;
974
}
975
 
976
# Delete the specified topic.
977
sub delete($) {
978
    my ($self) = @_;
979
 
980
    # Obtain a database connection.
981
    my $dbh = Codestriker::DB::DBI->get_connection();
982
 
983
    # Create the prepared statements.
984
    my $delete_topic = $dbh->prepare_cached('DELETE FROM topic WHERE id = ?');
985
    my $select = $dbh->prepare_cached('SELECT id FROM commentstate ' .
986
				      'WHERE topicid = ?');
987
    my $delete_comments =
988
	$dbh->prepare_cached('DELETE FROM commentdata ' .
989
			     'WHERE commentstateid = ?');
990
 
991
    my $delete_commentstate_metric =
992
	$dbh->prepare_cached('DELETE FROM commentstatemetric ' .
993
			     'WHERE id = ?');
994
 
995
    my $delete_commentstate =
996
	$dbh->prepare_cached('DELETE FROM commentstate ' .
997
			     'WHERE topicid = ?');
998
    my $delete_file =
999
	$dbh->prepare_cached('DELETE FROM topicfile WHERE topicid = ?');
1000
 
1001
    my $delete_delta =
1002
	$dbh->prepare_cached('DELETE FROM delta WHERE topicid = ?');
1003
 
1004
    my $topic_metrics =
1005
	$dbh->prepare_cached('DELETE FROM topicmetric WHERE topicid = ?');
1006
 
1007
    my $user_metrics =
1008
	$dbh->prepare_cached('DELETE FROM topicusermetric WHERE topicid = ?');
1009
 
1010
    my $topic_history =
1011
	$dbh->prepare_cached('DELETE FROM topichistory WHERE topicid = ?');
1012
 
1013
    my $topic_view_history =
1014
	$dbh->prepare_cached('DELETE FROM topicviewhistory WHERE topicid = ?');
1015
 
1016
    my $commentstate_history =
1017
	$dbh->prepare_cached('DELETE FROM commentstatehistory WHERE id = ?');
1018
 
1019
    my $obsolete_records =
1020
	$dbh->prepare_cached('DELETE FROM topicobsolete WHERE ' .
1021
			     'topicid = ? OR obsoleted_by = ?');
1022
 
1023
    my $success = defined $delete_topic && defined $delete_comments &&
1024
	defined $delete_commentstate && defined $select &&
1025
	defined $delete_file && defined $delete_delta && 
1026
	defined $topic_metrics && defined $user_metrics &&
1027
	defined $topic_history && defined $topic_view_history &&
1028
	defined $commentstate_history && $delete_commentstate_metric &&
1029
	defined $obsolete_records;
1030
 
1031
    # Now do the deed.
1032
    $success &&= $select->execute($self->{topicid});
1033
    if ($success) {
1034
        foreach my $commentstate (@{$select->fetchall_arrayref()}) {
1035
            my $commentstateid = $commentstate->[0];
1036
	    $success &&= $delete_comments->execute($commentstateid);
1037
	    $success &&= $commentstate_history->execute($commentstateid);
1038
            $success &&= $delete_commentstate_metric->execute($commentstateid);
1039
        }
1040
    }
1041
 
1042
    $success &&= $delete_commentstate->execute($self->{topicid});
1043
    $success &&= $delete_topic->execute($self->{topicid});
1044
    $success &&= $delete_comments->execute($self->{topicid});
1045
    $success &&= $delete_file->execute($self->{topicid});
1046
    $success &&= $delete_delta->execute($self->{topicid});
1047
    $success &&= $topic_metrics->execute($self->{topicid});
1048
    $success &&= $user_metrics->execute($self->{topicid});
1049
    $success &&= $self->_delete_bug_ids($dbh);
1050
    $success &&=
1051
	$self->_delete_participants($dbh, $Codestriker::PARTICIPANT_REVIEWER);
1052
    $success &&=
1053
	$self->_delete_participants($dbh, $Codestriker::PARTICIPANT_CC);
1054
    $success &&= $topic_history->execute($self->{topicid});
1055
    $success &&= $topic_view_history->execute($self->{topicid});
1056
    $success &&= $obsolete_records->execute($self->{topicid},
1057
					    $self->{topicid});
1058
 
1059
    Codestriker::DB::DBI->release_connection($dbh, $success);
1060
 
1061
    # Indicate the success of the operation.
1062
    return $success ? $Codestriker::OK : $Codestriker::INVALID_TOPIC;
1063
}
1064
 
1065
1;