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}