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.IOException;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Date;
030import java.util.HashMap;
031import java.util.List;
032import java.util.Locale;
033import java.util.Map;
034
035import javax.servlet.ServletRequest;
036import javax.servlet.jsp.JspWriter;
037import javax.servlet.jsp.PageContext;
038
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042import dk.netarkivet.archive.arcrepository.bitpreservation.ActiveBitPreservation;
043import dk.netarkivet.archive.arcrepository.bitpreservation.ActiveBitPreservationFactory;
044import dk.netarkivet.archive.arcrepository.bitpreservation.PreservationState;
045import dk.netarkivet.common.distribute.arcrepository.Replica;
046import dk.netarkivet.common.exceptions.ArgumentNotValid;
047import dk.netarkivet.common.exceptions.ForwardedToErrorPage;
048import dk.netarkivet.common.utils.I18n;
049import dk.netarkivet.common.utils.StringUtils;
050import dk.netarkivet.common.webinterface.HTMLUtils;
051
052/**
053 * Class encapsulating methods for handling web requests for ActiveBitPreservation.
054 */
055@SuppressWarnings({"unchecked"})
056public class BitpreserveFileState {
057    /** Internationalisation object. */
058    private static final I18n I18N = new I18n(dk.netarkivet.archive.Constants.TRANSLATIONS_BUNDLE);
059    /** The logger for this class. */
060    //private static Log log = LogFactory.getLog(BitpreserveFileState.class);
061    protected static final Logger log = LoggerFactory.getLogger(BitpreserveFileState.class);
062
063    /**
064     * Private constructor to avoid instantiation of this class.
065     */
066    private BitpreserveFileState() {
067    }
068
069    /**
070     * Extract the name of the replica (parameter Constants.BITARCHIVE_NAME_PARAM) and the type of update requested
071     * (parameter Constants.UPDATE_TYPE_PARAM). The latter is set to to Constants.FIND_MISSING_FILES_OPTION if the
072     * request is to update missing files, or to Constants.CHECKSUM_OPTION if the request is to update the checksum
073     * information.
074     *
075     * @param context the current JSP context
076     * @return an I18N string telling which type of update has just been initiated.
077     * @throws ForwardedToErrorPage if an unknown bitarchive or update type is posted, or one of the two required
078     * parameters are missing.
079     * @throws ArgumentNotValid If the context is null.
080     */
081    public static String processUpdateRequest(PageContext context) throws ArgumentNotValid, ForwardedToErrorPage {
082        ArgumentNotValid.checkNotNull(context, "PageContext context");
083        ServletRequest request = context.getRequest();
084
085        String bitarchiveName = request.getParameter(Constants.BITARCHIVE_NAME_PARAM);
086        if (bitarchiveName == null) {
087            HTMLUtils.forwardWithErrorMessage(context, I18N, "errormsg;missing.parameter.0",
088                    Constants.BITARCHIVE_NAME_PARAM);
089
090            throw new ForwardedToErrorPage("Parameter '" + Constants.BITARCHIVE_NAME_PARAM + "' not set");
091        }
092
093        String updateTypeRequested = request.getParameter(Constants.UPDATE_TYPE_PARAM);
094        if (updateTypeRequested == null) {
095            HTMLUtils.forwardWithErrorMessage(context, I18N, "errormsg;missing.parameter.0",
096                    Constants.UPDATE_TYPE_PARAM);
097
098            throw new ForwardedToErrorPage("Parameter '" + Constants.UPDATE_TYPE_PARAM + "' not set");
099        }
100
101        if (!Replica.isKnownReplicaName(bitarchiveName)) {
102            HTMLUtils.forwardWithErrorMessage(context, I18N, "errormsg;unknown.bitarchive.0", bitarchiveName);
103            throw new ForwardedToErrorPage("Unknown replica: " + bitarchiveName);
104        }
105
106        Replica bitarchive = Replica.getReplicaFromName(bitarchiveName);
107
108        Locale l = context.getResponse().getLocale();
109        String statusmessage = HTMLUtils.escapeHtmlValues(I18N.getString(l, "initiating;update.of.0.for.replica.1",
110                updateTypeRequested, bitarchiveName));
111        if (updateTypeRequested.equalsIgnoreCase(Constants.FIND_MISSING_FILES_OPTION)) {
112            // Start new thread for findmissing files action.
113            new BitpreservationUpdateThread(bitarchive, BitpreservationUpdateType.FINDMISSING).start();
114            return statusmessage;
115
116        } else if (updateTypeRequested.equalsIgnoreCase(Constants.CHECKSUM_OPTION)) {
117            // Start new thread for finding corrupt files action.
118            new BitpreservationUpdateThread(bitarchive, BitpreservationUpdateType.CHECKSUM).start();
119            return statusmessage;
120        } else {
121            HTMLUtils.forwardWithErrorMessage(context, I18N, "errormsg;unknown.filestatus.update.type.0",
122                    updateTypeRequested);
123            throw new ForwardedToErrorPage("Unknown filestatus update type: " + bitarchiveName);
124        }
125    }
126
127    /**
128     * Processes a missingFiles request.
129     * <p>
130     * Parameters of the form Constants.ADD_COMMAND=&lt;bitarchive&gt;##&lt;filename&gt; causes the file to be added to
131     * that bitarchive, if it is missing.
132     * <p>
133     * Parameters of the form Constants.GET_INFO_COMMAND=&lt;filename&gt; causes checksums to be computed for the file
134     * in all bitarchives and the information to be shown in the next update (notice that this information disappears
135     * when the page is next reloaded).
136     *
137     * @param context the current JSP context.
138     * @param res the result object. This is updated with result information, and expected to be printed to the
139     * resulting page.
140     * @return A map of info gathered for files as requested.
141     * @throws ArgumentNotValid If the context or res is null.
142     * @throws ForwardedToErrorPage if the commands have the wrong number of arguments.
143     */
144    public static Map<String, PreservationState> processMissingRequest(PageContext context, StringBuilder res)
145            throws ArgumentNotValid, ForwardedToErrorPage {
146        ArgumentNotValid.checkNotNull(context, "PageContext context");
147        ArgumentNotValid.checkNotNull(res, "StringBuilder res");
148        Map<String, String[]> params = context.getRequest().getParameterMap();
149        HTMLUtils.forwardOnMissingParameter(context, Constants.BITARCHIVE_NAME_PARAM);
150        String bitarchiveName = params.get(Constants.BITARCHIVE_NAME_PARAM)[0];
151        if (!Replica.isKnownReplicaName(bitarchiveName)) {
152            List<String> names = new ArrayList<String>();
153            HTMLUtils.forwardOnIllegalParameter(context, Constants.BITARCHIVE_NAME_PARAM,
154                    StringUtils.conjoin(", ", names.toArray(Replica.getKnownNames())));
155        }
156        ActiveBitPreservation preserve = ActiveBitPreservationFactory.getInstance();
157        Locale l = context.getResponse().getLocale();
158        if (params.containsKey(Constants.ADD_COMMAND)) {
159            String[] adds = params.get(Constants.ADD_COMMAND);
160            for (String s : adds) {
161                String[] parts = s.split(Constants.STRING_FILENAME_SEPARATOR);
162                checkArgs(context, parts, Constants.ADD_COMMAND, "bitarchive name", "filename");
163                final Replica ba = Replica.getReplicaFromName(parts[0]);
164                final String filename = parts[1];
165                try {
166                    preserve.uploadMissingFiles(ba, filename);
167                    res.append(HTMLUtils.escapeHtmlValues(I18N.getString(l, "file.0.has.been.restored.in.replica.on.1",
168                            filename, ba.getName())));
169                    res.append("<br/>");
170                } catch (Exception e) {
171                    res.append(I18N.getString(l, "errormsg;attempt.at.restoring.0.in.replica" + ".at.1.failed",
172                            filename, ba));
173                    res.append("<br/>");
174                    res.append(e.getMessage());
175                    res.append("<br/>");
176                    log.warn("Could not restore file '" + filename + "' in bitarchive '" + ba + "'", e);
177                }
178            }
179        }
180        // A map ([filename] -> [preservationstate]) to contain
181        // preservationstates for all files retrieved from the
182        // parameter Constants.GET_INFO_COMMAND.
183        // This map is an empty map, if this parameter is undefined.
184        Map<String, PreservationState> infoMap;
185        // Do this at the end so that the info reflects the current state.
186        if (params.containsKey(Constants.GET_INFO_COMMAND)) {
187            String[] getInfos = params.get(Constants.GET_INFO_COMMAND);
188            infoMap = preserve.getPreservationStateMap(getInfos);
189        } else {
190            infoMap = new HashMap<String, PreservationState>();
191        }
192
193        return infoMap;
194    }
195
196    /**
197     * Check that an array of strings has the arguments corresponding to a command.
198     *
199     * @param context the JSP context to forward to error to.
200     * @param parts Array of arguments given by user.
201     * @param cmd The command to match.
202     * @param argnames The names of the expected arguments.
203     * @throws ForwardedToErrorPage if the parts are not exactly as many as the arguments.
204     */
205    private static void checkArgs(PageContext context, String[] parts, String cmd, String... argnames)
206            throws ForwardedToErrorPage {
207        if (argnames.length != parts.length) {
208            HTMLUtils.forwardWithErrorMessage(context, I18N, "errormsg;argument.mismatch.command.needs"
209                    + ".arguments.0.but.got.1", Arrays.asList(argnames), Arrays.asList(parts));
210
211            throw new ForwardedToErrorPage("Command " + cmd + " needs arguments " + Arrays.asList(argnames)
212                    + ", but got '" + Arrays.asList(parts) + "'");
213        }
214    }
215
216    /**
217     * Processes a checksum request.
218     * <p>
219     * The name of a bitarchive must always be given in parameter Constants.BITARCHIVE_NAME_PARAM.
220     * <p>
221     * If parameter Constants.FILENAME_PARAM is given, file info for that file will be returned, and all actions will
222     * work on that file.
223     * <p>
224     * If parameter Constants.FIX_ADMIN_CHECKSUM_PARAM is given, the admin data checksum will be fixed for the file.
225     * <p>
226     * If parameter Constants.CREDENTIALS and Constants.CHECKSUM_PARAM is given, removes and reuploads a file with that
227     * checksum in the given bitarchive, using the credentials for authorisation.
228     *
229     * @param res the result object. This is updated with result information, and expected to be printed to the
230     * resulting page.
231     * @param context the current JSP pagecontext.
232     * @return The file preservation state for a file, if a filename is given in the request. Null otherwise.
233     * @throws ArgumentNotValid If the context or res is null.
234     */
235    public static PreservationState processChecksumRequest(StringBuilder res, PageContext context)
236            throws ArgumentNotValid {
237        ArgumentNotValid.checkNotNull(res, "StringBuilder res");
238        ArgumentNotValid.checkNotNull(context, "PageContext context");
239        ServletRequest request = context.getRequest();
240        Locale l = context.getResponse().getLocale();
241        HTMLUtils.forwardOnIllegalParameter(context, Constants.BITARCHIVE_NAME_PARAM, Replica.getKnownNames());
242        String bitarchiveName = request.getParameter(Constants.BITARCHIVE_NAME_PARAM);
243        Replica bitarchive = Replica.getReplicaFromName(bitarchiveName);
244        String filename = request.getParameter(Constants.FILENAME_PARAM);
245        String fixadminchecksum = request.getParameter(Constants.FIX_ADMIN_CHECKSUM_PARAM);
246        String credentials = request.getParameter(Constants.CREDENTIALS_PARAM);
247        String checksum = request.getParameter(Constants.CHECKSUM_PARAM);
248
249        // Parameter validation. Get filename. Complain about missing filename
250        // if we are trying to do actions.
251        if (filename == null) { // param "file" not set - no action to take
252            if (fixadminchecksum != null || credentials != null || checksum != null) {
253                // Only if an action was intended do we complain about
254                // a missing file.
255                res.append(I18N.getString(l, "errormsg;lack.name.for.file.to.be.corrected.in.0", bitarchiveName));
256            }
257            return null;
258        }
259
260        // At this point we know that the parameter filename is given.
261        // Now we check for actions.
262        ActiveBitPreservation preserve = ActiveBitPreservationFactory.getInstance();
263        if (fixadminchecksum != null) {
264            // Action to fix admin.data checksum.
265            preserve.changeStateForAdminData(filename);
266            res.append(I18N.getString(l, "file.0.now.has.correct.checksum.in.admin.data", filename));
267            res.append("<br/>");
268        } else if (checksum != null || credentials != null) {
269            // Action to replace a broken file with a correct file.
270            // Both parameters must be given.
271            if (checksum == null) { // param CHECKSUM_PARAM not set
272                res.append(I18N.getString(l, "errormsg;lack.checksum.for.corrupted.file.0", filename));
273                res.append("<br/>");
274            } else if (credentials == null) { // param CREDENTIALS_PARAM not set
275                res.append(I18N.getString(l, "errormsg;lacking.privileges.to.correct.in.replica"));
276                res.append("<br/>");
277            } else {
278                // Parameters are correct. Fix the file and report result.
279                try {
280                    preserve.replaceChangedFile(bitarchive, filename, credentials, checksum);
281                    res.append(I18N.getString(l, "file.0.has.been.replaced.in.1", filename, bitarchive));
282                    res.append("<br/>");
283                } catch (Exception e) {
284                    res.append(I18N.getString(l, "errormsg;attempt.at.restoring.0.in.replica" + ".at.1.failed",
285                            filename, bitarchive));
286                    res.append("<br/>");
287                    res.append(e.getMessage());
288                    res.append("<br/>");
289                    log.warn("Attempt at restoring '" + filename + "' in bitarchive on replica '" + bitarchive
290                            + "' failed", e);
291                }
292            }
293        }
294
295        return preserve.getPreservationState(filename);
296    }
297
298    /**
299     * Create a generic checkbox as used by processMissingRequest.
300     *
301     * @param command The name of the command
302     * @param args Arguments to the command
303     * @return A checkbox with the command and arguments in correct format and with HTML stuff escaped.
304     */
305    public static String makeCheckbox(String command, String... args) {
306        ArgumentNotValid.checkNotNull(command, "command");
307        ArgumentNotValid.checkNotNull(args, "args");
308        StringBuilder res = new StringBuilder();
309        for (String arg : args) {
310            if (res.length() == 0) {
311                res.append(" value=\"");
312            } else {
313                res.append(Constants.STRING_FILENAME_SEPARATOR);
314            }
315            res.append(HTMLUtils.escapeHtmlValues(arg));
316        }
317        if (res.length() != 0) {
318            res.append("\"");
319        }
320        return ("<input type=\"checkbox\" name=\"" + command + "\"" + res.toString() + " />");
321    }
322
323    /**
324     * Print HTML formatted state for missing files on a given replica in a given locale.
325     *
326     * @param out The writer to write state to.
327     * @param replica The replica to write state for.
328     * @param locale The locale to write state in.
329     * @throws IOException On IO trouble writing state to the writer.
330     */
331    public static void printMissingFileStateForReplica(JspWriter out, Replica replica, Locale locale)
332            throws IOException {
333        ArgumentNotValid.checkNotNull(out, "JspWriter out");
334        ArgumentNotValid.checkNotNull(replica, "Replica replica");
335        ArgumentNotValid.checkNotNull(locale, "Locale locale");
336        ActiveBitPreservation activeBitPreservation = ActiveBitPreservationFactory.getInstance();
337
338        //element id's
339        final String replicaName = replica.getName();
340        final String numberId = replicaName + "_number";
341        final String missingId = replicaName + "_missing";
342        final String updatedId = replicaName + "_updated";
343        //Header
344        out.println(I18N.getString(locale, "filestatus.for") + "&nbsp;<b>"
345                + HTMLUtils.escapeHtmlValues(replicaName) + "</b>");
346        out.println("<br/>");
347
348        // Number of files, and number of files missing
349        out.println(I18N.getString(locale, "number.of.files") + "&nbsp;<span id=\"" + numberId + "\">"
350                + HTMLUtils.localiseLong(activeBitPreservation.getNumberOfFiles(replica), locale)+ "</span>");
351        out.println("<br/>");
352        long numberOfMissingFiles = activeBitPreservation.getNumberOfMissingFiles(replica);
353        out.println(I18N.getString(locale, "missing.files") + "&nbsp;<span id=\"" + missingId + "\">"
354                + HTMLUtils.localiseLong(numberOfMissingFiles, locale) + "</span>");
355
356        if (numberOfMissingFiles > 0) {
357            out.print("&nbsp;<a href=\"" + Constants.FILESTATUS_MISSING_PAGE + "?"
358                    + (Constants.BITARCHIVE_NAME_PARAM + "=" + HTMLUtils.encodeAndEscapeHTML(replicaName))
359                    + " \">");
360            out.print(I18N.getString(locale, "show.missing.files"));
361            out.print("</a>");
362        }
363        out.println("<br/>");
364        Date lastMissingFilesupdate = activeBitPreservation.getDateForMissingFiles(replica);
365        if (lastMissingFilesupdate == null) {
366            lastMissingFilesupdate = new Date(0);
367        }
368        out.println("<span id=\"" + updatedId + "\">" + I18N.getString(locale, "last.update.at.0", lastMissingFilesupdate) + "</span>");
369        out.println("<br/>");
370
371        out.println("<a href=\"" + Constants.FILESTATUS_UPDATE_PAGE + "?" + Constants.UPDATE_TYPE_PARAM + "="
372                + Constants.FIND_MISSING_FILES_OPTION + "&amp;"
373                + (Constants.BITARCHIVE_NAME_PARAM + "=" + HTMLUtils.encodeAndEscapeHTML(replicaName)) + "\">"
374                + I18N.getString(locale, "update.filestatus.for.0", replica.getId()) + "</a>");
375        out.println("<br/><br/>");
376    }
377
378    /**
379     * Print HTML formatted state for checksum errors on a given replica in a given locale.
380     *
381     * @param out The writer to write state to.
382     * @param replica The replica to write state for.
383     * @param locale The locale to write state in.
384     * @throws IOException On IO trouble writing state to the writer.
385     */
386    public static void printChecksumErrorStateForReplica(JspWriter out, Replica replica, Locale locale)
387            throws IOException {
388        ArgumentNotValid.checkNotNull(out, "JspWriter out");
389        ArgumentNotValid.checkNotNull(replica, "Replica replica");
390        ArgumentNotValid.checkNotNull(locale, "Locale locale");
391        ActiveBitPreservation bitPreservation = ActiveBitPreservationFactory.getInstance();
392
393        // Header
394        out.println(I18N.getString(locale, "checksum.status.for") + "&nbsp;<b>"
395                + HTMLUtils.escapeHtmlValues(replica.getName()) + "</b>");
396        out.println("<br/>");
397
398        // Number of changed files
399        long numberOfChangedFiles = bitPreservation.getNumberOfChangedFiles(replica);
400        out.println(I18N.getString(locale, "number.of.files.with.error") + "&nbsp;"
401                + HTMLUtils.localiseLong(numberOfChangedFiles, locale));
402
403        // Link to fix-page
404        if (numberOfChangedFiles > 0) {
405            out.print("&nbsp;<a href=\"" + Constants.FILESTATUS_CHECKSUM_PAGE + "?"
406                    + (Constants.BITARCHIVE_NAME_PARAM + "=" + HTMLUtils.encodeAndEscapeHTML(replica.getName()))
407                    + " \">");
408            out.print(I18N.getString(locale, "show.files.with.error"));
409            out.print("</a>");
410        }
411        out.println("<br/>");
412
413        // Time for last update
414        Date lastChangedFilesupdate = bitPreservation.getDateForChangedFiles(replica);
415        if (lastChangedFilesupdate == null) {
416            lastChangedFilesupdate = new Date(0);
417        }
418        out.println(I18N.getString(locale, "last.update.at.0", lastChangedFilesupdate));
419        out.println("<br/>");
420
421        // Link for running a new job
422        out.println("<a href=\"" + Constants.FILESTATUS_UPDATE_PAGE + "?" + Constants.UPDATE_TYPE_PARAM + "="
423                + Constants.CHECKSUM_OPTION + "&amp;"
424                + (Constants.BITARCHIVE_NAME_PARAM + "=" + HTMLUtils.encodeAndEscapeHTML(replica.getName())) + "\">"
425                + I18N.getString(locale, "update.checksum.and.file.status.for.0", replica.getId()) + "</a>");
426
427        // Separator
428        out.println("<br/><br/>");
429    }
430
431    /**
432     * Print a table row with a file name and a checkbox to request more info.
433     *
434     * @param out The stream to print to.
435     * @param filename The name of the file.
436     * @param rowCount The rowcount, used for styling rows.
437     * @param locale The current locale for labels.
438     * @throws IOException On trouble writing to stream.
439     */
440    public static void printFileName(JspWriter out, String filename, int rowCount, Locale locale) throws IOException {
441        ArgumentNotValid.checkNotNull(out, "JspWriter out");
442        ArgumentNotValid.checkNotNullOrEmpty(filename, "String filename");
443        ArgumentNotValid.checkNotNull(locale, "Locale locale");
444        out.println("<tr class=\"" + HTMLUtils.getRowClass(rowCount) + "\">");
445        out.println(HTMLUtils.makeTableElement(filename));
446        out.print("<td>");
447        out.print(makeCheckbox(Constants.GET_INFO_COMMAND, filename));
448        out.print(I18N.getString(locale, "get.info"));
449        out.println("</td>");
450        out.println("</tr>");
451    }
452
453    /**
454     * Print a file state table for a file. This will present the state of the file in admin data and all bitarchives.
455     *
456     * @param out The stream to print to.
457     * @param fs The file state for the file.
458     * @param locale The locale to print labels in.
459     * @throws IOException On trouble printing to a stream.
460     */
461    public static void printFileState(JspWriter out, PreservationState fs, Locale locale) throws IOException {
462        ArgumentNotValid.checkNotNull(out, "JspWriter out");
463        ArgumentNotValid.checkNotNull(fs, "FilePreservationState fs");
464        ArgumentNotValid.checkNotNull(locale, "Locale locale");
465
466        // Table headers for info table
467        out.println("<table>");
468        out.print(HTMLUtils.makeTableRow(HTMLUtils.makeTableHeader(I18N.getString(locale, "replica")),
469                HTMLUtils.makeTableHeader(I18N.getString(locale, "admin.state")),
470                HTMLUtils.makeTableHeader(I18N.getString(locale, "checksum"))));
471
472        // Admin data info
473        printFileStateForAdminData(out, fs, locale);
474
475        // Info for all bitarchives
476        for (Replica l : Replica.getKnown()) {
477            printFileStateForBitarchive(out, l, fs, locale);
478        }
479        out.println("</table>");
480    }
481
482    /**
483     * Print a table row with current state of a file in admin data.
484     *
485     * @param out The stream to print state to.
486     * @param fs The file preservation state for that file.
487     * @param locale Locale of the labels.
488     * @throws IOException on trouble printing the state.
489     */
490    private static void printFileStateForAdminData(JspWriter out, PreservationState fs, Locale locale)
491            throws IOException {
492        out.print(HTMLUtils.makeTableRow(HTMLUtils.makeTableElement(I18N.getString(locale, "admin.data")),
493                HTMLUtils.makeTableElement("-"), HTMLUtils.makeTableElement(fs.getAdminChecksum())));
494    }
495
496    /**
497     * Print a table row with current state of a file in a given bitarchive.
498     *
499     * @param out The stream to print state to.
500     * @param baReplica The replica of the files.
501     * @param fs The file preservation state for that file.
502     * @param locale Locale of the labels.
503     * @throws IOException If an problem occurs when writing to the JspWriter.
504     */
505    private static void printFileStateForBitarchive(JspWriter out, Replica baReplica, PreservationState fs,
506            Locale locale) throws IOException {
507        log.debug("Printing filestate for bitarchive '" + baReplica.getName() + "'");
508        out.print(HTMLUtils.makeTableRow(HTMLUtils.makeTableElement(baReplica.getName()),
509                HTMLUtils.makeTableElement(fs.getAdminReplicaState(baReplica)),
510                HTMLUtils.makeTableElement(presentChecksum(fs.getReplicaChecksum(baReplica), locale))));
511    }
512
513    /**
514     * Print checkboxes for changing state for files. This will print two checkboxes for changing a number of
515     * checkboxes, one for getting more info, one for reestablishing missing files. This method assumes the file
516     * toggleCheckboxes.js to be available in the directory with the page this method is called from.
517     *
518     * @param out The stream to print the checkboxes to.
519     * @param locale The locale of the labels.
520     * @param numberOfMissingCheckboxes The total possible number of missing checkboxes.
521     * @param numberOfUploadableCheckboxes The total possible number of reestablish checkboxes.
522     * @throws IOException On trouble printing the checkboxes.
523     */
524    public static void printToggleCheckboxes(JspWriter out, Locale locale, int numberOfMissingCheckboxes,
525            int numberOfUploadableCheckboxes) throws IOException {
526        // Print the javascript needed.
527        ArgumentNotValid.checkNotNull(out, "JspWriter out");
528        ArgumentNotValid.checkNotNull(locale, "Locale locale");
529        out.println("<script type=\"text/javascript\" language=\"javascript\""
530                + " src=\"toggleCheckboxes.js\"></script>");
531        // Add checkbox to toggle multiple "fileinfo" checkboxes
532        printMultipleToggler(out, Constants.GET_INFO_COMMAND, numberOfMissingCheckboxes, "change.infobox.for.0.files",
533                locale);
534        // Add checkbox to toggle multiple "reupload" checkboxes
535        if (numberOfUploadableCheckboxes > 0) {
536            printMultipleToggler(out, Constants.ADD_COMMAND, numberOfUploadableCheckboxes, "change.0.may.be.added",
537                    locale);
538        }
539    }
540
541    /**
542     * Print a checkbox that on click will turn a number of checkboxes of a certain type on or off.
543     *
544     * @param out The stream to print the checkbox to.
545     * @param command The type of checkbox.
546     * @param numberOfCheckboxes The total number of checksboxes possible to turn on or off.
547     * @param label The I18N label for the describing text. An input box with the number to change will be added as
548     * parameter {0} in this label.
549     * @param locale The locale for the checkbox.
550     * @throws IOException On trouble printing the checkbox.
551     */
552    private static void printMultipleToggler(JspWriter out, String command, int numberOfCheckboxes, String label,
553            Locale locale) throws IOException {
554        out.print("<input type=\"checkbox\" id=\"toggle" + command + "\" onclick=\"toggleCheckboxes('" + command
555                + "')\"/>");
556        out.print(I18N.getString(
557                locale,
558                label,
559                "<input id=\"toggleAmount" + command + "\" value=\""
560                        + Math.min(numberOfCheckboxes, Constants.MAX_TOGGLE_AMOUNT) + "\" />"));
561        out.println("<br/> ");
562    }
563
564    /**
565     * Present a list of checksums in a human-readable form. If size of list is 0, it returns "No checksum". If size of
566     * list is 1, it returns the one available checksum. Otherwise, it returns toString of the list.
567     *
568     * @param csum List of checksum strings
569     * @param locale The given locale.
570     * @return String presenting the checksums.
571     */
572    public static String presentChecksum(List<String> csum, Locale locale) {
573        ArgumentNotValid.checkNotNull(csum, "List<String> csum");
574        ArgumentNotValid.checkNotNull(locale, "Locale locale");
575        String csumString = csum.toString();
576        if (csum.isEmpty()) {
577            csumString = I18N.getString(locale, "no.checksum");
578        } else if (csum.size() == 1) {
579            csumString = csum.get(0);
580        }
581        return csumString;
582    }
583}