Project

General

Profile

1 4080 daigle
/**
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$'
9
 *     '$Date$'
10
 * '$Revision$'
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 4105 daigle
import java.util.HashMap;
43 4080 daigle
import java.util.HashSet;
44 4105 daigle
import java.util.Map;
45 4080 daigle
import java.util.TreeSet;
46
import java.util.Vector;
47
48
import javax.servlet.ServletException;
49
import javax.servlet.http.HttpServletRequest;
50
import javax.servlet.http.HttpServletResponse;
51
import javax.servlet.http.HttpSession;
52
53 5027 daigle
import edu.ucsb.nceas.metacat.MetacatVersion;
54 5015 daigle
import edu.ucsb.nceas.metacat.database.DBConnection;
55
import edu.ucsb.nceas.metacat.database.DBConnectionPool;
56
import edu.ucsb.nceas.metacat.database.DBVersion;
57 5030 daigle
import edu.ucsb.nceas.metacat.properties.PropertyService;
58 4080 daigle
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 5027 daigle
public class DBAdmin extends MetacatAdmin {
74 4080 daigle
	// 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 4105 daigle
	private Map<String, String> scriptSuffixMap = new HashMap<String, String>();
90 4080 daigle
	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 4105 daigle
			scriptSuffixMap = DatabaseUtil.getScriptSuffixes();
109 4080 daigle
		} catch (PropertyNotFoundException pnfe) {
110 5030 daigle
			throw new AdminException("DBAdmin() - Could not retrieve database upgrade "
111 4080 daigle
					+ "versions during instantiation" + pnfe.getMessage());
112
		} catch (NumberFormatException nfe) {
113 5030 daigle
			throw new AdminException("DBAdmin() - Bad version format numbering: "
114 4080 daigle
					+ 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 5027 daigle
				MetacatVersion metacatVersion = SystemUtil.getMetacatVersion();
159 4080 daigle
160 5027 daigle
				session.setAttribute("metacatVersion", MetacatVersion.getVersionID());
161 4080 daigle
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 5027 daigle
				MetacatVersion metaCatVersion = SystemUtil.getMetacatVersion();
170 4080 daigle
				request.setAttribute("metacatVersion", metaCatVersion);
171
				DBVersion dbVersionString = getDBVersion();
172
				request.setAttribute("databaseVersion", dbVersionString);
173
				Vector<String> updateScriptList = getUpdateScripts();
174
				request.setAttribute("updateScriptList", updateScriptList);
175 4406 daigle
				String supportEmail = PropertyService.getProperty("email.recipient");
176
				request.setAttribute("supportEmail", supportEmail);
177 4080 daigle
178
				// Forward the request to the JSP page
179
				RequestUtil.clearRequestMessages(request);
180
				RequestUtil.forwardRequest(request, response,
181 5027 daigle
						"/admin/database-configuration.jsp", null);
182 4084 daigle
			} catch (GeneralPropertyException gpe) {
183 5030 daigle
				throw new AdminException("DBAdmin.configureDatabase - Problem getting or " +
184
						"setting property while initializing system properties page: " + gpe.getMessage());
185 4080 daigle
			} catch (IOException ioe) {
186 5030 daigle
				throw new AdminException("DBAdmin.configureDatabase - IO problem while initializing "
187 4080 daigle
						+ "system properties page:" + ioe.getMessage());
188
			} catch (ServletException se) {
189 5030 daigle
				throw new AdminException("DBAdmin.configureDatabase - problem forwarding request while "
190 4080 daigle
						+ "initializing system properties page: " + se.getMessage());
191
			}
192
		} else {
193
			// The configuration form is being submitted and needs to be
194
			// processed, setting the properties in the configuration file
195
			// then restart metacat
196
197
			// The configuration form is being submitted and needs to be
198
			// processed.
199
			Vector<String> validationErrors = new Vector<String>();
200
			Vector<String> processingSuccess = new Vector<String>();
201
202
			try {
203
				// Validate that the options provided are legitimate. Note that
204
				// we've allowed them to persist their entries. As of this point
205
				// there is no other easy way to go back to the configure form
206
				// and
207
				// preserve their entries.
208
				validationErrors.addAll(validateOptions(request));
209
210
				upgradeDatabase();
211
212
				// Now that the options have been set, change the
213
				// 'databaseConfigured' option to 'true' so that normal metacat
214
				// requests will go through
215
				PropertyService.setProperty("configutil.databaseConfigured",
216
						PropertyService.CONFIGURED);
217
218
				// Reload the main metacat configuration page
219
				processingSuccess.add("Database successfully upgraded");
220
				RequestUtil.clearRequestMessages(request);
221
				RequestUtil.setRequestSuccess(request, processingSuccess);
222
				RequestUtil.forwardRequest(request, response,
223 5027 daigle
						"/admin?configureType=configure&processForm=false", null);
224 4080 daigle
				// Write out the configurable properties to a backup file
225
				// outside the install directory.
226
227 5028 daigle
				PropertyService.persistMainBackupProperties();
228 4080 daigle
			} catch (GeneralPropertyException gpe) {
229 5030 daigle
				throw new AdminException("DBAdmin.configureDatabase - Problem getting or setting " +
230
						"property while upgrading database: " + gpe.getMessage());
231 4080 daigle
			}  catch (IOException ioe) {
232 5030 daigle
				throw new AdminException("DBAdmin.configureDatabase - IO problem while upgrading database: "
233 4080 daigle
						 + ioe.getMessage());
234
			} catch (ServletException se) {
235 5030 daigle
				throw new AdminException("DBAdmin.configureDatabase - problem forwarding request while "
236 4080 daigle
						+ "upgrading database: " + se.getMessage());
237
			}
238
		}
239
	}
240
241
242
	/**
243
	 * Performs a status check on the database.
244
	 *
245
	 * @returns integer representing the status of the database. These can be:
246
	 * 		-- DB_DOES_NOT_EXIST = 0;
247
	 *      -- TABLES_DO_NOT_EXIST = 1;
248
	 *      -- TABLES_EXIST = 2;
249
	 */
250
	public int getDBStatus() throws SQLException, PropertyNotFoundException {
251
		Connection connection = DBUtil.getConnection(PropertyService
252
				.getProperty("database.connectionURI"), PropertyService
253
				.getProperty("database.user"), PropertyService
254
				.getProperty("database.password"));
255
256
		if (DBUtil.tableExists(connection, "xml_documents")) {
257
			return TABLES_EXIST;
258
		}
259
260
		return TABLES_DO_NOT_EXIST;
261
	}
262
263
	/**
264
	 * Get the version of the database as a string
265
	 *
266
	 * @returns string representing the version of the database.
267
	 */
268
	public DBVersion getDBVersion() throws AdminException {
269
270
		// don't even try to search for a database version until system
271
		// properties have been configured
272
		try {
273 4561 daigle
			if (!PropertyService.arePropertiesConfigured()) {
274 5030 daigle
				throw new AdminException("DBAdmin.getDBVersion - An attempt was made to get " +
275
						"the database version before system properties were configured");
276 4561 daigle
			}
277 5030 daigle
		} catch (GeneralPropertyException gpe) {
278
			throw new AdminException("DBAdmin.getDBVersion - Could not determine the database version: "
279
					+ gpe.getMessage());
280 4080 daigle
		}
281
		if (databaseVersion == null) {
282
			databaseVersion = discoverDBVersion();
283
		}
284
285
		if (databaseVersion == null) {
286 5030 daigle
			throw new AdminException("DBAdmin.getDBVersion - Could not find database version");
287 4080 daigle
		}
288
289
		return databaseVersion;
290
	}
291
292
	/**
293
	 * Try to discover the database version, first by calling
294
	 * getRegisteredDBVersion() to see if the database version is in a table in
295
	 * the database. If not, getUnRegisteredDBVersion() is called to see if we
296
	 * can devine the version by looking for unique changes made by scripts for
297
	 * each version update.
298
	 *
299
	 * @returns string representing the version of the database, null if none
300
	 *          could be found.
301
	 */
302
	private DBVersion discoverDBVersion() throws AdminException {
303
		try {
304
			int dbStatus = getDBStatus();
305
			if (dbStatus == DB_DOES_NOT_EXIST) {
306 5030 daigle
				throw new AdminException("DBAdmin.discoverDBVersion - Database does not exist " +
307
						"for connection" + PropertyService.getProperty("database.connectionURI"));
308 4080 daigle
			} else if (dbStatus == TABLES_DO_NOT_EXIST) {
309
				databaseVersion = new DBVersion("0.0.0");
310
				return databaseVersion;
311
			}
312
313
			databaseVersion = getRegisteredDBVersion();
314
			if (databaseVersion != null) {
315
				return databaseVersion;
316
			}
317
318
			databaseVersion = getUnRegisteredDBVersion();
319
320
		} catch (SQLException sqle) {
321 5030 daigle
			String errorMessage = "DBAdmin.discoverDBVersion - SQL error during  database version discovery: "
322 4080 daigle
				+ sqle.getMessage();
323
			logMetacat.error(errorMessage);
324
			throw new AdminException(errorMessage);
325
		} catch (PropertyNotFoundException pnfe) {
326 5030 daigle
			String errorMessage = "DBAdmin.discoverDBVersion - Property not found during  database " +
327
			"version discovery: " + pnfe.getMessage();
328 4080 daigle
			logMetacat.error(errorMessage);
329
			throw new AdminException(errorMessage);
330
		} catch (NumberFormatException nfe) {
331 5030 daigle
			throw new AdminException("DBAdmin.discoverDBVersion - Bad version format numbering: "
332 4080 daigle
					+ nfe.getMessage());
333
		}
334
335
		if (databaseVersion == null) {
336 5030 daigle
			throw new AdminException("DBAdmin.discoverDBVersion - Database version discovery returned null");
337 4080 daigle
		}
338
		return databaseVersion;
339
	}
340
341
	/**
342
	 * Gets the version of the database from the db_version table. Usually this
343
	 * is the same as the version of the product, however the db version could
344
	 * be more granular if we applied a maintenance patch for instance.
345
	 *
346
	 * @returns string representing the version of the database.
347
	 */
348
	private DBVersion getRegisteredDBVersion() throws AdminException, SQLException {
349
		String dbVersionString = null;
350
		PreparedStatement pstmt = null;
351
352
		try {
353
			// check out DBConnection
354
			Connection connection =
355
				DBUtil.getConnection(
356
						PropertyService.getProperty("database.connectionURI"),
357
						PropertyService.getProperty("database.user"),
358
						PropertyService.getProperty("database.password"));
359
360
			if (!DBUtil.tableExists(connection, "db_version")) {
361
				return null;
362
			}
363
364
			pstmt =
365
				connection.prepareStatement("SELECT version FROM db_version WHERE status = ?");
366
367
			// Bind the values to the query
368
			pstmt.setInt(1, VERSION_ACTIVE);
369
			pstmt.execute();
370
			ResultSet rs = pstmt.getResultSet();
371
			boolean hasRows = rs.next();
372
			if (hasRows) {
373
				dbVersionString = rs.getString(1);
374
			}
375
376
			if (dbVersionString == null) {
377
				return null;
378
			}
379
380
			return new DBVersion(dbVersionString);
381
382
		} catch (SQLException sqle) {
383 5030 daigle
			throw new AdminException("DBAdmin.getRegisteredDBVersion - Could not run SQL to get registered db version: "
384 4080 daigle
					+ sqle.getMessage());
385
		} catch (PropertyNotFoundException pnfe) {
386 5030 daigle
			throw new AdminException("DBAdmin.getRegisteredDBVersion - Could not get property for registered db version: "
387 4080 daigle
					+ pnfe.getMessage());
388
		} catch (NumberFormatException nfe) {
389 5030 daigle
			throw new AdminException("DBAdmin.getRegisteredDBVersion - Bad version format numbering: "
390 4080 daigle
					+ nfe.getMessage());
391
		} finally {
392
			if (pstmt != null) {
393
				pstmt.close();
394
			}
395
		}
396
	}
397
398
	/**
399
	 * Finds the version of the database for a database that does not have a
400
	 * dbVersion table yet. Work backwards with various clues found in update
401
	 * scripts to find the version.
402
	 *
403
	 * @returns string representing the version of the database.
404
	 */
405
	public DBVersion getUnRegisteredDBVersion() throws AdminException, SQLException {
406
		Connection connection = null;
407
		try {
408
			connection = DBUtil.getConnection(PropertyService
409
					.getProperty("database.connectionURI"), PropertyService
410
					.getProperty("database.user"), PropertyService
411
					.getProperty("database.password"));
412
413
			String dbVersionString = null;
414
415 4975 daigle
			if (is1_9_1(connection)) {
416
				dbVersionString = "1.9.1";
417
			} else if (is1_9_0(connection)) {
418 4080 daigle
				dbVersionString = "1.9.0";
419
			} else if (is1_8_0(connection)) {
420
				dbVersionString = "1.8.0";
421
			} else if (is1_7_0(connection)) {
422
				dbVersionString = "1.7.0";
423
			} else if (is1_6_0(connection)) {
424
				dbVersionString = "1.6.0";
425
			} else if (is1_5_0(connection)) {
426
				dbVersionString = "1.5.0";
427
			} else if (is1_4_0(connection)) {
428
				dbVersionString = "1.4.0";
429
			} else if (is1_3_0(connection)) {
430
				dbVersionString = "1.3.0";
431
			} else if (is1_2_0(connection)) {
432
				dbVersionString = "1.2.0";
433
			}
434
435
			if (dbVersionString == null) {
436
				return null;
437
			} else {
438
				return new DBVersion(dbVersionString);
439
			}
440
		} catch (PropertyNotFoundException pnfe) {
441 5030 daigle
			throw new AdminException("DBAdmin.getUnRegisteredDBVersion - Could not get " +
442
					"property for unregistered db version: " + pnfe.getMessage());
443 4080 daigle
		} catch (NumberFormatException nfe) {
444 5030 daigle
			throw new AdminException("DBAdmin.getUnRegisteredDBVersion - Bad version format numbering: "
445 4080 daigle
					+ nfe.getMessage());
446
		}
447
	}
448
449
	/**
450
	 * Updates the version of the database. Typically this is done in the update
451
	 * scripts that get run when we upgrade the application. This method can be
452
	 * used if you are automating a patch on the database internally.
453
	 *
454
	 * @returns string representing the version of the database.
455
	 */
456
	public void updateDBVersion() throws SQLException {
457
		DBConnection conn = null;
458
		PreparedStatement pstmt = null;
459
		int serialNumber = -1;
460
		try {
461
462
			// check out DBConnection
463 5030 daigle
			conn = DBConnectionPool.getDBConnection("DBAdmin.updateDBVersion()");
464 4080 daigle
			serialNumber = conn.getCheckOutSerialNumber();
465
			conn.setAutoCommit(false);
466
467
			pstmt = conn.prepareStatement("UPDATE db_version SET status = ?");
468
			pstmt.setInt(1, VERSION_INACTIVE);
469
			pstmt.execute();
470
			pstmt.close();
471
472
			pstmt = conn.prepareStatement("INSERT INTO db_version "
473
					+ "(version, status, date_created) VALUES (?,?,?)");
474 5027 daigle
			pstmt.setString(1, MetacatVersion.getVersionID());
475 4080 daigle
			pstmt.setInt(2, VERSION_ACTIVE);
476
			pstmt.setTimestamp(3, new Timestamp(new Date().getTime()));
477
			pstmt.execute();
478
479
			conn.commit();
480
		} catch (SQLException e) {
481
			conn.rollback();
482 5030 daigle
			throw new SQLException("DBAdmin.updateDBVersion - sql error: " + e.getMessage());
483 4080 daigle
		} catch (PropertyNotFoundException pnfe) {
484
			conn.rollback();
485 5030 daigle
			throw new SQLException("DBAdmin.updateDBVersion - property error" + pnfe.getMessage());
486 4080 daigle
		}
487
		finally {
488
			try {
489
				pstmt.close();
490
			} finally {
491
				DBConnectionPool.returnDBConnection(conn, serialNumber);
492
			}
493
		}
494
	}
495
496
	/**
497
	 * Validate connectivity to the database. Validation methods return a string
498
	 * error message if there is an issue. This allows the calling code to run
499
	 * several validations and compile the errors into a list that can be
500
	 * displayed on a web page if desired.
501
	 *
502
	 * @param dbDriver
503
	 *            the database driver
504
	 * @param connection
505
	 *            the jdbc connection string
506
	 * @param user
507
	 *            the user name
508
	 * @param password
509
	 *            the login password
510
	 * @return a string holding error message if validation fails.
511
	 */
512
	public String validateDBConnectivity(String dbDriver, String connection,
513
			String user, String password) {
514
		try {
515
			DBConnection.testConnection(dbDriver, connection, user, password);
516
		} catch (SQLException se) {
517
			return "Invalid database credential was provided: "
518
					+ se.getMessage();
519
		}
520
521
		return null;
522
	}
523
524
	/**
525
	 * Checks to see if this is a 1.9.0 database schema by looking for the
526
	 * db_version table which was created for 1.9.0. Note, there is no guarantee
527
	 * that this table will not be removed in subsequent versions. You should
528
	 * search for db versions from newest to oldest, only getting to this
529
	 * function when newer versions have not been matched.
530
	 *
531
	 * @param dbMetaData
532
	 *            the meta data for this database.
533
	 * @returns boolean which is true if table is found, false otherwise
534
	 */
535
	private boolean is1_9_0(Connection connection) throws SQLException {
536
		return DBUtil.tableExists(connection, "db_version");
537
	}
538 4975 daigle
539
	/**
540
	 * Checks to see if this is a 1.9.1 database schema by looking for the
541
	 * scheduled_job table which was created for 1.9.0. Note, there is no guarantee
542
	 * that this table will not be removed in subsequent versions. You should
543
	 * search for db versions from newest to oldest, only getting to this
544
	 * function when newer versions have not been matched.
545
	 *
546
	 * @param dbMetaData
547
	 *            the meta data for this database.
548
	 * @returns boolean which is true if table is found, false otherwise
549
	 */
550
	private boolean is1_9_1(Connection connection) throws SQLException {
551
		return DBUtil.tableExists(connection, "db_version");
552
	}
553 4080 daigle
554
	/**
555
	 * Checks to see if this is a 1.8.0 database schema by looking for the
556
	 * xml_nodes_idx4 index which was created for 1.8.0. Note, there is no
557
	 * guarantee that this index will not be removed in subsequent versions. You
558
	 * should search for db versions from newest to oldest, only getting to this
559
	 * function when newer versions have not been matched.
560
	 *
561
	 * @param dbMetaData
562
	 *            the meta data for this database.
563
	 * @returns boolean which is true if index is found, false otherwise
564
	 */
565 4561 daigle
	private boolean is1_8_0(Connection connection) throws SQLException, PropertyNotFoundException {
566
		String tableName = "xml_nodes";
567
		String dbType = PropertyService.getProperty("database.type");
568
569
		boolean isOracle = dbType.equals("oracle");
570
		if(isOracle) {
571
			tableName = "XML_NODES";
572
		}
573
		return DBUtil.indexExists(connection, tableName, "xml_nodes_idx4");
574 4080 daigle
	}
575
576
	/**
577
	 * Checks to see if this is a 1.7.0 database schema by looking for the
578
	 * xml_documents_idx2 index which was created for 1.7.0. Note, there is no
579
	 * guarantee that this index will not be removed in subsequent versions. You
580
	 * should search for db versions from newest to oldest, only getting to this
581
	 * function when newer versions have not been matched.
582
	 *
583
	 * @param dbMetaData
584
	 *            the meta data for this database.
585
	 * @returns boolean which is true if index is found, false otherwise
586
	 */
587 4561 daigle
	private boolean is1_7_0(Connection connection) throws SQLException, PropertyNotFoundException {
588
		String tableName = "xml_documents";
589
		String dbType = PropertyService.getProperty("database.type");
590
591
		boolean isOracle = dbType.equals("oracle");
592
		if(isOracle) {
593
			tableName = "XML_DOCUMENTS";
594
		}
595
596
		return DBUtil.indexExists(connection, tableName, "xml_documents_idx2");
597 4080 daigle
	}
598
599
	/**
600
	 * Checks to see if this is a 1.6.0 database schema by looking for the
601
	 * identifier table which was created for 1.6.0. Note, there is no guarantee
602
	 * that this table will not be removed in subsequent versions. You should
603
	 * search for db versions from newest to oldest, only getting to this
604
	 * function when newer versions have not been matched.
605
	 *
606
	 * @param dbMetaData
607
	 *            the meta data for this database.
608
	 * @returns boolean which is true if table is found, false otherwise
609
	 */
610
	private boolean is1_6_0(Connection connection) throws SQLException {
611
		return DBUtil.tableExists(connection, "identifier");
612
	}
613
614
	/**
615
	 * Checks to see if this is a 1.5.0 database schema by looking for the
616
	 * xml_returnfield table which was created for 1.5.0. Note, there is no
617
	 * guarantee that this table will not be removed in subsequent versions. You
618
	 * should search for db versions from newest to oldest, only getting to this
619
	 * function when newer versions have not been matched.
620
	 *
621
	 * @param dbMetaData
622
	 *            the meta data for this database.
623
	 * @returns boolean which is true if table is found, false otherwise
624
	 */
625
	private boolean is1_5_0(Connection connection) throws SQLException {
626
		return DBUtil.tableExists(connection, "xml_returnfield");
627
	}
628
629
	/**
630
	 * Checks to see if this is a 1.4.0 database schema by looking for the
631
	 * access_log table which was created for 1.4.0. Note, there is no guarantee
632
	 * that this table will not be removed in subsequent versions. You should
633
	 * search for db versions from newest to oldest, only getting to this
634
	 * function when newer versions have not been matched.
635
	 *
636
	 * @param dbMetaData
637
	 *            the meta data for this database.
638
	 * @returns boolean which is true if table is found, false otherwise
639
	 */
640
	private boolean is1_4_0(Connection connection) throws SQLException {
641
		return DBUtil.tableExists(connection, "access_log");
642
	}
643
644
	/**
645
	 * Checks to see if this is a 1.3.0 database schema by looking for the
646
	 * xml_accesssubtree table which was created for 1.3.0. Note, there is no
647
	 * guarantee that this table will not be removed in subsequent versions. You
648
	 * should search for db versions from newest to oldest, only getting to this
649
	 * function when newer versions have not been matched.
650
	 *
651
	 * @param dbMetaData
652
	 *            the meta data for this database.
653
	 * @returns boolean which is true if table is found, false otherwise
654
	 */
655
	private boolean is1_3_0(Connection connection) throws SQLException {
656
		return DBUtil.tableExists(connection, "xml_accesssubtree");
657
	}
658
659
	/**
660
	 * Checks to see if this is a 1.2.0 database schema by looking for the
661
	 * datareplicate column which was created on the xml_replication table for
662
	 * 1.2.0. Note, there is no guarantee that this column will not be removed
663
	 * in subsequent versions. You should search for db versions from newest to
664
	 * oldest, only getting to this function when newer versions have not been
665
	 * matched.
666
	 *
667
	 * @param dbMetaData
668
	 *            the meta data for this database.
669
	 * @returns boolean which is true if column is found, false otherwise
670
	 */
671
	private boolean is1_2_0(Connection connection) throws SQLException {
672
		return DBUtil.columnExists(connection, "xml_replication",
673
				"datareplicate");
674
	}
675
676
	/**
677
	 * Creates a list of database update script names by looking at the database
678
	 * version and the metacat version and then getting any script that is
679
	 * inbetween the two (inclusive of metacat version).
680
	 *
681
	 * @returns a Vector of Strings holding the names of scripts that need to be
682
	 *          run to get the database updated to this version of metacat
683
	 *
684
	 */
685
	public Vector<String> getUpdateScripts() throws AdminException {
686
		Vector<String> updateScriptList = new Vector<String>();
687
		String sqlFileLocation = null;
688
		String databaseType = null;
689 5027 daigle
		MetacatVersion metaCatVersion = null;
690 4080 daigle
691
		// get the location of sql scripts
692
		try {
693
			metaCatVersion = SystemUtil.getMetacatVersion();
694
			sqlFileLocation = SystemUtil.getSQLDir();
695
			databaseType = PropertyService.getProperty("database.type");
696
		} catch (PropertyNotFoundException pnfe) {
697 5030 daigle
			throw new AdminException("DBAdmin.getUpdateScripts - Could not get property while trying "
698 4080 daigle
					+ "to retrieve database update scripts: " + pnfe.getMessage());
699
		}
700 4105 daigle
701
		// Each type of db has it's own set of scripts.  For instance, Oracle
702
		// scripts end in -oracle.sql.  Postges end in -postgres.sql, etc
703 4155 daigle
		String sqlSuffix = "-" + scriptSuffixMap.get("database.scriptsuffix." + databaseType);
704 4105 daigle
705 4080 daigle
		// if either of these is null, we don't want to do anything.  Just
706
		// return an empty list.
707
		if (metaCatVersion == null || databaseVersion == null) {
708
			return updateScriptList;
709
		}
710
711
		// go through all the versions that the the software went through and
712
		// figure out which ones need to be applied to the database
713
		for (DBVersion nextVersion : versionSet) {
714
			Vector<String> versionUpdateScripts = nextVersion
715
					.getUpdateScripts();
716
717
			// if the database version is 0.0.0, it is new.
718
			// apply all scripts.
719
			if (databaseVersion.getVersionString().equals("0.0.0")
720
					&& nextVersion.getVersionString().equals("0.0.0")) {
721
				for (String versionUpdateScript : versionUpdateScripts) {
722
					updateScriptList.add(sqlFileLocation + FileUtil.getFS()
723
							+ versionUpdateScript + sqlSuffix);
724
				}
725
				return updateScriptList;
726
			}
727
728
			// add every update script that is > than the db version
729
			// but <= to the metacat version to the update list.
730
			if (nextVersion.compareTo(databaseVersion) > 0
731
					&& nextVersion.compareTo(metaCatVersion) <= 0
732
					&& nextVersion.getUpdateScripts() != null) {
733
				for (String versionUpdateScript : versionUpdateScripts) {
734
					updateScriptList.add(sqlFileLocation + FileUtil.getFS()
735
							+ versionUpdateScript + sqlSuffix);
736
				}
737
			}
738
		}
739
740
		// this should now hold all the script names that need to be run
741
		// to bring the database up to date with this version of metacat
742
		return updateScriptList;
743
	}
744
745
	/**
746
	 * Iterates through the list of scripts that need to be run to upgrade
747
	 * the database and calls runSQLFile on each.
748
	 */
749
	public void upgradeDatabase() throws AdminException {
750
		try {
751
			// get a list of the script names that need to be run
752
			Vector<String> updateScriptList = getUpdateScripts();
753
754
			// call runSQLFile on each
755
			for (String updateScript : updateScriptList) {
756
				runSQLFile(updateScript);
757
			}
758
759
			// update the db version to be the metacat version
760
			databaseVersion = new DBVersion(SystemUtil.getMetacatVersion().getVersionString());
761
		} catch (SQLException sqle) {
762 5030 daigle
			throw new AdminException("DBAdmin.upgradeDatabase - SQL error when running upgrade scripts: "
763 4080 daigle
					+ sqle.getMessage());
764
		} catch (PropertyNotFoundException pnfe) {
765 5030 daigle
			throw new AdminException("DBAdmin.upgradeDatabase - SQL error when running upgrade scripts: "
766 4080 daigle
					+ pnfe.getMessage());
767
		}catch (NumberFormatException nfe) {
768 5030 daigle
			throw new AdminException("DBAdmin.upgradeDatabase - Bad version format numbering: "
769 4080 daigle
					+ nfe.getMessage());
770
		}
771
	}
772
773
	/**
774
	 * Runs the commands in a sql script. Individual commands are loaded into a
775
	 * string vector and run one at a time.
776
	 *
777
	 * @param sqlFileName
778
	 *            the name of the file holding the sql statements that need to
779
	 *            get run.
780
	 */
781
	public void runSQLFile(String sqlFileName) throws AdminException, SQLException {
782
783
		// if update file does not exist, do not do the update.
784
		if (FileUtil.getFileStatus(sqlFileName) < FileUtil.EXISTS_READABLE) {
785
			throw new AdminException("Could not read sql update file: "
786
					+ sqlFileName);
787
		}
788
789
		Connection connection = null;
790
		try {
791
			connection = DBUtil.getConnection(PropertyService
792
					.getProperty("database.connectionURI"), PropertyService
793
					.getProperty("database.user"), PropertyService
794
					.getProperty("database.password"));
795
			connection.setAutoCommit(false);
796
797
			// load the sql from the file into a vector of individual statements
798
			// and execute them.
799 5030 daigle
			logMetacat.debug("DBAdmin.runSQLFile - processing File: " + sqlFileName);
800 4080 daigle
			Vector<String> sqlCommands = loadSQLFromFile(sqlFileName);
801
			for (String sqlStatement : sqlCommands) {
802
				Statement statement = connection.createStatement();
803
				logMetacat.debug("executing sql: " + sqlStatement);
804 4400 daigle
				try {
805
					statement.execute(sqlStatement);
806
				} catch (SQLException sqle) {
807
					// Oracle complains if we try and drop a sequence (ORA-02289) or a
808
					// trigger (ORA-04098/ORA-04080) or a table/view (ORA-00942) or and index (ORA-01418)
809
					// that does not exist.  We don't care if this happens.
810
					if (sqlStatement.toUpperCase().startsWith("DROP") &&
811
							(sqle.getMessage().contains("ORA-02289") ||
812
							 sqle.getMessage().contains("ORA-04098") ||
813
							 sqle.getMessage().contains("ORA-04080") ||
814
							 sqle.getMessage().contains("ORA-00942"))) {
815 5030 daigle
						logMetacat.warn("DBAdmin.runSQLFile - did not process sql drop statement: " + sqle.getMessage());
816 4400 daigle
					} else {
817
						throw sqle;
818
					}
819
				}
820 4080 daigle
			}
821
			connection.commit();
822
823
		} catch (IOException ioe) {
824 5030 daigle
			throw new AdminException("DBAdmin.runSQLFile - Could not read SQL file"
825 4080 daigle
					+ ioe.getMessage());
826
		} catch (PropertyNotFoundException pnfe) {
827 5030 daigle
			throw new AdminException("DBAdmin.runSQLFile - Could not find property to run SQL file"
828 4080 daigle
					+ pnfe.getMessage());
829
		} catch (SQLException sqle) {
830
			if (connection != null) {
831
				connection.rollback();
832
			}
833
			throw sqle;
834
		} finally {
835
			if (connection != null) {
836
				connection.close();
837
			}
838
		}
839
	}
840
841
	/**
842
	 * Very basic utility to read sql from a file and return a vector of the
843
	 * individual sql statements. This ignores any line that starts with /* or *.
844
	 * It strips anything following --. Sql is parsed by looking for lines that
845
	 * start with one of the following identifiers: INSERT, UPDATE, ALTER,
846
	 * CREATE, DROP, BEGIN and COMMIT. It then assumes that everything until the
847
	 * line that ends with ; is part of the sql, excluding comments.
848
	 *
849
	 * @param sqlFileName
850
	 *            the name of the file to read.
851
	 * @return a vector holding the individual sql statements.
852
	 */
853
	public Vector<String> loadSQLFromFile(String sqlFileName)
854
			throws IOException {
855
856
		// this will end up holding individual sql statements
857
		Vector<String> sqlCommands = new Vector<String>();
858
859
		FileInputStream fin = null;
860
		try {
861
			fin = new FileInputStream(sqlFileName);
862
863
			BufferedReader reader = new BufferedReader(new InputStreamReader(
864
					fin));
865
866
			// Read in file
867
			String fileLine;
868
			while ((fileLine = reader.readLine()) != null) {
869 4400 daigle
				String endChar = ";";
870 4080 daigle
				String trimmedLine = fileLine.trim();
871
872
				// get the first word on the line
873
				String firstWord = trimmedLine;
874
				if (trimmedLine.indexOf(' ') > 0) {
875
					firstWord = trimmedLine.substring(0, trimmedLine
876
							.indexOf(' '));
877
				}
878 4400 daigle
				if (firstWord.endsWith(endChar)) {
879
					firstWord = firstWord.substring(0, firstWord.indexOf(endChar));
880 4080 daigle
				}
881
882
				// if the first word is a known sql command, start creating a
883
				// sql statement.
884
				if (sqlCommandSet.contains(firstWord.toUpperCase())) {
885 4400 daigle
					String sqlStatement = "";
886 4080 daigle
887
					// keep reading lines until we find one that is not a
888 4400 daigle
					// comment and ends with endChar
889
					do {
890
						String trimmedInnerLine = fileLine.trim();
891
892
						// if there is a BEGIN or DECLARE statement, we are now in plsql and we're
893
						// using the '/' character as our sql end delimiter.
894
						if (trimmedInnerLine.toUpperCase().equals("BEGIN")  ||
895
								trimmedInnerLine.toUpperCase().startsWith("BEGIN ")  ||
896
								trimmedInnerLine.toUpperCase().equals("DECLARE")  ||
897
								trimmedInnerLine.toUpperCase().startsWith("DECLARE ")) {
898
							endChar = "/";
899
						}
900
901 4080 daigle
						// ignore comments and empty lines
902
						if (trimmedInnerLine.matches("^$")
903
								|| trimmedInnerLine.matches("^\\*.*")
904
								|| trimmedInnerLine.matches("/\\*.*")) {
905
							continue;
906
						}
907
908
						// get rid of any "--" comments at the end of the line
909
						if (trimmedInnerLine.indexOf("--") >= 0) {
910
							trimmedInnerLine = trimmedInnerLine.substring(0,
911
									trimmedInnerLine.indexOf("--")).trim();
912
						}
913 4400 daigle
						if (sqlStatement.length() > 0) {
914
							sqlStatement += " ";
915
						}
916
						sqlStatement += trimmedInnerLine;
917
						if (trimmedInnerLine.endsWith(endChar)) {
918
							sqlStatement =
919
								sqlStatement.substring(0, sqlStatement.length() - 1);
920 4080 daigle
							sqlCommands.add(sqlStatement);
921
							break;
922
						}
923 4400 daigle
					} while ((fileLine = reader.readLine()) != null);
924 4080 daigle
				}
925
			}
926
		} finally {
927
			// Close our input stream
928
			fin.close();
929
		}
930
931
		return sqlCommands;
932
	}
933
934
	/**
935
	 * Validate the most important configuration options submitted by the user.
936
	 *
937
	 * @return a vector holding error message for any fields that fail
938
	 *         validation.
939
	 */
940
	protected Vector<String> validateOptions(HttpServletRequest request) {
941
		Vector<String> errorVector = new Vector<String>();
942
943
		// TODO MCD validate options.
944
945
		return errorVector;
946
	}
947
}