From 858dafeb7dbb9d65f48c25b3b83f339a96595ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A1ndor=20Holozsny=C3=A1k?= Date: Sun, 5 Feb 2023 21:27:18 +0100 Subject: [PATCH] feat(core): Java method and field extraction with the 'javasource' include macro --- .github/workflows/ci-build.yml | 2 +- .github/workflows/release.yml | 3 +- .sdkmanrc | 2 +- README.adoc | 4 +- pom.xml | 35 +++-- src/main/java/org/rodnansol/.gitkeep | 0 src/main/java/org/rodnansol/SingleModule.java | 9 -- .../asciidoctorj/CodeBlockProcessor.java | 24 ++++ .../asciidoctorj/ExtractCommand.java | 50 +++++++ .../asciidoctorj/ExtractFieldCommand.java | 42 ++++++ .../asciidoctorj/ExtractMethodCommand.java | 53 ++++++++ .../JavaFieldCodeBlockProcessor.java | 42 ++++++ .../JavaMethodCodeBlockProcessor.java | 44 ++++++ .../JavaMethodIncludeProcessor.java | 83 ++++++++++++ .../JavaSourceBlockMacroProcessor.java | 44 ++++++ .../JavaSourceCodeExtractionException.java | 18 +++ .../asciidoctorj/JavaSourceExtension.java | 22 +++ .../asciidoctorj/JavaSourceHelper.java | 125 ++++++++++++++++++ ...ctor.jruby.extension.spi.ExtensionRegistry | 1 + .../java/org/rodnansol/SingleModuleTest.java | 19 --- .../asciidoctorj/JavaSourceHelperTest.java | 77 +++++++++++ src/test/resources/UserService.java | 49 +++++++ 22 files changed, 706 insertions(+), 42 deletions(-) delete mode 100644 src/main/java/org/rodnansol/.gitkeep delete mode 100644 src/main/java/org/rodnansol/SingleModule.java create mode 100644 src/main/java/org/rodnansol/asciidoctorj/CodeBlockProcessor.java create mode 100644 src/main/java/org/rodnansol/asciidoctorj/ExtractCommand.java create mode 100644 src/main/java/org/rodnansol/asciidoctorj/ExtractFieldCommand.java create mode 100644 src/main/java/org/rodnansol/asciidoctorj/ExtractMethodCommand.java create mode 100644 src/main/java/org/rodnansol/asciidoctorj/JavaFieldCodeBlockProcessor.java create mode 100644 src/main/java/org/rodnansol/asciidoctorj/JavaMethodCodeBlockProcessor.java create mode 100644 src/main/java/org/rodnansol/asciidoctorj/JavaMethodIncludeProcessor.java create mode 100644 src/main/java/org/rodnansol/asciidoctorj/JavaSourceBlockMacroProcessor.java create mode 100644 src/main/java/org/rodnansol/asciidoctorj/JavaSourceCodeExtractionException.java create mode 100644 src/main/java/org/rodnansol/asciidoctorj/JavaSourceExtension.java create mode 100644 src/main/java/org/rodnansol/asciidoctorj/JavaSourceHelper.java create mode 100644 src/main/resources/META-INF/services/org.asciidoctor.jruby.extension.spi.ExtensionRegistry delete mode 100644 src/test/java/org/rodnansol/SingleModuleTest.java create mode 100644 src/test/java/org/rodnansol/asciidoctorj/JavaSourceHelperTest.java create mode 100644 src/test/resources/UserService.java diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 4cbf39a..89a1177 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -21,7 +21,7 @@ jobs: - name: install-java uses: actions/setup-java@v3 with: - java-version: 8 + java-version: 11 distribution: temurin cache: maven - name: Build & Verify Project diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c80846..ea97094 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,14 +33,13 @@ jobs: - name: Setup Java uses: actions/setup-java@v3 with: - java-version: 8 + java-version: 11 distribution: 'temurin' cache: 'maven' - name: Stage Files for Release run: | mvn versions:set -DnewVersion=$RELEASE_VERSION mvn -Prelease deploy -DaltDeploymentRepository=local::file:./target/staging-deploy - ./scripts/jbang-version-release.sh $RELEASE_VERSION env: RELEASE_VERSION: ${{ inputs.releaseVersion }} - name: Run JReleaser - Full Release diff --git a/.sdkmanrc b/.sdkmanrc index a06c0ff..426abb9 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,3 +1,3 @@ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below -java=8.0.332-tem +java=11.0.10-open diff --git a/README.adoc b/README.adoc index 4afc2f7..190d226 100644 --- a/README.adoc +++ b/README.adoc @@ -1,4 +1,4 @@ -= Single Module Project += AsciiDoctorJ Extensions ifndef::env-github[] :icons: font endif::[] @@ -14,7 +14,7 @@ endif::[] :toclevels: 4 [.text-center] -image:https://img.shields.io/maven-central/v/org.rodnansol/single-module-project.svg[Maven Central] +image:https://img.shields.io/maven-central/v/org.rodnansol/asciidoctorj-extensions.svg[Maven Central] image:https://img.shields.io/badge/License-Apache_2.0-blue.svg[Apache 2.0] image:https://img.shields.io/twitter/url/https/twitter.com/rodnansol.svg?style=social&label=Follow%20%40RodnanSol[] image:https://dcbadge.vercel.app/api/server/USyh6XUjvP[Discord] diff --git a/pom.xml b/pom.xml index 9be3980..8267ac1 100644 --- a/pom.xml +++ b/pom.xml @@ -5,12 +5,12 @@ 4.0.0 org.rodnansol - single-module-project + asciidoctorj-extensions 999-SNAPSHOT - 1.8 - 1.8 + 11 + 11 Apache-2.0 UTF-8 3.10.1 @@ -42,11 +42,14 @@ 3.3.0 false ALWAYS + 2.28.0.Final + 2.5.7 + 3.31.0 - Single Module Project - Single Module Project Description - https://github.com/rodnansol/single-module-project + AsciiDoctorJ Extensions + AsciiDoctorJ Extensions + https://github.com/rodnansol/asciidoctorj-extensions 2023 @@ -69,7 +72,7 @@ scm:git:git@${project.scm.url} scm:git:${project.scm.url} - git@github.com:rodnansol/single-module-project.git + git@github.com:rodnansol/asciidoctorj-extensions.git HEAD @@ -86,6 +89,22 @@ + + org.asciidoctor + asciidoctorj + ${asciidoctorj.version} + + + org.jboss.forge.roaster + roaster-api + ${roaster.version} + + + org.jboss.forge.roaster + roaster-jdt + ${roaster.version} + compile + ch.qos.logback logback-classic @@ -286,7 +305,7 @@ release https://s01.oss.sonatype.org - #tags + #asciidoctorj #java diff --git a/src/main/java/org/rodnansol/.gitkeep b/src/main/java/org/rodnansol/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/org/rodnansol/SingleModule.java b/src/main/java/org/rodnansol/SingleModule.java deleted file mode 100644 index 103938e..0000000 --- a/src/main/java/org/rodnansol/SingleModule.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.rodnansol; - -public class SingleModule { - - public int sum(int a, int b) { - return a + b; - } - -} diff --git a/src/main/java/org/rodnansol/asciidoctorj/CodeBlockProcessor.java b/src/main/java/org/rodnansol/asciidoctorj/CodeBlockProcessor.java new file mode 100644 index 0000000..24e503a --- /dev/null +++ b/src/main/java/org/rodnansol/asciidoctorj/CodeBlockProcessor.java @@ -0,0 +1,24 @@ +package org.rodnansol.asciidoctorj; + +import org.asciidoctor.ast.Document; +import org.asciidoctor.extension.PreprocessorReader; + +import java.util.Map; + +/** + * Interface describes a block processor blueprint that can handle different attributes from the 'javasource' include macro. + * + * @author nandorholozsnyak + * @since 0.1.0 + */ +public interface CodeBlockProcessor { + + boolean isActive(Map attributes); + + void process(ExtractCommand extractCommand, + Document document, + PreprocessorReader reader, + String target, + Map attributes); + +} diff --git a/src/main/java/org/rodnansol/asciidoctorj/ExtractCommand.java b/src/main/java/org/rodnansol/asciidoctorj/ExtractCommand.java new file mode 100644 index 0000000..4b8fc74 --- /dev/null +++ b/src/main/java/org/rodnansol/asciidoctorj/ExtractCommand.java @@ -0,0 +1,50 @@ +package org.rodnansol.asciidoctorj; + +import java.util.Objects; + +/** + * Class that wraps an extraction command. + * + * @author nandorholozsnyak + * @since 0.1.0 + */ +public class ExtractCommand { + + private final String sourceCodePath; + private final int spaceSize; + private final boolean withJavaDoc; + private final int lineLength; + + public ExtractCommand(String sourceCodePath, int spaceSize, boolean withJavaDoc, int lineLength) { + this.sourceCodePath = Objects.requireNonNull(sourceCodePath, "sourceCodePath is NULL"); + this.spaceSize = spaceSize; + this.withJavaDoc = withJavaDoc; + this.lineLength = lineLength; + } + + public String getSourceCodePath() { + return sourceCodePath; + } + + public int getSpaceSize() { + return spaceSize; + } + + public boolean isWithJavaDoc() { + return withJavaDoc; + } + + @Override + public String toString() { + return "ExtractCommand{" + + "sourceCodePath='" + sourceCodePath + '\'' + + ", spaceSize=" + spaceSize + + ", withJavaDoc=" + withJavaDoc + + ", lineLength=" + lineLength + + '}'; + } + + public int getLineLength() { + return lineLength; + } +} diff --git a/src/main/java/org/rodnansol/asciidoctorj/ExtractFieldCommand.java b/src/main/java/org/rodnansol/asciidoctorj/ExtractFieldCommand.java new file mode 100644 index 0000000..fd8f974 --- /dev/null +++ b/src/main/java/org/rodnansol/asciidoctorj/ExtractFieldCommand.java @@ -0,0 +1,42 @@ +package org.rodnansol.asciidoctorj; + +import java.util.Objects; + +/** + * Class that wraps parameters for field extraction. + * + * @author nandorholozsnyak + * @since 0.1.0 + */ +public class ExtractFieldCommand extends ExtractCommand { + + private final String fieldName; + + public ExtractFieldCommand(ExtractCommand extractCommand, String fieldName) { + super(extractCommand.getSourceCodePath(), extractCommand.getSpaceSize(), extractCommand.isWithJavaDoc(), extractCommand.getLineLength()); + this.fieldName = Objects.requireNonNull(fieldName, "fieldName is NULL"); + } + + /** + * @param sourceCodePath Java source file's path. + * @param fieldName field name to extract. + * @param spaceSize space size for indentation. + * @param withJavaDoc + * @param lineLength + */ + public ExtractFieldCommand(String sourceCodePath, String fieldName, int spaceSize, boolean withJavaDoc, int lineLength) { + super(sourceCodePath, spaceSize, withJavaDoc, lineLength); + this.fieldName = Objects.requireNonNull(fieldName, "fieldName is NULL"); + } + + public String getFieldName() { + return fieldName; + } + + @Override + public String toString() { + return "ExtractFieldCommand{" + + "fieldName='" + fieldName + '\'' + + "} " + super.toString(); + } +} diff --git a/src/main/java/org/rodnansol/asciidoctorj/ExtractMethodCommand.java b/src/main/java/org/rodnansol/asciidoctorj/ExtractMethodCommand.java new file mode 100644 index 0000000..780cd15 --- /dev/null +++ b/src/main/java/org/rodnansol/asciidoctorj/ExtractMethodCommand.java @@ -0,0 +1,53 @@ +package org.rodnansol.asciidoctorj; + +import java.util.Arrays; +import java.util.Objects; + +/** + * Class that wraps parameters for method extraction. + * + * @author nandorholozsnyak + * @since 0.1.0 + */ +public class ExtractMethodCommand extends ExtractCommand { + + + private final String methodName; + private final String[] paramTypes; + + public ExtractMethodCommand(ExtractCommand extractCommand, String methodName,String... paramTypes) { + super(extractCommand.getSourceCodePath(), extractCommand.getSpaceSize(), extractCommand.isWithJavaDoc(), extractCommand.getLineLength()); + this.methodName = Objects.requireNonNull(methodName, "methodName is NULL"); + this.paramTypes = paramTypes; + } + + /** + * @param sourceCodePath Java source file's path. + * @param spaceSize space size for indentation. + * @param methodName method's name that should be extracted from the source code. + * @param withJavaDoc if the JavaDoc should be included or not. + * @param lineLength + * @param paramTypes list of the method argument types in String values. + */ + public ExtractMethodCommand(String sourceCodePath, int spaceSize, String methodName, boolean withJavaDoc, int lineLength, String... paramTypes) { + super(sourceCodePath, spaceSize, withJavaDoc, lineLength); + this.methodName = Objects.requireNonNull(methodName, "methodName is NULL"); + this.paramTypes = paramTypes; + } + + public String getMethodName() { + return methodName; + } + + public String[] getParamTypes() { + return paramTypes; + } + + @Override + public String toString() { + return "ExtractMethodCommand{" + + "methodName='" + methodName + '\'' + + ", paramTypes=" + Arrays.toString(paramTypes) + + "} " + super.toString(); + } +} diff --git a/src/main/java/org/rodnansol/asciidoctorj/JavaFieldCodeBlockProcessor.java b/src/main/java/org/rodnansol/asciidoctorj/JavaFieldCodeBlockProcessor.java new file mode 100644 index 0000000..a01d146 --- /dev/null +++ b/src/main/java/org/rodnansol/asciidoctorj/JavaFieldCodeBlockProcessor.java @@ -0,0 +1,42 @@ +package org.rodnansol.asciidoctorj; + +import org.asciidoctor.ast.Document; +import org.asciidoctor.extension.PreprocessorReader; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +/** + * Code block processor that extracts the field from the given source path. + * + * @author nandorholozsnyak + * @since 0.1.0 + */ +public class JavaFieldCodeBlockProcessor implements CodeBlockProcessor { + + private static final String KEY_FIELD = "field"; + + @Override + public boolean isActive(Map attributes) { + return attributes.containsKey(KEY_FIELD); + } + + @Override + public void process(ExtractCommand extractCommand, Document document, PreprocessorReader reader, String target, Map attributes) { + String field = (String) attributes.get(KEY_FIELD); + if (field != null && !field.isEmpty()) { + try { + String fullField = JavaSourceHelper.getField(new ExtractFieldCommand(extractCommand, field)); + reader.pushInclude( + fullField, + target, + new File(".").getAbsolutePath(), + 1, + attributes); + } catch (IOException e) { + throw new JavaSourceCodeExtractionException("Unable to extract field with name:[" + field + "]", e); + } + } + } +} diff --git a/src/main/java/org/rodnansol/asciidoctorj/JavaMethodCodeBlockProcessor.java b/src/main/java/org/rodnansol/asciidoctorj/JavaMethodCodeBlockProcessor.java new file mode 100644 index 0000000..5005846 --- /dev/null +++ b/src/main/java/org/rodnansol/asciidoctorj/JavaMethodCodeBlockProcessor.java @@ -0,0 +1,44 @@ +package org.rodnansol.asciidoctorj; + +import org.asciidoctor.ast.Document; +import org.asciidoctor.extension.PreprocessorReader; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +/** + * Code block processor that extract the method from the given source path. + * + * @author nandorholozsnyak + * @since 0.1.0 + */ +public class JavaMethodCodeBlockProcessor implements CodeBlockProcessor { + + private static final String KEY_METHOD = "method"; + private static final String KEY_METHOD_PARAM_TYPES = "types"; + + @Override + public boolean isActive(Map attributes) { + return attributes.containsKey(KEY_METHOD); + } + + @Override + public void process(ExtractCommand extractCommand, Document document, PreprocessorReader reader, String target, Map attributes) { + String method = (String) attributes.get(KEY_METHOD); + try { + String paramTypeList = (String) attributes.getOrDefault(KEY_METHOD_PARAM_TYPES, ""); + String[] paramTypes = paramTypeList.split(","); + String fullMethod = JavaSourceHelper.getMethod(new ExtractMethodCommand(extractCommand, method, paramTypes)); + + reader.pushInclude( + fullMethod, + target, + new File(".").getAbsolutePath(), + 1, + attributes); + } catch (IOException e) { + throw new JavaSourceCodeExtractionException("Unable to extract method with name:[" + method + "]", e); + } + } +} diff --git a/src/main/java/org/rodnansol/asciidoctorj/JavaMethodIncludeProcessor.java b/src/main/java/org/rodnansol/asciidoctorj/JavaMethodIncludeProcessor.java new file mode 100644 index 0000000..9017d17 --- /dev/null +++ b/src/main/java/org/rodnansol/asciidoctorj/JavaMethodIncludeProcessor.java @@ -0,0 +1,83 @@ +package org.rodnansol.asciidoctorj; + +import org.asciidoctor.ast.Document; +import org.asciidoctor.extension.IncludeProcessor; +import org.asciidoctor.extension.PreprocessorReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Extension class that is able to handle the include::javasource macro and can extract and include full Java methods and fields. + *

+ * Usage: + *

+ *

+ * [source,java]
+ * ----
+ * include::javasource[source={docdir}/UserService.java,method='saveUser']
+ * ----
+ * 
+ * Available attributes: + *
    + *
  • source* - Path to the Java source file
  • + *
  • method - Name of the method to extract
  • + *
  • field - Name of the field to extract
  • + *
  • spaceSize - Space size to format the code - default is 4
  • + *
  • withJavaDoc - If the JavaDoc should be included or not - default is false
  • + *
  • lineLength - Maximum length per line - default is 120
  • + *
+ * + * @author nandorholozsnyak + * @since 0.1.0 + */ +public class JavaMethodIncludeProcessor extends IncludeProcessor { + + private static final Logger LOGGER = LoggerFactory.getLogger(JavaMethodIncludeProcessor.class); + + private static final String MACRO_JAVASOURCE = "javasource"; + private static final String KEY_SOURCE = "source"; + private static final String KEY_SPACE_SIZE = "spaceSize"; + private static final String KEY_WITH_JAVA_DOC = "withJavaDoc"; + private static final String KEY_LINE_LENGTH = "lineLength"; + + private final List codeBlockProcessorList; + + public JavaMethodIncludeProcessor() { + this(Arrays.asList(new JavaMethodCodeBlockProcessor(), new JavaFieldCodeBlockProcessor())); + } + + public JavaMethodIncludeProcessor(List codeBlockProcessorList) { + this.codeBlockProcessorList = codeBlockProcessorList; + } + + @Override + public boolean handles(String target) { + return MACRO_JAVASOURCE.equals(target); + } + + @Override + public void process(Document document, + PreprocessorReader reader, + String target, + Map attributes) { + + String source = (String) attributes.get(KEY_SOURCE); + if (source == null || source.isEmpty()) { + throw new JavaSourceCodeExtractionException("'source' attribute is empty."); + } + int spaceSize = Integer.parseInt((String) attributes.getOrDefault(KEY_SPACE_SIZE, String.valueOf(JavaSourceHelper.DEFAULT_SPACE_SIZE))); + int lineLength = Integer.parseInt((String) attributes.getOrDefault(KEY_LINE_LENGTH, String.valueOf(JavaSourceHelper.DEFAULT_LINE_LENGTH))); + boolean withJavaDoc = Boolean.parseBoolean((String) attributes.getOrDefault(KEY_WITH_JAVA_DOC, "false")); + ExtractCommand extractCommand = new ExtractCommand(source, spaceSize, withJavaDoc, lineLength); + LOGGER.debug("Extracting items from source code with basic command:[{}]", extractCommand); + codeBlockProcessorList.stream() + .filter(codeBlockProcessor -> codeBlockProcessor.isActive(attributes)) + .findFirst() + .ifPresent(codeBlockProcessor -> codeBlockProcessor.process(extractCommand, document, reader, target, attributes)); + + } +} diff --git a/src/main/java/org/rodnansol/asciidoctorj/JavaSourceBlockMacroProcessor.java b/src/main/java/org/rodnansol/asciidoctorj/JavaSourceBlockMacroProcessor.java new file mode 100644 index 0000000..ddfffae --- /dev/null +++ b/src/main/java/org/rodnansol/asciidoctorj/JavaSourceBlockMacroProcessor.java @@ -0,0 +1,44 @@ +package org.rodnansol.asciidoctorj; + +import org.asciidoctor.ast.StructuralNode; +import org.asciidoctor.extension.BlockMacroProcessor; +import org.asciidoctor.extension.Name; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * This class is in BETA phase, please do not use this macro if not interested in it. + * + * @author nandorholozsnyak + * @since 0.1.0 + */ +@Name("javasource") +public class JavaSourceBlockMacroProcessor extends BlockMacroProcessor { + + @Override + public Object process(StructuralNode parent, String target, Map attributes) { + try { + System.out.println("### ATTRIBUTES:" + parent.getAttributes()); + String method = JavaSourceHelper.getMethod(target, (String) attributes.get("method")); + String content = new StringBuilder() + .append("
\n") + .append("
\n") + .append("
\n")
+                .append("\n")
+                .append(method)
+                .append("\n")
+                .append("\n
") + .append("\n
") + .append("\n
") + .toString(); + Map params = new HashMap<>(); + params.put("style", "source"); + return createBlock(parent, "pass", content, params); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/org/rodnansol/asciidoctorj/JavaSourceCodeExtractionException.java b/src/main/java/org/rodnansol/asciidoctorj/JavaSourceCodeExtractionException.java new file mode 100644 index 0000000..20c2350 --- /dev/null +++ b/src/main/java/org/rodnansol/asciidoctorj/JavaSourceCodeExtractionException.java @@ -0,0 +1,18 @@ +package org.rodnansol.asciidoctorj; + +/** + * Exception to be thrown when the Java Source code can not be extracted. + * + * @author nandorholozsnyak + * @since 0.1.0 + */ +public class JavaSourceCodeExtractionException extends RuntimeException { + + public JavaSourceCodeExtractionException(String message) { + super(message); + } + + public JavaSourceCodeExtractionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/rodnansol/asciidoctorj/JavaSourceExtension.java b/src/main/java/org/rodnansol/asciidoctorj/JavaSourceExtension.java new file mode 100644 index 0000000..f04380a --- /dev/null +++ b/src/main/java/org/rodnansol/asciidoctorj/JavaSourceExtension.java @@ -0,0 +1,22 @@ +package org.rodnansol.asciidoctorj; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.jruby.extension.spi.ExtensionRegistry; + +/** + * Extension registry. + * + * @author nandorholozsnyak + * @since 0.1.0 + */ +public class JavaSourceExtension implements ExtensionRegistry { + + @Override + public void register(Asciidoctor asciidoctor) { + asciidoctor.javaExtensionRegistry() + .blockMacro(JavaSourceBlockMacroProcessor.class); + asciidoctor.javaExtensionRegistry() + .includeProcessor(JavaMethodIncludeProcessor.class); + } + +} diff --git a/src/main/java/org/rodnansol/asciidoctorj/JavaSourceHelper.java b/src/main/java/org/rodnansol/asciidoctorj/JavaSourceHelper.java new file mode 100644 index 0000000..a588561 --- /dev/null +++ b/src/main/java/org/rodnansol/asciidoctorj/JavaSourceHelper.java @@ -0,0 +1,125 @@ +package org.rodnansol.asciidoctorj; + +import org.jboss.forge.roaster.Roaster; +import org.jboss.forge.roaster._shade.org.apache.commons.lang3.StringUtils; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants; +import org.jboss.forge.roaster.model.source.JavaClassSource; +import org.jboss.forge.roaster.model.source.MemberSource; +import org.jboss.forge.roaster.model.source.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; + +/** + * Helper class containing methods to deal with Java source code files. + * + * @author nandorholozsnyak + * @since 0.1.0 + */ +class JavaSourceHelper { + + static final int DEFAULT_SPACE_SIZE = 4; + static final int DEFAULT_LINE_LENGTH = 120; + private static final String UNKNOWN_VALUE = ""; + private static final String MOCK_CLASS_NAME = "class Source"; + private static final Logger LOGGER = LoggerFactory.getLogger(JavaSourceHelper.class); + + private JavaSourceHelper() { + } + + /** + * Reads and extracts a specific field from the given Java source. + * + * @param extractFieldCommand@return Line containing the requested field or <UNKNOWN> if not found. + * @throws IOException when the source code can not be read. + */ + static String getField(ExtractFieldCommand extractFieldCommand) throws IOException { + Objects.requireNonNull(extractFieldCommand.getSourceCodePath(), "sourceCodePath is NULL"); + Objects.requireNonNull(extractFieldCommand.getFieldName(), "fieldName is NULL"); + LOGGER.debug("Extracting field with command:[{}]", extractFieldCommand); + JavaClassSource javaClassSource = parseSourceCode(extractFieldCommand.getSourceCodePath()); + return Optional.ofNullable(javaClassSource.getField(extractFieldCommand.getFieldName())) + .map(fieldSource -> setupMockClassAndFormatAndExtractMember(fieldSource, extractFieldCommand.getSpaceSize(), extractFieldCommand.isWithJavaDoc(), extractFieldCommand.getLineLength())) + .orElseGet(() -> { + LOGGER.warn("Unable to find field:[{}] in [{}], fallback being returned", extractFieldCommand.getFieldName(), extractFieldCommand.getSourceCodePath()); + return UNKNOWN_VALUE; + }).trim(); + } + + /** + * Reads and extracts a specific field from the given Java source. + * + * @param sourceCodePath Java source file's path. + * @param fieldName field name to extract. + * @return Line containing the requested field or <UNKNOWN> if not found. + * @throws IOException when the source code can not be read. + */ + static String getField(String sourceCodePath, String fieldName) throws IOException { + return getField(new ExtractFieldCommand(sourceCodePath, fieldName, DEFAULT_SPACE_SIZE, false, DEFAULT_LINE_LENGTH)); + } + + /** + * Extracts the requested method from the given Java source code. + * + * @param sourceCodePath Java source file's path. + * @param methodName method's name that should be extracted from the source code. + * @param paramTypes list of the method argument types in String values. + * @return if the method is found then the method's body with its signature, otherwise an empty string. + * @throws IOException when the source code can not be read. + */ + static String getMethod(String sourceCodePath, String methodName, String... paramTypes) throws IOException { + return getMethod(new ExtractMethodCommand(sourceCodePath, DEFAULT_SPACE_SIZE, methodName, false, DEFAULT_LINE_LENGTH, paramTypes)); + } + + /** + * Extracts the requested method from the given Java source code. + * + * @param extractMethodCommand@return if the method is found then the method's body with its signature, otherwise an empty string. + * @throws IOException when the source code can not be read. + */ + static String getMethod(ExtractMethodCommand extractMethodCommand) throws IOException { + Objects.requireNonNull(extractMethodCommand.getSourceCodePath(), "sourceCodePath is NULL"); + Objects.requireNonNull(extractMethodCommand.getMethodName(), "methodName is NULL"); + LOGGER.debug("Extracting method with command:[{}]", extractMethodCommand); + JavaClassSource javaClassSource = parseSourceCode(extractMethodCommand.getSourceCodePath()); + MethodSource methodSource = javaClassSource.getMethods() + .stream() + .filter(javaClassSourceMethodSource -> javaClassSourceMethodSource.getName().equals(extractMethodCommand.getMethodName())) + .findFirst() + .orElseGet(() -> javaClassSource.getMethod(extractMethodCommand.getMethodName(), extractMethodCommand.getParamTypes())); + if (methodSource == null) { + return StringUtils.EMPTY; + } + return setupMockClassAndFormatAndExtractMember(methodSource, extractMethodCommand.getSpaceSize(), extractMethodCommand.isWithJavaDoc(), extractMethodCommand.getLineLength()); + } + + private static JavaClassSource parseSourceCode(String sourceCodePath) throws IOException { + return Roaster.parse(JavaClassSource.class, new File(sourceCodePath)); + } + + private static String setupMockClassAndFormatAndExtractMember(MemberSource memberSource, int spaceSize, boolean withJavaDoc, int lineLength) { + Properties formatterProperties = setupFormatterProperties(spaceSize, lineLength); + if (!withJavaDoc) { + memberSource.removeJavaDoc(); + } + //It sets up a mock class that can be formatted and removes the unneccessary spaces and indentations. + String javaCode = MOCK_CLASS_NAME + "{" + memberSource.toString() + "}"; + String formattedCode = Roaster.format(formatterProperties, javaCode) + .substring((MOCK_CLASS_NAME + " {\n" + StringUtils.repeat(" ", spaceSize) + "").length()); + return formattedCode.substring(0, formattedCode.length() - 2) + .replaceAll("\n" + StringUtils.repeat(" ", spaceSize), "\n"); + } + + private static Properties setupFormatterProperties(int spaceSize, int lineLength) { + Properties formatterProperties = new Properties(); + formatterProperties.put(DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE, String.valueOf(spaceSize)); + formatterProperties.put(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR, "space"); + formatterProperties.put(DefaultCodeFormatterConstants.FORMATTER_LINE_SPLIT, lineLength); + return formatterProperties; + } +} diff --git a/src/main/resources/META-INF/services/org.asciidoctor.jruby.extension.spi.ExtensionRegistry b/src/main/resources/META-INF/services/org.asciidoctor.jruby.extension.spi.ExtensionRegistry new file mode 100644 index 0000000..d47e3be --- /dev/null +++ b/src/main/resources/META-INF/services/org.asciidoctor.jruby.extension.spi.ExtensionRegistry @@ -0,0 +1 @@ +org.rodnansol.asciidoctorj.JavaSourceExtension diff --git a/src/test/java/org/rodnansol/SingleModuleTest.java b/src/test/java/org/rodnansol/SingleModuleTest.java deleted file mode 100644 index 7cd2890..0000000 --- a/src/test/java/org/rodnansol/SingleModuleTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.rodnansol; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -class SingleModuleTest { - - - @Test - void name() { - // Given - // When - int sum = new SingleModule().sum(1, 1); - // Then - Assertions.assertThat(sum).isEqualTo(2); - } -} diff --git a/src/test/java/org/rodnansol/asciidoctorj/JavaSourceHelperTest.java b/src/test/java/org/rodnansol/asciidoctorj/JavaSourceHelperTest.java new file mode 100644 index 0000000..805cc1d --- /dev/null +++ b/src/test/java/org/rodnansol/asciidoctorj/JavaSourceHelperTest.java @@ -0,0 +1,77 @@ +package org.rodnansol.asciidoctorj; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class JavaSourceHelperTest { + + @Test + void getField_shouldReturnUnknown_whenNotFound() throws IOException { + // Given + // When + String logger = JavaSourceHelper.getField("src/test/resources/UserService.java", "NOT_EXIST"); + // Then + assertThat(logger).isEqualTo(""); + } + + @Test + void getField_shouldReturnField_whenFound() throws IOException { + // Given + // When + String logger = JavaSourceHelper.getField("src/test/resources/UserService.java", "LOGGER"); + // Then + assertThat(logger).isEqualTo("private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class);"); + } + + @Test + void getMethod_shouldReturnEmptyString_whenNotFound() throws IOException { + // Given + // When + String logger = JavaSourceHelper.getMethod("src/test/resources/UserService.java", "notExists"); + // Then + assertThat(logger).isEmpty(); + } + + @MethodSource("getMethodArgs") + @ParameterizedTest + void getMethod_shouldReturnMethod_whenFound(String methodName, String expectedExtractedContent, String[] paramTypes) throws IOException { + // Given + // When + String method = JavaSourceHelper.getMethod("src/test/resources/UserService.java", methodName, paramTypes); + // Then + assertThat(method).isEqualTo(expectedExtractedContent); + } + + static Stream getMethodArgs() { + return Stream.of( + arguments("createUser","public final User createUser(String username, String password) {\n" + + " LOGGER.info(\"Creating new user with username:[{}]\", username);\n" + + " userRepository.save(new User(username, password));\n" + + "}",(Object) new String[]{"String","String"}), + arguments("deleteUser","protected void deleteUser(String username) {\n" + + " LOGGER.info(\"Deleting user with username:[{}]\", username);\n" + + " userRepository.deleteByUsername(username);\n" + + "}",(Object) new String[]{"String"}), + arguments("disableUser","void disableUser(User user) {\n" + + " LOGGER.info(\"Disabling user with username:[{}]\", user.username);\n" + + " if (user.enabled == true) {\n" + + " user.enabled = false;\n" + + " userRepository.save(user);\n" + + " } else {\n" + + " logUnableToDisableUser();\n" + + " }\n" + + "}",(Object) new String[]{"User"}), + arguments("logUnableToDisableUser","private static void logUnableToDisableUser() {\n" + + " LOGGER.info(\"Unable to disable disabled user\");\n" + + "}", new String[]{}) + ); + } +} diff --git a/src/test/resources/UserService.java b/src/test/resources/UserService.java new file mode 100644 index 0000000..429a8f5 --- /dev/null +++ b/src/test/resources/UserService.java @@ -0,0 +1,49 @@ +class UserService { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class); + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + /** + * Creates a new user based on the incoming stuff. + */ + public final User createUser(String username, String password) { + LOGGER.info("Creating new user with username:[{}]", username); + userRepository.save(new User(username, password)); + } + + protected void deleteUser(String username) { + LOGGER.info("Deleting user with username:[{}]", username); + userRepository.deleteByUsername(username); + } + + void disableUser(User user) { + LOGGER.info("Disabling user with username:[{}]", user.username); + if (user.enabled == true) { + user.enabled = false; + userRepository.save(user); + } else { + logUnableToDisableUser(); + } + } + + private static void logUnableToDisableUser() { + LOGGER.info("Unable to disable disabled user"); + } + +} + +class User { + public String username; + public String password; + public boolean enabled; +} + +interface UserRepository { + save(User user); + + deleteByUsername(String username); +}