From 6417a861e5ec7816f3bea431d1a186e0e3596d05 Mon Sep 17 00:00:00 2001 From: Scott Beddall <45376673+scbedd@users.noreply.github.com> Date: Tue, 12 May 2020 10:31:15 -0700 Subject: [PATCH] Maven CodeSnippet Plugin (#544) * adding a plugin that can replace embedme and codesnippet usage in the azure-sdk CI builds --- .gitignore | 1 + README.md | 7 +- .../snippet-replacer-maven-plugin/README.md | 133 +++++ .../snippet-replacer-maven-plugin/pom.xml | 149 +++++ .../azuresdk/plugin/SnippetDictionary.java | 33 ++ .../plugin/SnippetOperationResult.java | 15 + .../azuresdk/plugin/SnippetPluginEntry.java | 35 ++ .../java/azuresdk/plugin/SnippetReplacer.java | 520 ++++++++++++++++++ .../java/azuresdk/plugin/VerifyResult.java | 15 + .../azuresdk/plugin/SnippetReplacerTests.java | 186 +++++++ .../basic_readme_insertion_after.txt | 23 + .../basic_readme_insertion_before.txt | 15 + .../basic_src_snippet_insertion_after.txt | 86 +++ .../basic_src_snippet_insertion_before.txt | 66 +++ .../basic_src_snippet_parse.txt | 61 ++ .../project-to-test/duplicate_snippet_src.txt | 19 + .../duplicate_snippet_src_multiple.txt | 9 + .../project-to-test/empty_snippet_def.txt | 6 + .../test/resources/project-to-test/pom.xml | 24 + .../readme_insertion_verification_failure.txt | 20 + .../src_insertion_verification_failure.txt | 84 +++ 21 files changed, 1505 insertions(+), 2 deletions(-) create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/README.md create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/pom.xml create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetDictionary.java create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetOperationResult.java create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetPluginEntry.java create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetReplacer.java create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/VerifyResult.java create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/test/java/azuresdk/plugin/SnippetReplacerTests.java create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_readme_insertion_after.txt create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_readme_insertion_before.txt create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_src_snippet_insertion_after.txt create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_src_snippet_insertion_before.txt create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_src_snippet_parse.txt create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/duplicate_snippet_src.txt create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/duplicate_snippet_src_multiple.txt create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/empty_snippet_def.txt create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/pom.xml create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/readme_insertion_verification_failure.txt create mode 100644 packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/src_insertion_verification_failure.txt diff --git a/.gitignore b/.gitignore index 51a809037..85d90b51a 100644 --- a/.gitignore +++ b/.gitignore @@ -290,6 +290,7 @@ PublishScripts/ #*.nupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* +!packages/java-packages/ # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed diff --git a/README.md b/README.md index b8618ba8b..1a900146d 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,10 @@ This repository contains useful tools that the Azure SDK team utilizes across th | Package or Intent | Path | Description | Status | | ------------------------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | doc-warden | [Readme](packages/python-packages/doc-warden/README.md) | A tool used to enforce readme standards across Azure SDK Repos. | [![Build Status](https://dev.azure.com/azure-sdk/public/_apis/build/status/108?branchName=master)](https://dev.azure.com/azure-sdk/public/_build/latest?definitionId=108&branchName=master) | -| pixel-server | [Readme](/web/pixel-server/README.md) | A tiny ASP.NET Core site used to serve a pixel and record impressions. | Not Yet Enabled | +| pixel-server | [Readme](web/pixel-server/README.md) | A tiny ASP.NET Core site used to serve a pixel and record impressions. | Not Yet Enabled | | pixel insertion tool | [Readme](scripts/python/readme_tracking/readme.md) | A tool used to insert the requests for images served by `pixel server`. | Not Yet Enabled | -| Check Enforcer | [Readme](/tools/check-enforcer/README.md) | Manage GitHub check-runs in a mono-repo. | Not Yet Enabled | +| Check Enforcer | [Readme](tools/check-enforcer/README.md) | Manage GitHub check-runs in a mono-repo. | Not Yet Enabled | +| Maven Plugin for Snippets | [Readme](packages/java-packages/snippet-replacer-maven-plugin/README.md) | A Maven plugin that that updates code snippets referenced from javadoc comments. | Not Yet Enabled | ## Contributing @@ -26,3 +27,5 @@ For more information see the [Code of Conduct FAQ](https://opensource.microsoft. contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. [![Azure DevOps builds](https://img.shields.io/azure-devops/build/azure-sdk/internal/1372?label=eng%2Fcommon%20sync)](https://dev.azure.com/azure-sdk/internal/_build/latest?definitionId=1372&branchName=master) + +C:/repo/sdk-tools/packages/java-packages/snippet-replacer-maven-plugin \ No newline at end of file diff --git a/packages/java-packages/snippet-replacer-maven-plugin/README.md b/packages/java-packages/snippet-replacer-maven-plugin/README.md new file mode 100644 index 000000000..9588ee3d3 --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/README.md @@ -0,0 +1,133 @@ +# Snippet Replacer + +`snippet-replacer-maven-plugin` allows java devs to reference actual java code in their javadoc comments. Developers running code written by the Azure team want their documentation to be useful. This plugin helps the Azure SDK team deliver on that promise by ensuring that code samples presented in doc-comments is always _actual running code_. + +### How does it work? + +First, reference the plugin in your maven project's `pom.xml` file. + +```xml + + com.azure.tools + snippet-replacer-maven-plugin + 1.0.0 + + update + ${project.basedir} + + + + process-sources + + snippet-engine + + + + + +``` + +The plugin is intended to be run in the `process-sources` phase. + +### The modes of operation + +`snippet-replacer` has two modes, `update` and `verify`. + +`update` mode runs during your build and _actually updates_ your Javadoc comments with referenced source code. +`verify` mode is intended to be run during CI/PR builds. It will error a javadoc comment that is not updated. + +## How to define a referencable snippet + +Embed a beginning and end html comment wherever you want the source code inserted. + +Within working java code, we have a snippet definition. The string after the `BEGIN:` or `END:` comments is an identifer that can be referenced from javadoc or readmes. + +``` +public ConfigurationClient createSyncConfigurationClient() { + String connectionString = getConnectionString(); + // BEGIN: com.azure.data.applicationconfig.configurationclient.instantiation + ConfigurationClient configurationClient = new ConfigurationClientBuilder() + .connectionString(connectionString) + .buildClient(); + // END: com.azure.data.applicationconfig.configurationclient.instantiation + return configurationClient; +} +``` +The above example defines a code snippet of identifier `com.azure.data.applicationconfig.configurationclient.instantiation`. + +### Example of `update` + +Within a javadoc comment, a snippet is referenced by a matching pair html comments. + +``` +... + *

Instantiating a synchronous Configuration Client

+ * + * + * + * + *

View {@link ConfigurationClientBuilder this} for additional ways to construct the client.

+ * + * @see ConfigurationClientBuilder + */ +@ServiceClient(builder = ConfigurationClientBuilder.class, serviceInterfaces = ConfigurationService.class) +public final class ConfigurationClient { +... +``` +After update runs: +``` +... + *

Instantiating a synchronous Configuration Client

+ * + * + *
+ * ConfigurationClient configurationClient = new ConfigurationClientBuilder()
+ *     .connectionString(connectionString)
+ *     .buildClient();
+ * 
+ * + * + *

View {@link ConfigurationClientBuilder this} for additional ways to construct the client.

+ * + * @see ConfigurationClientBuilder + */ +@ServiceClient(builder = ConfigurationClientBuilder.class, serviceInterfaces = ConfigurationService.class) +public final class ConfigurationClient { +... +``` + +The referenced code snippet will be embedded (with javadoc appropriate encoding) with the properly spaced code snippet. + +### Example of `verify` + +This mode is intended for use within CI systems. It will throw an error detailing where snippets are in need of updating. Any dev can rebuild your project with `update` mode and commit the result. + +### Functionality against root README.md + +While the plugin is mostly intended for use in Javadoc comments, it also will run update and verify operations against the file ${project.basedir}/README.md. + +Example reference before update: + +```` +```Java com.azure.data.applicationconfig.configurationclient.instantiation +``` +```` + +Example of README.md after update: + +```` +```Java com.azure.data.applicationconfig.configurationclient.instantiation +ConfigurationClient configurationClient = new ConfigurationClientBuilder() + .connectionString(connectionString) + .buildClient(); +``` +```` + +Which Renders: + +```Java com.azure.data.applicationconfig.configurationclient.instantiation +ConfigurationClient configurationClient = new ConfigurationClientBuilder() + .connectionString(connectionString) + .buildClient(); +``` + diff --git a/packages/java-packages/snippet-replacer-maven-plugin/pom.xml b/packages/java-packages/snippet-replacer-maven-plugin/pom.xml new file mode 100644 index 000000000..5ca15e03b --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/pom.xml @@ -0,0 +1,149 @@ + + + + 4.0.0 + + com.azure.tools + snippet-replacer-maven-plugin + 1.0.0-SNAPSHOT + maven-plugin + snippet-replacer-maven-plugin Maven Plugin + + https://github.com/azure/azure-sdk-tools + + + ${maven.version} + + + + UTF-8 + 8 + 8 + 3.3.9 + + + + + org.apache.maven + maven-plugin-api + ${maven.version} + provided + + + org.apache.maven + maven-core + ${maven.version} + provided + + + org.apache.maven + maven-artifact + ${maven.version} + provided + + + org.apache.maven + maven-compat + ${maven.version} + test + + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.6.0 + provided + + + junit + junit + 4.12 + test + + + org.apache.maven.plugin-testing + maven-plugin-testing-harness + 3.3.0 + test + + + + + + + ${basedir}/src/test/resources + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + org.apache.maven.plugins + maven-plugin-plugin + 3.6.0 + + true + + + + maven-surefire-plugin + 2.22.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + maven-invoker-plugin + 3.1.0 + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.6.0 + + + true + + + + mojo-descriptor + + descriptor + + + + help-goal + + helpmojo + + + + + + + + diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetDictionary.java b/packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetDictionary.java new file mode 100644 index 000000000..7cb87a1ac --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetDictionary.java @@ -0,0 +1,33 @@ +package azuresdk.plugin; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SnippetDictionary { + private HashMap> _snippetDictionary = new HashMap<>(); + + public boolean isActive() { + return !_snippetDictionary.isEmpty(); + } + + public void beginSnippet(String key) { + if (!this._snippetDictionary.containsKey((key))) { + this._snippetDictionary.put(key, new ArrayList<>()); + } + } + + public void processLine(String line) { + for (Map.Entry> entry : this._snippetDictionary.entrySet()) { + entry.getValue().add(line); + } + } + + public List finalizeSnippet(String key) { + List value = this._snippetDictionary.get(key); + this._snippetDictionary.remove(key); + + return value; + } +} diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetOperationResult.java b/packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetOperationResult.java new file mode 100644 index 000000000..0c7441782 --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetOperationResult.java @@ -0,0 +1,15 @@ +package azuresdk.plugin; + +import java.util.ArrayList; +import java.util.List; + +public class SnippetOperationResult { + public T result; + List errorList; + + public SnippetOperationResult(T resultObject, List errors){ + super(); + this.result = resultObject; + this.errorList = errors; + } +} diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetPluginEntry.java b/packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetPluginEntry.java new file mode 100644 index 000000000..c40b450f4 --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetPluginEntry.java @@ -0,0 +1,35 @@ +package azuresdk.plugin; + + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; + +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +@Mojo( name = "snippet-engine", defaultPhase = LifecyclePhase.PROCESS_SOURCES ) +public class SnippetPluginEntry + extends AbstractMojo +{ + @Parameter( property = "report.mode", required = true ) + private String mode; + + @Parameter( defaultValue = "${project.basedir}", property = "report.targetDir", required = true ) + private File targetDir; + + public void execute() + { + try { + SnippetReplacer replacer = new SnippetReplacer(mode, targetDir, getLog()); + getLog().info("Completed Execution for Snippet Replacer"); + } catch (Exception e) { + getLog().error(e); + return; + } + } +} diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetReplacer.java b/packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetReplacer.java new file mode 100644 index 000000000..e140a6ff3 --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/SnippetReplacer.java @@ -0,0 +1,520 @@ +package azuresdk.plugin; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.*; +import java.util.regex.*; + +import com.google.common.base.Verify; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.logging.Log; + +public class SnippetReplacer { + public final static Pattern SNIPPET_DEF_BEGIN = Pattern.compile("\\s*\\/\\/\\s*BEGIN\\:\\s+([a-zA-Z0-9\\.\\#\\-\\_]*)\\s*"); + public final static Pattern SNIPPET_DEF_END = Pattern.compile("\\s*\\/\\/\\s*END\\:\\s+([a-zA-Z0-9\\.\\#\\-\\_]*)\\s*"); + public final static Pattern SNIPPET_SRC_CALL_BEGIN = Pattern.compile("(\\s*)\\*?\\s*"); + public final static Pattern SNIPPET_SRC_CALL_END = Pattern.compile("(\\s*)\\*?\\s*"); + public final static Pattern SNIPPET_README_CALL_BEGIN = Pattern.compile("```(\\s*)?Java\\s+([a-zA-Z0-9\\.\\#\\-\\_]*)\\s*"); + public final static Pattern SNIPPET_README_CALL_END = Pattern.compile("```"); + public final static Pattern WHITESPACE_EXTRACTION = Pattern.compile("(\\s*)(.*)"); + + private static PathMatcher SAMPLE_PATH_GLOB = FileSystems.getDefault().getPathMatcher("glob:**/src/samples/java/**/*.java"); + private static PathMatcher JAVA_GLOB = FileSystems.getDefault().getPathMatcher("glob:**/*.java"); + + private static HashMap REPLACEMENT_SET = new HashMap(){{ + put("\"", """); + put(">", ">"); + put("<", "<"); + put("@", "{@literal @}"); + put("{", "{"); + put("}", "}"); + put("(", "("); + put(")", ")"); + put("/", "/"); + put("\\", "\"); + }}; + + public SnippetReplacer(){} + + public SnippetReplacer(String mode, File folderToVerify, Log logger) throws MojoExecutionException, IOException { + switch (mode) { + case "update": + this.runUpdate(folderToVerify, logger); + break; + case "verify": + this.runVerification(folderToVerify, logger); + break; + default: + throw new MojoExecutionException(String.format("Unrecognized snippet-replacer mode: %s.", mode)); + } + } + + /** + * The "verification" operation encapsulated by this function is as follows. + * + * 1. Scan under the target direction for all discovered code snippet DEFINITIONS + * 2. Examine all snippet CALLS, finding where updates are needed. + * 3. Report all discovered snippets in need of update as well as all bad snippet calls + * + * A "bad snippet call" is simply calling for a snippet whose Id has no definition. + * + * See {@link #runUpdate(File, Log)} for details on actually defining and calling snippets. + */ + public void runVerification(File folderToVerify, Log logger) throws IOException, MojoExecutionException { + List allLocatedJavaFiles = glob(folderToVerify.toPath(), JAVA_GLOB); + List snippetSources = globFiles(allLocatedJavaFiles, SAMPLE_PATH_GLOB); + List snippetsNeedingUpdate = new ArrayList<>(); + HashMap> foundSnippets = new HashMap<>(); + List badSnippetCalls = new ArrayList<>(); + + // scan the sample files for all the snippet files + foundSnippets = this.getAllSnippets(snippetSources); + + // walk across all the java files, run UpdateSrcSnippets + for (Path sourcePath : allLocatedJavaFiles) { + SnippetOperationResult> verifyResult = this.verifySrcSnippets(sourcePath, foundSnippets); + snippetsNeedingUpdate.addAll(verifyResult.result); + badSnippetCalls.addAll(verifyResult.errorList); + } + + // now find folderToVerify/README.md + // run Update ReadmeSnippets on that + File readmeInBaseDir = new File(folderToVerify, "README.md"); + SnippetOperationResult> rdmeResult = this.verifyReadmeSnippets(readmeInBaseDir.toPath(), foundSnippets); + snippetsNeedingUpdate.addAll(rdmeResult.result); + badSnippetCalls.addAll(rdmeResult.errorList); + + if (snippetsNeedingUpdate.size() > 0 || badSnippetCalls.size() > 0) { + for (VerifyResult result : snippetsNeedingUpdate) { + logger.error(String.format("SnippetId %s needs update in file %s.", result.SnippetWithIssues, result.ReadmeLocation.toString())); + } + + for (VerifyResult result : badSnippetCalls) { + logger.error(String.format("Unable to locate snippet with Id of %s. Reference in %s", result.SnippetWithIssues, result.ReadmeLocation.toString())); + } + + throw new MojoExecutionException("Snippet-Replacer has encountered errors, check above output for details."); + } + } + + /** + * This method encapsulates the "update" lifecycle of the snippet-replacer plugin. + * + * Given a root folder, the plugin will scan for snippet DEFINITIONS or snippet CALLS. Once a snippet definition + * index has been formulated, all java files located under the target directory will have snippet CALLS updated with + * the source from the DEFINITIONS. + * + *

Snippet Definition

+ * + * A snippet definition is delineated by BEGIN and END comments directly in your java source. + * Example: + *
+     * // BEGIN: com.azure.data.applicationconfig.configurationclient.instantiation
+     * ConfigurationClient configurationClient = new ConfigurationClientBuilder()
+     *     .connectionString(connectionString)
+     *     .buildClient();
+     * // END: com.azure.data.applicationconfig.configurationclient.instantiation
+     * 
+ * + *

Calling a Snippet

+ * + * From within a javadoc comment, embed an html comment + * /* + * <!-- src_embed com.azure.data.applicationconfig.configurationclient.instantiation --> + * ConfigurationClient configurationClient = new ConfigurationClientBuilder() + * .connectionString(connectionString) + * .buildClient(); + * <!-- end com.azure.data.applicationconfig.configurationclient.instantiation --> + * Other javadoc details perhaps. + * */ + * public void myfunction() + * + * + * After finishing update operations, this function will throw a MojoExecutionException after reporting all snippet + * CALLS that have no DEFINITION. + */ + public void runUpdate(File folderToVerify, Log logger) throws IOException, MojoExecutionException { + List allLocatedJavaFiles = glob(folderToVerify.toPath(), JAVA_GLOB); + List snippetSources = globFiles(allLocatedJavaFiles, SAMPLE_PATH_GLOB); + HashMap> foundSnippets = new HashMap>(); + List badSnippetCalls = new ArrayList<>(); + + // scan the sample files for all the snippet files + foundSnippets = this.getAllSnippets(snippetSources); + + // walk across all the java files, run UpdateSrcSnippets + for (Path sourcePath : allLocatedJavaFiles) { + badSnippetCalls.addAll(this.updateSrcSnippets(sourcePath, foundSnippets)); + } + + // now find folderToVerify/README.md + // run Update ReadmeSnippets on that + File readmeInBaseDir = new File(folderToVerify, "README.md"); + badSnippetCalls.addAll(this.updateReadmeSnippets(readmeInBaseDir.toPath(), foundSnippets)); + + if (badSnippetCalls.size() > 0) { + for (VerifyResult result : badSnippetCalls) { + logger.error(String.format("Unable to locate snippet with Id of %s. Reference in %s", result.SnippetWithIssues, result.ReadmeLocation.toString())); + } + throw new MojoExecutionException("Discovered snippets in need of updating. Please run this plugin in update mode and commit the changes."); + } + } + + public List updateReadmeSnippets(Path file, HashMap> snippetMap) throws IOException, MojoExecutionException { + List lines = Files.readAllLines(file, StandardCharsets.UTF_8); + SnippetOperationResult opResult = this.updateSnippets(file, + lines, + SNIPPET_README_CALL_BEGIN, + SNIPPET_README_CALL_END, + snippetMap, + "", + "", + 0, + "", + true); + + if (opResult.result != null) { + try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { + writer.write(opResult.result.toString()); + } + } + + return opResult.errorList; + } + + public List updateSrcSnippets(Path file, HashMap> snippetMap) throws IOException, MojoExecutionException { + List lines = Files.readAllLines(file, StandardCharsets.UTF_8); + SnippetOperationResult opResult = this.updateSnippets(file, + lines, + SNIPPET_SRC_CALL_BEGIN, + SNIPPET_SRC_CALL_END, + snippetMap, + "
",
+                "
", + 1, + "* ", + false); + + if (opResult.result != null) { + try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { + writer.write(opResult.result.toString()); + } + } + + return opResult.errorList; + } + + public SnippetOperationResult updateSnippets(Path file, List lines, Pattern beginRegex, Pattern endRegex, HashMap> snippetMap, + String preFence, String postFence, int prefixGroupNum, String additionalLinePrefix, boolean disableEscape) { + + List badSnippetCalls = new ArrayList<>(); + StringBuilder modifiedLines = new StringBuilder(); + boolean inSnippet = false; + boolean needsAmend = false; + String lineSep = System.lineSeparator(); + String currentSnippetId = ""; + + for (String line : lines) { + Matcher begin = beginRegex.matcher(line); + Matcher end = endRegex.matcher(line); + + if (begin.matches()) { + modifiedLines.append(line + lineSep); + currentSnippetId = begin.group(2); + inSnippet = true; + } + else if (end.matches()) { + if (inSnippet) { + List newSnippets = null; + if (snippetMap.containsKey(currentSnippetId)) { + newSnippets = snippetMap.get(currentSnippetId); + } else { + badSnippetCalls.add(new VerifyResult(file, currentSnippetId)); + needsAmend = true; + inSnippet = false; + continue; + } + + List modifiedSnippets = new ArrayList<>(); + + // We use this additional prefix because in src snippet cases we need to prespace + // for readme snippet cases we DONT need the prespace at all. + String linePrefix = this.prefixFunction(end, prefixGroupNum, additionalLinePrefix); + + for (String snippet : this.respaceLines(newSnippets)) { + String moddedSnippet = disableEscape ? snippet : this.escapeString(snippet); + modifiedSnippets.add(moddedSnippet.length() == 0 + ? linePrefix.replaceAll("[\\s]+$", "") + lineSep + : linePrefix + moddedSnippet + lineSep); + } + + if (preFence != null && preFence.length() > 0) { + modifiedLines.append(linePrefix + preFence + lineSep); + } + + modifiedLines.append(String.join("", modifiedSnippets)); + + if (postFence != null && postFence.length() > 0) { + modifiedLines.append(linePrefix + postFence + lineSep); + } + + modifiedLines.append(line + lineSep); + needsAmend = true; + inSnippet = false; + } + } + else { + if (inSnippet) { + // do nothing. we'll write everything at the end, + // we'd do a comparison here if we were verifying + } + else { + modifiedLines.append(line + lineSep); + } + } + } + + if (needsAmend) { + return new SnippetOperationResult(modifiedLines, badSnippetCalls); + } + else{ + return null; + } + } + + public SnippetOperationResult> verifyReadmeSnippets(Path file, HashMap> snippetMap) throws IOException, MojoExecutionException { + List lines = Files.readAllLines(file, StandardCharsets.UTF_8); + return this.verifySnippets(file, lines, SNIPPET_README_CALL_BEGIN, SNIPPET_README_CALL_END, snippetMap, "", "", 0, "", true); + } + + public SnippetOperationResult> verifySrcSnippets(Path file, HashMap> snippetMap) throws IOException, MojoExecutionException { + List lines = Files.readAllLines(file, StandardCharsets.UTF_8); + return this.verifySnippets(file, lines, SNIPPET_SRC_CALL_BEGIN, SNIPPET_SRC_CALL_END, snippetMap, "
", "
", 1, "* ", false); + } + + public SnippetOperationResult> verifySnippets(Path file, List lines, Pattern beginRegex, Pattern endRegex, + HashMap> snippetMap, String preFence, String postFence, int prefixGroupNum, + String additionalLinePrefix, boolean disableEscape) { + + boolean inSnippet = false; + String lineSep = System.lineSeparator(); + List currentSnippetSet = null; + List foundIssues = new ArrayList<>(); + List badSnippetCalls = new ArrayList<>(); + String currentSnippetId = ""; + + for (String line : lines) { + Matcher begin = beginRegex.matcher(line); + Matcher end = endRegex.matcher(line); + + if (begin.matches()) { + currentSnippetId = begin.group(2); + inSnippet = true; + currentSnippetSet = new ArrayList<>(); + } + else if (end.matches()) { + if (inSnippet) { + List newSnippets; + if (snippetMap.containsKey(currentSnippetId)) { + newSnippets = snippetMap.get(currentSnippetId); + } else { + badSnippetCalls.add(new VerifyResult(file, currentSnippetId)); + inSnippet = false; + currentSnippetSet = null; + continue; + } + List modifiedSnippets = new ArrayList<>(); + + // We use this additional prefix because in src snippet cases we need to prespace + // for readme snippet cases we DONT need the prespace at all. + String linePrefix = this.prefixFunction(end, prefixGroupNum, additionalLinePrefix); + + for (String snippet : this.respaceLines(newSnippets)) { + String moddedSnippet = disableEscape ? snippet : this.escapeString(snippet); + modifiedSnippets.add(moddedSnippet.length() == 0 + ? linePrefix.replaceAll("[\\s]+$", "") + lineSep + : linePrefix + moddedSnippet + lineSep); + } + + Collections.sort(modifiedSnippets); + Collections.sort(currentSnippetSet); + + if (!modifiedSnippets.equals(currentSnippetSet)) { + foundIssues.add(new VerifyResult(file, currentSnippetId)); + } + + inSnippet = false; + currentSnippetSet = null; + } + } + else { + if (inSnippet) { + if (preFence.length() > 0 && postFence.length() > 0) { + if (!line.contains(preFence) && !line.contains(postFence)) { + currentSnippetSet.add(line + lineSep); + } + } + else { + currentSnippetSet.add(line + lineSep); + } + } + } + } + + return new SnippetOperationResult>(foundIssues, badSnippetCalls); + } + + public HashMap> getAllSnippets(List snippetSources) throws IOException, MojoExecutionException { + HashMap> locatedSnippets = new HashMap<>(); + List detectedIssues = new ArrayList<>(); + + for(Path samplePath: snippetSources){ + List fileContent = Files.readAllLines(samplePath, StandardCharsets.UTF_8); + HashMap> tempSnippetMap = new HashMap<>(); + SnippetDictionary snippetReader = new SnippetDictionary(); + int counter = 0; + + for (String line : fileContent) { + Matcher begin = SNIPPET_DEF_BEGIN.matcher(line); + Matcher end = SNIPPET_DEF_END.matcher(line); + + if (begin.matches() ){ + String id_beginning = begin.group(1); + snippetReader.beginSnippet((id_beginning)); + } + else if (end.matches()) { + String id_ending = end.group(1); + List snippetContent = snippetReader.finalizeSnippet((id_ending)); + if (!tempSnippetMap.containsKey((id_ending))) { + tempSnippetMap.put(id_ending, snippetContent); + } + else { + // detect duplicate in file + detectedIssues.add(new VerifyResult(samplePath, id_ending)); + } + } + else if (snippetReader.isActive()) { + snippetReader.processLine(line); + } + + counter++; + } + + // we need to examine them individually, as we want to get a complete list of all the duplicates in a run + for (String snippetId : tempSnippetMap.keySet()) { + if (!locatedSnippets.containsKey(snippetId)) { + locatedSnippets.put(snippetId, tempSnippetMap.get(snippetId)); + } + else { + // detect duplicate across multiple files + detectedIssues.add(new VerifyResult(samplePath, snippetId)); + } + } + }; + + if (detectedIssues.size() > 0) { + throw new MojoExecutionException("Duplicate Snippet Definitions Detected. " + System.lineSeparator() + this.getErrorString(detectedIssues)); + } + + return locatedSnippets; + } + + private String getErrorString(List errors) { + StringBuilder results = new StringBuilder(); + + for (VerifyResult result : errors) { + results.append(String.format("Duplicate snippetId %s detected in %s.", result.SnippetWithIssues, result.ReadmeLocation.toString()) + System.lineSeparator()); + } + + return results.toString(); + } + + private List respaceLines(List snippetText) { + // get List of all the the leading whitespace in the sample + // toss out lines that are empty (as they shouldn't mess with the minimum) + String minWhitespace = null; + List modifiedStrings = new ArrayList<>(); + + for (String snippetLine : snippetText) { + // only look at non-whitespace only strings for the min indent + if(snippetLine.trim().length() != 0) { + Matcher leadSpaceMatch = WHITESPACE_EXTRACTION.matcher(snippetLine); + + if (leadSpaceMatch.matches()) { + String leadSpace = leadSpaceMatch.group(1); + + if (minWhitespace == null || leadSpace.length() < minWhitespace.length()) + minWhitespace = leadSpace; + } + } + } + + for (String snippetLine : snippetText) { + modifiedStrings.add(snippetLine.replaceFirst(minWhitespace, "")); + } + + return modifiedStrings; + } + + private int getEndIndex(List lines, int startIndex) { + for (int i = startIndex; i < lines.size(); i++) { + Matcher end = SNIPPET_SRC_CALL_END.matcher(lines.get(i)); + if (end.matches()) + return i; + } + + return -1; + } + + private String prefixFunction(Matcher match, int groupNum, String additionalPrefix) { + // if we pass -1 as the matcher groupNum, we don't want any prefix at all + if (match == null || groupNum < 1) { + return ""; + } else { + return match.group(groupNum) + additionalPrefix; + } + } + + private String escapeString(String target) { + if (target != null && target.trim().length() > 0) { + for (String key : this.REPLACEMENT_SET.keySet()) { + target = target.replace(key, REPLACEMENT_SET.get(key)); + } + } + + return target; + } + + private List glob(Path rootFolder, PathMatcher pathMatcher) throws IOException { + List locatedPaths = new ArrayList<>(); + + Files.walkFileTree(rootFolder, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (pathMatcher.matches(file)) { + locatedPaths.add(file); + } + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + return FileVisitResult.CONTINUE; + } + }); + + return locatedPaths; + } + + private List globFiles(List paths, PathMatcher pathMatcher) throws IOException { + List locatedPaths = new ArrayList<>(); + + for (Path path : paths) { + if (pathMatcher.matches(path)) { + locatedPaths.add(path); + } + }; + + return locatedPaths; + } +} diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/VerifyResult.java b/packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/VerifyResult.java new file mode 100644 index 000000000..573b24440 --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/main/java/azuresdk/plugin/VerifyResult.java @@ -0,0 +1,15 @@ +package azuresdk.plugin; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +public class VerifyResult { + public String SnippetWithIssues; + public Path ReadmeLocation; + + public VerifyResult(Path readmeLocation, String snippetIdWithIssues) { + this.ReadmeLocation = readmeLocation; + this.SnippetWithIssues = snippetIdWithIssues; + } +} \ No newline at end of file diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/test/java/azuresdk/plugin/SnippetReplacerTests.java b/packages/java-packages/snippet-replacer-maven-plugin/src/test/java/azuresdk/plugin/SnippetReplacerTests.java new file mode 100644 index 000000000..eacc00066 --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/test/java/azuresdk/plugin/SnippetReplacerTests.java @@ -0,0 +1,186 @@ +package azuresdk.plugin; + +import static org.junit.Assert.*; + +import org.apache.maven.plugin.MojoExecutionException; +import org.junit.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +public class SnippetReplacerTests { + + private Path _getPathToResource(String relativeLocation){ + String pathToTestFile = SnippetReplacerTests.class.getResource(relativeLocation).getPath(); + + if(pathToTestFile.startsWith("/")){ + pathToTestFile = pathToTestFile.substring(1); + } + + return Paths.get(pathToTestFile); + } + + @Test + public void testSrcParse() + throws Exception + { + Path testFile = _getPathToResource("../../project-to-test/basic_src_snippet_parse.txt"); + HashMap> foundSnippets = new SnippetReplacer().getAllSnippets( + new ArrayList(Arrays.asList(testFile.toAbsolutePath()))); + + assertTrue(foundSnippets.size() == 3); + assertTrue( + foundSnippets.get("com.azure.data.applicationconfig.configurationclient.instantiation").size() == 3); + assertTrue( + foundSnippets.get("com.azure.data.appconfiguration.ConfigurationClient.addConfigurationSetting#String-String-String").size() == 3); + assertTrue(foundSnippets.get("com.azure.core.http.rest.pagedflux.instantiation").size() == 9); + } + + @Test + public void testSrcInsertion() + throws Exception + { + /* + Ensures html encoding, empty and populated snippets replacement + */ + Path snippetSourceFile = _getPathToResource("../../project-to-test/basic_src_snippet_parse.txt"); + Path codeForReplacement = _getPathToResource("../../project-to-test/basic_src_snippet_insertion_before.txt"); + Path expectedOutCome = _getPathToResource("../../project-to-test/basic_src_snippet_insertion_after.txt"); + SnippetReplacer testReplacer = new SnippetReplacer(); + List testLines = Files.readAllLines(codeForReplacement, StandardCharsets.UTF_8); + byte[] rawBytes = Files.readAllBytes(expectedOutCome); + String expectedString = new String(rawBytes, StandardCharsets.UTF_8); + + HashMap> foundSnippets = testReplacer.getAllSnippets( + new ArrayList(Arrays.asList(snippetSourceFile.toAbsolutePath()))); + SnippetOperationResult opResult = testReplacer.updateSnippets(codeForReplacement, testLines, + testReplacer.SNIPPET_SRC_CALL_BEGIN, testReplacer.SNIPPET_SRC_CALL_END, foundSnippets, + "
", "
", 1, "* ", false); + + assertTrue(opResult.result != null); + assertEquals(opResult.result.toString(),expectedString); + } + + @Test + public void testReadmeInsertion() + throws Exception { + /* + Ensures html encoding, empty and populated snippets replacement + */ + Path snippetSourceFile = _getPathToResource("../../project-to-test/basic_src_snippet_parse.txt"); + Path codeForReplacement = _getPathToResource("../../project-to-test/basic_readme_insertion_before.txt"); + Path expectedOutCome = _getPathToResource("../../project-to-test/basic_readme_insertion_after.txt"); + SnippetReplacer testReplacer = new SnippetReplacer(); + List testLines = Files.readAllLines(codeForReplacement, StandardCharsets.UTF_8); + byte[] rawBytes = Files.readAllBytes(expectedOutCome); + String expectedString = new String(rawBytes, StandardCharsets.UTF_8); + + HashMap> foundSnippets = testReplacer.getAllSnippets( + new ArrayList(Arrays.asList(snippetSourceFile.toAbsolutePath()))); + SnippetOperationResult opResult = testReplacer.updateSnippets(codeForReplacement, testLines, + testReplacer.SNIPPET_README_CALL_BEGIN, testReplacer.SNIPPET_README_CALL_END, foundSnippets, + "", "", 0, "", true); + + assertTrue(opResult.result != null); + assertTrue(opResult.result.toString().equals(expectedString)); + } + + @Test + public void testReadmeVerification() throws Exception { + Path snippetSourceFile = _getPathToResource("../../project-to-test/basic_src_snippet_parse.txt"); + Path verification = _getPathToResource("../../project-to-test/readme_insertion_verification_failure.txt"); + SnippetReplacer testReplacer = new SnippetReplacer(); + List testLines = Files.readAllLines(verification, StandardCharsets.UTF_8); + + HashMap> foundSnippets = testReplacer.getAllSnippets( + new ArrayList(Arrays.asList(snippetSourceFile.toAbsolutePath()))); + SnippetOperationResult> opResult = testReplacer.verifySnippets(verification, testLines, + testReplacer.SNIPPET_README_CALL_BEGIN, testReplacer.SNIPPET_README_CALL_END, foundSnippets, + "", "", 0, "", true); + + assertTrue(opResult.result != null); + assertTrue(opResult.result.size() == 1); + assertTrue(opResult.result.get(0).SnippetWithIssues.equals("com.azure.core.http.rest.pagedflux.instantiation")); + } + + @Test + public void testSrcVerification() throws Exception { + Path snippetSourceFile = _getPathToResource("../../project-to-test/basic_src_snippet_parse.txt"); + Path verification = _getPathToResource("../../project-to-test/src_insertion_verification_failure.txt"); + SnippetReplacer testReplacer = new SnippetReplacer(); + List testLines = Files.readAllLines(verification, StandardCharsets.UTF_8); + + HashMap> foundSnippets = testReplacer.getAllSnippets( + new ArrayList(Arrays.asList(snippetSourceFile.toAbsolutePath()))); + SnippetOperationResult> opResult = testReplacer.verifySnippets(verification, testLines, + testReplacer.SNIPPET_SRC_CALL_BEGIN, testReplacer.SNIPPET_SRC_CALL_END, foundSnippets, + "
", "
", 1, "* ", false); + + assertTrue(opResult.result != null); + assertTrue(opResult.result.size() == 2); + assertTrue(opResult.result.get(0).SnippetWithIssues.equals("com.azure.data.applicationconfig.configurationclient.instantiation")); + assertTrue(opResult.result.get(1).SnippetWithIssues.equals("com.azure.core.http.rest.pagedflux.instantiation")); + } + + @Test + public void emptySnippetWorks() throws Exception { + Path single = _getPathToResource("../../project-to-test/empty_snippet_def.txt"); + + List srcs = new ArrayList(Arrays.asList(single.toAbsolutePath())); + HashMap> foundSnippets = new SnippetReplacer().getAllSnippets(srcs); + + assertTrue(foundSnippets.keySet().size() == 1); + assertTrue(foundSnippets.containsKey("com.azure.data.applicationconfig.configurationclient.testEmpty")); + } + + @Test + public void duplicateSnippetsCrashWithSingleFile() { + try { + Path single = _getPathToResource("../../project-to-test/duplicate_snippet_src.txt"); + + List srcs = new ArrayList(Arrays.asList(single.toAbsolutePath())); + HashMap> foundSnippets = new SnippetReplacer().getAllSnippets(srcs); + } catch (Exception e){ + // check for snippet id in string + assertTrue(e.toString().contains("com.azure.data.applicationconfig.configurationclient.instantiation")); + } + } + + @Test + public void duplicateSnippetsCrashWithMultipleFiles() { + try { + Path single = _getPathToResource("../../project-to-test/duplicate_snippet_src.txt"); + Path multiple = _getPathToResource("../../project-to-test/duplicate_snippet_src_multiple.txt"); + + List srcs = new ArrayList(Arrays.asList(single.toAbsolutePath(), multiple.toAbsolutePath())); + HashMap> foundSnippets = new SnippetReplacer().getAllSnippets(srcs); + } catch (Exception e){ + // check for snippet id in string + assertTrue(e.toString().contains("com.azure.data.applicationconfig.configurationclient.instantiation")); + // should be one duplicate message from each file + assertTrue((e.toString().split("Duplicate snippetId", -1).length - 1) == 2); + } + } + + @Test + public void notFoundSnippetCrashes() throws IOException{ + HashMap> emptyMap = new HashMap>(); + + Path codeForReplacement = _getPathToResource("../../project-to-test/basic_src_snippet_insertion_before.txt"); + List testLines = Files.readAllLines(codeForReplacement, StandardCharsets.UTF_8); + SnippetReplacer testReplacer = new SnippetReplacer(); + + SnippetOperationResult opResult = testReplacer.updateSnippets(codeForReplacement, testLines, + testReplacer.SNIPPET_SRC_CALL_BEGIN, testReplacer.SNIPPET_SRC_CALL_END, emptyMap, "
",
+                "
", 1, "* ", false); + + assertTrue(opResult.errorList.size() == 3); + } +} diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_readme_insertion_after.txt b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_readme_insertion_after.txt new file mode 100644 index 000000000..5d48d2dac --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_readme_insertion_after.txt @@ -0,0 +1,23 @@ +##### Create a Configuration Client + +Once you have the value of the connection string you can create the configuration client: + +```Java com.azure.data.applicationconfig.configurationclient.instantiation +ConfigurationClient configurationClient = new ConfigurationClientBuilder() + .connectionString(connectionString) + .buildClient(); +``` + +or + +``` Java com.azure.core.http.rest.pagedflux.instantiation +// A supplier that fetches the first page of data from source/service +Supplier>> firstPageRetriever = () -> getFirstPage(); + +// A function that fetches subsequent pages of data from source/service given a continuation token +Function>> nextPageRetriever = + continuationToken -> getNextPage(continuationToken); + +PagedFlux pagedFlux = new PagedFlux<>(firstPageRetriever, + nextPageRetriever); +``` diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_readme_insertion_before.txt b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_readme_insertion_before.txt new file mode 100644 index 000000000..2e19c85d9 --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_readme_insertion_before.txt @@ -0,0 +1,15 @@ +##### Create a Configuration Client + +Once you have the value of the connection string you can create the configuration client: + +```Java com.azure.data.applicationconfig.configurationclient.instantiation +ConfigurationClient configurationClient = new ConfigurationClientBuilder() + .connectionString(connectionString) + .buildClient(); + Some other crap +``` + +or + +``` Java com.azure.core.http.rest.pagedflux.instantiation +``` diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_src_snippet_insertion_after.txt b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_src_snippet_insertion_after.txt new file mode 100644 index 000000000..f6822f0d5 --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_src_snippet_insertion_after.txt @@ -0,0 +1,86 @@ +import java.time.OffsetDateTime; + +/** + * This class provides a client that contains all the operations for {@link ConfigurationSetting ConfigurationSettings} + * in Azure App Configuration Store. Operations allowed by the client are adding, retrieving, deleting, set read-only + * status ConfigurationSettings, and listing settings or revision of a setting based on a + * {@link SettingSelector filter}. + * + *

Instantiating a synchronous Configuration Client

+ * + * + *
+ * ConfigurationClient configurationClient = new ConfigurationClientBuilder()
+ *     .connectionString(connectionString)
+ *     .buildClient();
+ * 
+ * + * + *

View {@link ConfigurationClientBuilder this} for additional ways to construct the client.

+ * + * @see ConfigurationClientBuilder + */ +@ServiceClient(builder = ConfigurationClientBuilder.class, serviceInterfaces = ConfigurationService.class) +public final class ConfigurationClient { + private final ConfigurationAsyncClient client; + + /** + * Creates a ConfigurationClient that sends requests to the configuration service at {@code serviceEndpoint}. Each + * service call goes through the {@code pipeline}. + * + * @param client The {@link ConfigurationAsyncClient} that the client routes its request through. + */ + ConfigurationClient(ConfigurationAsyncClient client) { + this.client = client; + } + + /** + * Adds a configuration value in the service if that key does not exist. The {@code label} is optional. + * + *

Code Samples

+ * + *

Add a setting with the key "prodDBConnection", label "westUS" and value "db_connection".

+ * + * + *
+     * ConfigurationSetting result = configurationClient
+     *     .addConfigurationSetting("prodDBConnection", "westUS", "db_connection");
+     * System.out.printf("Key: %s, Label: %s, Value: %s", result.getKey(), result.getLabel(), result.getValue());
+     * 
+ * + * + * @param key The key of the configuration setting to add. + * @param label The label of the configuration setting to create. If {@code null} no label will be used. + * @param value The value associated with this configuration setting key. + * @return The {@link ConfigurationSetting} that was created, or {@code null} if a key collision occurs or the key + * is an invalid value (which will also throw ServiceRequestException described below). + * @throws IllegalArgumentException If {@code key} is {@code null}. + * @throws ResourceModifiedException If a ConfigurationSetting with the same key exists. + * @throws HttpResponseException If {@code key} is an empty string. + */ + @ServiceMethod(returns = ReturnType.SINGLE) + public ConfigurationSetting addConfigurationSetting(String key, String label, String value) { + return addConfigurationSettingWithResponse( + new ConfigurationSetting().setKey(key).setLabel(label).setValue(value), Context.NONE).getValue(); + } + + /** + * + *
+     * // A supplier that fetches the first page of data from source/service
+     * Supplier<Mono<PagedResponse<Integer>>> firstPageRetriever = () -> getFirstPage();
+     *
+     * // A function that fetches subsequent pages of data from source/service given a continuation token
+     * Function<String, Mono<PagedResponse<Integer>>> nextPageRetriever =
+     *     continuationToken -> getNextPage(continuationToken);
+     *
+     * PagedFlux<Integer> pagedFlux = new PagedFlux<>(firstPageRetriever,
+     *     nextPageRetriever);
+     * 
+ * + */ + public ConfigurationSetting addConfigurationSetting(String key, String label, String value) { + return addConfigurationSettingWithResponse( + new ConfigurationSetting().setKey(key).setLabel(label).setValue(value), Context.NONE).getValue(); + } +} diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_src_snippet_insertion_before.txt b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_src_snippet_insertion_before.txt new file mode 100644 index 000000000..db2887ed9 --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_src_snippet_insertion_before.txt @@ -0,0 +1,66 @@ +import java.time.OffsetDateTime; + +/** + * This class provides a client that contains all the operations for {@link ConfigurationSetting ConfigurationSettings} + * in Azure App Configuration Store. Operations allowed by the client are adding, retrieving, deleting, set read-only + * status ConfigurationSettings, and listing settings or revision of a setting based on a + * {@link SettingSelector filter}. + * + *

Instantiating a synchronous Configuration Client

+ * + * + * Existing content waiting for cleanup. + * + * + *

View {@link ConfigurationClientBuilder this} for additional ways to construct the client.

+ * + * @see ConfigurationClientBuilder + */ +@ServiceClient(builder = ConfigurationClientBuilder.class, serviceInterfaces = ConfigurationService.class) +public final class ConfigurationClient { + private final ConfigurationAsyncClient client; + + /** + * Creates a ConfigurationClient that sends requests to the configuration service at {@code serviceEndpoint}. Each + * service call goes through the {@code pipeline}. + * + * @param client The {@link ConfigurationAsyncClient} that the client routes its request through. + */ + ConfigurationClient(ConfigurationAsyncClient client) { + this.client = client; + } + + /** + * Adds a configuration value in the service if that key does not exist. The {@code label} is optional. + * + *

Code Samples

+ * + *

Add a setting with the key "prodDBConnection", label "westUS" and value "db_connection".

+ * + * + * + * + * @param key The key of the configuration setting to add. + * @param label The label of the configuration setting to create. If {@code null} no label will be used. + * @param value The value associated with this configuration setting key. + * @return The {@link ConfigurationSetting} that was created, or {@code null} if a key collision occurs or the key + * is an invalid value (which will also throw ServiceRequestException described below). + * @throws IllegalArgumentException If {@code key} is {@code null}. + * @throws ResourceModifiedException If a ConfigurationSetting with the same key exists. + * @throws HttpResponseException If {@code key} is an empty string. + */ + @ServiceMethod(returns = ReturnType.SINGLE) + public ConfigurationSetting addConfigurationSetting(String key, String label, String value) { + return addConfigurationSettingWithResponse( + new ConfigurationSetting().setKey(key).setLabel(label).setValue(value), Context.NONE).getValue(); + } + + /** + * + * + */ + public ConfigurationSetting addConfigurationSetting(String key, String label, String value) { + return addConfigurationSettingWithResponse( + new ConfigurationSetting().setKey(key).setLabel(label).setValue(value), Context.NONE).getValue(); + } +} diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_src_snippet_parse.txt b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_src_snippet_parse.txt new file mode 100644 index 000000000..3baa9040d --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/basic_src_snippet_parse.txt @@ -0,0 +1,61 @@ + +/** + * This class contains code samples for generating javadocs through doclets for {@link ConfigurationClient} + */ +public final class ConfigurationClientJavaDocCodeSnippets { + + private String key1 = "key1"; + private String key2 = "key2"; + private String value1 = "val1"; + private String value2 = "val2"; + + /** + * Generates code sample for creating a {@link ConfigurationClient} + * + * @return An instance of {@link ConfigurationClient} + * @throws IllegalStateException If configuration credentials cannot be created. + */ + public ConfigurationClient createAsyncConfigurationClientWithPipeline() { + + /** + * Generates code sample for creating a {@link ConfigurationClient} + * + * @return An instance of {@link ConfigurationClient} + * @throws IllegalStateException If configuration credentials cannot be created + */ + public ConfigurationClient createSyncConfigurationClient() { + String connectionString = getConnectionString(); + // BEGIN: com.azure.data.applicationconfig.configurationclient.instantiation + ConfigurationClient configurationClient = new ConfigurationClientBuilder() + .connectionString(connectionString) + .buildClient(); + // END: com.azure.data.applicationconfig.configurationclient.instantiation + return configurationClient; + } + + /** + * Generates code sample for using {@link ConfigurationClient#addConfigurationSetting(String, String, String)} + */ + public void addConfigurationSetting() { + ConfigurationClient configurationClient = createSyncConfigurationClient(); + // BEGIN: com.azure.data.appconfiguration.ConfigurationClient.addConfigurationSetting#String-String-String + ConfigurationSetting result = configurationClient + .addConfigurationSetting("prodDBConnection", "westUS", "db_connection"); + System.out.printf("Key: %s, Label: %s, Value: %s", result.getKey(), result.getLabel(), result.getValue()); + // END: com.azure.data.appconfiguration.ConfigurationClient.addConfigurationSetting#String-String-String + } + + public void encodedHtmlWorks(){ + // BEGIN: com.azure.core.http.rest.pagedflux.instantiation + // A supplier that fetches the first page of data from source/service + Supplier>> firstPageRetriever = () -> getFirstPage(); + + // A function that fetches subsequent pages of data from source/service given a continuation token + Function>> nextPageRetriever = + continuationToken -> getNextPage(continuationToken); + + PagedFlux pagedFlux = new PagedFlux<>(firstPageRetriever, + nextPageRetriever); + // END: com.azure.core.http.rest.pagedflux.instantiation + } +} \ No newline at end of file diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/duplicate_snippet_src.txt b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/duplicate_snippet_src.txt new file mode 100644 index 000000000..1b269a4aa --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/duplicate_snippet_src.txt @@ -0,0 +1,19 @@ +public ConfigurationClient createSyncConfigurationClient() { + String connectionString = getConnectionString(); + // BEGIN: com.azure.data.applicationconfig.configurationclient.instantiation + ConfigurationClient configurationClient = new ConfigurationClientBuilder() + .connectionString(connectionString) + .buildClient(); + // END: com.azure.data.applicationconfig.configurationclient.instantiation + return configurationClient; +} + +public ConfigurationClient createSyncConfigurationClient() { + String connectionString = getConnectionString(); + // BEGIN: com.azure.data.applicationconfig.configurationclient.instantiation + ConfigurationClient configurationClient = new ConfigurationClientBuilder() + .connectionString(connectionString) + .buildClient(); + // END: com.azure.data.applicationconfig.configurationclient.instantiation + return configurationClient; +} diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/duplicate_snippet_src_multiple.txt b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/duplicate_snippet_src_multiple.txt new file mode 100644 index 000000000..660ad91c3 --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/duplicate_snippet_src_multiple.txt @@ -0,0 +1,9 @@ +public ConfigurationClient createSyncConfigurationClient() { + String connectionString = getConnectionString(); + // BEGIN: com.azure.data.applicationconfig.configurationclient.instantiation + ConfigurationClient configurationClient = new ConfigurationClientBuilder() + .connectionString(connectionString) + .buildClient(); + // END: com.azure.data.applicationconfig.configurationclient.instantiation + return configurationClient; +} \ No newline at end of file diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/empty_snippet_def.txt b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/empty_snippet_def.txt new file mode 100644 index 000000000..5ef1f9b61 --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/empty_snippet_def.txt @@ -0,0 +1,6 @@ +public ConfigurationClient createSyncConfigurationClient() { + String connectionString = getConnectionString(); + // BEGIN: com.azure.data.applicationconfig.configurationclient.testEmpty + // END: com.azure.data.applicationconfig.configurationclient.testEmpty + return configurationClient; +} \ No newline at end of file diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/pom.xml b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/pom.xml new file mode 100644 index 000000000..e71677f60 --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + azuresdk.plugin + snippet-replacer-maven-plugin + 1.0-SNAPSHOT + jar + Test MyMojo + + + + + maven-my-plugin + + + target/test-harness/project-to-test + + + + + diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/readme_insertion_verification_failure.txt b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/readme_insertion_verification_failure.txt new file mode 100644 index 000000000..8733fb877 --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/readme_insertion_verification_failure.txt @@ -0,0 +1,20 @@ +##### Create a Configuration Client + +Once you have the value of the connection string you can create the configuration client: + +```Java com.azure.data.applicationconfig.configurationclient.instantiation +ConfigurationClient configurationClient = new ConfigurationClientBuilder() + .connectionString(connectionString) + .buildClient(); +``` + +or + +``` Java com.azure.core.http.rest.pagedflux.instantiation +Supplier>> firstPageRetriever = () -> getFirstPage(); + +// A function that fetches subsequent pages of data from source/service given a continuation token +Function>> nextPageRetriever = + continuationToken -> getNextPage(continuationToken); + +``` diff --git a/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/src_insertion_verification_failure.txt b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/src_insertion_verification_failure.txt new file mode 100644 index 000000000..d04f740bd --- /dev/null +++ b/packages/java-packages/snippet-replacer-maven-plugin/src/test/resources/project-to-test/src_insertion_verification_failure.txt @@ -0,0 +1,84 @@ +import java.time.OffsetDateTime; + +/** + * This class provides a client that contains all the operations for {@link ConfigurationSetting ConfigurationSettings} + * in Azure App Configuration Store. Operations allowed by the client are adding, retrieving, deleting, set read-only + * status ConfigurationSettings, and listing settings or revision of a setting based on a + * {@link SettingSelector filter}. + * + *

Instantiating a synchronous Configuration Client

+ * + * + *
+ * ConfigurationClient configurationClient = new ConfigurationClientBuilder()
+ *     .connectionString(connectionString)
+ *     .buildClient()
+ * 
+ * + * + *

View {@link ConfigurationClientBuilder this} for additional ways to construct the client.

+ * + * @see ConfigurationClientBuilder + */ +@ServiceClient(builder = ConfigurationClientBuilder.class, serviceInterfaces = ConfigurationService.class) +public final class ConfigurationClient { + private final ConfigurationAsyncClient client; + + /** + * Creates a ConfigurationClient that sends requests to the configuration service at {@code serviceEndpoint}. Each + * service call goes through the {@code pipeline}. + * + * @param client The {@link ConfigurationAsyncClient} that the client routes its request through. + */ + ConfigurationClient(ConfigurationAsyncClient client) { + this.client = client; + } + + /** + * Adds a configuration value in the service if that key does not exist. The {@code label} is optional. + * + *

Code Samples

+ * + *

Add a setting with the key "prodDBConnection", label "westUS" and value "db_connection".

+ * + * + *
+     * ConfigurationSetting result = configurationClient
+     *     .addConfigurationSetting("prodDBConnection", "westUS", "db_connection");
+     * System.out.printf("Key: %s, Label: %s, Value: %s", result.getKey(), result.getLabel(), result.getValue());
+     * 
+ * + * + * @param key The key of the configuration setting to add. + * @param label The label of the configuration setting to create. If {@code null} no label will be used. + * @param value The value associated with this configuration setting key. + * @return The {@link ConfigurationSetting} that was created, or {@code null} if a key collision occurs or the key + * is an invalid value (which will also throw ServiceRequestException described below). + * @throws IllegalArgumentException If {@code key} is {@code null}. + * @throws ResourceModifiedException If a ConfigurationSetting with the same key exists. + * @throws HttpResponseException If {@code key} is an empty string. + */ + @ServiceMethod(returns = ReturnType.SINGLE) + public ConfigurationSetting addConfigurationSetting(String key, String label, String value) { + return addConfigurationSettingWithResponse( + new ConfigurationSetting().setKey(key).setLabel(label).setValue(value), Context.NONE).getValue(); + } + + /** + * + *
+     * // A supplier that fetches the first page of data from source/service
+     * Supplier<Mono<PagedResponse<Integer>>> firstPageRetriever = () -> getFirstPage();
+     *
+     * // A function that fetches subsequent pages of data from source/service given a continuation token
+     * Function<String, Mono<PagedResponse<Integer>>> nextPageRetriever =
+     *     continuationToken -> getNextPage(continuationToken);
+     *
+     * 
+ * + */ + public ConfigurationSetting addConfigurationSetting(String key, String label, String value) { + return addConfigurationSettingWithResponse( + new ConfigurationSetting().setKey(key).setLabel(label).setValue(value), Context.NONE).getValue(); + } +}