Project

General

Profile

1
package edu.ucsb.nceas.metacat.admin.upgrade;
2
/**
3
 *  '$RCSfile$'
4
 *  Copyright: 2012 Regents of the University of California and the
5
 *             National Center for Ecological Analysis and Synthesis
6
 *    Authors: Matthew Jones
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
import java.io.IOException;
27
import java.sql.Connection;
28
import java.sql.Driver;
29
import java.sql.DriverManager;
30
import java.sql.PreparedStatement;
31
import java.sql.ResultSet;
32
import java.sql.SQLException;
33
import java.sql.Statement;
34

    
35
import org.apache.commons.logging.Log;
36
import org.apache.commons.logging.LogFactory;
37

    
38
import edu.ucsb.nceas.metacat.admin.AdminException;
39
import edu.ucsb.nceas.metacat.properties.PropertyService;
40
import edu.ucsb.nceas.metacat.shared.ServiceException;
41
import edu.ucsb.nceas.utilities.PropertyNotFoundException;
42
import edu.ucsb.nceas.utilities.SortedProperties;
43

    
44
/**
45
 * Optionally upgrade all of the existing identifiers in the tables to be official
46
 * DOIs that are assigned by a DOI registration authority.  This class first checks
47
 * if GUIDs should be converted to DOIs, and if so, updates all relevant
48
 * tables and resources in Metacat, and then registers each of the newly minted
49
 * identifiers with the EZID service. You must have an EZID account for this to
50
 * work properly.
51
 * 
52
 * Whether or not identifiers should be converted is determined by several metacat.properties.
53
 * First, only upgrade if 'guid.assignGUIDs' is true.  Next, for each server registered in 
54
 * the xml_replication table, only convert the documents belonging to that server if
55
 * there is a corresponding property that gives the 'shoulder' or 'prefix'' to be used to
56
 * convert existing IDs to GUIDs.  For example, if servers 1 & 2 are registered in 
57
 * xml_replication, then only convert the IDs for server 1 to GUIDs if the
58
 * property 'guid.ezid.doishoulder.1' defines a valid doi shoulder.
59
 * 
60
 * NOTE: Metacat v2.0.5 has changed the name of the systemmetadata* tables to accommodate Oracle
61
 * length restrictions. The table names have been updated in this utility to enable
62
 * users to run this utility on v2.0.5+. You can only run this version of the utility on 
63
 * Metacat deployments that have been upgraded to 2.0.5+
64
 * 
65
 * @author jones
66
 */
67
public class GenerateGlobalIdentifiers implements UpgradeUtilityInterface {
68

    
69
	private static Log log = LogFactory.getLog(GenerateGlobalIdentifiers.class);
70
    private String driver = null;
71
    private String url = null;
72
    private String user = null;
73
    private String password = null;
74
//    private String eziduser = null;
75
//    private String ezidPassword = null;
76

    
77
    /**
78
     * The main upgrade() procedure, which first upgrades identifiers in the database,
79
     * then registers these with EZID.
80
     */
81
    public boolean upgrade() throws AdminException {
82
        boolean success = false;
83
        log.debug("Upgrading identifiers to DOIs");
84
        
85
        // Check if assignDOIs is true, and if EZID is configured
86
        boolean shouldUpgrade = false;
87
        try {
88
            shouldUpgrade = new Boolean(PropertyService.getProperty("guid.assignGUIDs")).booleanValue();
89
            if (shouldUpgrade) {
90
                log.debug("Upgrading identifiers to DOIs");
91

    
92
                success = updateIdentifierReferences();                
93
            }
94

    
95
        } catch (PropertyNotFoundException e) {
96
            // When the guid properties are not found, do nothing
97
            shouldUpgrade = false;
98
            success = true;
99
        }
100
        		
101
    	return success;
102
    }
103
    
104
    /**
105
     * Loop through the servers registered in this instance of Metacat, and for
106
     * each server determine if we should convert to GUIDs for the documents associated
107
     * with that server.  If so, convert the identifier table to use a GUID for all
108
     * documents for that server found in xml_documents and xml_replication.
109
     * @return true if all conversions succeed
110
     * @throws AdminException
111
     */
112
    private boolean updateIdentifierReferences() throws AdminException {
113

    
114
        log.debug("Updating identifier references...");
115

    
116
        try {
117
            driver = PropertyService.getProperty("database.driver");
118
            url = PropertyService.getProperty("database.connectionURI");
119
            user = PropertyService.getProperty("database.user");
120
            password = PropertyService.getProperty("database.password");
121
//            eziduser = PropertyService.getProperty("guid.ezid.username");
122
//            ezidPassword = PropertyService.getProperty("guid.ezid.password");
123
            
124
        } catch (PropertyNotFoundException e) {
125
            String msg = "PropertyNotFound while trying to convert identifiers: " + e.getMessage();
126
            log.error(msg, e);
127
            throw new AdminException(msg);
128
        }
129

    
130
        // Create a connection, and start a transaction for which FK constraint checking is deferred
131
        Connection con = openDatabaseConnection();
132
        deferConstraints(con);
133
        // Copy the identifier.guid column for later use
134
        createDuplicateGUIDColumn(con);
135

    
136
        // Look up the maximum server number from the database
137
        int maxServerId = lookupMaxServers(con);
138
        
139
        // Loop across each replica server, checking if it is to have DOIs assigned
140
        // This is determined by the presence of a shoulder in the configuration file for
141
        // that server number
142
        for (int server = 1; server <= maxServerId; server++) {
143
            
144
            // Get the server's shoulder, or skip if not found
145
            try {
146
                String shoulder = PropertyService.getProperty("guid.ezid.doishoulder." + server);
147
                log.debug("Processing shoulder for server " + server + ": " + shoulder);
148

    
149
                // Update identifiers table (but only for local documents, not replicated documents)
150
                convertIdentifierTable(con, server, shoulder);
151
                
152
            } catch (PropertyNotFoundException e) {
153
                // The shoulder was not found for this server, so we just skip it
154
                log.debug("No shoulder found: " + e.getMessage());
155
            }
156
        }
157
        
158
        // Convert other tables once, after the identifier table conversion has established the new
159
        // global identifiers to be used
160
        // This incudes: xml_access table, including accessfileid field, systemmetadata, 
161
        // smreplicationpolicy,and smreplicationstatus tables
162
        convertIdentifierField(con, "xml_access", "guid");
163
        convertIdentifierField(con, "xml_access", "accessfileid");
164
        convertIdentifierField(con, "systemmetadata", "guid");
165
        convertIdentifierField(con, "smreplicationpolicy", "guid");
166
        convertIdentifierField(con, "smreplicationstatus", "guid");
167
        convertIdentifierField(con, "smmediatypeproperties", "guid");
168

    
169
        // Now clean up the temporary column we created, but note this makes it much
170
        // harder to revert our changes
171
        dropDuplicateGUIDColumn(con);
172
        
173
        commitAndCloseConnection(con);
174

    
175
        /*      
176
        // The following SQL commands will revert the changes made by this script as long as the identifier.old_guid column is intact
177
        start transaction;
178
        update smreplicationstatus set guid = id.old_guid FROM identifier id WHERE smreplicationstatus.guid = id.guid;
179
        update smreplicationpolicy set guid = id.old_guid FROM identifier id WHERE smreplicationpolicy.guid = id.guid;
180
        update systemmetadata set guid = id.old_guid FROM identifier id WHERE systemmetadata.guid = id.guid;
181
        update xml_access set accessfileid = id.old_guid FROM identifier id WHERE xml_access.accessfileid = id.guid;
182
        update xml_access set guid = id.old_guid FROM identifier id WHERE xml_access.guid = id.guid;
183
        update identifier set guid = old_guid;
184
        alter table identifier drop column old_guid;
185
        commit;
186
        */
187
        
188
        return true;
189
    }
190

    
191
    /**
192
     * Open a JDBC connection that will be used for all of the subsequent methods that
193
     * access or modify the database.  All changes are done in one transaction, so need
194
     * to be done using a single connection.
195
     * @return the Connection created
196
     * @throws AdminException
197
     */
198
    private Connection openDatabaseConnection() throws AdminException {
199
        Connection con = null;
200
            try {
201
                Driver d = (Driver) Class.forName(driver).newInstance();
202
                DriverManager.registerDriver(d);
203
                con = DriverManager.getConnection(url, user, password);
204
            } catch (InstantiationException e) {
205
                String msg = "InstantiationException updating creating DB connection: " + e.getMessage();
206
                log.error(msg, e);
207
                throw new AdminException(msg);
208
            } catch (IllegalAccessException e) {
209
                String msg = "IllegalAccessException updating creating DB connection: " + e.getMessage();
210
                log.error(msg, e);
211
                throw new AdminException(msg);
212
            } catch (ClassNotFoundException e) {
213
                String msg = "ClassNotFoundException updating creating DB connection: " + e.getMessage();
214
                log.error(msg, e);
215
                throw new AdminException(msg);
216
            } catch (SQLException e) {
217
                String msg = "SQLException updating creating DB connection: " + e.getMessage();
218
                log.error(msg, e);
219
                throw new AdminException(msg);
220
            }
221
            
222
            return con;
223
    }
224

    
225
    /**
226
     * Start a transaction on the given connection, and defer all constraints so
227
     * that changes to foreign keys will not be checked until after all of the 
228
     * identifiers have been updated in all tables.  Without this, we would get
229
     * FK constraint violations as we changed the tables.
230
     * @param con the connection to the database
231
     * @throws AdminException
232
     */
233
    private void deferConstraints(Connection con) throws AdminException {
234
                
235
        try {
236
            con.setAutoCommit(false);
237
            Statement sqlStatement = con.createStatement();
238
            boolean hasResults = sqlStatement.execute("SET CONSTRAINTS ALL DEFERRED;");
239
            log.debug("Constraints Deferred.");
240
        } catch (SQLException e) {
241
            String msg = "SQLException while disabling constraints: " + e.getMessage();
242
            log.error(msg, e);
243
            throw new AdminException(msg);
244
        }
245
    }
246

    
247
    /**
248
     * Temporarily save the current identifier.guid in a backup column (old_guid), to be used
249
     * for updating additional tables. 
250
     * @param con the connection to the database with an open transaction
251
     * @throws AdminException
252
     */
253
    private void createDuplicateGUIDColumn(Connection con) throws AdminException {
254
        try {
255
            Statement sqlStatement = con.createStatement();
256
            boolean hasResults = sqlStatement.execute("ALTER TABLE identifier ADD COLUMN old_guid TEXT;");
257
            log.debug("old_guid column added.");
258
            sqlStatement = con.createStatement();
259
            hasResults = sqlStatement.execute("UPDATE identifier SET old_guid = guid;");
260
            log.debug("Copied original identifiers to old_guid column.");
261
        } catch (SQLException e) {
262
            String msg = "SQLException while duplicating GUID column: " + e.getMessage();
263
            log.error(msg, e);
264
            throw new AdminException(msg);
265
        }        
266
    }
267
    
268
    /**
269
     * Drop the temporary GUID backup column (old_guid) once upgrading identifiers
270
     * is complete. 
271
     * @param con the connection to the database with an open transaction
272
     * @throws AdminException
273
     */
274
    private void dropDuplicateGUIDColumn(Connection con) throws AdminException {
275
        try {
276
            Statement sqlStatement = con.createStatement();
277
            boolean hasResults = sqlStatement.execute("ALTER TABLE identifier DROP COLUMN old_guid;");
278
            log.debug("old_guid column dropped.");
279
        } catch (SQLException e) {
280
            String msg = "SQLException dropping GUID column: " + e.getMessage();
281
            log.error(msg, e);
282
            throw new AdminException(msg);
283
        }        
284
    }
285
    
286
    /**
287
     * Query the database to determine which servers are registered and can be
288
     * inspected for potential identifier upgrades.
289
     * @param con the connection to the database with an open transaction
290
     * @throws AdminException
291
     */
292
    private int lookupMaxServers(Connection con) throws AdminException {
293
        int maxServers = 0;
294
        try {
295
            Statement sqlStatement = con.createStatement();
296
            boolean hasResults = sqlStatement.execute("SELECT max(serverid) AS maxserverid FROM xml_replication;");
297
            if (hasResults) {
298
                ResultSet rs = sqlStatement.getResultSet();
299
                if (rs.next()) {
300
                    maxServers = rs.getInt(1);
301
                    log.debug("MaxServerID: " + maxServers);
302
                } else {
303
                    String msg = "Could not determine max serverid; database query cursor had zero rows.";
304
                    log.error(msg);
305
                    throw new AdminException(msg);
306
                }
307
            } else {
308
                String msg = "Could not determine max serverid; database query failed to return results.";
309
                log.error(msg);
310
                throw new AdminException(msg);
311
            }
312
            
313
        } catch (SQLException e) {
314
            String msg = "SQLException while looking up serverId: " + e.getMessage();
315
            log.error(msg, e);
316
            throw new AdminException(msg);
317
        }        
318
        return maxServers;
319
    }
320

    
321
    /**
322
     * Convert identifiers to DOI GUIDs in the identifier table for any documents found in
323
     * xml_documents or xml_revisions that reside on a server for which a DOI shoulder prefix
324
     * has been assigned.
325
     * 
326
     * @param con the connection to the database with an open transaction
327
     * @param server the id of the server whose documents are to be converted
328
     * @param shoulder the DOI shoulder to be used for converting
329
     * @return true if conversion is successful
330
     * @throws AdminException
331
     */
332
    private boolean convertIdentifierTable(Connection con, int server, String shoulder) throws AdminException {
333
        PreparedStatement pstmt = null;
334
        try {
335
            
336
            String sql_1 =
337
                "UPDATE identifier " + 
338
                "SET    guid = p.doi " + 
339
                "FROM (" +
340
                       "SELECT  guid, ? || guid as doi " + 
341
                       "FROM    identifier id2, ";
342
            String sql_2 =
343
                " xd " +
344
                       "WHERE   id2.docid = xd.docid AND id2.rev = xd.rev " +   
345
                       "AND xd.server_location = ? " +
346
                     ") p " +
347
                "WHERE identifier.guid = p.guid;";         
348

    
349
            // Convert identifiers found in xml_documents
350
            pstmt = con.prepareStatement(sql_1 + "xml_documents" + sql_2);
351
            pstmt.setString(1, shoulder);
352
            pstmt.setInt(2, server);
353
            pstmt.execute();
354
            
355
            // Convert identifiers found in xml_revisions
356
            pstmt = con.prepareStatement(sql_1 + "xml_revisions" + sql_2);
357
            pstmt.setString(1, shoulder);
358
            pstmt.setInt(2, server);
359
            pstmt.execute();
360
            
361
            log.debug("Finished shoulder for server " + server + ": " + shoulder);
362

    
363
        } catch (SQLException e) {
364
            String msg = "SQLException updating identifier table: " + e.getMessage();
365
            log.error(msg, e);
366
            throw new AdminException(msg);
367
        }
368

    
369
        return true;
370
    }
371

    
372
    /**
373
     * Convert the given field in the given table to use the new identifier.guid that
374
     * has been assigned.
375
     * 
376
     * @param con the connection to the database with an open transaction
377
     * @param table the name of the table with identifiers to be converted
378
     * @param field the name of the field containing identifiers to be converted
379
     * @return true if conversion was successful
380
     * @throws AdminException
381
     */
382
    private boolean convertIdentifierField(Connection con, String table, String field) throws AdminException {
383
        PreparedStatement pstmt = null;
384
        try {
385

    
386
            String sql_1 =
387
                "UPDATE " + table + " " +
388
                "SET " + field + " = id.guid " + 
389
                "FROM identifier id " +
390
                "WHERE " + table + "."+ field + " = id.old_guid;";
391
            
392
            // Convert identifiers found in the given table and field
393
            pstmt = con.prepareStatement(sql_1);
394
            pstmt.execute();
395
                        
396
            log.debug("Finished converting table " + table + " (" + field +")" );
397

    
398
        } catch (SQLException e) {
399
            String msg = "SQLException while converting identifier field: " + e.getMessage();
400
            log.error(msg, e);
401
            throw new AdminException(msg);
402
        }
403

    
404
        return true;
405
    }
406

    
407
    /**
408
     * Commit the current transaction on the connection, and close the connection.
409
     * @param con the connection to the database with an open transaction
410
     * @return true if the connection is successfully closed
411
     * @throws AdminException
412
     */
413
    private boolean commitAndCloseConnection(Connection con) throws AdminException {
414
        try {
415
                        
416
            log.debug("Committing and closing connection.");
417
            con.commit();
418
            con.close();
419
        } catch (SQLException e) {
420
            String msg = "SQLException while commiting and closing connection: " + e.getMessage();
421
            log.error(msg, e);
422
            throw new AdminException(msg);
423
        } 
424
        
425
        return true;
426
    }
427

    
428
    /**
429
     * Main method, solely for testing.  Not used in ordinary operation.
430
     */
431
    public static void main(String [] ags){
432

    
433
        try {
434
        	// set up the properties based on the test/deployed configuration of the workspace
435
        	SortedProperties testProperties = 
436
				new SortedProperties("test/test.properties");
437
			testProperties.load();
438
			String metacatContextDir = testProperties.getProperty("metacat.contextDir");
439
			PropertyService.getInstance(metacatContextDir + "/WEB-INF");
440
			// now run it
441
            GenerateGlobalIdentifiers upgrader = new GenerateGlobalIdentifiers();
442
	        upgrader.upgrade();
443
        } catch (IOException e) {
444
            e.printStackTrace();
445
        } catch (PropertyNotFoundException e) {
446
            e.printStackTrace();
447
        } catch (ServiceException e) {
448
            e.printStackTrace();
449
        } catch (AdminException e) {
450
            e.printStackTrace();
451
        }
452
    }
453
}
(1-1/14)