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: leinfelder $'
9
 *     '$Date: 2014-02-14 15:19:40 -0800 (Fri, 14 Feb 2014) $'
10
 * '$Revision: 8605 $'
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
		sqlCommandSet.add("WITH");
109

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

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

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

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

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

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

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

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

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

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

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

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

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

    
257

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

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

    
276
		return TABLES_DO_NOT_EXIST;
277
	}
278

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

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

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

    
305
		return databaseVersion;
306
	}
307

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

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

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

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

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

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

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

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

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

    
429
			String dbVersionString = null;
430

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

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

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

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

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

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

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

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

    
537
		return null;
538
	}
539

    
540
	/**
541
	 * Checks to see if this is a 1.9.0 database schema by looking for the
542
	 * db_version table which was created for 1.9.0. Note, there is no guarantee
543
	 * that this table will not be removed in subsequent versions. You should
544
	 * search for db versions from newest to oldest, only getting to this
545
	 * function when newer versions have not been matched.
546
	 * 
547
	 * @param dbMetaData
548
	 *            the meta data for this database.
549
	 * @returns boolean which is true if table is found, false otherwise
550
	 */
551
	private boolean is1_9_0(Connection connection) throws SQLException {
552
		return DBUtil.tableExists(connection, "db_version");
553
	}
554
	
555
	/**
556
	 * Checks to see if this is a 1.9.1 database schema by looking for the
557
	 * scheduled_job table which was created for 1.9.0. Note, there is no guarantee
558
	 * that this table will not be removed in subsequent versions. You should
559
	 * search for db versions from newest to oldest, only getting to this
560
	 * function when newer versions have not been matched.
561
	 * 
562
	 * @param dbMetaData
563
	 *            the meta data for this database.
564
	 * @returns boolean which is true if table is found, false otherwise
565
	 */
566
	private boolean is1_9_1(Connection connection) throws SQLException {
567
		return DBUtil.tableExists(connection, "db_version");
568
	}
569

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1009
		return sqlCommands;
1010
	}
1011

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

    
1021
		// TODO MCD validate options.
1022

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