From dc78405a68f52745bdc2ad0f98b97ee82bdf283c Mon Sep 17 00:00:00 2001 From: john lilley Date: Wed, 20 Mar 2019 10:17:10 -0600 Subject: [PATCH] 1391 jel minimal overwrite option (#2451) Option to overwrite only changed files --- .../openapitools/codegen/cmd/Generate.java | 8 ++ .../codegen/AbstractGenerator.java | 77 ++++++++++++++++--- .../openapitools/codegen/CodegenConfig.java | 4 + .../openapitools/codegen/DefaultCodegen.java | 20 +++++ .../codegen/DefaultGenerator.java | 14 ++-- .../codegen/config/CodegenConfigurator.java | 11 +++ .../codegen/DefaultGeneratorTest.java | 32 ++++++++ 7 files changed, 151 insertions(+), 15 deletions(-) diff --git a/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java b/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java index 1907231e9fc..5191bbbd1f9 100644 --- a/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java +++ b/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java @@ -217,6 +217,11 @@ public class Generate implements Runnable { @Option(name = {"--generate-alias-as-model"}, title = "generate alias (array, map) as model", description = CodegenConstants.GENERATE_ALIAS_AS_MODEL_DESC) private Boolean generateAliasAsModel; + @Option(name = {"--minimal-update"}, + title = "Minimal update", + description = "Only write output files that have changed.") + private Boolean minimalUpdate; + @Override public void run() { if (logToStderr != null) { @@ -346,6 +351,9 @@ public class Generate implements Runnable { if (generateAliasAsModel != null) { configurator.setGenerateAliasAsModel(generateAliasAsModel); } + if (minimalUpdate != null) { + configurator.setEnableMinimalUpdate(minimalUpdate); + } applySystemPropertiesKvpList(systemProperties, configurator); applyInstantiationTypesKvpList(instantiationTypes, configurator); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/AbstractGenerator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/AbstractGenerator.java index eccbcc827a6..65bd31ceb55 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/AbstractGenerator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/AbstractGenerator.java @@ -17,6 +17,10 @@ package org.openapitools.codegen; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,23 +32,78 @@ import java.util.regex.Pattern; public abstract class AbstractGenerator { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractGenerator.class); + + /** + * Is the minimal-file-update option enabled? + * + * @return Option value + */ + public abstract boolean getEnableMinimalUpdate(); - @SuppressWarnings("static-method") + /** + * Write String to a file, formatting as UTF-8 + * + * @param filename The name of file to write + * @param contents The contents string. + * @return File representing the written file. + * @throws IOException If file cannot be written. + */ public File writeToFile(String filename, String contents) throws IOException { - LOGGER.info("writing file " + filename); + return writeToFile(filename, contents.getBytes(Charset.forName("UTF-8"))); + } + /** + * Write bytes to a file + * + * @param filename The name of file to write + * @param contents The contents bytes. Typically this is a UTF-8 formatted string. + * @return File representing the written file. + * @throws IOException If file cannot be written. + */ + @SuppressWarnings("static-method") + public File writeToFile(String filename, byte contents[]) throws IOException { + if (getEnableMinimalUpdate()) { + String tempFilename = filename + ".tmp"; + // Use Paths.get here to normalize path (for Windows file separator, space escaping on Linux/Mac, etc) + File outputFile = Paths.get(filename).toFile(); + File tempFile = null; + try { + tempFile = writeToFileRaw(tempFilename, contents); + if (!filesEqual(tempFile, outputFile)) { + LOGGER.info("writing file " + filename); + Files.move(tempFile.toPath(), outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + tempFile = null; + } else { + LOGGER.info("skipping unchanged file " + filename); + } + } finally { + if (tempFile != null && tempFile.exists()) { + try { + tempFile.delete(); + } catch (Exception ex) { + LOGGER.error("Error removing temporary file " + tempFile, ex); + } + } + } + return outputFile; + } else { + LOGGER.info("writing file " + filename); + return writeToFileRaw(filename, contents); + } + } + + private boolean filesEqual(File file1, File file2) throws IOException { + return file1.exists() && file2.exists() && Arrays.equals(Files.readAllBytes(file1.toPath()), Files.readAllBytes(file2.toPath())); + } + + private File writeToFileRaw(String filename, byte[] contents) throws IOException { // Use Paths.get here to normalize path (for Windows file separator, space escaping on Linux/Mac, etc) File output = Paths.get(filename).toFile(); - if (output.getParent() != null && !new File(output.getParent()).exists()) { - File parent = new File(output.getParent()); + File parent = Paths.get(output.getParent()).toFile(); parent.mkdirs(); } - - try (Writer out = new BufferedWriter(new OutputStreamWriter( - new FileOutputStream(output), "UTF-8"))) { - out.write(contents); - } + Files.write(output.toPath(), contents); return output; } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java index 5d554cf9713..2e788e48cf7 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java @@ -260,4 +260,8 @@ public interface CodegenConfig { */ void setOpenAPI(OpenAPI openAPI); + public boolean isEnableMinimalUpdate(); + + public void setEnableMinimalUpdate(boolean isEnableMinimalUpdate); + } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index d17598228ef..3a90ef15460 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -111,6 +111,8 @@ public class DefaultCodegen implements CodegenConfig { protected String ignoreFilePathOverride; // flag to indicate whether to use environment variable to post process file protected boolean enablePostProcessFile = false; + // flag to indicate whether to only update files whose contents have changed + protected boolean enableMinimalUpdate = false; // make openapi available to all methods protected OpenAPI openAPI; @@ -4834,4 +4836,22 @@ public class DefaultCodegen implements CodegenConfig { this.enablePostProcessFile = enablePostProcessFile; } + /** + * Get the boolean value indicating the state of the option for updating only changed files + */ + @Override + public boolean isEnableMinimalUpdate() { + return enableMinimalUpdate; + } + + /** + * Set the boolean value indicating the state of the option for updating only changed files + * + * @param enableMinimalUpdate true to enable minimal update + */ + @Override + public void setEnableMinimalUpdate(boolean enableMinimalUpdate) { + this.enableMinimalUpdate = enableMinimalUpdate; + } + } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java index 41d8c6daa5b..8a566e21990 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java @@ -67,6 +67,11 @@ public class DefaultGenerator extends AbstractGenerator implements Generator { private String contextPath; private Map generatorPropertyDefaults = new HashMap<>(); + @Override + public boolean getEnableMinimalUpdate() { + return config.isEnableMinimalUpdate(); + } + @Override public Generator opts(ClientOptInput opts) { this.opts = opts; @@ -797,16 +802,13 @@ public class DefaultGenerator extends AbstractGenerator implements Generator { } protected File writeInputStreamToFile(String filename, InputStream in, String templateFile) throws FileNotFoundException, IOException { - File outputFile = java.nio.file.Paths.get(filename).toFile(); if (in != null) { - OutputStream out = new FileOutputStream(outputFile, false); - LOGGER.info("writing file " + outputFile); - IOUtils.copy(in, out); - out.close(); + byte bytes[] = IOUtils.toByteArray(in); + return writeToFile(filename, bytes); } else { LOGGER.error("can't open '" + templateFile + "' for input; cannot write '" + filename + "'"); + return null; } - return outputFile; } private Map buildSupportFileBundle(List allOperations, List allModels) { diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java index 64aa97b280f..6f6b748c315 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java @@ -101,6 +101,7 @@ public class CodegenConfigurator implements Serializable { private boolean logToStderr; private boolean validateSpec; private boolean enablePostProcessFile; + private boolean enableMinimalUpdate; private String templateDir; private String auth; private String apiPackage; @@ -239,6 +240,15 @@ public class CodegenConfigurator implements Serializable { return this; } + public boolean getEnableMinimalUpdate() { + return enableMinimalUpdate; + } + + public CodegenConfigurator setEnableMinimalUpdate(boolean enableMinimalUpdate) { + this.enableMinimalUpdate = enableMinimalUpdate; + return this; + } + public boolean isGenerateAliasAsModel() { return ModelUtils.isGenerateAliasAsModel(); } @@ -545,6 +555,7 @@ public class CodegenConfigurator implements Serializable { config.setIgnoreFilePathOverride(ignoreFileOverride); config.setRemoveOperationIdPrefix(removeOperationIdPrefix); config.setEnablePostProcessFile(enablePostProcessFile); + config.setEnableMinimalUpdate(enableMinimalUpdate); config.instantiationTypes().putAll(instantiationTypes); config.typeMapping().putAll(typeMappings); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java index 315a33bc120..fc8a3a17d49 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java @@ -9,11 +9,15 @@ import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.parameters.QueryParameter; import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.responses.ApiResponses; +import java.io.File; +import java.io.IOException; import org.testng.Assert; import org.testng.annotations.Test; import java.util.List; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; public class DefaultGeneratorTest { @@ -46,4 +50,32 @@ public class DefaultGeneratorTest { Assert.assertEquals(defaultList.get(3).path, "/path4"); Assert.assertEquals(defaultList.get(3).allParams.size(), 1); } + + @Test + public void minimalUpdateTest() throws IOException { + OpenAPI openAPI = TestUtils.createOpenAPI(); + ClientOptInput opts = new ClientOptInput(); + opts.setOpenAPI(openAPI); + DefaultCodegen codegen = new DefaultCodegen(); + codegen.setEnableMinimalUpdate(true); + opts.setConfig(codegen); + opts.setOpts(new ClientOpts()); + DefaultGenerator generator = new DefaultGenerator(); + generator.opts(opts); + File testPath = new File("temp/overwrite.test"); + if (testPath.exists()) { + testPath.delete(); + } + generator.writeToFile(testPath.toString(), "some file contents"); + long createTime = testPath.lastModified(); + try { + Thread.sleep(100); + } catch (InterruptedException ex) { + } + generator.writeToFile(testPath.toString(), "some file contents"); + Assert.assertEquals(createTime, testPath.lastModified()); + File testPathTmp = new File("temp/overwrite.test.tmp"); + Assert.assertFalse(testPathTmp.exists()); + testPath.delete(); + } }