1 | #include "oaiaction.h"
|
---|
2 | #include "oaitools.h"
|
---|
3 | #include "recptprototools.h"
|
---|
4 |
|
---|
5 | #if defined(GSDL_USE_IOS_H)
|
---|
6 | # if defined(__WIN32__)
|
---|
7 | # include <strstrea.h> // vc4
|
---|
8 | # else
|
---|
9 | # include <strstream.h>
|
---|
10 | # endif
|
---|
11 | #else
|
---|
12 | # include <sstream>
|
---|
13 | #endif
|
---|
14 |
|
---|
15 | #include <time.h>
|
---|
16 |
|
---|
17 |
|
---|
18 | oaiaction::oaiaction(const text_t &name)
|
---|
19 | {
|
---|
20 | this->logout = new ofstream("oai.log", ios::app);
|
---|
21 | this->configuration = NULL;
|
---|
22 | this->name = name;
|
---|
23 | }
|
---|
24 |
|
---|
25 | //----------------------------------------------------------------------------------------------
|
---|
26 |
|
---|
27 | // Over-ridden by child classes
|
---|
28 | bool oaiaction::validateAction(recptproto *protocol, oaiargs ¶ms)
|
---|
29 | {
|
---|
30 | this->errorType = "";
|
---|
31 | return true;
|
---|
32 | }
|
---|
33 |
|
---|
34 | //----------------------------------------------------------------------------------------------
|
---|
35 |
|
---|
36 | /**********
|
---|
37 | * Compare the supplied metadataPrefix to all those that
|
---|
38 | * are supported. If there is NO match, return true. If
|
---|
39 | * it DOES match one, return false.
|
---|
40 | */
|
---|
41 | bool oaiaction::formatNotSupported(text_t &metaFormat)
|
---|
42 | {
|
---|
43 | // is it in our list?
|
---|
44 | if (this->configuration->getMetadataSet().count(metaFormat) == 0) return true;
|
---|
45 | return false;
|
---|
46 | }
|
---|
47 |
|
---|
48 | //----------------------------------------------------------------------------------------------
|
---|
49 |
|
---|
50 | /**********
|
---|
51 | * Function for outputting the appropriate error(s) with the (version 2.0) request.
|
---|
52 | * The error(s) MUST be included in the response, and take the form:
|
---|
53 | * <error code="errorType">Description of error</error>
|
---|
54 | */
|
---|
55 | void oaiaction::output_error(ostream &output, text_t &errorType)
|
---|
56 | {
|
---|
57 | text_t description = "";
|
---|
58 |
|
---|
59 | if(errorType == "badArgument"){
|
---|
60 | description = "The request includes illegal arguments, is missing required arguments, repeats arguments or the value for an argument has an illegal syntax";
|
---|
61 | }
|
---|
62 | else if(errorType == "noRecordsMatch"){
|
---|
63 | description = "No record matches all the requested parameters";
|
---|
64 | }
|
---|
65 | else if(errorType == "cannotDisseminateFormat"){
|
---|
66 | description = "The metadata format specified is not supported by the item or by the repository";
|
---|
67 | }
|
---|
68 | else if(errorType == "idDoesNotExist"){
|
---|
69 | description = "The value of the identifier is unknown or illegal in this repository";
|
---|
70 | }
|
---|
71 | else if(errorType == "badVerb"){
|
---|
72 | description = "Value of the verb argument is illegal, missing, or repeated";
|
---|
73 | }
|
---|
74 | else if(errorType == "noMetadataFormats"){
|
---|
75 | description = "There are no metadata formats available for the item";
|
---|
76 | }
|
---|
77 | else if(errorType == "badResumptionToken"){
|
---|
78 | description = "The value of the resumptionToken argument is invalid or expired";
|
---|
79 | }
|
---|
80 | else if(errorType == "noSetHierarchy"){
|
---|
81 | description = "The repository does not support sets";
|
---|
82 | }
|
---|
83 |
|
---|
84 | output << " <error code=\"" << errorType << "\">" << description << "</error>\n";
|
---|
85 | }
|
---|
86 |
|
---|
87 | //----------------------------------------------------------------------------------------------
|
---|
88 |
|
---|
89 | text_t oaiaction::getName()
|
---|
90 | {
|
---|
91 | return this->name;
|
---|
92 | }
|
---|
93 |
|
---|
94 | //----------------------------------------------------------------------------------------------
|
---|
95 |
|
---|
96 | /**********
|
---|
97 | * Used in version 2.0 to provide the <datestamp> tag for each document. The function is passed
|
---|
98 | * the 'last modified' date of the file in epoch time (time in seconds since 1970-01-01 on Unix
|
---|
99 | * systems) and converts it to YYYY-MM-DD format.
|
---|
100 | */
|
---|
101 | text_t oaiaction::parseDatestamp(time_t &rawtime)
|
---|
102 | {
|
---|
103 | text_t year, month, day, lastModified;
|
---|
104 | tm *ptm;
|
---|
105 | ptm = gmtime(&rawtime);
|
---|
106 | int raw_month = ptm->tm_mon + 1;
|
---|
107 | int raw_day = ptm->tm_mday;
|
---|
108 |
|
---|
109 | year = (ptm->tm_year + 1900); // Takes off 1900 for year by default, so replace it to get YYYY format
|
---|
110 |
|
---|
111 | // Need the month in MM format, so if month is 1..9, add a 0 to the front
|
---|
112 | if(raw_month < 10){
|
---|
113 | month = "0";
|
---|
114 | month += raw_month;
|
---|
115 | }
|
---|
116 | else month = raw_month;
|
---|
117 |
|
---|
118 | if(raw_day < 10){
|
---|
119 | day = "0";
|
---|
120 | day += raw_day;
|
---|
121 | }
|
---|
122 | else day = raw_day;
|
---|
123 |
|
---|
124 | lastModified = year + "-" + month + "-" + day;
|
---|
125 |
|
---|
126 | return lastModified;
|
---|
127 | }
|
---|
128 |
|
---|
129 | //----------------------------------------------------------------------------------------------
|
---|
130 | /**********
|
---|
131 | * Used by both versions to get the date & time of the client's request. The <responseDate> tag is
|
---|
132 | * in the format YYYY-MM-DDTHH:MM:SSZ, where T is simply the letter 'T' and Z is the local offset from
|
---|
133 | * GMT (now UTC). Examples of Z are +12:00 (NZ) or -03:00 for version 1.1. In version 2.0, the time
|
---|
134 | * is expected to be in UTC, and Z should simply be the character 'Z'.
|
---|
135 | */
|
---|
136 | void oaiaction::getResponseDate(text_t &date)
|
---|
137 | {
|
---|
138 | time_t rawtime;
|
---|
139 | tm *ptm;
|
---|
140 |
|
---|
141 | time(&rawtime); // Get the epoch time
|
---|
142 |
|
---|
143 | ptm = gmtime(&rawtime); // Convert to UTC-time formatted tm object
|
---|
144 |
|
---|
145 | text_t month, day, hour, minute, second;
|
---|
146 | int raw_month = ptm->tm_mon + 1; // Note Jan = 0 ... Dec = 11, so add 1
|
---|
147 | int raw_day = ptm->tm_mday;
|
---|
148 | int raw_hour = ptm->tm_hour;
|
---|
149 | int raw_minute = ptm->tm_min;
|
---|
150 | int raw_second = ptm->tm_sec;
|
---|
151 |
|
---|
152 | // Need the month in MM format, so if month is 1..9, add a 0 to the front
|
---|
153 | if(raw_month < 10){
|
---|
154 | month = "0";
|
---|
155 | }
|
---|
156 | month += raw_month;
|
---|
157 |
|
---|
158 | // Same for days, hours, minutes and seconds
|
---|
159 | if(raw_day < 10){
|
---|
160 | day = "0";
|
---|
161 | }
|
---|
162 | day += raw_day;
|
---|
163 |
|
---|
164 | if(raw_hour < 10){
|
---|
165 | hour = "0";
|
---|
166 | }
|
---|
167 | hour += raw_hour;
|
---|
168 |
|
---|
169 | if(raw_minute < 10){
|
---|
170 | minute = "0";
|
---|
171 | }
|
---|
172 | minute += raw_minute;
|
---|
173 |
|
---|
174 | if(raw_second < 10){
|
---|
175 | second = "0";
|
---|
176 | }
|
---|
177 | second += raw_second;
|
---|
178 |
|
---|
179 | // Want YYYY-MM-DDTHH:MM:SS+UTC, where UTC is num hours from UTC(GMT) to localtime
|
---|
180 | date += (ptm->tm_year + 1900); // Takes off 1900 for year, so replace it to get YYYY format
|
---|
181 | date += "-";
|
---|
182 | date += month;
|
---|
183 | date += "-";
|
---|
184 | date += day;
|
---|
185 | date += "T";
|
---|
186 | date += hour;
|
---|
187 | date += ":";
|
---|
188 | date += minute;
|
---|
189 | date += ":";
|
---|
190 | date += second;
|
---|
191 | // If we're using v1.1, then tack on local time offset, otherwise don't
|
---|
192 | if(this->configuration->getOAIVersion() == 110){
|
---|
193 | date += _LOCALTIME_; // Defined in oaiaction.h. States how many hours localtime is from
|
---|
194 | // UTC (GMT), e.g. "+8:00", "-5:00"
|
---|
195 | }
|
---|
196 | else
|
---|
197 | date += "Z"; // If v2.0, we put 'Z' on the end rather than the localtime offset
|
---|
198 | }
|
---|
199 |
|
---|
200 | //----------------------------------------------------------------------------------------------
|
---|
201 | /**********
|
---|
202 | * Does different request tags depending on the version of the OAI protocol running
|
---|
203 | */
|
---|
204 | void oaiaction::getRequestURL(oaiargs ¶ms, text_t &requestURL)
|
---|
205 | {
|
---|
206 | // Iterators for moving through the list of parameters (keys) specified
|
---|
207 | text_tmap::const_iterator here;
|
---|
208 | text_tmap::const_iterator end;
|
---|
209 | int numArgs = params.getSize();
|
---|
210 |
|
---|
211 | here = params.begin();
|
---|
212 | end = params.end();
|
---|
213 |
|
---|
214 | text_t baseURL = this->configuration->getBaseURL();
|
---|
215 |
|
---|
216 | int version = this->configuration->getOAIVersion();
|
---|
217 |
|
---|
218 | switch(version){
|
---|
219 | case 110:
|
---|
220 | /* Takes the form:
|
---|
221 | * <requestURL>http://baseURL.com/oaimain?verb="someVerb"&key=value</requestURL>
|
---|
222 | */
|
---|
223 | requestURL = " <requestURL>" + baseURL;
|
---|
224 |
|
---|
225 | if(numArgs == 0) break; // If no args, all done - the error will be picked up later
|
---|
226 |
|
---|
227 | // The following lines will give us the "label=value" syntax
|
---|
228 | requestURL += "?";
|
---|
229 | requestURL += here->first;
|
---|
230 | requestURL += "=";
|
---|
231 | requestURL += html_safe(here->second); // parse the argument to ensure it is URL-encoding compliant
|
---|
232 | ++here;
|
---|
233 |
|
---|
234 | while(here != end){
|
---|
235 | requestURL +="&"; // Stick in the ampersand in URL encoding
|
---|
236 | requestURL += (here->first + "=" + html_safe(here->second));
|
---|
237 | ++here;
|
---|
238 | }
|
---|
239 | requestURL += "</requestURL>\n";
|
---|
240 | break;
|
---|
241 |
|
---|
242 | case 200:
|
---|
243 | default:
|
---|
244 | /* Takes the form:
|
---|
245 | * <request verb="someVerb" key="value" key="value">
|
---|
246 | * http://baseURL.com/oaimain</request>
|
---|
247 | */
|
---|
248 | if(numArgs == 0) {
|
---|
249 | requestURL = " <request>" + baseURL + "</request>\n";
|
---|
250 | break;
|
---|
251 | }
|
---|
252 | requestURL = " <request " + here->first + "=\"" + html_safe(here->second) + "\"";
|
---|
253 | ++here;
|
---|
254 | while(here != end){
|
---|
255 | requestURL += (" " + here->first + "=\"" + html_safe(here->second) + "\"");
|
---|
256 | ++here;
|
---|
257 | }
|
---|
258 | requestURL += ">\n " + baseURL + "</request>\n";
|
---|
259 | break;
|
---|
260 | }
|
---|
261 | }
|
---|
262 |
|
---|
263 | //----------------------------------------------------------------------------------------------
|
---|
264 | /**********
|
---|
265 | * Send the (OAI version-dependent) response text to the output stream
|
---|
266 | */
|
---|
267 | void oaiaction::getResponse(ostream &output, recptproto *protocol, oaiargs ¶ms)
|
---|
268 | {
|
---|
269 | bool error;
|
---|
270 | text_t date, requestURL;
|
---|
271 |
|
---|
272 | // Write the response date & time into 'date'
|
---|
273 | this->getResponseDate(date);
|
---|
274 | int version = this->configuration->getOAIVersion();
|
---|
275 |
|
---|
276 | // validate the action
|
---|
277 | error = !this->validateAction(protocol, params);
|
---|
278 |
|
---|
279 | // raise an error for duplicated arguments and set the
|
---|
280 | // error type "manually" here...
|
---|
281 | if (params.hasDuplicateArg() && !error) {
|
---|
282 | this->errorType = "badArgument";
|
---|
283 | error = true;
|
---|
284 | }
|
---|
285 |
|
---|
286 | // start with the required http header
|
---|
287 | if (version <= 110 && error){
|
---|
288 | output << "Status: 400 " << this->errorType << "\n";
|
---|
289 | output << "Content-Type: text/xml\n\n";
|
---|
290 | return;
|
---|
291 | }
|
---|
292 |
|
---|
293 | output << "Status: 200\n";
|
---|
294 | output << "Content-Type: text/xml\n\n";
|
---|
295 |
|
---|
296 | // output xml header parts
|
---|
297 | output << "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n";
|
---|
298 |
|
---|
299 | if(version <= 110){
|
---|
300 | // output OAI v1.1 action header tag
|
---|
301 | output << "<" << this->name;
|
---|
302 | output << "\n xmlns=\"http://www.openarchives.org/OAI/1.1/OAI_" << this->name << "\" ";
|
---|
303 | output << "\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ";
|
---|
304 | output << "\n xsi:schemaLocation=\"http://www.openarchives.org/OAI/1.1/OAI_" << this->name;
|
---|
305 | output << "\n http://www.openarchives.org/OAI/1.1/OAI_" << this->name << ".xsd\">\n";
|
---|
306 | }
|
---|
307 | else {
|
---|
308 | text_t baseDocRoot = this->configuration->getBaseDocRoot();
|
---|
309 | output << "<?xml-stylesheet type=\"text/xsl\" href=\""<<baseDocRoot<<"/web/style/oai2.xsl\" ?>\n";
|
---|
310 | // output OAI v2.0 action header tag
|
---|
311 | output << "<OAI-PMH xmlns=\"http://www.openarchives.org/OAI/2.0/\"\n"
|
---|
312 | << " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
|
---|
313 | << " xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/\n"
|
---|
314 | << " http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd\">\n";
|
---|
315 | }
|
---|
316 | // output current time for response
|
---|
317 | output << " <responseDate>" << date << "</responseDate>\n";
|
---|
318 |
|
---|
319 | // output request URL. This differs depending on the OAI protocol version currently running, so
|
---|
320 | // the entire field - including tags - must be put into the text_t variable by getRequestURL()
|
---|
321 | this->getRequestURL(params, requestURL);
|
---|
322 |
|
---|
323 | output << requestURL ;
|
---|
324 |
|
---|
325 | if (error == false) {
|
---|
326 | // a string stream to write the content of the action to; this is done so that we can
|
---|
327 | // avoid outputting the action tag if the action's body is unsuccessful, in which
|
---|
328 | // case the leading tag must be suppressed
|
---|
329 | #if defined(GSDL_USE_IOS_H)
|
---|
330 | ostrstream outstream;
|
---|
331 | #else
|
---|
332 | ostringstream outstream;
|
---|
333 | #endif
|
---|
334 |
|
---|
335 | // Version 2.0 needs an <Identify>, etc. tag after the OAI-PMH header, IF there is no error
|
---|
336 | //
|
---|
337 | // An action that outputs no content should raise an error state to suppress the
|
---|
338 | // matching opening and close tags if it outputs no content in OAI 2.0
|
---|
339 | error = !this->output_content(outstream, protocol, params);
|
---|
340 |
|
---|
341 | // output the leading tag if no error occurred
|
---|
342 | if (error == false) {
|
---|
343 | if (version >= 200) {
|
---|
344 | this->output_action_tag(output, true);
|
---|
345 | }
|
---|
346 | }
|
---|
347 |
|
---|
348 | // now output the body of the action content
|
---|
349 | #if defined(GSDL_USE_IOS_H)
|
---|
350 | outstream << ends; // Ensure outstream is null-terminated correctly
|
---|
351 | #endif
|
---|
352 | output << outstream.str();
|
---|
353 | }
|
---|
354 | else {
|
---|
355 | if (version >= 200) {
|
---|
356 | this->output_error(output, this->errorType);
|
---|
357 | }
|
---|
358 | }
|
---|
359 |
|
---|
360 | // close out our response - both versions need this line, but v2.0 only needs it if there was no error
|
---|
361 | if((version == 110) || (version >= 200 && error == false)){
|
---|
362 | this->output_action_tag(output, false);
|
---|
363 | }
|
---|
364 | if(version >= 200){
|
---|
365 | output << "</OAI-PMH>\n";
|
---|
366 | }
|
---|
367 | }
|
---|
368 |
|
---|
369 | void oaiaction::output_action_tag(ostream &output, bool openTag)
|
---|
370 | {
|
---|
371 | output << " <";
|
---|
372 | if (!openTag) {
|
---|
373 | output << "/";
|
---|
374 | }
|
---|
375 | output << this->name << ">" << endl;
|
---|
376 | }
|
---|
377 |
|
---|
378 | void oaiaction::output_record_header(ostream &output, const text_t &oaiLabel, const text_t &lastModified,
|
---|
379 | const text_tarray &memberOf, int oaiVersion)
|
---|
380 | {
|
---|
381 | output << " <header>" << endl;
|
---|
382 | output << " <identifier>" << oaiLabel << "</identifier>" << endl;
|
---|
383 | output << " <datestamp>" << lastModified << "</datestamp>" << endl;
|
---|
384 |
|
---|
385 | text_t collection_id;
|
---|
386 | // Find the collection id from oai:repos-id:collection:doc
|
---|
387 | oaiclassifier::getCollectionFromOAIID(oaiLabel, collection_id);
|
---|
388 | if(oaiVersion >= 200){
|
---|
389 | text_tarray::const_iterator member = memberOf.begin();
|
---|
390 | text_tarray::const_iterator memEnd = memberOf.end();
|
---|
391 |
|
---|
392 | // As well as all the subsets that a doc appears in, it is also a member of the 'collection' set
|
---|
393 | output << " <setSpec>" << collection_id << "</setSpec>" << endl;
|
---|
394 | while (member != memEnd) {
|
---|
395 | text_t oaiSet = *member;
|
---|
396 | oaiclassifier::toOAI(collection_id, oaiSet);
|
---|
397 | output << " <setSpec>" << oaiSet << "</setSpec>" << endl;
|
---|
398 | ++member;
|
---|
399 | }
|
---|
400 | }
|
---|
401 | output << " </header>" << endl;
|
---|
402 | }
|
---|
403 |
|
---|
404 | void oaiaction::getLastModifiedDate(ResultDocInfo_t &doc_info, text_t &lastModified)
|
---|
405 | {
|
---|
406 | text_t temp;
|
---|
407 |
|
---|
408 |
|
---|
409 | MetadataInfo_tmap::iterator current = doc_info.metadata.begin();
|
---|
410 | MetadataInfo_tmap::iterator end = doc_info.metadata.end();
|
---|
411 |
|
---|
412 | while(current != end){
|
---|
413 | temp = current->first;
|
---|
414 | lc(temp);
|
---|
415 | if(temp == "gs.oaidatestamp"){
|
---|
416 | // assume it is correct format
|
---|
417 | lastModified = current->second.values[0];
|
---|
418 | return;
|
---|
419 | }
|
---|
420 | else{
|
---|
421 | if (temp == "lastmodified" && lastModified == "" && current->second.values.size() >= 1) {
|
---|
422 | lastModified = current->second.values[0];
|
---|
423 | time_t raw_time = (time_t)lastModified.getint();
|
---|
424 | lastModified = this->parseDatestamp(raw_time);
|
---|
425 | }
|
---|
426 | }
|
---|
427 | ++current;
|
---|
428 | }
|
---|
429 | }
|
---|
430 |
|
---|
431 | bool oaiaction::inDateRange(const text_t &from, const text_t &until, const text_t &collection,
|
---|
432 | const text_t &OID, recptproto *protocol, ostream &logout)
|
---|
433 | {
|
---|
434 | FilterResponse_t response;
|
---|
435 | text_tset metadata;
|
---|
436 | bool status_ok = get_info(OID, collection, "", metadata, false, protocol, response, logout);
|
---|
437 | bool not_too_early = false, not_too_recent = false;
|
---|
438 |
|
---|
439 | if(status_ok) {
|
---|
440 | ResultDocInfo_t doc_info = response.docInfo[0];
|
---|
441 | text_t lastModDate;
|
---|
442 | this->getLastModifiedDate(doc_info, lastModDate);
|
---|
443 |
|
---|
444 | // All date fields should be in the form YYYY-MM-DD, which allows for a direct comparison
|
---|
445 | if(from != ""){
|
---|
446 | if(from <= lastModDate)
|
---|
447 | not_too_early = true;
|
---|
448 | }
|
---|
449 | else
|
---|
450 | not_too_early = true; // If there's no FROM field, then the record can't be too early
|
---|
451 |
|
---|
452 | if(until != ""){
|
---|
453 | if(lastModDate <= until)
|
---|
454 | not_too_recent = true;
|
---|
455 | }
|
---|
456 | else
|
---|
457 | not_too_recent = true; // If there's no UNTIL field, then the record can't be too recent
|
---|
458 |
|
---|
459 | if(not_too_early && not_too_recent)
|
---|
460 | return true;
|
---|
461 | else
|
---|
462 | return false;
|
---|
463 | }
|
---|
464 | else
|
---|
465 | return false;
|
---|
466 | }
|
---|