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
 * @author jones
61
 */
62
public class GenerateGlobalIdentifiers implements UpgradeUtilityInterface {
63

    
64
	private static Log log = LogFactory.getLog(GenerateGlobalIdentifiers.class);
65
    private String driver = null;
66
    private String url = null;
67
    private String user = null;
68
    private String password = null;
69
//    private String eziduser = null;
70
//    private String ezidPassword = null;
71

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

    
87
                success = updateIdentifierReferences();                
88
            }
89

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

    
109
        log.debug("Updating identifier references...");
110

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

    
125
        // Create a connection, and start a transaction for which FK constraint checking is deferred
126
        Connection con = openDatabaseConnection();
127
        deferConstraints(con);
128
        // Copy the identifier.guid column for later use
129
        createDuplicateGUIDColumn(con);
130

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

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

    
163
        // Now clean up the temporary column we created, but note this makes it much
164
        // harder to revert our changes
165
        dropDuplicateGUIDColumn(con);
166
        
167
        commitAndCloseConnection(con);
168

    
169
        /*      
170
        // The following SQL commands will revert the changes made by this script as long as the identifier.old_guid column is intact
171
        start transaction;
172
        update systemmetadatareplicationstatus set guid = id.old_guid FROM identifier id WHERE systemmetadatareplicationstatus.guid = id.guid;
173
        update systemmetadatareplicationpolicy set guid = id.old_guid FROM identifier id WHERE systemmetadatareplicationpolicy.guid = id.guid;
174
        update systemmetadata set guid = id.old_guid FROM identifier id WHERE systemmetadata.guid = id.guid;
175
        update xml_access set accessfileid = id.old_guid FROM identifier id WHERE xml_access.accessfileid = id.guid;
176
        update xml_access set guid = id.old_guid FROM identifier id WHERE xml_access.guid = id.guid;
177
        update identifier set guid = old_guid;
178
        alter table identifier drop column old_guid;
179
        commit;
180
        */
181
        
182
        return true;
183
    }
184

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

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

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

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

    
343
            // Convert identifiers found in xml_documents
344
            pstmt = con.prepareStatement(sql_1 + "xml_documents" + sql_2);
345
            pstmt.setString(1, shoulder);
346
            pstmt.setInt(2, server);
347
            pstmt.execute();
348
            
349
            // Convert identifiers found in xml_revisions
350
            pstmt = con.prepareStatement(sql_1 + "xml_revisions" + sql_2);
351
            pstmt.setString(1, shoulder);
352
            pstmt.setInt(2, server);
353
            pstmt.execute();
354
            
355
            log.debug("Finished shoulder for server " + server + ": " + shoulder);
356

    
357
        } catch (SQLException e) {
358
            String msg = "SQLException updating identifier table: " + e.getMessage();
359
            log.error(msg, e);
360
            throw new AdminException(msg);
361
        }
362

    
363
        return true;
364
    }
365

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

    
380
            String sql_1 =
381
                "UPDATE " + table + " " +
382
                "SET " + field + " = id.guid " + 
383
                "FROM identifier id " +
384
                "WHERE " + table + "."+ field + " = id.old_guid;";
385
            
386
            // Convert identifiers found in the given table and field
387
            pstmt = con.prepareStatement(sql_1);
388
            pstmt.execute();
389
                        
390
            log.debug("Finished converting table " + table + " (" + field +")" );
391

    
392
        } catch (SQLException e) {
393
            String msg = "SQLException while converting identifier field: " + e.getMessage();
394
            log.error(msg, e);
395
            throw new AdminException(msg);
396
        }
397

    
398
        return true;
399
    }
400

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

    
422
    /**
423
     * Main method, solely for testing.  Not used in ordinary operation.
424
     */
425
    public static void main(String [] ags){
426

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