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
   * 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 docid, doctype, date_updated " +
94
  "FROM xml_documents " +
95
  "WHERE doctype like 'eml://ecoinformatics.org/eml-2%' AND " + 
96
  "  (docid IN " +
97
  "     (SELECT docid " +
98
  "      FROM xml_access " +
99
  "      WHERE( (lower(principal_name) = 'public') AND " +
100
  "             perm_type = 'allow' AND " +
101
  "             permission > 3" +
102
  "           )" +
103
  "     )" +
104
  "   AND " +
105
  "   docid NOT IN " +
106
  "     (SELECT docid " +
107
  "      FROM xml_access " +
108
  "      WHERE( (lower(principal_name) = 'public') AND " +
109
  "             perm_type = 'deny' AND " +
110
  "             perm_order ='allowFirst' AND " +
111
  "             permission > 3" +
112
  "           )" +
113
  "     )" +
114
  "  )";
115
  
116
  
117
/* Constructors */
118
  
119
  public MetacatCatalog(Properties properties) {
120
    String errorStr;
121
    String temp;
122

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

    
152
    loadCatalog();
153
  }
154

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

    
168
  
169
  /* Instance methods */
170

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

    
178

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

    
197
    if (metadataPrefix != null) {
198
      if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
199
        throw new CannotDisseminateFormatException(metadataPrefix);
200
    }
201
    
202
    RecordFactory recordFactory = getRecordFactory();
203
    String recordString = recordFactory.create(nativeItem, schemaURL, 
204
                                              metadataPrefix, setSpecs, abouts);
205
    return recordString;
206
  }
207

    
208

    
209
  private String date2OAIDatestamp(Date date) {
210
    return dateFormatter.format(date);
211
  }
212

    
213

    
214
  /**
215
   * get an Iterator containing the abouts for the nativeItem
216
   * 
217
   * @param rs
218
   *          ResultSet containing the nativeItem
219
   * @return an Iterator containing the list of about values for this nativeItem
220
   */
221
  private Iterator getAbouts(HashMap nativeItem) {
222
    return null;
223
  }
224

    
225

    
226
  /**
227
   * Returns a connection to the database. Opens the connection if a connection
228
   * has not already been made previously.
229
   * 
230
   * @return  conn  the database Connection object
231
   */
232
  public Connection getConnection() {
233
    Connection conn = null;
234
    
235
    try {
236
      Class.forName(metacatDBDriver);
237
    }
238
    catch (ClassNotFoundException e) {
239
      logger.error("Can't load driver " + e);
240
      return conn;
241
    } 
242

    
243
    // Make the database connection
244
    try {
245
      conn = DriverManager.getConnection(metacatDBURL, metacatDBUser, 
246
                                           metacatDBPassword);
247

    
248
      // If a SQLWarning object is available, print its warning(s).
249
      // There may be multiple warnings chained.
250
      SQLWarning warn = conn.getWarnings();
251
      
252
      if (warn != null) {
253
        while (warn != null) {
254
          logger.warn("SQLState: " + warn.getSQLState());
255
          logger.warn("Message:  " + warn.getMessage());
256
          logger.warn("Vendor: " + warn.getErrorCode());
257
          warn = warn.getNextWarning();
258
        }
259
      }
260
    }
261
    catch (SQLException e) {
262
      logger.error("Database access failed " + e);
263
    }
264
    
265
    return conn;
266
  }
267

    
268

    
269
  /**
270
   * Get the most recent date that the xml_documents table was updated
271
   * @return
272
   */
273
  public String getMaxDateUpdated() {
274
    String maxDateUpdated = null;
275
    String query = 
276
              "SELECT MAX(date_updated) AS max_date_updated FROM xml_documents";
277
    Statement stmt;
278

    
279
    try {
280
      Connection conn = getConnection();    
281
      if (conn != null) {
282
        stmt = conn.createStatement();                          
283
        ResultSet rs = stmt.executeQuery(query);
284
        while (rs.next()) {
285
          maxDateUpdated = rs.getDate("max_date_updated").toString();
286
        }
287
        stmt.close();   
288
        conn.close();
289
      }
290
    }
291
    catch(SQLException e) {
292
      logger.error("SQLException: " + e.getMessage());
293
    }
294
    
295
    return maxDateUpdated;
296
  }
297
  
298
  
299
  /**
300
   * Get a document from Metacat.
301
   * 
302
   * @param docid  the docid of the document to read
303
   * 
304
   * @return recordMap       a HashMap holding the document contents
305
   * 
306
   * @throws IOException
307
   */
308
  private HashMap<String, String> getMetacatDocument(String docid) 
309
      throws IOException {
310
    HashMap<String, String> recordMap = getNativeHeader(docid);
311
    
312
    if (recordMap == null) {
313
      return null;
314
    } 
315
    else {
316
      try {
317
        /* Perform a Metacat read operation on this docid */
318
        Metacat metacat = MetacatFactory.createMetacatConnection(metacatURL);
319
        Reader reader = metacat.read(docid);
320
        StringBuffer stringBuffer = IOUtil.getAsStringBuffer(reader, true);
321
        String emlString = stringBuffer.toString();
322
        recordMap.put("recordBytes", emlString);
323
      }
324
      catch (MetacatInaccessibleException e) {
325
        logger.error("MetacatInaccessibleException:\n" + e.getMessage());
326
      }
327
      catch (MetacatException e) {
328
        logger.error("MetacatException:\n" + e.getMessage());
329
      }
330
      catch (DocumentNotFoundException e) {
331
        logger.error("DocumentNotFoundException:\n" + e.getMessage());
332
      }
333
      catch (InsufficientKarmaException e) {
334
        logger.error("InsufficientKarmaException:\n" + e.getMessage());
335
      }
336
      catch (IOException e) {
337
        logger.error("Error reading EML document from metacat:\n" + 
338
                     e.getMessage()
339
                    );
340
      }
341
    }
342
    
343
    return recordMap;
344
  }
345

    
346

    
347
  private HashMap<String, String> getNativeHeader(String localIdentifier) {
348
    HashMap<String, String> recordMap = null;
349
    
350
    if (dateMap.containsKey(localIdentifier)) {
351
      recordMap = new HashMap<String, String>();
352
      recordMap.put("localIdentifier", localIdentifier);
353
      recordMap.put("lastModified", dateMap.get(localIdentifier));
354
      recordMap.put("doctype", docTypeMap.get(localIdentifier));
355
      return recordMap;
356
    }
357
    
358
    return recordMap;
359
  }
360

    
361

    
362
  /**
363
   * Retrieve the specified metadata for the specified oaiIdentifier
364
   * 
365
   * @param oaiIdentifier
366
   *          the OAI identifier
367
   * @param metadataPrefix
368
   *          the OAI metadataPrefix
369
   * @return the Record object containing the result.
370
   * @exception CannotDisseminateFormatException
371
   *              signals an http status code 400 problem
372
   * @exception IdDoesNotExistException
373
   *              signals an http status code 404 problem
374
   * @exception OAIInternalServerError
375
   *              signals an http status code 500 problem
376
   */
377
  public String getRecord(String oaiIdentifier, String metadataPrefix)
378
      throws IdDoesNotExistException, CannotDisseminateFormatException,
379
             OAIInternalServerError {
380
    HashMap<String, String> nativeItem = null;
381
    
382
    try {
383
      RecordFactory recordFactory = getRecordFactory();
384
      String localIdentifier = recordFactory.fromOAIIdentifier(oaiIdentifier);
385
      nativeItem = getMetacatDocument(localIdentifier);
386
      if (nativeItem == null) throw new IdDoesNotExistException(oaiIdentifier);
387
      return constructRecord(nativeItem, metadataPrefix);
388
    } 
389
    catch (IOException e) {
390
      e.printStackTrace();
391
      throw new OAIInternalServerError("Database Failure");
392
    }
393
  }
394

    
395

    
396
  /**
397
   * Retrieve a list of schemaLocation values associated with the specified
398
   * oaiIdentifier.
399
   * 
400
   * We get passed the ID for a record and are supposed to return a list of the
401
   * formats that we can deliver the record in. Since we are assuming that all
402
   * the records in the directory have the same format, the response to this is
403
   * static;
404
   * 
405
   * @param oaiIdentifier       the OAI identifier
406
   * 
407
   * @return a Vector containing schemaLocation Strings
408
   * 
409
   * @exception OAIBadRequestException
410
   *              signals an http status code 400 problem
411
   * @exception OAINotFoundException
412
   *              signals an http status code 404 problem
413
   * @exception OAIInternalServerError
414
   *              signals an http status code 500 problem
415
   */
416
  public Vector getSchemaLocations(String oaiIdentifier)
417
      throws IdDoesNotExistException, OAIInternalServerError,
418
      NoMetadataFormatsException {
419
    HashMap<String, String> nativeItem = null;
420
    
421
    try {
422
      String localIdentifier = getRecordFactory().fromOAIIdentifier(
423
          oaiIdentifier);
424
      nativeItem = getMetacatDocument(localIdentifier);
425
    } 
426
    catch (IOException e) {
427
      e.printStackTrace();
428
      throw new OAIInternalServerError("Database Failure");
429
    }
430

    
431
    if (nativeItem != null) {
432
      RecordFactory recordFactory = getRecordFactory();
433
      return recordFactory.getSchemaLocations(nativeItem);
434
    } 
435
    else {
436
      throw new IdDoesNotExistException(oaiIdentifier);
437
    }
438
  }
439

    
440

    
441
  /**
442
   * get an Iterator containing the setSpecs for the nativeItem
443
   * 
444
   * @param rs
445
   *          ResultSet containing the nativeItem
446
   * @return an Iterator containing the list of setSpec values for this
447
   *         nativeItem
448
   */
449
  private Iterator getSetSpecs(HashMap nativeItem) {
450
    return null;
451
  }
452

    
453

    
454
  /**
455
   * Override this method if some files exist in the filesystem that aren't
456
   * metadata records.
457
   * 
458
   * @param child
459
   *          the File to be investigated
460
   * @return true if it contains metadata, false otherwise
461
   */
462
  protected boolean isMetadataFile(File child) {
463
    return true;
464
  }
465

    
466

    
467
  /**
468
   * Retrieve a list of Identifiers that satisfy the criteria parameters
469
   * 
470
   * @param from
471
   *          beginning date in the form of YYYY-MM-DD or null if earliest date
472
   *          is desired
473
   * @param until
474
   *          ending date in the form of YYYY-MM-DD or null if latest date is
475
   *          desired
476
   * @param set
477
   *          set name or null if no set is desired
478
   * @return a Map object containing an optional "resumptionToken" key/value
479
   *         pair and an "identifiers" Map object. The "identifiers" Map
480
   *         contains OAI identifier keys with corresponding values of "true" or
481
   *         null depending on whether the identifier is deleted or not.
482
   * @exception OAIBadRequestException
483
   *              signals an http status code 400 problem
484
   */
485
  public Map listIdentifiers(String from, String until, String set,
486
      String metadataPrefix) throws NoItemsMatchException {
487
    purge(); // clean out old resumptionTokens
488
    Map listIdentifiersMap = new HashMap();
489
    ArrayList headers = new ArrayList();
490
    ArrayList identifiers = new ArrayList();
491
    Iterator iterator = dateMap.entrySet().iterator();
492
    int numRows = dateMap.entrySet().size();
493
    int count = 0;
494
    RecordFactory recordFactory = getRecordFactory();
495
    while (count < maxListSize && iterator.hasNext()) {
496
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
497
      String fileDate = (String) entryDateMap.getValue();
498
      if (fileDate.compareTo(from) >= 0 && fileDate.compareTo(until) <= 0) {
499
        String key = (String) entryDateMap.getKey();
500
        HashMap nativeHeader = getNativeHeader(key);
501
        String[] header = recordFactory.createHeader(nativeHeader);
502
        headers.add(header[0]);
503
        identifiers.add(header[1]);
504
        count++;
505
      }
506
    }
507

    
508
    if (count == 0)
509
      throw new NoItemsMatchException();
510

    
511
    /* decide if you're done */
512
    if (iterator.hasNext()) {
513
      String resumptionId = getRSName();
514
      resumptionResults.put(resumptionId, iterator);
515

    
516
      /*****************************************************************
517
       * Construct the resumptionToken String however you see fit.
518
       *****************************************************************/
519
      StringBuffer resumptionTokenSb = new StringBuffer();
520
      resumptionTokenSb.append(resumptionId);
521
      resumptionTokenSb.append(":");
522
      resumptionTokenSb.append(Integer.toString(count));
523
      resumptionTokenSb.append(":");
524
      resumptionTokenSb.append(Integer.toString(numRows));
525
      resumptionTokenSb.append(":");
526
      resumptionTokenSb.append(metadataPrefix);
527

    
528
      /*****************************************************************
529
       * Use the following line if you wish to include the optional
530
       * resumptionToken attributes in the response. Otherwise, use the line
531
       * after it that I've commented out.
532
       *****************************************************************/
533
      listIdentifiersMap.put("resumptionMap", getResumptionMap(
534
          resumptionTokenSb.toString(), numRows, 0));
535
      // listIdentifiersMap.put("resumptionMap",
536
      // getResumptionMap(resumptionTokenSb.toString()));
537
    }
538
    listIdentifiersMap.put("headers", headers.iterator());
539
    listIdentifiersMap.put("identifiers", identifiers.iterator());
540
    return listIdentifiersMap;
541
  }
542

    
543

    
544
  /**
545
   * Retrieve the next set of Identifiers associated with the resumptionToken
546
   * 
547
   * @param resumptionToken
548
   *          implementation-dependent format taken from the previous
549
   *          listIdentifiers() Map result.
550
   * @return a Map object containing an optional "resumptionToken" key/value
551
   *         pair and an "identifiers" Map object. The "identifiers" Map
552
   *         contains OAI identifier keys with corresponding values of "true" or
553
   *         null depending on whether the identifier is deleted or not.
554
   * @exception OAIBadRequestException
555
   *              signals an http status code 400 problem
556
   */
557
  public Map listIdentifiers(String resumptionToken)
558
      throws BadResumptionTokenException {
559
    purge(); // clean out old resumptionTokens
560
    Map listIdentifiersMap = new HashMap();
561
    ArrayList headers = new ArrayList();
562
    ArrayList identifiers = new ArrayList();
563

    
564
    /**********************************************************************
565
     * parse your resumptionToken and look it up in the resumptionResults, if
566
     * necessary
567
     **********************************************************************/
568
    StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
569
    String resumptionId;
570
    int oldCount;
571
    String metadataPrefix;
572
    int numRows;
573
    try {
574
      resumptionId = tokenizer.nextToken();
575
      oldCount = Integer.parseInt(tokenizer.nextToken());
576
      numRows = Integer.parseInt(tokenizer.nextToken());
577
      metadataPrefix = tokenizer.nextToken();
578
    } catch (NoSuchElementException e) {
579
      throw new BadResumptionTokenException();
580
    }
581

    
582
    /* Get some more records from your database */
583
    Iterator iterator = (Iterator) resumptionResults.remove(resumptionId);
584
    if (iterator == null) {
585
      System.out
586
          .println("MetacatCatalog.listIdentifiers(): reuse of old resumptionToken?");
587
      iterator = dateMap.entrySet().iterator();
588
      for (int i = 0; i < oldCount; ++i)
589
        iterator.next();
590
    }
591

    
592
    /* load the headers and identifiers ArrayLists. */
593
    int count = 0;
594
    while (count < maxListSize && iterator.hasNext()) {
595
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
596
      HashMap nativeHeader = getNativeHeader((String) entryDateMap.getKey());
597
      String[] header = getRecordFactory().createHeader(nativeHeader);
598
      headers.add(header[0]);
599
      identifiers.add(header[1]);
600
      count++;
601
    }
602

    
603
    /* decide if you're done. */
604
    if (iterator.hasNext()) {
605
      resumptionId = getRSName();
606
      resumptionResults.put(resumptionId, iterator);
607

    
608
      /*****************************************************************
609
       * Construct the resumptionToken String however you see fit.
610
       *****************************************************************/
611
      StringBuffer resumptionTokenSb = new StringBuffer();
612
      resumptionTokenSb.append(resumptionId);
613
      resumptionTokenSb.append(":");
614
      resumptionTokenSb.append(Integer.toString(oldCount + count));
615
      resumptionTokenSb.append(":");
616
      resumptionTokenSb.append(Integer.toString(numRows));
617
      resumptionTokenSb.append(":");
618
      resumptionTokenSb.append(metadataPrefix);
619

    
620
      /*****************************************************************
621
       * Use the following line if you wish to include the optional
622
       * resumptionToken attributes in the response. Otherwise, use the line
623
       * after it that I've commented out.
624
       *****************************************************************/
625
      listIdentifiersMap.put("resumptionMap", getResumptionMap(
626
          resumptionTokenSb.toString(), numRows, oldCount));
627
      // listIdentifiersMap.put("resumptionMap",
628
      // getResumptionMap(resumptionTokenSb.toString()));
629
    }
630

    
631
    listIdentifiersMap.put("headers", headers.iterator());
632
    listIdentifiersMap.put("identifiers", identifiers.iterator());
633
    return listIdentifiersMap;
634
  }
635

    
636

    
637
  /**
638
   * Run a query of the Metacat database to load the catalog of EML documents.
639
   * For each EML document, we store its 'docid', 'doctype', and 'date_updated'
640
   * values.
641
   */
642
  public void loadCatalog() {
643
    Statement stmt;
644

    
645
    try {
646
      Connection conn = getConnection();
647
      
648
      if (conn != null) {
649
        stmt = conn.createStatement();                          
650
        ResultSet rs = stmt.executeQuery(QUERY);
651
        
652
        int documentCount = 0;
653

    
654
        while (rs.next()) {
655
          documentCount++;
656
          String docid = rs.getString("docid");
657
          String doctype = rs.getString("doctype");
658
          String dateUpdated = rs.getDate("date_updated").toString();
659
          docTypeMap.put(docid, doctype);
660
          dateMap.put(docid, dateUpdated);
661
        }
662
        
663
        logger.info("Number of documents in catalog: " + documentCount);
664

    
665
        stmt.close();   
666
        conn.close();
667
      }
668
    }
669
    catch(SQLException e) {
670
      logger.error("SQLException: " + e.getMessage());
671
    }
672
  }
673
  
674

    
675
  /**
676
   * Retrieve a list of records that satisfy the specified criteria
677
   * 
678
   * @param from
679
   *          beginning date in the form of YYYY-MM-DD or null if earliest date
680
   *          is desired
681
   * @param until
682
   *          ending date in the form of YYYY-MM-DD or null if latest date is
683
   *          desired
684
   * @param set
685
   *          set name or null if no set is desired
686
   * @param metadataPrefix
687
   *          the OAI metadataPrefix
688
   * @return a Map object containing an optional "resumptionToken" key/value
689
   *         pair and a "records" Iterator object. The "records" Iterator
690
   *         contains a set of Records objects.
691
   * @exception OAIBadRequestException
692
   *              signals an http status code 400 problem
693
   * @exception OAIInternalServerError
694
   *              signals an http status code 500 problem
695
   */
696
  public Map listRecords(String from, String until, String set,
697
      String metadataPrefix) throws CannotDisseminateFormatException,
698
      OAIInternalServerError, NoItemsMatchException {
699
    purge(); // clean out old resumptionTokens
700
    Map listRecordsMap = new HashMap();
701
    ArrayList records = new ArrayList();
702
    Iterator iterator = dateMap.entrySet().iterator();
703
    int numRows = dateMap.entrySet().size();
704
    int count = 0;
705
    
706
    while (count < maxListSize && iterator.hasNext()) {
707
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
708
      String fileDate = (String) entryDateMap.getValue();
709
      
710
      if (fileDate.compareTo(from) >= 0 && fileDate.compareTo(until) <= 0) {
711
        try {
712
          String localIdentifier = (String) entryDateMap.getKey();
713
          HashMap nativeItem = getMetacatDocument(localIdentifier);
714
          String record = constructRecord(nativeItem, metadataPrefix);
715
          records.add(record);
716
          count++;
717
        } 
718
        catch (IOException e) {
719
          e.printStackTrace();
720
          throw new OAIInternalServerError(e.getMessage());
721
        }
722
      }
723
    }
724

    
725
    if (count == 0)
726
      throw new NoItemsMatchException();
727

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

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

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

    
759

    
760
  /**
761
   * Retrieve the next set of records associated with the resumptionToken
762
   * 
763
   * @param resumptionToken
764
   *          implementation-dependent format taken from the previous
765
   *          listRecords() Map result.
766
   * @return a Map object containing an optional "resumptionToken" key/value
767
   *         pair and a "records" Iterator object. The "records" Iterator
768
   *         contains a set of Records objects.
769
   * @exception OAIBadRequestException
770
   *              signals an http status code 400 problem
771
   */
772
  public Map listRecords(String resumptionToken)
773
      throws BadResumptionTokenException {
774
    purge(); // clean out old resumptionTokens
775
    Map listRecordsMap = new HashMap();
776
    ArrayList records = new ArrayList();
777

    
778
    /**********************************************************************
779
     * parse your resumptionToken and look it up in the resumptionResults, if
780
     * necessary
781
     **********************************************************************/
782
    StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
783
    String resumptionId;
784
    int oldCount;
785
    String metadataPrefix;
786
    int numRows;
787
    
788
    try {
789
      resumptionId = tokenizer.nextToken();
790
      oldCount = Integer.parseInt(tokenizer.nextToken());
791
      numRows = Integer.parseInt(tokenizer.nextToken());
792
      metadataPrefix = tokenizer.nextToken();
793
    } 
794
    catch (NoSuchElementException e) {
795
      throw new BadResumptionTokenException();
796
    }
797

    
798
    /* Get some more records from your database */
799
    Iterator iterator = (Iterator) resumptionResults.remove(resumptionId);
800
    
801
    if (iterator == null) {
802
      System.out
803
          .println("MetacatCatalog.listRecords(): reuse of old resumptionToken?");
804
      iterator = dateMap.entrySet().iterator();
805
      for (int i = 0; i < oldCount; ++i)
806
        iterator.next();
807
    }
808

    
809
    /* load the records ArrayLists. */
810
    int count = 0;
811
    
812
    while (count < maxListSize && iterator.hasNext()) {
813
      Map.Entry entryDateMap = (Map.Entry) iterator.next();
814
      
815
      try {
816
        String localIdentifier = (String) entryDateMap.getKey();
817
        HashMap nativeItem = getMetacatDocument(localIdentifier);
818
        String record = constructRecord(nativeItem, metadataPrefix);
819
        records.add(record);
820
        count++;
821
      } 
822
      catch (CannotDisseminateFormatException e) {
823
        /* the client hacked the resumptionToken beyond repair */
824
        throw new BadResumptionTokenException();
825
      } 
826
      catch (IOException e) {
827
        /* the file is probably missing */
828
        throw new BadResumptionTokenException();
829
      }
830
    }
831

    
832
    /* decide if you're done. */
833
    if (iterator.hasNext()) {
834
      resumptionId = getRSName();
835
      resumptionResults.put(resumptionId, iterator);
836

    
837
      /*****************************************************************
838
       * Construct the resumptionToken String however you see fit.
839
       *****************************************************************/
840
      StringBuffer resumptionTokenSb = new StringBuffer();
841
      resumptionTokenSb.append(resumptionId);
842
      resumptionTokenSb.append(":");
843
      resumptionTokenSb.append(Integer.toString(oldCount + count));
844
      resumptionTokenSb.append(":");
845
      resumptionTokenSb.append(Integer.toString(numRows));
846
      resumptionTokenSb.append(":");
847
      resumptionTokenSb.append(metadataPrefix);
848

    
849
      /*****************************************************************
850
       * Use the following line if you wish to include the optional
851
       * resumptionToken attributes in the response. Otherwise, use the line
852
       * after it that I've commented out.
853
       *****************************************************************/
854
      listRecordsMap.put("resumptionMap", 
855
                         getResumptionMap(resumptionTokenSb.toString(), 
856
                         numRows, 
857
                         oldCount)
858
                        );
859
      // listRecordsMap.put("resumptionMap",
860
      // getResumptionMap(resumptionTokenSb.toString()));
861
    }
862

    
863
    listRecordsMap.put("records", records.iterator());
864
    
865
    return listRecordsMap;
866
  }
867

    
868

    
869
  public Map listSets() throws NoSetHierarchyException {
870
    throw new NoSetHierarchyException();
871
    // Map listSetsMap = new HashMap();
872
    // listSetsMap.put("sets", setsList.iterator());
873
    // return listSetsMap;
874
  }
875

    
876

    
877
  public Map listSets(String resumptionToken)
878
      throws BadResumptionTokenException {
879
    throw new BadResumptionTokenException();
880
  }
881

    
882

    
883
  /**
884
   * Purge tokens that are older than the time-to-live.
885
   */
886
  private void purge() {
887
    ArrayList old = new ArrayList();
888
    Date then, now = new Date();
889
    Iterator keySet = resumptionResults.keySet().iterator();
890
    String key;
891

    
892
    while (keySet.hasNext()) {
893
      key = (String) keySet.next();
894
      then = new Date(Long.parseLong(key) + getMillisecondsToLive());
895
      if (now.after(then)) {
896
        old.add(key);
897
      }
898
    }
899
    Iterator iterator = old.iterator();
900
    while (iterator.hasNext()) {
901
      key = (String) iterator.next();
902
      resumptionResults.remove(key);
903
    }
904
  }
905

    
906
}
(1-1/2)