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: daigle $'
9
 *     '$Date: 2008-11-13 15:20:40 -0800 (Thu, 13 Nov 2008) $'
10
 * '$Revision: 4561 $'
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.ServletException;
49
import javax.servlet.http.HttpServletRequest;
50
import javax.servlet.http.HttpServletResponse;
51
import javax.servlet.http.HttpSession;
52

    
53
import edu.ucsb.nceas.metacat.DBConnection;
54
import edu.ucsb.nceas.metacat.DBConnectionPool;
55
import edu.ucsb.nceas.metacat.DBVersion;
56
import edu.ucsb.nceas.metacat.MetaCatVersion;
57
import edu.ucsb.nceas.metacat.service.PropertyService;
58
import edu.ucsb.nceas.metacat.util.DatabaseUtil;
59
import edu.ucsb.nceas.metacat.util.RequestUtil;
60
import edu.ucsb.nceas.metacat.util.SystemUtil;
61
import edu.ucsb.nceas.metacat.util.UtilException;
62

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

    
68
import org.apache.log4j.Logger;
69

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

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

    
85
	private TreeSet<DBVersion> versionSet = null;
86

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

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

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

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

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

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

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

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

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

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

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

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

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

    
211
				upgradeDatabase();
212

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

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

    
228
				PropertyService.persistMainBackupProperties(request.getSession()
229
						.getServletContext());
230
			} catch (GeneralPropertyException gpe) {
231
				throw new AdminException("Problem getting or setting property while "
232
						+ "upgrading database: " + gpe.getMessage());
233
			}  catch (IOException ioe) {
234
				throw new AdminException("IO problem while upgrading database: "
235
						 + ioe.getMessage());
236
			} catch (ServletException se) {
237
				throw new AdminException("problem forwarding request while "
238
						+ "upgrading database: " + se.getMessage());
239
			}
240
		}
241
	}
242

    
243

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

    
258
		if (DBUtil.tableExists(connection, "xml_documents")) {
259
			return TABLES_EXIST;
260
		}
261

    
262
		return TABLES_DO_NOT_EXIST;
263
	}
264

    
265
	/**
266
	 * Get the version of the database as a string
267
	 * 
268
	 * @returns string representing the version of the database.
269
	 */
270
	public DBVersion getDBVersion() throws AdminException {
271

    
272
		// don't even try to search for a database version until system
273
		// properties have been configured
274
		try {
275
			if (!PropertyService.arePropertiesConfigured()) {
276
				throw new AdminException("An attempt was made to get the database version "
277
								+ "before system properties were configured");
278
			}
279
		} catch (UtilException ue) {
280
			throw new AdminException("Could not determine the database version: "
281
					+ ue.getMessage());
282
		}
283
		if (databaseVersion == null) {
284
			databaseVersion = discoverDBVersion();
285
		}
286

    
287
		if (databaseVersion == null) {
288
			throw new AdminException("Could not find database version");
289
		}
290

    
291
		return databaseVersion;
292
	}
293

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

    
316
			databaseVersion = getRegisteredDBVersion();
317
			if (databaseVersion != null) {
318
				return databaseVersion;
319
			}
320

    
321
			databaseVersion = getUnRegisteredDBVersion();
322
			
323
		} catch (SQLException sqle) {
324
			String errorMessage = "SQL error during  database version discovery: "
325
				+ sqle.getMessage();
326
			logMetacat.error(errorMessage);
327
			throw new AdminException(errorMessage);
328
		} catch (PropertyNotFoundException pnfe) {
329
			String errorMessage = "Property not found during  database version discovery: "
330
					+ pnfe.getMessage();
331
			logMetacat.error(errorMessage);
332
			throw new AdminException(errorMessage);
333
		} catch (NumberFormatException nfe) {
334
			throw new AdminException("Bad version format numbering: "
335
					+ nfe.getMessage());
336
		}
337
		
338
		if (databaseVersion == null) {
339
			throw new AdminException("Database version discovery returned null");
340
		}
341
		return databaseVersion;
342
	}
343

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

    
355
		try {
356
			// check out DBConnection
357
			Connection connection = 
358
				DBUtil.getConnection(
359
						PropertyService.getProperty("database.connectionURI"),
360
						PropertyService.getProperty("database.user"),
361
						PropertyService.getProperty("database.password"));
362

    
363
			if (!DBUtil.tableExists(connection, "db_version")) {
364
				return null;
365
			}
366

    
367
			pstmt = 
368
				connection.prepareStatement("SELECT version FROM db_version WHERE status = ?");
369

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

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

    
416
			String dbVersionString = null;
417

    
418
			if (is1_9_0(connection)) {
419
				dbVersionString = "1.9.0";
420
			} else if (is1_8_0(connection)) {
421
				dbVersionString = "1.8.0";
422
			} else if (is1_7_0(connection)) {
423
				dbVersionString = "1.7.0";
424
			} else if (is1_6_0(connection)) {
425
				dbVersionString = "1.6.0";
426
			} else if (is1_5_0(connection)) {
427
				dbVersionString = "1.5.0";
428
			} else if (is1_4_0(connection)) {
429
				dbVersionString = "1.4.0";
430
			} else if (is1_3_0(connection)) {
431
				dbVersionString = "1.3.0";
432
			} else if (is1_2_0(connection)) {
433
				dbVersionString = "1.2.0";
434
			}
435

    
436
			if (dbVersionString == null) {
437
				return null;
438
			} else {
439
				return new DBVersion(dbVersionString);
440
			}
441
		} catch (PropertyNotFoundException pnfe) {
442
			throw new AdminException(
443
					"Could not get property for unregistered db version: "
444
							+ pnfe.getMessage());
445
		} catch (NumberFormatException nfe) {
446
			throw new AdminException("Bad version format numbering: "
447
					+ nfe.getMessage());
448
		}
449
	}
450

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

    
464
			// check out DBConnection
465
			conn = DBConnectionPool
466
					.getDBConnection("DBAdmin.updateDBVersion()");
467
			serialNumber = conn.getCheckOutSerialNumber();
468
			conn.setAutoCommit(false);
469

    
470
			pstmt = conn.prepareStatement("UPDATE db_version SET status = ?");
471
			pstmt.setInt(1, VERSION_INACTIVE);
472
			pstmt.execute();
473
			pstmt.close();
474

    
475
			pstmt = conn.prepareStatement("INSERT INTO db_version "
476
					+ "(version, status, date_created) VALUES (?,?,?)");
477
			pstmt.setString(1, MetaCatVersion.getVersionID());
478
			pstmt.setInt(2, VERSION_ACTIVE);
479
			pstmt.setTimestamp(3, new Timestamp(new Date().getTime()));
480
			pstmt.execute();
481

    
482
			conn.commit();
483
		} catch (SQLException e) {
484
			conn.rollback();
485
			throw new SQLException("DBAdmin.getDBVersion(). " + e.getMessage());
486
		} catch (PropertyNotFoundException pnfe) {
487
			conn.rollback();
488
			throw new SQLException("DBAdmin.getDBVersion(). " + pnfe.getMessage());
489
		}
490
		finally {
491
			try {
492
				pstmt.close();
493
			} finally {
494
				DBConnectionPool.returnDBConnection(conn, serialNumber);
495
			}
496
		}
497
	}
498

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

    
524
		return null;
525
	}
526

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

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

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

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

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

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

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

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

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

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

    
716
			// add every update script that is > than the db version
717
			// but <= to the metacat version to the update list.
718
			if (nextVersion.compareTo(databaseVersion) > 0
719
					&& nextVersion.compareTo(metaCatVersion) <= 0
720
					&& nextVersion.getUpdateScripts() != null) {
721
				for (String versionUpdateScript : versionUpdateScripts) {
722
					updateScriptList.add(sqlFileLocation + FileUtil.getFS()
723
							+ versionUpdateScript + sqlSuffix);
724
				}
725
			}
726
		}
727

    
728
		// this should now hold all the script names that need to be run
729
		// to bring the database up to date with this version of metacat
730
		return updateScriptList;
731
	}
732

    
733
	/**
734
	 * Iterates through the list of scripts that need to be run to upgrade
735
	 * the database and calls runSQLFile on each.
736
	 */
737
	public void upgradeDatabase() throws AdminException {
738
		try {
739
			// get a list of the script names that need to be run
740
			Vector<String> updateScriptList = getUpdateScripts();
741

    
742
			// call runSQLFile on each
743
			for (String updateScript : updateScriptList) {
744
				runSQLFile(updateScript);
745
			}
746

    
747
			// update the db version to be the metacat version
748
			databaseVersion = new DBVersion(SystemUtil.getMetacatVersion().getVersionString());
749
		} catch (SQLException sqle) {
750
			throw new AdminException("SQL error when running upgrade scripts: "
751
					+ sqle.getMessage());
752
		} catch (PropertyNotFoundException pnfe) {
753
			throw new AdminException("SQL error when running upgrade scripts: "
754
					+ pnfe.getMessage());
755
		}catch (NumberFormatException nfe) {
756
			throw new AdminException("Bad version format numbering: "
757
					+ nfe.getMessage());
758
		}
759
	}
760

    
761
	/**
762
	 * Runs the commands in a sql script. Individual commands are loaded into a
763
	 * string vector and run one at a time.
764
	 * 
765
	 * @param sqlFileName
766
	 *            the name of the file holding the sql statements that need to
767
	 *            get run.
768
	 */
769
	public void runSQLFile(String sqlFileName) throws AdminException, SQLException {
770

    
771
		// if update file does not exist, do not do the update.
772
		if (FileUtil.getFileStatus(sqlFileName) < FileUtil.EXISTS_READABLE) {
773
			throw new AdminException("Could not read sql update file: "
774
					+ sqlFileName);
775
		}
776

    
777
		Connection connection = null;
778
		try {
779
			connection = DBUtil.getConnection(PropertyService
780
					.getProperty("database.connectionURI"), PropertyService
781
					.getProperty("database.user"), PropertyService
782
					.getProperty("database.password"));
783
			connection.setAutoCommit(false);
784

    
785
			// load the sql from the file into a vector of individual statements
786
			// and execute them.
787
			logMetacat.debug("processing File: " + sqlFileName);
788
			Vector<String> sqlCommands = loadSQLFromFile(sqlFileName);
789
			for (String sqlStatement : sqlCommands) {
790
				Statement statement = connection.createStatement();
791
				logMetacat.debug("executing sql: " + sqlStatement);
792
				try {
793
					statement.execute(sqlStatement);
794
				} catch (SQLException sqle) {
795
					// Oracle complains if we try and drop a sequence (ORA-02289) or a 
796
					// trigger (ORA-04098/ORA-04080) or a table/view (ORA-00942) or and index (ORA-01418) 
797
					// that does not exist.  We don't care if this happens.
798
					if (sqlStatement.toUpperCase().startsWith("DROP") && 
799
							(sqle.getMessage().contains("ORA-02289") ||
800
							 sqle.getMessage().contains("ORA-04098") ||
801
							 sqle.getMessage().contains("ORA-04080") ||
802
							 sqle.getMessage().contains("ORA-00942"))) {
803
						logMetacat.warn("did not process sql drop statement: " + sqle.getMessage());
804
					} else {
805
						throw sqle;
806
					}
807
				}
808
			}
809
			connection.commit();
810
			
811
		} catch (IOException ioe) {
812
			throw new AdminException("Could not read SQL file" 
813
					+ ioe.getMessage());
814
		} catch (PropertyNotFoundException pnfe) {
815
			throw new AdminException("Could not find property to run SQL file" 
816
					+ pnfe.getMessage());
817
		} catch (SQLException sqle) {
818
			if (connection != null) {
819
				connection.rollback();
820
			}
821
			throw sqle;
822
		} finally {
823
			if (connection != null) {
824
				connection.close();
825
			}
826
		}
827
	}
828

    
829
	/**
830
	 * Very basic utility to read sql from a file and return a vector of the
831
	 * individual sql statements. This ignores any line that starts with /* or *.
832
	 * It strips anything following --. Sql is parsed by looking for lines that
833
	 * start with one of the following identifiers: INSERT, UPDATE, ALTER,
834
	 * CREATE, DROP, BEGIN and COMMIT. It then assumes that everything until the
835
	 * line that ends with ; is part of the sql, excluding comments.
836
	 * 
837
	 * @param sqlFileName
838
	 *            the name of the file to read.
839
	 * @return a vector holding the individual sql statements.
840
	 */
841
	public Vector<String> loadSQLFromFile(String sqlFileName)
842
			throws IOException {
843

    
844
		// this will end up holding individual sql statements
845
		Vector<String> sqlCommands = new Vector<String>();
846

    
847
		FileInputStream fin = null;
848
		try {
849
			fin = new FileInputStream(sqlFileName);
850

    
851
			BufferedReader reader = new BufferedReader(new InputStreamReader(
852
					fin));
853

    
854
			// Read in file
855
			String fileLine;
856
			while ((fileLine = reader.readLine()) != null) {
857
				String endChar = ";";
858
				String trimmedLine = fileLine.trim();
859

    
860
				// get the first word on the line
861
				String firstWord = trimmedLine;
862
				if (trimmedLine.indexOf(' ') > 0) {
863
					firstWord = trimmedLine.substring(0, trimmedLine
864
							.indexOf(' '));
865
				}
866
				if (firstWord.endsWith(endChar)) {
867
					firstWord = firstWord.substring(0, firstWord.indexOf(endChar));
868
				}
869

    
870
				// if the first word is a known sql command, start creating a
871
				// sql statement.
872
				if (sqlCommandSet.contains(firstWord.toUpperCase())) {
873
					String sqlStatement = "";
874

    
875
					// keep reading lines until we find one that is not a
876
					// comment and ends with endChar
877
					do {
878
						String trimmedInnerLine = fileLine.trim();
879
						
880
						// if there is a BEGIN or DECLARE statement, we are now in plsql and we're 
881
						// using the '/' character as our sql end delimiter.
882
						if (trimmedInnerLine.toUpperCase().equals("BEGIN")  ||
883
								trimmedInnerLine.toUpperCase().startsWith("BEGIN ")  ||
884
								trimmedInnerLine.toUpperCase().equals("DECLARE")  ||
885
								trimmedInnerLine.toUpperCase().startsWith("DECLARE ")) {
886
							endChar = "/";
887
						}
888
						
889
						// ignore comments and empty lines
890
						if (trimmedInnerLine.matches("^$")
891
								|| trimmedInnerLine.matches("^\\*.*")
892
								|| trimmedInnerLine.matches("/\\*.*")) {
893
							continue;
894
						}
895

    
896
						// get rid of any "--" comments at the end of the line
897
						if (trimmedInnerLine.indexOf("--") >= 0) {
898
							trimmedInnerLine = trimmedInnerLine.substring(0,
899
									trimmedInnerLine.indexOf("--")).trim();
900
						}
901
						if (sqlStatement.length() > 0) {
902
							sqlStatement += " ";
903
						}
904
						sqlStatement += trimmedInnerLine;
905
						if (trimmedInnerLine.endsWith(endChar)) {
906
							sqlStatement = 
907
								sqlStatement.substring(0, sqlStatement.length() - 1);
908
							sqlCommands.add(sqlStatement);
909
							break;
910
						}
911
					} while ((fileLine = reader.readLine()) != null);
912
				}
913
			}
914
		} finally {
915
			// Close our input stream
916
			fin.close();
917
		}
918

    
919
		return sqlCommands;
920
	}
921

    
922
	/**
923
	 * Validate the most important configuration options submitted by the user.
924
	 * 
925
	 * @return a vector holding error message for any fields that fail
926
	 *         validation.
927
	 */
928
	protected Vector<String> validateOptions(HttpServletRequest request) {
929
		Vector<String> errorVector = new Vector<String>();
930

    
931
		// TODO MCD validate options.
932

    
933
		return errorVector;
934
	}
935
}
(3-3/9)