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: tao $'
9
 *     '$Date: 2013-10-16 16:29:58 -0700 (Wed, 16 Oct 2013) $'
10
 * '$Revision: 8323 $'
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.admin.upgrade.solr.SolrSchemaModificationException;
55
import edu.ucsb.nceas.metacat.database.DBConnection;
56
import edu.ucsb.nceas.metacat.database.DBConnectionPool;
57
import edu.ucsb.nceas.metacat.database.DBVersion;
58
import edu.ucsb.nceas.metacat.properties.PropertyService;
59
import edu.ucsb.nceas.metacat.shared.MetacatUtilException;
60
import edu.ucsb.nceas.metacat.util.DatabaseUtil;
61
import edu.ucsb.nceas.metacat.util.RequestUtil;
62
import edu.ucsb.nceas.metacat.util.SystemUtil;
63

    
64
import edu.ucsb.nceas.utilities.DBUtil;
65
import edu.ucsb.nceas.utilities.FileUtil;
66
import edu.ucsb.nceas.utilities.GeneralPropertyException;
67
import edu.ucsb.nceas.utilities.PropertyNotFoundException;
68

    
69
import org.apache.commons.io.IOUtils;
70
import org.apache.log4j.Logger;
71

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

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

    
87
	private TreeSet<DBVersion> versionSet = null;
88

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

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

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

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

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

    
147
		String processForm = request.getParameter("processForm");
148
		String formErrors = (String) request.getAttribute("formErrors");
149
		HttpSession session = request.getSession();
150
		String supportEmail = null;
151

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

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

    
162
				databaseVersion = discoverDBVersion();
163
				MetacatVersion metacatVersion = SystemUtil.getMetacatVersion();
164
				
165
				session.setAttribute("metacatVersion", MetacatVersion.getVersionID());
166

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

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

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

    
204
			try {
205
				// Validate that the options provided are legitimate. Note that
206
				// we've allowed them to persist their entries. As of this point
207
				// there is no other easy way to go back to the configure form
208
				// and
209
				// preserve their entries.
210
			    supportEmail = PropertyService.getProperty("email.recipient");
211
				validationErrors.addAll(validateOptions(request));
212
				
213
				
214
				upgradeDatabase();
215
				
216
				
217

    
218
				// Now that the options have been set, change the
219
				// 'databaseConfigured' option to 'true' so that normal metacat
220
				// requests will go through
221
				PropertyService.setProperty("configutil.databaseConfigured",
222
						PropertyService.CONFIGURED);
223
				PropertyService.persistMainBackupProperties();
224
                if(solrSchemaException != null) {
225
                    //Show the warning message
226
                    Vector<String> errorVector = new Vector<String>();
227
                    errorVector.add(solrSchemaException.getMessage());
228
                    RequestUtil.clearRequestMessages(request);
229
                    request.setAttribute("supportEmail", supportEmail);
230
                    RequestUtil.setRequestErrors(request, errorVector);
231
                    RequestUtil.forwardRequest(request, response,
232
                                    "/admin/solr-schema-warn.jsp", null);
233
                } else {
234
                    // Reload the main metacat configuration page
235
                    processingSuccess.add("Database successfully upgraded");
236
                    RequestUtil.clearRequestMessages(request);
237
                    RequestUtil.setRequestSuccess(request, processingSuccess);
238
                    RequestUtil.forwardRequest(request, response,
239
                            "/admin?configureType=configure&processForm=false", null);
240
                    // Write out the configurable properties to a backup file
241
                    // outside the install directory.
242

    
243
                    
244
                }
245
			
246
			} catch (GeneralPropertyException gpe) {
247
				throw new AdminException("DBAdmin.configureDatabase - Problem getting or setting " +
248
						"property while upgrading database: " + gpe.getMessage());
249
			}  catch (MetacatUtilException mue) {
250
				throw new AdminException("DBAdmin.configureDatabase - utility problem while upgrading database: "
251
						 + mue.getMessage());
252
			} 
253
		}
254
	}
255

    
256

    
257
	/**
258
	 * Performs a status check on the database.
259
	 * 
260
	 * @returns integer representing the status of the database. These can be: 
261
	 * 		-- DB_DOES_NOT_EXIST = 0; 
262
	 *      -- TABLES_DO_NOT_EXIST = 1; 
263
	 *      -- TABLES_EXIST = 2;
264
	 */
265
	public int getDBStatus() throws SQLException, PropertyNotFoundException {
266
		Connection connection = DBUtil.getConnection(PropertyService
267
				.getProperty("database.connectionURI"), PropertyService
268
				.getProperty("database.user"), PropertyService
269
				.getProperty("database.password"));
270

    
271
		if (DBUtil.tableExists(connection, "xml_documents")) {
272
			return TABLES_EXIST;
273
		}
274

    
275
		return TABLES_DO_NOT_EXIST;
276
	}
277

    
278
	/**
279
	 * Get the version of the database as a string
280
	 * 
281
	 * @returns string representing the version of the database.
282
	 */
283
	public DBVersion getDBVersion() throws AdminException {
284

    
285
		// don't even try to search for a database version until system
286
		// properties have been configured
287
		try {
288
			if (!PropertyService.arePropertiesConfigured()) {
289
				throw new AdminException("DBAdmin.getDBVersion - An attempt was made to get " + 
290
						"the database version before system properties were configured");
291
			}
292
		} catch (GeneralPropertyException gpe) {
293
			throw new AdminException("DBAdmin.getDBVersion - Could not determine the database version: "
294
					+ gpe.getMessage());
295
		}
296
		if (databaseVersion == null) {
297
			databaseVersion = discoverDBVersion();
298
		}
299

    
300
		if (databaseVersion == null) {
301
			throw new AdminException("DBAdmin.getDBVersion - Could not find database version");
302
		}
303

    
304
		return databaseVersion;
305
	}
306

    
307
	/**
308
	 * Try to discover the database version, first by calling
309
	 * getRegisteredDBVersion() to see if the database version is in a table in
310
	 * the database. If not, getUnRegisteredDBVersion() is called to see if we
311
	 * can devine the version by looking for unique changes made by scripts for
312
	 * each version update.
313
	 * 
314
	 * @returns string representing the version of the database, null if none
315
	 *          could be found.
316
	 */
317
	private DBVersion discoverDBVersion() throws AdminException {
318
		try {
319
			int dbStatus = getDBStatus();
320
			if (dbStatus == DB_DOES_NOT_EXIST) {
321
				throw new AdminException("DBAdmin.discoverDBVersion - Database does not exist " + 
322
						"for connection" + PropertyService.getProperty("database.connectionURI"));
323
			} else if (dbStatus == TABLES_DO_NOT_EXIST) {
324
				databaseVersion = new DBVersion("0.0.0");
325
				return databaseVersion;
326
			}
327

    
328
			databaseVersion = getRegisteredDBVersion();
329
			if (databaseVersion != null) {
330
				return databaseVersion;
331
			}
332

    
333
			databaseVersion = getUnRegisteredDBVersion();
334
			
335
		} catch (SQLException sqle) {
336
			String errorMessage = "DBAdmin.discoverDBVersion - SQL error during  database version discovery: "
337
				+ sqle.getMessage();
338
			logMetacat.error(errorMessage);
339
			throw new AdminException(errorMessage);
340
		} catch (PropertyNotFoundException pnfe) {
341
			String errorMessage = "DBAdmin.discoverDBVersion - Property not found during  database " + 
342
			"version discovery: " + pnfe.getMessage();
343
			logMetacat.error(errorMessage);
344
			throw new AdminException(errorMessage);
345
		} catch (NumberFormatException nfe) {
346
			throw new AdminException("DBAdmin.discoverDBVersion - Bad version format numbering: "
347
					+ nfe.getMessage());
348
		}
349
		
350
		if (databaseVersion == null) {
351
			throw new AdminException("DBAdmin.discoverDBVersion - Database version discovery returned null");
352
		}
353
		return databaseVersion;
354
	}
355

    
356
	/**
357
	 * Gets the version of the database from the db_version table. Usually this
358
	 * is the same as the version of the product, however the db version could
359
	 * be more granular if we applied a maintenance patch for instance.
360
	 * 
361
	 * @returns string representing the version of the database.
362
	 */
363
	private DBVersion getRegisteredDBVersion() throws AdminException, SQLException {
364
		String dbVersionString = null;
365
		PreparedStatement pstmt = null;
366

    
367
		try {
368
			// check out DBConnection
369
			Connection connection = 
370
				DBUtil.getConnection(
371
						PropertyService.getProperty("database.connectionURI"),
372
						PropertyService.getProperty("database.user"),
373
						PropertyService.getProperty("database.password"));
374

    
375
			if (!DBUtil.tableExists(connection, "db_version")) {
376
				return null;
377
			}
378

    
379
			pstmt = 
380
				connection.prepareStatement("SELECT version FROM db_version WHERE status = ?");
381

    
382
			// Bind the values to the query
383
			pstmt.setInt(1, VERSION_ACTIVE);
384
			pstmt.execute();
385
			ResultSet rs = pstmt.getResultSet();
386
			boolean hasRows = rs.next();
387
			if (hasRows) {
388
				dbVersionString = rs.getString(1);
389
			}
390
			
391
			if (dbVersionString == null) {
392
				return null;
393
			} 
394
				
395
			return new DBVersion(dbVersionString);
396
			
397
		} catch (SQLException sqle) {
398
			throw new AdminException("DBAdmin.getRegisteredDBVersion - Could not run SQL to get registered db version: " 
399
					+ sqle.getMessage());			
400
		} catch (PropertyNotFoundException pnfe) {
401
			throw new AdminException("DBAdmin.getRegisteredDBVersion - Could not get property for registered db version: " 
402
					+ pnfe.getMessage());		
403
		} catch (NumberFormatException nfe) {
404
			throw new AdminException("DBAdmin.getRegisteredDBVersion - Bad version format numbering: "
405
					+ nfe.getMessage());
406
		} finally {
407
			if (pstmt != null) {
408
				pstmt.close();
409
			}
410
		}
411
	}
412

    
413
	/**
414
	 * Finds the version of the database for a database that does not have a
415
	 * dbVersion table yet. Work backwards with various clues found in update
416
	 * scripts to find the version.
417
	 * 
418
	 * @returns string representing the version of the database.
419
	 */
420
	public DBVersion getUnRegisteredDBVersion() throws AdminException, SQLException {
421
		Connection connection = null;
422
		try {
423
			connection = DBUtil.getConnection(PropertyService
424
					.getProperty("database.connectionURI"), PropertyService
425
					.getProperty("database.user"), PropertyService
426
					.getProperty("database.password"));
427

    
428
			String dbVersionString = null;
429

    
430
			if (is1_9_1(connection)) {
431
				dbVersionString = "1.9.1";
432
			} else if (is1_9_0(connection)) {
433
				dbVersionString = "1.9.0";
434
			} else if (is1_8_0(connection)) {
435
				dbVersionString = "1.8.0";
436
			} else if (is1_7_0(connection)) {
437
				dbVersionString = "1.7.0";
438
			} else if (is1_6_0(connection)) {
439
				dbVersionString = "1.6.0";
440
			} else if (is1_5_0(connection)) {
441
				dbVersionString = "1.5.0";
442
			} else if (is1_4_0(connection)) {
443
				dbVersionString = "1.4.0";
444
			} else if (is1_3_0(connection)) {
445
				dbVersionString = "1.3.0";
446
			} else if (is1_2_0(connection)) {
447
				dbVersionString = "1.2.0";
448
			}
449

    
450
			if (dbVersionString == null) {
451
				return null;
452
			} else {
453
				return new DBVersion(dbVersionString);
454
			}
455
		} catch (PropertyNotFoundException pnfe) {
456
			throw new AdminException("DBAdmin.getUnRegisteredDBVersion - Could not get " + 
457
					"property for unregistered db version: " + pnfe.getMessage());
458
		} catch (NumberFormatException nfe) {
459
			throw new AdminException("DBAdmin.getUnRegisteredDBVersion - Bad version format numbering: "
460
					+ nfe.getMessage());
461
		}
462
	}
463

    
464
	/**
465
	 * Updates the version of the database. Typically this is done in the update
466
	 * scripts that get run when we upgrade the application. This method can be
467
	 * used if you are automating a patch on the database internally.
468
	 * 
469
	 * @returns string representing the version of the database.
470
	 */
471
	public void updateDBVersion() throws SQLException {
472
		DBConnection conn = null;
473
		PreparedStatement pstmt = null;
474
		int serialNumber = -1;
475
		try {
476

    
477
			// check out DBConnection
478
			conn = DBConnectionPool.getDBConnection("DBAdmin.updateDBVersion()");
479
			serialNumber = conn.getCheckOutSerialNumber();
480
			conn.setAutoCommit(false);
481

    
482
			pstmt = conn.prepareStatement("UPDATE db_version SET status = ?");
483
			pstmt.setInt(1, VERSION_INACTIVE);
484
			pstmt.execute();
485
			pstmt.close();
486

    
487
			pstmt = conn.prepareStatement("INSERT INTO db_version "
488
					+ "(version, status, date_created) VALUES (?,?,?)");
489
			pstmt.setString(1, MetacatVersion.getVersionID());
490
			pstmt.setInt(2, VERSION_ACTIVE);
491
			pstmt.setTimestamp(3, new Timestamp(new Date().getTime()));
492
			pstmt.execute();
493

    
494
			conn.commit();
495
		} catch (SQLException e) {
496
			conn.rollback();
497
			throw new SQLException("DBAdmin.updateDBVersion - sql error: " + e.getMessage());
498
		} catch (PropertyNotFoundException pnfe) {
499
			conn.rollback();
500
			throw new SQLException("DBAdmin.updateDBVersion - property error" + pnfe.getMessage());
501
		}
502
		finally {
503
			try {
504
				pstmt.close();
505
			} finally {
506
				DBConnectionPool.returnDBConnection(conn, serialNumber);
507
			}
508
		}
509
	}
510

    
511
	/**
512
	 * Validate connectivity to the database. Validation methods return a string
513
	 * error message if there is an issue. This allows the calling code to run
514
	 * several validations and compile the errors into a list that can be
515
	 * displayed on a web page if desired.
516
	 * 
517
	 * @param dbDriver
518
	 *            the database driver
519
	 * @param connection
520
	 *            the jdbc connection string
521
	 * @param user
522
	 *            the user name
523
	 * @param password
524
	 *            the login password
525
	 * @return a string holding error message if validation fails.
526
	 */
527
	public String validateDBConnectivity(String dbDriver, String connection,
528
			String user, String password) {
529
		try {
530
			DBConnection.testConnection(dbDriver, connection, user, password);
531
		} catch (SQLException se) {
532
			return "Invalid database credential was provided: "
533
					+ se.getMessage();
534
		}
535

    
536
		return null;
537
	}
538

    
539
	/**
540
	 * Checks to see if this is a 1.9.0 database schema by looking for the
541
	 * db_version 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_0(Connection connection) throws SQLException {
551
		return DBUtil.tableExists(connection, "db_version");
552
	}
553
	
554
	/**
555
	 * Checks to see if this is a 1.9.1 database schema by looking for the
556
	 * scheduled_job table which was created for 1.9.0. Note, there is no guarantee
557
	 * that this table will not be removed in subsequent versions. You should
558
	 * 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 table is found, false otherwise
564
	 */
565
	private boolean is1_9_1(Connection connection) throws SQLException {
566
		return DBUtil.tableExists(connection, "db_version");
567
	}
568

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

    
591
	/**
592
	 * Checks to see if this is a 1.7.0 database schema by looking for the
593
	 * xml_documents_idx2 index which was created for 1.7.0. Note, there is no
594
	 * guarantee that this index will not be removed in subsequent versions. You
595
	 * should search for db versions from newest to oldest, only getting to this
596
	 * function when newer versions have not been matched.
597
	 * 
598
	 * @param dbMetaData
599
	 *            the meta data for this database.
600
	 * @returns boolean which is true if index is found, false otherwise
601
	 */
602
	private boolean is1_7_0(Connection connection) throws SQLException, PropertyNotFoundException {
603
		String tableName = "xml_documents";
604
		String dbType = PropertyService.getProperty("database.type");
605
			
606
		boolean isOracle = dbType.equals("oracle");		
607
		if(isOracle) {
608
			tableName = "XML_DOCUMENTS";
609
		}
610
	
611
		return DBUtil.indexExists(connection, tableName, "xml_documents_idx2");
612
	}
613

    
614
	/**
615
	 * Checks to see if this is a 1.6.0 database schema by looking for the
616
	 * identifier table which was created for 1.6.0. Note, there is no guarantee
617
	 * that this table will not be removed in subsequent versions. You should
618
	 * 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_6_0(Connection connection) throws SQLException {
626
		return DBUtil.tableExists(connection, "identifier");
627
	}
628

    
629
	/**
630
	 * Checks to see if this is a 1.5.0 database schema by looking for the
631
	 * xml_returnfield table which was created for 1.5.0. Note, there is no
632
	 * guarantee that this table will not be removed in subsequent versions. You
633
	 * should 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_5_0(Connection connection) throws SQLException {
641
		return DBUtil.tableExists(connection, "xml_returnfield");
642
	}
643

    
644
	/**
645
	 * Checks to see if this is a 1.4.0 database schema by looking for the
646
	 * access_log table which was created for 1.4.0. Note, there is no guarantee
647
	 * that this table will not be removed in subsequent versions. You should
648
	 * 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_4_0(Connection connection) throws SQLException {
656
		return DBUtil.tableExists(connection, "access_log");
657
	}
658

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

    
674
	/**
675
	 * Checks to see if this is a 1.2.0 database schema by looking for the
676
	 * datareplicate column which was created on the xml_replication table for
677
	 * 1.2.0. Note, there is no guarantee that this column will not be removed
678
	 * in subsequent versions. You should search for db versions from newest to
679
	 * oldest, only getting to this function when newer versions have not been
680
	 * matched.
681
	 * 
682
	 * @param dbMetaData
683
	 *            the meta data for this database.
684
	 * @returns boolean which is true if column is found, false otherwise
685
	 */
686
	private boolean is1_2_0(Connection connection) throws SQLException {
687
		return DBUtil.columnExists(connection, "xml_replication",
688
				"datareplicate");
689
	}
690

    
691
	/**
692
	 * Creates a list of database update script names by looking at the database
693
	 * version and the metacat version and then getting any script that is
694
	 * inbetween the two (inclusive of metacat version).
695
	 * 
696
	 * @returns a Vector of Strings holding the names of scripts that need to be
697
	 *          run to get the database updated to this version of metacat
698
	 * 
699
	 */
700
	public Vector<String> getUpdateScripts() throws AdminException {
701
		Vector<String> updateScriptList = new Vector<String>();
702
		String sqlFileLocation = null;
703
		String databaseType = null;
704
		MetacatVersion metaCatVersion = null; 
705
		
706
		// get the location of sql scripts
707
		try {
708
			metaCatVersion = SystemUtil.getMetacatVersion();
709
			sqlFileLocation = SystemUtil.getSQLDir();
710
			databaseType = PropertyService.getProperty("database.type");
711
		} catch (PropertyNotFoundException pnfe) {
712
			throw new AdminException("DBAdmin.getUpdateScripts - Could not get property while trying " 
713
					+ "to retrieve database update scripts: " + pnfe.getMessage());
714
		}
715
		
716
		// Each type of db has it's own set of scripts.  For instance, Oracle
717
		// scripts end in -oracle.sql.  Postges end in -postgres.sql, etc
718
		String sqlSuffix = "-" + scriptSuffixMap.get("database.scriptsuffix." + databaseType);
719
		
720
		// if either of these is null, we don't want to do anything.  Just 
721
		// return an empty list.
722
		if (metaCatVersion == null || databaseVersion == null) {
723
			return updateScriptList;
724
		}
725

    
726
		// go through all the versions that the the software went through and 
727
		// figure out which ones need to be applied to the database	
728
		for (DBVersion nextVersion : versionSet) {
729
			Vector<String> versionUpdateScripts = nextVersion
730
					.getUpdateScripts();
731
			
732
			// if the database version is 0.0.0, it is new.
733
			// apply all scripts.
734
			if (databaseVersion.getVersionString().equals("0.0.0")
735
					&& nextVersion.getVersionString().equals("0.0.0")) {
736
				for (String versionUpdateScript : versionUpdateScripts) {
737
					updateScriptList.add(sqlFileLocation + FileUtil.getFS()
738
							+ versionUpdateScript + sqlSuffix);
739
				}
740
				return updateScriptList;
741
			}
742

    
743
			// add every update script that is > than the db version
744
			// but <= to the metacat version to the update list.
745
			if (nextVersion.compareTo(databaseVersion) > 0
746
					&& nextVersion.compareTo(metaCatVersion) <= 0
747
					&& nextVersion.getUpdateScripts() != null) {
748
				for (String versionUpdateScript : versionUpdateScripts) {
749
					updateScriptList.add(sqlFileLocation + FileUtil.getFS()
750
							+ versionUpdateScript + sqlSuffix);
751
				}
752
			}
753
		}
754

    
755
		// this should now hold all the script names that need to be run
756
		// to bring the database up to date with this version of metacat
757
		return updateScriptList;
758
	}
759

    
760
	public Vector<String> getUpdateClasses() throws AdminException {
761
		Vector<String> updateClassList = new Vector<String>();
762
		MetacatVersion metaCatVersion = null; 
763
		
764
		// get the location of sql scripts
765
		try {
766
			metaCatVersion = SystemUtil.getMetacatVersion();
767
		} catch (PropertyNotFoundException pnfe) {
768
			throw new AdminException("DBAdmin.getUpdateScripts - Could not get property while trying " 
769
					+ "to retrieve update utilities: " + pnfe.getMessage());
770
		}
771
		
772
		// if either of these is null, we don't want to do anything.  Just 
773
		// return an empty list.
774
		if (metaCatVersion == null || databaseVersion == null) {
775
			return updateClassList;
776
		}
777

    
778
		// go through all the versions that the the software went through and 
779
		// figure out which ones need to be applied to the database	
780
		for (DBVersion nextVersion : versionSet) {
781

    
782
			// add every update script that is > than the db version
783
			// but <= to the metacat version to the update list.
784
			if (nextVersion.compareTo(databaseVersion) > 0
785
					&& nextVersion.compareTo(metaCatVersion) <= 0) {
786
				String key = "database.upgradeUtility." + nextVersion.getVersionString();
787
				String className = null;
788
				try {
789
					className = PropertyService.getProperty(key);
790
				} catch (PropertyNotFoundException pnfe) {
791
					// there probably isn't a utility needed for this version
792
					logMetacat.warn("No utility defined for version: " + key);
793
					continue;
794
				}
795
				updateClassList.add(className);
796
			}
797
		}
798

    
799
		// this should now hold all the script names that need to be run
800
		// to bring the database up to date with this version of metacat
801
		return updateClassList;
802
	}
803
	
804
	/**
805
	 * Iterates through the list of scripts that need to be run to upgrade
806
	 * the database and calls runSQLFile on each.
807
	 */
808
	public void upgradeDatabase() throws AdminException {
809
		try {
810
			// get a list of the script names that need to be run
811
			Vector<String> updateScriptList = getUpdateScripts();
812

    
813
			// call runSQLFile on each
814
			for (String updateScript : updateScriptList) {
815
				runSQLFile(updateScript);
816
			}
817
			
818
			// get the classes we need to execute in order to bring DB to current version
819
			Vector<String> updateClassList = getUpdateClasses();
820
			for (String className : updateClassList) {
821
				UpgradeUtilityInterface utility = null;
822
				try {
823
					utility = (UpgradeUtilityInterface) Class.forName(className).newInstance();
824
					utility.upgrade();
825
				} catch (SolrSchemaModificationException e) {
826
				    //don't throw the exception and continue 
827
				    solrSchemaException = e;
828
				    continue;
829
				} catch (Exception e) {
830
					throw new AdminException("DBAdmin.upgradeDatabase - error getting utility class: " 
831
							+ className + ". Error message: "
832
							+ e.getMessage());
833
				}
834
			}
835

    
836
			// update the db version to be the metacat version
837
			databaseVersion = new DBVersion(SystemUtil.getMetacatVersion().getVersionString());
838
		} catch (SQLException sqle) {
839
			throw new AdminException("DBAdmin.upgradeDatabase - SQL error when running upgrade scripts: "
840
					+ sqle.getMessage());
841
		} catch (PropertyNotFoundException pnfe) {
842
			throw new AdminException("DBAdmin.upgradeDatabase - SQL error when running upgrade scripts: "
843
					+ pnfe.getMessage());
844
		}catch (NumberFormatException nfe) {
845
			throw new AdminException("DBAdmin.upgradeDatabase - Bad version format numbering: "
846
					+ nfe.getMessage());
847
		}
848
	}
849

    
850
	/**
851
	 * Runs the commands in a sql script. Individual commands are loaded into a
852
	 * string vector and run one at a time.
853
	 * 
854
	 * @param sqlFileName
855
	 *            the name of the file holding the sql statements that need to
856
	 *            get run.
857
	 */
858
	public void runSQLFile(String sqlFileName) throws AdminException, SQLException {
859

    
860
		// if update file does not exist, do not do the update.
861
		if (FileUtil.getFileStatus(sqlFileName) < FileUtil.EXISTS_READABLE) {
862
			throw new AdminException("Could not read sql update file: "
863
					+ sqlFileName);
864
		}
865

    
866
		Connection connection = null;
867
		try {
868
			connection = DBUtil.getConnection(PropertyService
869
					.getProperty("database.connectionURI"), PropertyService
870
					.getProperty("database.user"), PropertyService
871
					.getProperty("database.password"));
872
			connection.setAutoCommit(false);
873

    
874
			// load the sql from the file into a vector of individual statements
875
			// and execute them.
876
			logMetacat.debug("DBAdmin.runSQLFile - processing File: " + sqlFileName);
877
			Vector<String> sqlCommands = loadSQLFromFile(sqlFileName);
878
			for (String sqlStatement : sqlCommands) {
879
				Statement statement = connection.createStatement();
880
				logMetacat.debug("executing sql: " + sqlStatement);
881
				try {
882
					statement.execute(sqlStatement);
883
				} catch (SQLException sqle) {
884
					// Oracle complains if we try and drop a sequence (ORA-02289) or a 
885
					// trigger (ORA-04098/ORA-04080) or a table/view (ORA-00942) or and index (ORA-01418) 
886
					// that does not exist.  We don't care if this happens.
887
					if (sqlStatement.toUpperCase().startsWith("DROP") && 
888
							(sqle.getMessage().contains("ORA-02289") ||
889
							 sqle.getMessage().contains("ORA-04098") ||
890
							 sqle.getMessage().contains("ORA-04080") ||
891
							 sqle.getMessage().contains("ORA-00942"))) {
892
						logMetacat.warn("DBAdmin.runSQLFile - did not process sql drop statement: " + sqle.getMessage());
893
					} else {
894
						throw sqle;
895
					}
896
				}
897
			}
898
			connection.commit();
899
			
900
		} catch (IOException ioe) {
901
			throw new AdminException("DBAdmin.runSQLFile - Could not read SQL file" 
902
					+ ioe.getMessage());
903
		} catch (PropertyNotFoundException pnfe) {
904
			throw new AdminException("DBAdmin.runSQLFile - Could not find property to run SQL file" 
905
					+ pnfe.getMessage());
906
		} catch (SQLException sqle) {
907
			if (connection != null) {
908
				connection.rollback();
909
			}
910
			throw sqle;
911
		} finally {
912
			if (connection != null) {
913
				connection.close();
914
			}
915
		}
916
	}
917

    
918
	/**
919
	 * Very basic utility to read sql from a file and return a vector of the
920
	 * individual sql statements. This ignores any line that starts with /* or *.
921
	 * It strips anything following --. Sql is parsed by looking for lines that
922
	 * start with one of the following identifiers: INSERT, UPDATE, ALTER,
923
	 * CREATE, DROP, BEGIN and COMMIT. It then assumes that everything until the
924
	 * line that ends with ; is part of the sql, excluding comments.
925
	 * 
926
	 * @param sqlFileName
927
	 *            the name of the file to read.
928
	 * @return a vector holding the individual sql statements.
929
	 */
930
	public Vector<String> loadSQLFromFile(String sqlFileName)
931
			throws IOException {
932

    
933
		// this will end up holding individual sql statements
934
		Vector<String> sqlCommands = new Vector<String>();
935

    
936
		FileInputStream fin = null;
937
		try {
938
			fin = new FileInputStream(sqlFileName);
939

    
940
			BufferedReader reader = new BufferedReader(new InputStreamReader(
941
					fin));
942

    
943
			// Read in file
944
			String fileLine;
945
			while ((fileLine = reader.readLine()) != null) {
946
				String endChar = ";";
947
				String trimmedLine = fileLine.trim();
948

    
949
				// get the first word on the line
950
				String firstWord = trimmedLine;
951
				if (trimmedLine.indexOf(' ') > 0) {
952
					firstWord = trimmedLine.substring(0, trimmedLine
953
							.indexOf(' '));
954
				}
955
				if (firstWord.endsWith(endChar)) {
956
					firstWord = firstWord.substring(0, firstWord.indexOf(endChar));
957
				}
958

    
959
				// if the first word is a known sql command, start creating a
960
				// sql statement.
961
				if (sqlCommandSet.contains(firstWord.toUpperCase())) {
962
					String sqlStatement = "";
963

    
964
					// keep reading lines until we find one that is not a
965
					// comment and ends with endChar
966
					do {
967
						String trimmedInnerLine = fileLine.trim();
968
						
969
						// if there is a BEGIN or DECLARE statement, we are now in plsql and we're 
970
						// using the '/' character as our sql end delimiter.
971
						if (trimmedInnerLine.toUpperCase().equals("BEGIN")  ||
972
								trimmedInnerLine.toUpperCase().startsWith("BEGIN ")  ||
973
								trimmedInnerLine.toUpperCase().equals("DECLARE")  ||
974
								trimmedInnerLine.toUpperCase().startsWith("DECLARE ")) {
975
							endChar = "/";
976
						}
977
						
978
						// ignore comments and empty lines
979
						if (trimmedInnerLine.matches("^$")
980
								|| trimmedInnerLine.matches("^\\*.*")
981
								|| trimmedInnerLine.matches("/\\*.*")) {
982
							continue;
983
						}
984

    
985
						// get rid of any "--" comments at the end of the line
986
						if (trimmedInnerLine.indexOf("--") >= 0) {
987
							trimmedInnerLine = trimmedInnerLine.substring(0,
988
									trimmedInnerLine.indexOf("--")).trim();
989
						}
990
						if (sqlStatement.length() > 0) {
991
							sqlStatement += " ";
992
						}
993
						sqlStatement += trimmedInnerLine;
994
						if (trimmedInnerLine.endsWith(endChar)) {
995
							sqlStatement = 
996
								sqlStatement.substring(0, sqlStatement.length() - 1);
997
							sqlCommands.add(sqlStatement);
998
							break;
999
						}
1000
					} while ((fileLine = reader.readLine()) != null);
1001
				}
1002
			}
1003
			fin.close();
1004
		} finally {
1005
			IOUtils.closeQuietly(fin);
1006
		}
1007

    
1008
		return sqlCommands;
1009
	}
1010

    
1011
	/**
1012
	 * Validate the most important configuration options submitted by the user.
1013
	 * 
1014
	 * @return a vector holding error message for any fields that fail
1015
	 *         validation.
1016
	 */
1017
	protected Vector<String> validateOptions(HttpServletRequest request) {
1018
		Vector<String> errorVector = new Vector<String>();
1019

    
1020
		// TODO MCD validate options.
1021

    
1022
		return errorVector;
1023
	}
1024
}
(5-5/12)