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