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. <br/><br/> 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:<br/><br/> 384 * regex (The regular expression for the urls.)<br/><br/> 385 * <input name="arg1" size="50" value=""><br/><br/> 386 * mimetype<br/><br/> 387 * <input name="arg2" size="50" value=""><br/><br/> 388 * Argument 3 (missing argument metadata)<br/><br/> 389 * <input name="arg3" size="50" value=""><br/><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: <br/><br/> 626 * <input type="radio" name="replica" value="CsOne" disabled>CsOne CHECKSUM</input><br/><br/> 627 * <input type="radio" name="replica" value="BarOne" checked>BarOne BITARCHIVE</input><br/><br/> 628 * <input type="radio" name="replica" value="BarTwo">BarTwo BITARCHIVE</input><br/><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: <br/><br/> 670 * Job ID: <input name="JobId" size="25" value="1" /><br/>\n<br/> 671 * <input type="radio" name="filetype" value="Metadata" checked />Metadata<br/>\n<br/> 672 * <input type="radio" name="filetype" value="Content" checked />Content<br/>\n<br/> 673 * <input type="radio" name="filetype" value="Both" checked />Both<br/>\n<br/> 674 * <p> 675 * <br/> 676 * Which gives:<br/> 677 * <br/> 678 * <p> 679 * Which files: <br/> 680 * Job ID: <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[] {}) + " <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):<br/><br/> 720 * <input name="regex" size="50" value=".*"> </input><br/><br/><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 * <tr><br/> 749 * <th>ChecksumJob</th><br/> 750 * <th>Tue Mar 23 13:56:45 CET 2010</th><br/> 751 * <th><input type="submit" name="ChecksumJob_output" value="view" /><br/> 752 * <input type="submit" name="ChecksumJob_output" value="download" /> <br/> 753 * 5 bytes</th><br/> 754 * <th><input type="submit" name="ChecksumJob_error" value="view" /><br/> 755 * <input type="submit" name="ChecksumJob_error" value="download" /><br/> 756 * 5 bytes</th><br/> 757 * </tr> 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}