source: CMDIValidator/trunk/cmdi-validator-tool/src/main/java/eu/clarin/cmdi/validator/tool/CMDIValidatorTool.java @ 5384

Last change on this file since 5384 was 5384, checked in by Oliver Schonefeld, 10 years ago
  • add license stuff
  • Property svn:eol-style set to native
File size: 24.9 KB
Line 
1/**
2 * This software is copyright (c) 2014 by
3 *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
4 * This is free software. You can redistribute it
5 * and/or modify it under the terms described in
6 * the GNU General Public License v3 of which you
7 * should have received a copy. Otherwise you can download
8 * it from
9 *
10 *   http://www.gnu.org/licenses/gpl-3.0.txt
11 *
12 * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
13 *
14 * @license http://www.gnu.org/licenses/gpl-3.0.txt
15 *  GNU General Public License v3
16 */
17package eu.clarin.cmdi.validator.tool;
18
19import humanize.Humanize;
20
21import java.io.File;
22import java.io.PrintWriter;
23import java.util.Locale;
24import java.util.concurrent.TimeUnit;
25import net.java.truevfs.access.TFile;
26import net.java.truevfs.access.TVFS;
27import net.java.truevfs.kernel.spec.FsSyncException;
28
29import org.apache.commons.cli.CommandLine;
30import org.apache.commons.cli.CommandLineParser;
31import org.apache.commons.cli.GnuParser;
32import org.apache.commons.cli.HelpFormatter;
33import org.apache.commons.cli.OptionBuilder;
34import org.apache.commons.cli.OptionGroup;
35import org.apache.commons.cli.Options;
36import org.apache.commons.cli.ParseException;
37import org.slf4j.Logger;
38import org.slf4j.LoggerFactory;
39
40import eu.clarin.cmdi.validator.ThreadedCMDIValidatorProcessor;
41import eu.clarin.cmdi.validator.CMDIValidator;
42import eu.clarin.cmdi.validator.CMDIValidatorConfig;
43import eu.clarin.cmdi.validator.CMDIValidatorException;
44import eu.clarin.cmdi.validator.CMDIValidatorInitException;
45import eu.clarin.cmdi.validator.CMDIValidatorHandlerAdapter;
46import eu.clarin.cmdi.validator.CMDIValidatorResult;
47import eu.clarin.cmdi.validator.CMDIValidatorResult.Message;
48import eu.clarin.cmdi.validator.CMDIValidatorResult.Severity;
49import eu.clarin.cmdi.validator.extensions.CheckHandlesExtension;
50
51
52public class CMDIValidatorTool {
53    private static final String PRG_NAME                = "cmdi-validator";
54    private static final long DEFAULT_PROGRESS_INTERVAL = 15000;
55    private static final Locale LOCALE                  = Locale.ENGLISH;
56    private static final char OPT_DEBUG                 = 'd';
57    private static final char OPT_QUIET                 = 'q';
58    private static final char OPT_VERBOSE               = 'v';
59    private static final char OPT_NO_PROGRESS           = 'P';
60    private static final char OPT_THREAD_COUNT          = 't';
61    private static final char OPT_NO_THREADS            = 'T';
62    private static final char OPT_NO_ESTIMATE           = 'E';
63    private static final char OPT_SCHEMA_CACHE_DIR      = 'c';
64    private static final char OPT_NO_SCHEMATRON         = 'S';
65    private static final char OPT_SCHEMATRON_FILE       = 's';
66    private static final char OPT_CHECK_PIDS            = 'p';
67    private static final Logger logger =
68            LoggerFactory.getLogger(CMDIValidatorTool.class);
69    private static final org.apache.log4j.ConsoleAppender appender;
70
71
72    public static void main(String[] args) {
73        /*
74         * application defaults
75         */
76        boolean debugging         = false;
77        boolean quiet             = false;
78        boolean verbose           = false;
79        int threadCount           = Runtime.getRuntime().availableProcessors();
80        boolean estimate          = true;
81        long progressInterval     = DEFAULT_PROGRESS_INTERVAL;
82        File schemaCacheDir       = null;
83        boolean disableSchematron = false;
84        File schematronFile       = null;
85        boolean checkPids         = false;
86
87        /*
88         * setup command line parser
89         */
90        final Options options = createCommandLineOptions();
91        try {
92            final CommandLineParser parser = new GnuParser();
93            final CommandLine line = parser.parse(options, args);
94            // check incompatible combinations
95            if (line.hasOption(OPT_THREAD_COUNT) && line.hasOption(OPT_NO_THREADS)) {
96                throw new ParseException("The -t and -T options are mutually exclusive");
97            }
98            if (line.hasOption(OPT_DEBUG) && line.hasOption(OPT_QUIET)) {
99                throw new ParseException("The -d and -q switches are mutually exclusive");
100            }
101            if (line.hasOption(OPT_VERBOSE) && line.hasOption(OPT_QUIET)) {
102                throw new ParseException("The -v and -q switches are mutually exclusive");
103            }
104            if (line.hasOption(OPT_NO_SCHEMATRON) && line.hasOption(OPT_SCHEMATRON_FILE)) {
105                throw new ParseException("The -s and -T options are mutually exclusive");
106            }
107            // extract options
108            if (line.hasOption(OPT_DEBUG)) {
109                debugging = true;
110            }
111            if (line.hasOption(OPT_QUIET)) {
112                quiet = true;
113            }
114            if (line.hasOption(OPT_VERBOSE)) {
115                verbose = true;
116            }
117            if (line.hasOption(OPT_NO_PROGRESS) || quiet) {
118                progressInterval = -1;
119            }
120            if (line.hasOption(OPT_THREAD_COUNT)) {
121                try {
122                    threadCount = Integer.parseInt(
123                            line.getOptionValue(OPT_THREAD_COUNT));
124                    if (threadCount < 1) {
125                        throw new ParseException(
126                                "thread count must be larger then 0");
127                    }
128                } catch (NumberFormatException e) {
129                    throw new ParseException("invalid number");
130                }
131            }
132            if (line.hasOption(OPT_NO_THREADS)) {
133                threadCount = 1;
134            }
135            if (line.hasOption(OPT_NO_ESTIMATE) || (progressInterval < 0)) {
136                estimate = false;
137            }
138            if (line.hasOption(OPT_SCHEMA_CACHE_DIR)) {
139                String dir = line.getOptionValue(OPT_SCHEMA_CACHE_DIR);
140                if ((dir == null) || dir.isEmpty()) {
141                    throw new ParseException("invalid argument for -" +
142                            OPT_SCHEMA_CACHE_DIR);
143                }
144                schemaCacheDir = new File(dir);
145            }
146            if (line.hasOption(OPT_NO_SCHEMATRON)) {
147                disableSchematron = true;
148            }
149            if (line.hasOption(OPT_SCHEMATRON_FILE)) {
150                String name = line.getOptionValue(OPT_SCHEMATRON_FILE);
151                if ((name == null) || name.isEmpty()) {
152                    throw new ParseException("invalid argument for -" +
153                            OPT_SCHEMATRON_FILE);
154                }
155                schematronFile = new File(name);
156            }
157            if (line.hasOption(OPT_CHECK_PIDS)) {
158                checkPids = true;
159            }
160
161            final String[] remaining = line.getArgs();
162            if ((remaining == null) || (remaining.length == 0)) {
163                throw new ParseException("require <DIRECTORY> or <FILE> as " +
164                        "additional command line parameter");
165            }
166
167            final org.apache.log4j.Logger log =
168                    org.apache.log4j.Logger.getLogger(
169                            CMDIValidator.class.getPackage().getName());
170            if (debugging) {
171                appender.setLayout(
172                        new org.apache.log4j.PatternLayout("[%p] %t: %m%n"));
173                log.setLevel(org.apache.log4j.Level.DEBUG);
174            } else {
175                if (quiet) {
176                    log.setLevel(org.apache.log4j.Level.ERROR);
177                } else {
178                    log.setLevel(org.apache.log4j.Level.INFO);
179                }
180            }
181
182            TFile archive = null;
183            try {
184                if (schemaCacheDir != null) {
185                    logger.info("using schema cache directory: {}", schemaCacheDir);
186                }
187                if (schematronFile != null) {
188                    logger.info("using Schematron schema from file: {}", schematronFile);
189                }
190
191                /*
192                 * process archive
193                 */
194                archive = new TFile(remaining[0]);
195                if (archive.exists()) {
196                    if (archive.isArchive()) {
197                        logger.info("reading archive '{}'", archive);
198                    } else {
199                        logger.info("reading directory '{}'", archive);
200                    }
201
202                    int totalFileCount = -1;
203                    if (estimate && logger.isInfoEnabled()) {
204                        logger.debug("counting files ...");
205                        totalFileCount = countFiles(archive);
206                    }
207
208                    if (threadCount > 1) {
209                      logger.debug("using {} threads", threadCount);
210                    }
211
212
213                    final ValidationHandler handler = new ValidationHandler(verbose);
214
215                    final CMDIValidatorConfig.Builder builder =
216                            new CMDIValidatorConfig.Builder(archive, handler);
217                    if (schemaCacheDir != null) {
218                        builder.schemaCacheDirectory(schemaCacheDir);
219                    }
220                    if (schematronFile != null) {
221                        builder.schematronSchemaFile(schematronFile);
222                    }
223                    if (disableSchematron) {
224                        builder.disableSchematron();
225                    }
226                    if (checkPids) {
227                        logger.info("performing PID checking");
228                        builder.extension(
229                                new CheckHandlesExtension(threadCount, true));
230                    }
231
232                    final ThreadedCMDIValidatorProcessor processor =
233                            new ThreadedCMDIValidatorProcessor(threadCount);
234                    processor.start();
235                    try {
236                        final CMDIValidator validator =
237                                new CMDIValidator(builder.build());
238                        processor.process(validator);
239
240                        /*
241                         * Wait until validation is done and report about
242                         * progress every now and then ...
243                         */
244                        for (;;) {
245                            try {
246                                if (handler.await(progressInterval)) {
247                                    break;
248                                }
249                            } catch (InterruptedException e) {
250                                /* IGNORE */
251                            }
252                            if ((progressInterval > 0) && logger.isInfoEnabled()) {
253                                final long now = System.currentTimeMillis();
254                                int fps    = -1;
255                                long bps   = -1;
256                                int count  = 0;
257                                long delta = -1;
258                                synchronized (handler) {
259                                    delta = (now - handler.getTimeStarted()) / 1000;
260                                    if (delta > 0) {
261                                        fps = (int) (handler.getTotalFileCount() / delta);
262                                        bps = (handler.getTotalBytes() / delta);
263                                    }
264                                    count = handler.getTotalFileCount();
265                                } // synchronized (result)
266                                if (totalFileCount > 0) {
267                                    float complete = (count / (float)  totalFileCount) * 100f;
268                                    logger.info("processed {} files ({}%) in {} ({} files/second, {}/second) ...",
269                                            count,
270                                            String.format(LOCALE, "%.2f", complete),
271                                            Humanize.duration(delta, LOCALE),
272                                            ((fps != -1) ? fps : "N/A"),
273                                            ((bps != -1) ? Humanize.binaryPrefix(bps, LOCALE) : "N/A MB"));
274                                } else {
275                                    logger.info("processed {} files in {} ({} files/second, {}/second) ...",
276                                            count,
277                                            Humanize.duration(delta, LOCALE),
278                                            ((fps != -1) ? fps : "N/A"),
279                                            ((bps != -1) ? Humanize.binaryPrefix(bps, LOCALE) : "N/A MB"));
280                                }
281                            }
282                        } // for (;;)
283                    } finally {
284                        processor.shutdown();
285                    }
286
287                    int fps = -1;
288                    long bps = -1;
289                    if (handler.getTimeElapsed() > 0) {
290                        fps = (int) (handler.getTotalFileCount() / handler.getTimeElapsed());
291                        bps = handler.getTotalBytes() / handler.getTimeElapsed();
292                    }
293
294                    logger.info("time elapsed: {}, validation result: {}% failure rate (files: {} total, {} passed, {} failed; {} total, {} files/second, {}/second)",
295                            Humanize.duration(handler.getTimeElapsed(), LOCALE),
296                            String.format(LOCALE, "%.2f", handler.getFailureRate() * 100f),
297                            handler.getTotalFileCount(),
298                            handler.getValidFileCount(),
299                            handler.getInvalidFileCount(),
300                            Humanize.binaryPrefix(handler.getTotalBytes(), LOCALE),
301                            ((fps != -1) ? fps : "N/A"),
302                            ((bps != -1) ? Humanize.binaryPrefix(bps, LOCALE) : "N/A MB"));
303                    logger.debug("... done");
304                } else {
305                    logger.error("not found: {}", archive);
306                }
307            } finally {
308                if (archive != null) {
309                    try {
310                        TVFS.umount(archive);
311                    } catch (FsSyncException e) {
312                        logger.error("error unmounting archive", e);
313                    }
314                }
315            }
316        } catch (CMDIValidatorException e) {
317            logger.error("error initalizing job: {}", e.getMessage());
318            if (debugging) {
319                logger.error(e.getMessage(), e);
320            }
321            System.exit(1);
322        } catch (CMDIValidatorInitException e) {
323            logger.error("error initializing validator: {}", e.getMessage());
324            if (debugging) {
325                logger.error(e.getMessage(), e);
326            }
327            System.exit(2);
328        } catch (ParseException e) {
329            PrintWriter writer = new PrintWriter(System.err);
330            if (e.getMessage() != null) {
331                writer.print("ERROR: ");
332                writer.println(e.getMessage());
333            }
334            HelpFormatter formatter = new HelpFormatter();
335            formatter.printHelp(writer, HelpFormatter.DEFAULT_WIDTH, PRG_NAME,
336                    null, options, HelpFormatter.DEFAULT_LEFT_PAD,
337                    HelpFormatter.DEFAULT_DESC_PAD, null, true);
338            writer.flush();
339            writer.close();
340            System.exit(64); /* EX_USAGE */
341        }
342    }
343
344
345    @SuppressWarnings("static-access")
346    private static Options createCommandLineOptions() {
347        final Options options = new Options();
348        OptionGroup g1 = new OptionGroup();
349        g1.addOption(OptionBuilder
350                .withDescription("enable debugging output")
351                .withLongOpt("debug")
352                .create(OPT_DEBUG));
353        g1.addOption(OptionBuilder
354                .withDescription("be quiet")
355                .withLongOpt("quiet")
356                .create(OPT_QUIET));
357        options.addOptionGroup(g1);
358        options.addOption(OptionBuilder
359                .withDescription("be verbose")
360                .withLongOpt("verbose")
361                .create(OPT_VERBOSE));
362        options.addOption(OptionBuilder
363                .withDescription("no progress reporting")
364                .withLongOpt("no-progress")
365                .create(OPT_NO_PROGRESS));
366        OptionGroup g2 = new OptionGroup();
367        g2.addOption(OptionBuilder
368                .withDescription("number of validator threads")
369                .hasArg()
370                .withArgName("COUNT")
371                .withLongOpt("threads")
372                .create(OPT_THREAD_COUNT));
373        g2.addOption(OptionBuilder
374                .withDescription("disable threading")
375                .withLongOpt("no-threads")
376                .create(OPT_NO_THREADS));
377        options.addOptionGroup(g2);
378        options.addOption(OptionBuilder
379            .withDescription("disable gathering of total file count for progress reporting")
380            .withLongOpt("no-estimate")
381            .create(OPT_NO_ESTIMATE));
382        options.addOption(OptionBuilder
383                .withDescription("schema caching directory")
384                .hasArg()
385                .withArgName("DIRECTORY")
386                .withLongOpt("schema-cache-dir")
387                .create(OPT_SCHEMA_CACHE_DIR));
388        OptionGroup g3 = new OptionGroup();
389        g3.addOption(OptionBuilder
390                .withDescription("disable Schematron validator")
391                .withLongOpt("no-schematron")
392                .create(OPT_NO_SCHEMATRON));
393        g3.addOption(OptionBuilder
394                .withDescription("load Schematron schema from file")
395                .hasArg()
396                .withArgName("FILE")
397                .withLongOpt("schematron-file")
398                .create(OPT_SCHEMATRON_FILE));
399        options.addOptionGroup(g3);
400        options.addOption(OptionBuilder
401                .withDescription("check if persistent identifiers resolve correctly")
402                .withLongOpt("check-pids")
403                .create(OPT_CHECK_PIDS));
404        return options;
405    }
406
407
408    private static final int countFiles(TFile directory) {
409        int count = 0;
410        final TFile[] entries = directory.listFiles();
411        if ((entries != null) && (entries.length > 0)) {
412            for (TFile entry : entries) {
413                if (entry.isDirectory()) {
414                    count += countFiles(entry);
415                } else {
416                    count++;
417                }
418            }
419        }
420        return count;
421    }
422
423
424    private static class ValidationHandler extends CMDIValidatorHandlerAdapter {
425        private final boolean verbose;
426        private long started     = System.currentTimeMillis();
427        private long finished    = -1;
428        private int filesTotal   = 0;
429        private int filesInvalid = 0;
430        private long totalBytes  = 0;
431        private boolean isCompleted = false;
432        private final Object waiter = new Object();
433
434
435        private ValidationHandler(boolean verbose) {
436            this.verbose = verbose;
437        }
438
439
440        public long getTimeStarted() {
441            return started;
442        }
443
444
445        public long getTimeElapsed() {
446            long duration = (finished != -1)
447                    ? (finished - started)
448                    : (System.currentTimeMillis() - started);
449            return TimeUnit.MILLISECONDS.toSeconds(duration);
450        }
451
452
453        public int getTotalFileCount() {
454            return filesTotal;
455        }
456
457
458        public int getValidFileCount() {
459            return filesTotal - filesInvalid;
460        }
461
462        public int getInvalidFileCount() {
463            return filesInvalid;
464        }
465
466
467        public float getFailureRate() {
468            return (filesTotal > 0)
469                    ? ((float) filesInvalid / (float) filesTotal)
470                    : 0.0f;
471        }
472
473
474        public long getTotalBytes() {
475            return totalBytes;
476        }
477
478
479        public boolean await(long timeout) throws InterruptedException {
480            synchronized (waiter) {
481                if (isCompleted) {
482                    return true;
483                }
484                if (timeout > 0) {
485                    waiter.wait(timeout);
486                } else {
487                    waiter.wait();
488                }
489                return isCompleted;
490            }
491        }
492
493
494        @Override
495        public void onJobStarted() throws CMDIValidatorException {
496            logger.debug("validation process started");
497        }
498
499
500        @Override
501        public void onJobFinished(final CMDIValidator.Result result)
502                throws CMDIValidatorException {
503            finished = System.currentTimeMillis();
504            switch (result) {
505            case OK:
506                logger.debug("validation process finished successfully");
507                break;
508            case ABORTED:
509                logger.info("processing was aborted");
510                break;
511            case ERROR:
512                logger.debug("validation process yielded an error");
513                break;
514            default:
515                logger.debug("unknown result: " + result);
516            } // switch
517
518            synchronized (waiter) {
519                isCompleted = true;
520                waiter.notifyAll();
521            } // synchronized (waiter)
522        }
523
524
525        @Override
526        public void onValidationSuccess(final CMDIValidatorResult result)
527                throws CMDIValidatorException {
528            final File file = result.getFile();
529            synchronized (this) {
530                filesTotal++;
531                if (file != null) {
532                    totalBytes += file.length();
533                }
534            } // synchronized (this) {
535
536            if (result.isHighestSeverity(Severity.WARNING)) {
537                if (verbose) {
538                    logger.warn("file '{}' is valid (with warnings):", file);
539                    for (Message msg : result.getMessages()) {
540                        if ((msg.getLineNumber() != -1) &&
541                                (msg.getColumnNumber() != -1)) {
542                            logger.warn(" ({}) {} [line={}, column={}]",
543                                    msg.getSeverity().getShortcut(),
544                                    msg.getMessage(),
545                                    msg.getLineNumber(),
546                                    msg.getColumnNumber());
547                        } else {
548                            logger.warn(" ({}) {}",
549                                    msg.getSeverity().getShortcut(),
550                                    msg.getMessage());
551                        }
552                    }
553                } else {
554                    Message msg = result.getFirstMessage(Severity.WARNING);
555                    int count   = result.getMessageCount(Severity.WARNING);
556                    if (count > 1) {
557                        logger.warn("file '{}' is valid (with warnings): {} ({} more warnings)",
558                                file, msg.getMessage(), (count - 1));
559                    } else {
560                        logger.warn("file '{}' is valid (with warnings): {}",
561                                file, msg.getMessage());
562                    }
563                }
564            } else {
565                logger.debug("file '{}' is valid", file);
566            }
567        }
568
569
570        @Override
571        public void onValidationFailure(final CMDIValidatorResult result) {
572            final File file = result.getFile();
573            synchronized (this) {
574                filesTotal++;
575                filesInvalid++;
576                if (file != null) {
577                    totalBytes += file.length();
578                }
579            } // synchronized (this)
580
581            if (verbose) {
582            logger.error("file '{}' is invalid:", file);
583                for (Message msg : result.getMessages()) {
584                    if ((msg.getLineNumber() != -1) &&
585                            (msg.getColumnNumber() != -1)) {
586                        logger.error(" ({}) {} [line={}, column={}]",
587                                msg.getSeverity().getShortcut(),
588                                msg.getMessage(),
589                                msg.getLineNumber(),
590                                msg.getColumnNumber());
591                    } else {
592                        logger.error(" ({}) {}",
593                                msg.getSeverity().getShortcut(),
594                                msg.getMessage());
595                    }
596                }
597            } else {
598                Message msg = result.getFirstMessage(Severity.ERROR);
599                int count   = result.getMessageCount(Severity.ERROR);
600                if (count > 1) {
601                    logger.error("file '{}' is invalid: {} ({} more errors)",
602                            file, msg.getMessage(), (count - 1));
603                } else {
604                    logger.error("file '{}' is invalid: {}",
605                            file, msg.getMessage());
606                }
607            }
608        }
609    } // class JobHandler
610
611
612    static {
613        appender = new org.apache.log4j.ConsoleAppender(
614                new org.apache.log4j.PatternLayout("%m%n"),
615                org.apache.log4j.ConsoleAppender.SYSTEM_OUT);
616        org.apache.log4j.BasicConfigurator.configure(appender);
617        org.apache.log4j.Logger logger =
618                org.apache.log4j.Logger.getRootLogger();
619        logger.setLevel(org.apache.log4j.Level.WARN);
620    }
621
622} // class CMDIValidatorTool
Note: See TracBrowser for help on using the repository browser.