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.InputStreamReader;
16
import java.io.Reader;
17
import java.sql.Connection;
18
import java.sql.DriverManager;
19
import java.sql.ResultSet;
20
import java.sql.SQLException;
21
import java.sql.SQLWarning;
22
import java.sql.Statement;
23
import java.text.SimpleDateFormat;
24
import java.util.ArrayList;
25
import java.util.Date;
26
import java.util.HashMap;
27
import java.util.Iterator;
28
import java.util.Map;
29
import java.util.NoSuchElementException;
30
import java.util.Properties;
31
import java.util.StringTokenizer;
32
import java.util.Vector;
33

    
34
import org.apache.log4j.Logger;
35

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

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

    
57

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

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

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

    
79
  /* Instance fields */
80
  
81
  protected String homeDir;
82
  private HashMap<String, String> dateMap = new HashMap<String, String>();
83
  private HashMap<String, String> filteredDateMap = null;
84
  private HashMap<String, String> docTypeMap = new HashMap<String, String>();
85
  private HashMap resumptionResults = new HashMap();
86
  private int maxListSize;
87
  
88
  /*
89
   * QUERY string to find all eml-2.x.y documents in the Metacat database
90
   * that are publicly accessible
91
   */
92
  private final String QUERY =
93
  "SELECT xd.docid, xd.doctype, xd.date_updated " +
94
  "FROM xml_documents xd, identifier id " +
95
  "WHERE xd.doctype like 'eml://ecoinformatics.org/eml-2%' " +
96
  " AND xd.docid = id.docid " +
97
  " AND xd.rev = id.rev " +
98
  // ALLOW rule
99
  " AND id.guid IN " +
100
  "     (SELECT guid " +
101
  "     FROM xml_access " +
102
  "		AND lower(principal_name) = 'public' " +
103
  " 	AND perm_type = 'allow' " +
104
  " 	AND permission > 3" +
105
  "		) " +
106
  // DENY rules?
107
  " AND id.guid NOT IN " +
108
  "     (SELECT guid " +
109
  "     FROM xml_access " +
110
  "     WHERE lower(principal_name) = 'public' " +
111
  "		AND perm_type = 'deny' " +
112
  "		AND perm_order ='allowFirst' " +
113
  "		AND permission > 3 " +
114
  "     ) ";
115
  
116
  
117
/* Constructors */
118
  
119
  public MetacatCatalog(Properties properties) {
120
    String errorStr;
121
    String temp;
122

    
123
    temp = properties.getProperty("oaipmh.maxListSize");
124
    if (temp == null) {
125
      errorStr = "oaipmh.maxListSize is missing from the properties file";
126
      throw new IllegalArgumentException(errorStr);
127
    }
128
    maxListSize = Integer.parseInt(temp);
129
    
130
    metacatDBDriver = properties.getProperty("database.driver");
131
    metacatDBURL = properties.getProperty("database.connectionURI");
132
    metacatDBUser = properties.getProperty("database.user");
133
    metacatDBPassword = properties.getProperty("database.password");
134
    
135
    try {
136
      if (OAIHandler.isIntegratedWithMetacat()) {
137
        metacatURL = SystemUtil.getServletURL();
138
      }
139
      else {
140
        metacatURL = properties.getProperty("test.metacatUrl");
141
      }
142
      
143
      logger.warn("metacatURL: " + metacatURL);
144
    }
145
    catch (PropertyNotFoundException e) {
146
      logger.error("PropertyNotFoundException: " + 
147
             "unable to determine metacat URL from SystemUtil.getServletURL()");
148
    }
149

    
150
    loadCatalog();
151
  }
152

    
153
  
154
  /* Class methods */
155
  
156
  /**
157
   * Use the current date as the basis for the resumptiontoken
158
   * 
159
   * @return a long integer version of the current time
160
   */
161
  private synchronized static String getRSName() {
162
    Date now = new Date();
163
    return Long.toString(now.getTime());
164
  }
165

    
166
  
167
  /* Instance methods */
168

    
169
  
170
  /**
171
   * close the repository
172
   */
173
  public void close() {
174
  }
175

    
176

    
177
  /**
178
   * Utility method to construct a Record object for a specified metadataFormat
179
   * from a native record
180
   * 
181
   * @param nativeItem
182
   *          native item from the dataase
183
   * @param metadataPrefix
184
   *          the desired metadataPrefix for performing the crosswalk
185
   * @return the <record/> String
186
   * @exception CannotDisseminateFormatException
187
   *              the record is not available for the specified metadataPrefix.
188
   */
189
  private String constructRecord(HashMap nativeItem, String metadataPrefix)
190
      throws CannotDisseminateFormatException {
191
    String schemaURL = null;
192
    Iterator setSpecs = getSetSpecs(nativeItem);
193
    Iterator abouts = getAbouts(nativeItem);
194

    
195
    if (metadataPrefix != null) {
196
      if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
197
        throw new CannotDisseminateFormatException(metadataPrefix);
198
    }
199
    
200
    RecordFactory recordFactory = getRecordFactory();
201
    String recordString = recordFactory.create(nativeItem, schemaURL, 
202
                                              metadataPrefix, setSpecs, abouts);
203
    return recordString;
204
  }
205
  
206
  
207
  /**
208
   * Using the original dateMap catalog, produce a filtered dateMap catalog
209
   * consisting of only those entries that match the 'from', 'until', and
210
   * 'metadataPrefix' criteria.
211
   * 
212
   * @param from                 the from date, e.g. "2008-06-01"
213
   * @param until                the until date, e.g. "2009-01-01"
214
   * @param metadataPrefix       the metadataPrefix value, e.g. "oai_dc"
215
   * 
216
   * @return   aDateMap, a HashMap containing only the matched entries.
217
   */
218
  private HashMap<String, String> filterDateMap(String from, String until,
219
      String metadataPrefix) {
220
    
221
    if (shouldRefreshCatalog()) {
222
      loadCatalog();
223
    }
224
    
225
    HashMap<String, String> aDateMap = new HashMap<String, String>();
226
    Iterator iterator = dateMap.entrySet().iterator();
227

    
228
    while (iterator.hasNext()) {
229
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
230
      String dateUpdated = (String) entryDateMap.getValue();
231

    
232
      /*
233
       * First filter catalog entries based on whether their date updated falls
234
       * within the 'from' and 'until' parameters.
235
       */
236
      if (dateUpdated.compareTo(from) >= 0 && dateUpdated.compareTo(until) <= 0) 
237
      {
238
        String docid = (String) entryDateMap.getKey();
239
        HashMap<String, String> nativeHeader = getNativeHeader(docid);
240
        String doctype = nativeHeader.get("doctype");
241

    
242
        /*
243
         * Next filter catalog entries based on Metacat doctype as compared to
244
         * OAI-PMH metadataPrefix.
245
         */
246
        if (isIncludedDoctype(doctype, metadataPrefix)) {
247
          aDateMap.put(docid, dateUpdated);
248
        }
249
      }
250

    
251
    }
252

    
253
    return aDateMap;
254
  }
255

    
256

    
257
  /**
258
   * get an Iterator containing the abouts for the nativeItem
259
   * 
260
   * @param rs
261
   *          ResultSet containing the nativeItem
262
   * @return an Iterator containing the list of about values for this nativeItem
263
   */
264
  private Iterator getAbouts(HashMap nativeItem) {
265
    return null;
266
  }
267

    
268

    
269
  /**
270
   * Returns a connection to the database. Opens the connection if a connection
271
   * has not already been made previously.
272
   * 
273
   * @return  conn  the database Connection object
274
   */
275
  public Connection getConnection() {
276
    Connection conn = null;
277
    
278
    try {
279
      Class.forName(metacatDBDriver);
280
    }
281
    catch (ClassNotFoundException e) {
282
      logger.error("Can't load driver " + e);
283
      return conn;
284
    } 
285

    
286
    // Make the database connection
287
    try {
288
      conn = DriverManager.getConnection(metacatDBURL, metacatDBUser, 
289
                                           metacatDBPassword);
290

    
291
      // If a SQLWarning object is available, print its warning(s).
292
      // There may be multiple warnings chained.
293
      SQLWarning warn = conn.getWarnings();
294
      
295
      if (warn != null) {
296
        while (warn != null) {
297
          logger.warn("SQLState: " + warn.getSQLState());
298
          logger.warn("Message:  " + warn.getMessage());
299
          logger.warn("Vendor: " + warn.getErrorCode());
300
          warn = warn.getNextWarning();
301
        }
302
      }
303
    }
304
    catch (SQLException e) {
305
      logger.error("Database access failed " + e);
306
    }
307
    
308
    return conn;
309
  }
310

    
311

    
312
  /**
313
   * Get the most recent date that the xml_documents table was updated
314
   * @return
315
   */
316
  public String getMaxDateUpdated() {
317
    String maxDateUpdated = null;
318
    String query = 
319
              "SELECT MAX(date_updated) AS max_date_updated FROM xml_documents";
320
    Statement stmt;
321

    
322
    try {
323
      Connection conn = getConnection();    
324
      if (conn != null) {
325
        stmt = conn.createStatement();                          
326
        ResultSet rs = stmt.executeQuery(query);
327
        while (rs.next()) {
328
          maxDateUpdated = rs.getDate("max_date_updated").toString();
329
        }
330
        stmt.close();   
331
        conn.close();
332
      }
333
    }
334
    catch(SQLException e) {
335
      logger.error("SQLException: " + e.getMessage());
336
    }
337
    
338
    return maxDateUpdated;
339
  }
340
  
341
  
342
  /**
343
   * Get a document from Metacat.
344
   * 
345
   * @param docid  the docid of the document to read
346
   * 
347
   * @return recordMap       a HashMap holding the document contents
348
   * 
349
   * @throws IOException
350
   */
351
  private HashMap<String, String> getMetacatDocument(String docid) 
352
      throws IOException {
353
    HashMap<String, String> recordMap = getNativeHeader(docid);
354
    
355
    if (recordMap == null) {
356
      return null;
357
    } 
358
    else {
359
      try {
360
        /* Perform a Metacat read operation on this docid */
361
        Metacat metacat = MetacatFactory.createMetacatConnection(metacatURL);
362
        Reader reader = new InputStreamReader(metacat.read(docid));
363
        StringBuffer stringBuffer = IOUtil.getAsStringBuffer(reader, true);
364
        String emlString = stringBuffer.toString();
365
        recordMap.put("recordBytes", emlString);
366
      }
367
      catch (MetacatInaccessibleException e) {
368
        logger.error("MetacatInaccessibleException:\n" + e.getMessage());
369
      }
370
      catch (MetacatException e) {
371
        logger.error("MetacatException:\n" + e.getMessage());
372
      }
373
      catch (DocumentNotFoundException e) {
374
        logger.error("DocumentNotFoundException:\n" + e.getMessage());
375
      }
376
      catch (InsufficientKarmaException e) {
377
        logger.error("InsufficientKarmaException:\n" + e.getMessage());
378
      }
379
      catch (IOException e) {
380
        logger.error("Error reading EML document from metacat:\n" + 
381
                     e.getMessage()
382
                    );
383
      }
384
    }
385
    
386
    return recordMap;
387
  }
388

    
389

    
390
  private HashMap<String, String> getNativeHeader(String localIdentifier) {
391
    HashMap<String, String> recordMap = null;
392
    
393
    if (dateMap.containsKey(localIdentifier)) {
394
      recordMap = new HashMap<String, String>();
395
      recordMap.put("localIdentifier", localIdentifier);
396
      recordMap.put("lastModified", dateMap.get(localIdentifier));
397
      recordMap.put("doctype", docTypeMap.get(localIdentifier));
398
      return recordMap;
399
    }
400
    
401
    return recordMap;
402
  }
403

    
404

    
405
  /**
406
   * Retrieve the specified metadata for the specified oaiIdentifier
407
   * 
408
   * @param oaiIdentifier
409
   *          the OAI identifier
410
   * @param metadataPrefix
411
   *          the OAI metadataPrefix
412
   * @return the Record object containing the result.
413
   * @exception CannotDisseminateFormatException
414
   *              signals an http status code 400 problem
415
   * @exception IdDoesNotExistException
416
   *              signals an http status code 404 problem
417
   * @exception OAIInternalServerError
418
   *              signals an http status code 500 problem
419
   */
420
  public String getRecord(String oaiIdentifier, String metadataPrefix)
421
      throws IdDoesNotExistException, 
422
             CannotDisseminateFormatException,
423
             OAIInternalServerError 
424
  {
425
    HashMap<String, String> nativeItem = null;
426
    
427
    try {
428
      RecordFactory recordFactory = getRecordFactory();
429
      String localIdentifier = recordFactory.fromOAIIdentifier(oaiIdentifier);
430
      nativeItem = getMetacatDocument(localIdentifier);
431
      if (nativeItem == null) throw new IdDoesNotExistException(oaiIdentifier);
432
      return constructRecord(nativeItem, metadataPrefix);
433
    } 
434
    catch (IOException e) {
435
      e.printStackTrace();
436
      throw new OAIInternalServerError("Database Failure");
437
    }
438
  }
439

    
440

    
441
  /**
442
   * Retrieve a list of schemaLocation values associated with the specified
443
   * oaiIdentifier.
444
   * 
445
   * We get passed the ID for a record and are supposed to return a list of the
446
   * formats that we can deliver the record in. Since we are assuming that all
447
   * the records in the directory have the same format, the response to this is
448
   * static;
449
   * 
450
   * @param oaiIdentifier       the OAI identifier
451
   * 
452
   * @return a Vector containing schemaLocation Strings
453
   * 
454
   * @exception OAIBadRequestException
455
   *              signals an http status code 400 problem
456
   * @exception OAINotFoundException
457
   *              signals an http status code 404 problem
458
   * @exception OAIInternalServerError
459
   *              signals an http status code 500 problem
460
   */
461
  public Vector getSchemaLocations(String oaiIdentifier)
462
      throws IdDoesNotExistException, OAIInternalServerError,
463
      NoMetadataFormatsException {
464
    HashMap<String, String> nativeItem = null;
465
    
466
    try {
467
      String localIdentifier = getRecordFactory().fromOAIIdentifier(
468
          oaiIdentifier);
469
      nativeItem = getMetacatDocument(localIdentifier);
470
    } 
471
    catch (IOException e) {
472
      e.printStackTrace();
473
      throw new OAIInternalServerError("Database Failure");
474
    }
475

    
476
    if (nativeItem != null) {
477
      RecordFactory recordFactory = getRecordFactory();
478
      return recordFactory.getSchemaLocations(nativeItem);
479
    } 
480
    else {
481
      throw new IdDoesNotExistException(oaiIdentifier);
482
    }
483
  }
484

    
485

    
486
  /**
487
   * get an Iterator containing the setSpecs for the nativeItem
488
   * 
489
   * @param rs
490
   *          ResultSet containing the nativeItem
491
   * @return an Iterator containing the list of setSpec values for this
492
   *         nativeItem
493
   */
494
  private Iterator getSetSpecs(HashMap nativeItem) {
495
    return null;
496
  }
497

    
498

    
499
  /**
500
   * Should a document with the specified Metacat doctype be included in the
501
   * list of identifiers/records for the specified OAI-PMH metadataPrefix?
502
   * 
503
   * @param doctype              e.g. "eml://ecoinformatics.org/eml-2.1.0"
504
   * @param metadataPrefix       e.g. "oai_dc", "eml-2.0.1", "eml-2.1.0"
505
   * @return
506
   */
507
  private boolean isIncludedDoctype(String doctype, String metadataPrefix) {
508
    boolean isIncluded = false;
509
    
510
    /*
511
     * If the metadataPrefix is "oai_dc", then include all catalog entries
512
     * in the list of identifiers. Else if the metadataPrefix is an EML
513
     * document type, then only include those catalog entries whose
514
     * document type matches that of the metadataPrefix. 
515
     */
516
    if (doctype != null && 
517
        (metadataPrefix.equals("oai_dc") ||
518
         (doctype.startsWith("eml://ecoinformatics.org/eml-") && 
519
          doctype.endsWith(metadataPrefix)
520
         )
521
        )
522
       ) {
523
      isIncluded = true;
524
    }
525
 
526
    return isIncluded;
527
  }
528

    
529
  
530
  /**
531
   * Override this method if some files exist in the filesystem that aren't
532
   * metadata records.
533
   * 
534
   * @param child
535
   *          the File to be investigated
536
   * @return true if it contains metadata, false otherwise
537
   */
538
  protected boolean isMetadataFile(File child) {
539
    return true;
540
  }
541
  
542
 
543
  /**
544
   * Retrieve a list of Identifiers that satisfy the criteria parameters
545
   * 
546
   * @param from
547
   *          beginning date in the form of YYYY-MM-DD or null if earliest date
548
   *          is desired
549
   * @param until
550
   *          ending date in the form of YYYY-MM-DD or null if latest date is
551
   *          desired
552
   * @param set
553
   *          set name or null if no set is desired        
554
   * @param metadataPrefix       
555
   *          e.g. "oai_dc", "eml-2.0.1", "eml-2.1.0"
556
   *        
557
   * @return a Map object containing an optional "resumptionToken" key/value
558
   *         pair and an "identifiers" Map object. The "identifiers" Map
559
   *         contains OAI identifier keys with corresponding values of "true" or
560
   *         null depending on whether the identifier is deleted or not.
561
   * @exception OAIBadRequestException
562
   *              signals an http status code 400 problem
563
   */
564
  public Map listIdentifiers(String from, String until, String set,
565
                             String metadataPrefix) 
566
          throws NoItemsMatchException {
567
    purge(); // clean out old resumptionTokens
568
    
569
    Map<String, Object> listIdentifiersMap = new HashMap<String, Object>();
570
    ArrayList<String> headers = new ArrayList<String>();
571
    ArrayList<String> identifiers = new ArrayList<String>();
572
    
573
    filteredDateMap = filterDateMap(from, until, metadataPrefix);
574
    
575
    Iterator iterator = filteredDateMap.entrySet().iterator();
576
    int numRows = filteredDateMap.entrySet().size();
577
    int count = 0;
578
    RecordFactory recordFactory = getRecordFactory();
579
    
580
    while (count < maxListSize && iterator.hasNext()) {
581
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
582
      String dateUpdated = (String) entryDateMap.getValue();
583
      String key = (String) entryDateMap.getKey();
584
      HashMap<String, String> nativeHeader = getNativeHeader(key);
585
      String[] headerArray = recordFactory.createHeader(nativeHeader);
586
      
587
     /* 
588
      * header, e.g.
589
      * 
590
      * <header>
591
      *   <identifier>urn:lsid:knb.ecoinformatics.org:knb-lter-gce:26</identifier>
592
      *   <datestamp>2009-03-11</datestamp>
593
      * </header>
594
      */
595
      String header = headerArray[0];
596
      headers.add(header);
597
         
598
      /*
599
       * identifier, e.g. urn:lsid:knb.ecoinformatics.org:knb-lter-gce:26
600
       */
601
      String identifier = headerArray[1]; 
602
      identifiers.add(identifier);
603
      count++;
604
    }
605

    
606
    if (count == 0) { throw new NoItemsMatchException(); }
607

    
608
    /* decide if you're done */
609
    if (iterator.hasNext()) {
610
      String resumptionId = getRSName();
611
      resumptionResults.put(resumptionId, iterator);
612

    
613
      /*****************************************************************
614
       * Construct the resumptionToken String however you see fit.
615
       *****************************************************************/
616
      StringBuffer resumptionTokenSb = new StringBuffer();
617
      resumptionTokenSb.append(resumptionId);
618
      resumptionTokenSb.append(":");
619
      resumptionTokenSb.append(Integer.toString(count));
620
      resumptionTokenSb.append(":");
621
      resumptionTokenSb.append(Integer.toString(numRows));
622
      resumptionTokenSb.append(":");
623
      resumptionTokenSb.append(metadataPrefix);
624

    
625
      /*****************************************************************
626
       * Use the following line if you wish to include the optional
627
       * resumptionToken attributes in the response. Otherwise, use the line
628
       * after it that I've commented out.
629
       *****************************************************************/
630
      listIdentifiersMap.put("resumptionMap", getResumptionMap(
631
          resumptionTokenSb.toString(), numRows, 0));
632
      // listIdentifiersMap.put("resumptionMap",
633
      // getResumptionMap(resumptionTokenSb.toString()));
634
    }
635
    
636
    listIdentifiersMap.put("headers", headers.iterator());
637
    listIdentifiersMap.put("identifiers", identifiers.iterator());
638
    
639
    return listIdentifiersMap;
640
  }
641

    
642

    
643
  /**
644
   * Retrieve the next set of Identifiers associated with the resumptionToken
645
   * 
646
   * @param resumptionToken
647
   *          implementation-dependent format taken from the previous
648
   *          listIdentifiers() Map result.
649
   * @return a Map object containing an optional "resumptionToken" key/value
650
   *         pair and an "identifiers" Map object. The "identifiers" Map
651
   *         contains OAI identifier keys with corresponding values of "true" or
652
   *         null depending on whether the identifier is deleted or not.
653
   * @exception OAIBadRequestException
654
   *              signals an http status code 400 problem
655
   */
656
  public Map listIdentifiers(String resumptionToken)
657
      throws BadResumptionTokenException {
658
    purge(); // clean out old resumptionTokens
659
    Map listIdentifiersMap = new HashMap();
660
    ArrayList headers = new ArrayList();
661
    ArrayList identifiers = new ArrayList();
662

    
663
    /**********************************************************************
664
     * parse your resumptionToken and look it up in the resumptionResults, if
665
     * necessary
666
     **********************************************************************/
667
    StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
668
    String resumptionId;
669
    int oldCount;
670
    String metadataPrefix;
671
    int numRows;
672
    try {
673
      resumptionId = tokenizer.nextToken();
674
      oldCount = Integer.parseInt(tokenizer.nextToken());
675
      numRows = Integer.parseInt(tokenizer.nextToken());
676
      metadataPrefix = tokenizer.nextToken();
677
    } catch (NoSuchElementException e) {
678
      throw new BadResumptionTokenException();
679
    }
680

    
681
    /* Get some more records from your database */
682
    Iterator iterator = (Iterator) resumptionResults.remove(resumptionId);
683
    if (iterator == null) {
684
      System.out
685
          .println("MetacatCatalog.listIdentifiers(): reuse of old resumptionToken?");
686
      iterator = dateMap.entrySet().iterator();
687
      for (int i = 0; i < oldCount; ++i)
688
        iterator.next();
689
    }
690

    
691
    /* load the headers and identifiers ArrayLists. */
692
    int count = 0;
693
    while (count < maxListSize && iterator.hasNext()) {
694
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
695
      HashMap nativeHeader = getNativeHeader((String) entryDateMap.getKey());
696
      String[] header = getRecordFactory().createHeader(nativeHeader);
697
      headers.add(header[0]);
698
      identifiers.add(header[1]);
699
      count++;
700
    }
701

    
702
    /* decide if you're done. */
703
    if (iterator.hasNext()) {
704
      resumptionId = getRSName();
705
      resumptionResults.put(resumptionId, iterator);
706

    
707
      /*****************************************************************
708
       * Construct the resumptionToken String however you see fit.
709
       *****************************************************************/
710
      StringBuffer resumptionTokenSb = new StringBuffer();
711
      resumptionTokenSb.append(resumptionId);
712
      resumptionTokenSb.append(":");
713
      resumptionTokenSb.append(Integer.toString(oldCount + count));
714
      resumptionTokenSb.append(":");
715
      resumptionTokenSb.append(Integer.toString(numRows));
716
      resumptionTokenSb.append(":");
717
      resumptionTokenSb.append(metadataPrefix);
718

    
719
      /*****************************************************************
720
       * Use the following line if you wish to include the optional
721
       * resumptionToken attributes in the response. Otherwise, use the line
722
       * after it that I've commented out.
723
       *****************************************************************/
724
      listIdentifiersMap.put("resumptionMap", getResumptionMap(
725
          resumptionTokenSb.toString(), numRows, oldCount));
726
      // listIdentifiersMap.put("resumptionMap",
727
      // getResumptionMap(resumptionTokenSb.toString()));
728
    }
729

    
730
    listIdentifiersMap.put("headers", headers.iterator());
731
    listIdentifiersMap.put("identifiers", identifiers.iterator());
732
    return listIdentifiersMap;
733
  }
734

    
735

    
736
  /**
737
   * Retrieve a list of records that satisfy the specified criteria
738
   * 
739
   * @param from
740
   *          beginning date in the form of YYYY-MM-DD or null if earliest date
741
   *          is desired
742
   * @param until
743
   *          ending date in the form of YYYY-MM-DD or null if latest date is
744
   *          desired
745
   * @param set
746
   *          set name or null if no set is desired
747
   * @param metadataPrefix       
748
   *          e.g. "oai_dc", "eml-2.0.1", "eml-2.1.0"
749
   *        
750
   * @return a Map object containing an optional "resumptionToken" key/value
751
   *         pair and a "records" Iterator object. The "records" Iterator
752
   *         contains a set of Records objects.
753
   * @exception OAIBadRequestException
754
   *              signals an http status code 400 problem
755
   * @exception OAIInternalServerError
756
   *              signals an http status code 500 problem
757
   */
758
  public Map listRecords(String from, String until, String set,
759
                         String metadataPrefix) 
760
      throws CannotDisseminateFormatException,
761
             OAIInternalServerError, 
762
             NoItemsMatchException 
763
  {
764
    purge(); // clean out old resumptionTokens
765
    
766
    Map<String, Object> listRecordsMap = new HashMap<String, Object>();
767
    ArrayList<String> records = new ArrayList<String>();
768
    filteredDateMap = filterDateMap(from, until, metadataPrefix);
769
    Iterator iterator = filteredDateMap.entrySet().iterator();
770
    int numRows = filteredDateMap.entrySet().size();
771
    int count = 0;
772
    
773
    while (count < maxListSize && iterator.hasNext()) {
774
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
775
      
776
      try {
777
        String localIdentifier = (String) entryDateMap.getKey();
778
        HashMap<String, String> nativeItem =getMetacatDocument(localIdentifier);
779
        String record = constructRecord(nativeItem, metadataPrefix);
780
        records.add(record);
781
        count++;
782
      } 
783
      catch (IOException e) {
784
        e.printStackTrace();
785
        throw new OAIInternalServerError(e.getMessage());
786
      }
787
    }
788

    
789
    if (count == 0) { throw new NoItemsMatchException(); }
790

    
791
    /* decide if you're done */
792
    if (iterator.hasNext()) {
793
      String resumptionId = getRSName();
794
      resumptionResults.put(resumptionId, iterator);
795

    
796
      /*****************************************************************
797
       * Construct the resumptionToken String however you see fit.
798
       *****************************************************************/
799
      StringBuffer resumptionTokenSb = new StringBuffer();
800
      resumptionTokenSb.append(resumptionId);
801
      resumptionTokenSb.append(":");
802
      resumptionTokenSb.append(Integer.toString(count));
803
      resumptionTokenSb.append(":");
804
      resumptionTokenSb.append(Integer.toString(numRows));
805
      resumptionTokenSb.append(":");
806
      resumptionTokenSb.append(metadataPrefix);
807

    
808
      /*****************************************************************
809
       * Use the following line if you wish to include the optional
810
       * resumptionToken attributes in the response. Otherwise, use the line
811
       * after it that I've commented out.
812
       *****************************************************************/
813
      listRecordsMap.put("resumptionMap", 
814
                         getResumptionMap(resumptionTokenSb.toString(), 
815
                                          numRows, 0
816
                                         )
817
                        );
818
      // listRecordsMap.put("resumptionMap",
819
      // getResumptionMap(resumptionTokenSb.toString()));
820
    }
821
    
822
    listRecordsMap.put("records", records.iterator()); 
823
    return listRecordsMap;
824
  }
825

    
826

    
827
  /**
828
   * Retrieve the next set of records associated with the resumptionToken
829
   * 
830
   * @param resumptionToken
831
   *          implementation-dependent format taken from the previous
832
   *          listRecords() Map result.
833
   * @return a Map object containing an optional "resumptionToken" key/value
834
   *         pair and a "records" Iterator object. The "records" Iterator
835
   *         contains a set of Records objects.
836
   * @exception OAIBadRequestException
837
   *              signals an http status code 400 problem
838
   */
839
  public Map listRecords(String resumptionToken)
840
      throws BadResumptionTokenException {
841
    purge(); // clean out old resumptionTokens
842
    Map listRecordsMap = new HashMap();
843
    ArrayList records = new ArrayList();
844

    
845
    /**********************************************************************
846
     * parse your resumptionToken and look it up in the resumptionResults, if
847
     * necessary
848
     **********************************************************************/
849
    StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
850
    String resumptionId;
851
    int oldCount;
852
    String metadataPrefix;
853
    int numRows;
854
    
855
    try {
856
      resumptionId = tokenizer.nextToken();
857
      oldCount = Integer.parseInt(tokenizer.nextToken());
858
      numRows = Integer.parseInt(tokenizer.nextToken());
859
      metadataPrefix = tokenizer.nextToken();
860
    } 
861
    catch (NoSuchElementException e) {
862
      throw new BadResumptionTokenException();
863
    }
864

    
865
    /* Get some more records from your database */
866
    Iterator iterator = (Iterator) resumptionResults.remove(resumptionId);
867
    
868
    if (iterator == null) {
869
      System.out
870
          .println("MetacatCatalog.listRecords(): reuse of old resumptionToken?");
871
      iterator = dateMap.entrySet().iterator();
872
      for (int i = 0; i < oldCount; ++i)
873
        iterator.next();
874
    }
875

    
876
    /* load the records ArrayLists. */
877
    int count = 0;
878
    
879
    while (count < maxListSize && iterator.hasNext()) {
880
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
881
      
882
      try {
883
        String localIdentifier = (String) entryDateMap.getKey();
884
        HashMap nativeItem = getMetacatDocument(localIdentifier);
885
        String record = constructRecord(nativeItem, metadataPrefix);
886
        records.add(record);
887
        count++;
888
      } 
889
      catch (CannotDisseminateFormatException e) {
890
        /* the client hacked the resumptionToken beyond repair */
891
        throw new BadResumptionTokenException();
892
      } 
893
      catch (IOException e) {
894
        /* the file is probably missing */
895
        throw new BadResumptionTokenException();
896
      }
897
    }
898

    
899
    /* decide if you're done. */
900
    if (iterator.hasNext()) {
901
      resumptionId = getRSName();
902
      resumptionResults.put(resumptionId, iterator);
903

    
904
      /*****************************************************************
905
       * Construct the resumptionToken String however you see fit.
906
       *****************************************************************/
907
      StringBuffer resumptionTokenSb = new StringBuffer();
908
      resumptionTokenSb.append(resumptionId);
909
      resumptionTokenSb.append(":");
910
      resumptionTokenSb.append(Integer.toString(oldCount + count));
911
      resumptionTokenSb.append(":");
912
      resumptionTokenSb.append(Integer.toString(numRows));
913
      resumptionTokenSb.append(":");
914
      resumptionTokenSb.append(metadataPrefix);
915

    
916
      /*****************************************************************
917
       * Use the following line if you wish to include the optional
918
       * resumptionToken attributes in the response. Otherwise, use the line
919
       * after it that I've commented out.
920
       *****************************************************************/
921
      listRecordsMap.put("resumptionMap", 
922
                         getResumptionMap(resumptionTokenSb.toString(), 
923
                         numRows, 
924
                         oldCount)
925
                        );
926
      // listRecordsMap.put("resumptionMap",
927
      // getResumptionMap(resumptionTokenSb.toString()));
928
    }
929

    
930
    listRecordsMap.put("records", records.iterator());
931
    
932
    return listRecordsMap;
933
  }
934

    
935

    
936
  public Map listSets() throws NoSetHierarchyException {
937
    throw new NoSetHierarchyException();
938
    // Map listSetsMap = new HashMap();
939
    // listSetsMap.put("sets", setsList.iterator());
940
    // return listSetsMap;
941
  }
942

    
943

    
944
  public Map listSets(String resumptionToken)
945
      throws BadResumptionTokenException {
946
    throw new BadResumptionTokenException();
947
  }
948

    
949

    
950
  /**
951
   * Run a query of the Metacat database to load the catalog of EML documents.
952
   * For each EML document, we store its 'docid', 'doctype', and 'date_updated'
953
   * values.
954
   */
955
  public void loadCatalog() {
956
    Statement stmt;
957

    
958
    try {
959
      Connection conn = getConnection();
960
      
961
      if (conn != null) {
962
        stmt = conn.createStatement();                          
963
        ResultSet rs = stmt.executeQuery(QUERY);
964
        
965
        int documentCount = 0;
966

    
967
        while (rs.next()) {
968
          documentCount++;
969
          String docid = rs.getString("docid");
970
          String doctype = rs.getString("doctype");
971
          String dateUpdated = rs.getDate("date_updated").toString();
972
          docTypeMap.put(docid, doctype);
973
          dateMap.put(docid, dateUpdated);
974
        }
975
        
976
        logger.info("Number of documents in catalog: " + documentCount);
977

    
978
        stmt.close();   
979
        conn.close();
980
      }
981
      
982
      updateRefreshDate();
983
    }
984
    catch(SQLException e) {
985
      logger.error("SQLException: " + e.getMessage());
986
    }
987
  }
988
  
989

    
990
  /**
991
   * Purge tokens that are older than the time-to-live.
992
   */
993
  private void purge() {
994
    ArrayList old = new ArrayList();
995
    Date then, now = new Date();
996
    Iterator keySet = resumptionResults.keySet().iterator();
997
    String key;
998

    
999
    while (keySet.hasNext()) {
1000
      key = (String) keySet.next();
1001
      then = new Date(Long.parseLong(key) + getMillisecondsToLive());
1002
      if (now.after(then)) {
1003
        old.add(key);
1004
      }
1005
    }
1006
    Iterator iterator = old.iterator();
1007
    while (iterator.hasNext()) {
1008
      key = (String) iterator.next();
1009
      resumptionResults.remove(key);
1010
    }
1011
  }
1012

    
1013

    
1014
  /**
1015
   * Boolean to determine whether the catalog should be refreshed in memory.
1016
   * 
1017
   * @return   true if the catalog should be refreshed, else false
1018
   */
1019
  private boolean shouldRefreshCatalog() {
1020
    boolean shouldRefresh = false;
1021
    String maxDateUpdated = getMaxDateUpdated();
1022
  
1023
    logger.info("refreshDate: " + refreshDate);
1024
    logger.info("maxDateUpdated: " + maxDateUpdated);
1025
  
1026
    /* If we don't know the last date that Metacat was updated or the last date
1027
     * the catalog was refreshed, then the catalog should be refreshed.
1028
     */
1029
    if ((refreshDate == null) || (maxDateUpdated == null)) { 
1030
      shouldRefresh = true;
1031
    }
1032
    /* If the last date that Metacat was updated is greater than the last date
1033
     * the catalog was refreshed, then the catalog should be refreshed.
1034
     */
1035
    else if (maxDateUpdated.compareTo(refreshDate) > 0) {
1036
      shouldRefresh = true; 
1037
    }
1038
  
1039
    logger.info("shouldRefresh: " + shouldRefresh);
1040
    return shouldRefresh;
1041
  }
1042
  
1043
  
1044
  /**
1045
   * Updates the refreshDate string to the current date.
1046
   */
1047
  private void updateRefreshDate() {
1048
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
1049
    Date now = new Date();
1050
    MetacatCatalog.refreshDate = simpleDateFormat.format(now);
1051
  }
1052

    
1053
}
(1-1/2)