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: jones $'
9
 *     '$Date: 2013-10-09 23:52:11 -0700 (Wed, 09 Oct 2013) $'
10
 * '$Revision: 8304 $'
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.admin.upgrade.UpgradeUtilityInterface;
54
import edu.ucsb.nceas.metacat.database.DBConnection;
55
import edu.ucsb.nceas.metacat.database.DBConnectionPool;
56
import edu.ucsb.nceas.metacat.database.DBVersion;
57
import edu.ucsb.nceas.metacat.properties.PropertyService;
58
import edu.ucsb.nceas.metacat.shared.MetacatUtilException;
59
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.commons.io.IOUtils;
69
import org.apache.log4j.Logger;
70

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

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

    
86
	private TreeSet<DBVersion> versionSet = null;
87

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

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

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

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

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

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

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

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

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

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

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

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

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

    
209
				upgradeDatabase();
210

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

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

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

    
237

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

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

    
256
		return TABLES_DO_NOT_EXIST;
257
	}
258

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

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

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

    
285
		return databaseVersion;
286
	}
287

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

    
309
			databaseVersion = getRegisteredDBVersion();
310
			if (databaseVersion != null) {
311
				return databaseVersion;
312
			}
313

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

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

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

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

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

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

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

    
409
			String dbVersionString = null;
410

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

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

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

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

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

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

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

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

    
517
		return null;
518
	}
519

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

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

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

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

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

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

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

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

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

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

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

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

    
741
	public Vector<String> getUpdateClasses() throws AdminException {
742
		Vector<String> updateClassList = new Vector<String>();
743
		MetacatVersion metaCatVersion = null; 
744
		
745
		// get the location of sql scripts
746
		try {
747
			metaCatVersion = SystemUtil.getMetacatVersion();
748
		} catch (PropertyNotFoundException pnfe) {
749
			throw new AdminException("DBAdmin.getUpdateScripts - Could not get property while trying " 
750
					+ "to retrieve update utilities: " + pnfe.getMessage());
751
		}
752
		
753
		// if either of these is null, we don't want to do anything.  Just 
754
		// return an empty list.
755
		if (metaCatVersion == null || databaseVersion == null) {
756
			return updateClassList;
757
		}
758

    
759
		// go through all the versions that the the software went through and 
760
		// figure out which ones need to be applied to the database	
761
		for (DBVersion nextVersion : versionSet) {
762

    
763
			// add every update script that is > than the db version
764
			// but <= to the metacat version to the update list.
765
			if (nextVersion.compareTo(databaseVersion) > 0
766
					&& nextVersion.compareTo(metaCatVersion) <= 0) {
767
				String key = "database.upgradeUtility." + nextVersion.getVersionString();
768
				String className = null;
769
				try {
770
					className = PropertyService.getProperty(key);
771
				} catch (PropertyNotFoundException pnfe) {
772
					// there probably isn't a utility needed for this version
773
					logMetacat.warn("No utility defined for version: " + key);
774
					continue;
775
				}
776
				updateClassList.add(className);
777
			}
778
		}
779

    
780
		// this should now hold all the script names that need to be run
781
		// to bring the database up to date with this version of metacat
782
		return updateClassList;
783
	}
784
	
785
	/**
786
	 * Iterates through the list of scripts that need to be run to upgrade
787
	 * the database and calls runSQLFile on each.
788
	 */
789
	public void upgradeDatabase() throws AdminException {
790
		try {
791
			// get a list of the script names that need to be run
792
			Vector<String> updateScriptList = getUpdateScripts();
793

    
794
			// call runSQLFile on each
795
			for (String updateScript : updateScriptList) {
796
				runSQLFile(updateScript);
797
			}
798
			
799
			// get the classes we need to execute in order to bring DB to current version
800
			Vector<String> updateClassList = getUpdateClasses();
801
			for (String className : updateClassList) {
802
				UpgradeUtilityInterface utility = null;
803
				try {
804
					utility = (UpgradeUtilityInterface) Class.forName(className).newInstance();
805
					utility.upgrade();
806
				} catch (Exception e) {
807
					throw new AdminException("DBAdmin.upgradeDatabase - error getting utility class: " 
808
							+ className + ". Error message: "
809
							+ e.getMessage());
810
				}
811
			}
812

    
813
			// update the db version to be the metacat version
814
			databaseVersion = new DBVersion(SystemUtil.getMetacatVersion().getVersionString());
815
		} catch (SQLException sqle) {
816
			throw new AdminException("DBAdmin.upgradeDatabase - SQL error when running upgrade scripts: "
817
					+ sqle.getMessage());
818
		} catch (PropertyNotFoundException pnfe) {
819
			throw new AdminException("DBAdmin.upgradeDatabase - SQL error when running upgrade scripts: "
820
					+ pnfe.getMessage());
821
		}catch (NumberFormatException nfe) {
822
			throw new AdminException("DBAdmin.upgradeDatabase - Bad version format numbering: "
823
					+ nfe.getMessage());
824
		}
825
	}
826

    
827
	/**
828
	 * Runs the commands in a sql script. Individual commands are loaded into a
829
	 * string vector and run one at a time.
830
	 * 
831
	 * @param sqlFileName
832
	 *            the name of the file holding the sql statements that need to
833
	 *            get run.
834
	 */
835
	public void runSQLFile(String sqlFileName) throws AdminException, SQLException {
836

    
837
		// if update file does not exist, do not do the update.
838
		if (FileUtil.getFileStatus(sqlFileName) < FileUtil.EXISTS_READABLE) {
839
			throw new AdminException("Could not read sql update file: "
840
					+ sqlFileName);
841
		}
842

    
843
		Connection connection = null;
844
		try {
845
			connection = DBUtil.getConnection(PropertyService
846
					.getProperty("database.connectionURI"), PropertyService
847
					.getProperty("database.user"), PropertyService
848
					.getProperty("database.password"));
849
			connection.setAutoCommit(false);
850

    
851
			// load the sql from the file into a vector of individual statements
852
			// and execute them.
853
			logMetacat.debug("DBAdmin.runSQLFile - processing File: " + sqlFileName);
854
			Vector<String> sqlCommands = loadSQLFromFile(sqlFileName);
855
			for (String sqlStatement : sqlCommands) {
856
				Statement statement = connection.createStatement();
857
				logMetacat.debug("executing sql: " + sqlStatement);
858
				try {
859
					statement.execute(sqlStatement);
860
				} catch (SQLException sqle) {
861
					// Oracle complains if we try and drop a sequence (ORA-02289) or a 
862
					// trigger (ORA-04098/ORA-04080) or a table/view (ORA-00942) or and index (ORA-01418) 
863
					// that does not exist.  We don't care if this happens.
864
					if (sqlStatement.toUpperCase().startsWith("DROP") && 
865
							(sqle.getMessage().contains("ORA-02289") ||
866
							 sqle.getMessage().contains("ORA-04098") ||
867
							 sqle.getMessage().contains("ORA-04080") ||
868
							 sqle.getMessage().contains("ORA-00942"))) {
869
						logMetacat.warn("DBAdmin.runSQLFile - did not process sql drop statement: " + sqle.getMessage());
870
					} else {
871
						throw sqle;
872
					}
873
				}
874
			}
875
			connection.commit();
876
			
877
		} catch (IOException ioe) {
878
			throw new AdminException("DBAdmin.runSQLFile - Could not read SQL file" 
879
					+ ioe.getMessage());
880
		} catch (PropertyNotFoundException pnfe) {
881
			throw new AdminException("DBAdmin.runSQLFile - Could not find property to run SQL file" 
882
					+ pnfe.getMessage());
883
		} catch (SQLException sqle) {
884
			if (connection != null) {
885
				connection.rollback();
886
			}
887
			throw sqle;
888
		} finally {
889
			if (connection != null) {
890
				connection.close();
891
			}
892
		}
893
	}
894

    
895
	/**
896
	 * Very basic utility to read sql from a file and return a vector of the
897
	 * individual sql statements. This ignores any line that starts with /* or *.
898
	 * It strips anything following --. Sql is parsed by looking for lines that
899
	 * start with one of the following identifiers: INSERT, UPDATE, ALTER,
900
	 * CREATE, DROP, BEGIN and COMMIT. It then assumes that everything until the
901
	 * line that ends with ; is part of the sql, excluding comments.
902
	 * 
903
	 * @param sqlFileName
904
	 *            the name of the file to read.
905
	 * @return a vector holding the individual sql statements.
906
	 */
907
	public Vector<String> loadSQLFromFile(String sqlFileName)
908
			throws IOException {
909

    
910
		// this will end up holding individual sql statements
911
		Vector<String> sqlCommands = new Vector<String>();
912

    
913
		FileInputStream fin = null;
914
		try {
915
			fin = new FileInputStream(sqlFileName);
916

    
917
			BufferedReader reader = new BufferedReader(new InputStreamReader(
918
					fin));
919

    
920
			// Read in file
921
			String fileLine;
922
			while ((fileLine = reader.readLine()) != null) {
923
				String endChar = ";";
924
				String trimmedLine = fileLine.trim();
925

    
926
				// get the first word on the line
927
				String firstWord = trimmedLine;
928
				if (trimmedLine.indexOf(' ') > 0) {
929
					firstWord = trimmedLine.substring(0, trimmedLine
930
							.indexOf(' '));
931
				}
932
				if (firstWord.endsWith(endChar)) {
933
					firstWord = firstWord.substring(0, firstWord.indexOf(endChar));
934
				}
935

    
936
				// if the first word is a known sql command, start creating a
937
				// sql statement.
938
				if (sqlCommandSet.contains(firstWord.toUpperCase())) {
939
					String sqlStatement = "";
940

    
941
					// keep reading lines until we find one that is not a
942
					// comment and ends with endChar
943
					do {
944
						String trimmedInnerLine = fileLine.trim();
945
						
946
						// if there is a BEGIN or DECLARE statement, we are now in plsql and we're 
947
						// using the '/' character as our sql end delimiter.
948
						if (trimmedInnerLine.toUpperCase().equals("BEGIN")  ||
949
								trimmedInnerLine.toUpperCase().startsWith("BEGIN ")  ||
950
								trimmedInnerLine.toUpperCase().equals("DECLARE")  ||
951
								trimmedInnerLine.toUpperCase().startsWith("DECLARE ")) {
952
							endChar = "/";
953
						}
954
						
955
						// ignore comments and empty lines
956
						if (trimmedInnerLine.matches("^$")
957
								|| trimmedInnerLine.matches("^\\*.*")
958
								|| trimmedInnerLine.matches("/\\*.*")) {
959
							continue;
960
						}
961

    
962
						// get rid of any "--" comments at the end of the line
963
						if (trimmedInnerLine.indexOf("--") >= 0) {
964
							trimmedInnerLine = trimmedInnerLine.substring(0,
965
									trimmedInnerLine.indexOf("--")).trim();
966
						}
967
						if (sqlStatement.length() > 0) {
968
							sqlStatement += " ";
969
						}
970
						sqlStatement += trimmedInnerLine;
971
						if (trimmedInnerLine.endsWith(endChar)) {
972
							sqlStatement = 
973
								sqlStatement.substring(0, sqlStatement.length() - 1);
974
							sqlCommands.add(sqlStatement);
975
							break;
976
						}
977
					} while ((fileLine = reader.readLine()) != null);
978
				}
979
			}
980
			fin.close();
981
		} finally {
982
			IOUtils.closeQuietly(fin);
983
		}
984

    
985
		return sqlCommands;
986
	}
987

    
988
	/**
989
	 * Validate the most important configuration options submitted by the user.
990
	 * 
991
	 * @return a vector holding error message for any fields that fail
992
	 *         validation.
993
	 */
994
	protected Vector<String> validateOptions(HttpServletRequest request) {
995
		Vector<String> errorVector = new Vector<String>();
996

    
997
		// TODO MCD validate options.
998

    
999
		return errorVector;
1000
	}
1001
}
(5-5/12)