Project

General

Profile

1
/**
2
 *  '$RCSfile$'
3
 *    Purpose: A Class that implements database configuration methods
4
 *  Copyright: 2008 Regents of the University of California and the
5
 *             National Center for Ecological Analysis and Synthesis
6
 *    Authors: Michael Daigle
7
 * 
8
 *   '$Author: daigle $'
9
 *     '$Date: 2009-10-06 10:55:18 -0700 (Tue, 06 Oct 2009) $'
10
 * '$Revision: 5076 $'
11
 *
12
 * This program is free software; you can redistribute it and/or modify
13
 * it under the terms of the GNU General Public License as published by
14
 * the Free Software Foundation; either version 2 of the License, or
15
 * (at your option) any later version.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
 * GNU General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU General Public License
23
 * along with this program; if not, write to the Free Software
24
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
25
 */
26

    
27
package edu.ucsb.nceas.metacat.admin;
28

    
29
import java.sql.Connection;
30
import java.sql.PreparedStatement;
31
import java.sql.SQLException;
32
import java.sql.ResultSet;
33
import java.sql.Statement;
34
import java.sql.Timestamp;
35

    
36
import java.io.BufferedReader;
37
import java.io.FileInputStream;
38
import java.io.IOException;
39
import java.io.InputStreamReader;
40

    
41
import java.util.Date;
42
import java.util.HashMap;
43
import java.util.HashSet;
44
import java.util.Map;
45
import java.util.TreeSet;
46
import java.util.Vector;
47

    
48
import javax.servlet.http.HttpServletRequest;
49
import javax.servlet.http.HttpServletResponse;
50
import javax.servlet.http.HttpSession;
51

    
52
import edu.ucsb.nceas.metacat.MetacatVersion;
53
import edu.ucsb.nceas.metacat.database.DBConnection;
54
import edu.ucsb.nceas.metacat.database.DBConnectionPool;
55
import edu.ucsb.nceas.metacat.database.DBVersion;
56
import edu.ucsb.nceas.metacat.properties.PropertyService;
57
import edu.ucsb.nceas.metacat.shared.MetacatUtilException;
58
import edu.ucsb.nceas.metacat.util.DatabaseUtil;
59
import edu.ucsb.nceas.metacat.util.RequestUtil;
60
import edu.ucsb.nceas.metacat.util.SystemUtil;
61

    
62
import edu.ucsb.nceas.utilities.DBUtil;
63
import edu.ucsb.nceas.utilities.FileUtil;
64
import edu.ucsb.nceas.utilities.GeneralPropertyException;
65
import edu.ucsb.nceas.utilities.PropertyNotFoundException;
66

    
67
import org.apache.log4j.Logger;
68

    
69
/**
70
 * Control the display of the database configuration page and the processing
71
 * of the configuration values.
72
 */
73
public class DBAdmin extends MetacatAdmin {
74
	// db statuses used by discovery code
75
	public static final int DB_DOES_NOT_EXIST = 0;
76
	public static final int TABLES_DO_NOT_EXIST = 1;
77
	public static final int TABLES_EXIST = 2;
78

    
79
	// db version statuses. This allows us to keep version history
80
	// in the db. Only the latest version record should be active.
81
	public static final int VERSION_INACTIVE = 0;
82
	public static final int VERSION_ACTIVE = 1;
83

    
84
	private TreeSet<DBVersion> versionSet = null;
85

    
86
	private static DBAdmin dbAdmin = null;
87
	private Logger logMetacat = Logger.getLogger(DBAdmin.class);
88
	private HashSet<String> sqlCommandSet = new HashSet<String>();
89
	private Map<String, String> scriptSuffixMap = new HashMap<String, String>();
90
	private static DBVersion databaseVersion = null;
91

    
92
	/**
93
	 * private constructor since this is a singleton
94
	 */
95
	private DBAdmin() throws AdminException {
96
		sqlCommandSet.add("INSERT");
97
		sqlCommandSet.add("UPDATE");
98
		sqlCommandSet.add("DELETE");
99
		sqlCommandSet.add("ALTER");
100
		sqlCommandSet.add("CREATE");
101
		sqlCommandSet.add("DROP");
102
		sqlCommandSet.add("BEGIN");
103
		sqlCommandSet.add("COMMIT");
104

    
105
		// gets all the upgrade version objects
106
		try {
107
			versionSet = DatabaseUtil.getUpgradeVersions();
108
			scriptSuffixMap = DatabaseUtil.getScriptSuffixes();
109
		} catch (PropertyNotFoundException pnfe) {
110
			throw new AdminException("DBAdmin() - Could not retrieve database upgrade " 
111
					+ "versions during instantiation" + pnfe.getMessage());
112
		} catch (NumberFormatException nfe) {
113
			throw new AdminException("DBAdmin() - Bad version format numbering: "
114
					+ nfe.getMessage());
115
		}
116
	}
117

    
118
	/**
119
	 * Get the single instance of DBAdmin.
120
	 * 
121
	 * @return the single instance of DBAdmin
122
	 */
123
	public static DBAdmin getInstance() throws AdminException {
124
		if (dbAdmin == null) {
125
			dbAdmin = new DBAdmin();
126
		}
127
		return dbAdmin;
128
	}
129

    
130
	/**
131
	 * Handle configuration of the database the first time that Metacat starts
132
	 * or when it is explicitly called. Collect necessary update information
133
	 * from the administrator.
134
	 * 
135
	 * @param request
136
	 *            the http request information
137
	 * @param response
138
	 *            the http response to be sent back to the client
139
	 */
140
	public void configureDatabase(HttpServletRequest request,
141
			HttpServletResponse response) throws AdminException {
142

    
143
		String processForm = request.getParameter("processForm");
144
		String formErrors = (String) request.getAttribute("formErrors");
145
		HttpSession session = request.getSession();
146

    
147
		if (processForm == null || !processForm.equals("true") || formErrors != null) {
148
			// The servlet configuration parameters have not been set, or there
149
			// were form errors on the last attempt to configure, so redirect to
150
			// the web form for configuring metacat
151

    
152
			try {
153
				// get the current metacat version and the database version. If
154
				// the database version is older that the metacat version, run
155
				// the appropriate scripts to get them synchronized.
156

    
157
				databaseVersion = discoverDBVersion();
158
				MetacatVersion metacatVersion = SystemUtil.getMetacatVersion();
159
				
160
				session.setAttribute("metacatVersion", MetacatVersion.getVersionID());
161

    
162
				// if the db version is already the same as the metacat
163
				// version, update metacat.properties
164
				if (databaseVersion.compareTo(metacatVersion) == 0) {
165
					PropertyService.setProperty("configutil.databaseConfigured",
166
							PropertyService.CONFIGURED);
167
				}
168
				
169
				MetacatVersion metaCatVersion = SystemUtil.getMetacatVersion();
170
				request.setAttribute("metacatVersion", metaCatVersion);
171
				DBVersion dbVersionString = getDBVersion();
172
				request.setAttribute("databaseVersion", dbVersionString);
173
				Vector<String> updateScriptList = getUpdateScripts();
174
				request.setAttribute("updateScriptList", updateScriptList);
175
				String supportEmail = PropertyService.getProperty("email.recipient");
176
				request.setAttribute("supportEmail", supportEmail);
177

    
178
				// Forward the request to the JSP page
179
				RequestUtil.clearRequestMessages(request);
180
				RequestUtil.forwardRequest(request, response,
181
						"/admin/database-configuration.jsp", null);
182
			} catch (GeneralPropertyException gpe) {
183
				throw new AdminException("DBAdmin.configureDatabase - Problem getting or " + 
184
						"setting property while initializing system properties page: " + gpe.getMessage());
185
			} catch (MetacatUtilException mue) {
186
				throw new AdminException("DBAdmin.configureDatabase - utility problem while initializing "
187
						+ "system properties page:" + mue.getMessage());
188
			} 
189
		} else {
190
			// The configuration form is being submitted and needs to be
191
			// processed, setting the properties in the configuration file
192
			// then restart metacat
193

    
194
			// The configuration form is being submitted and needs to be
195
			// processed.
196
			Vector<String> validationErrors = new Vector<String>();
197
			Vector<String> processingSuccess = new Vector<String>();
198

    
199
			try {
200
				// Validate that the options provided are legitimate. Note that
201
				// we've allowed them to persist their entries. As of this point
202
				// there is no other easy way to go back to the configure form
203
				// and
204
				// preserve their entries.
205
				validationErrors.addAll(validateOptions(request));
206

    
207
				upgradeDatabase();
208

    
209
				// Now that the options have been set, change the
210
				// 'databaseConfigured' option to 'true' so that normal metacat
211
				// requests will go through
212
				PropertyService.setProperty("configutil.databaseConfigured",
213
						PropertyService.CONFIGURED);
214

    
215
				// Reload the main metacat configuration page
216
				processingSuccess.add("Database successfully upgraded");
217
				RequestUtil.clearRequestMessages(request);
218
				RequestUtil.setRequestSuccess(request, processingSuccess);
219
				RequestUtil.forwardRequest(request, response,
220
						"/admin?configureType=configure&processForm=false", null);
221
				// Write out the configurable properties to a backup file
222
				// outside the install directory.
223

    
224
				PropertyService.persistMainBackupProperties();
225
			} catch (GeneralPropertyException gpe) {
226
				throw new AdminException("DBAdmin.configureDatabase - Problem getting or setting " +
227
						"property while upgrading database: " + gpe.getMessage());
228
			}  catch (MetacatUtilException mue) {
229
				throw new AdminException("DBAdmin.configureDatabase - utility problem while upgrading database: "
230
						 + mue.getMessage());
231
			} 
232
		}
233
	}
234

    
235

    
236
	/**
237
	 * Performs a status check on the database.
238
	 * 
239
	 * @returns integer representing the status of the database. These can be: 
240
	 * 		-- DB_DOES_NOT_EXIST = 0; 
241
	 *      -- TABLES_DO_NOT_EXIST = 1; 
242
	 *      -- TABLES_EXIST = 2;
243
	 */
244
	public int getDBStatus() throws SQLException, PropertyNotFoundException {
245
		Connection connection = DBUtil.getConnection(PropertyService
246
				.getProperty("database.connectionURI"), PropertyService
247
				.getProperty("database.user"), PropertyService
248
				.getProperty("database.password"));
249

    
250
		if (DBUtil.tableExists(connection, "xml_documents")) {
251
			return TABLES_EXIST;
252
		}
253

    
254
		return TABLES_DO_NOT_EXIST;
255
	}
256

    
257
	/**
258
	 * Get the version of the database as a string
259
	 * 
260
	 * @returns string representing the version of the database.
261
	 */
262
	public DBVersion getDBVersion() throws AdminException {
263

    
264
		// don't even try to search for a database version until system
265
		// properties have been configured
266
		try {
267
			if (!PropertyService.arePropertiesConfigured()) {
268
				throw new AdminException("DBAdmin.getDBVersion - An attempt was made to get " + 
269
						"the database version before system properties were configured");
270
			}
271
		} catch (GeneralPropertyException gpe) {
272
			throw new AdminException("DBAdmin.getDBVersion - Could not determine the database version: "
273
					+ gpe.getMessage());
274
		}
275
		if (databaseVersion == null) {
276
			databaseVersion = discoverDBVersion();
277
		}
278

    
279
		if (databaseVersion == null) {
280
			throw new AdminException("DBAdmin.getDBVersion - Could not find database version");
281
		}
282

    
283
		return databaseVersion;
284
	}
285

    
286
	/**
287
	 * Try to discover the database version, first by calling
288
	 * getRegisteredDBVersion() to see if the database version is in a table in
289
	 * the database. If not, getUnRegisteredDBVersion() is called to see if we
290
	 * can devine the version by looking for unique changes made by scripts for
291
	 * each version update.
292
	 * 
293
	 * @returns string representing the version of the database, null if none
294
	 *          could be found.
295
	 */
296
	private DBVersion discoverDBVersion() throws AdminException {
297
		try {
298
			int dbStatus = getDBStatus();
299
			if (dbStatus == DB_DOES_NOT_EXIST) {
300
				throw new AdminException("DBAdmin.discoverDBVersion - Database does not exist " + 
301
						"for connection" + PropertyService.getProperty("database.connectionURI"));
302
			} else if (dbStatus == TABLES_DO_NOT_EXIST) {
303
				databaseVersion = new DBVersion("0.0.0");
304
				return databaseVersion;
305
			}
306

    
307
			databaseVersion = getRegisteredDBVersion();
308
			if (databaseVersion != null) {
309
				return databaseVersion;
310
			}
311

    
312
			databaseVersion = getUnRegisteredDBVersion();
313
			
314
		} catch (SQLException sqle) {
315
			String errorMessage = "DBAdmin.discoverDBVersion - SQL error during  database version discovery: "
316
				+ sqle.getMessage();
317
			logMetacat.error(errorMessage);
318
			throw new AdminException(errorMessage);
319
		} catch (PropertyNotFoundException pnfe) {
320
			String errorMessage = "DBAdmin.discoverDBVersion - Property not found during  database " + 
321
			"version discovery: " + pnfe.getMessage();
322
			logMetacat.error(errorMessage);
323
			throw new AdminException(errorMessage);
324
		} catch (NumberFormatException nfe) {
325
			throw new AdminException("DBAdmin.discoverDBVersion - Bad version format numbering: "
326
					+ nfe.getMessage());
327
		}
328
		
329
		if (databaseVersion == null) {
330
			throw new AdminException("DBAdmin.discoverDBVersion - Database version discovery returned null");
331
		}
332
		return databaseVersion;
333
	}
334

    
335
	/**
336
	 * Gets the version of the database from the db_version table. Usually this
337
	 * is the same as the version of the product, however the db version could
338
	 * be more granular if we applied a maintenance patch for instance.
339
	 * 
340
	 * @returns string representing the version of the database.
341
	 */
342
	private DBVersion getRegisteredDBVersion() throws AdminException, SQLException {
343
		String dbVersionString = null;
344
		PreparedStatement pstmt = null;
345

    
346
		try {
347
			// check out DBConnection
348
			Connection connection = 
349
				DBUtil.getConnection(
350
						PropertyService.getProperty("database.connectionURI"),
351
						PropertyService.getProperty("database.user"),
352
						PropertyService.getProperty("database.password"));
353

    
354
			if (!DBUtil.tableExists(connection, "db_version")) {
355
				return null;
356
			}
357

    
358
			pstmt = 
359
				connection.prepareStatement("SELECT version FROM db_version WHERE status = ?");
360

    
361
			// Bind the values to the query
362
			pstmt.setInt(1, VERSION_ACTIVE);
363
			pstmt.execute();
364
			ResultSet rs = pstmt.getResultSet();
365
			boolean hasRows = rs.next();
366
			if (hasRows) {
367
				dbVersionString = rs.getString(1);
368
			}
369
			
370
			if (dbVersionString == null) {
371
				return null;
372
			} 
373
				
374
			return new DBVersion(dbVersionString);
375
			
376
		} catch (SQLException sqle) {
377
			throw new AdminException("DBAdmin.getRegisteredDBVersion - Could not run SQL to get registered db version: " 
378
					+ sqle.getMessage());			
379
		} catch (PropertyNotFoundException pnfe) {
380
			throw new AdminException("DBAdmin.getRegisteredDBVersion - Could not get property for registered db version: " 
381
					+ pnfe.getMessage());		
382
		} catch (NumberFormatException nfe) {
383
			throw new AdminException("DBAdmin.getRegisteredDBVersion - Bad version format numbering: "
384
					+ nfe.getMessage());
385
		} finally {
386
			if (pstmt != null) {
387
				pstmt.close();
388
			}
389
		}
390
	}
391

    
392
	/**
393
	 * Finds the version of the database for a database that does not have a
394
	 * dbVersion table yet. Work backwards with various clues found in update
395
	 * scripts to find the version.
396
	 * 
397
	 * @returns string representing the version of the database.
398
	 */
399
	public DBVersion getUnRegisteredDBVersion() throws AdminException, SQLException {
400
		Connection connection = null;
401
		try {
402
			connection = DBUtil.getConnection(PropertyService
403
					.getProperty("database.connectionURI"), PropertyService
404
					.getProperty("database.user"), PropertyService
405
					.getProperty("database.password"));
406

    
407
			String dbVersionString = null;
408

    
409
			if (is1_9_1(connection)) {
410
				dbVersionString = "1.9.1";
411
			} else if (is1_9_0(connection)) {
412
				dbVersionString = "1.9.0";
413
			} else if (is1_8_0(connection)) {
414
				dbVersionString = "1.8.0";
415
			} else if (is1_7_0(connection)) {
416
				dbVersionString = "1.7.0";
417
			} else if (is1_6_0(connection)) {
418
				dbVersionString = "1.6.0";
419
			} else if (is1_5_0(connection)) {
420
				dbVersionString = "1.5.0";
421
			} else if (is1_4_0(connection)) {
422
				dbVersionString = "1.4.0";
423
			} else if (is1_3_0(connection)) {
424
				dbVersionString = "1.3.0";
425
			} else if (is1_2_0(connection)) {
426
				dbVersionString = "1.2.0";
427
			}
428

    
429
			if (dbVersionString == null) {
430
				return null;
431
			} else {
432
				return new DBVersion(dbVersionString);
433
			}
434
		} catch (PropertyNotFoundException pnfe) {
435
			throw new AdminException("DBAdmin.getUnRegisteredDBVersion - Could not get " + 
436
					"property for unregistered db version: " + pnfe.getMessage());
437
		} catch (NumberFormatException nfe) {
438
			throw new AdminException("DBAdmin.getUnRegisteredDBVersion - Bad version format numbering: "
439
					+ nfe.getMessage());
440
		}
441
	}
442

    
443
	/**
444
	 * Updates the version of the database. Typically this is done in the update
445
	 * scripts that get run when we upgrade the application. This method can be
446
	 * used if you are automating a patch on the database internally.
447
	 * 
448
	 * @returns string representing the version of the database.
449
	 */
450
	public void updateDBVersion() throws SQLException {
451
		DBConnection conn = null;
452
		PreparedStatement pstmt = null;
453
		int serialNumber = -1;
454
		try {
455

    
456
			// check out DBConnection
457
			conn = DBConnectionPool.getDBConnection("DBAdmin.updateDBVersion()");
458
			serialNumber = conn.getCheckOutSerialNumber();
459
			conn.setAutoCommit(false);
460

    
461
			pstmt = conn.prepareStatement("UPDATE db_version SET status = ?");
462
			pstmt.setInt(1, VERSION_INACTIVE);
463
			pstmt.execute();
464
			pstmt.close();
465

    
466
			pstmt = conn.prepareStatement("INSERT INTO db_version "
467
					+ "(version, status, date_created) VALUES (?,?,?)");
468
			pstmt.setString(1, MetacatVersion.getVersionID());
469
			pstmt.setInt(2, VERSION_ACTIVE);
470
			pstmt.setTimestamp(3, new Timestamp(new Date().getTime()));
471
			pstmt.execute();
472

    
473
			conn.commit();
474
		} catch (SQLException e) {
475
			conn.rollback();
476
			throw new SQLException("DBAdmin.updateDBVersion - sql error: " + e.getMessage());
477
		} catch (PropertyNotFoundException pnfe) {
478
			conn.rollback();
479
			throw new SQLException("DBAdmin.updateDBVersion - property error" + pnfe.getMessage());
480
		}
481
		finally {
482
			try {
483
				pstmt.close();
484
			} finally {
485
				DBConnectionPool.returnDBConnection(conn, serialNumber);
486
			}
487
		}
488
	}
489

    
490
	/**
491
	 * Validate connectivity to the database. Validation methods return a string
492
	 * error message if there is an issue. This allows the calling code to run
493
	 * several validations and compile the errors into a list that can be
494
	 * displayed on a web page if desired.
495
	 * 
496
	 * @param dbDriver
497
	 *            the database driver
498
	 * @param connection
499
	 *            the jdbc connection string
500
	 * @param user
501
	 *            the user name
502
	 * @param password
503
	 *            the login password
504
	 * @return a string holding error message if validation fails.
505
	 */
506
	public String validateDBConnectivity(String dbDriver, String connection,
507
			String user, String password) {
508
		try {
509
			DBConnection.testConnection(dbDriver, connection, user, password);
510
		} catch (SQLException se) {
511
			return "Invalid database credential was provided: "
512
					+ se.getMessage();
513
		}
514

    
515
		return null;
516
	}
517

    
518
	/**
519
	 * Checks to see if this is a 1.9.0 database schema by looking for the
520
	 * db_version table which was created for 1.9.0. Note, there is no guarantee
521
	 * that this table will not be removed in subsequent versions. You should
522
	 * search for db versions from newest to oldest, only getting to this
523
	 * function when newer versions have not been matched.
524
	 * 
525
	 * @param dbMetaData
526
	 *            the meta data for this database.
527
	 * @returns boolean which is true if table is found, false otherwise
528
	 */
529
	private boolean is1_9_0(Connection connection) throws SQLException {
530
		return DBUtil.tableExists(connection, "db_version");
531
	}
532
	
533
	/**
534
	 * Checks to see if this is a 1.9.1 database schema by looking for the
535
	 * scheduled_job table which was created for 1.9.0. Note, there is no guarantee
536
	 * that this table will not be removed in subsequent versions. You should
537
	 * search for db versions from newest to oldest, only getting to this
538
	 * function when newer versions have not been matched.
539
	 * 
540
	 * @param dbMetaData
541
	 *            the meta data for this database.
542
	 * @returns boolean which is true if table is found, false otherwise
543
	 */
544
	private boolean is1_9_1(Connection connection) throws SQLException {
545
		return DBUtil.tableExists(connection, "db_version");
546
	}
547

    
548
	/**
549
	 * Checks to see if this is a 1.8.0 database schema by looking for the
550
	 * xml_nodes_idx4 index which was created for 1.8.0. Note, there is no
551
	 * guarantee that this index will not be removed in subsequent versions. You
552
	 * should search for db versions from newest to oldest, only getting to this
553
	 * function when newer versions have not been matched.
554
	 * 
555
	 * @param dbMetaData
556
	 *            the meta data for this database.
557
	 * @returns boolean which is true if index is found, false otherwise
558
	 */
559
	private boolean is1_8_0(Connection connection) throws SQLException, PropertyNotFoundException {
560
		String tableName = "xml_nodes";
561
		String dbType = PropertyService.getProperty("database.type");
562
			
563
		boolean isOracle = dbType.equals("oracle");		
564
		if(isOracle) {
565
			tableName = "XML_NODES";
566
		}
567
		return DBUtil.indexExists(connection, tableName, "xml_nodes_idx4");
568
	}
569

    
570
	/**
571
	 * Checks to see if this is a 1.7.0 database schema by looking for the
572
	 * xml_documents_idx2 index which was created for 1.7.0. Note, there is no
573
	 * guarantee that this index will not be removed in subsequent versions. You
574
	 * should search for db versions from newest to oldest, only getting to this
575
	 * function when newer versions have not been matched.
576
	 * 
577
	 * @param dbMetaData
578
	 *            the meta data for this database.
579
	 * @returns boolean which is true if index is found, false otherwise
580
	 */
581
	private boolean is1_7_0(Connection connection) throws SQLException, PropertyNotFoundException {
582
		String tableName = "xml_documents";
583
		String dbType = PropertyService.getProperty("database.type");
584
			
585
		boolean isOracle = dbType.equals("oracle");		
586
		if(isOracle) {
587
			tableName = "XML_DOCUMENTS";
588
		}
589
	
590
		return DBUtil.indexExists(connection, tableName, "xml_documents_idx2");
591
	}
592

    
593
	/**
594
	 * Checks to see if this is a 1.6.0 database schema by looking for the
595
	 * identifier table which was created for 1.6.0. Note, there is no guarantee
596
	 * that this table will not be removed in subsequent versions. You should
597
	 * search for db versions from newest to oldest, only getting to this
598
	 * function when newer versions have not been matched.
599
	 * 
600
	 * @param dbMetaData
601
	 *            the meta data for this database.
602
	 * @returns boolean which is true if table is found, false otherwise
603
	 */
604
	private boolean is1_6_0(Connection connection) throws SQLException {
605
		return DBUtil.tableExists(connection, "identifier");
606
	}
607

    
608
	/**
609
	 * Checks to see if this is a 1.5.0 database schema by looking for the
610
	 * xml_returnfield table which was created for 1.5.0. Note, there is no
611
	 * guarantee that this table will not be removed in subsequent versions. You
612
	 * should search for db versions from newest to oldest, only getting to this
613
	 * function when newer versions have not been matched.
614
	 * 
615
	 * @param dbMetaData
616
	 *            the meta data for this database.
617
	 * @returns boolean which is true if table is found, false otherwise
618
	 */
619
	private boolean is1_5_0(Connection connection) throws SQLException {
620
		return DBUtil.tableExists(connection, "xml_returnfield");
621
	}
622

    
623
	/**
624
	 * Checks to see if this is a 1.4.0 database schema by looking for the
625
	 * access_log table which was created for 1.4.0. Note, there is no guarantee
626
	 * that this table will not be removed in subsequent versions. You should
627
	 * search for db versions from newest to oldest, only getting to this
628
	 * function when newer versions have not been matched.
629
	 * 
630
	 * @param dbMetaData
631
	 *            the meta data for this database.
632
	 * @returns boolean which is true if table is found, false otherwise
633
	 */
634
	private boolean is1_4_0(Connection connection) throws SQLException {
635
		return DBUtil.tableExists(connection, "access_log");
636
	}
637

    
638
	/**
639
	 * Checks to see if this is a 1.3.0 database schema by looking for the
640
	 * xml_accesssubtree table which was created for 1.3.0. Note, there is no
641
	 * guarantee that this table will not be removed in subsequent versions. You
642
	 * should search for db versions from newest to oldest, only getting to this
643
	 * function when newer versions have not been matched.
644
	 * 
645
	 * @param dbMetaData
646
	 *            the meta data for this database.
647
	 * @returns boolean which is true if table is found, false otherwise
648
	 */
649
	private boolean is1_3_0(Connection connection) throws SQLException {
650
		return DBUtil.tableExists(connection, "xml_accesssubtree");
651
	}
652

    
653
	/**
654
	 * Checks to see if this is a 1.2.0 database schema by looking for the
655
	 * datareplicate column which was created on the xml_replication table for
656
	 * 1.2.0. Note, there is no guarantee that this column will not be removed
657
	 * in subsequent versions. You should search for db versions from newest to
658
	 * oldest, only getting to this function when newer versions have not been
659
	 * matched.
660
	 * 
661
	 * @param dbMetaData
662
	 *            the meta data for this database.
663
	 * @returns boolean which is true if column is found, false otherwise
664
	 */
665
	private boolean is1_2_0(Connection connection) throws SQLException {
666
		return DBUtil.columnExists(connection, "xml_replication",
667
				"datareplicate");
668
	}
669

    
670
	/**
671
	 * Creates a list of database update script names by looking at the database
672
	 * version and the metacat version and then getting any script that is
673
	 * inbetween the two (inclusive of metacat version).
674
	 * 
675
	 * @returns a Vector of Strings holding the names of scripts that need to be
676
	 *          run to get the database updated to this version of metacat
677
	 * 
678
	 */
679
	public Vector<String> getUpdateScripts() throws AdminException {
680
		Vector<String> updateScriptList = new Vector<String>();
681
		String sqlFileLocation = null;
682
		String databaseType = null;
683
		MetacatVersion metaCatVersion = null; 
684
		
685
		// get the location of sql scripts
686
		try {
687
			metaCatVersion = SystemUtil.getMetacatVersion();
688
			sqlFileLocation = SystemUtil.getSQLDir();
689
			databaseType = PropertyService.getProperty("database.type");
690
		} catch (PropertyNotFoundException pnfe) {
691
			throw new AdminException("DBAdmin.getUpdateScripts - Could not get property while trying " 
692
					+ "to retrieve database update scripts: " + pnfe.getMessage());
693
		}
694
		
695
		// Each type of db has it's own set of scripts.  For instance, Oracle
696
		// scripts end in -oracle.sql.  Postges end in -postgres.sql, etc
697
		String sqlSuffix = "-" + scriptSuffixMap.get("database.scriptsuffix." + databaseType);
698
		
699
		// if either of these is null, we don't want to do anything.  Just 
700
		// return an empty list.
701
		if (metaCatVersion == null || databaseVersion == null) {
702
			return updateScriptList;
703
		}
704

    
705
		// go through all the versions that the the software went through and 
706
		// figure out which ones need to be applied to the database	
707
		for (DBVersion nextVersion : versionSet) {
708
			Vector<String> versionUpdateScripts = nextVersion
709
					.getUpdateScripts();
710
			
711
			// if the database version is 0.0.0, it is new.
712
			// apply all scripts.
713
			if (databaseVersion.getVersionString().equals("0.0.0")
714
					&& nextVersion.getVersionString().equals("0.0.0")) {
715
				for (String versionUpdateScript : versionUpdateScripts) {
716
					updateScriptList.add(sqlFileLocation + FileUtil.getFS()
717
							+ versionUpdateScript + sqlSuffix);
718
				}
719
				return updateScriptList;
720
			}
721

    
722
			// add every update script that is > than the db version
723
			// but <= to the metacat version to the update list.
724
			if (nextVersion.compareTo(databaseVersion) > 0
725
					&& nextVersion.compareTo(metaCatVersion) <= 0
726
					&& nextVersion.getUpdateScripts() != null) {
727
				for (String versionUpdateScript : versionUpdateScripts) {
728
					updateScriptList.add(sqlFileLocation + FileUtil.getFS()
729
							+ versionUpdateScript + sqlSuffix);
730
				}
731
			}
732
		}
733

    
734
		// this should now hold all the script names that need to be run
735
		// to bring the database up to date with this version of metacat
736
		return updateScriptList;
737
	}
738

    
739
	/**
740
	 * Iterates through the list of scripts that need to be run to upgrade
741
	 * the database and calls runSQLFile on each.
742
	 */
743
	public void upgradeDatabase() throws AdminException {
744
		try {
745
			// get a list of the script names that need to be run
746
			Vector<String> updateScriptList = getUpdateScripts();
747

    
748
			// call runSQLFile on each
749
			for (String updateScript : updateScriptList) {
750
				runSQLFile(updateScript);
751
			}
752

    
753
			// update the db version to be the metacat version
754
			databaseVersion = new DBVersion(SystemUtil.getMetacatVersion().getVersionString());
755
		} catch (SQLException sqle) {
756
			throw new AdminException("DBAdmin.upgradeDatabase - SQL error when running upgrade scripts: "
757
					+ sqle.getMessage());
758
		} catch (PropertyNotFoundException pnfe) {
759
			throw new AdminException("DBAdmin.upgradeDatabase - SQL error when running upgrade scripts: "
760
					+ pnfe.getMessage());
761
		}catch (NumberFormatException nfe) {
762
			throw new AdminException("DBAdmin.upgradeDatabase - Bad version format numbering: "
763
					+ nfe.getMessage());
764
		}
765
	}
766

    
767
	/**
768
	 * Runs the commands in a sql script. Individual commands are loaded into a
769
	 * string vector and run one at a time.
770
	 * 
771
	 * @param sqlFileName
772
	 *            the name of the file holding the sql statements that need to
773
	 *            get run.
774
	 */
775
	public void runSQLFile(String sqlFileName) throws AdminException, SQLException {
776

    
777
		// if update file does not exist, do not do the update.
778
		if (FileUtil.getFileStatus(sqlFileName) < FileUtil.EXISTS_READABLE) {
779
			throw new AdminException("Could not read sql update file: "
780
					+ sqlFileName);
781
		}
782

    
783
		Connection connection = null;
784
		try {
785
			connection = DBUtil.getConnection(PropertyService
786
					.getProperty("database.connectionURI"), PropertyService
787
					.getProperty("database.user"), PropertyService
788
					.getProperty("database.password"));
789
			connection.setAutoCommit(false);
790

    
791
			// load the sql from the file into a vector of individual statements
792
			// and execute them.
793
			logMetacat.debug("DBAdmin.runSQLFile - processing File: " + sqlFileName);
794
			Vector<String> sqlCommands = loadSQLFromFile(sqlFileName);
795
			for (String sqlStatement : sqlCommands) {
796
				Statement statement = connection.createStatement();
797
				logMetacat.debug("executing sql: " + sqlStatement);
798
				try {
799
					statement.execute(sqlStatement);
800
				} catch (SQLException sqle) {
801
					// Oracle complains if we try and drop a sequence (ORA-02289) or a 
802
					// trigger (ORA-04098/ORA-04080) or a table/view (ORA-00942) or and index (ORA-01418) 
803
					// that does not exist.  We don't care if this happens.
804
					if (sqlStatement.toUpperCase().startsWith("DROP") && 
805
							(sqle.getMessage().contains("ORA-02289") ||
806
							 sqle.getMessage().contains("ORA-04098") ||
807
							 sqle.getMessage().contains("ORA-04080") ||
808
							 sqle.getMessage().contains("ORA-00942"))) {
809
						logMetacat.warn("DBAdmin.runSQLFile - did not process sql drop statement: " + sqle.getMessage());
810
					} else {
811
						throw sqle;
812
					}
813
				}
814
			}
815
			connection.commit();
816
			
817
		} catch (IOException ioe) {
818
			throw new AdminException("DBAdmin.runSQLFile - Could not read SQL file" 
819
					+ ioe.getMessage());
820
		} catch (PropertyNotFoundException pnfe) {
821
			throw new AdminException("DBAdmin.runSQLFile - Could not find property to run SQL file" 
822
					+ pnfe.getMessage());
823
		} catch (SQLException sqle) {
824
			if (connection != null) {
825
				connection.rollback();
826
			}
827
			throw sqle;
828
		} finally {
829
			if (connection != null) {
830
				connection.close();
831
			}
832
		}
833
	}
834

    
835
	/**
836
	 * Very basic utility to read sql from a file and return a vector of the
837
	 * individual sql statements. This ignores any line that starts with /* or *.
838
	 * It strips anything following --. Sql is parsed by looking for lines that
839
	 * start with one of the following identifiers: INSERT, UPDATE, ALTER,
840
	 * CREATE, DROP, BEGIN and COMMIT. It then assumes that everything until the
841
	 * line that ends with ; is part of the sql, excluding comments.
842
	 * 
843
	 * @param sqlFileName
844
	 *            the name of the file to read.
845
	 * @return a vector holding the individual sql statements.
846
	 */
847
	public Vector<String> loadSQLFromFile(String sqlFileName)
848
			throws IOException {
849

    
850
		// this will end up holding individual sql statements
851
		Vector<String> sqlCommands = new Vector<String>();
852

    
853
		FileInputStream fin = null;
854
		try {
855
			fin = new FileInputStream(sqlFileName);
856

    
857
			BufferedReader reader = new BufferedReader(new InputStreamReader(
858
					fin));
859

    
860
			// Read in file
861
			String fileLine;
862
			while ((fileLine = reader.readLine()) != null) {
863
				String endChar = ";";
864
				String trimmedLine = fileLine.trim();
865

    
866
				// get the first word on the line
867
				String firstWord = trimmedLine;
868
				if (trimmedLine.indexOf(' ') > 0) {
869
					firstWord = trimmedLine.substring(0, trimmedLine
870
							.indexOf(' '));
871
				}
872
				if (firstWord.endsWith(endChar)) {
873
					firstWord = firstWord.substring(0, firstWord.indexOf(endChar));
874
				}
875

    
876
				// if the first word is a known sql command, start creating a
877
				// sql statement.
878
				if (sqlCommandSet.contains(firstWord.toUpperCase())) {
879
					String sqlStatement = "";
880

    
881
					// keep reading lines until we find one that is not a
882
					// comment and ends with endChar
883
					do {
884
						String trimmedInnerLine = fileLine.trim();
885
						
886
						// if there is a BEGIN or DECLARE statement, we are now in plsql and we're 
887
						// using the '/' character as our sql end delimiter.
888
						if (trimmedInnerLine.toUpperCase().equals("BEGIN")  ||
889
								trimmedInnerLine.toUpperCase().startsWith("BEGIN ")  ||
890
								trimmedInnerLine.toUpperCase().equals("DECLARE")  ||
891
								trimmedInnerLine.toUpperCase().startsWith("DECLARE ")) {
892
							endChar = "/";
893
						}
894
						
895
						// ignore comments and empty lines
896
						if (trimmedInnerLine.matches("^$")
897
								|| trimmedInnerLine.matches("^\\*.*")
898
								|| trimmedInnerLine.matches("/\\*.*")) {
899
							continue;
900
						}
901

    
902
						// get rid of any "--" comments at the end of the line
903
						if (trimmedInnerLine.indexOf("--") >= 0) {
904
							trimmedInnerLine = trimmedInnerLine.substring(0,
905
									trimmedInnerLine.indexOf("--")).trim();
906
						}
907
						if (sqlStatement.length() > 0) {
908
							sqlStatement += " ";
909
						}
910
						sqlStatement += trimmedInnerLine;
911
						if (trimmedInnerLine.endsWith(endChar)) {
912
							sqlStatement = 
913
								sqlStatement.substring(0, sqlStatement.length() - 1);
914
							sqlCommands.add(sqlStatement);
915
							break;
916
						}
917
					} while ((fileLine = reader.readLine()) != null);
918
				}
919
			}
920
		} finally {
921
			// Close our input stream
922
			fin.close();
923
		}
924

    
925
		return sqlCommands;
926
	}
927

    
928
	/**
929
	 * Validate the most important configuration options submitted by the user.
930
	 * 
931
	 * @return a vector holding error message for any fields that fail
932
	 *         validation.
933
	 */
934
	protected Vector<String> validateOptions(HttpServletRequest request) {
935
		Vector<String> errorVector = new Vector<String>();
936

    
937
		// TODO MCD validate options.
938

    
939
		return errorVector;
940
	}
941
}
(4-4/10)