package org.codefx.mvn.jdeps.parse; import org.codefx.mvn.jdeps.dependency.InternalType; import org.codefx.mvn.jdeps.dependency.Type; import org.codefx.mvn.jdeps.dependency.Violation; import org.codefx.mvn.jdeps.dependency.Violation.ViolationBuilder; import org.codefx.mvn.jdeps.mojo.MojoLogging; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import static java.lang.String.format; /** * Parses violation blocks from the JDeps output line by line and hands created {@link Violation}s to a * {@link Consumer} that can further process it. */ public class ViolationParser { public static final String MESSAGE_MARKER_JDEPS_LINE = "[d]"; public static final String MESSAGE_MARKER_UNKNOWN_LINE = "[ ]"; private static final String MESSAGE_PARSED_LINE = " %s %s"; /** * Pattern to match the reported type line, e.g. * * <pre> * org.codefx.lab.App (...) * </pre> */ private static final Pattern REPORTED_TYPE_PATTERN = Pattern.compile("" + "\\s+" // leading spaces + "([a-zA-Z_][\\.\\w]*)" // qualified class name (simplified), e.g. "sun.misc.Unsafe" + "\\s+" // spaces to separate class name + ".*"); private final InternalTypeLineParser internalTypeLineParser; private final Consumer<Violation> violationConsumer; private LineParserState lineParser; /** * Creates a new parser. * * @param violationConsumer * the {@link Consumer} to which parsed {@link Violation}s are handed over */ public ViolationParser(Consumer<Violation> violationConsumer) { this(new InternalTypeLineParser(), violationConsumer); } /** * Creates a new parser. * * @param internalTypeLineParser * used to parse individual internal dependencies * @param violationConsumer * the {@link Consumer} to which parsed {@link Violation}s are handed over */ public ViolationParser(InternalTypeLineParser internalTypeLineParser, Consumer<Violation> violationConsumer) { Objects.requireNonNull(internalTypeLineParser, "The argument 'internalTypeLineParser' must not be null."); Objects.requireNonNull(violationConsumer, "The argument 'violationConsumer' must not be null."); this.internalTypeLineParser = internalTypeLineParser; this.violationConsumer = violationConsumer; this.lineParser = new NoBlock(); } // #begin PARSE SUPPORT /** * Parses the specified line. * <p> * As soon as a new {@link Violation} is created it is handed to the {@link Consumer} specified during * construction. * * @param line * the line to parse */ public void parseLine(String line) { Objects.requireNonNull(line, "The argument 'line' must not be null."); lineParser = lineParser.parseLine(line); lineParser.logLine(line); } /** * Informs the parser that parsing is done (for now). * <p> * If a violation is currently being created, calling this method will build it. */ public void finish() { lineParser = lineParser.parseLine(""); } private LineParserState determineWhetherNewBlockStarted(String line) { Optional<String> asFirstBlockLine = parseAsFirstBlockLine(line); if (asFirstBlockLine.isPresent()) return new BlockBegan(asFirstBlockLine.get()); else return new NoBlock(); } private static Optional<String> parseAsFirstBlockLine(String line) { Matcher firstLineMatcher = REPORTED_TYPE_PATTERN.matcher(line); boolean isFirstLine = firstLineMatcher.matches(); if (isFirstLine) return Optional.of(firstLineMatcher.group(1)); else return Optional.empty(); } // #end PARSE SUPPORT // #begin PARSE STATE MACHINE private interface LineParserState { LineParserState parseLine(String line); void logLine(String line); } /** * There is currently no violations block. * <p> * The next line may start a new block if it is a {@link #REPORTED_TYPE_PATTERN REPORTED_TYPE}. */ private class NoBlock implements LineParserState { @Override public LineParserState parseLine(String line) { return determineWhetherNewBlockStarted(line); } @Override public void logLine(String line) { MojoLogging.logger().debug(format(MESSAGE_PARSED_LINE, MESSAGE_MARKER_UNKNOWN_LINE, line)); } } /** * A block began and a violation is being build. * <p> * The block can either be continued with an internal dependency or may end. If it ends: * <ul> * <li>the violation which is currently being build is finished and handed to the {@link #violationConsumer} * <li>a new block might start with the next line ~> transition to new {@link BlockBegan} * <li>some other lines might occur ~> transition to {@link NoBlock} * </ul> */ private class BlockBegan implements LineParserState { private final ViolationBuilder violationBuilder; public BlockBegan(String fullyQualifiedClassName) { assert fullyQualifiedClassName != null : "The argument 'fullyQualifiedClassName' must not be null."; Type dependent = Type.of(fullyQualifiedClassName); violationBuilder = Violation.buildForDependent(dependent); } @Override public LineParserState parseLine(String line) { assert line != null : "The argument 'line' must not be null."; boolean lineCouldBeProcessed = processLine(line); return computeNextState(line, lineCouldBeProcessed); } private boolean processLine(String line) { Optional<InternalType> parsedInternalType = internalTypeLineParser.parseLine(line); parsedInternalType.ifPresent(violationBuilder::addDependency); return parsedInternalType.isPresent(); } private LineParserState computeNextState(String line, boolean lineCouldBeProcessed) { if (lineCouldBeProcessed) return this; else { finishViolation(); return determineWhetherNewBlockStarted(line); } } private void finishViolation() { Violation violation = violationBuilder.build(); violationConsumer.accept(violation); } @Override public void logLine(String line) { MojoLogging.logger().debug(format(MESSAGE_PARSED_LINE, MESSAGE_MARKER_JDEPS_LINE, line)); } } // #end PARSE STATE MACHINE }