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 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
  /*
118
   * QUERY string to determine the most recent 'date_updated' value stored
119
   * in Metacat's 'xml_documents' table.
120
   */
121
  private final String LAST_UPDATED_QUERY =
122
  "SELECT MAX(date_updated) " +
123
  "FROM xml_documents " +
124
  "WHERE doctype like 'eml://ecoinformatics.org/eml-2%' AND " + 
125
  "  (docid IN " +
126
  "     (SELECT docid " +
127
  "      FROM xml_access " +
128
  "      WHERE( (lower(principal_name) = 'public') AND " +
129
  "             perm_type = 'allow' AND " +
130
  "             permission > 3" +
131
  "           )" +
132
  "     )" +
133
  "   AND " +
134
  "   docid NOT IN " +
135
  "     (SELECT docid " +
136
  "      FROM xml_access " +
137
  "      WHERE( (lower(principal_name) = 'public') AND " +
138
  "             perm_type = 'deny' AND " +
139
  "             perm_order ='allowFirst' AND " +
140
  "             permission > 3" +
141
  "           )" +
142
  "     )" +
143
  "  )";
144
  
145
  
146
/* Constructors */
147
  
148
  public MetacatCatalog(Properties properties) {
149
    String errorStr;
150
    String temp;
151

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

    
179
    loadCatalog();
180
  }
181

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

    
195
  
196
  /* Instance methods */
197

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

    
205

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

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

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

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

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

    
280
    }
281

    
282
    return aDateMap;
283
  }
284

    
285

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

    
297

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

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

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

    
340

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

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

    
418

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

    
433

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

    
469

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

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

    
514

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

    
527

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

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

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

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

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

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

    
671

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

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

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

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

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

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

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

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

    
764

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

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

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

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

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

    
855

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

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

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

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

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

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

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

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

    
964

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

    
972

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

    
978

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

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

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

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

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

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

    
1042

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

    
1082
}
(1-1/2)