001/*
002 * #%L
003 * Netarchivesuite - archive
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 */
023
024package dk.netarkivet.archive.webinterface;
025
026import java.io.File;
027import java.io.IOException;
028import java.lang.reflect.Constructor;
029import java.lang.reflect.Type;
030import java.util.ArrayList;
031import java.util.Collections;
032import java.util.Date;
033import java.util.HashSet;
034import java.util.List;
035import java.util.Locale;
036import java.util.Set;
037import java.util.regex.Pattern;
038
039import javax.annotation.Resource;
040import javax.annotation.Resources;
041import javax.servlet.ServletRequest;
042import javax.servlet.jsp.JspWriter;
043import javax.servlet.jsp.PageContext;
044
045import org.slf4j.Logger;
046import org.slf4j.LoggerFactory;
047
048import dk.netarkivet.common.CommonSettings;
049import dk.netarkivet.common.distribute.arcrepository.Replica;
050import dk.netarkivet.common.distribute.arcrepository.ReplicaType;
051import dk.netarkivet.common.exceptions.ArgumentNotValid;
052import dk.netarkivet.common.exceptions.ForwardedToErrorPage;
053import dk.netarkivet.common.exceptions.IOFailure;
054import dk.netarkivet.common.exceptions.IllegalState;
055import dk.netarkivet.common.exceptions.NetarkivetException;
056import dk.netarkivet.common.exceptions.UnknownID;
057import dk.netarkivet.common.utils.FileUtils;
058import dk.netarkivet.common.utils.I18n;
059import dk.netarkivet.common.utils.Settings;
060import dk.netarkivet.common.utils.batch.ByteJarLoader;
061import dk.netarkivet.common.utils.batch.FileBatchJob;
062import dk.netarkivet.common.utils.batch.LoadableJarBatchJob;
063import dk.netarkivet.common.webinterface.HTMLUtils;
064
065/**
066 * Utility class for creating the web page content for the batchjob pages.
067 */
068public final class BatchGUI {
069    /** The log. */
070    //private static Log log = LogFactory.getLog(BatchGUI.class);
071    protected static final Logger log = LoggerFactory.getLogger(BatchGUI.class);
072
073    /** The language translator. */
074    private static final I18n I18N = new I18n(dk.netarkivet.archive.Constants.TRANSLATIONS_BUNDLE);
075
076    /**
077     * Private Constructor to prevent instantiation of this utility class.
078     */
079    private BatchGUI() {
080    }
081
082    /**
083     * Method for creating the batchjob overview page. Creates both the heading and the table for the batchjobs defined
084     * in settings.
085     *
086     * @param context The context of the page. Contains the locale for the language package.
087     * @throws ArgumentNotValid If the PageContext is null.
088     * @throws IOException If it is not possible to write to the JspWriter.
089     */
090    public static void getBatchOverviewPage(PageContext context) throws ArgumentNotValid, IOException {
091        ArgumentNotValid.checkNotNull(context, "PageContext context");
092        JspWriter out = context.getOut();
093
094        // retrieve the jobs etc.
095        String[] jobs = Settings.getAll(CommonSettings.BATCHJOBS_CLASS);
096        Locale locale = context.getResponse().getLocale();
097
098        if (jobs.length == 0) {
099            out.print("<h3>" + I18N.getString(locale, "batchpage;No.batchjobs.defined.in.settings", new Object[] {})
100                    + "</h3>");
101            return;
102        }
103
104        // add header for batchjob selection table
105        out.print("<table class=\"selection_table\" cols=\"4\">\n");
106        out.print("  <tr>\n");
107        out.print("    <th>" + I18N.getString(locale, "batchpage;Batchjob", new Object[] {}) + "</th>\n");
108        out.print("    <th>" + I18N.getString(locale, "batchpage;Last.run", new Object[] {}) + "</th>\n");
109        out.print("    <th>" + I18N.getString(locale, "batchpage;Output.file", new Object[] {}) + "</th>\n");
110        out.print("    <th>" + I18N.getString(locale, "batchpage;Error.file", new Object[] {}) + "</th>\n");
111        out.print("  </tr>\n");
112
113        for (int i = 0; i < jobs.length; i++) {
114            out.print("  <tr Class=\"" + HTMLUtils.getRowClass(i) + "\">\n");
115            out.print(getOverviewTableEntry(jobs[i], locale));
116            out.print("  </tr>\n");
117        }
118
119        out.print("</table>\n");
120    }
121
122    /**
123     * Method for creating the page for a batchjob. It contains the following informations:
124     * <p>
125     * <br/>
126     * - Creates a line with the name of the batchjob.<br/>
127     * - Write the description if the batchjob has a metadata resource annotation description of the batchjob class.<br/>
128     * - The last run information, date and size of the error and output files. <br/>
129     * - The arguments of the batchjob, with information if they have been defined in the resource annotations of the
130     * class.<br/>
131     * - Radio buttons for choosing the replica.<br/>
132     * - Input box for regular expression for filenames to match.<br/>
133     * - Execution button.<br/>
134     *
135     * @param context The context of the page. Must contains a class name of the batchjob.
136     * @throws UnknownID If the class cannot be found.
137     * @throws ArgumentNotValid If the context is null.
138     * @throws IllegalState If the class is not an instance of FileBatchJob.
139     * @throws ForwardedToErrorPage If the context does not contain the required information.
140     * @throws IOFailure If there is problems with the JspWriter.
141     */
142    @SuppressWarnings("rawtypes")
143    public static void getPageForClass(PageContext context) throws UnknownID, ArgumentNotValid, IllegalState,
144            ForwardedToErrorPage, IOFailure {
145        ArgumentNotValid.checkNotNull(context, "PageContext context");
146
147        HTMLUtils.forwardOnEmptyParameter(context, Constants.BATCHJOB_PARAMETER);
148
149        try {
150            // Retrieve variables
151            Locale locale = context.getResponse().getLocale();
152            ServletRequest request = context.getRequest();
153            String className = request.getParameter(Constants.BATCHJOB_PARAMETER);
154            JspWriter out = context.getOut();
155
156            // retrieve the batch class and the constructor.
157            Class c = getBatchClass(className);
158
159            out.print(I18N.getString(locale, "batchpage;Name.of.batchjob", new Object[] {}) + ": <b>" + c.getName()
160                    + "</b><br/>\n");
161            out.print(getClassDescription(c, locale));
162            out.print(getPreviousRuns(c.getName(), locale));
163
164            // begin form
165            out.println("<form method=\"post\" action=\"" + Constants.URL_BATCHJOB_EXECUTE + "?"
166                    + Constants.BATCHJOB_PARAMETER + "=" + className + "\">");
167
168            out.print(getHTMLarguments(c, locale));
169            out.print(getReplicaRadioButtons(locale));
170            out.print(getRegularExpressionInputBox(locale));
171            out.print(getSubmitButton(locale));
172
173            // end form
174            out.print("</form>");
175        } catch (IOException e) {
176            String errMsg = "Could not create page with batchjobs.";
177            log.warn(errMsg, e);
178            throw new IOFailure(errMsg, e);
179        }
180    }
181
182    /**
183     * Method for executing a batchjob.
184     *
185     * @param context The page context containing the needed information for executing the batchjob.
186     */
187    @SuppressWarnings("rawtypes")
188    public static void execute(PageContext context) {
189        try {
190            ServletRequest request = context.getRequest();
191
192            // get parameters
193            String filetype = request.getParameter(Constants.FILETYPE_PARAMETER);
194            String jobId = request.getParameter(Constants.JOB_ID_PARAMETER);
195            String jobName = request.getParameter(Constants.BATCHJOB_PARAMETER);
196            String repName = request.getParameter(Constants.REPLICA_PARAMETER);
197
198            FileBatchJob batchjob;
199
200            // Retrieve the list of arguments.
201            List<String> args = new ArrayList<String>();
202            String arg;
203            Integer i = 1;
204            // retrieve the constructor to find out how many arguments
205            Class c = getBatchClass(jobName);
206            Constructor con = findStringConstructor(c);
207
208            // retrieve the arguments and put them into the list.
209            while (i <= con.getParameterTypes().length) {
210                arg = request.getParameter("arg" + i.toString());
211                if (arg != null) {
212                    args.add(arg);
213                } else {
214                    log.warn("Should contain argument number " + i + ", but "
215                            + "found a null instead, indicating missing " + "argument. Use empty string instead.");
216                    args.add("");
217                }
218                i++;
219            }
220
221            File jarfile = getJarFile(jobName);
222            if (jarfile == null) {
223                // get the constructor and instantiate it.
224                Constructor construct = findStringConstructor(getBatchClass(jobName));
225                batchjob = (FileBatchJob) construct.newInstance(args.toArray());
226            } else {
227                batchjob = new LoadableJarBatchJob(jobName, args, jarfile);
228            }
229
230            // get the regular expression.
231            String regex = jobId + "-";
232            if (filetype.equals(BatchFileType.Metadata.toString())) {
233                regex += Constants.REGEX_METADATA;
234            } else if (filetype.equals(BatchFileType.Content.toString())) {
235                // TODO fix this 'content' regex. (NAS-1394)
236                regex += Constants.REGEX_CONTENT;
237            } else {
238                regex += Constants.REGEX_ALL;
239            }
240
241            // validate the regular expression (throws exception if wrong).
242            Pattern.compile(regex);
243
244            Replica rep = Replica.getReplicaFromName(repName);
245
246            new BatchExecuter(batchjob, regex, rep).start();
247
248            JspWriter out = context.getOut();
249            out.write("Executing batchjob with the following parameters. " + "<br/>\n");
250            out.write("BatchJob name: " + jobName + "<br/>\n");
251            out.write("Replica: " + rep.getName() + "<br/>\n");
252            out.write("Regular expression: " + regex + "<br/>\n");
253        } catch (Exception e) {
254            throw new IOFailure("Could not instantiate the batchjob.", e);
255        }
256    }
257
258    /**
259     * Extracts and validates the class from the class name.
260     *
261     * @param className The name of the class to extract.
262     * @return The class from the class name.
263     * @throws UnknownID If the className does not refer to a known class.
264     * @throws IllegalState If the class is not an instance of FileBatchJob.
265     */
266    @SuppressWarnings("rawtypes")
267    private static Class getBatchClass(String className) throws UnknownID, IllegalState {
268        Class res;
269        // validate whether a class with the classname can be found
270        try {
271            File arcfile = getJarFile(className);
272
273            // handle whether internal or loadable batchjob.
274            if (arcfile != null) {
275                ByteJarLoader bjl = new ByteJarLoader(arcfile);
276                res = bjl.findClass(className);
277            } else {
278                res = Class.forName(className);
279            }
280        } catch (ClassNotFoundException e) {
281            String errMsg = "Cannot find the class '" + className
282                    + "' in the classpath. Perhaps bad path or missing library file.";
283            log.warn(errMsg);
284            throw new UnknownID(errMsg, e);
285        }
286
287        // Test whether the class is a sub class to FileBatchJob
288        if (!(FileBatchJob.class.isAssignableFrom(res))) {
289            String errMsg = "The class '" + className + "' is not an instance " + "of '" + FileBatchJob.class.getName()
290                    + "' as required.";
291            log.warn(errMsg);
292            throw new IllegalState(errMsg);
293        }
294
295        return res;
296    }
297
298    /**
299     * Finds a constructor which either does not take any arguments, or does only takes string arguments. Any
300     * constructor which does has other arguments than string is ignored.
301     *
302     * @param c The class to retrieve the constructor from.
303     * @return The string argument based constructor of the class.
304     * @throws UnknownID If no valid constructor can be found.
305     */
306    @SuppressWarnings("rawtypes")
307    private static Constructor findStringConstructor(Class c) throws UnknownID {
308        for (Constructor con : c.getConstructors()) {
309            boolean valid = true;
310
311            // validate the parameter classes. Ignore if not string.
312            for (Class cl : con.getParameterTypes()) {
313                if (!cl.equals(java.lang.String.class)) {
314                    valid = false;
315                    break;
316                }
317            }
318            if (valid) {
319                return con;
320            }
321        }
322
323        // throw an exception if no valid constructor can be found.
324        throw new UnknownID("No valid constructor can be found for class '" + c.getName() + "'.");
325    }
326
327    /**
328     * Retrieves the HTML code for the description of the class. The description of the class is given in the resource
329     * annotation which has the type of the given class.
330     * <p>
331     * <br/>
332     * E.g. <br/>
333     *
334     * @param c The class to be described.
335     * @param locale The locale language package.
336     * @return The HTML code describing the class.
337     * @Resource(description="Batchjob for finding URLs which matches a given" +
338     * " regular expression and has a mimetype which matches another" + " regular expression.",
339     * type=dk.netarkivet.common.utils.batch.UrlSearch.class)}
340     * <p>
341     * <br/>
342     * <br/>
343     * Which gives the UrlSearch batchjob the following description: <br/>
344     * <br/>
345     * Description: Batchjob for finding URLs which matches a given regular expression and has a mimetype which matches
346     * another regular expression. &lt;br/&gt;&lt;br/&gt;
347     */
348    @SuppressWarnings({"rawtypes", "unchecked"})
349    private static String getClassDescription(Class c, Locale locale) {
350        // retrieve the resources.
351        Resources r = (Resources) c.getAnnotation(Resources.class);
352        if (r == null) {
353            return "<br/>\n";
354        }
355
356        // Find and return the description of this class (if any).
357        for (Resource resource : r.value()) {
358            if (resource.type().getName().equals(c.getName())) {
359                return I18N.getString(locale, "batchpage;Description", new Object[] {}) + ": " + resource.description()
360                        + "<br/><br/>\n";
361            }
362        }
363
364        // no description found, then return empty string.
365        return "<br/>\n";
366    }
367
368    /**
369     * Creates the HTML code for the arguments of the constructor. It reads the resources for the batchjob, where the
370     * metadata for the constructor is defined in the 'resources' annotation for the class.
371     * <p>
372     * <br/>
373     * E.g. The UrlSearch batchjob. Which has the following resources:<br/>
374     *
375     * @param c The class whose constructor should be used.
376     * @param locale The language package.
377     * @return The HTML code for the arguments for executing the batchjob.
378     * @Resource(name="regex", description="The regular expression for the " + "urls.", type=java.lang.String.class)<br/>
379     * @Resource(name="mimetype", type=java.lang.String.class)<br/>
380     * Though the batchjob takes three arguments (thus one undefined). <br/>
381     * <br/>
382     * <p>
383     * Arguments:&lt;br/&gt;<br/>
384     * regex (The regular expression for the urls.)&lt;br/&gt;<br/>
385     * &lt;input name="arg1" size="50" value=""&gt;&lt;br/&gt;<br/>
386     * mimetype&lt;br/&gt;<br/>
387     * &lt;input name="arg2" size="50" value=""&gt;&lt;br/&gt;<br/>
388     * Argument 3 (missing argument metadata)&lt;br/&gt;<br/>
389     * &lt;input name="arg3" size="50" value=""&gt;&lt;br/&gt;<br/>
390     * <p>
391     * <br/>
392     * Which will look like: <br/>
393     * <br/>
394     * <p>
395     * Arguments:<br/>
396     * regex (The regular expression for the urls.)<br/>
397     * <input name="arg1" size="50" value="" /><br/>
398     * mimetype<br/>
399     * <input name="arg2" size="50" value="" /><br/>
400     * Argument 3 (missing argument metadata)<br/>
401     * <input name="arg3" size="50" value="" /><br/>
402     * <p>
403     * TODO this does not work until batchjobs can be used with arguments.
404     */
405    @SuppressWarnings({"unchecked", "rawtypes"})
406    private static String getHTMLarguments(Class c, Locale locale) {
407        Constructor con = findStringConstructor(c);
408        Type[] params = con.getParameterTypes();
409
410        // If no parameters, then return no content (new line).
411        if (params.length < 1) {
412            return "<br/>\n";
413        }
414
415        // Retrieve the resources (metadata for the arguments).
416        Resources r = (Resources) c.getAnnotation(Resources.class);
417        if (r == null) {
418            return "<br/>\n";
419        }
420        Resource[] resource = r.value();
421
422        StringBuilder res = new StringBuilder();
423
424        res.append(I18N.getString(locale, "batchpage;Arguments", new Object[] {}) + ":<br/>\n");
425
426        if (resource.length < params.length) {
427            // warn about no metadata.
428            res.append(I18N.getString(locale, "batchpage;Bad.argument.metadata.for.the.constructor", con.toString())
429                    + ".<br/>\n");
430            // make default 'arguments'.
431            for (int i = 1; i <= params.length; i++) {
432                res.append(I18N.getString(locale, "batchpage;Argument.i", i) + "<br/>\n");
433                res.append("<input name=\"arg" + i + "\" size=\"" + Constants.HTML_INPUT_SIZE + "\" value=\"\"><br/>\n");
434            }
435        } else {
436            // handle the case, when there is arguments.
437            int parmIndex = 0;
438            // retrieve the arguments from the resources.
439            for (int i = 0; i < resource.length && parmIndex < params.length; i++) {
440                if (resource[i].type() == params[parmIndex]) {
441                    // Use the resource to describe the argument.
442                    parmIndex++;
443                    res.append(resource[i].name());
444                    if (resource[i].description() != null && !resource[i].description().isEmpty()) {
445                        res.append(" (" + resource[i].description() + ")");
446                    }
447                    res.append("<br/>\n");
448                    res.append("<input name=\"arg" + parmIndex + "\" size=\"" + Constants.HTML_INPUT_SIZE
449                            + "\" value=\"\"><br/>\n");
450                }
451            }
452            // If some arguments did not have a resource description, then
453            // use a default 'unknown argument' input box.
454            if (parmIndex < params.length) {
455                for (int i = parmIndex + 1; i <= params.length; i++) {
456                    res.append(I18N.getString(locale, "batchpage;Argument.i.missing.argument.metadata", i) + "<br/>\n");
457                    res.append("<input name=\"arg" + i + "\" size=\"" + Constants.HTML_INPUT_SIZE
458                            + "\" value=\"\"><br/>\n");
459                }
460            }
461        }
462
463        res.append("<br/>\n");
464
465        return res.toString();
466    }
467
468    /**
469     * Creates the HTML code for describing the previous executions of a given batchjob. If any previous results are
470     * found, then a table will be created. Each result (output and/or error file) will have an entry in the table. A
471     * row containing the following: <br/>
472     * - The start date (extractable from the result file name). <br/>
473     * - The end date (last modified date for either result file). <br/>
474     * - The size of the output file.<br/>
475     * - The number of lines in the output file. <br/>
476     * - A link to download the output file.<br/>
477     * - The size of the error file.<br/>
478     * - The number of lines in the error file. <br/>
479     * - A link to download the error file.<br/>
480     *
481     * @param jobPath The name of the batch job.
482     * @param locale The locale language package.
483     * @return The HTML code for describing the previous executions of the batchjob.
484     */
485    private static String getPreviousRuns(String jobPath, Locale locale) {
486        // initialize the resulting string.
487        StringBuilder res = new StringBuilder();
488
489        // extract the final name of the batch job (used for the files).
490        String batchName = getJobName(jobPath);
491
492        // extract the batch directory, where the old batchjobs files lies.
493        File batchDir = getBatchDir();
494
495        // extract the files for the batchjob.
496        String[] filenames = batchDir.list();
497        // use a hash-set to avoid counting both '.err' and '.out' files.
498        Set<String> prefixes = new HashSet<String>();
499
500        for (String filename : filenames) {
501            // match and put into set.
502            if (filename.startsWith(batchName)
503                    && (filename.endsWith(Constants.ERROR_FILE_EXTENSION) || filename
504                            .endsWith(Constants.OUTPUT_FILE_EXTENSION))) {
505                String prefix = filename.split("[.]")[0];
506                // the prefix is not added twice, since it is a hash-set.
507                prefixes.add(prefix);
508            }
509        }
510
511        // No files => No previous runs.
512        if (prefixes.isEmpty()) {
513            res.append(I18N.getString(locale, "batchpage;Batchjob.has.never.been.run", new Object[] {})
514                    + "<br/><br/>\n");
515            return res.toString();
516        }
517
518        // make header of output
519        res.append(I18N.getString(locale, "batchpage;Number.of.runs.0", prefixes.size()) + "<br/>\n");
520        res.append("<table class=\"selection_table\" cols=\"3\">\n");
521        res.append("  <tr>\n");
522        res.append("    <th colspan=\"1\">" + I18N.getString(locale, "batchpage;Started.date", new Object[] {})
523                + "</th>\n");
524        res.append("    <th colspan=\"1\">" + I18N.getString(locale, "batchpage;Ended.date", new Object[] {})
525                + "</th>\n");
526        res.append("    <th colspan=\"3\">" + I18N.getString(locale, "batchpage;Output.file", new Object[] {})
527                + "</th>\n");
528        res.append("    <th colspan=\"3\">" + I18N.getString(locale, "batchpage;Error.file", new Object[] {})
529                + "</th>\n");
530        res.append("  </tr>\n");
531
532        int i = 0;
533        for (String prefix : prefixes) {
534            res.append("  <tr class=" + HTMLUtils.getRowClass(i++) + ">\n");
535
536            File outputFile = new File(batchDir, prefix + ".out");
537            File errorFile = new File(batchDir, prefix + ".err");
538
539            // Retrieve the timestamp from the file-name or "" if not found
540            String timestamp = getTimestamp(prefix, locale);
541
542            // insert start-time
543            res.append("    <td>" + timestamp + "</td>\n");
544
545            // retrieve the last-modified date for the files
546            Long lastModified = 0L;
547            if (outputFile.exists() && outputFile.lastModified() > lastModified) {
548                lastModified = outputFile.lastModified();
549            }
550            if (errorFile.exists() && errorFile.lastModified() > lastModified) {
551                lastModified = errorFile.lastModified();
552            }
553
554            // insert ended-time
555            res.append("    <td>" + new Date(lastModified).toString() + "</td>\n");
556
557            // insert information about the output file.
558            if (!outputFile.exists()) {
559                res.append("    <td>" + I18N.getString(locale, "batchpage;No.outputfile", new Object[] {}) + "</td>\n");
560                res.append("    <td>" + I18N.getString(locale, "batchpage;No.outputfile", new Object[] {}) + "</td>\n");
561                res.append("    <td>" + I18N.getString(locale, "batchpage;No.outputfile", new Object[] {}) + "</td>\n");
562            } else {
563                res.append("    <td>" + outputFile.length() + " "
564                        + I18N.getString(locale, "batchpage;bytes", new Object[] {}) + "</td>\n");
565                res.append("    <td>" + FileUtils.countLines(outputFile) + " "
566                        + I18N.getString(locale, "batchpage;lines", new Object[] {}) + "</td>\n");
567                res.append("    <td><a href=" + Constants.URL_RETRIEVE_RESULT_FILES + "?filename="
568                        + outputFile.getName() + ">"
569                        + I18N.getString(locale, "batchpage;Download.outputfile", new Object[] {}) + "</a></td>\n");
570            }
571
572            // insert information about error file
573            if (!errorFile.exists()) {
574                res.append("    <td>" + I18N.getString(locale, "batchpage;No.errorfile", new Object[] {}) + "</td>\n");
575                res.append("    <td>" + I18N.getString(locale, "batchpage;No.errorfile", new Object[] {}) + "</td>\n");
576                res.append("    <td>" + I18N.getString(locale, "batchpage;No.errorfile", new Object[] {}) + "</td>\n");
577            } else {
578                res.append("    <td>" + errorFile.length() + " "
579                        + I18N.getString(locale, "batchpage;bytes", new Object[] {}) + "</td>\n");
580                res.append("    <td>" + FileUtils.countLines(errorFile) + " "
581                        + I18N.getString(locale, "batchpage;lines", new Object[] {}) + "</td>\n");
582                res.append("    <td><a href=" + Constants.URL_RETRIEVE_RESULT_FILES + "?filename="
583                        + errorFile.getName() + ">"
584                        + I18N.getString(locale, "batchpage;Download.errorfile", new Object[] {}) + "</a></td>\n");
585            }
586
587            // end row
588            res.append("  </tr>\n");
589        }
590        res.append("</table>\n");
591        return res.toString();
592    }
593
594    /**
595     * Find the timestamp from the given prefix.
596     *
597     * @param prefix a given prefix of a filename
598     * @param locale a given locale used for an error-message
599     * @return a timestamp as a string or a message telling that the timestamp was not valid
600     */
601    private static String getTimestamp(String prefix, Locale locale) {
602        String[] split = prefix.split("[-]");
603        // default, if no timestamp is found
604        String timestamp = "";
605        if (split.length >= 2) {
606            try {
607                timestamp = new Date(Long.parseLong(split[1])).toString();
608            } catch (NumberFormatException e) {
609                log.warn("Could not parse batchjob result file name: " + prefix, e);
610            }
611        }
612
613        // If no timestamp was identified, write an error-message instead
614        if (timestamp.isEmpty()) {
615            timestamp = I18N.getString(locale, "batchpage;No.valid.timestamp", new Object[] {});
616        }
617        return timestamp;
618    }
619
620    /**
621     * Creates the HTML code for making the radio buttons for choosing which replica the batchjob will be run upon. <br/>
622     * E.g. the default replica settings (with two bitarchive replicas and one checksum replica) will give:<br/>
623     * <br/>
624     * <p>
625     * Choose replica: &lt;br/&gt;<br/>
626     * &lt;input type="radio" name="replica" value="CsOne" disabled&gtCsOne CHECKSUM&lt;/input&gt&lt;br/&gt;<br/>
627     * &lt;input type="radio" name="replica" value="BarOne" checked&gtBarOne BITARCHIVE&lt;/input&gt&lt;br/&gt;<br/>
628     * &lt;input type="radio" name="replica" value="BarTwo"&gtBarTwo BITARCHIVE&lt;/input&gt;&lt;br/&gt;<br/>
629     * <p>
630     * <br/>
631     * which gives: <br/>
632     * <p>
633     * Choose replica: <br/>
634     * <input type="radio" name="replica" value="CsOne" disabled>CsOne CHECKSUM </input><br/>
635     * <input type="radio" name="replica" value="BarOne" checked>BarOne BITARCHIVE</input><br/>
636     * <input type="radio" name="replica" value="BarTwo">BarTwo BITARCHIVE </input><br/>
637     * <br/>
638     *
639     * @param locale The locale language package.
640     * @return The HTML code for the radio buttons for choosing which replica to run a batchjob upon.
641     */
642    private static String getReplicaRadioButtons(Locale locale) {
643        StringBuilder res = new StringBuilder();
644
645        res.append(I18N.getString(locale, "batchpage;Choose.replica", new Object[] {}) + ": <br/>\n");
646
647        // Make radio buttons and categorize them as replica.
648        for (Replica rep : Replica.getKnown()) {
649            res.append("<input type=\"radio\" name=\"replica\" value=\"" + rep.getName() + "\"");
650            // Disable for checksum replica.
651            if (rep.getType().equals(ReplicaType.CHECKSUM)) {
652                res.append(" disabled");
653            } else if (rep.getId().equals(Settings.get(CommonSettings.USE_REPLICA_ID))) {
654                res.append(" checked");
655            }
656
657            res.append(">" + rep.getName() + " " + rep.getType() + "</input>");
658            res.append("<br/>\n");
659        }
660        res.append("<br/>\n");
661        return res.toString();
662    }
663
664    /**
665     * Creates the HTML code for choosing the regular expression for limiting the amount the files to be run upon. <br/>
666     * E.g. a default class with no specific argument for the limit will give:<br/>
667     * <br/>
668     * <p>
669     * Which files: &lt;br/&gt;<br/>
670     * Job ID: &nbsp; &nbsp; &nbsp; &lt;input name="JobId" size="25" value="1" /&gt;&lt;br/&gt;\n<br/>
671     * &lt;input type="radio" name="filetype" value="Metadata" checked /&gt;Metadata&lt;br/&gt;\n<br/>
672     * &lt;input type="radio" name="filetype" value="Content" checked /&gt;Content&lt;br/&gt;\n<br/>
673     * &lt;input type="radio" name="filetype" value="Both" checked /&gt;Both&lt;br/&gt;\n<br/>
674     * <p>
675     * <br/>
676     * Which gives:<br/>
677     * <br/>
678     * <p>
679     * Which files: <br/>
680     * Job ID: &nbsp; &nbsp; &nbsp; <input name="JobId" size="25" value="1" /> <br/>
681     * <input type="radio" name="filetype" value="Metadata" checked />Metadata <br/>
682     * <input type="radio" name="filetype" value="Content" checked />Content <br/>
683     * <input type="radio" name="filetype" value="Both" checked />Both<br/>
684     *
685     * @param locale The locale language package.
686     * @return The HTML code for creating the regular expression input box.
687     */
688    private static String getRegularExpressionInputBox(Locale locale) {
689        StringBuilder res = new StringBuilder();
690
691        // Make header ('Which files')
692        res.append(I18N.getString(locale, "batchpage;Which.files", new Object[] {}));
693        res.append(":<br/>\n");
694
695        // Make job id input:
696        res.append(I18N.getString(locale, "batchpage;Job.ID", new Object[] {}) + " &nbsp; &nbsp; &nbsp; <input name=\""
697                + Constants.JOB_ID_PARAMETER + "\" size=\"25\" value=\"1\" " + "/><br/>\n");
698
699        // Add metadata option (checked radiobutton)
700        res.append("<input type=\"radio\" name=\"filetype\" value=\"" + BatchFileType.Metadata + "\" checked />"
701                + I18N.getString(locale, "batchpage;Metadata", new Object[] {}) + "<br/>\n");
702        // Add content option
703        res.append("<input type=\"radio\" name=\"filetype\" value=\"" + BatchFileType.Content + "\" />"
704                + I18N.getString(locale, "batchpage;Content", new Object[] {}) + "<br/>\n");
705        // Add both option
706        res.append("<input type=\"radio\" name=\"filetype\" value=\"" + BatchFileType.Both + "\" />"
707                + I18N.getString(locale, "batchpage;Both", new Object[] {}) + "<br/>\n");
708
709        return res.toString();
710    }
711
712    /**
713     * Creates the HTML code for the submit button.
714     * <p>
715     * <br/>
716     * E.g. a default class with no specific argument for the limit will give:<br/>
717     * <br/>
718     * <p>
719     * Regular expression for file names (".*" = all files):&lt;br/&gt;<br/>
720     * &lt;input name="regex" size="50" value=".*"&gt; &lt;/input&gt;&lt;br/&gt;&lt;br/&gt;<br/>
721     * <p>
722     * <br/>
723     * Which gives:<br/>
724     * <br/>
725     * <p>
726     * Regular expression for file names (".*" = all files):<br/>
727     * <input name="regex" size="50" value=".*"> </input><br/>
728     * <br/>
729     *
730     * @param locale The locale language package.
731     * @return The HTML code for the submit button.
732     */
733    private static String getSubmitButton(Locale locale) {
734        StringBuilder res = new StringBuilder();
735        res.append("<br/>\n");
736        res.append("<input type=\"submit\" name=\"execute\" value=\""
737                + I18N.getString(locale, "batchpage;Execute.batchjob", new Object[] {}) + "\"/>");
738        res.append("<br/><br/>\n");
739        return res.toString();
740    }
741
742    /**
743     * Creates an entry for the overview table for the batchjobs. If the batchjob cannot be instatiated, then an error
744     * is written before the table entry, and only the name of the batchjob is written, though the whole name.
745     * <p>
746     * <br/>
747     * E.g.: <br/>
748     * &lt;tr&gt;<br/>
749     * &lt;th&gt;ChecksumJob&lt;/th&gt;<br/>
750     * &lt;th&gt;Tue Mar 23 13:56:45 CET 2010&lt;/th&gt;<br/>
751     * &lt;th&gt;&lt;input type="submit" name="ChecksumJob_output" value="view" /&gt;<br/>
752     * &lt;input type="submit" name="ChecksumJob_output" value="download" /&gt; <br/>
753     * 5 bytes&lt;/th&gt;<br/>
754     * &lt;th&gt;&lt;input type="submit" name="ChecksumJob_error" value="view" /&gt;<br/>
755     * &lt;input type="submit" name="ChecksumJob_error" value="download" /&gt;<br/>
756     * 5 bytes&lt;/th&gt;<br/>
757     * &lt;/tr&gt;
758     * <p>
759     * <br/>
760     * Which looks something like this: <br/>
761     * <br/>
762     * <p>
763     * <tr>
764     * <th>ChecksumJob</th>
765     * <th>Tue Mar 23 13:56:45 CET 2010</th>
766     * <th><input type="submit" name="ChecksumJob_output" value="view" /> <input type="submit" name="ChecksumJob_output"
767     * value="download" /> 5 bytes</th>
768     * <th><input type="submit" name="ChecksumJob_error" value="view" /> <input type="submit" name="ChecksumJob_error"
769     * value="download" /> 5 bytes</th>
770     * </tr>
771     *
772     * @param batchClassPath The name of the batch job.
773     * @param locale The language package.
774     * @return The HTML code for the entry in the table.
775     */
776    private static String getOverviewTableEntry(String batchClassPath, Locale locale) {
777        StringBuilder res = new StringBuilder();
778        try {
779            // Check whether it is retrievable. (Throws UnknownID if not).
780            getBatchClass(batchClassPath);
781
782            final String batchName = getJobName(batchClassPath);
783            File batchDir = getBatchDir();
784
785            // retrieve the latest batchjob results.
786            String timestamp = getLatestTimestamp(batchName);
787            File outputFile = new File(batchDir, batchName + timestamp + Constants.OUTPUT_FILE_EXTENSION);
788            File errorFile = new File(batchDir, batchName + timestamp + Constants.ERROR_FILE_EXTENSION);
789
790            // write the HTML
791            res.append("    <td><a href=\"" + Constants.URL_BATCHJOB + "?" + Constants.BATCHJOB_PARAMETER + "="
792                    + batchClassPath + "\">" + batchName + "</a></td>\n");
793            // add time of last run
794            String lastRun = "";
795            if (timestamp.isEmpty()) {
796                lastRun = I18N.getString(locale, "batchpage;Batchjob.has.never.been.run", new Object[] {});
797            } else {
798                try {
799                    lastRun = new Date(Long.parseLong(timestamp.substring(1))).toString();
800                } catch (NumberFormatException e) {
801                    log.warn("Could not parse the timestamp '" + timestamp + "'", e);
802                    lastRun = e.getMessage();
803                }
804            }
805            res.append("    <td>" + lastRun + "</td>\n");
806
807            // add output file references (retrieval and size)
808            if (outputFile.exists() && outputFile.isFile() && outputFile.canRead()) {
809                res.append("    <td><a href=" + Constants.URL_RETRIEVE_RESULT_FILES + "?filename="
810                        + outputFile.getName() + ">"
811                        + I18N.getString(locale, "batchpage;Download.outputfile", new Object[] {}) + "</a> "
812                        + outputFile.length() + " bytes, " + FileUtils.countLines(outputFile) + " lines</td>\n");
813            } else {
814                res.append("    <td>" + I18N.getString(locale, "batchpage;No.outputfile", new Object[] {}) + "</td>\n");
815            }
816            // add error file references (retrieval and size)
817            if (errorFile.exists() && errorFile.isFile() && errorFile.canRead()) {
818                res.append("    <td><a href=" + Constants.URL_RETRIEVE_RESULT_FILES + "?filename="
819                        + errorFile.getName() + ">"
820                        + I18N.getString(locale, "batchpage;Download.errorfile", new Object[] {}) + "</a> "
821                        + errorFile.length() + " bytes, " + FileUtils.countLines(errorFile) + " lines</td>\n");
822            } else {
823                res.append("    <td>" + I18N.getString(locale, "batchpage;No.errorfile", new Object[] {}) + "</td>\n");
824            }
825        } catch (NetarkivetException e) {
826            // Handle unretrievable batchjob.
827            String errMsg = "Unable to instantiate '" + batchClassPath + "' as a batchjob.";
828            log.warn(errMsg, e);
829
830            // clear the string builder.
831            res = new StringBuilder();
832            res.append(I18N.getString(locale, "batchpage;Warning.0", errMsg) + "\n");
833            res.append("    <td>" + batchClassPath + "</td>\n");
834            res.append("    <td>" + "--" + "</td>\n");
835            res.append("    <td>" + "--" + "</td>\n");
836            res.append("    <td>" + "--" + "</td>\n");
837        }
838
839        return res.toString();
840    }
841
842    /**
843     * Method for aquiring the name of the files with the latest timestamp. Creates a list with all the names of the
844     * result-files for the given batchjob. The list is sorted and the last (and thus latest) is returned.
845     *
846     * @param batchjobName The name of the batchjob in question. Is has to be the name without the path (e.g.
847     * dk.netarkivet.archive.arcrepository.bitpreservation.ChecksumJob should just be ChecksumJob).
848     * @return The name of the files for the given batchjob. The empty string is returned if no result files have been
849     * found, indicating that the batchjob has never been run.
850     * @throws ArgumentNotValid If the name of the batchjob is either null or the empty string.
851     */
852    private static String getLatestTimestamp(String batchjobName) throws ArgumentNotValid {
853        ArgumentNotValid.checkNotNullOrEmpty(batchjobName, "String batchjobName");
854
855        File dir = getBatchDir();
856        File[] list = dir.listFiles();
857        List<String> jobTimestamps = new ArrayList<String>();
858        for (File f : list) {
859            if (f.getName().startsWith(batchjobName)) {
860                int dash = f.getName().indexOf("-");
861                int dot = f.getName().lastIndexOf(".");
862                // check whether valid positions.
863                if (dash > 0 && dot > 0 && dot > dash) {
864                    jobTimestamps.add(f.getName().substring(dash, dot));
865                }
866            }
867        }
868
869        // send empty string back, no valid files exists.
870        if (jobTimestamps.isEmpty()) {
871            return "";
872        }
873
874        // extract the latest.
875        Collections.sort(jobTimestamps);
876        return jobTimestamps.get(jobTimestamps.size() - 1);
877    }
878
879    /**
880     * Method for extracting the name of the batchjob from the batchjob path. E.g. the batchjob:
881     * dk.netarkivet.archive.arcrepository.bitpreservation.ChecksumJob would become ChecksumJob.
882     *
883     * @param classPath The complete path for class (retrieve by class.getName()).
884     * @return The batchjob name of the class.
885     * @throws ArgumentNotValid If the classPath is either null or empty.
886     */
887    public static String getJobName(String classPath) throws ArgumentNotValid {
888        ArgumentNotValid.checkNotNullOrEmpty(classPath, "String className");
889
890        String[] jobSplit = classPath.split("[.]");
891        return jobSplit[jobSplit.length - 1];
892    }
893
894    /**
895     * Retrieves the directory for the batchDir (defined in settings).
896     *
897     * @return The directory containing all the batchjob results.
898     */
899    public static File getBatchDir() {
900        // extract the batch directory, where the old batchjobs files lies.
901        File batchDir = new File(Settings.get(CommonSettings.BATCHJOBS_BASEDIR));
902
903        // Create the directory, if it does not exist.
904        if (!batchDir.exists()) {
905            FileUtils.createDir(batchDir);
906        }
907
908        return batchDir;
909    }
910
911    /**
912     * Method for retrieving the path to the arcfile corresponding to the classpath.
913     *
914     * @param classpath The classpath to a batchjob.
915     * @return The path to the arc file for the batchjob.
916     * @throws UnknownID If the classpath is not within the settings.
917     */
918    private static String getArcFileForBatchjob(String classpath) throws UnknownID {
919        String[] jobs = Settings.getAll(CommonSettings.BATCHJOBS_CLASS);
920        String[] arcfiles = Settings.getAll(CommonSettings.BATCHJOBS_JARFILE);
921
922        // go through the lists to find the arc-file.
923        for (int i = 0; i < jobs.length; i++) {
924            if (jobs[i].equals(classpath)) {
925                return arcfiles[i];
926            }
927        }
928
929        throw new UnknownID("Unknown or undefined classpath for batchjob: '" + classpath + "'.");
930    }
931
932    /**
933     * Method for retrieving and validating the arc-file for a given DOOM!
934     *
935     * @param classPath The path to the file.
936     * @return The arc-file at the given path, or if the path is null or the empty string, then a null is returned.
937     * @throws ArgumentNotValid If the classPath argument is null or the empty string.
938     * @throws IOFailure If the file does not exist, or it is not a valid file.
939     */
940    public static File getJarFile(String classPath) throws ArgumentNotValid, IOFailure {
941        ArgumentNotValid.checkNotNullOrEmpty(classPath, "String classPath");
942
943        // retrieve the path to the arc-file.
944        String path = getArcFileForBatchjob(classPath);
945
946        // If no file, then return null.
947        if (path == null || path.isEmpty()) {
948            return null;
949        }
950
951        // retrieve file, and ensure that it exists and is a valid file.
952        File res = new File(path);
953        if (!res.isFile()) {
954            throw new IOFailure("The file '" + path + "' does not exist, or " + "is maybe not a file but a directory.");
955        }
956
957        return res;
958    }
959}