Project

General

Profile

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