All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openapitools.codegen.cmd.GenerateBatch Maven / Gradle / Ivy

There is a newer version: 7.10.0
Show newest version
/*
 * Copyright 2019 OpenAPI-Generator Contributors (https://openapi-generator.tech)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.openapitools.codegen.cmd;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.util.TokenBuffer;

import io.airlift.airline.Arguments;
import io.airlift.airline.Command;
import io.airlift.airline.Option;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.ClientOptInput;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.DefaultGenerator;
import org.openapitools.codegen.config.CodegenConfigurator;
import org.openapitools.codegen.config.DynamicSettings;
import org.openapitools.codegen.config.GlobalSettings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
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.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@SuppressWarnings({"unused", "MismatchedQueryAndUpdateOfCollection", "java:S106"})
@Command(name = "batch", description = "Generate code in batch via external configs.")
public class GenerateBatch extends OpenApiGeneratorCommand {
    private static AtomicInteger failures = new AtomicInteger(0);
    private static AtomicInteger successes = new AtomicInteger(0);
    private final Logger LOGGER = LoggerFactory.getLogger(GenerateBatch.class);

    @Option(name = {"-v", "--verbose"}, description = "verbose mode")
    private Boolean verbose;

    @Option(name = {"-r", "--threads"}, description = "thread count")
    private Integer threads;

    @Arguments(description = "Generator configuration files.", required = true)
    private List configs;

    @Option(name = {"--fail-fast"}, description = "fail fast on any errors")
    private Boolean failFast;

    @Option(name = {"--clean"}, description = "clean output of previously written files before generation")
    private Boolean clean;

    @Option(name = {"--timeout"}, description = "execution timeout (minutes)")
    private Integer timeout;

    @Option(name = {"--includes-base-dir"}, description = "base directory used for includes")
    private String includes;

    @Option(name = {"--root-dir"}, description = "root directory used output/includes (includes can be overridden)")
    private String root;

    /**
     * When an object implementing interface Runnable is used
     * to create a thread, starting the thread causes the object's
     * run method to be called in that separately executing
     * thread.
     * 

* The general contract of the method run is that it may * take any action whatsoever. * * @see Thread#run() */ @Override public void execute() { if (configs.size() < 1) { LOGGER.error("No configuration file inputs specified"); System.exit(1); } int cores = Runtime.getRuntime().availableProcessors(); int numThreads = 2 * cores; if (null != threads && (threads > 0 && threads < Thread.activeCount())) { numThreads = threads; } Path rootDir; if (root != null) { rootDir = Paths.get(root); } else { rootDir = Paths.get(System.getProperty("user.dir")); } // This allows us to put meta-configs in a different file from referenced configs. // If not specified, we'll assume it's the parent directory of the first file. File includesDir; if (includes != null) { includesDir = new File(includes); } else { Path first = Paths.get(configs.get(0)); if (Files.isRegularFile(first) && !Files.isSymbolicLink(first)) { includesDir = first.toAbsolutePath().getParent().toFile(); } else { // Not traversing symbolic links for includes. Falling back to rooted working directory. includesDir = rootDir.toFile(); } } LOGGER.info(String.format(Locale.ROOT, "Batch generation using up to %d threads.\nIncludes: %s\nRoot: %s", numThreads, includesDir.getAbsolutePath(), rootDir.toAbsolutePath().toString())); // Create a module which loads our config files, but supports a special "!include" key which can point to an existing config file. // This allows us to create a sort of meta-config which holds configs which are otherwise required at CLI time (via generate task). // That is, this allows us to create a wrapper config for generatorName, inputSpec, outputDir, etc. SimpleModule module = getCustomDeserializationModel(includesDir); List configurators = configs.stream().map(config -> CodegenConfigurator.fromFile(config, module)).collect(Collectors.toList()); // it doesn't make sense to interleave INFO level logs, so limit these to only ERROR. LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); Stream.of(Logger.ROOT_LOGGER_NAME, "io.swagger", "org.openapitools") .map(lc::getLogger) .forEach(logger -> logger.setLevel(Level.ERROR)); ExecutorService executor = Executors.newFixedThreadPool(numThreads); // Execute each configurator on a separate pooled thread. configurators.forEach(configurator -> { GenerationRunner runner = new GenerationRunner(configurator, rootDir, Boolean.TRUE.equals(failFast), Boolean.TRUE.equals(clean)); executor.execute(runner); }); executor.shutdown(); try { // Allow the batch job to terminate, never running for more than 30 minutes (defaulted to max 10 minutes) if (timeout == null) timeout = 10; int awaitFor = Math.min(Math.max(timeout, 1), 30); executor.awaitTermination(awaitFor, TimeUnit.MINUTES); int failCount = failures.intValue(); if (failCount > 0) { System.err.println(String.format(Locale.ROOT, "[FAIL] Completed with %d failures, %d successes", failCount, successes.intValue())); System.exit(1); } else { System.out.println(String.format(Locale.ROOT, "[SUCCESS] Batch generation finished %d generators successfully.", successes.intValue())); } } catch (InterruptedException e) { e.printStackTrace(); // re-interrupt Thread.currentThread().interrupt(); } } private static class GenerationRunner implements Runnable { private final CodegenConfigurator configurator; private final Path rootDir; private final boolean exitOnError; private final boolean clean; private GenerationRunner(CodegenConfigurator configurator, Path rootDir, boolean failFast, boolean clean) { this.configurator = configurator; this.rootDir = rootDir; this.exitOnError = failFast; this.clean = clean; } /** * When an object implementing interface Runnable is used * to create a thread, starting the thread causes the object's * run method to be called in that separately executing * thread. *

* The general contract of the method run is that it may * take any action whatsoever. * * @see Thread#run() */ @Override public void run() { String name = null; try { GlobalSettings.reset(); ClientOptInput opts = configurator.toClientOptInput(); CodegenConfig config = opts.getConfig(); name = config.getName(); Path target = Paths.get(config.getOutputDir()); Path updated = rootDir.resolve(target); config.setOutputDir(updated.toString()); if (this.clean) { cleanPreviousFiles(name, updated); } System.out.printf(Locale.ROOT, "[%s] Generating %s (outputs to %s)…%n", Thread.currentThread().getName(), name, updated.toString()); DefaultGenerator defaultGenerator = new DefaultGenerator(); defaultGenerator.opts(opts); defaultGenerator.generate(); System.out.printf(Locale.ROOT, "[%s] Finished generating %s…%n", Thread.currentThread().getName(), name); successes.incrementAndGet(); } catch (Throwable e) { failures.incrementAndGet(); String failedOn = name; if (StringUtils.isEmpty(failedOn)) { failedOn = "unspecified"; } System.err.printf(Locale.ROOT, "[%s] Generation failed for %s: (%s) %s%n", Thread.currentThread().getName(), failedOn, e.getClass().getSimpleName(), e.getMessage()); e.printStackTrace(System.err); if (exitOnError) { System.exit(1); } } finally { GlobalSettings.reset(); } } private void cleanPreviousFiles(final String name, Path outDir) throws IOException { System.out.printf(Locale.ROOT, "[%s] Cleaning previous contents for %s in %s…%n", Thread.currentThread().getName(), name, outDir.toString()); Path filesMeta = Paths.get(outDir.toAbsolutePath().toString(), ".openapi-generator", "FILES"); if (filesMeta.toFile().exists()) { FileUtils.readLines(filesMeta.toFile(), StandardCharsets.UTF_8).forEach(relativePath -> { if (!StringUtils.startsWith(relativePath, ".")) { Path file = outDir.resolve(relativePath).toAbsolutePath(); // hack: disallow directory traversal outside of output directory. we don't want to delete wrong files. if (file.toString().startsWith(outDir.toAbsolutePath().toString())) { try { Files.delete(file); } catch (Throwable e) { System.out.printf(Locale.ROOT, "[%s] Generator %s failed to clean file %s…%n", Thread.currentThread().getName(), name, file); } } } else { System.out.printf(Locale.ROOT, "[%s] Generator %s skip cleaning special filename %s…%n", Thread.currentThread().getName(), name, relativePath); } }); } } } static SimpleModule getCustomDeserializationModel(final File includesDir) { // Create a module which loads our config files, but supports a special "!include" key which can point to an existing config file. // This allows us to create a sort of meta-config which holds configs which are otherwise required at CLI time (via generate task). // That is, this allows us to create a wrapper config for generatorName, inputSpec, outputDir, etc. SimpleModule module = new SimpleModule("GenerateBatch"); module.setDeserializerModifier(new BeanDeserializerModifier() { @Override public JsonDeserializer modifyDeserializer(DeserializationConfig config, BeanDescription bd, JsonDeserializer original) { JsonDeserializer result; if (bd.getBeanClass() == DynamicSettings.class) { result = new DynamicSettingsRefSupport(original, includesDir); } else { result = original; } return result; } }); return module; } static class DynamicSettingsRefSupport extends DelegatingDeserializer { private static final String INCLUDE = "!include"; private File scanDir; DynamicSettingsRefSupport(JsonDeserializer delegate, File scanDir) { super(delegate); this.scanDir = scanDir; } @Override protected JsonDeserializer newDelegatingInstance(JsonDeserializer newDelegatee) { return new DynamicSettingsRefSupport(newDelegatee, null); } @Override public Object deserialize(JsonParser p, DeserializationContext ctx) throws IOException { ObjectMapper codec = (ObjectMapper) ctx.getParser().getCodec(); TokenBuffer buffer = new TokenBuffer(p); recurse(buffer, p, codec, false); JsonParser newParser = buffer.asParser(codec); newParser.nextToken(); return super.deserialize(newParser, ctx); } private void recurse(TokenBuffer buffer, JsonParser p, ObjectMapper codec, boolean skipOuterbraces) throws IOException { boolean firstToken = true; JsonToken token; while ((token = p.nextToken()) != null) { String name = p.currentName(); if (skipOuterbraces && firstToken && JsonToken.START_OBJECT.equals(token)) { continue; } if (skipOuterbraces && p.getParsingContext().inRoot() && JsonToken.END_OBJECT.equals(token)) { continue; } if (JsonToken.VALUE_NULL.equals(token)) { continue; } if (name != null && JsonToken.FIELD_NAME.equals(token) && name.startsWith(INCLUDE)) { p.nextToken(); String fileName = p.getText(); if (fileName != null) { File includeFile = scanDir != null ? new File(scanDir, fileName) : new File(fileName); if (includeFile.exists()) { recurse(buffer, codec.getFactory().createParser(includeFile), codec, true); } } } else { buffer.copyCurrentEvent(p); } firstToken = false; } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy