dk.netarkivet.archive.webinterface.BitpreserveFileState.java Source code

Java tutorial

Introduction

Here is the source code for dk.netarkivet.archive.webinterface.BitpreserveFileState.java

Source

/*
 * #%L
 * Netarchivesuite - archive
 * %%
 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
 *             the National Library of France and the Austrian National Library.
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 2.1 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
 * #L%
 */

package dk.netarkivet.archive.webinterface;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.servlet.ServletRequest;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.PageContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import dk.netarkivet.archive.arcrepository.bitpreservation.ActiveBitPreservation;
import dk.netarkivet.archive.arcrepository.bitpreservation.ActiveBitPreservationFactory;
import dk.netarkivet.archive.arcrepository.bitpreservation.PreservationState;
import dk.netarkivet.common.distribute.arcrepository.Replica;
import dk.netarkivet.common.exceptions.ArgumentNotValid;
import dk.netarkivet.common.exceptions.ForwardedToErrorPage;
import dk.netarkivet.common.utils.I18n;
import dk.netarkivet.common.utils.StringUtils;
import dk.netarkivet.common.webinterface.HTMLUtils;

/**
 * Class encapsulating methods for handling web requests for ActiveBitPreservation.
 */
@SuppressWarnings({ "unchecked" })
public class BitpreserveFileState {
    /** Internationalisation object. */
    private static final I18n I18N = new I18n(dk.netarkivet.archive.Constants.TRANSLATIONS_BUNDLE);
    /** The logger for this class. */
    private static Log log = LogFactory.getLog(BitpreserveFileState.class);

    /**
     * Private constructor to avoid instantiation of this class.
     */
    private BitpreserveFileState() {
    }

    /**
     * Extract the name of the replica (parameter Constants.BITARCHIVE_NAME_PARAM) and the type of update requested
     * (parameter Constants.UPDATE_TYPE_PARAM). The latter is set to to Constants.FIND_MISSING_FILES_OPTION if the
     * request is to update missing files, or to Constants.CHECKSUM_OPTION if the request is to update the checksum
     * information.
     *
     * @param context the current JSP context
     * @return an I18N string telling which type of update has just been initiated.
     * @throws ForwardedToErrorPage if an unknown bitarchive or update type is posted, or one of the two required
     * parameters are missing.
     * @throws ArgumentNotValid If the context is null.
     */
    public static String processUpdateRequest(PageContext context) throws ArgumentNotValid, ForwardedToErrorPage {
        ArgumentNotValid.checkNotNull(context, "PageContext context");
        ServletRequest request = context.getRequest();

        String bitarchiveName = request.getParameter(Constants.BITARCHIVE_NAME_PARAM);
        if (bitarchiveName == null) {
            HTMLUtils.forwardWithErrorMessage(context, I18N, "errormsg;missing.parameter.0",
                    Constants.BITARCHIVE_NAME_PARAM);

            throw new ForwardedToErrorPage("Parameter '" + Constants.BITARCHIVE_NAME_PARAM + "' not set");
        }

        String updateTypeRequested = request.getParameter(Constants.UPDATE_TYPE_PARAM);
        if (updateTypeRequested == null) {
            HTMLUtils.forwardWithErrorMessage(context, I18N, "errormsg;missing.parameter.0",
                    Constants.UPDATE_TYPE_PARAM);

            throw new ForwardedToErrorPage("Parameter '" + Constants.UPDATE_TYPE_PARAM + "' not set");
        }

        if (!Replica.isKnownReplicaName(bitarchiveName)) {
            HTMLUtils.forwardWithErrorMessage(context, I18N, "errormsg;unknown.bitarchive.0", bitarchiveName);
            throw new ForwardedToErrorPage("Unknown replica: " + bitarchiveName);
        }

        Replica bitarchive = Replica.getReplicaFromName(bitarchiveName);

        Locale l = context.getResponse().getLocale();
        String statusmessage = HTMLUtils.escapeHtmlValues(
                I18N.getString(l, "initiating;update.of.0.for.replica.1", updateTypeRequested, bitarchiveName));
        if (updateTypeRequested.equalsIgnoreCase(Constants.FIND_MISSING_FILES_OPTION)) {
            // Start new thread for findmissing files action.
            new BitpreservationUpdateThread(bitarchive, BitpreservationUpdateType.FINDMISSING).start();
            return statusmessage;

        } else if (updateTypeRequested.equalsIgnoreCase(Constants.CHECKSUM_OPTION)) {
            // Start new thread for finding corrupt files action.
            new BitpreservationUpdateThread(bitarchive, BitpreservationUpdateType.CHECKSUM).start();
            return statusmessage;
        } else {
            HTMLUtils.forwardWithErrorMessage(context, I18N, "errormsg;unknown.filestatus.update.type.0",
                    updateTypeRequested);
            throw new ForwardedToErrorPage("Unknown filestatus update type: " + bitarchiveName);
        }
    }

    /**
     * Processes a missingFiles request.
     * <p>
     * Parameters of the form Constants.ADD_COMMAND=&lt;bitarchive&gt;##&lt;filename&gt; causes the file to be added to
     * that bitarchive, if it is missing.
     * <p>
     * Parameters of the form Constants.GET_INFO_COMMAND=&lt;filename&gt; causes checksums to be computed for the file
     * in all bitarchives and the information to be shown in the next update (notice that this information disappears
     * when the page is next reloaded).
     *
     * @param context the current JSP context.
     * @param res the result object. This is updated with result information, and expected to be printed to the
     * resulting page.
     * @return A map of info gathered for files as requested.
     * @throws ArgumentNotValid If the context or res is null.
     * @throws ForwardedToErrorPage if the commands have the wrong number of arguments.
     */
    public static Map<String, PreservationState> processMissingRequest(PageContext context, StringBuilder res)
            throws ArgumentNotValid, ForwardedToErrorPage {
        ArgumentNotValid.checkNotNull(context, "PageContext context");
        ArgumentNotValid.checkNotNull(res, "StringBuilder res");
        Map<String, String[]> params = context.getRequest().getParameterMap();
        HTMLUtils.forwardOnMissingParameter(context, Constants.BITARCHIVE_NAME_PARAM);
        String bitarchiveName = params.get(Constants.BITARCHIVE_NAME_PARAM)[0];
        if (!Replica.isKnownReplicaName(bitarchiveName)) {
            List<String> names = new ArrayList<String>();
            HTMLUtils.forwardOnIllegalParameter(context, Constants.BITARCHIVE_NAME_PARAM,
                    StringUtils.conjoin(", ", names.toArray(Replica.getKnownNames())));
        }
        ActiveBitPreservation preserve = ActiveBitPreservationFactory.getInstance();
        Locale l = context.getResponse().getLocale();
        if (params.containsKey(Constants.ADD_COMMAND)) {
            String[] adds = params.get(Constants.ADD_COMMAND);
            for (String s : adds) {
                String[] parts = s.split(Constants.STRING_FILENAME_SEPARATOR);
                checkArgs(context, parts, Constants.ADD_COMMAND, "bitarchive name", "filename");
                final Replica ba = Replica.getReplicaFromName(parts[0]);
                final String filename = parts[1];
                try {
                    preserve.uploadMissingFiles(ba, filename);
                    res.append(HTMLUtils.escapeHtmlValues(
                            I18N.getString(l, "file.0.has.been.restored.in.replica.on.1", filename, ba.getName())));
                    res.append("<br/>");
                } catch (Exception e) {
                    res.append(I18N.getString(l, "errormsg;attempt.at.restoring.0.in.replica" + ".at.1.failed",
                            filename, ba));
                    res.append("<br/>");
                    res.append(e.getMessage());
                    res.append("<br/>");
                    log.warn("Could not restore file '" + filename + "' in bitarchive '" + ba + "'", e);
                }
            }
        }
        // A map ([filename] -> [preservationstate]) to contain
        // preservationstates for all files retrieved from the
        // parameter Constants.GET_INFO_COMMAND.
        // This map is an empty map, if this parameter is undefined.
        Map<String, PreservationState> infoMap;
        // Do this at the end so that the info reflects the current state.
        if (params.containsKey(Constants.GET_INFO_COMMAND)) {
            String[] getInfos = params.get(Constants.GET_INFO_COMMAND);
            infoMap = preserve.getPreservationStateMap(getInfos);
        } else {
            infoMap = new HashMap<String, PreservationState>();
        }

        return infoMap;
    }

    /**
     * Check that an array of strings has the arguments corresponding to a command.
     *
     * @param context the JSP context to forward to error to.
     * @param parts Array of arguments given by user.
     * @param cmd The command to match.
     * @param argnames The names of the expected arguments.
     * @throws ForwardedToErrorPage if the parts are not exactly as many as the arguments.
     */
    private static void checkArgs(PageContext context, String[] parts, String cmd, String... argnames)
            throws ForwardedToErrorPage {
        if (argnames.length != parts.length) {
            HTMLUtils.forwardWithErrorMessage(context, I18N,
                    "errormsg;argument.mismatch.command.needs" + ".arguments.0.but.got.1", Arrays.asList(argnames),
                    Arrays.asList(parts));

            throw new ForwardedToErrorPage("Command " + cmd + " needs arguments " + Arrays.asList(argnames)
                    + ", but got '" + Arrays.asList(parts) + "'");
        }
    }

    /**
     * Processes a checksum request.
     * <p>
     * The name of a bitarchive must always be given in parameter Constants.BITARCHIVE_NAME_PARAM.
     * <p>
     * If parameter Constants.FILENAME_PARAM is given, file info for that file will be returned, and all actions will
     * work on that file.
     * <p>
     * If parameter Constants.FIX_ADMIN_CHECKSUM_PARAM is given, the admin data checksum will be fixed for the file.
     * <p>
     * If parameter Constants.CREDENTIALS and Constants.CHECKSUM_PARAM is given, removes and reuploads a file with that
     * checksum in the given bitarchive, using the credentials for authorisation.
     *
     * @param res the result object. This is updated with result information, and expected to be printed to the
     * resulting page.
     * @param context the current JSP pagecontext.
     * @return The file preservation state for a file, if a filename is given in the request. Null otherwise.
     * @throws ArgumentNotValid If the context or res is null.
     */
    public static PreservationState processChecksumRequest(StringBuilder res, PageContext context)
            throws ArgumentNotValid {
        ArgumentNotValid.checkNotNull(res, "StringBuilder res");
        ArgumentNotValid.checkNotNull(context, "PageContext context");
        ServletRequest request = context.getRequest();
        Locale l = context.getResponse().getLocale();
        HTMLUtils.forwardOnIllegalParameter(context, Constants.BITARCHIVE_NAME_PARAM, Replica.getKnownNames());
        String bitarchiveName = request.getParameter(Constants.BITARCHIVE_NAME_PARAM);
        Replica bitarchive = Replica.getReplicaFromName(bitarchiveName);
        String filename = request.getParameter(Constants.FILENAME_PARAM);
        String fixadminchecksum = request.getParameter(Constants.FIX_ADMIN_CHECKSUM_PARAM);
        String credentials = request.getParameter(Constants.CREDENTIALS_PARAM);
        String checksum = request.getParameter(Constants.CHECKSUM_PARAM);

        // Parameter validation. Get filename. Complain about missing filename
        // if we are trying to do actions.
        if (filename == null) { // param "file" not set - no action to take
            if (fixadminchecksum != null || credentials != null || checksum != null) {
                // Only if an action was intended do we complain about
                // a missing file.
                res.append(I18N.getString(l, "errormsg;lack.name.for.file.to.be.corrected.in.0", bitarchiveName));
            }
            return null;
        }

        // At this point we know that the parameter filename is given.
        // Now we check for actions.
        ActiveBitPreservation preserve = ActiveBitPreservationFactory.getInstance();
        if (fixadminchecksum != null) {
            // Action to fix admin.data checksum.
            preserve.changeStateForAdminData(filename);
            res.append(I18N.getString(l, "file.0.now.has.correct.checksum.in.admin.data", filename));
            res.append("<br/>");
        } else if (checksum != null || credentials != null) {
            // Action to replace a broken file with a correct file.
            // Both parameters must be given.
            if (checksum == null) { // param CHECKSUM_PARAM not set
                res.append(I18N.getString(l, "errormsg;lack.checksum.for.corrupted.file.0", filename));
                res.append("<br/>");
            } else if (credentials == null) { // param CREDENTIALS_PARAM not set
                res.append(I18N.getString(l, "errormsg;lacking.privileges.to.correct.in.replica"));
                res.append("<br/>");
            } else {
                // Parameters are correct. Fix the file and report result.
                try {
                    preserve.replaceChangedFile(bitarchive, filename, credentials, checksum);
                    res.append(I18N.getString(l, "file.0.has.been.replaced.in.1", filename, bitarchive));
                    res.append("<br/>");
                } catch (Exception e) {
                    res.append(I18N.getString(l, "errormsg;attempt.at.restoring.0.in.replica" + ".at.1.failed",
                            filename, bitarchive));
                    res.append("<br/>");
                    res.append(e.getMessage());
                    res.append("<br/>");
                    log.warn("Attempt at restoring '" + filename + "' in bitarchive on replica '" + bitarchive
                            + "' failed", e);
                }
            }
        }

        return preserve.getPreservationState(filename);
    }

    /**
     * Create a generic checkbox as used by processMissingRequest.
     *
     * @param command The name of the command
     * @param args Arguments to the command
     * @return A checkbox with the command and arguments in correct format and with HTML stuff escaped.
     */
    public static String makeCheckbox(String command, String... args) {
        ArgumentNotValid.checkNotNull(command, "command");
        ArgumentNotValid.checkNotNull(args, "args");
        StringBuilder res = new StringBuilder();
        for (String arg : args) {
            if (res.length() == 0) {
                res.append(" value=\"");
            } else {
                res.append(Constants.STRING_FILENAME_SEPARATOR);
            }
            res.append(HTMLUtils.escapeHtmlValues(arg));
        }
        if (res.length() != 0) {
            res.append("\"");
        }
        return ("<input type=\"checkbox\" name=\"" + command + "\"" + res.toString() + " />");
    }

    /**
     * Print HTML formatted state for missing files on a given replica in a given locale.
     *
     * @param out The writer to write state to.
     * @param replica The replica to write state for.
     * @param locale The locale to write state in.
     * @throws IOException On IO trouble writing state to the writer.
     */
    public static void printMissingFileStateForReplica(JspWriter out, Replica replica, Locale locale)
            throws IOException {
        ArgumentNotValid.checkNotNull(out, "JspWriter out");
        ArgumentNotValid.checkNotNull(replica, "Replica replica");
        ArgumentNotValid.checkNotNull(locale, "Locale locale");
        ActiveBitPreservation activeBitPreservation = ActiveBitPreservationFactory.getInstance();

        //element id's
        final String replicaName = replica.getName();
        final String numberId = replicaName + "_number";
        final String missingId = replicaName + "_missing";
        final String updatedId = replicaName + "_updated";
        //Header
        out.println(I18N.getString(locale, "filestatus.for") + "&nbsp;<b>" + HTMLUtils.escapeHtmlValues(replicaName)
                + "</b>");
        out.println("<br/>");

        // Number of files, and number of files missing
        out.println(I18N.getString(locale, "number.of.files") + "&nbsp;<span id=\"" + numberId + "\">"
                + HTMLUtils.localiseLong(activeBitPreservation.getNumberOfFiles(replica), locale) + "</span>");
        out.println("<br/>");
        long numberOfMissingFiles = activeBitPreservation.getNumberOfMissingFiles(replica);
        out.println(I18N.getString(locale, "missing.files") + "&nbsp;<span id=\"" + missingId + "\">"
                + HTMLUtils.localiseLong(numberOfMissingFiles, locale) + "</span>");

        if (numberOfMissingFiles > 0) {
            out.print("&nbsp;<a href=\"" + Constants.FILESTATUS_MISSING_PAGE + "?"
                    + (Constants.BITARCHIVE_NAME_PARAM + "=" + HTMLUtils.encodeAndEscapeHTML(replicaName))
                    + " \">");
            out.print(I18N.getString(locale, "show.missing.files"));
            out.print("</a>");
        }
        out.println("<br/>");
        Date lastMissingFilesupdate = activeBitPreservation.getDateForMissingFiles(replica);
        if (lastMissingFilesupdate == null) {
            lastMissingFilesupdate = new Date(0);
        }
        out.println("<span id=\"" + updatedId + "\">"
                + I18N.getString(locale, "last.update.at.0", lastMissingFilesupdate) + "</span>");
        out.println("<br/>");

        out.println("<a href=\"" + Constants.FILESTATUS_UPDATE_PAGE + "?" + Constants.UPDATE_TYPE_PARAM + "="
                + Constants.FIND_MISSING_FILES_OPTION + "&amp;"
                + (Constants.BITARCHIVE_NAME_PARAM + "=" + HTMLUtils.encodeAndEscapeHTML(replicaName)) + "\">"
                + I18N.getString(locale, "update.filestatus.for.0", replica.getId()) + "</a>");
        out.println("<br/><br/>");
    }

    /**
     * Print HTML formatted state for checksum errors on a given replica in a given locale.
     *
     * @param out The writer to write state to.
     * @param replica The replica to write state for.
     * @param locale The locale to write state in.
     * @throws IOException On IO trouble writing state to the writer.
     */
    public static void printChecksumErrorStateForReplica(JspWriter out, Replica replica, Locale locale)
            throws IOException {
        ArgumentNotValid.checkNotNull(out, "JspWriter out");
        ArgumentNotValid.checkNotNull(replica, "Replica replica");
        ArgumentNotValid.checkNotNull(locale, "Locale locale");
        ActiveBitPreservation bitPreservation = ActiveBitPreservationFactory.getInstance();

        // Header
        out.println(I18N.getString(locale, "checksum.status.for") + "&nbsp;<b>"
                + HTMLUtils.escapeHtmlValues(replica.getName()) + "</b>");
        out.println("<br/>");

        // Number of changed files
        long numberOfChangedFiles = bitPreservation.getNumberOfChangedFiles(replica);
        out.println(I18N.getString(locale, "number.of.files.with.error") + "&nbsp;"
                + HTMLUtils.localiseLong(numberOfChangedFiles, locale));

        // Link to fix-page
        if (numberOfChangedFiles > 0) {
            out.print("&nbsp;<a href=\"" + Constants.FILESTATUS_CHECKSUM_PAGE + "?"
                    + (Constants.BITARCHIVE_NAME_PARAM + "=" + HTMLUtils.encodeAndEscapeHTML(replica.getName()))
                    + " \">");
            out.print(I18N.getString(locale, "show.files.with.error"));
            out.print("</a>");
        }
        out.println("<br/>");

        // Time for last update
        Date lastChangedFilesupdate = bitPreservation.getDateForChangedFiles(replica);
        if (lastChangedFilesupdate == null) {
            lastChangedFilesupdate = new Date(0);
        }
        out.println(I18N.getString(locale, "last.update.at.0", lastChangedFilesupdate));
        out.println("<br/>");

        // Link for running a new job
        out.println("<a href=\"" + Constants.FILESTATUS_UPDATE_PAGE + "?" + Constants.UPDATE_TYPE_PARAM + "="
                + Constants.CHECKSUM_OPTION + "&amp;"
                + (Constants.BITARCHIVE_NAME_PARAM + "=" + HTMLUtils.encodeAndEscapeHTML(replica.getName())) + "\">"
                + I18N.getString(locale, "update.checksum.and.file.status.for.0", replica.getId()) + "</a>");

        // Separator
        out.println("<br/><br/>");
    }

    /**
     * Print a table row with a file name and a checkbox to request more info.
     *
     * @param out The stream to print to.
     * @param filename The name of the file.
     * @param rowCount The rowcount, used for styling rows.
     * @param locale The current locale for labels.
     * @throws IOException On trouble writing to stream.
     */
    public static void printFileName(JspWriter out, String filename, int rowCount, Locale locale)
            throws IOException {
        ArgumentNotValid.checkNotNull(out, "JspWriter out");
        ArgumentNotValid.checkNotNullOrEmpty(filename, "String filename");
        ArgumentNotValid.checkNotNull(locale, "Locale locale");
        out.println("<tr class=\"" + HTMLUtils.getRowClass(rowCount) + "\">");
        out.println(HTMLUtils.makeTableElement(filename));
        out.print("<td>");
        out.print(makeCheckbox(Constants.GET_INFO_COMMAND, filename));
        out.print(I18N.getString(locale, "get.info"));
        out.println("</td>");
        out.println("</tr>");
    }

    /**
     * Print a file state table for a file. This will present the state of the file in admin data and all bitarchives.
     *
     * @param out The stream to print to.
     * @param fs The file state for the file.
     * @param locale The locale to print labels in.
     * @throws IOException On trouble printing to a stream.
     */
    public static void printFileState(JspWriter out, PreservationState fs, Locale locale) throws IOException {
        ArgumentNotValid.checkNotNull(out, "JspWriter out");
        ArgumentNotValid.checkNotNull(fs, "FilePreservationState fs");
        ArgumentNotValid.checkNotNull(locale, "Locale locale");

        // Table headers for info table
        out.println("<table>");
        out.print(HTMLUtils.makeTableRow(HTMLUtils.makeTableHeader(I18N.getString(locale, "replica")),
                HTMLUtils.makeTableHeader(I18N.getString(locale, "admin.state")),
                HTMLUtils.makeTableHeader(I18N.getString(locale, "checksum"))));

        // Admin data info
        printFileStateForAdminData(out, fs, locale);

        // Info for all bitarchives
        for (Replica l : Replica.getKnown()) {
            printFileStateForBitarchive(out, l, fs, locale);
        }
        out.println("</table>");
    }

    /**
     * Print a table row with current state of a file in admin data.
     *
     * @param out The stream to print state to.
     * @param fs The file preservation state for that file.
     * @param locale Locale of the labels.
     * @throws IOException on trouble printing the state.
     */
    private static void printFileStateForAdminData(JspWriter out, PreservationState fs, Locale locale)
            throws IOException {
        out.print(HTMLUtils.makeTableRow(HTMLUtils.makeTableElement(I18N.getString(locale, "admin.data")),
                HTMLUtils.makeTableElement("-"), HTMLUtils.makeTableElement(fs.getAdminChecksum())));
    }

    /**
     * Print a table row with current state of a file in a given bitarchive.
     *
     * @param out The stream to print state to.
     * @param baReplica The replica of the files.
     * @param fs The file preservation state for that file.
     * @param locale Locale of the labels.
     * @throws IOException If an problem occurs when writing to the JspWriter.
     */
    private static void printFileStateForBitarchive(JspWriter out, Replica baReplica, PreservationState fs,
            Locale locale) throws IOException {
        log.debug("Printing filestate for bitarchive '" + baReplica.getName() + "'");
        out.print(HTMLUtils.makeTableRow(HTMLUtils.makeTableElement(baReplica.getName()),
                HTMLUtils.makeTableElement(fs.getAdminReplicaState(baReplica)),
                HTMLUtils.makeTableElement(presentChecksum(fs.getReplicaChecksum(baReplica), locale))));
    }

    /**
     * Print checkboxes for changing state for files. This will print two checkboxes for changing a number of
     * checkboxes, one for getting more info, one for reestablishing missing files. This method assumes the file
     * toggleCheckboxes.js to be available in the directory with the page this method is called from.
     *
     * @param out The stream to print the checkboxes to.
     * @param locale The locale of the labels.
     * @param numberOfMissingCheckboxes The total possible number of missing checkboxes.
     * @param numberOfUploadableCheckboxes The total possible number of reestablish checkboxes.
     * @throws IOException On trouble printing the checkboxes.
     */
    public static void printToggleCheckboxes(JspWriter out, Locale locale, int numberOfMissingCheckboxes,
            int numberOfUploadableCheckboxes) throws IOException {
        // Print the javascript needed.
        ArgumentNotValid.checkNotNull(out, "JspWriter out");
        ArgumentNotValid.checkNotNull(locale, "Locale locale");
        out.println("<script type=\"text/javascript\" language=\"javascript\""
                + " src=\"toggleCheckboxes.js\"></script>");
        // Add checkbox to toggle multiple "fileinfo" checkboxes
        printMultipleToggler(out, Constants.GET_INFO_COMMAND, numberOfMissingCheckboxes,
                "change.infobox.for.0.files", locale);
        // Add checkbox to toggle multiple "reupload" checkboxes
        if (numberOfUploadableCheckboxes > 0) {
            printMultipleToggler(out, Constants.ADD_COMMAND, numberOfUploadableCheckboxes, "change.0.may.be.added",
                    locale);
        }
    }

    /**
     * Print a checkbox that on click will turn a number of checkboxes of a certain type on or off.
     *
     * @param out The stream to print the checkbox to.
     * @param command The type of checkbox.
     * @param numberOfCheckboxes The total number of checksboxes possible to turn on or off.
     * @param label The I18N label for the describing text. An input box with the number to change will be added as
     * parameter {0} in this label.
     * @param locale The locale for the checkbox.
     * @throws IOException On trouble printing the checkbox.
     */
    private static void printMultipleToggler(JspWriter out, String command, int numberOfCheckboxes, String label,
            Locale locale) throws IOException {
        out.print("<input type=\"checkbox\" id=\"toggle" + command + "\" onclick=\"toggleCheckboxes('" + command
                + "')\"/>");
        out.print(I18N.getString(locale, label, "<input id=\"toggleAmount" + command + "\" value=\""
                + Math.min(numberOfCheckboxes, Constants.MAX_TOGGLE_AMOUNT) + "\" />"));
        out.println("<br/> ");
    }

    /**
     * Present a list of checksums in a human-readable form. If size of list is 0, it returns "No checksum". If size of
     * list is 1, it returns the one available checksum. Otherwise, it returns toString of the list.
     *
     * @param csum List of checksum strings
     * @param locale The given locale.
     * @return String presenting the checksums.
     */
    public static String presentChecksum(List<String> csum, Locale locale) {
        ArgumentNotValid.checkNotNull(csum, "List<String> csum");
        ArgumentNotValid.checkNotNull(locale, "Locale locale");
        String csumString = csum.toString();
        if (csum.isEmpty()) {
            csumString = I18N.getString(locale, "no.checksum");
        } else if (csum.size() == 1) {
            csumString = csum.get(0);
        }
        return csumString;
    }
}