All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.mmm.code.impl.java.parser.JavaSourceCodeReaderLowlevel Maven / Gradle / Ivy

There is a newer version: 0.9.10
Show newest version
/* Copyright (c) The m-m-m Team, Licensed under the Apache License, Version 2.0
 * http://www.apache.org/licenses/LICENSE-2.0 */
package io.github.mmm.code.impl.java.parser;

import java.io.Reader;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.github.mmm.base.filter.CharFilter;
import io.github.mmm.base.text.TextFormatMessageType;
import io.github.mmm.code.api.CodeName;
import io.github.mmm.code.api.annotation.CodeAnnotation;
import io.github.mmm.code.api.comment.CodeComment;
import io.github.mmm.code.api.expression.CodeExpression;
import io.github.mmm.code.api.imports.CodeImport;
import io.github.mmm.code.api.modifier.CodeModifiers;
import io.github.mmm.code.api.modifier.CodeVisibility;
import io.github.mmm.code.api.operator.CodeNAryOperator;
import io.github.mmm.code.api.operator.CodeOperator;
import io.github.mmm.code.base.BaseFile;
import io.github.mmm.code.base.annoation.BaseAnnotation;
import io.github.mmm.code.base.comment.BaseBlockComment;
import io.github.mmm.code.base.comment.BaseComments;
import io.github.mmm.code.base.comment.BaseSingleLineComment;
import io.github.mmm.code.base.expression.BaseArrayInstatiation;
import io.github.mmm.code.base.expression.BaseFieldReferenceLazy;
import io.github.mmm.code.base.expression.BaseMethodInvocation;
import io.github.mmm.code.base.member.BaseMethod;
import io.github.mmm.code.base.operator.BaseOperator;
import io.github.mmm.code.impl.java.expression.JavaNAryOperatorExpression;
import io.github.mmm.code.impl.java.expression.literal.JavaLiteral;
import io.github.mmm.code.impl.java.expression.literal.JavaLiteralBoolean;
import io.github.mmm.code.impl.java.expression.literal.JavaLiteralChar;
import io.github.mmm.code.impl.java.expression.literal.JavaLiteralString;
import io.github.mmm.scanner.CharReaderScanner;

/**
 * Wrapper for a {@link Reader} with internal char buffer to read and parse textual data.
 *
 * @author Joerg Hohwiller (hohwille at users.sourceforge.net)
 * @since 1.0.0
 */
public abstract class JavaSourceCodeReaderLowlevel extends CharReaderScanner {

  private static final Logger LOG = LoggerFactory.getLogger(JavaSourceCodeReaderLowlevel.class);

  static final CharFilter CHAR_FILTER_SPACES = c -> ((c == ' ') || (c == '\t'));

  static final CharFilter CHAR_FILTER_IDENTIFIER = c -> Character.isJavaIdentifierPart(c);

  static final CharFilter CHAR_FILTER_QNAME = c -> Character.isJavaIdentifierPart(c) || (c == '.');

  static final CharFilter CHAR_FILTER_ANNOTATION_KEY = c -> ((c == '{') || (c == '=') || (c == ','));

  static final CharFilter CHAR_FILTER_OPERATOR = c -> ((c == '+') || (c == '-') || (c == '*') || (c == '/')
      || (c == '^') || (c == '%') || (c == '>') || (c == '<') || (c == '!') || (c == '~') || (c == '='));

  static final CharFilter CHAR_FILTER_NUMBER_LITERAL_START = c -> ((c >= '0') && (c <= '9') || (c == '+')
      || (c == '-'));

  static final CharFilter CHAR_FILTER_STATEMENT_END = c -> ((c == ';') || (c == '\r') || (c == '\n'));

  /** {@link List} of plain JavaDoc lines collected whilst parsing. */
  protected final List javaDocLines;

  /** {@link List} of {@link CodeComment}s collected whilst parsing. */
  protected final List comments;

  /** @see #getElementComment() */
  protected CodeComment elementComment;

  /** {@link #getAnnotations()} */
  protected final List annotations;

  /** The current {@link BaseFile} to parse. */
  protected BaseFile file;

  /**
   * The constructor.
   */
  public JavaSourceCodeReaderLowlevel() {

    this(4096);
  }

  /**
   * The constructor.
   *
   * @param capacity the buffer capacity.
   */
  public JavaSourceCodeReaderLowlevel(int capacity) {

    super(capacity);
    this.javaDocLines = new ArrayList<>();
    this.comments = new ArrayList<>();
    this.annotations = new ArrayList<>();
  }

  @Override
  protected void reset() {

    super.reset();
    clearConsumeState();
    this.file = null;
  }

  /**
   * Clears all collected comments, annotations and javaDocs.
   */
  protected void clearConsumeState() {

    this.javaDocLines.clear();
    this.comments.clear();
    this.elementComment = null;
    this.annotations.clear();
  }

  /**
   * @return the (last) comments that have been parsed by the last invocation of {@link #consume()}. Will be
   *         {@link List#isEmpty() empty} for none.
   * @see #getElementComment()
   */
  public List getComments() {

    return this.comments;
  }

  /**
   * @return the {@link CodeComment} for the currently parsed "element" (type, member, etc.) parsed by the last
   *         invocation of {@link #consume()}.
   */
  public CodeComment getElementComment() {

    if (this.elementComment == null) {
      int size = this.comments.size();
      if (size == 1) {
        this.elementComment = this.comments.get(0);
      } else if (size > 1) {
        this.elementComment = new BaseComments(new ArrayList<>(this.comments));
      }
      this.comments.clear();
    }
    return this.elementComment;
  }

  /**
   * @return the plain JavaDoc lines that have been parsed by the last invocation of {@link #consume()}. Will be
   *         {@link List#isEmpty() empty} for no JavaDoc.
   */
  public List getJavaDocLines() {

    return this.javaDocLines;
  }

  /**
   * @return the {@link List} of {@link CodeAnnotation}s that have been parsed by the last invocation of
   *         {@link #consume()}.
   */
  public List getAnnotations() {

    return this.annotations;
  }

  /**
   * Consumes all standard text like spaces, comments, JavaDoc and annotations
   */
  public void consume() {

    // clearConsumeState();
    skipWhile(CharFilter.WHITESPACE);
    char c = peek();
    if (c == '/') { // comment or doc?
      next();
      parseDocOrComment();
      consume();
    } else if (c == '@') { // annotation?
      next();
      getElementComment();
      parseAnnotations();
      consume();
    }
  }

  /**
   * Skips all whitespaces and parses all {@link CodeComment}s.
   */
  protected void parseWhitespacesAndComments() {

    skipWhile(CharFilter.WHITESPACE);
    if (expectOne('/')) {
      parseDocOrComment();
    }
  }

  private CodeComment getAndClearComments() {

    CodeComment comment = null;
    int commentCount = this.comments.size();
    if (commentCount > 0) {
      if (commentCount == 1) {
        comment = this.comments.get(0);
      } else {
        comment = new BaseComments(new ArrayList<>(this.comments));
      }
      this.comments.clear();
    }
    return comment;
  }

  /**
   * @return the current identifier or {@code null} if not pointing to such.
   */
  protected String parseIdentifier() {

    if (Character.isJavaIdentifierStart(peek())) {
      return readWhile(CHAR_FILTER_IDENTIFIER);
    }
    return null;
  }

  /**
   * @return the current (qualified) name or {@code null} if not pointing to such.
   */
  protected String parseQName() {

    if (Character.isJavaIdentifierStart(peek())) {
      return readWhile(CHAR_FILTER_QNAME);
    }
    return null;
  }

  private void parseAnnotations() {

    String annotationTypeName = parseQName();
    String annotationQName = getQualifiedName(annotationTypeName);
    CodeAnnotation annotation = new BaseAnnotation(this.file.getAnnotations(), annotationTypeName, annotationQName);
    if (expectOne('(')) {
      parseAnnotationParameters(annotation, annotationTypeName);
    }
    CodeComment comment = getAndClearComments();
    if (comment != null) {
      annotation.setComment(comment);
    }
    this.annotations.add(annotation);
  }

  private void parseAnnotationParameters(CodeAnnotation annotation, String annotationTypeName) {

    parseWhitespacesAndComments();
    if (expectOne(')')) {
      return;
    }
    Map parameters = annotation.getParameters();
    boolean first = true;
    while (!expectOne(')')) {
      if (!first) {
        boolean comma = expectOne(',');
        if (!comma) {
          LOG.warn("Annotation {} parameters not separated with comma in {}.", annotationTypeName, this.file);
        }
      }
      CodeExpression value = null;
      String key = readUntil(CHAR_FILTER_ANNOTATION_KEY, false, ")", false, true);
      if (key.isEmpty()) {
        if (first) {
          key = "value"; // Java build in default
        } else {
          LOG.warn("Annotation {} parameter without name (found '{}') in {}.", annotationTypeName,
              Character.toString(peek()), this.file);
        }
      } else {
        parseWhitespacesAndComments();
        if (!expectOne('=')) {
          for (CodeImport importStatement : this.file.getImports()) {
            if (importStatement.isStatic()) {
              String reference = importStatement.getReference();
              int index = reference.length() - key.length() - 1;
              if (reference.endsWith(key) && (index > 0)) {
                if ((reference.charAt(index) == '.')) {
                  String fieldName = key;
                  key = "value";
                  String typeName = reference.substring(0, index);
                  value = new BaseFieldReferenceLazy(this.file.getContext(), typeName, null, fieldName);
                  break;
                }
              }
            }
          }
          if (value == null) {
            LOG.warn("Invalid character '{}' after annotation parameter {}.{} name in {}.", Character.toString(peek()),
                annotationTypeName, key, this.file);
          }
        }
      }
      if (value == null) {
        if (expectOne('{')) {
          List args = new ArrayList<>();
          CodeExpression arg;
          do {
            arg = parseAssignmentValue();
            if (arg != null) {
              args.add(arg);
            }
          } while ((arg != null) && expectOne(','));
          if (!expectOne('}')) {
            LOG.warn("Invalid annotation array value - missing closing curly brace '}' for annotation {} at value {}",
                annotationTypeName, key);
          }
          value = new BaseArrayInstatiation(args);
        } else {
          value = parseAssignmentValue();
        }
      }
      parameters.put(key, value);
      first = false;
    }

  }

  CodeExpression parseAssignmentValue() {

    parseWhitespacesAndComments();
    CodeExpression expression = parseSingleAssignmentValue();
    CodeOperator operator = null;
    List expressions = null;
    while (true) {
      parseWhitespacesAndComments();
      char c = peek();
      if (CHAR_FILTER_OPERATOR.accept(c)) {
        String operatorName = readWhile(CHAR_FILTER_OPERATOR);
        CodeOperator nextOperator = BaseOperator.of(operatorName);
        if (operator == null) {
          operator = nextOperator;
          if (expressions == null) {
            expressions = new ArrayList<>();
          }
          expressions.add(expression);
        } else if (operator != nextOperator) {
          if (operator.isNAry()) {
            expression = new JavaNAryOperatorExpression((CodeNAryOperator) operator, new ArrayList<>(expressions));
            Objects.requireNonNull(expressions);
            expressions.clear();
            expressions.add(expression);
          }
          operator = nextOperator;
        }

      } else {
        break;
      }
    }
    return expression;
  }

  private CodeExpression parseSingleAssignmentValue() {

    CodeExpression expression = parseLiteral();
    if (expression != null) {
      return expression;
    }
    String qName = parseQName();
    parseWhitespacesAndComments();
    if (expectOne('(')) {
      List arguments = new ArrayList<>();
      CodeExpression arg = parseSingleAssignmentValue();
      if (arg != null) {
        arguments.add(arg);
        parseWhitespacesAndComments();
        while (expectOne(',')) {
          arg = parseSingleAssignmentValue();
          if (arg == null) {
            LOG.debug("Missing argument after ','");
            break;
          }
          arguments.add(arg);
          parseWhitespacesAndComments();
        }
      }
      if (!expectOne(')')) {
        LOG.debug("Missing ')'");
      }
      CodeName codeName = new CodeName(qName, '.');
      CodeName parent = codeName.getParent();
      if (parent != null) {

      }
      BaseMethod method = null;
      expression = new BaseMethodInvocation(method, arguments);
    } else {

    }
    // TODO parse constants, static method references, etc.
    return expression;
  }

  private JavaLiteral parseLiteral() {

    String stringLiteral = readJavaStringLiteral(TextFormatMessageType.WARNING);
    if (stringLiteral != null) {
      return JavaLiteralString.of(stringLiteral);
    }
    Boolean booleanLiteral = readBoolean();
    if (booleanLiteral != null) {
      return JavaLiteralBoolean.of(booleanLiteral.booleanValue());
    }
    Character charLiteral = readJavaCharLiteral(TextFormatMessageType.WARNING);
    if (charLiteral != null) {
      return JavaLiteralChar.of(charLiteral);
    }
    Number numberLiteral = readJavaNumberLiteral();
    if (numberLiteral != null) {
      return JavaLiteral.of(numberLiteral);
    }
    return null;
  }

  private String getQualifiedName(String name) {

    if (name.indexOf('.') == -1) {
      return this.file.getContext().getQualifiedName(name, this.file, false);
    }
    return name;
  }

  private void parseDocOrComment() {

    char c = peek();
    if (c == '/') {
      String docLine = readLine(true);
      this.comments.add(new BaseSingleLineComment(docLine));
    } else if (c == '*') {
      next();
      c = peek();
      if (c == '*') { // JavaDoc or regular comment
        next();
        if (!this.javaDocLines.isEmpty()) {
          LOG.warn("Duplicate JavaDoc in {}.", this.file);
        }
        parseDocOrBlockComment(this.javaDocLines);
      } else {
        List lines = new ArrayList<>();
        parseDocOrBlockComment(lines);
        BaseBlockComment comment = new BaseBlockComment(lines);
        this.comments.add(comment);
      }
    } else {
      LOG.warn("Illegal language: {} in {}.", "/" + c, this.file);
    }
  }

  private void parseDocOrBlockComment(List lines) {

    String docLine = readDocOrCommentLine();
    if (!docLine.isEmpty()) {
      lines.add(docLine);
    }
    if (expect("*/")) {
      return;
    }
    while (true) {
      skipWhile(CharFilter.WHITESPACE);
      char c = peek();
      if (c == '*') {
        next();
        c = peek();
        if (c == '/') {
          next();
          skipWhile(CharFilter.WHITESPACE);
          return;
        }
      }
      docLine = readDocOrCommentLine();
      lines.add(docLine);
    }
  }

  private String readDocOrCommentLine() {

    expectOne(' ');
    String docLine = readUntil(CharFilter.NEWLINE, true, "*/", false, false);
    // trim end
    int max = docLine.length() - 1;
    int end = max;
    while ((end > 0) && (docLine.charAt(end) == ' ')) {
      end--;
    }
    if (end < max) {
      docLine = docLine.substring(0, end + 1);
    }
    return docLine;
  }

  /**
   * @param inInterface - {@code true} if in the context of an interface (where public is the default), {@code false}
   *        otherwise.
   * @return the parsed {@link CodeModifiers}.
   */
  protected CodeModifiers parseModifiers(boolean inInterface) {

    CodeVisibility visibility = parseVisibility(null);
    Set modifiers = new HashSet<>();
    boolean found = true;
    while (found) {
      skipWhile(CharFilter.WHITESPACE);
      char c = peek();
      if (c == 'a') {
        found = parseModifierKeyword(modifiers, CodeModifiers.KEY_ABSTRACT);
      } else if (c == 'd') {
        found = parseModifierKeyword(modifiers, CodeModifiers.KEY_DEFAULT);
      } else if (c == 'n') {
        found = parseModifierKeyword(modifiers, CodeModifiers.KEY_NATIVE);
      } else if (c == 'f') {
        found = parseModifierKeyword(modifiers, CodeModifiers.KEY_FINAL);
      } else if (c == 'v') {
        found = parseModifierKeyword(modifiers, CodeModifiers.KEY_VOLATILE);
      } else if (c == 's') {
        found = parseModifierKeyword(modifiers, CodeModifiers.KEY_STATIC);
        if (!found) {
          found = parseModifierKeyword(modifiers, CodeModifiers.KEY_SYNCHRONIZED);
          if (!found) {
            found = parseModifierKeyword(modifiers, CodeModifiers.KEY_SYNCHRONIZED);
          }
        }
      } else if (c == 't') {
        found = parseModifierKeyword(modifiers, CodeModifiers.KEY_TRANSIENT);
      } else {
        found = false;
      }
      if (visibility == null) {
        visibility = parseVisibility(null);
        if (!found) {
          found = (visibility != null);
        }
      }
    }
    if (visibility == null) {
      visibility = getVisibilityFallback(inInterface);
    }
    return new CodeModifiers(visibility, modifiers);
  }

  private boolean parseModifierKeyword(Set modifiers, String modifier) {

    if (expect(modifier)) {
      modifiers.add(modifier);
      return true;
    }
    return false;
  }

  private CodeVisibility getVisibilityFallback(boolean inInterface) {

    if (inInterface) {
      return CodeVisibility.PUBLIC;
    } else {
      return CodeVisibility.DEFAULT;
    }
  }

  private CodeVisibility parseVisibility(CodeVisibility fallback) {

    // private, protected, public
    // static final volatitle strictfp
    // class enum interface @interface
    if (peek() != 'p') {
      return fallback;
    }
    if (expect("private")) {
      return CodeVisibility.PRIVATE;
    } else if (expect("public")) {
      return CodeVisibility.PUBLIC;
    } else if (expect("protected")) {
      return CodeVisibility.PROTECTED;
    }
    return fallback;
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy