diff --git a/eng/.docsettings.yml b/eng/.docsettings.yml index 37b3255841d1..fdd691ebebeb 100644 --- a/eng/.docsettings.yml +++ b/eng/.docsettings.yml @@ -140,6 +140,7 @@ known_content_issues: - ['sdk/storage/azure-storage-internal-avro/README.md', '#3113'] - ['sdk/storage/README.md', '#3113'] - ['sdk/tools/azure-sdk-archetype/README.md', '#3113'] + - ['sdk/tools/azure-sdk-build-tool/README.md', '#3113'] - ['sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/README.md', '#3113'] - ['sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/README.md', '#3113'] - ['sdk/appconfiguration/azure-spring-cloud-feature-management-web/README.md', '#3113'] diff --git a/sdk/tools/azure-sdk-build-tool/CHANGELOG.md b/sdk/tools/azure-sdk-build-tool/CHANGELOG.md new file mode 100644 index 000000000000..48eeef7b02f0 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/CHANGELOG.md @@ -0,0 +1,5 @@ +# Release History + +## 1.0.0-beta.1 (Unreleased) + +### Features Added diff --git a/sdk/tools/azure-sdk-build-tool/README.md b/sdk/tools/azure-sdk-build-tool/README.md new file mode 100644 index 000000000000..3d950a95374e --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/README.md @@ -0,0 +1,41 @@ +# Azure SDK Maven Build Tool + +The Azure SDK for Java project ships a Maven build tool that developers can choose to include in their projects. This tool runs locally and does not transmit any data to Microsoft. It can be configured to generate a report or fail the build when certain conditions are met, which is useful to ensure compliance with numerous best practices. These include: + +- Validating the correct use of the azure-sdk-for-java BOM, including using the latest version and relying on it to +define dependency versions on Azure SDK for Java client libraries. +- Validating that historical Azure client libraries are not being used when newer and improved versions exist. +- Providing insight into usage of beta APIs. + +The build tool can be configured in a project Maven POM file as such: + +```xml + + + + com.azure.tools + azure-sdk-build-tool + {latest_version} + + ... + + + + +``` +Within the configuration section, it is possible to configure the settings in the table below if desired, but by default they are configured with the recommended settings. Because of this, it is ok to not have any configuration specified at all. + + +| Property | Default Value | Description | +|------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| validateAzureSdkBomUsed | true | Ensures that the build has the azure-sdk-for-java BOM referenced appropriately, so that Azure SDK for Java client library dependencies may take their versions from the BOM. | +| validateBomVersionsAreUsed | true | Ensures that where a dependency is available from the azure-sdk-for-java BOM the version is not being manually overridden. | +| validateNoDeprecatedMicrosoftLibraryUsed | true | Ensures that the project does not make use of previous-generation Azure libraries. Using the new and previous-generation libraries in a single project is unlikely to cause any issue, but is will result in a sub-optimal developer experience. | +| validateNoBetaLibraryUsed | false | Some Azure SDK for Java client libraries have beta releases, with version strings in the form x.y.z-beta.n. Enabling this feature will ensure that no beta libraries are being used. | +| validateNoBetaAPIUsed | true | Azure SDK for Java client libraries sometimes do GA releases with methods annotated with @Beta. This check looks to see if any such methods are being used. | +| validateLatestBomVersionUsed | true | Ensures that dependencies are kept up to date by reporting back (or failing the build) if a newer azure-sdk-for-java BOM exists. | +| reportFile | "" | (Optional) Specifies the location to write the build report out to, in JSON format. If not specified, no report will be written (and a summary of the build, or the appropriate build failures), will be shown in the terminal. | +After adding the build tool into a Maven project, the tool can be run by calling `mvn compile azure:run`. Depending on +the configuration provided, you can expect to see build failures or report files generated that can inform you about potential issues before they become more serious. + +As the build tool evolves, new releases will be published, and it is recommended that developers frequently check for new releases and update as appropriate. diff --git a/sdk/tools/azure-sdk-build-tool/pom.xml b/sdk/tools/azure-sdk-build-tool/pom.xml new file mode 100644 index 000000000000..545cab10a729 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/pom.xml @@ -0,0 +1,153 @@ + + + 4.0.0 + + com.azure.tools + azure-sdk-build-tool + maven-plugin + 1.0.0-beta.1 + + Azure SDK for Java Maven Build Tool + A tool that makes working with the Azure SDK for Java more productive. + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + https://github.com/azure/azure-sdk-for-java + + Microsoft Corporation + http://microsoft.com + + + + The MIT License (MIT) + http://opensource.org/licenses/MIT + repo + + + + + + microsoft + Microsoft Corporation + + + + + GitHub + https://github.com/Azure/azure-sdk-for-java/issues + + + + https://github.com/Azure/azure-sdk-for-java + scm:git:https://github.com/Azure/azure-sdk-for-java.git + + HEAD + + + + 8 + 8 + + + + + + org.apache.maven + maven-plugin-api + 3.8.1 + + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.6.0 + + + org.apache.maven + maven-project + 2.2.1 + + + junit + junit + + + + + com.github.javaparser + javaparser-core + 3.20.2 + + + + + net.oneandone.reflections8 + reflections8 + 0.11.7 + + + + com.azure + azure-monitor-opentelemetry-exporter + 1.0.0-beta.5 + + + + + org.junit.jupiter + junit-jupiter-api + 5.8.2 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.8.2 + test + + + org.junit.jupiter + junit-jupiter-params + 5.8.2 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + org.apache.maven.plugins + maven-plugin-plugin + 3.6.0 + + + + mojo-descriptor + + descriptor + + + + + azure + + + + + + diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/AnnotationProcessingTool.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/AnnotationProcessingTool.java new file mode 100644 index 000000000000..ae4e9a7c6c42 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/AnnotationProcessingTool.java @@ -0,0 +1,98 @@ +package com.azure.sdk.build.tool; + +import com.azure.sdk.build.tool.mojo.AzureSdkMojo; +import com.azure.sdk.build.tool.util.AnnotatedMethodCallerResult; +import com.azure.sdk.build.tool.util.logging.Logger; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Stream; + +import static com.azure.sdk.build.tool.util.AnnotationUtils.findCallsToAnnotatedMethod; +import static com.azure.sdk.build.tool.util.AnnotationUtils.getAnnotation; +import static com.azure.sdk.build.tool.util.AnnotationUtils.getCompleteClassLoader; +import static com.azure.sdk.build.tool.util.MojoUtils.failOrWarn; +import static com.azure.sdk.build.tool.util.MojoUtils.getAllDependencies; +import static com.azure.sdk.build.tool.util.MojoUtils.getCompileSourceRoots; +import static com.azure.sdk.build.tool.util.MojoUtils.getString; + +/** + * Performs the following tasks: + * + * + */ +public class AnnotationProcessingTool implements Runnable { + private static Logger LOGGER = Logger.getInstance(); + + /** + * Runs the annotation processing task to look for @ServiceMethod and @Beta usage. + */ + public void run() { + LOGGER.info("Running Annotation Processing Tool"); + + // We build up a list of packages in the source of the user maven project, so that we only report on the + // usage of annotation methods from code within these packages + final Set interestedPackages = new TreeSet<>(Comparator.comparingInt(String::length)); + getCompileSourceRoots().forEach(root -> buildPackageList(root, root, interestedPackages)); + + final ClassLoader classLoader = getCompleteClassLoader(getAllPaths()); + + // Collect all calls to methods annotated with the Azure SDK @ServiceMethod annotation + getAnnotation("com.azure.core.annotation.ServiceMethod", classLoader) + .map(a -> findCallsToAnnotatedMethod(a, getAllPaths(), interestedPackages, true)) + .ifPresent(AzureSdkMojo.MOJO.getReport()::setServiceMethodCalls); + + // Collect all calls to methods annotated with the Azure SDK @Beta annotation + Optional> annotatedMethodCallerResults = getAnnotation("com.azure.cosmos.util.Beta", classLoader) + .map(a -> findCallsToAnnotatedMethod(a, getAllPaths(), interestedPackages, true)); + if (annotatedMethodCallerResults.isPresent()) { + Set betaMethodCallers = annotatedMethodCallerResults.get(); + AzureSdkMojo.MOJO.getReport().setBetaMethodCalls(betaMethodCallers); + if (!betaMethodCallers.isEmpty()) { + StringBuilder message = new StringBuilder(); + message.append(getString("betaApiUsed")).append(System.lineSeparator()); + betaMethodCallers.forEach(method -> message.append(" - ").append(method.toString()).append(System.lineSeparator())); + failOrWarn(() -> AzureSdkMojo.MOJO.isValidateNoBetaApiUsed(), message.toString()); + } + } + } + + private static Stream getAllPaths() { + // This is the user maven build target directory - we look in here for the compiled source code + final File targetDir = new File(AzureSdkMojo.MOJO.getProject().getBuild().getDirectory() + "/classes/"); + + // this stream of paths is a stream containing the users maven project compiled class files, as well as all + // jar file dependencies. We use this to analyse the use of annotations and report back to the user. + return Stream.concat( + Stream.of(targetDir.getAbsolutePath()), + getAllDependencies().stream().map(a -> a.getFile().getAbsolutePath())) + .map(Paths::get); + } + + private static void buildPackageList(String rootDir, String currentDir, Set packages) { + final File directory = new File(currentDir); + + final File[] files = directory.listFiles(); + if (files == null) { + return; + } + + for (final File file : files) { + if (file.isFile()) { + final String path = file.getPath(); + final String packageName = path.substring(rootDir.length() + 1, path.lastIndexOf(File.separator)); + packages.add(packageName.replace(File.separatorChar, '.')); + } else if (file.isDirectory()) { + buildPackageList(rootDir, file.getAbsolutePath(), packages); + } + } + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/AzureDependencyMapping.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/AzureDependencyMapping.java new file mode 100644 index 000000000000..edcdf2a9b620 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/AzureDependencyMapping.java @@ -0,0 +1,81 @@ +package com.azure.sdk.build.tool; + +import com.azure.sdk.build.tool.models.OutdatedDependency; +import com.azure.sdk.build.tool.util.MavenUtils; +import com.azure.sdk.build.tool.util.logging.Logger; +import org.apache.maven.artifact.Artifact; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Contains the mapping for outdated dependencies and it's replacement. + */ +public class AzureDependencyMapping { + private static Logger LOGGER = Logger.getInstance(); + + private static final String TRACK_ONE_GROUP_ID = "com.microsoft.azure"; + private static final String TRACK_TWO_GROUP_ID = "com.azure"; + + // This map is for all com.microsoft.* group IDs, mapping them into their com.azure equivalents + private static final Map> TRACK_ONE_REDIRECTS = new HashMap<>(); + static { + // Cosmos + TRACK_ONE_REDIRECTS.put("azure-cosmosdb", Collections.singletonList("azure-cosmos")); + + // Key Vault - Track 1 KeyVault library is split into three Track 2 libraries + TRACK_ONE_REDIRECTS.put("azure-keyvault", + Arrays.asList("azure-security-keyvault-keys", + "azure-security-keyvault-certificates", + "azure-security-keyvault-secrets")); + + // Blob Storage + TRACK_ONE_REDIRECTS.put("azure-storage-blob", Collections.singletonList("azure-storage-blob")); + + // Event Hubs + TRACK_ONE_REDIRECTS.put("azure-eventhubs", Collections.singletonList("azure-messaging-eventhubs")); + TRACK_ONE_REDIRECTS.put("azure-eventhubs-eph", Collections.singletonList("azure-messaging-eventhubs-checkpointstore-blob")); + + // Service Bus + TRACK_ONE_REDIRECTS.put("azure-servicebus", Collections.singletonList("azure-messaging-servicebus")); + + // Event Grid + TRACK_ONE_REDIRECTS.put("azure-eventgrid", Collections.singletonList("azure-messaging-eventgrid")); + + // Log Analytics + TRACK_ONE_REDIRECTS.put("azure-loganalytics", Collections.singletonList("azure-monitor-query")); + } + + /** + * This method will look to see if we have any recorded guidance on how to replace the given artifact with + * something else. + * + * @param artifact The artifact for which we want to find a replacement + * @return An {@link OutdatedDependency} if a replacement for the given {@code artifact} exists. + */ + public static Optional lookupReplacement(Artifact artifact) { + String groupId = artifact.getGroupId(); + String artifactId = artifact.getArtifactId(); + + if (TRACK_ONE_GROUP_ID.equals(groupId)) { + if (TRACK_ONE_REDIRECTS.containsKey(artifactId)) { + final List newArtifactIds = TRACK_ONE_REDIRECTS.get(artifactId); + + List newGavs = newArtifactIds.stream() + .map(newArtifactId -> TRACK_TWO_GROUP_ID + ":" + newArtifactId + ":" + MavenUtils.getLatestArtifactVersion(TRACK_TWO_GROUP_ID, newArtifactId)).collect(Collectors.toList()); + return Optional.of(new OutdatedDependency(MavenUtils.toGAV(artifact), newGavs)); + } else { + // we've hit artifact location where we don't know know if the com.microsoft.azure artifact has artifact newer + // replacement...For now we will not give artifact failure + return Optional.empty(); + } + } + + return Optional.empty(); + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/DependencyCheckerTool.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/DependencyCheckerTool.java new file mode 100644 index 000000000000..4022201f9876 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/DependencyCheckerTool.java @@ -0,0 +1,142 @@ +package com.azure.sdk.build.tool; + +import com.azure.sdk.build.tool.models.OutdatedDependency; +import com.azure.sdk.build.tool.mojo.AzureSdkMojo; +import com.azure.sdk.build.tool.util.MavenUtils; +import com.azure.sdk.build.tool.util.logging.Logger; +import org.apache.maven.model.Dependency; +import org.apache.maven.model.DependencyManagement; +import org.apache.maven.model.InputLocation; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.azure.sdk.build.tool.util.MojoUtils.failOrWarn; +import static com.azure.sdk.build.tool.util.MojoUtils.getAllDependencies; +import static com.azure.sdk.build.tool.util.MojoUtils.getDirectDependencies; +import static com.azure.sdk.build.tool.util.MojoUtils.getString; + +/** + * Performs the following tasks: + * + *
    + *
  • Warnings about missing BOM.
  • + *
  • Warnings about not using the latest available version of BOM.
  • + *
  • Warnings about explicit dependency versions.
  • + *
  • Warnings about dependency clashes between Azure libraries and other dependencies.
  • + *
  • Warnings about using track one libraries.
  • + *
  • Warnings about out of date track two dependencies (BOM and individual libraries).
  • + *
+ */ +public class DependencyCheckerTool implements Runnable { + private static Logger LOGGER = Logger.getInstance(); + + private static final String AZURE_SDK_BOM_ARTIFACT_ID = "azure-sdk-bom"; + private static final String COM_MICROSOFT_AZURE_GROUP_ID = "com.microsoft.azure"; + + public void run() { + LOGGER.info("Running Dependency Checker Tool"); + + checkForBom(); + checkForAzureSdkTrackOneDependencies(); + } + + private void checkForBom() { + // we are looking for the azure-sdk-bom artifact ID listed as a dependency in the dependency management section + DependencyManagement depMgmt = AzureSdkMojo.MOJO.getProject().getDependencyManagement(); + DependencyManagement originalDepMgmt = + AzureSdkMojo.MOJO.getProject().getOriginalModel().getDependencyManagement(); + + Optional bomDependency = Optional.empty(); + Optional originalBomDependency = Optional.empty(); + if (depMgmt != null) { + bomDependency = depMgmt.getDependencies().stream() + .filter(d -> d.getArtifactId().equals(AZURE_SDK_BOM_ARTIFACT_ID)) + .findAny(); + } + + if (originalDepMgmt != null) { + originalBomDependency = originalDepMgmt.getDependencies().stream() + .filter(d -> d.getArtifactId().equals(AZURE_SDK_BOM_ARTIFACT_ID)) + .findAny(); + } + + bomDependency = bomDependency.isPresent() ? bomDependency : originalBomDependency; + + if (bomDependency.isPresent()) { + String latestAvailableBomVersion = MavenUtils.getLatestArtifactVersion("com.azure", "azure-sdk-bom"); + boolean isLatestBomVersion = bomDependency.get().getVersion().equals(latestAvailableBomVersion); + if (!isLatestBomVersion) { + failOrWarn(AzureSdkMojo.MOJO::isValidateAzureSdkBomUsed, getString("outdatedBomDependency")); + } + checkForAzureSdkDependencyVersions(); + } else { + failOrWarn(AzureSdkMojo.MOJO::isValidateAzureSdkBomUsed, getString("missingBomDependency")); + } + } + + private void checkForAzureSdkDependencyVersions() { + List dependencies = AzureSdkMojo.MOJO.getProject().getDependencies(); + List dependenciesWithOverriddenVersions = dependencies.stream() + .filter(dependency -> dependency.getGroupId().equals("com.azure")) + .filter(dependency -> { + InputLocation location = dependency.getLocation("version"); + // if the version is not coming from Azure SDK BOM, filter those dependencies + return !location.getSource().getModelId().startsWith("com.azure:azure-sdk-bom"); + }).collect(Collectors.toList()); + + dependenciesWithOverriddenVersions.forEach(dependency -> failOrWarn(AzureSdkMojo.MOJO::isValidateBomVersionsAreUsed, + dependency.getArtifactId() + " " + getString("overrideBomVersion"))); + + List betaDependencies = dependencies.stream() + .filter(dependency -> dependency.getGroupId().equals("com.azure")) + .filter(dependency -> dependency.getVersion().contains("-beta")) + .collect(Collectors.toList()); + + betaDependencies.forEach(dependency -> failOrWarn(AzureSdkMojo.MOJO::isValidateNoBetaLibraryUsed, + dependency.getArtifactId() + " " + getString("betaDependencyUsed"))); + } + + private void checkForAzureSdkTrackOneDependencies() { + // Check direct dependencies first for any 'com.microsoft.azure' group IDs. These are under the users direct + // control, so they could try to upgrade to a newer 'com.azure' version instead. + Set outdatedDirectDependencies = getDirectDependencies().stream() + .filter(a -> COM_MICROSOFT_AZURE_GROUP_ID.equals(a.getGroupId())) + .map(AzureDependencyMapping::lookupReplacement) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + + // check indirect dependencies too, but filter out any dependencies we've already discovered above + Set outdatedTransitiveDependencies = getAllDependencies().stream() + .filter(d -> COM_MICROSOFT_AZURE_GROUP_ID.equals(d.getGroupId())) + .map(AzureDependencyMapping::lookupReplacement) + .filter(Optional::isPresent) + .map(Optional::get) + .filter(d -> !outdatedDirectDependencies.contains(d)) + .collect(Collectors.toSet()); + + // The report is only concerned with GAV, so we simplify it here + AzureSdkMojo.MOJO.getReport().setOutdatedDirectDependencies(outdatedDirectDependencies); + AzureSdkMojo.MOJO.getReport().setOutdatedTransitiveDependencies(outdatedTransitiveDependencies); + + if (!outdatedDirectDependencies.isEmpty()) { + // convert each track one dependency into actionable guidance + String message = getString("deprecatedDirectDependency"); + for (OutdatedDependency outdatedDependency : outdatedDirectDependencies) { + message += "\n - " + outdatedDependency.getGav() + " --> " + outdatedDependency.getSuggestedReplacementGav(); + } + failOrWarn(AzureSdkMojo.MOJO::isValidateNoDeprecatedMicrosoftLibraryUsed, message); + } + if (!outdatedTransitiveDependencies.isEmpty()) { + // convert each track one dependency into actionable guidance + String message = getString("deprecatedIndirectDependency"); + for (OutdatedDependency outdatedDependency : outdatedDirectDependencies) { + message += "\n - " + outdatedDependency.getGav(); + } + failOrWarn(AzureSdkMojo.MOJO::isValidateNoDeprecatedMicrosoftLibraryUsed, message); + } + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/ReportGenerator.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/ReportGenerator.java new file mode 100644 index 000000000000..4d1c23510c64 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/ReportGenerator.java @@ -0,0 +1,150 @@ +package com.azure.sdk.build.tool; + +import com.azure.sdk.build.tool.models.BuildReport; +import com.azure.sdk.build.tool.mojo.AzureSdkMojo; +import com.azure.sdk.build.tool.util.AnnotatedMethodCallerResult; +import com.azure.sdk.build.tool.util.MavenUtils; +import com.azure.sdk.build.tool.util.logging.Logger; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import org.apache.maven.model.Dependency; +import org.apache.maven.model.DependencyManagement; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static com.azure.sdk.build.tool.util.MojoUtils.getAllDependencies; + +/** + * The tool to generate the final report of the build. + */ +public class ReportGenerator { + private static final Logger LOGGER = Logger.getInstance(); + private static final String AZURE_DEPENDENCY_GROUP = "com.azure"; + private static final String AZURE_SDK_BOM_ARTIFACT_ID = "azure-sdk-bom"; + private final BuildReport report; + + public ReportGenerator(BuildReport report) { + this.report = report; + } + + public void generateReport() { + if (!report.getWarningMessages().isEmpty() && LOGGER.isWarnEnabled()) { + report.getWarningMessages().forEach(LOGGER::warn); + } + if (!report.getErrorMessages().isEmpty() && LOGGER.isErrorEnabled()) { + report.getErrorMessages().forEach(LOGGER::error); + } + report.setBomVersion(computeBomVersion()); + report.setAzureDependencies(computeAzureDependencies()); + + createJsonReport(); + // we throw a single runtime exception encapsulating all failure messages into one + if (!report.getFailureMessages().isEmpty()) { + StringBuilder sb = new StringBuilder("Build failure for the following reasons:\n"); + report.getFailureMessages().forEach(s -> sb.append(" - " + s + "\n")); + throw new RuntimeException(sb.toString()); + } + } + + private String computeBomVersion() { + DependencyManagement depMgmt = AzureSdkMojo.MOJO.getProject().getDependencyManagement(); + Optional bomDependency = Optional.empty(); + if (depMgmt != null) { + bomDependency = depMgmt.getDependencies().stream() + .filter(d -> d.getArtifactId().equals(AZURE_SDK_BOM_ARTIFACT_ID)) + .findAny(); + } + + if (bomDependency.isPresent()) { + return bomDependency.get().getVersion(); + } + return null; + } + + private void createJsonReport() { + + try { + StringWriter writer = new StringWriter(); + JsonGenerator generator = new JsonFactory().createGenerator(writer).useDefaultPrettyPrinter(); + + generator.writeStartObject(); + generator.writeStringField("group", AzureSdkMojo.MOJO.getProject().getGroupId()); + generator.writeStringField("artifact", AzureSdkMojo.MOJO.getProject().getArtifactId()); + generator.writeStringField("version", AzureSdkMojo.MOJO.getProject().getVersion()); + generator.writeStringField("name", AzureSdkMojo.MOJO.getProject().getName()); + if (report.getBomVersion() != null && !report.getBomVersion().isEmpty()) { + generator.writeStringField("bomVersion", report.getBomVersion()); + } + if (report.getAzureDependencies() != null && !report.getAzureDependencies().isEmpty()) { + writeArray("azureDependencies", report.getAzureDependencies(), generator); + } + + if (report.getServiceMethodCalls() != null && !report.getServiceMethodCalls().isEmpty()) { + writeArray("serviceMethodCalls", report.getServiceMethodCalls() + .stream() + .map(AnnotatedMethodCallerResult::toString) + .collect(Collectors.toList()), generator); + } + + if (report.getBetaMethodCalls() != null && !report.getBetaMethodCalls().isEmpty()) { + writeArray("betaMethodCalls", report.getBetaMethodCalls() + .stream() + .map(AnnotatedMethodCallerResult::toString) + .collect(Collectors.toList()), generator); + } + + + if (!report.getErrorMessages().isEmpty()) { + writeArray("errorMessages", report.getErrorMessages(), generator); + } + + if (!report.getWarningMessages().isEmpty()) { + writeArray("warningMessages", report.getWarningMessages(), generator); + } + + if (!report.getFailureMessages().isEmpty()) { + writeArray("failureMessages", report.getFailureMessages(), generator); + } + + generator.writeEndObject(); + generator.close(); + writer.close(); + + report.setJsonReport(writer.toString()); + final String reportFileString = AzureSdkMojo.MOJO.getReportFile(); + if (reportFileString != null && !reportFileString.isEmpty()) { + final File reportFile = new File(reportFileString); + try (FileWriter fileWriter = new FileWriter(reportFile)) { + fileWriter.write(report.getJsonReport()); + } + } + } catch (IOException exception) { + + } + } + + private void writeArray(String fieldName, Collection values, JsonGenerator generator) throws IOException { + generator.writeFieldName(fieldName); + generator.writeStartArray(); + for (String value : values) { + generator.writeString(value); + } + generator.writeEndArray(); + } + + private List computeAzureDependencies() { + return getAllDependencies().stream() + // this includes Track 2 mgmt libraries, spring libraries and data plane libraries + .filter(artifact -> artifact.getGroupId().startsWith(AZURE_DEPENDENCY_GROUP)) + .map(MavenUtils::toGAV) + .collect(Collectors.toList()); + } +} + diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/Tools.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/Tools.java new file mode 100644 index 000000000000..fae94f888ef2 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/Tools.java @@ -0,0 +1,23 @@ +package com.azure.sdk.build.tool; + +import java.util.ArrayList; +import java.util.List; + +/** + * A class containing list of build tools that can be executed when the plugin is run. + */ +public class Tools { + private static final List TOOLS = new ArrayList<>(); + static { + TOOLS.add(new DependencyCheckerTool()); + TOOLS.add(new AnnotationProcessingTool()); + } + + /** + * Returns the list of tools available to run. + * @return The list of tools. + */ + public static List getTools() { + return TOOLS; + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/models/BuildReport.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/models/BuildReport.java new file mode 100644 index 000000000000..725813775c52 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/models/BuildReport.java @@ -0,0 +1,104 @@ +package com.azure.sdk.build.tool.models; + +import com.azure.sdk.build.tool.util.AnnotatedMethodCallerResult; +import com.azure.sdk.build.tool.util.logging.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * The build report that contains detailed information about the build including failure messages, recommended + * changes and Azure SDK usage. + */ +public class BuildReport { + private final List warningMessages; + private final List errorMessages; + private final List failureMessages; + + private List azureDependencies; + private Set serviceMethodCalls; + private Set betaMethodCalls; + private Set outdatedDirectDependencies; + private Set outdatedTransitiveDependencies; + private String bomVersion; + private String jsonReport; + + public List getWarningMessages() { + return warningMessages; + } + + public List getErrorMessages() { + return errorMessages; + } + + public List getFailureMessages() { + return failureMessages; + } + + public BuildReport() { + this.warningMessages = new ArrayList<>(); + this.errorMessages = new ArrayList<>(); + this.failureMessages = new ArrayList<>(); + } + + public String getBomVersion() { + return this.bomVersion; + } + + public List getAzureDependencies() { + return this.azureDependencies; + } + + public void addWarningMessage(String message) { + warningMessages.add(message); + } + + public void addErrorMessage(String message) { + errorMessages.add(message); + } + + public void addFailureMessage(String message) { + failureMessages.add(message); + } + + public void setServiceMethodCalls(Set serviceMethodCalls) { + this.serviceMethodCalls = serviceMethodCalls; + } + + public void setBetaMethodCalls(Set betaMethodCalls) { + this.betaMethodCalls = betaMethodCalls; + } + + public void setOutdatedDirectDependencies(Set outdatedDirectDependencies) { + this.outdatedDirectDependencies = outdatedDirectDependencies; + } + + public void setOutdatedTransitiveDependencies(Set outdatedTransitiveDependencies) { + this.outdatedTransitiveDependencies = outdatedTransitiveDependencies; + } + + public void setBomVersion(String bomVersion) { + this.bomVersion = bomVersion; + } + + public void setAzureDependencies(List azureDependencies) { + this.azureDependencies = azureDependencies; + } + + public void setJsonReport(String jsonReport) { + this.jsonReport = jsonReport; + } + + public String getJsonReport() { + return jsonReport; + } + + public Set getServiceMethodCalls() { + return this.serviceMethodCalls; + } + + public Set getBetaMethodCalls() { + return this.betaMethodCalls; + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/models/OutdatedDependency.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/models/OutdatedDependency.java new file mode 100644 index 000000000000..88f604b77719 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/models/OutdatedDependency.java @@ -0,0 +1,55 @@ +package com.azure.sdk.build.tool.models; + +import java.util.List; +import java.util.Objects; + +/** + * A model class to represent an outdated dependency and a list of suggested replacements. + */ +public class OutdatedDependency { + private final String gav; + private final List suggestedReplacementGav; + + /** + * Creates an instance of {@link OutdatedDependency}. + * @param gav The group, artifact and version string. + * @param suggestedReplacementGav The suggested replacement for the outdated dependency. + */ + public OutdatedDependency(final String gav, final List suggestedReplacementGav) { + this.gav = gav; + this.suggestedReplacementGav = suggestedReplacementGav; + } + + /** + * Returns the group, artifact and version string for the outdated dependency. + * @return The group, artifact and version string. + */ + public String getGav() { + return gav; + } + + /** + * Returns the list of suggested replacements for the outdated dependency. + * @return The list of suggested replacements for the outdated dependency. + */ + public List getSuggestedReplacementGav() { + return suggestedReplacementGav; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final OutdatedDependency that = (OutdatedDependency) o; + return gav.equals(that.gav); + } + + @Override + public int hashCode() { + return Objects.hash(gav); + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/models/PingSpanData.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/models/PingSpanData.java new file mode 100644 index 000000000000..6530e8291b7e --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/models/PingSpanData.java @@ -0,0 +1,136 @@ +package com.azure.sdk.build.tool.models; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +/** + * Sends a ping to Application Insights when the build tool is run. + */ +public class PingSpanData implements SpanData { + + private final String traceId; + private final String spanId; + private final long startEpochNanos; + private final long endEpochNanos; + + /** + * Creates an instance of ping span to export to Application Insights. + */ + public PingSpanData() { + this.traceId = UUID.randomUUID().toString(); + this.spanId = UUID.randomUUID().toString(); + this.startEpochNanos = MILLISECONDS.toNanos(Instant.now().toEpochMilli()); + this.endEpochNanos = MILLISECONDS.toNanos(Instant.now().toEpochMilli()); + } + + @Override + public SpanContext getSpanContext() { + return SpanContext.create(traceId, spanId, TraceFlags.getDefault(), TraceState.getDefault()); + } + + @Override + public String getTraceId() { + return this.traceId; + } + + @Override + public String getSpanId() { + return this.spanId; + } + + @Override + public SpanContext getParentSpanContext() { + return SpanContext.create(traceId, spanId, TraceFlags.getDefault(), TraceState.getDefault()); + } + + @Override + public String getParentSpanId() { + return SpanData.super.getParentSpanId(); + } + + @Override + public Resource getResource() { + return null; + } + + @Override + public InstrumentationLibraryInfo getInstrumentationLibraryInfo() { + return InstrumentationLibraryInfo.create("AzureSDKMavenBuildTool", "1"); + } + + @Override + public String getName() { + return "azsdk-maven-build-tool"; + } + + @Override + public SpanKind getKind() { + return SpanKind.INTERNAL; + } + + @Override + public long getStartEpochNanos() { + return this.startEpochNanos; + } + + @Override + public Attributes getAttributes() { + return Attributes.builder().build(); + } + + @Override + public List getEvents() { + return Collections.emptyList(); + } + + @Override + public List getLinks() { + return Collections.emptyList(); + } + + @Override + public StatusData getStatus() { + return StatusData.ok(); + } + + @Override + public long getEndEpochNanos() { + return this.endEpochNanos; + } + + @Override + public boolean hasEnded() { + return false; + } + + @Override + public int getTotalRecordedEvents() { + return 0; + } + + @Override + public int getTotalRecordedLinks() { + return 0; + } + + @Override + public int getTotalAttributeCount() { + return 0; + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/mojo/AzureSdkMojo.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/mojo/AzureSdkMojo.java new file mode 100644 index 000000000000..bedd8ed2c05e --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/mojo/AzureSdkMojo.java @@ -0,0 +1,181 @@ +package com.azure.sdk.build.tool.mojo; + +import com.azure.monitor.opentelemetry.exporter.AzureMonitorExporterBuilder; +import com.azure.monitor.opentelemetry.exporter.AzureMonitorTraceExporter; +import com.azure.sdk.build.tool.ReportGenerator; +import com.azure.sdk.build.tool.Tools; +import com.azure.sdk.build.tool.models.BuildReport; +import com.azure.sdk.build.tool.models.PingSpanData; +import com.azure.sdk.build.tool.util.logging.Logger; +import edu.emory.mathcs.backport.java.util.Collections; +import io.opentelemetry.sdk.common.CompletableResultCode; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; + +import java.util.concurrent.TimeUnit; + +/** + * Azure SDK build tools Maven plugin Mojo for analyzing Maven configuration of an application to provide Azure + * SDK-specific recommendations. + */ +@Mojo(name = "run", + defaultPhase = LifecyclePhase.PREPARE_PACKAGE, + requiresDependencyCollection = ResolutionScope.RUNTIME, + requiresDependencyResolution = ResolutionScope.RUNTIME) +public class AzureSdkMojo extends AbstractMojo { + + public static AzureSdkMojo MOJO; + private static final Logger LOGGER = Logger.getInstance(); + private static final String APP_INSIGHTS_CONNECTION_STRING = ""; + + @Parameter(defaultValue = "${project}", readonly = true, required = true) + private MavenProject project; + + @Parameter(property = "validateAzureSdkBomUsed", defaultValue = "true") + private boolean validateAzureSdkBomUsed; + + @Parameter(property = "validateNoDeprecatedMicrosoftLibraryUsed", defaultValue = "true") + private boolean validateNoDeprecatedMicrosoftLibraryUsed; + + @Parameter(property = "validateBomVersionsAreUsed", defaultValue = "true") + private boolean validateBomVersionsAreUsed; + + @Parameter(property = "validateNoBetaLibraryUsed", defaultValue = "true") + private boolean validateNoBetaLibraryUsed; + + @Parameter(property = "validateNoBetaApiUsed", defaultValue = "true") + private boolean validateNoBetaApiUsed; + + @Parameter(property = "reportFile", defaultValue = "") + private String reportFile; + + @Parameter(property = "sendToMicrosoft", defaultValue = "true") + private boolean sendToMicrosoft; + + private final BuildReport buildReport; + + /** + * Creates an instance of Azure SDK build tool Mojo. + */ + public AzureSdkMojo() { + MOJO = this; + this.buildReport = new BuildReport(); + } + + /** + * Returns the build report. + * @return The build report. + */ + public BuildReport getReport() { + return buildReport; + } + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + getLog().info("========================================================================"); + getLog().info("= Running the Azure SDK Maven Build Tool ="); + getLog().info("========================================================================"); + + // Run all of the tools. They will collect their results in the report. + if (sendToMicrosoft) { + // front-load pinging App Insights asynchronously to avoid any blocking at the end of the plugin execution + pingAppInsights(); + } + Tools.getTools().forEach(Runnable::run); + ReportGenerator reportGenerator = new ReportGenerator(buildReport); + reportGenerator.generateReport(); + } + + private void pingAppInsights() { + try { + LOGGER.info("Sending ping message to Application Insights"); + AzureMonitorTraceExporter azureMonitorExporter = new AzureMonitorExporterBuilder() + .connectionString(APP_INSIGHTS_CONNECTION_STRING) + .buildTraceExporter(); + + PingSpanData pingSpanData = new PingSpanData(); + CompletableResultCode completionCode = + azureMonitorExporter.export(Collections.singletonList(pingSpanData)).join(30, TimeUnit.SECONDS); + if (completionCode.isSuccess()) { + LOGGER.info("Successfully sent ping message to Application Insights"); + } else { + if (LOGGER.isWarnEnabled()) { + LOGGER.warn("Failed to send ping message to Application Insights"); + } + } + } catch (Exception ex) { + if (LOGGER.isWarnEnabled()) { + LOGGER.warn("Unable to send ping message to Application Insights. " + ex.getMessage()); + } + } + } + + /** + * Returns the Maven project. + * @return The Maven project. + */ + public MavenProject getProject() { + return project; + } + + /** + * If this validation is enabled, build is configured to fail if Azure SDK BOM is not used. By default, this is + * set to {@code true}. + * + * @return {@code true} if this validation is enabled. + */ + public boolean isValidateAzureSdkBomUsed() { + return validateAzureSdkBomUsed; + } + + /** + * If this validation is enabled, build will fail if the application uses deprecated Microsoft libraries. By + * default, this is set to {@code true}. + * @return {@code true} if validation is enabled. + */ + public boolean isValidateNoDeprecatedMicrosoftLibraryUsed() { + return validateNoDeprecatedMicrosoftLibraryUsed; + } + + /** + * If this validation is enabled, build will fail if the any dependency overrides the version used in Azure SDK + * BOM. By default, this is set to {@code true}. + * + * @return {@code true} if this validation is enabled. + */ + public boolean isValidateBomVersionsAreUsed() { + return validateBomVersionsAreUsed; + } + + /** + * If this validation is enabled, build will fail if a beta (preview) version of Azure library is used. By + * default, this is set to {@code true}. + * @return {@code true} if this validation is enabled. + */ + public boolean isValidateNoBetaLibraryUsed() { + return validateNoBetaLibraryUsed; + } + + /** + * If this validation is enabled, build will fail if any method annotated with @Beta is called. By + * default, this is set to {@code true}. + * @return {@code true} if this validation is enabled. + */ + public boolean isValidateNoBetaApiUsed() { + return validateNoBetaApiUsed; + } + + /** + * The report file to which the build report is written to. + * @return The report file. + */ + public String getReportFile() { + return reportFile; + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/AnnotatedMethodCallerResult.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/AnnotatedMethodCallerResult.java new file mode 100644 index 000000000000..34426389984d --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/AnnotatedMethodCallerResult.java @@ -0,0 +1,44 @@ +package com.azure.sdk.build.tool.util; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.Objects; + +public class AnnotatedMethodCallerResult { + private final Class annotation; + private final Method annotatedMethod; + private final Member callingMember; + + public AnnotatedMethodCallerResult(final Class annotation, + final Method annotatedMethod, + final Member callingMember) { + this.annotation = Objects.requireNonNull(annotation); + this.annotatedMethod = Objects.requireNonNull(annotatedMethod); + this.callingMember = Objects.requireNonNull(callingMember); + } + + @Override + public String toString() { + return "Method " + annotatedMethod + " is annotated with @" + annotation.getSimpleName() + " and called by " + callingMember; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final AnnotatedMethodCallerResult that = (AnnotatedMethodCallerResult) o; + return annotation.getSimpleName().equals(that.annotation.getSimpleName()) + && annotatedMethod.getName().equals(that.annotatedMethod.getName()) + && callingMember.getName().equals(that.callingMember.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(annotation.getSimpleName(), annotatedMethod.getName(), callingMember.getName()); + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/AnnotationUtils.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/AnnotationUtils.java new file mode 100644 index 000000000000..c169ef2318ff --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/AnnotationUtils.java @@ -0,0 +1,144 @@ +package com.azure.sdk.build.tool.util; + +import com.azure.sdk.build.tool.util.logging.Logger; +import org.reflections8.Reflections; +import org.reflections8.ReflectionsException; +import org.reflections8.scanners.MemberUsageScanner; +import org.reflections8.scanners.MethodAnnotationsScanner; +import org.reflections8.util.ClasspathHelper; +import org.reflections8.util.ConfigurationBuilder; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Utility class to check for annotations. + */ +public final class AnnotationUtils { + private static Logger LOGGER = Logger.getInstance(); + + private AnnotationUtils() { + // no-op + } + + public static ClassLoader getCompleteClassLoader(final Stream paths) { + final List urls = paths.map(AnnotationUtils::pathToUrl).collect(Collectors.toList()); + return URLClassLoader.newInstance(urls.toArray(new URL[0])); + } + + public static Optional> getAnnotation(String name, ClassLoader classLoader) { + try { + return Optional.of(Class.forName(name, false, classLoader).asSubclass(Annotation.class)); + } catch (ClassNotFoundException e) { + LOGGER.info("Unable to find annotation " + name + " in classpath"); + } + return Optional.empty(); + } + + /** + * Returns a list of methods that call methods that are annotated with the given annotation. + * @param annotation The annotation on the method to look for. + * @param paths The paths to scan. + * @param interestedPackages The packages that this scan should be limited to. + * @param recursive If true, look for packages in the sub-directories of the given paths. + * @return A set of methods that call methods with the annotation. + */ + public static Set findCallsToAnnotatedMethod(final Class annotation, + final Stream paths, + final Set interestedPackages, + final boolean recursive) { + + final ConfigurationBuilder config = new ConfigurationBuilder() + .setScanners(new MethodAnnotationsScanner(), new MemberUsageScanner()); + + final List urls = paths.map(AnnotationUtils::pathToUrl).collect(Collectors.toList()); + config.addUrls(urls); + config.addClassLoader(URLClassLoader.newInstance(urls.toArray(new URL[0]))); + + // This is extremely ugly code, but it is necessary as the reflections library throws away the classloader + // I have built above, and so when it goes looking for classes it cannot always find them. What I am doing here + // is augmenting the actual context class loader with the additional urls, so that when the reflections library + // falls back to using the context class loader (which it does by default, because it throws away the proper + // class loader I built above), it can still find the classes I want it to find. + final URLClassLoader contextClassLoader = (URLClassLoader) ClasspathHelper.contextClassLoader(); + try { + final Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + method.setAccessible(true); + for (final URL url : urls) { + method.invoke(contextClassLoader, url); + } + } catch (Exception e) { + if (LOGGER.isErrorEnabled()) { + LOGGER.error("Unable to reflectively call addURL method on URL class. " + e.getMessage()); + } + } + + final Reflections reflections = new Reflections(config); + final Set annotatedMethods = reflections.getMethodsAnnotatedWith(annotation); + final Set results = new HashSet<>(); + + annotatedMethods.forEach(method -> { + checkMethod(reflections, annotation, method, interestedPackages, recursive, results); + }); + + return results; + } + + private static void checkMethod(final Reflections reflections, + final Class annotation, + final Method method, + final Set interestedPackages, + final boolean recursive, + final Set results) { + final Set callingMethods; + try { + callingMethods = reflections.getMethodUsage(method); + } catch (ReflectionsException e) { + LOGGER.info("Unable to get method usage for method " + method.getName() + ". " + e.getMessage()); + return; + } + + callingMethods.forEach(member -> { + // we only add a result if the calling method is in the list of packages we are interested in + if (member instanceof Method) { + final Method methodMember = (Method) member; + final String packageName = methodMember.getDeclaringClass().getPackage().getName(); + + if (interestedPackages.contains(packageName)) { + // we have reached a point where we have found a method call from code in a package + // we are interested in, so we will record it as a valid result. We do not recurse + // further from this method. + results.add(new AnnotatedMethodCallerResult(annotation, method, member)); + } else { + if (recursive && !methodMember.equals(method)) { + // we are looking at code that we know calls an annotated service method, but it is not + // within one of the packages we are interested in. We recurse here, finding all methods + // that call this method, until such time that we run out of methods to check. + checkMethod(reflections, annotation, methodMember, interestedPackages, recursive, results); + } + } + } + }); + } + + private static URL pathToUrl(Path path) { + try { + URL url = path.toUri().toURL(); + return url; + } catch (MalformedURLException e) { + LOGGER.info("Path " + path + " cannot be converted to URL. " + e.getMessage()); + return null; + } + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/MavenUtils.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/MavenUtils.java new file mode 100644 index 000000000000..92eeb5ff8e86 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/MavenUtils.java @@ -0,0 +1,89 @@ +package com.azure.sdk.build.tool.util; + +import com.azure.sdk.build.tool.util.logging.Logger; +import org.apache.maven.artifact.Artifact; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * Utility class to perform Maven related operations. + */ +public class MavenUtils { + private static final Logger LOGGER = Logger.getInstance(); + + /** + * Creates artifact string representation of an artifact. + * @param artifact The artifact. + * @return The string representation of the artifact. + */ + public static String toGAV(Artifact artifact) { + return artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getVersion(); + } + + /** + * Gets the latest released version of the given artifact from Maven repository. + * @return The latest version or {@code null} if an error occurred while retrieving the latest + * version. + */ + public static String getLatestArtifactVersion(String groupId, String artifactId) { + HttpURLConnection connection = null; + try { + groupId = groupId.replace(".", "/"); + URL url = new URL("https://repo1.maven.org/maven2/" + groupId + "/" + artifactId + "/maven-metadata.xml"); + if (LOGGER.isVerboseEnabled()) { + LOGGER.verbose(url.toString()); + } + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("accept", "application/xml"); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + int responseCode = connection.getResponseCode(); + if (HttpURLConnection.HTTP_OK == responseCode) { + InputStream responseStream = connection.getInputStream(); + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document doc = db.parse(responseStream); + + // Maven-metadata.xml lists versions from oldest to newest. Therefore, we want the bottom-most version + // that is not a beta, preview, etc release. + NodeList versionsList = doc.getElementsByTagName("version"); + String latestVersion = null; + for (int i = versionsList.getLength() - 1; i >=0; i--) { + Node versionNode = versionsList.item(i); + if (!versionNode.getTextContent().contains("beta")) { + latestVersion = versionNode.getTextContent(); + break; + } + } + + if (LOGGER.isVerboseEnabled()) { + LOGGER.verbose("The latest version of " + artifactId + " is " + latestVersion); + } + return latestVersion; + } else { + if (LOGGER.isWarnEnabled()) { + LOGGER.warn("Got a non-successful response for " + artifactId + ": " + responseCode); + } + } + } catch (Exception exception) { + if (LOGGER.isErrorEnabled()) { + LOGGER.error("Got error getting latest maven dependency version. " + exception.getMessage()); + } + } finally { + if (connection != null) { + // closes the input streams and discards the socket + connection.disconnect(); + } + } + return null; + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/MojoUtils.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/MojoUtils.java new file mode 100644 index 000000000000..bbd68261a6e5 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/MojoUtils.java @@ -0,0 +1,54 @@ +package com.azure.sdk.build.tool.util; + +import com.azure.sdk.build.tool.mojo.AzureSdkMojo; +import org.apache.maven.artifact.Artifact; + +import java.text.MessageFormat; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.function.Supplier; + +/** + * Utility class to perform an miscellaneous Mojo-related operations. + */ +public final class MojoUtils { + + private static final ResourceBundle strings = ResourceBundle.getBundle("strings"); + + private MojoUtils() { + // no-op + } + + @SuppressWarnings("unchecked") + public static Set getDirectDependencies() { + return AzureSdkMojo.MOJO.getProject().getDependencyArtifacts(); + } + + @SuppressWarnings("unchecked") + public static Set getAllDependencies() { + return AzureSdkMojo.MOJO.getProject().getArtifacts(); + } + + @SuppressWarnings("unchecked") + public static List getCompileSourceRoots() { + return ((List)AzureSdkMojo.MOJO.getProject() + .getCompileSourceRoots()); + } + + public static String getString(String key) { + return strings.getString(key); + } + + public static String getString(String key, String... parameters) { + return MessageFormat.format(getString(key), parameters); + } + + public static void failOrWarn(Supplier condition, String message) { + if (condition.get()) { + AzureSdkMojo.MOJO.getReport().addFailureMessage(message); + } else { + AzureSdkMojo.MOJO.getReport().addWarningMessage(message); + } + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/logging/ConsoleLogger.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/logging/ConsoleLogger.java new file mode 100644 index 000000000000..c62cf17a5dad --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/logging/ConsoleLogger.java @@ -0,0 +1,50 @@ +package com.azure.sdk.build.tool.util.logging; + +/** + * An implementation of {@link Logger} that logs messages to console. + */ +public class ConsoleLogger implements Logger { + private static ConsoleLogger INSTANCE; + + public static Logger getInstance() { + if (INSTANCE == null) { + INSTANCE = new ConsoleLogger(); + } + return INSTANCE; + } + + @Override + public void info(String msg) { + System.out.println(msg); + } + + @Override + public boolean isWarnEnabled() { + return true; + } + + @Override + public void warn(String msg) { + System.err.println(msg); + } + + @Override + public boolean isErrorEnabled() { + return true; + } + + @Override + public void error(String msg) { + System.err.println(msg); + } + + @Override + public boolean isVerboseEnabled() { + return false; + } + + @Override + public void verbose(String msg) { + System.out.println(msg); + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/logging/Logger.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/logging/Logger.java new file mode 100644 index 000000000000..90c7a1e6128b --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/logging/Logger.java @@ -0,0 +1,60 @@ +package com.azure.sdk.build.tool.util.logging; + +import com.azure.sdk.build.tool.mojo.AzureSdkMojo; + +/** + * A simple logger interface to support enable logging for the Azure SDK build tool. + */ +public interface Logger { + + static Logger getInstance() { + if (AzureSdkMojo.MOJO == null) { + return ConsoleLogger.getInstance(); + } else { + return MojoLogger.getInstance(); + } + } + + /** + * Logs message at info level. + * @param msg The message to log. + */ + void info(String msg); + + /** + * Returns true if logging at warning level is enabled. + * @return {@code true} if logging at warning level is enabled. + */ + boolean isWarnEnabled(); + + /** + * Logs the message at warning level. + * @param msg The message to log. + */ + void warn(String msg); + + /** + * Returns true if logging at error level is enabled. + * @return {@code true} if logging at error level is enabled. + */ + boolean isErrorEnabled(); + + /** + * Logs the message at error level. + * @param msg The message to log. + */ + void error(String msg); + + + /** + * Returns true if logging at verbose level is enabled. + * @return {@code true} if logging at verbose level is enabled. + */ + boolean isVerboseEnabled(); + + /** + * Logs the message at verbose level. + * @param msg The message to log. + */ + void verbose(String msg); +} diff --git a/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/logging/MojoLogger.java b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/logging/MojoLogger.java new file mode 100644 index 000000000000..06ab50e86763 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/java/com/azure/sdk/build/tool/util/logging/MojoLogger.java @@ -0,0 +1,58 @@ +package com.azure.sdk.build.tool.util.logging; + +import com.azure.sdk.build.tool.mojo.AzureSdkMojo; +import org.apache.maven.plugin.logging.Log; + +/** + * An implementation of {@link Logger} that uses the Maven plugin logger. + */ +public class MojoLogger implements Logger { + private static MojoLogger INSTANCE; + private Log mojoLog; + + public static Logger getInstance() { + if (INSTANCE == null) { + INSTANCE = new MojoLogger(AzureSdkMojo.MOJO.getLog()); + } + return INSTANCE; + } + + private MojoLogger(Log mojoLog) { + this.mojoLog = mojoLog; + } + + @Override + public void info(String msg) { + mojoLog.info(msg); + } + + @Override + public boolean isWarnEnabled() { + return mojoLog.isWarnEnabled(); + } + + @Override + public void warn(String msg) { + mojoLog.warn(msg); + } + + @Override + public boolean isErrorEnabled() { + return mojoLog.isErrorEnabled(); + } + + @Override + public void error(String msg) { + mojoLog.error(msg); + } + + @Override + public boolean isVerboseEnabled() { + return mojoLog.isDebugEnabled(); + } + + @Override + public void verbose(String msg) { + mojoLog.debug(msg); + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/main/resources/strings.properties b/sdk/tools/azure-sdk-build-tool/src/main/resources/strings.properties new file mode 100644 index 000000000000..f6a514055d91 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/main/resources/strings.properties @@ -0,0 +1,10 @@ +# Error messages +missingBomDependency = The azure-sdk-bom is not being used! +outdatedBomDependency = The azure-sdk-bom is outdated - consider updating to the latest release! +overrideBomVersion = The azure-sdk-bom version is ignored and a dependency version is explicitly specified. +betaDependencyUsed = A beta dependency is used. + +deprecatedDirectDependency = A direct dependency of this project relies on a `com.microsoft.*` library. Consider upgrading to the `com.azure.*` library listed below: +deprecatedIndirectDependency = A transitive dependency of this project relies on a `com.microsoft.*` library. Consider checking for a newer release of the following libraries: + +betaApiUsed = A method annotated with Beta is called! diff --git a/sdk/tools/azure-sdk-build-tool/src/test/azure-sdk-build-tool-test/pom.xml b/sdk/tools/azure-sdk-build-tool/src/test/azure-sdk-build-tool-test/pom.xml new file mode 100644 index 000000000000..1186015308fe --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/test/azure-sdk-build-tool-test/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + com.azure.tools + azure-maven-build-tool-test + 1.0.0-SNAPSHOT + + + 8 + 8 + + + + + + com.azure + azure-sdk-bom + 1.0.5 + pom + import + + + + + + + com.azure + azure-data-appconfiguration + + + + com.azure + azure-cosmos + + + + + com.azure + azure-security-keyvault-keys + 4.3.6 + + + + + com.microsoft.azure + azure-cosmosdb + 2.6.13 + + + + com.microsoft.azure + azure-keyvault + 1.2.6 + + + + + io.quarkus + quarkus-jdbc-mssql + 1.13.1.Final + + + + + com.azure + azure-monitor-opentelemetry-exporter + 1.0.0-beta.5 + + + + + + + com.azure.tools + azure-sdk-build-tool + 1.0.0-beta.1 + + true + true + true + true + true + ./azure-sdk-usage-report.txt + + + + + diff --git a/sdk/tools/azure-sdk-build-tool/src/test/azure-sdk-build-tool-test/src/main/java/com/test/annotation/AppConfigTestApp.java b/sdk/tools/azure-sdk-build-tool/src/test/azure-sdk-build-tool-test/src/main/java/com/test/annotation/AppConfigTestApp.java new file mode 100644 index 000000000000..9540a07f4a7a --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/test/azure-sdk-build-tool-test/src/main/java/com/test/annotation/AppConfigTestApp.java @@ -0,0 +1,23 @@ +package com.test.annotation; + +import com.azure.data.appconfiguration.ConfigurationClient; +import com.azure.data.appconfiguration.ConfigurationClientBuilder; +import com.azure.data.appconfiguration.models.ConfigurationSetting; + +/** + * Test @ServiceMethod annotation usage. + */ +public class AppConfigTestApp { + public static void main(String[] args) { + final ConfigurationClient configurationClient = new ConfigurationClientBuilder() + .connectionString("foo") + .buildClient(); + + System.out.println("Setting configuration"); + ConfigurationSetting setting = configurationClient.setConfigurationSetting("key", "label", "value"); + System.out.println("Done: " + setting.getLastModified()); + + setting = configurationClient.getConfigurationSetting("key", "label"); + System.out.println("Retrieved setting again, value is " + setting.getValue()); + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/test/azure-sdk-build-tool-test/src/main/java/com/test/annotation/BetaApiTestApp.java b/sdk/tools/azure-sdk-build-tool/src/test/azure-sdk-build-tool-test/src/main/java/com/test/annotation/BetaApiTestApp.java new file mode 100644 index 000000000000..b266a40f8917 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/test/azure-sdk-build-tool-test/src/main/java/com/test/annotation/BetaApiTestApp.java @@ -0,0 +1,23 @@ +package com.test.annotation; + +import com.azure.core.credential.AzureKeyCredential; +import com.azure.cosmos.CosmosClient; +import com.azure.cosmos.CosmosClientBuilder; + +import java.util.UUID; + +/** + * Test @Beta annotation usage. + */ +public class BetaApiTestApp { + public static void main(String[] args) { + + CosmosClient cosmosClient = new CosmosClientBuilder() + .credential(new AzureKeyCredential("key")) + .buildClient(); + + String uuid = UUID.randomUUID().toString(); + // this is a beta API + cosmosClient.createGlobalThroughputControlConfigBuilder("db" + uuid, "container" + uuid); + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/test/java/com/azure/sdk/build/tool/util/AnnotationUtilsTests.java b/sdk/tools/azure-sdk-build-tool/src/test/java/com/azure/sdk/build/tool/util/AnnotationUtilsTests.java new file mode 100644 index 000000000000..9ace7c25ff70 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/test/java/com/azure/sdk/build/tool/util/AnnotationUtilsTests.java @@ -0,0 +1,51 @@ +package com.azure.sdk.build.tool.util; + +import com.test.models.AnnotationA; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Stream; + +public class AnnotationUtilsTests { + + @Disabled("Due to classloader issue noted below") + @Test + public void findAnnotationTest() { + final Set interestedPackages = new TreeSet<>(Comparator.comparingInt(String::length)); + Path path = Paths.get("target", "test-classes"); + Stream pathStream = Stream.of(path); + + buildPackageList(path.toFile().getAbsolutePath(), path.toFile().getAbsolutePath(), interestedPackages); + + // This throws ClassCastException because AnnotationUtils has some custom logic to use the URLClassLoader + // java.lang.ClassCastException: class jdk.internal.loader.ClassLoaders$AppClassLoader + // cannot be cast to class java.net.URLClassLoader (jdk.internal.loader.ClassLoaders$AppClassLoader + // and java.net.URLClassLoader are in module java.base of loader 'bootstrap') + Set callsToAnnotatedMethod = AnnotationUtils.findCallsToAnnotatedMethod(AnnotationA.class, pathStream, interestedPackages, true); + } + + static void buildPackageList(String rootDir, String currentDir, Set packages) { + final File directory = new File(currentDir); + + final File[] files = directory.listFiles(); + if (files == null) { + return; + } + + for (final File file : files) { + if (file.isFile()) { + final String path = file.getPath(); + final String packageName = path.substring(rootDir.length() + 1, path.lastIndexOf(File.separator)); + packages.add(packageName.replace(File.separatorChar, '.')); + } else if (file.isDirectory()) { + buildPackageList(rootDir, file.getAbsolutePath(), packages); + } + } + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/test/java/com/test/models/AnnotationA.java b/sdk/tools/azure-sdk-build-tool/src/test/java/com/test/models/AnnotationA.java new file mode 100644 index 000000000000..3a7ce26170a0 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/test/java/com/test/models/AnnotationA.java @@ -0,0 +1,8 @@ +package com.test.models; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface AnnotationA { +} diff --git a/sdk/tools/azure-sdk-build-tool/src/test/java/com/test/models/ClassA.java b/sdk/tools/azure-sdk-build-tool/src/test/java/com/test/models/ClassA.java new file mode 100644 index 000000000000..83482381006a --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/test/java/com/test/models/ClassA.java @@ -0,0 +1,9 @@ +package com.test.models; + +public class ClassA { + + @AnnotationA + public void methodA() { + // no-op + } +} diff --git a/sdk/tools/azure-sdk-build-tool/src/test/java/com/test/models/ClassB.java b/sdk/tools/azure-sdk-build-tool/src/test/java/com/test/models/ClassB.java new file mode 100644 index 000000000000..f370fb8afd84 --- /dev/null +++ b/sdk/tools/azure-sdk-build-tool/src/test/java/com/test/models/ClassB.java @@ -0,0 +1,9 @@ +package com.test.models; + +public class ClassB { + + public void methodB() { + // calls through to ClassA.methodA(), which is annotated + new ClassA().methodA(); + } +} diff --git a/sdk/tools/ci.yml b/sdk/tools/ci.yml index c8e26fc06e80..6d65e147ea42 100644 --- a/sdk/tools/ci.yml +++ b/sdk/tools/ci.yml @@ -5,6 +5,7 @@ trigger: paths: include: - /sdk/tools/azure-sdk-archetype/ + - /sdk/tools/azure-sdk-build-tool/ pr: branches: @@ -16,6 +17,7 @@ pr: paths: include: - /sdk/tools/azure-sdk-archetype/ + - /sdk/tools/azure-sdk-build-tool/ extends: template: /eng/pipelines/templates/stages/archetype-sdk-client.yml @@ -26,3 +28,6 @@ extends: - name: azure-sdk-archetype groupId: com.azure.tools safeName: azuresdkarchetype + - name: azure-sdk-build-tool + groupId: com.azure.tools + safeName: azuresdkbuildtool diff --git a/sdk/tools/pom.xml b/sdk/tools/pom.xml index 4e99e5b20b1a..b253bde79589 100644 --- a/sdk/tools/pom.xml +++ b/sdk/tools/pom.xml @@ -10,5 +10,6 @@ 1.0.0 azure-sdk-archetype + azure-sdk-build-tool