001/* 002 * #%L 003 * Netarchivesuite - harvester 004 * %% 005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library, 006 * the National Library of France and the Austrian National Library. 007 * %% 008 * This program is free software: you can redistribute it and/or modify 009 * it under the terms of the GNU Lesser General Public License as 010 * published by the Free Software Foundation, either version 2.1 of the 011 * License, or (at your option) any later version. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Lesser Public License for more details. 017 * 018 * You should have received a copy of the GNU General Lesser Public 019 * License along with this program. If not, see 020 * <http://www.gnu.org/licenses/lgpl-2.1.html>. 021 * #L% 022 */ 023package dk.netarkivet.harvester.harvesting; 024 025import java.io.File; 026import java.io.IOException; 027 028import org.slf4j.Logger; 029import org.slf4j.LoggerFactory; 030 031import dk.netarkivet.common.exceptions.ArgumentNotValid; 032import dk.netarkivet.common.exceptions.IOFailure; 033import dk.netarkivet.common.utils.FileUtils; 034import dk.netarkivet.common.utils.Settings; 035import dk.netarkivet.common.utils.SimpleXml; 036import dk.netarkivet.common.utils.archive.ArchiveDateConverter; 037import dk.netarkivet.harvester.HarvesterSettings; 038import dk.netarkivet.harvester.datamodel.HarvestDefinitionInfo; 039import dk.netarkivet.harvester.datamodel.Job; 040import dk.netarkivet.harvester.harvesting.PersistentJobData.XmlState.OKSTATE; 041 042/** 043 * Class PersistentJobData holds information about an ongoing harvest. Presently the information is stored in a 044 * XML-file. 045 */ 046public class PersistentJobData implements JobInfo { 047 048 /** The logger to use. */ 049 private static final Logger log = LoggerFactory.getLogger(PersistentJobData.class); 050 051 /** the crawlDir. */ 052 private final File crawlDir; 053 054 /** 055 * The filename for the file containing the persistent job data, stored in crawlDir. 056 */ 057 private static final String HARVEST_INFO_FILENAME = "harvestInfo.xml"; 058 /** XML-root element for the persistent Job Data. */ 059 private static final String ROOT_ELEMENT = "harvestInfo"; 060 /** Key in harvestinfo file for the ID of the job. */ 061 private static final String JOBID_KEY = ROOT_ELEMENT + ".jobId"; 062 /** Key in harvestinfo file for the harvestNum of the job. */ 063 private static final String HARVESTNUM_KEY = ROOT_ELEMENT + ".harvestNum"; 064 /** Key in harvestinfo file for the maxBytesPerDomain value for the job. */ 065 private static final String MAXBYTESPERDOMAIN_KEY = ROOT_ELEMENT + ".maxBytesPerDomain"; 066 /** 067 * Key in harvestinfo file for the maxObjectsPerDomain value for the job. 068 */ 069 private static final String MAXOBJECTSPERDOMAIN_KEY = ROOT_ELEMENT + ".maxObjectsPerDomain"; 070 /** Key in harvestinfo file for the orderXMLName of the job. */ 071 private static final String ORDERXMLNAME_KEY = ROOT_ELEMENT + ".orderXMLName"; 072 /** Key in harvestinfo file for the harvestID of the job. */ 073 private static final String ORIGHARVESTDEFINITIONID_KEY = ROOT_ELEMENT + ".origHarvestDefinitionID"; 074 /** Key in harvestinfo file for the harvest channel of the job. */ 075 private static final String CHANNEL_KEY = ROOT_ELEMENT + ".channel"; 076 077 private static final String PRIORITY_KEY = ROOT_ELEMENT + ".priority"; 078 079 /** Key in harvestinfo file for the original harvest definition name. */ 080 private static final String HARVEST_NAME_KEY = ROOT_ELEMENT + ".origHarvestDefinitionName"; 081 082 /** 083 * Key in harvestinfo file for the original harvest definition description. 084 */ 085 private static final String HARVEST_DESC_KEY = ROOT_ELEMENT + ".origHarvestDefinitionComments"; 086 087 /** 088 * Key in harvestinfo file for the original harvest definition schedule, will be empty for broad crawls. 089 */ 090 private static final String HARVEST_SCHED_KEY = ROOT_ELEMENT + ".scheduleName"; 091 /** The harvestfilename prefix used by this job set in the Job class. */ 092 private static final String HARVEST_FILENAME_PREFIX_KEY = ROOT_ELEMENT + ".harvestFilenamePrefix"; 093 /** The submitted date of this job. */ 094 private static final String JOB_SUBMIT_DATE_KEY = ROOT_ELEMENT + ".jobSubmitDate"; 095 /** The performer of this harvest. */ 096 private static final String HARVEST_PERFORMER_KEY = ROOT_ELEMENT + ".performer"; 097 /** The audience of this harvest. */ 098 private static final String HARVEST_AUDIENCE_KEY = ROOT_ELEMENT + ".audience"; 099 100 /** Key in harvestinfo file for the file version. */ 101 private static final String HARVESTINFO_VERSION_KEY = "harvestInfo.version"; 102 /** Value for current version number. */ 103 private static final String HARVESTINFO_VERSION_NUMBER = "0.5"; 104 105 /** 106 * Also support for version 0.4 of harvestInfo xml. In the previous format the channel and snapshot keys were 107 * absent. Instead there was the priority key. 108 */ 109 private static final String OLD_HARVESTINFO_VERSION_NUMBER = "0.4"; 110 111 /** String array containing all mandatory keys contained in valid version 0.5 xml. */ 112 private static final String[] ALL_KEYS = {JOBID_KEY, HARVESTNUM_KEY, MAXBYTESPERDOMAIN_KEY, 113 MAXOBJECTSPERDOMAIN_KEY, ORDERXMLNAME_KEY, ORIGHARVESTDEFINITIONID_KEY, CHANNEL_KEY, 114 HARVESTINFO_VERSION_KEY, HARVEST_NAME_KEY, HARVEST_FILENAME_PREFIX_KEY, JOB_SUBMIT_DATE_KEY}; 115 116 /** 117 * Optional keys are HARVEST_DESC_KEY representing harvest comments, and HARVEST_SCHED_KEY representing the 118 * scheduleName behind the harvest, only applicable for selective harvests. 119 */ 120 121 /** 122 * String array containing all mandatory keys contained in old valid version 0.4 xml. 123 */ 124 private static final String[] ALL_KEYS_OLD = {JOBID_KEY, HARVESTNUM_KEY, MAXBYTESPERDOMAIN_KEY, 125 MAXOBJECTSPERDOMAIN_KEY, ORDERXMLNAME_KEY, ORIGHARVESTDEFINITIONID_KEY, PRIORITY_KEY, 126 HARVESTINFO_VERSION_KEY, HARVEST_NAME_KEY, HARVEST_FILENAME_PREFIX_KEY, JOB_SUBMIT_DATE_KEY}; 127 128 /** the SimpleXml object, that contains the XML in HARVEST_INFO_FILENAME. */ 129 private SimpleXml theXML = null; 130 131 /** 132 * Constructor for class PersistentJobData. 133 * 134 * @param crawlDir The directory where the harvestInfo can be found 135 * @throws ArgumentNotValid if crawlDir is null or does not exist. 136 */ 137 public PersistentJobData(File crawlDir) { 138 ArgumentNotValid.checkExistsDirectory(crawlDir, "crawlDir"); 139 140 this.crawlDir = crawlDir; 141 } 142 143 /** 144 * Returns true, if harvestInfo exists in crawDir, otherwise false. 145 * 146 * @return true, if harvestInfo exists, otherwise false 147 */ 148 public boolean exists() { 149 return getHarvestInfoFile().isFile(); 150 } 151 152 /** 153 * Returns true if the given directory exists and contains a harvestInfo file. 154 * 155 * @param crawlDir A directory that may contain harvestInfo file. 156 * @return True if the harvestInfo file exists. 157 */ 158 public static boolean existsIn(File crawlDir) { 159 return new File(crawlDir, HARVEST_INFO_FILENAME).exists(); 160 } 161 162 /** 163 * Read harvestInfo into SimpleXML object. 164 * 165 * @return SimpleXml object for harvestInfo 166 * @throws IOFailure if HarvestInfoFile does not exist or if HarvestInfoFile is invalid 167 */ 168 private synchronized SimpleXml read() { 169 if (theXML != null) { 170 return theXML; 171 } 172 if (!exists()) { 173 throw new IOFailure("The harvestInfo file '" + getHarvestInfoFile().getAbsolutePath() + "' does not exist!"); 174 } 175 SimpleXml sx = new SimpleXml(getHarvestInfoFile()); 176 XmlState validationResult = validateHarvestInfo(sx); 177 if (validationResult.getOkState().equals(XmlState.OKSTATE.NOTOK)) { 178 try { 179 String errorMsg = "The harvestInfoFile '" + getHarvestInfoFile().getAbsolutePath() + "' is invalid: " 180 + validationResult.getError() + ". The contents of the file is this: " 181 + FileUtils.readFile(getHarvestInfoFile()); 182 throw new IOFailure(errorMsg); 183 } catch (IOException e) { 184 String errorMsg = "Unable to read HarvestInfoFile: '" + getHarvestInfoFile().getAbsolutePath() + "'"; 185 throw new IOFailure(errorMsg); 186 } 187 } else { // The xml is valid 188 theXML = sx; 189 return sx; 190 } 191 } 192 193 /** 194 * Write information about given Job to XML-structure. 195 * 196 * @param harvestJob the given Job 197 * @param hdi Information about the harvestJob. 198 * @throws IOFailure if any failure occurs while persisting data, or if the file has already been written. 199 */ 200 public synchronized void write(Job harvestJob, HarvestDefinitionInfo hdi) { 201 ArgumentNotValid.checkNotNull(harvestJob, "Job harvestJob"); 202 ArgumentNotValid.checkNotNull(hdi, "HarvestDefinitionInfo hdi"); 203 if (exists()) { 204 String errorMsg = "Persistent Job data already exists in '" + crawlDir + "'. Aborting"; 205 log.warn(errorMsg); 206 throw new IOFailure(errorMsg); 207 } 208 209 SimpleXml sx = new SimpleXml(ROOT_ELEMENT); 210 sx.add(HARVESTINFO_VERSION_KEY, HARVESTINFO_VERSION_NUMBER); 211 sx.add(JOBID_KEY, harvestJob.getJobID().toString()); 212 sx.add(CHANNEL_KEY, harvestJob.getChannel()); 213 sx.add(HARVESTNUM_KEY, Integer.toString(harvestJob.getHarvestNum())); 214 sx.add(ORIGHARVESTDEFINITIONID_KEY, Long.toString(harvestJob.getOrigHarvestDefinitionID())); 215 sx.add(MAXBYTESPERDOMAIN_KEY, Long.toString(harvestJob.getMaxBytesPerDomain())); 216 sx.add(MAXOBJECTSPERDOMAIN_KEY, Long.toString(harvestJob.getMaxObjectsPerDomain())); 217 sx.add(ORDERXMLNAME_KEY, harvestJob.getOrderXMLName()); 218 219 sx.add(HARVEST_NAME_KEY, hdi.getOrigHarvestName()); 220 221 String comments = hdi.getOrigHarvestDesc(); 222 if (!comments.isEmpty()) { 223 sx.add(HARVEST_DESC_KEY, comments); 224 } 225 226 String schedName = hdi.getScheduleName(); 227 if (!schedName.isEmpty()) { 228 sx.add(HARVEST_SCHED_KEY, schedName); 229 } 230 // Store the harvestname prefix selected by the used Naming Strategy. 231 sx.add(HARVEST_FILENAME_PREFIX_KEY, harvestJob.getHarvestFilenamePrefix()); 232 233 // store the submitted date in WARC Date format 234 sx.add(JOB_SUBMIT_DATE_KEY, ArchiveDateConverter.getWarcDateFormat().format(harvestJob.getSubmittedDate())); 235 // if performer set to something different from the empty String 236 if (!Settings.get(HarvesterSettings.PERFORMER).isEmpty()) { 237 sx.add(HARVEST_PERFORMER_KEY, Settings.get(HarvesterSettings.PERFORMER)); 238 } 239 if (harvestJob.getHarvestAudience() != null && !harvestJob.getHarvestAudience().isEmpty()) { 240 sx.add(HARVEST_AUDIENCE_KEY, harvestJob.getHarvestAudience()); 241 } 242 243 XmlState validationResult = validateHarvestInfo(sx); 244 if (validationResult.getOkState().equals(XmlState.OKSTATE.NOTOK)) { 245 String msg = "Could not create a valid harvestinfo file for job " + harvestJob.getJobID() + ": " 246 + validationResult.getError(); 247 throw new IOFailure(msg); 248 } else { 249 sx.save(getHarvestInfoFile()); 250 } 251 } 252 253 /** 254 * Checks that the xml data in the persistent job data file is valid. 255 * 256 * @param sx the SimpleXml object containing the persistent job data 257 * @return empty string, if valid persistent job data, otherwise a string containing the problem. 258 */ 259 private static XmlState validateHarvestInfo(SimpleXml sx) { 260 final String version; 261 if (sx.hasKey(HARVESTINFO_VERSION_KEY)) { 262 version = sx.getString(HARVESTINFO_VERSION_KEY); 263 } else { 264 final String errMsg = "Missing version information"; 265 return new XmlState(OKSTATE.NOTOK, errMsg); 266 } 267 268 final String[] keysToCheck; 269 if (version.equals(HARVESTINFO_VERSION_NUMBER)) { 270 keysToCheck = ALL_KEYS; 271 } else if (version.equals(OLD_HARVESTINFO_VERSION_NUMBER)) { 272 keysToCheck = ALL_KEYS_OLD; 273 } else { 274 final String errMsg = "Invalid version: " + version; 275 return new XmlState(OKSTATE.NOTOK, errMsg); 276 } 277 278 /* Check, if all necessary components exist in the SimpleXml */ 279 280 for (String key : keysToCheck) { 281 if (!sx.hasKey(key)) { 282 final String errMsg = "Could not find key " + key + " in harvestInfoFile, version " + version; 283 return new XmlState(OKSTATE.NOTOK, errMsg); 284 } 285 } 286 287 /* Check, if the jobId element contains a long value */ 288 final String jobidAsString = sx.getString(JOBID_KEY); 289 try { 290 Long.valueOf(jobidAsString); 291 } catch (Throwable t) { 292 final String errMsg = "The id '" + jobidAsString + "' in harvestInfoFile must be a long value"; 293 return new XmlState(OKSTATE.NOTOK, errMsg); 294 } 295 296 // Verify, that the job channel and snapshot elements are not the empty String (version 0.5+) 297 if (version.equals(HARVESTINFO_VERSION_NUMBER) && sx.getString(CHANNEL_KEY).isEmpty()) { 298 final String errMsg = "The channel and/or the snapshot value of the job is undefined"; 299 return new XmlState(OKSTATE.NOTOK, errMsg); 300 } 301 302 if (version.equals(OLD_HARVESTINFO_VERSION_NUMBER) && sx.getString(PRIORITY_KEY).isEmpty()) { 303 final String errMsg = "The priority value of the job is undefined"; 304 return new XmlState(OKSTATE.NOTOK, errMsg); 305 } 306 307 // Verify, that the job channel element is not the empty String 308 if (version.equals(HARVESTINFO_VERSION_NUMBER) && sx.getString(CHANNEL_KEY).isEmpty()) { 309 final String errMsg = "The channel and/or the snapshot value of the job is undefined"; 310 return new XmlState(OKSTATE.NOTOK, errMsg); 311 } 312 313 // Verify, that the ORDERXMLNAME element is not the empty String 314 if (sx.getString(ORDERXMLNAME_KEY).isEmpty()) { 315 final String errMsg = "The orderxmlname of the job is undefined"; 316 return new XmlState(OKSTATE.NOTOK, errMsg); 317 } 318 319 // Verify that the HARVESTNUM element is an integer 320 final String harvestNumAsString = sx.getString(HARVESTNUM_KEY); 321 try { 322 Integer.valueOf(harvestNumAsString); 323 } catch (Throwable t) { 324 final String errMsg = "The HARVESTNUM in harvestInfoFile must be a Integer " 325 + "value. The value given is '" + harvestNumAsString + "'."; 326 return new XmlState(OKSTATE.NOTOK, errMsg); 327 } 328 329 /* 330 * Check, if the OrigHarvestDefinitionID element contains a long value. 331 */ 332 final String origHarvestDefinitionIDAsString = sx.getString(ORIGHARVESTDEFINITIONID_KEY); 333 try { 334 Long.valueOf(origHarvestDefinitionIDAsString); 335 } catch (Throwable t) { 336 final String errMsg = "The OrigHarvestDefinitionID in harvestInfoFile must be a long value. " 337 + "The value given is: '" + origHarvestDefinitionIDAsString + "'."; 338 return new XmlState(OKSTATE.NOTOK, errMsg); 339 } 340 341 /* Check, if the MaxBytesPerDomain element contains a long value */ 342 final String maxBytesPerDomainAsString = sx.getString(MAXBYTESPERDOMAIN_KEY); 343 try { 344 Long.valueOf(maxBytesPerDomainAsString); 345 } catch (Throwable t) { 346 final String errMsg = "The MaxBytesPerDomain element in harvestInfoFile must be a long value. " 347 + "The value given is: '" + maxBytesPerDomainAsString + "'."; 348 return new XmlState(OKSTATE.NOTOK, errMsg); 349 } 350 351 /* Check, if the MaxObjectsPerDomain element contains a long value */ 352 final String maxObjectsPerDomainAsString = sx.getString(MAXOBJECTSPERDOMAIN_KEY); 353 try { 354 Long.valueOf(maxObjectsPerDomainAsString); 355 } catch (Throwable t) { 356 final String errMsg = "The MaxObjectsPerDomain element in harvestInfoFile must be a long value. " 357 + "The value given is: '" + maxObjectsPerDomainAsString + "'."; 358 return new XmlState(OKSTATE.NOTOK, errMsg); 359 } 360 361 return new XmlState(OKSTATE.OK, ""); 362 } 363 364 /** 365 * @return the harvestInfoFile. 366 */ 367 private File getHarvestInfoFile() { 368 return new File(crawlDir, HARVEST_INFO_FILENAME); 369 } 370 371 /** 372 * Return the harvestInfo jobID. 373 * 374 * @return the harvestInfo JobID 375 * @throws IOFailure if no harvestInfo exists or it is invalid. 376 */ 377 public Long getJobID() { 378 SimpleXml sx = read(); // reads and validates XML 379 String jobIDString = sx.getString(JOBID_KEY); 380 return Long.parseLong(jobIDString); 381 } 382 383 /** 384 * Return the job's harvest channel name. 385 * 386 * @return the job's harvest channel name 387 * @throws IOFailure if no harvestInfo exists or it is invalid. 388 */ 389 public String getChannel() { 390 SimpleXml sx = read(); // reads and validates XML 391 return sx.getString(CHANNEL_KEY); 392 } 393 394 /** 395 * Return the job harvestNum. 396 * 397 * @return the job harvestNum 398 * @throws IOFailure if no harvestInfo exists or it is invalid. 399 */ 400 public int getJobHarvestNum() { 401 SimpleXml sx = read(); // reads and validates XML 402 String harvestNumString = sx.getString(HARVESTNUM_KEY); 403 return Integer.parseInt(harvestNumString); 404 } 405 406 /** 407 * Return the job origHarvestDefinitionID. 408 * 409 * @return the job origHarvestDefinitionID 410 * @throws IOFailure if no harvestInfo exists or it is invalid. 411 */ 412 public Long getOrigHarvestDefinitionID() { 413 SimpleXml sx = read(); // reads and validates XML 414 String origHarvestDefinitionIDString = sx.getString(ORIGHARVESTDEFINITIONID_KEY); 415 return Long.parseLong(origHarvestDefinitionIDString); 416 } 417 418 /** 419 * Return the job maxBytesPerDomain value. 420 * 421 * @return the job maxBytesPerDomain value. 422 * @throws IOFailure if no harvestInfo exists or it is invalid. 423 */ 424 public long getMaxBytesPerDomain() { 425 SimpleXml sx = read(); // reads and validates XML 426 String maxBytesPerDomainString = sx.getString(MAXBYTESPERDOMAIN_KEY); 427 return Long.parseLong(maxBytesPerDomainString); 428 } 429 430 /** 431 * Return the job maxObjectsPerDomain value. 432 * 433 * @return the job maxObjectsPerDomain value. 434 * @throws IOFailure if no harvestInfo exists or it is invalid. 435 */ 436 public long getMaxObjectsPerDomain() { 437 SimpleXml sx = read(); // reads and validates XML 438 String maxObjectsPerDomainString = sx.getString(MAXOBJECTSPERDOMAIN_KEY); 439 return Long.parseLong(maxObjectsPerDomainString); 440 } 441 442 /** 443 * Return the job orderXMLName. 444 * 445 * @return the job orderXMLName. 446 * @throws IOFailure if no harvestInfo exists or it is invalid. 447 */ 448 public String getOrderXMLName() { 449 SimpleXml sx = read(); // reads and validates XML 450 return sx.getString(ORDERXMLNAME_KEY); 451 } 452 453 /** 454 * Return the version of the xml. 455 * 456 * @return the version of the xml 457 * @throws IOFailure if no harvestInfo exists or it is invalid. 458 */ 459 public String getVersion() { 460 SimpleXml sx = read(); // reads and validates XML 461 return sx.getString(HARVESTINFO_VERSION_KEY); 462 } 463 464 /** 465 * Helper class for returning the OK-state back to the caller. 466 */ 467 protected static class XmlState { 468 /** enum for holding OK/NOTOK values. */ 469 public enum OKSTATE { 470 OK, NOTOK 471 } 472 473 /** the state of the XML. */ 474 private OKSTATE ok; 475 /** The error coming from an xml-validation. */ 476 private String error;; 477 478 /** 479 * Constructor of an XmlState object. 480 * 481 * @param ok Is the XML OK or not OKAY? 482 * @param error The error found during validation, if any. 483 */ 484 public XmlState(OKSTATE ok, String error) { 485 this.ok = ok; 486 this.error = error; 487 } 488 489 /** 490 * @return the OK value of this object. 491 */ 492 public OKSTATE getOkState() { 493 return ok; 494 } 495 496 /** 497 * @return the error value of this object (maybe null). 498 */ 499 public String getError() { 500 return error; 501 } 502 } 503 504 /** 505 * If not set in persistentJobData, fall back to the standard way. 506 * jobid-harvestid. 507 */ 508 public String getHarvestFilenamePrefix() { 509 SimpleXml sx = read(); // reads and validates XML 510 String prefix = null; 511 if (!sx.hasKey(HARVEST_FILENAME_PREFIX_KEY)) { 512 prefix = this.getJobID() + "-" + this.getOrigHarvestDefinitionID(); 513 log.warn("harvestFilenamePrefix not part of persistentJobData. Using old standard naming: {}", prefix); 514 } else { 515 prefix = sx.getString(HARVEST_FILENAME_PREFIX_KEY); 516 } 517 return prefix; 518 } 519 520 /** 521 * Return the harvestname in this xml. 522 * 523 * @return the harvestname in this xml. 524 * @throws IOFailure if no harvestInfo exists or it is invalid. 525 */ 526 public String getharvestName() { 527 SimpleXml sx = read(); // reads and validates XML 528 return sx.getString(HARVEST_NAME_KEY); 529 } 530 531 /** 532 * Return the schedulename in this xml. 533 * 534 * @return the schedulename in this xml (or null, if undefined for this job) 535 * @throws IOFailure if no harvestInfo exists or it is invalid. 536 */ 537 public String getScheduleName() { 538 SimpleXml sx = read(); // reads and validates XML 539 if (sx.hasKey(HARVEST_SCHED_KEY)) { 540 return sx.getString(HARVEST_SCHED_KEY); 541 } else { 542 return null; 543 } 544 } 545 546 /** 547 * Return the submit date of the job in this xml. 548 * 549 * @return the submit date of the job in this xml. 550 * @throws IOFailure if no harvestInfo exists or it is invalid. 551 */ 552 public String getJobSubmitDate() { 553 SimpleXml sx = read(); // reads and validates XML 554 return sx.getString(JOB_SUBMIT_DATE_KEY); 555 } 556 557 /** 558 * Return the performer information in this xml. 559 * 560 * @return the performer information in this xml or null if value undefined 561 * @throws IOFailure if no harvestInfo exists or it is invalid. 562 */ 563 public String getPerformer() { 564 SimpleXml sx = read(); // reads and validates XML 565 if (sx.hasKey(HARVEST_PERFORMER_KEY)) { 566 return sx.getString(HARVEST_PERFORMER_KEY); 567 } else { 568 return null; 569 } 570 } 571 572 /** 573 * Return the audience information in this xml. 574 * 575 * @return the audience information in this xml or null if value undefined 576 * @throws IOFailure if no harvestInfo exists or it is invalid. 577 */ 578 public String getAudience() { 579 SimpleXml sx = read(); // reads and validates XML 580 if (sx.hasKey(HARVEST_AUDIENCE_KEY)) { 581 return sx.getString(HARVEST_AUDIENCE_KEY); 582 } else { 583 return null; 584 } 585 } 586 587}