Project

General

Profile

1
/**
2
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
3
 * License, Version 2.0 (the "License"); you may not use this file except in
4
 * compliance with the License. You may obtain a copy of the License at
5
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
6
 * agreed to in writing, software distributed under the License is distributed on
7
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
8
 * or implied. See the License for the specific language governing permissions and
9
 * limitations under the License.
10
 */
11
package edu.ucsb.nceas.metacat.oaipmh.provider.server.catalog;
12

    
13
import java.io.File;
14
import java.io.IOException;
15
import java.io.Reader;
16
import java.sql.Connection;
17
import java.sql.DriverManager;
18
import java.sql.ResultSet;
19
import java.sql.SQLException;
20
import java.sql.SQLWarning;
21
import java.sql.Statement;
22
import java.text.SimpleDateFormat;
23
import java.util.ArrayList;
24
import java.util.Date;
25
import java.util.HashMap;
26
import java.util.Iterator;
27
import java.util.Map;
28
import java.util.NoSuchElementException;
29
import java.util.Properties;
30
import java.util.StringTokenizer;
31
import java.util.Vector;
32

    
33
import org.apache.log4j.Logger;
34

    
35
import edu.ucsb.nceas.metacat.client.DocumentNotFoundException;
36
import edu.ucsb.nceas.metacat.client.InsufficientKarmaException;
37
import edu.ucsb.nceas.metacat.client.Metacat;
38
import edu.ucsb.nceas.metacat.client.MetacatException;
39
import edu.ucsb.nceas.metacat.client.MetacatFactory;
40
import edu.ucsb.nceas.metacat.client.MetacatInaccessibleException;
41
import edu.ucsb.nceas.metacat.oaipmh.provider.server.OAIHandler;
42
import edu.ucsb.nceas.metacat.util.SystemUtil;
43
import edu.ucsb.nceas.utilities.IOUtil;
44
import edu.ucsb.nceas.utilities.PropertyNotFoundException;
45

    
46
import ORG.oclc.oai.server.catalog.AbstractCatalog;
47
import ORG.oclc.oai.server.catalog.RecordFactory;
48
import ORG.oclc.oai.server.verb.BadResumptionTokenException;
49
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
50
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
51
import ORG.oclc.oai.server.verb.NoItemsMatchException;
52
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
53
import ORG.oclc.oai.server.verb.NoSetHierarchyException;
54
import ORG.oclc.oai.server.verb.OAIInternalServerError;
55

    
56

    
57
/**
58
 * MetacatCatalog is an implementation of AbstractCatalog interface.
59
 * 
60
 * @author Ralph LeVan, OCLC Online Computer Library Center
61
 */
62

    
63
public class MetacatCatalog extends AbstractCatalog {
64
  
65
  /* Class fields */
66
  
67
  private static final Logger logger = Logger.getLogger(MetacatCatalog.class);
68
  private static String refreshDate = null;
69

    
70
  /** Database connection */
71
  private static String metacatDBDriver;
72
  private static String metacatDBURL;
73
  private static String metacatDBUser;
74
  private static String metacatDBPassword;
75
  private static String metacatURL;
76
  
77

    
78
  /* Instance fields */
79
  
80
  protected String homeDir;
81
  private HashMap<String, String> dateMap = new HashMap<String, String>();
82
  private HashMap<String, String> filteredDateMap = null;
83
  private HashMap<String, String> docTypeMap = new HashMap<String, String>();
84
  private HashMap resumptionResults = new HashMap();
85
  private int maxListSize;
86
  
87
  /*
88
   * QUERY string to find all eml-2.x.y documents in the Metacat database
89
   * that are publicly accessible
90
   */
91
  private final String QUERY =
92
  "SELECT docid, doctype, date_updated " +
93
  "FROM xml_documents " +
94
  "WHERE doctype like 'eml://ecoinformatics.org/eml-2%' AND " + 
95
  "  (docid IN " +
96
  "     (SELECT docid " +
97
  "      FROM xml_access " +
98
  "      WHERE( (lower(principal_name) = 'public') AND " +
99
  "             perm_type = 'allow' AND " +
100
  "             permission > 3" +
101
  "           )" +
102
  "     )" +
103
  "   AND " +
104
  "   docid NOT IN " +
105
  "     (SELECT docid " +
106
  "      FROM xml_access " +
107
  "      WHERE( (lower(principal_name) = 'public') AND " +
108
  "             perm_type = 'deny' AND " +
109
  "             perm_order ='allowFirst' AND " +
110
  "             permission > 3" +
111
  "           )" +
112
  "     )" +
113
  "  )";
114
  
115
  
116
  /*
117
   * QUERY string to determine the most recent 'date_updated' value stored
118
   * in Metacat's 'xml_documents' table.
119
   */
120
  private final String LAST_UPDATED_QUERY =
121
  "SELECT MAX(date_updated) " +
122
  "FROM xml_documents " +
123
  "WHERE doctype like 'eml://ecoinformatics.org/eml-2%' AND " + 
124
  "  (docid IN " +
125
  "     (SELECT docid " +
126
  "      FROM xml_access " +
127
  "      WHERE( (lower(principal_name) = 'public') AND " +
128
  "             perm_type = 'allow' AND " +
129
  "             permission > 3" +
130
  "           )" +
131
  "     )" +
132
  "   AND " +
133
  "   docid NOT IN " +
134
  "     (SELECT docid " +
135
  "      FROM xml_access " +
136
  "      WHERE( (lower(principal_name) = 'public') AND " +
137
  "             perm_type = 'deny' AND " +
138
  "             perm_order ='allowFirst' AND " +
139
  "             permission > 3" +
140
  "           )" +
141
  "     )" +
142
  "  )";
143
  
144
  
145
/* Constructors */
146
  
147
  public MetacatCatalog(Properties properties) {
148
    String errorStr;
149
    String temp;
150

    
151
    temp = properties.getProperty("oaipmh.maxListSize");
152
    if (temp == null) {
153
      errorStr = "oaipmh.maxListSize is missing from the properties file";
154
      throw new IllegalArgumentException(errorStr);
155
    }
156
    maxListSize = Integer.parseInt(temp);
157
    
158
    metacatDBDriver = properties.getProperty("database.driver");
159
    metacatDBURL = properties.getProperty("database.connectionURI");
160
    metacatDBUser = properties.getProperty("database.user");
161
    metacatDBPassword = properties.getProperty("database.password");
162
    
163
    try {
164
      if (OAIHandler.isIntegratedWithMetacat()) {
165
        metacatURL = SystemUtil.getServletURL();
166
      }
167
      else {
168
        metacatURL = properties.getProperty("test.metacatUrl");
169
      }
170
      
171
      logger.warn("metacatURL: " + metacatURL);
172
    }
173
    catch (PropertyNotFoundException e) {
174
      logger.error("PropertyNotFoundException: " + 
175
             "unable to determine metacat URL from SystemUtil.getServletURL()");
176
    }
177

    
178
    loadCatalog();
179
  }
180

    
181
  
182
  /* Class methods */
183
  
184
  /**
185
   * Use the current date as the basis for the resumptiontoken
186
   * 
187
   * @return a long integer version of the current time
188
   */
189
  private synchronized static String getRSName() {
190
    Date now = new Date();
191
    return Long.toString(now.getTime());
192
  }
193

    
194
  
195
  /* Instance methods */
196

    
197
  
198
  /**
199
   * close the repository
200
   */
201
  public void close() {
202
  }
203

    
204

    
205
  /**
206
   * Utility method to construct a Record object for a specified metadataFormat
207
   * from a native record
208
   * 
209
   * @param nativeItem
210
   *          native item from the dataase
211
   * @param metadataPrefix
212
   *          the desired metadataPrefix for performing the crosswalk
213
   * @return the <record/> String
214
   * @exception CannotDisseminateFormatException
215
   *              the record is not available for the specified metadataPrefix.
216
   */
217
  private String constructRecord(HashMap nativeItem, String metadataPrefix)
218
      throws CannotDisseminateFormatException {
219
    String schemaURL = null;
220
    Iterator setSpecs = getSetSpecs(nativeItem);
221
    Iterator abouts = getAbouts(nativeItem);
222

    
223
    if (metadataPrefix != null) {
224
      if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
225
        throw new CannotDisseminateFormatException(metadataPrefix);
226
    }
227
    
228
    RecordFactory recordFactory = getRecordFactory();
229
    String recordString = recordFactory.create(nativeItem, schemaURL, 
230
                                              metadataPrefix, setSpecs, abouts);
231
    return recordString;
232
  }
233
  
234
  
235
  /**
236
   * Using the original dateMap catalog, produce a filtered dateMap catalog
237
   * consisting of only those entries that match the 'from', 'until', and
238
   * 'metadataPrefix' criteria.
239
   * 
240
   * @param from                 the from date, e.g. "2008-06-01"
241
   * @param until                the until date, e.g. "2009-01-01"
242
   * @param metadataPrefix       the metadataPrefix value, e.g. "oai_dc"
243
   * 
244
   * @return   aDateMap, a HashMap containing only the matched entries.
245
   */
246
  private HashMap<String, String> filterDateMap(String from, String until,
247
      String metadataPrefix) {
248
    
249
    if (shouldRefreshCatalog()) {
250
      loadCatalog();
251
    }
252
    
253
    HashMap<String, String> aDateMap = new HashMap<String, String>();
254
    Iterator iterator = dateMap.entrySet().iterator();
255

    
256
    while (iterator.hasNext()) {
257
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
258
      String dateUpdated = (String) entryDateMap.getValue();
259

    
260
      /*
261
       * First filter catalog entries based on whether their date updated falls
262
       * within the 'from' and 'until' parameters.
263
       */
264
      if (dateUpdated.compareTo(from) >= 0 && dateUpdated.compareTo(until) <= 0) 
265
      {
266
        String docid = (String) entryDateMap.getKey();
267
        HashMap<String, String> nativeHeader = getNativeHeader(docid);
268
        String doctype = nativeHeader.get("doctype");
269

    
270
        /*
271
         * Next filter catalog entries based on Metacat doctype as compared to
272
         * OAI-PMH metadataPrefix.
273
         */
274
        if (isIncludedDoctype(doctype, metadataPrefix)) {
275
          aDateMap.put(docid, dateUpdated);
276
        }
277
      }
278

    
279
    }
280

    
281
    return aDateMap;
282
  }
283

    
284

    
285
  /**
286
   * get an Iterator containing the abouts for the nativeItem
287
   * 
288
   * @param rs
289
   *          ResultSet containing the nativeItem
290
   * @return an Iterator containing the list of about values for this nativeItem
291
   */
292
  private Iterator getAbouts(HashMap nativeItem) {
293
    return null;
294
  }
295

    
296

    
297
  /**
298
   * Returns a connection to the database. Opens the connection if a connection
299
   * has not already been made previously.
300
   * 
301
   * @return  conn  the database Connection object
302
   */
303
  public Connection getConnection() {
304
    Connection conn = null;
305
    
306
    try {
307
      Class.forName(metacatDBDriver);
308
    }
309
    catch (ClassNotFoundException e) {
310
      logger.error("Can't load driver " + e);
311
      return conn;
312
    } 
313

    
314
    // Make the database connection
315
    try {
316
      conn = DriverManager.getConnection(metacatDBURL, metacatDBUser, 
317
                                           metacatDBPassword);
318

    
319
      // If a SQLWarning object is available, print its warning(s).
320
      // There may be multiple warnings chained.
321
      SQLWarning warn = conn.getWarnings();
322
      
323
      if (warn != null) {
324
        while (warn != null) {
325
          logger.warn("SQLState: " + warn.getSQLState());
326
          logger.warn("Message:  " + warn.getMessage());
327
          logger.warn("Vendor: " + warn.getErrorCode());
328
          warn = warn.getNextWarning();
329
        }
330
      }
331
    }
332
    catch (SQLException e) {
333
      logger.error("Database access failed " + e);
334
    }
335
    
336
    return conn;
337
  }
338

    
339

    
340
  /**
341
   * Get the most recent date that the xml_documents table was updated
342
   * @return
343
   */
344
  public String getMaxDateUpdated() {
345
    String maxDateUpdated = null;
346
    String query = 
347
              "SELECT MAX(date_updated) AS max_date_updated FROM xml_documents";
348
    Statement stmt;
349

    
350
    try {
351
      Connection conn = getConnection();    
352
      if (conn != null) {
353
        stmt = conn.createStatement();                          
354
        ResultSet rs = stmt.executeQuery(query);
355
        while (rs.next()) {
356
          maxDateUpdated = rs.getDate("max_date_updated").toString();
357
        }
358
        stmt.close();   
359
        conn.close();
360
      }
361
    }
362
    catch(SQLException e) {
363
      logger.error("SQLException: " + e.getMessage());
364
    }
365
    
366
    return maxDateUpdated;
367
  }
368
  
369
  
370
  /**
371
   * Get a document from Metacat.
372
   * 
373
   * @param docid  the docid of the document to read
374
   * 
375
   * @return recordMap       a HashMap holding the document contents
376
   * 
377
   * @throws IOException
378
   */
379
  private HashMap<String, String> getMetacatDocument(String docid) 
380
      throws IOException {
381
    HashMap<String, String> recordMap = getNativeHeader(docid);
382
    
383
    if (recordMap == null) {
384
      return null;
385
    } 
386
    else {
387
      try {
388
        /* Perform a Metacat read operation on this docid */
389
        Metacat metacat = MetacatFactory.createMetacatConnection(metacatURL);
390
        Reader reader = metacat.read(docid);
391
        StringBuffer stringBuffer = IOUtil.getAsStringBuffer(reader, true);
392
        String emlString = stringBuffer.toString();
393
        recordMap.put("recordBytes", emlString);
394
      }
395
      catch (MetacatInaccessibleException e) {
396
        logger.error("MetacatInaccessibleException:\n" + e.getMessage());
397
      }
398
      catch (MetacatException e) {
399
        logger.error("MetacatException:\n" + e.getMessage());
400
      }
401
      catch (DocumentNotFoundException e) {
402
        logger.error("DocumentNotFoundException:\n" + e.getMessage());
403
      }
404
      catch (InsufficientKarmaException e) {
405
        logger.error("InsufficientKarmaException:\n" + e.getMessage());
406
      }
407
      catch (IOException e) {
408
        logger.error("Error reading EML document from metacat:\n" + 
409
                     e.getMessage()
410
                    );
411
      }
412
    }
413
    
414
    return recordMap;
415
  }
416

    
417

    
418
  private HashMap<String, String> getNativeHeader(String localIdentifier) {
419
    HashMap<String, String> recordMap = null;
420
    
421
    if (dateMap.containsKey(localIdentifier)) {
422
      recordMap = new HashMap<String, String>();
423
      recordMap.put("localIdentifier", localIdentifier);
424
      recordMap.put("lastModified", dateMap.get(localIdentifier));
425
      recordMap.put("doctype", docTypeMap.get(localIdentifier));
426
      return recordMap;
427
    }
428
    
429
    return recordMap;
430
  }
431

    
432

    
433
  /**
434
   * Retrieve the specified metadata for the specified oaiIdentifier
435
   * 
436
   * @param oaiIdentifier
437
   *          the OAI identifier
438
   * @param metadataPrefix
439
   *          the OAI metadataPrefix
440
   * @return the Record object containing the result.
441
   * @exception CannotDisseminateFormatException
442
   *              signals an http status code 400 problem
443
   * @exception IdDoesNotExistException
444
   *              signals an http status code 404 problem
445
   * @exception OAIInternalServerError
446
   *              signals an http status code 500 problem
447
   */
448
  public String getRecord(String oaiIdentifier, String metadataPrefix)
449
      throws IdDoesNotExistException, 
450
             CannotDisseminateFormatException,
451
             OAIInternalServerError 
452
  {
453
    HashMap<String, String> nativeItem = null;
454
    
455
    try {
456
      RecordFactory recordFactory = getRecordFactory();
457
      String localIdentifier = recordFactory.fromOAIIdentifier(oaiIdentifier);
458
      nativeItem = getMetacatDocument(localIdentifier);
459
      if (nativeItem == null) throw new IdDoesNotExistException(oaiIdentifier);
460
      return constructRecord(nativeItem, metadataPrefix);
461
    } 
462
    catch (IOException e) {
463
      e.printStackTrace();
464
      throw new OAIInternalServerError("Database Failure");
465
    }
466
  }
467

    
468

    
469
  /**
470
   * Retrieve a list of schemaLocation values associated with the specified
471
   * oaiIdentifier.
472
   * 
473
   * We get passed the ID for a record and are supposed to return a list of the
474
   * formats that we can deliver the record in. Since we are assuming that all
475
   * the records in the directory have the same format, the response to this is
476
   * static;
477
   * 
478
   * @param oaiIdentifier       the OAI identifier
479
   * 
480
   * @return a Vector containing schemaLocation Strings
481
   * 
482
   * @exception OAIBadRequestException
483
   *              signals an http status code 400 problem
484
   * @exception OAINotFoundException
485
   *              signals an http status code 404 problem
486
   * @exception OAIInternalServerError
487
   *              signals an http status code 500 problem
488
   */
489
  public Vector getSchemaLocations(String oaiIdentifier)
490
      throws IdDoesNotExistException, OAIInternalServerError,
491
      NoMetadataFormatsException {
492
    HashMap<String, String> nativeItem = null;
493
    
494
    try {
495
      String localIdentifier = getRecordFactory().fromOAIIdentifier(
496
          oaiIdentifier);
497
      nativeItem = getMetacatDocument(localIdentifier);
498
    } 
499
    catch (IOException e) {
500
      e.printStackTrace();
501
      throw new OAIInternalServerError("Database Failure");
502
    }
503

    
504
    if (nativeItem != null) {
505
      RecordFactory recordFactory = getRecordFactory();
506
      return recordFactory.getSchemaLocations(nativeItem);
507
    } 
508
    else {
509
      throw new IdDoesNotExistException(oaiIdentifier);
510
    }
511
  }
512

    
513

    
514
  /**
515
   * get an Iterator containing the setSpecs for the nativeItem
516
   * 
517
   * @param rs
518
   *          ResultSet containing the nativeItem
519
   * @return an Iterator containing the list of setSpec values for this
520
   *         nativeItem
521
   */
522
  private Iterator getSetSpecs(HashMap nativeItem) {
523
    return null;
524
  }
525

    
526

    
527
  /**
528
   * Should a document with the specified Metacat doctype be included in the
529
   * list of identifiers/records for the specified OAI-PMH metadataPrefix?
530
   * 
531
   * @param doctype              e.g. "eml://ecoinformatics.org/eml-2.1.0"
532
   * @param metadataPrefix       e.g. "oai_dc", "eml-2.0.1", "eml-2.1.0"
533
   * @return
534
   */
535
  private boolean isIncludedDoctype(String doctype, String metadataPrefix) {
536
    boolean isIncluded = false;
537
    
538
    /*
539
     * If the metadataPrefix is "oai_dc", then include all catalog entries
540
     * in the list of identifiers. Else if the metadataPrefix is an EML
541
     * document type, then only include those catalog entries whose
542
     * document type matches that of the metadataPrefix. 
543
     */
544
    if (doctype != null && 
545
        (metadataPrefix.equals("oai_dc") ||
546
         (doctype.startsWith("eml://ecoinformatics.org/eml-") && 
547
          doctype.endsWith(metadataPrefix)
548
         )
549
        )
550
       ) {
551
      isIncluded = true;
552
    }
553
 
554
    return isIncluded;
555
  }
556

    
557
  
558
  /**
559
   * Override this method if some files exist in the filesystem that aren't
560
   * metadata records.
561
   * 
562
   * @param child
563
   *          the File to be investigated
564
   * @return true if it contains metadata, false otherwise
565
   */
566
  protected boolean isMetadataFile(File child) {
567
    return true;
568
  }
569
  
570
 
571
  /**
572
   * Retrieve a list of Identifiers that satisfy the criteria parameters
573
   * 
574
   * @param from
575
   *          beginning date in the form of YYYY-MM-DD or null if earliest date
576
   *          is desired
577
   * @param until
578
   *          ending date in the form of YYYY-MM-DD or null if latest date is
579
   *          desired
580
   * @param set
581
   *          set name or null if no set is desired        
582
   * @param metadataPrefix       
583
   *          e.g. "oai_dc", "eml-2.0.1", "eml-2.1.0"
584
   *        
585
   * @return a Map object containing an optional "resumptionToken" key/value
586
   *         pair and an "identifiers" Map object. The "identifiers" Map
587
   *         contains OAI identifier keys with corresponding values of "true" or
588
   *         null depending on whether the identifier is deleted or not.
589
   * @exception OAIBadRequestException
590
   *              signals an http status code 400 problem
591
   */
592
  public Map listIdentifiers(String from, String until, String set,
593
                             String metadataPrefix) 
594
          throws NoItemsMatchException {
595
    purge(); // clean out old resumptionTokens
596
    
597
    Map<String, Object> listIdentifiersMap = new HashMap<String, Object>();
598
    ArrayList<String> headers = new ArrayList<String>();
599
    ArrayList<String> identifiers = new ArrayList<String>();
600
    
601
    filteredDateMap = filterDateMap(from, until, metadataPrefix);
602
    
603
    Iterator iterator = filteredDateMap.entrySet().iterator();
604
    int numRows = filteredDateMap.entrySet().size();
605
    int count = 0;
606
    RecordFactory recordFactory = getRecordFactory();
607
    
608
    while (count < maxListSize && iterator.hasNext()) {
609
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
610
      String dateUpdated = (String) entryDateMap.getValue();
611
      String key = (String) entryDateMap.getKey();
612
      HashMap<String, String> nativeHeader = getNativeHeader(key);
613
      String[] headerArray = recordFactory.createHeader(nativeHeader);
614
      
615
     /* 
616
      * header, e.g.
617
      * 
618
      * <header>
619
      *   <identifier>urn:lsid:knb.ecoinformatics.org:knb-lter-gce:26</identifier>
620
      *   <datestamp>2009-03-11</datestamp>
621
      * </header>
622
      */
623
      String header = headerArray[0];
624
      headers.add(header);
625
         
626
      /*
627
       * identifier, e.g. urn:lsid:knb.ecoinformatics.org:knb-lter-gce:26
628
       */
629
      String identifier = headerArray[1]; 
630
      identifiers.add(identifier);
631
      count++;
632
    }
633

    
634
    if (count == 0) { throw new NoItemsMatchException(); }
635

    
636
    /* decide if you're done */
637
    if (iterator.hasNext()) {
638
      String resumptionId = getRSName();
639
      resumptionResults.put(resumptionId, iterator);
640

    
641
      /*****************************************************************
642
       * Construct the resumptionToken String however you see fit.
643
       *****************************************************************/
644
      StringBuffer resumptionTokenSb = new StringBuffer();
645
      resumptionTokenSb.append(resumptionId);
646
      resumptionTokenSb.append(":");
647
      resumptionTokenSb.append(Integer.toString(count));
648
      resumptionTokenSb.append(":");
649
      resumptionTokenSb.append(Integer.toString(numRows));
650
      resumptionTokenSb.append(":");
651
      resumptionTokenSb.append(metadataPrefix);
652

    
653
      /*****************************************************************
654
       * Use the following line if you wish to include the optional
655
       * resumptionToken attributes in the response. Otherwise, use the line
656
       * after it that I've commented out.
657
       *****************************************************************/
658
      listIdentifiersMap.put("resumptionMap", getResumptionMap(
659
          resumptionTokenSb.toString(), numRows, 0));
660
      // listIdentifiersMap.put("resumptionMap",
661
      // getResumptionMap(resumptionTokenSb.toString()));
662
    }
663
    
664
    listIdentifiersMap.put("headers", headers.iterator());
665
    listIdentifiersMap.put("identifiers", identifiers.iterator());
666
    
667
    return listIdentifiersMap;
668
  }
669

    
670

    
671
  /**
672
   * Retrieve the next set of Identifiers associated with the resumptionToken
673
   * 
674
   * @param resumptionToken
675
   *          implementation-dependent format taken from the previous
676
   *          listIdentifiers() Map result.
677
   * @return a Map object containing an optional "resumptionToken" key/value
678
   *         pair and an "identifiers" Map object. The "identifiers" Map
679
   *         contains OAI identifier keys with corresponding values of "true" or
680
   *         null depending on whether the identifier is deleted or not.
681
   * @exception OAIBadRequestException
682
   *              signals an http status code 400 problem
683
   */
684
  public Map listIdentifiers(String resumptionToken)
685
      throws BadResumptionTokenException {
686
    purge(); // clean out old resumptionTokens
687
    Map listIdentifiersMap = new HashMap();
688
    ArrayList headers = new ArrayList();
689
    ArrayList identifiers = new ArrayList();
690

    
691
    /**********************************************************************
692
     * parse your resumptionToken and look it up in the resumptionResults, if
693
     * necessary
694
     **********************************************************************/
695
    StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
696
    String resumptionId;
697
    int oldCount;
698
    String metadataPrefix;
699
    int numRows;
700
    try {
701
      resumptionId = tokenizer.nextToken();
702
      oldCount = Integer.parseInt(tokenizer.nextToken());
703
      numRows = Integer.parseInt(tokenizer.nextToken());
704
      metadataPrefix = tokenizer.nextToken();
705
    } catch (NoSuchElementException e) {
706
      throw new BadResumptionTokenException();
707
    }
708

    
709
    /* Get some more records from your database */
710
    Iterator iterator = (Iterator) resumptionResults.remove(resumptionId);
711
    if (iterator == null) {
712
      System.out
713
          .println("MetacatCatalog.listIdentifiers(): reuse of old resumptionToken?");
714
      iterator = dateMap.entrySet().iterator();
715
      for (int i = 0; i < oldCount; ++i)
716
        iterator.next();
717
    }
718

    
719
    /* load the headers and identifiers ArrayLists. */
720
    int count = 0;
721
    while (count < maxListSize && iterator.hasNext()) {
722
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
723
      HashMap nativeHeader = getNativeHeader((String) entryDateMap.getKey());
724
      String[] header = getRecordFactory().createHeader(nativeHeader);
725
      headers.add(header[0]);
726
      identifiers.add(header[1]);
727
      count++;
728
    }
729

    
730
    /* decide if you're done. */
731
    if (iterator.hasNext()) {
732
      resumptionId = getRSName();
733
      resumptionResults.put(resumptionId, iterator);
734

    
735
      /*****************************************************************
736
       * Construct the resumptionToken String however you see fit.
737
       *****************************************************************/
738
      StringBuffer resumptionTokenSb = new StringBuffer();
739
      resumptionTokenSb.append(resumptionId);
740
      resumptionTokenSb.append(":");
741
      resumptionTokenSb.append(Integer.toString(oldCount + count));
742
      resumptionTokenSb.append(":");
743
      resumptionTokenSb.append(Integer.toString(numRows));
744
      resumptionTokenSb.append(":");
745
      resumptionTokenSb.append(metadataPrefix);
746

    
747
      /*****************************************************************
748
       * Use the following line if you wish to include the optional
749
       * resumptionToken attributes in the response. Otherwise, use the line
750
       * after it that I've commented out.
751
       *****************************************************************/
752
      listIdentifiersMap.put("resumptionMap", getResumptionMap(
753
          resumptionTokenSb.toString(), numRows, oldCount));
754
      // listIdentifiersMap.put("resumptionMap",
755
      // getResumptionMap(resumptionTokenSb.toString()));
756
    }
757

    
758
    listIdentifiersMap.put("headers", headers.iterator());
759
    listIdentifiersMap.put("identifiers", identifiers.iterator());
760
    return listIdentifiersMap;
761
  }
762

    
763

    
764
  /**
765
   * Retrieve a list of records that satisfy the specified criteria
766
   * 
767
   * @param from
768
   *          beginning date in the form of YYYY-MM-DD or null if earliest date
769
   *          is desired
770
   * @param until
771
   *          ending date in the form of YYYY-MM-DD or null if latest date is
772
   *          desired
773
   * @param set
774
   *          set name or null if no set is desired
775
   * @param metadataPrefix       
776
   *          e.g. "oai_dc", "eml-2.0.1", "eml-2.1.0"
777
   *        
778
   * @return a Map object containing an optional "resumptionToken" key/value
779
   *         pair and a "records" Iterator object. The "records" Iterator
780
   *         contains a set of Records objects.
781
   * @exception OAIBadRequestException
782
   *              signals an http status code 400 problem
783
   * @exception OAIInternalServerError
784
   *              signals an http status code 500 problem
785
   */
786
  public Map listRecords(String from, String until, String set,
787
                         String metadataPrefix) 
788
      throws CannotDisseminateFormatException,
789
             OAIInternalServerError, 
790
             NoItemsMatchException 
791
  {
792
    purge(); // clean out old resumptionTokens
793
    
794
    Map<String, Object> listRecordsMap = new HashMap<String, Object>();
795
    ArrayList<String> records = new ArrayList<String>();
796
    filteredDateMap = filterDateMap(from, until, metadataPrefix);
797
    Iterator iterator = filteredDateMap.entrySet().iterator();
798
    int numRows = filteredDateMap.entrySet().size();
799
    int count = 0;
800
    
801
    while (count < maxListSize && iterator.hasNext()) {
802
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
803
      
804
      try {
805
        String localIdentifier = (String) entryDateMap.getKey();
806
        HashMap<String, String> nativeItem =getMetacatDocument(localIdentifier);
807
        String record = constructRecord(nativeItem, metadataPrefix);
808
        records.add(record);
809
        count++;
810
      } 
811
      catch (IOException e) {
812
        e.printStackTrace();
813
        throw new OAIInternalServerError(e.getMessage());
814
      }
815
    }
816

    
817
    if (count == 0) { throw new NoItemsMatchException(); }
818

    
819
    /* decide if you're done */
820
    if (iterator.hasNext()) {
821
      String resumptionId = getRSName();
822
      resumptionResults.put(resumptionId, iterator);
823

    
824
      /*****************************************************************
825
       * Construct the resumptionToken String however you see fit.
826
       *****************************************************************/
827
      StringBuffer resumptionTokenSb = new StringBuffer();
828
      resumptionTokenSb.append(resumptionId);
829
      resumptionTokenSb.append(":");
830
      resumptionTokenSb.append(Integer.toString(count));
831
      resumptionTokenSb.append(":");
832
      resumptionTokenSb.append(Integer.toString(numRows));
833
      resumptionTokenSb.append(":");
834
      resumptionTokenSb.append(metadataPrefix);
835

    
836
      /*****************************************************************
837
       * Use the following line if you wish to include the optional
838
       * resumptionToken attributes in the response. Otherwise, use the line
839
       * after it that I've commented out.
840
       *****************************************************************/
841
      listRecordsMap.put("resumptionMap", 
842
                         getResumptionMap(resumptionTokenSb.toString(), 
843
                                          numRows, 0
844
                                         )
845
                        );
846
      // listRecordsMap.put("resumptionMap",
847
      // getResumptionMap(resumptionTokenSb.toString()));
848
    }
849
    
850
    listRecordsMap.put("records", records.iterator()); 
851
    return listRecordsMap;
852
  }
853

    
854

    
855
  /**
856
   * Retrieve the next set of records associated with the resumptionToken
857
   * 
858
   * @param resumptionToken
859
   *          implementation-dependent format taken from the previous
860
   *          listRecords() Map result.
861
   * @return a Map object containing an optional "resumptionToken" key/value
862
   *         pair and a "records" Iterator object. The "records" Iterator
863
   *         contains a set of Records objects.
864
   * @exception OAIBadRequestException
865
   *              signals an http status code 400 problem
866
   */
867
  public Map listRecords(String resumptionToken)
868
      throws BadResumptionTokenException {
869
    purge(); // clean out old resumptionTokens
870
    Map listRecordsMap = new HashMap();
871
    ArrayList records = new ArrayList();
872

    
873
    /**********************************************************************
874
     * parse your resumptionToken and look it up in the resumptionResults, if
875
     * necessary
876
     **********************************************************************/
877
    StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
878
    String resumptionId;
879
    int oldCount;
880
    String metadataPrefix;
881
    int numRows;
882
    
883
    try {
884
      resumptionId = tokenizer.nextToken();
885
      oldCount = Integer.parseInt(tokenizer.nextToken());
886
      numRows = Integer.parseInt(tokenizer.nextToken());
887
      metadataPrefix = tokenizer.nextToken();
888
    } 
889
    catch (NoSuchElementException e) {
890
      throw new BadResumptionTokenException();
891
    }
892

    
893
    /* Get some more records from your database */
894
    Iterator iterator = (Iterator) resumptionResults.remove(resumptionId);
895
    
896
    if (iterator == null) {
897
      System.out
898
          .println("MetacatCatalog.listRecords(): reuse of old resumptionToken?");
899
      iterator = dateMap.entrySet().iterator();
900
      for (int i = 0; i < oldCount; ++i)
901
        iterator.next();
902
    }
903

    
904
    /* load the records ArrayLists. */
905
    int count = 0;
906
    
907
    while (count < maxListSize && iterator.hasNext()) {
908
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
909
      
910
      try {
911
        String localIdentifier = (String) entryDateMap.getKey();
912
        HashMap nativeItem = getMetacatDocument(localIdentifier);
913
        String record = constructRecord(nativeItem, metadataPrefix);
914
        records.add(record);
915
        count++;
916
      } 
917
      catch (CannotDisseminateFormatException e) {
918
        /* the client hacked the resumptionToken beyond repair */
919
        throw new BadResumptionTokenException();
920
      } 
921
      catch (IOException e) {
922
        /* the file is probably missing */
923
        throw new BadResumptionTokenException();
924
      }
925
    }
926

    
927
    /* decide if you're done. */
928
    if (iterator.hasNext()) {
929
      resumptionId = getRSName();
930
      resumptionResults.put(resumptionId, iterator);
931

    
932
      /*****************************************************************
933
       * Construct the resumptionToken String however you see fit.
934
       *****************************************************************/
935
      StringBuffer resumptionTokenSb = new StringBuffer();
936
      resumptionTokenSb.append(resumptionId);
937
      resumptionTokenSb.append(":");
938
      resumptionTokenSb.append(Integer.toString(oldCount + count));
939
      resumptionTokenSb.append(":");
940
      resumptionTokenSb.append(Integer.toString(numRows));
941
      resumptionTokenSb.append(":");
942
      resumptionTokenSb.append(metadataPrefix);
943

    
944
      /*****************************************************************
945
       * Use the following line if you wish to include the optional
946
       * resumptionToken attributes in the response. Otherwise, use the line
947
       * after it that I've commented out.
948
       *****************************************************************/
949
      listRecordsMap.put("resumptionMap", 
950
                         getResumptionMap(resumptionTokenSb.toString(), 
951
                         numRows, 
952
                         oldCount)
953
                        );
954
      // listRecordsMap.put("resumptionMap",
955
      // getResumptionMap(resumptionTokenSb.toString()));
956
    }
957

    
958
    listRecordsMap.put("records", records.iterator());
959
    
960
    return listRecordsMap;
961
  }
962

    
963

    
964
  public Map listSets() throws NoSetHierarchyException {
965
    throw new NoSetHierarchyException();
966
    // Map listSetsMap = new HashMap();
967
    // listSetsMap.put("sets", setsList.iterator());
968
    // return listSetsMap;
969
  }
970

    
971

    
972
  public Map listSets(String resumptionToken)
973
      throws BadResumptionTokenException {
974
    throw new BadResumptionTokenException();
975
  }
976

    
977

    
978
  /**
979
   * Run a query of the Metacat database to load the catalog of EML documents.
980
   * For each EML document, we store its 'docid', 'doctype', and 'date_updated'
981
   * values.
982
   */
983
  public void loadCatalog() {
984
    Statement stmt;
985

    
986
    try {
987
      Connection conn = getConnection();
988
      
989
      if (conn != null) {
990
        stmt = conn.createStatement();                          
991
        ResultSet rs = stmt.executeQuery(QUERY);
992
        
993
        int documentCount = 0;
994

    
995
        while (rs.next()) {
996
          documentCount++;
997
          String docid = rs.getString("docid");
998
          String doctype = rs.getString("doctype");
999
          String dateUpdated = rs.getDate("date_updated").toString();
1000
          docTypeMap.put(docid, doctype);
1001
          dateMap.put(docid, dateUpdated);
1002
        }
1003
        
1004
        logger.info("Number of documents in catalog: " + documentCount);
1005

    
1006
        stmt.close();   
1007
        conn.close();
1008
      }
1009
      
1010
      updateRefreshDate();
1011
    }
1012
    catch(SQLException e) {
1013
      logger.error("SQLException: " + e.getMessage());
1014
    }
1015
  }
1016
  
1017

    
1018
  /**
1019
   * Purge tokens that are older than the time-to-live.
1020
   */
1021
  private void purge() {
1022
    ArrayList old = new ArrayList();
1023
    Date then, now = new Date();
1024
    Iterator keySet = resumptionResults.keySet().iterator();
1025
    String key;
1026

    
1027
    while (keySet.hasNext()) {
1028
      key = (String) keySet.next();
1029
      then = new Date(Long.parseLong(key) + getMillisecondsToLive());
1030
      if (now.after(then)) {
1031
        old.add(key);
1032
      }
1033
    }
1034
    Iterator iterator = old.iterator();
1035
    while (iterator.hasNext()) {
1036
      key = (String) iterator.next();
1037
      resumptionResults.remove(key);
1038
    }
1039
  }
1040

    
1041

    
1042
  /**
1043
   * Boolean to determine whether the catalog should be refreshed in memory.
1044
   * 
1045
   * @return   true if the catalog should be refreshed, else false
1046
   */
1047
  private boolean shouldRefreshCatalog() {
1048
    boolean shouldRefresh = false;
1049
    String maxDateUpdated = getMaxDateUpdated();
1050
  
1051
    logger.info("refreshDate: " + refreshDate);
1052
    logger.info("maxDateUpdated: " + maxDateUpdated);
1053
  
1054
    /* If we don't know the last date that Metacat was updated or the last date
1055
     * the catalog was refreshed, then the catalog should be refreshed.
1056
     */
1057
    if ((refreshDate == null) || (maxDateUpdated == null)) { 
1058
      shouldRefresh = true;
1059
    }
1060
    /* If the last date that Metacat was updated is greater than the last date
1061
     * the catalog was refreshed, then the catalog should be refreshed.
1062
     */
1063
    else if (maxDateUpdated.compareTo(refreshDate) > 0) {
1064
      shouldRefresh = true; 
1065
    }
1066
  
1067
    logger.info("shouldRefresh: " + shouldRefresh);
1068
    return shouldRefresh;
1069
  }
1070
  
1071
  
1072
  /**
1073
   * Updates the refreshDate string to the current date.
1074
   */
1075
  private void updateRefreshDate() {
1076
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
1077
    Date now = new Date();
1078
    MetacatCatalog.refreshDate = simpleDateFormat.format(now);
1079
  }
1080

    
1081
}
(1-1/2)