Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,17 @@ public class DefaultGenerator implements Generator {
private String basePath;
private String basePathWithoutHost;
private String contextPath;
private Map<String, String> generatorPropertyDefaults = new HashMap<>();
private final Map<String, String> generatorPropertyDefaults = new HashMap<>();
/**
* Retrieves an instance to the configured template processor, available after user-defined options are
* applied via
*/
@Getter protected TemplateProcessor templateProcessor = null;
@Getter
protected TemplateProcessor templateProcessor = null;

private List<TemplateDefinition> userDefinedTemplates = new ArrayList<>();
private String generatorCheck = "spring";
private String templateCheck = "apiController.mustache";
private final String generatorCheck = "spring";
private final String templateCheck = "apiController.mustache";


public DefaultGenerator() {
Expand Down Expand Up @@ -266,8 +267,7 @@ void configureGeneratorProperties() {
openapiNormalizer.normalize();
}
} catch (Exception e) {
LOGGER.error("An exception occurred in OpenAPI Normalizer. Please report the issue via https://github.com/openapitools/openapi-generator/issues/new/: ");
e.printStackTrace();
LOGGER.error("An exception occurred in OpenAPI Normalizer. Please report the issue via https://github.com/openapitools/openapi-generator/issues/new/: ", e);
}

// resolve inline models
Expand Down Expand Up @@ -607,10 +607,10 @@ private void generateModelsForVariable(List<File> files, List<ModelMap> allModel
if (!processedModels.contains(key) && allSchemas.containsKey(key)) {
generateModels(files, allModels, unusedModels, aliasModels, processedModels, () -> Set.of(key));
} else {
LOGGER.info("Type " + variable.getComplexType() + " of variable " + variable.getName() + " could not be resolve because it is not declared as a model.");
LOGGER.info("Type {} of variable {} could not be resolve because it is not declared as a model.", variable.getComplexType(), variable.getName());
}
} else {
LOGGER.info("Type " + variable.getOpenApiType() + " of variable " + variable.getName() + " could not be resolve because it is not declared as a model.");
LOGGER.info("Type {} of variable {} could not be resolve because it is not declared as a model.", variable.getOpenApiType(), variable.getName());
}
}

Expand Down Expand Up @@ -639,8 +639,8 @@ private Set<String> getPropertyAsSet(String propertyName) {
}

return Arrays.stream(propertyRaw.split(","))
.map(String::trim)
.collect(Collectors.toSet());
.map(String::trim)
.collect(Collectors.toSet());
}

private Set<String> modelKeys() {
Expand All @@ -665,7 +665,6 @@ private Set<String> modelKeys() {
return modelKeys;
}

@SuppressWarnings("unchecked")
void generateApis(List<File> files, List<OperationsMap> allOperations, List<ModelMap> allModels) {
if (!generateApis) {
// TODO: Process these anyway and present info via dryRun?
Expand Down Expand Up @@ -1006,7 +1005,7 @@ private void generateOpenapiGeneratorIgnoreFile() {
File ignoreFile = new File(ignoreFileNameTarget);
// use the entries provided by the users to pre-populate .openapi-generator-ignore
try {
LOGGER.info("Writing file " + ignoreFileNameTarget + " (which is always overwritten when the option `openapiGeneratorIgnoreFile` is enabled.)");
LOGGER.info("Writing file {} (which is always overwritten when the option `openapiGeneratorIgnoreFile` is enabled.)", ignoreFileNameTarget);
new File(config.outputFolder()).mkdirs();
if (!ignoreFile.createNewFile()) {
// file may already exist, do nothing
Expand Down Expand Up @@ -1430,7 +1429,10 @@ protected File processTemplateToFile(Map<String, Object> templateData, String te
return processTemplateToFile(templateData, templateName, outputFilename, shouldGenerate, skippedByOption, this.config.getOutputDir());
}

private final Set<String> seenFiles = new HashSet<>();
/**
* Stores lowercased absolute paths for O(1) case-insensitive duplicate detection.
*/
private final Set<String> seenFilesLower = new HashSet<>();

private File processTemplateToFile(Map<String, Object> templateData, String templateName, String outputFilename, boolean shouldGenerate, String skippedByOption, String intendedOutputDir) throws IOException {
String adjustedOutputFilename = outputFilename.replaceAll("//", "/").replace('/', File.separatorChar);
Expand All @@ -1443,10 +1445,10 @@ private File processTemplateToFile(Map<String, Object> templateData, String temp
throw new RuntimeException(String.format(Locale.ROOT, "Target files must be generated within the output directory; absoluteTarget=%s outDir=%s", absoluteTarget, outDir));
}

if (seenFiles.stream().filter(f -> f.toLowerCase(Locale.ROOT).equals(absoluteTarget.toString().toLowerCase(Locale.ROOT))).findAny().isPresent()) {
LOGGER.warn("Duplicate file path detected. Not all operating systems can handle case sensitive file paths. path={}", absoluteTarget.toString());
// O(1) case-insensitive duplicate check via a pre-lowercased shadow set
if (!seenFilesLower.add(absoluteTarget.toString().toLowerCase(Locale.ROOT))) {
LOGGER.warn("Duplicate file path detected. Not all operating systems can handle case sensitive file paths. path={}", absoluteTarget);
}
seenFiles.add(absoluteTarget.toString());
return this.templateProcessor.write(templateData, templateName, target);
} else {
this.templateProcessor.skip(target.toPath(), String.format(Locale.ROOT, "Skipped by %s options supplied by user.", skippedByOption));
Expand Down Expand Up @@ -2002,10 +2004,8 @@ private void generateFilesMetadata(List<File> files) {
}
});

Collections.sort(relativePaths, (a, b) -> IOCase.SENSITIVE.checkCompareTo(a, b));
relativePaths.forEach(relativePath -> {
sb.append(relativePath).append(System.lineSeparator());
});
relativePaths.sort(IOCase.SENSITIVE::checkCompareTo);
relativePaths.forEach(relativePath -> sb.append(relativePath).append(System.lineSeparator()));

String targetFile = config.outputFolder() + File.separator + METADATA_DIR + File.separator + config.getFilesMetadataFilename();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Scanner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

/**
Expand All @@ -33,6 +34,9 @@ public class TemplateManager implements TemplatingExecutor, TemplateProcessor {

private final Logger LOGGER = LoggerFactory.getLogger(TemplateManager.class);

/** Cache of resolved template path -> raw template content, populated on first read per run. */
private final Map<String, String> templateContentCache = new ConcurrentHashMap<>();

/**
* Constructs a new instance of a {@link TemplateManager}
*
Expand Down Expand Up @@ -75,7 +79,8 @@ private String getFullTemplateFile(String name) {
*/
@Override
public String getFullTemplateContents(String name) {
return readTemplate(getFullTemplateFile(name));
String fullPath = getFullTemplateFile(name);
return templateContentCache.computeIfAbsent(fullPath, this::readTemplate);
}

/**
Expand All @@ -89,15 +94,22 @@ public Path getFullTemplatePath(String name) {
return Paths.get(getFullTemplateFile(name));
}

/**
* Pre-compiled pattern for replacing the OS file separator with '/' in classpath resource paths.
* Only non-null on operating systems where {@link File#separator} is not already '/'.
*/
private static final Pattern FILE_SEP_PATTERN =
"/".equals(File.separator) ? null : Pattern.compile(Pattern.quote(File.separator));

/**
* Gets a normalized classpath resource location according to OS-specific file separator
*
* @param name The name of the resource file/directory to find
* @return A normalized string according to OS-specific file separator
*/
public static String getCPResourcePath(final String name) {
if (!"/".equals(File.separator)) {
return name.replaceAll(Pattern.quote(File.separator), "/");
if (FILE_SEP_PATTERN != null) {
return FILE_SEP_PATTERN.matcher(name).replaceAll("/");
}
return name;
}
Expand Down Expand Up @@ -262,6 +274,8 @@ private File writeToFileRaw(String filename, byte[] contents) throws IOException
}

private boolean filesEqual(File file1, File file2) throws IOException {
return file1.exists() && file2.exists() && Arrays.equals(Files.readAllBytes(file1.toPath()), Files.readAllBytes(file2.toPath()));
if (!file1.exists() || !file2.exists()) return false;
if (file1.length() != file2.length()) return false;
return Arrays.equals(Files.readAllBytes(file1.toPath()), Files.readAllBytes(file2.toPath()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@

import java.io.File;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

/**
* Locates templates according to {@link CodegenConfig} settings.
*/
public class GeneratorTemplateContentLocator implements TemplatePathLocator {
private final CodegenConfig codegenConfig;

/**
* Cache of relativeTemplateFile -> resolved full path (or empty Optional when the template does not exist).
* The filesystem/classpath existence probes inside resolveFullTemplatePath are expensive on repeated calls
* for the same template name, so we memoize the result for the lifetime of this locator instance.
*/
private final ConcurrentHashMap<String, Optional<String>> templatePathCache = new ConcurrentHashMap<>();

/**
* Constructs a new instance of {@link GeneratorTemplateContentLocator} for the provided {@link CodegenConfig}
*
Expand Down Expand Up @@ -51,12 +60,25 @@ private boolean classpathTemplateExists(String name) {
* 4) (embedded template dir)
* <p>
* Where "template dir" may be user defined and "embedded template dir" are the built-in templates for the given generator.
* <p>
* Results are cached per {@code relativeTemplateFile} name because the filesystem/classpath probes are expensive
* and the outcome is constant for the lifetime of this locator instance.
*
* @param relativeTemplateFile Template file
* @return String Full template file path
* @return String Full template file path, or {@code null} if the template does not exist in any location
*/
@Override
public String getFullTemplatePath(String relativeTemplateFile) {
return templatePathCache
.computeIfAbsent(relativeTemplateFile, key -> Optional.ofNullable(resolveFullTemplatePath(key)))
.orElse(null);
}

/**
* Performs the actual filesystem/classpath probes to find the full template path.
* Called at most once per unique {@code relativeTemplateFile} value; all subsequent lookups use the cache.
*/
private String resolveFullTemplatePath(String relativeTemplateFile) {
CodegenConfig config = this.codegenConfig;

//check the supplied template library folder for the file
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@

import java.io.IOException;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class HandlebarsEngineAdapter extends AbstractTemplatingEngineAdapter {
final Logger LOGGER = LoggerFactory.getLogger(HandlebarsEngineAdapter.class);
Expand All @@ -48,7 +48,24 @@ public class HandlebarsEngineAdapter extends AbstractTemplatingEngineAdapter {
// We use this as a simple lookup for valid file name extensions. This adapter will inspect .mustache (built-in) and infer the relevant handlebars filename
private final String[] canCompileFromExtensions = {".handlebars", ".hbs", ".mustache"};
private boolean infiniteLoops = false;
@Setter private boolean prettyPrint = false;
@Setter
private boolean prettyPrint = false;

/**
* Per-executor cache of fully-configured {@link Handlebars} engine instances.
* Each executor gets its own engine because the engine's {@link TemplateLoader} closes over the
* executor; sharing an engine across executors would silently resolve templates from the wrong source.
* {@link ConcurrentHashMap#computeIfAbsent} ensures the engine is built at most once per executor.
*/
private final ConcurrentHashMap<TemplatingExecutor, Handlebars> engineCache = new ConcurrentHashMap<>();

/**
* Per-executor cache of compiled {@link Template} objects.
* Keying on the executor instance eliminates the non-atomic check-clear-update invalidation
* that the previous single-cache approach required; no state ever needs to be cleared.
*/
private final ConcurrentHashMap<TemplatingExecutor, ConcurrentHashMap<String, Template>> templateCaches =
new ConcurrentHashMap<>();

/**
* Provides an identifier used to load the adapter. This could be a name, uuid, or any other string.
Expand All @@ -63,13 +80,6 @@ public String getIdentifier() {
@Override
public String compileTemplate(TemplatingExecutor executor,
Map<String, Object> bundle, String templateFile) throws IOException {
TemplateLoader loader = new AbstractTemplateLoader() {
@Override
public TemplateSource sourceAt(String location) {
return findTemplate(executor, location);
}
};

Context context = Context
.newBuilder(bundle)
.resolver(
Expand All @@ -79,9 +89,33 @@ public TemplateSource sourceAt(String location) {
AccessAwareFieldValueResolver.INSTANCE)
.build();

// Each executor gets its own Handlebars engine (the loader closes over the executor) and its
// own compiled-template cache. computeIfAbsent is atomic, so concurrent calls with the same
// executor share one engine/cache rather than racing to create duplicates.
Handlebars handlebars = engineCache.computeIfAbsent(executor, this::buildHandlebars);
ConcurrentHashMap<String, Template> cache =
templateCaches.computeIfAbsent(executor, k -> new ConcurrentHashMap<>());

// Manual get → compile → put so IOException propagates naturally.
Template tmpl = cache.get(templateFile);
if (tmpl == null) {
tmpl = handlebars.compile(templateFile);
cache.put(templateFile, tmpl);
}
return tmpl.apply(context);
}

/** Constructs and fully configures a {@link Handlebars} engine for the given executor. */
private Handlebars buildHandlebars(TemplatingExecutor executor) {
TemplateLoader loader = new AbstractTemplateLoader() {
@Override
public TemplateSource sourceAt(String location) {
return findTemplate(executor, location);
}
};
Handlebars handlebars = new Handlebars(loader);
handlebars.registerHelperMissing((obj, options) -> {
LOGGER.warn(String.format(Locale.ROOT, "Unregistered helper name '%s', processing template:%n%s", options.helperName, options.fn.text()));
LOGGER.warn("Unregistered helper name '{}', processing template:\n{}", options.helperName, options.fn.text());
return "";
});
handlebars.registerHelper("json", Jackson2Helper.INSTANCE);
Expand All @@ -90,8 +124,7 @@ public TemplateSource sourceAt(String location) {
handlebars.registerHelpers(org.openapitools.codegen.templating.handlebars.StringHelpers.class);
handlebars.setInfiniteLoops(infiniteLoops);
handlebars.setPrettyPrint(prettyPrint);
Template tmpl = handlebars.compile(templateFile);
return tmpl.apply(context);
return handlebars;
}

@SuppressWarnings("java:S108")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


public class MustacheEngineAdapter implements TemplatingEngineAdapter {
Expand All @@ -51,6 +52,20 @@ public String getIdentifier() {
@Getter @Setter
Mustache.Compiler compiler = Mustache.compiler();

/**
* Per-executor cache of template file name → compiled {@link Template}.
* <p>
* Keying on the executor instance eliminates the non-atomic check-clear-update invalidation pattern
* that the previous single-cache approach required. Each executor gets its own independent inner
* map, so different executors (e.g. different generator runs, test fixtures) can never observe
* each other's compiled templates, and no state ever needs to be cleared.
* <p>
* {@link ConcurrentHashMap#computeIfAbsent} guarantees that the inner map for a given executor
* is created exactly once even under concurrent access.
*/
private final ConcurrentHashMap<TemplatingExecutor, ConcurrentHashMap<String, Template>> compiledTemplateCaches =
new ConcurrentHashMap<>();

/**
* Compiles a template into a string
*
Expand All @@ -62,10 +77,22 @@ public String getIdentifier() {
*/
@Override
public String compileTemplate(TemplatingExecutor executor, Map<String, Object> bundle, String templateFile) throws IOException {
Template tmpl = compiler
.withLoader(name -> findTemplate(executor, name))
.defaultValue("")
.compile(executor.getFullTemplateContents(templateFile));
// Each executor gets its own compiled-template cache. computeIfAbsent is atomic, so two threads
// racing on the same executor key will share one inner map rather than creating two separate ones.
ConcurrentHashMap<String, Template> cache =
compiledTemplateCaches.computeIfAbsent(executor, k -> new ConcurrentHashMap<>());

// Manual get → compile → put so IOException propagates naturally.
// At worst, two threads compile the same template simultaneously; the last writer wins,
// which is harmless because compilation is pure/deterministic.
Template tmpl = cache.get(templateFile);
if (tmpl == null) {
tmpl = compiler
.withLoader(name -> findTemplate(executor, name))
.defaultValue("")
.compile(executor.getFullTemplateContents(templateFile));
cache.put(templateFile, tmpl);
}
StringWriter out = new StringWriter();

// the value of bundle[MUSTACHE_PARENT_CONTEXT] is used a parent content in mustache.
Expand Down
Loading