Skip to content

Commit

Permalink
[maven-plugin] Generate hash from actual resolved spec rather than in…
Browse files Browse the repository at this point in the history
…putSpec file (OpenAPITools#18849)

* Ensure temp directories are deleted after test execution

* Implement test that external $ref changes are reflected in checksum

* Generate hash checksum from actual resolved spec instead of inputSpec file

Otherwise regeneration will not happen when skipIfSpecIsUnchanged is enabled,
although formally the spec content has changed.

Fixes OpenAPITools#4512 and OpenAPITools#16489

* Use try-with-resources to ensure stream is closed properly on exit

* Fix deprecation warning on SimpleLocalRepositoryManagerFactory no-arg constructor

* Apply minor code cleanup

- use fluent setters where possible
- remove undocumented @throws from JavaDoc
- use List.of() instead of Arrays.asList() for single-element-collection
  (more memory efficient)
- fix some grammar issues in comments and JavaDoc

* Use non-blocking java.nio API for file existence checks
  • Loading branch information
Philzen authored Jun 5, 2024
1 parent 446e168 commit 880df7a
Show file tree
Hide file tree
Showing 5 changed files with 337 additions and 176 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,40 +17,14 @@

package org.openapitools.codegen.plugin;

import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import static org.openapitools.codegen.config.CodegenConfiguratorUtils.*;

import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.core.util.Yaml;
import io.swagger.v3.parser.OpenAPIResolver;
import io.swagger.v3.parser.OpenAPIV3Parser;
import io.swagger.v3.parser.core.models.AuthorizationValue;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import com.google.common.io.ByteSource;
import com.google.common.io.CharSource;
import io.swagger.v3.parser.core.models.ParseOptions;
import io.swagger.v3.parser.util.ClasspathHelper;
import lombok.Setter;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
Expand All @@ -59,30 +33,28 @@
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
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.plugins.annotations.*;
import org.apache.maven.project.MavenProject;

import org.apache.maven.project.MavenProjectHelper;
import org.openapitools.codegen.CliOption;
import org.openapitools.codegen.ClientOptInput;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.DefaultGenerator;
import org.openapitools.codegen.auth.AuthParser;
import org.openapitools.codegen.*;
import org.openapitools.codegen.config.CodegenConfigurator;
import org.openapitools.codegen.config.GlobalSettings;
import org.openapitools.codegen.config.MergedSpecBuilder;
import org.sonatype.plexus.build.incremental.BuildContext;
import org.sonatype.plexus.build.incremental.DefaultBuildContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonatype.plexus.build.incremental.BuildContext;
import org.sonatype.plexus.build.incremental.DefaultBuildContext;

import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.*;

import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import static org.openapitools.codegen.config.CodegenConfiguratorUtils.*;

/**
* Goal which generates client/server code from a OpenAPI json/yaml definition.
Expand Down Expand Up @@ -609,19 +581,11 @@ public void execute() throws MojoExecutionException {
}

if (Boolean.TRUE.equals(skipIfSpecIsUnchanged)) {
File storedInputSpecHashFile = getHashFile(inputSpecFile);
final File storedInputSpecHashFile = getHashFile(inputSpecFile);
if (storedInputSpecHashFile.exists()) {
String inputSpecHash = null;
try {
inputSpecHash = calculateInputSpecHash(inputSpecFile);
} catch (IOException ex) {
ex.printStackTrace();
}
@SuppressWarnings("UnstableApiUsage")
String storedInputSpecHash = Files.asCharSource(storedInputSpecHashFile, StandardCharsets.UTF_8).read();
if (storedInputSpecHash.equals(inputSpecHash)) {
getLog().info(
"Code generation is skipped because input was unchanged");
if (storedInputSpecHash.equals(calculateInputSpecHash(inputSpec))) {
getLog().info("Code generation is skipped because input was unchanged");
return;
}
}
Expand Down Expand Up @@ -1010,17 +974,15 @@ public void execute() throws MojoExecutionException {

// Store a checksum of the input spec
File storedInputSpecHashFile = getHashFile(inputSpecFile);
String inputSpecHash = calculateInputSpecHash(inputSpecFile);

if (storedInputSpecHashFile.getParent() != null && !new File(storedInputSpecHashFile.getParent()).exists()) {
File parent = new File(storedInputSpecHashFile.getParent());
if (!parent.mkdirs()) {
throw new RuntimeException("Failed to create the folder " + parent.getAbsolutePath() +
" to store the checksum of the input spec.");
}
}
Files.asCharSink(storedInputSpecHashFile, StandardCharsets.UTF_8).write(inputSpecHash);

Files.asCharSink(storedInputSpecHashFile, StandardCharsets.UTF_8).write(calculateInputSpecHash(inputSpec));
} catch (Exception e) {
// Maven logs exceptions thrown by plugins only if invoked with -e
// I find it annoying to jump through hoops to get basic diagnostic information,
Expand All @@ -1035,45 +997,21 @@ public void execute() throws MojoExecutionException {
}

/**
* Calculate openapi specification file hash. If specification is hosted on remote resource it is downloaded first
* Calculate an SHA256 hash for the openapi specification.
* If the specification is hosted on a remote resource it is downloaded first.
*
* @param inputSpecFile - Openapi specification input file to calculate its hash.
* Does not take into account if input spec is hosted on remote resource
* @return openapi specification file hash
* @throws IOException
* @param inputSpec - Openapi specification input file. Can denote a URL or file path.
* @return openapi specification hash
*/
private String calculateInputSpecHash(File inputSpecFile) throws IOException {

URL inputSpecRemoteUrl = inputSpecRemoteUrl();

File inputSpecTempFile = inputSpecFile;

if (inputSpecRemoteUrl != null) {
inputSpecTempFile = java.nio.file.Files.createTempFile("openapi-spec", ".tmp").toFile();

URLConnection conn = inputSpecRemoteUrl.openConnection();
if (isNotEmpty(auth)) {
List<AuthorizationValue> authList = AuthParser.parse(auth);
for (AuthorizationValue a : authList) {
conn.setRequestProperty(a.getKeyName(), a.getValue());
}
}
try (ReadableByteChannel readableByteChannel = Channels.newChannel(conn.getInputStream())) {
FileChannel fileChannel;
try (FileOutputStream fileOutputStream = new FileOutputStream(inputSpecTempFile)) {
fileChannel = fileOutputStream.getChannel();
fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE);
}
}
}

ByteSource inputSpecByteSource =
inputSpecTempFile.exists()
? Files.asByteSource(inputSpecTempFile)
: CharSource.wrap(ClasspathHelper.loadFileFromClasspath(inputSpecTempFile.toString().replaceAll("\\\\","/")))
.asByteSource(StandardCharsets.UTF_8);

return inputSpecByteSource.hash(Hashing.sha256()).toString();
private String calculateInputSpecHash(String inputSpec) {
final ParseOptions parseOptions = new ParseOptions();
parseOptions.setResolve(true);

final URL remoteUrl = inputSpecRemoteUrl();
return Hashing.sha256().hashBytes(
new OpenAPIParser().readLocation(remoteUrl == null ? inputSpec : remoteUrl.toString(), null, parseOptions)
.getOpenAPI().toString().getBytes(StandardCharsets.UTF_8)
).toString();
}

/**
Expand Down
Loading

0 comments on commit 880df7a

Please sign in to comment.