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.service.PropertyService;
43
import edu.ucsb.nceas.metacat.service.ServiceException;
44
import edu.ucsb.nceas.metacat.util.SystemUtil;
45
import edu.ucsb.nceas.utilities.IOUtil;
46
import edu.ucsb.nceas.utilities.PropertyNotFoundException;
47

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

    
58

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

    
65
public class MetacatCatalog extends AbstractCatalog {
66
  
67
  /* Class fields */
68
  
69
  private static final Logger logger = Logger.getLogger(MetacatCatalog.class);
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
  private SimpleDateFormat dateFormatter = new SimpleDateFormat();
82
  protected String homeDir;
83
  private HashMap<String, String> dateMap = new HashMap<String, String>();
84
  private HashMap<String, String> docTypeMap = new HashMap<String, String>();
85
  private HashMap resumptionResults = new HashMap();
86
  private int maxListSize;
87
  
88
  
89
  /* Constructors */
90
  
91
  public MetacatCatalog(Properties properties) {
92
    String errorStr;
93
    String temp;
94

    
95
    dateFormatter.applyPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
96
    
97
    temp = properties.getProperty("oaipmh.maxListSize");
98
    if (temp == null) {
99
      errorStr = "oaipmh.maxListSize is missing from the properties file";
100
      throw new IllegalArgumentException(errorStr);
101
    }
102
    maxListSize = Integer.parseInt(temp);
103
    
104
    metacatDBDriver = properties.getProperty("database.driver");
105
    metacatDBURL = properties.getProperty("database.connectionURI");
106
    metacatDBUser = properties.getProperty("database.user");
107
    metacatDBPassword = properties.getProperty("database.password");
108
    
109
    try {
110
      if (OAIHandler.isIntegratedWithMetacat()) {
111
        metacatURL = SystemUtil.getServletURL();
112
      }
113
      else {
114
        metacatURL = "http://localhost:8080/knb/metacat";
115
      }
116
      
117
      logger.warn("metacatURL: " + metacatURL);
118
    }
119
    catch (PropertyNotFoundException e) {
120
      logger.error("PropertyNotFoundException: " + 
121
             "unable to determine metacat URL from SystemUtil.getServletURL()");
122
    }
123

    
124
    loadCatalog();
125
  }
126

    
127
  
128
  /* Class methods */
129
  
130
  /**
131
   * Use the current date as the basis for the resumptiontoken
132
   * 
133
   * @return a long integer version of the current time
134
   */
135
  private synchronized static String getRSName() {
136
    Date now = new Date();
137
    return Long.toString(now.getTime());
138
  }
139

    
140
  
141
  /* Instance methods */
142

    
143
  
144
  /**
145
   * close the repository
146
   */
147
  public void close() {
148
  }
149

    
150

    
151
  /**
152
   * Utility method to construct a Record object for a specified metadataFormat
153
   * from a native record
154
   * 
155
   * @param nativeItem
156
   *          native item from the dataase
157
   * @param metadataPrefix
158
   *          the desired metadataPrefix for performing the crosswalk
159
   * @return the <record/> String
160
   * @exception CannotDisseminateFormatException
161
   *              the record is not available for the specified metadataPrefix.
162
   */
163
  private String constructRecord(HashMap nativeItem, String metadataPrefix)
164
      throws CannotDisseminateFormatException {
165
    String schemaURL = null;
166
    Iterator setSpecs = getSetSpecs(nativeItem);
167
    Iterator abouts = getAbouts(nativeItem);
168

    
169
    if (metadataPrefix != null) {
170
      if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
171
        throw new CannotDisseminateFormatException(metadataPrefix);
172
    }
173
    
174
    RecordFactory recordFactory = getRecordFactory();
175
    String recordString = recordFactory.create(nativeItem, schemaURL, 
176
                                              metadataPrefix, setSpecs, abouts);
177
    return recordString;
178
  }
179

    
180

    
181
  private String date2OAIDatestamp(Date date) {
182
    return dateFormatter.format(date);
183
  }
184

    
185

    
186
  /**
187
   * get an Iterator containing the abouts for the nativeItem
188
   * 
189
   * @param rs
190
   *          ResultSet containing the nativeItem
191
   * @return an Iterator containing the list of about values for this nativeItem
192
   */
193
  private Iterator getAbouts(HashMap nativeItem) {
194
    return null;
195
  }
196

    
197

    
198
  /**
199
   * Returns a connection to the database. Opens the connection if a connection
200
   * has not already been made previously.
201
   * 
202
   * @return  conn  the database Connection object
203
   */
204
  public Connection getConnection() {
205
    Connection conn = null;
206
    
207
    try {
208
      Class.forName(metacatDBDriver);
209
    }
210
    catch (ClassNotFoundException e) {
211
      logger.error("Can't load driver " + e);
212
      return conn;
213
    } 
214

    
215
    // Make the database connection
216
    try {
217
      conn = DriverManager.getConnection(metacatDBURL, metacatDBUser, 
218
                                           metacatDBPassword);
219

    
220
      // If a SQLWarning object is available, print its warning(s).
221
      // There may be multiple warnings chained.
222
      SQLWarning warn = conn.getWarnings();
223
      
224
      if (warn != null) {
225
        while (warn != null) {
226
          logger.warn("SQLState: " + warn.getSQLState());
227
          logger.warn("Message:  " + warn.getMessage());
228
          logger.warn("Vendor: " + warn.getErrorCode());
229
          warn = warn.getNextWarning();
230
        }
231
      }
232
    }
233
    catch (SQLException e) {
234
      logger.error("Database access failed " + e);
235
    }
236
    
237
    return conn;
238
  }
239

    
240

    
241
  /**
242
   * Get the most recent date that the xml_documents table was updated
243
   * @return
244
   */
245
  public String getMaxDateUpdated() {
246
    String maxDateUpdated = null;
247
    String query = 
248
              "SELECT MAX(date_updated) AS max_date_updated FROM xml_documents";
249
    Statement stmt;
250

    
251
    try {
252
      Connection conn = getConnection();    
253
      if (conn != null) {
254
        stmt = conn.createStatement();                          
255
        ResultSet rs = stmt.executeQuery(query);
256
        while (rs.next()) {
257
          maxDateUpdated = rs.getDate("max_date_updated").toString();
258
        }
259
        stmt.close();   
260
        conn.close();
261
      }
262
    }
263
    catch(SQLException e) {
264
      logger.error("SQLException: " + e.getMessage());
265
    }
266
    
267
    return maxDateUpdated;
268
  }
269
  
270
  
271
  /**
272
   * Get a document from Metacat.
273
   * 
274
   * @param docid  the docid of the document to read
275
   * 
276
   * @return recordMap       a HashMap holding the document contents
277
   * 
278
   * @throws IOException
279
   */
280
  private HashMap<String, String> getMetacatDocument(String docid) 
281
      throws IOException {
282
    HashMap<String, String> recordMap = getNativeHeader(docid);
283
    
284
    if (recordMap == null) {
285
      return null;
286
    } 
287
    else {
288
      try {
289
        /* Perform a Metacat read operation on this docid */
290
        Metacat metacat = MetacatFactory.createMetacatConnection(metacatURL);
291
        Reader reader = metacat.read(docid);
292
        StringBuffer stringBuffer = IOUtil.getAsStringBuffer(reader, true);
293
        String emlString = stringBuffer.toString();
294
        recordMap.put("recordBytes", emlString);
295
      }
296
      catch (MetacatInaccessibleException e) {
297
        logger.error("MetacatInaccessibleException:\n" + e.getMessage());
298
      }
299
      catch (MetacatException e) {
300
        logger.error("MetacatException:\n" + e.getMessage());
301
      }
302
      catch (DocumentNotFoundException e) {
303
        logger.error("DocumentNotFoundException:\n" + e.getMessage());
304
      }
305
      catch (InsufficientKarmaException e) {
306
        logger.error("InsufficientKarmaException:\n" + e.getMessage());
307
      }
308
      catch (IOException e) {
309
        logger.error("Error reading EML document from metacat:\n" + 
310
                     e.getMessage()
311
                    );
312
      }
313
    }
314
    
315
    return recordMap;
316
  }
317

    
318

    
319
  private HashMap<String, String> getNativeHeader(String localIdentifier) {
320
    HashMap<String, String> recordMap = null;
321
    
322
    if (dateMap.containsKey(localIdentifier)) {
323
      recordMap = new HashMap<String, String>();
324
      recordMap.put("localIdentifier", localIdentifier);
325
      recordMap.put("lastModified", dateMap.get(localIdentifier));
326
      recordMap.put("doctype", docTypeMap.get(localIdentifier));
327
      return recordMap;
328
    }
329
    
330
    return recordMap;
331
  }
332

    
333

    
334
  /**
335
   * Retrieve the specified metadata for the specified oaiIdentifier
336
   * 
337
   * @param oaiIdentifier
338
   *          the OAI identifier
339
   * @param metadataPrefix
340
   *          the OAI metadataPrefix
341
   * @return the Record object containing the result.
342
   * @exception CannotDisseminateFormatException
343
   *              signals an http status code 400 problem
344
   * @exception IdDoesNotExistException
345
   *              signals an http status code 404 problem
346
   * @exception OAIInternalServerError
347
   *              signals an http status code 500 problem
348
   */
349
  public String getRecord(String oaiIdentifier, String metadataPrefix)
350
      throws IdDoesNotExistException, CannotDisseminateFormatException,
351
             OAIInternalServerError {
352
    HashMap<String, String> nativeItem = null;
353
    
354
    try {
355
      RecordFactory recordFactory = getRecordFactory();
356
      String localIdentifier = recordFactory.fromOAIIdentifier(oaiIdentifier);
357
      nativeItem = getMetacatDocument(localIdentifier);
358
      if (nativeItem == null) throw new IdDoesNotExistException(oaiIdentifier);
359
      return constructRecord(nativeItem, metadataPrefix);
360
    } 
361
    catch (IOException e) {
362
      e.printStackTrace();
363
      throw new OAIInternalServerError("Database Failure");
364
    }
365
  }
366

    
367

    
368
  /**
369
   * Retrieve a list of schemaLocation values associated with the specified
370
   * oaiIdentifier.
371
   * 
372
   * We get passed the ID for a record and are supposed to return a list of the
373
   * formats that we can deliver the record in. Since we are assuming that all
374
   * the records in the directory have the same format, the response to this is
375
   * static;
376
   * 
377
   * @param oaiIdentifier       the OAI identifier
378
   * 
379
   * @return a Vector containing schemaLocation Strings
380
   * 
381
   * @exception OAIBadRequestException
382
   *              signals an http status code 400 problem
383
   * @exception OAINotFoundException
384
   *              signals an http status code 404 problem
385
   * @exception OAIInternalServerError
386
   *              signals an http status code 500 problem
387
   */
388
  public Vector getSchemaLocations(String oaiIdentifier)
389
      throws IdDoesNotExistException, OAIInternalServerError,
390
      NoMetadataFormatsException {
391
    HashMap<String, String> nativeItem = null;
392
    
393
    try {
394
      String localIdentifier = getRecordFactory().fromOAIIdentifier(
395
          oaiIdentifier);
396
      nativeItem = getMetacatDocument(localIdentifier);
397
    } 
398
    catch (IOException e) {
399
      e.printStackTrace();
400
      throw new OAIInternalServerError("Database Failure");
401
    }
402

    
403
    if (nativeItem != null) {
404
      RecordFactory recordFactory = getRecordFactory();
405
      return recordFactory.getSchemaLocations(nativeItem);
406
    } 
407
    else {
408
      throw new IdDoesNotExistException(oaiIdentifier);
409
    }
410
  }
411

    
412

    
413
  /**
414
   * get an Iterator containing the setSpecs for the nativeItem
415
   * 
416
   * @param rs
417
   *          ResultSet containing the nativeItem
418
   * @return an Iterator containing the list of setSpec values for this
419
   *         nativeItem
420
   */
421
  private Iterator getSetSpecs(HashMap nativeItem) {
422
    return null;
423
  }
424

    
425

    
426
  /**
427
   * Override this method if some files exist in the filesystem that aren't
428
   * metadata records.
429
   * 
430
   * @param child
431
   *          the File to be investigated
432
   * @return true if it contains metadata, false otherwise
433
   */
434
  protected boolean isMetadataFile(File child) {
435
    return true;
436
  }
437

    
438

    
439
  /**
440
   * Retrieve a list of Identifiers that satisfy the criteria parameters
441
   * 
442
   * @param from
443
   *          beginning date in the form of YYYY-MM-DD or null if earliest date
444
   *          is desired
445
   * @param until
446
   *          ending date in the form of YYYY-MM-DD or null if latest date is
447
   *          desired
448
   * @param set
449
   *          set name or null if no set is desired
450
   * @return a Map object containing an optional "resumptionToken" key/value
451
   *         pair and an "identifiers" Map object. The "identifiers" Map
452
   *         contains OAI identifier keys with corresponding values of "true" or
453
   *         null depending on whether the identifier is deleted or not.
454
   * @exception OAIBadRequestException
455
   *              signals an http status code 400 problem
456
   */
457
  public Map listIdentifiers(String from, String until, String set,
458
      String metadataPrefix) throws NoItemsMatchException {
459
    purge(); // clean out old resumptionTokens
460
    Map listIdentifiersMap = new HashMap();
461
    ArrayList headers = new ArrayList();
462
    ArrayList identifiers = new ArrayList();
463
    Iterator iterator = dateMap.entrySet().iterator();
464
    int numRows = dateMap.entrySet().size();
465
    int count = 0;
466
    RecordFactory recordFactory = getRecordFactory();
467
    while (count < maxListSize && iterator.hasNext()) {
468
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
469
      String fileDate = (String) entryDateMap.getValue();
470
      if (fileDate.compareTo(from) >= 0 && fileDate.compareTo(until) <= 0) {
471
        String key = (String) entryDateMap.getKey();
472
        HashMap nativeHeader = getNativeHeader(key);
473
        String[] header = recordFactory.createHeader(nativeHeader);
474
        headers.add(header[0]);
475
        identifiers.add(header[1]);
476
        count++;
477
      }
478
    }
479

    
480
    if (count == 0)
481
      throw new NoItemsMatchException();
482

    
483
    /* decide if you're done */
484
    if (iterator.hasNext()) {
485
      String resumptionId = getRSName();
486
      resumptionResults.put(resumptionId, iterator);
487

    
488
      /*****************************************************************
489
       * Construct the resumptionToken String however you see fit.
490
       *****************************************************************/
491
      StringBuffer resumptionTokenSb = new StringBuffer();
492
      resumptionTokenSb.append(resumptionId);
493
      resumptionTokenSb.append(":");
494
      resumptionTokenSb.append(Integer.toString(count));
495
      resumptionTokenSb.append(":");
496
      resumptionTokenSb.append(Integer.toString(numRows));
497
      resumptionTokenSb.append(":");
498
      resumptionTokenSb.append(metadataPrefix);
499

    
500
      /*****************************************************************
501
       * Use the following line if you wish to include the optional
502
       * resumptionToken attributes in the response. Otherwise, use the line
503
       * after it that I've commented out.
504
       *****************************************************************/
505
      listIdentifiersMap.put("resumptionMap", getResumptionMap(
506
          resumptionTokenSb.toString(), numRows, 0));
507
      // listIdentifiersMap.put("resumptionMap",
508
      // getResumptionMap(resumptionTokenSb.toString()));
509
    }
510
    listIdentifiersMap.put("headers", headers.iterator());
511
    listIdentifiersMap.put("identifiers", identifiers.iterator());
512
    return listIdentifiersMap;
513
  }
514

    
515

    
516
  /**
517
   * Retrieve the next set of Identifiers associated with the resumptionToken
518
   * 
519
   * @param resumptionToken
520
   *          implementation-dependent format taken from the previous
521
   *          listIdentifiers() Map result.
522
   * @return a Map object containing an optional "resumptionToken" key/value
523
   *         pair and an "identifiers" Map object. The "identifiers" Map
524
   *         contains OAI identifier keys with corresponding values of "true" or
525
   *         null depending on whether the identifier is deleted or not.
526
   * @exception OAIBadRequestException
527
   *              signals an http status code 400 problem
528
   */
529
  public Map listIdentifiers(String resumptionToken)
530
      throws BadResumptionTokenException {
531
    purge(); // clean out old resumptionTokens
532
    Map listIdentifiersMap = new HashMap();
533
    ArrayList headers = new ArrayList();
534
    ArrayList identifiers = new ArrayList();
535

    
536
    /**********************************************************************
537
     * parse your resumptionToken and look it up in the resumptionResults, if
538
     * necessary
539
     **********************************************************************/
540
    StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
541
    String resumptionId;
542
    int oldCount;
543
    String metadataPrefix;
544
    int numRows;
545
    try {
546
      resumptionId = tokenizer.nextToken();
547
      oldCount = Integer.parseInt(tokenizer.nextToken());
548
      numRows = Integer.parseInt(tokenizer.nextToken());
549
      metadataPrefix = tokenizer.nextToken();
550
    } catch (NoSuchElementException e) {
551
      throw new BadResumptionTokenException();
552
    }
553

    
554
    /* Get some more records from your database */
555
    Iterator iterator = (Iterator) resumptionResults.remove(resumptionId);
556
    if (iterator == null) {
557
      System.out
558
          .println("MetacatCatalog.listIdentifiers(): reuse of old resumptionToken?");
559
      iterator = dateMap.entrySet().iterator();
560
      for (int i = 0; i < oldCount; ++i)
561
        iterator.next();
562
    }
563

    
564
    /* load the headers and identifiers ArrayLists. */
565
    int count = 0;
566
    while (count < maxListSize && iterator.hasNext()) {
567
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
568
      HashMap nativeHeader = getNativeHeader((String) entryDateMap.getKey());
569
      String[] header = getRecordFactory().createHeader(nativeHeader);
570
      headers.add(header[0]);
571
      identifiers.add(header[1]);
572
      count++;
573
    }
574

    
575
    /* decide if you're done. */
576
    if (iterator.hasNext()) {
577
      resumptionId = getRSName();
578
      resumptionResults.put(resumptionId, iterator);
579

    
580
      /*****************************************************************
581
       * Construct the resumptionToken String however you see fit.
582
       *****************************************************************/
583
      StringBuffer resumptionTokenSb = new StringBuffer();
584
      resumptionTokenSb.append(resumptionId);
585
      resumptionTokenSb.append(":");
586
      resumptionTokenSb.append(Integer.toString(oldCount + count));
587
      resumptionTokenSb.append(":");
588
      resumptionTokenSb.append(Integer.toString(numRows));
589
      resumptionTokenSb.append(":");
590
      resumptionTokenSb.append(metadataPrefix);
591

    
592
      /*****************************************************************
593
       * Use the following line if you wish to include the optional
594
       * resumptionToken attributes in the response. Otherwise, use the line
595
       * after it that I've commented out.
596
       *****************************************************************/
597
      listIdentifiersMap.put("resumptionMap", getResumptionMap(
598
          resumptionTokenSb.toString(), numRows, oldCount));
599
      // listIdentifiersMap.put("resumptionMap",
600
      // getResumptionMap(resumptionTokenSb.toString()));
601
    }
602

    
603
    listIdentifiersMap.put("headers", headers.iterator());
604
    listIdentifiersMap.put("identifiers", identifiers.iterator());
605
    return listIdentifiersMap;
606
  }
607

    
608

    
609
  /**
610
   * Run a query of the Metacat database to load the catalog of EML documents.
611
   * For each EML document, we store its 'docid', 'doctype', and 'date_updated'
612
   * values.
613
   */
614
  public void loadCatalog() {
615
    String query = 
616
      "SELECT docid, doctype, date_updated " +
617
      "FROM xml_documents " +
618
      "WHERE doctype like 'eml://ecoinformatics.org/eml-2%'";
619
    Statement stmt;
620

    
621
    try {
622
      Connection conn = getConnection();
623
      
624
      if (conn != null) {
625
        stmt = conn.createStatement();                          
626
        ResultSet rs = stmt.executeQuery(query);
627

    
628
        while (rs.next()) {
629
          String docid = rs.getString("docid");
630
          String doctype = rs.getString("doctype");
631
          String dateUpdated = rs.getDate("date_updated").toString();
632
          docTypeMap.put(docid, doctype);
633
          dateMap.put(docid, dateUpdated);
634
        }
635

    
636
        stmt.close();   
637
        conn.close();
638
      }
639
    }
640
    catch(SQLException e) {
641
      logger.error("SQLException: " + e.getMessage());
642
    }
643
  }
644
  
645

    
646
  /**
647
   * Retrieve a list of records that satisfy the specified criteria
648
   * 
649
   * @param from
650
   *          beginning date in the form of YYYY-MM-DD or null if earliest date
651
   *          is desired
652
   * @param until
653
   *          ending date in the form of YYYY-MM-DD or null if latest date is
654
   *          desired
655
   * @param set
656
   *          set name or null if no set is desired
657
   * @param metadataPrefix
658
   *          the OAI metadataPrefix
659
   * @return a Map object containing an optional "resumptionToken" key/value
660
   *         pair and a "records" Iterator object. The "records" Iterator
661
   *         contains a set of Records objects.
662
   * @exception OAIBadRequestException
663
   *              signals an http status code 400 problem
664
   * @exception OAIInternalServerError
665
   *              signals an http status code 500 problem
666
   */
667
  public Map listRecords(String from, String until, String set,
668
      String metadataPrefix) throws CannotDisseminateFormatException,
669
      OAIInternalServerError, NoItemsMatchException {
670
    purge(); // clean out old resumptionTokens
671
    Map listRecordsMap = new HashMap();
672
    ArrayList records = new ArrayList();
673
    Iterator iterator = dateMap.entrySet().iterator();
674
    int numRows = dateMap.entrySet().size();
675
    int count = 0;
676
    
677
    while (count < maxListSize && iterator.hasNext()) {
678
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
679
      String fileDate = (String) entryDateMap.getValue();
680
      
681
      if (fileDate.compareTo(from) >= 0 && fileDate.compareTo(until) <= 0) {
682
        try {
683
          String localIdentifier = (String) entryDateMap.getKey();
684
          HashMap nativeItem = getMetacatDocument(localIdentifier);
685
          String record = constructRecord(nativeItem, metadataPrefix);
686
          records.add(record);
687
          count++;
688
        } 
689
        catch (IOException e) {
690
          e.printStackTrace();
691
          throw new OAIInternalServerError(e.getMessage());
692
        }
693
      }
694
    }
695

    
696
    if (count == 0)
697
      throw new NoItemsMatchException();
698

    
699
    /* decide if you're done */
700
    if (iterator.hasNext()) {
701
      String resumptionId = getRSName();
702
      resumptionResults.put(resumptionId, iterator);
703

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

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

    
730

    
731
  /**
732
   * Retrieve the next set of records associated with the resumptionToken
733
   * 
734
   * @param resumptionToken
735
   *          implementation-dependent format taken from the previous
736
   *          listRecords() Map result.
737
   * @return a Map object containing an optional "resumptionToken" key/value
738
   *         pair and a "records" Iterator object. The "records" Iterator
739
   *         contains a set of Records objects.
740
   * @exception OAIBadRequestException
741
   *              signals an http status code 400 problem
742
   */
743
  public Map listRecords(String resumptionToken)
744
      throws BadResumptionTokenException {
745
    purge(); // clean out old resumptionTokens
746
    Map listRecordsMap = new HashMap();
747
    ArrayList records = new ArrayList();
748

    
749
    /**********************************************************************
750
     * parse your resumptionToken and look it up in the resumptionResults, if
751
     * necessary
752
     **********************************************************************/
753
    StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
754
    String resumptionId;
755
    int oldCount;
756
    String metadataPrefix;
757
    int numRows;
758
    
759
    try {
760
      resumptionId = tokenizer.nextToken();
761
      oldCount = Integer.parseInt(tokenizer.nextToken());
762
      numRows = Integer.parseInt(tokenizer.nextToken());
763
      metadataPrefix = tokenizer.nextToken();
764
    } 
765
    catch (NoSuchElementException e) {
766
      throw new BadResumptionTokenException();
767
    }
768

    
769
    /* Get some more records from your database */
770
    Iterator iterator = (Iterator) resumptionResults.remove(resumptionId);
771
    
772
    if (iterator == null) {
773
      System.out
774
          .println("MetacatCatalog.listRecords(): reuse of old resumptionToken?");
775
      iterator = dateMap.entrySet().iterator();
776
      for (int i = 0; i < oldCount; ++i)
777
        iterator.next();
778
    }
779

    
780
    /* load the records ArrayLists. */
781
    int count = 0;
782
    
783
    while (count < maxListSize && iterator.hasNext()) {
784
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
785
      
786
      try {
787
        String localIdentifier = (String) entryDateMap.getKey();
788
        HashMap nativeItem = getMetacatDocument(localIdentifier);
789
        String record = constructRecord(nativeItem, metadataPrefix);
790
        records.add(record);
791
        count++;
792
      } 
793
      catch (CannotDisseminateFormatException e) {
794
        /* the client hacked the resumptionToken beyond repair */
795
        throw new BadResumptionTokenException();
796
      } 
797
      catch (IOException e) {
798
        /* the file is probably missing */
799
        throw new BadResumptionTokenException();
800
      }
801
    }
802

    
803
    /* decide if you're done. */
804
    if (iterator.hasNext()) {
805
      resumptionId = getRSName();
806
      resumptionResults.put(resumptionId, iterator);
807

    
808
      /*****************************************************************
809
       * Construct the resumptionToken String however you see fit.
810
       *****************************************************************/
811
      StringBuffer resumptionTokenSb = new StringBuffer();
812
      resumptionTokenSb.append(resumptionId);
813
      resumptionTokenSb.append(":");
814
      resumptionTokenSb.append(Integer.toString(oldCount + count));
815
      resumptionTokenSb.append(":");
816
      resumptionTokenSb.append(Integer.toString(numRows));
817
      resumptionTokenSb.append(":");
818
      resumptionTokenSb.append(metadataPrefix);
819

    
820
      /*****************************************************************
821
       * Use the following line if you wish to include the optional
822
       * resumptionToken attributes in the response. Otherwise, use the line
823
       * after it that I've commented out.
824
       *****************************************************************/
825
      listRecordsMap.put("resumptionMap", 
826
                         getResumptionMap(resumptionTokenSb.toString(), 
827
                         numRows, 
828
                         oldCount)
829
                        );
830
      // listRecordsMap.put("resumptionMap",
831
      // getResumptionMap(resumptionTokenSb.toString()));
832
    }
833

    
834
    listRecordsMap.put("records", records.iterator());
835
    
836
    return listRecordsMap;
837
  }
838

    
839

    
840
  public Map listSets() throws NoSetHierarchyException {
841
    throw new NoSetHierarchyException();
842
    // Map listSetsMap = new HashMap();
843
    // listSetsMap.put("sets", setsList.iterator());
844
    // return listSetsMap;
845
  }
846

    
847

    
848
  public Map listSets(String resumptionToken)
849
      throws BadResumptionTokenException {
850
    throw new BadResumptionTokenException();
851
  }
852

    
853

    
854
  /**
855
   * Purge tokens that are older than the time-to-live.
856
   */
857
  private void purge() {
858
    ArrayList old = new ArrayList();
859
    Date then, now = new Date();
860
    Iterator keySet = resumptionResults.keySet().iterator();
861
    String key;
862

    
863
    while (keySet.hasNext()) {
864
      key = (String) keySet.next();
865
      then = new Date(Long.parseLong(key) + getMillisecondsToLive());
866
      if (now.after(then)) {
867
        old.add(key);
868
      }
869
    }
870
    Iterator iterator = old.iterator();
871
    while (iterator.hasNext()) {
872
      key = (String) iterator.next();
873
      resumptionResults.remove(key);
874
    }
875
  }
876

    
877
}
(1-1/2)