Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7077014
Test with
a-d Feb 25, 2025
5e50752
Revert changes
a-d Feb 25, 2025
592a389
Add fix
a-d Feb 25, 2025
0076de5
Add test
a-d Feb 25, 2025
ed8532a
Format
a-d Feb 26, 2025
5f8c291
Fix PMD
a-d Feb 26, 2025
947ee19
Fix merge
a-d Feb 26, 2025
54fb0b3
Delete datamodel/openapi/openapi-generator/src/test/resources/DataMod…
newtork Feb 26, 2025
9f8eb8a
Update datamodel/openapi/openapi-generator/src/test/java/com/sap/clou…
newtork Feb 26, 2025
b9c0813
Merge remote-tracking branch 'origin/main' into float-array-instead-o…
a-d Feb 27, 2025
a7dd05f
Merge
a-d Feb 27, 2025
4b51e58
Merge remote-tracking branch 'origin/float-array-instead-of-bigdecima…
a-d Feb 27, 2025
2b502a1
Adapting equals and hashcode generation for float[] (#737)
rpanackal Feb 28, 2025
d27fa30
Move out the custom gen feature login
rpanackal Feb 28, 2025
9966ed8
change access to package private
rpanackal Feb 28, 2025
fd64019
service loader pattern, visitor like pattern
a-d Feb 28, 2025
c6ea658
PoC
a-d Feb 28, 2025
693f5d0
Refine chained strategy pattern
a-d Mar 3, 2025
15919d9
Fix naming; add Javadoc
a-d Mar 3, 2025
83276ee
Fix log level
a-d Mar 4, 2025
9dcd3d4
Merge remote-tracking branch 'origin/main' into openapi/partial-gener…
a-d Mar 4, 2025
2b2ad23
Fix merge; Move files
a-d Mar 4, 2025
1fb0561
Fix PMD, re-order methods
a-d Mar 4, 2025
9118f7e
Minor format improvement
a-d Mar 4, 2025
a912116
Fix checkstyle
a-d Mar 4, 2025
231f62b
Fix PMD
a-d Mar 4, 2025
85bdc58
Merge remote-tracking branch 'origin/main' into openapi/partial-gener…
a-d Mar 10, 2025
2cb2e39
Revert refactoring
a-d Mar 10, 2025
4eed5eb
Revert "Revert refactoring"
a-d Mar 10, 2025
d5f637c
Merge remote-tracking branch 'origin/main' into openapi/generator-ext…
a-d Mar 12, 2025
d79d776
update class name
a-d Mar 12, 2025
2c0f7d8
Fix UseExcludeProperties
a-d Mar 12, 2025
70fc5ba
Fix FixRemoveUnusedComponents
a-d Mar 12, 2025
bed7181
Format
a-d Mar 12, 2025
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
Original file line number Diff line number Diff line change
@@ -1,27 +1,14 @@
package com.sap.cloud.sdk.datamodel.openapi.generator;

import static com.sap.cloud.sdk.datamodel.openapi.generator.GeneratorCustomProperties.FIX_REDUNDANT_IS_BOOLEAN_PREFIX;
import static com.sap.cloud.sdk.datamodel.openapi.generator.GeneratorCustomProperties.FIX_REMOVE_UNUSED_COMPONENTS;
import static com.sap.cloud.sdk.datamodel.openapi.generator.GeneratorCustomProperties.USE_EXCLUDE_PATHS;
import static com.sap.cloud.sdk.datamodel.openapi.generator.GeneratorCustomProperties.USE_EXCLUDE_PROPERTIES;
import static com.sap.cloud.sdk.datamodel.openapi.generator.GeneratorCustomProperties.USE_FLOAT_ARRAYS;
import static com.sap.cloud.sdk.datamodel.openapi.generator.GeneratorCustomProperties.USE_ONE_OF_CREATORS;

import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.function.Consumer;
import java.util.function.Function;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.CodegenOperation;
import org.openapitools.codegen.CodegenProperty;
import org.openapitools.codegen.languages.JavaClientCodegen;
import org.openapitools.codegen.model.ModelMap;
Expand All @@ -33,79 +20,85 @@
import io.swagger.v3.oas.models.media.Schema;
import lombok.extern.slf4j.Slf4j;

@SuppressWarnings( "PMD.TooManyStaticImports" )
@Slf4j
class CustomJavaClientCodegen extends JavaClientCodegen
{
private final List<GeneratorCustomization> customizations;
private final GenerationConfiguration config;
private static final Predicate<String> DOUBLE_IS_PATTERN = Pattern.compile("^isIs[A-Z]").asPredicate();
private static final Set<String> PRIMITIVES = Set.of("String", "Integer", "Long", "Double", "Float", "Byte");

public CustomJavaClientCodegen( @Nonnull final GenerationConfiguration config )
{
this.config = config;
this.customizations = GeneratorCustomization.getCustomizations(config);
}

@Override
public void preprocessOpenAPI( @Nonnull final OpenAPI openAPI )
private <HandlerT extends GeneratorCustomization.ChainableReturn<HandlerT>, ValueT> ValueT chainedContextReturn(
@Nonnull final Class<? extends HandlerT> handlerClass,
@Nonnull final HandlerT rootHandler,
@Nonnull final Function<GeneratorCustomization.ChainElementReturn<HandlerT, ValueT>, ValueT> initiator )
{
if( USE_EXCLUDE_PROPERTIES.isEnabled(config) ) {
final String[] exclusions = USE_EXCLUDE_PROPERTIES.getValue(config).trim().split("[,\\s]+");
for( final String exclusion : exclusions ) {
final String[] split = exclusion.split("\\.", 2);
preprocessRemoveProperty(openAPI, split[0], split[1]);
var chainedContext = rootHandler.<ValueT> chained(config, null);
for( final GeneratorCustomization customization : customizations ) {
if( handlerClass.isInstance(customization) ) {
chainedContext = handlerClass.cast(customization).chained(config, chainedContext);
}
}
return initiator.apply(chainedContext);
}

if( USE_EXCLUDE_PATHS.isEnabled(config) ) {
final String[] exclusions = USE_EXCLUDE_PATHS.getValue(config).trim().split("[,\\s]+");
for( final String exclusion : exclusions ) {
if( !openAPI.getPaths().keySet().remove(exclusion) ) {
log.error("Could not remove path {}", exclusion);
}
private <HandlerT extends GeneratorCustomization.ChainableVoid<HandlerT>> void chainedContextVoid(
@Nonnull final Class<? extends HandlerT> handlerClass,
@Nonnull final HandlerT rootHandler,
@Nonnull final Consumer<GeneratorCustomization.ChainElementVoid<HandlerT>> initiator )
{
var chainedContext = rootHandler.chained(config, null);
for( final GeneratorCustomization customization : customizations ) {
if( handlerClass.isInstance(customization) ) {
chainedContext = handlerClass.cast(customization).chained(config, chainedContext);
}
}
initiator.accept(chainedContext);
}

super.preprocessOpenAPI(openAPI);

if( FIX_REMOVE_UNUSED_COMPONENTS.isEnabled(config) ) {
preprocessRemoveRedundancies(openAPI);
}
@Override
public void preprocessOpenAPI( @Nonnull final OpenAPI openAPI )
{
chainedContextVoid(
GeneratorCustomization.PreProcessOpenAPI.class,
( context, openAPI1 ) -> super.preprocessOpenAPI(openAPI1),
context -> context.get().preprocessOpenAPI(context, openAPI));
}

@Override
protected
void
updatePropertyForArray( @Nonnull final CodegenProperty property, @Nonnull final CodegenProperty innerProperty )
{
super.updatePropertyForArray(property, innerProperty);

if( USE_FLOAT_ARRAYS.isEnabled(config) && innerProperty.isNumber && property.isArray ) {
property.datatypeWithEnum = "float[]";
property.vendorExtensions.put("isPrimitiveArray", true);
}
chainedContextVoid(
GeneratorCustomization.UpdatePropertyForArray.class,
( context, property1, innerProperty1 ) -> super.updatePropertyForArray(property1, innerProperty1),
context -> context.get().updatePropertyForArray(context, property, innerProperty));
}

@SuppressWarnings( { "rawtypes", "RedundantSuppression" } )
@Override
@Nullable
public String toDefaultValue( @Nonnull final CodegenProperty cp, @Nonnull final Schema schema )
{
if( USE_FLOAT_ARRAYS.isEnabled(config) && "float[]".equals(cp.datatypeWithEnum) ) {
return null;
}
return super.toDefaultValue(cp, schema);
return chainedContextReturn(
GeneratorCustomization.ToDefaultValue.class,
( context, cp1, schema1 ) -> super.toDefaultValue(cp1, schema1),
context -> context.get().toDefaultValue(context, cp, schema));
}

@Override
@Nullable
public String toBooleanGetter( @Nullable final String name )
{
final String result = super.toBooleanGetter(name);
if( FIX_REDUNDANT_IS_BOOLEAN_PREFIX.isEnabled(config) && result != null && DOUBLE_IS_PATTERN.test(result) ) {
return "is" + result.substring(4);
}
return result;
return chainedContextReturn(
GeneratorCustomization.ToBooleanGetter.class,
( context, name1 ) -> super.toBooleanGetter(name1),
context -> context.get().toBooleanGetter(context, name));
}

// Custom processor to inject "x-return-nullable" extension
Expand All @@ -115,14 +108,10 @@ public String toBooleanGetter( @Nullable final String name )
OperationsMap
postProcessOperationsWithModels( @Nonnull final OperationsMap ops, @Nonnull final List<ModelMap> allModels )
{
for( final CodegenOperation op : ops.getOperations().getOperation() ) {
final var noContent =
op.isResponseOptional
|| op.responses == null
|| op.responses.stream().anyMatch(r -> "204".equals(r.code));
op.vendorExtensions.put("x-return-nullable", op.returnType != null && noContent);
}
return super.postProcessOperationsWithModels(ops, allModels);
return chainedContextReturn(
GeneratorCustomization.PostProcessOperationsWithModels.class,
( context, ops1, allModels1 ) -> super.postProcessOperationsWithModels(ops1, allModels1),
context -> context.get().postProcessOperationsWithModels(context, ops, allModels));
}

@SuppressWarnings( { "rawtypes", "RedundantSuppression" } )
Expand All @@ -132,182 +121,19 @@ protected void updateModelForComposedSchema(
@Nonnull final Schema schema,
@Nonnull final Map<String, Schema> allDefinitions )
{
super.updateModelForComposedSchema(m, schema, allDefinitions);

if( USE_ONE_OF_CREATORS.isEnabled(config) ) {
useCreatorsForInterfaceSubtypes(m);
}
}

/**
* Remove property from specification.
*
* @param openAPI
* The OpenAPI specification to update.
* @param schemaName
* The name of the schema to update.
* @param propertyName
* The name of the property to remove.
*/
@SuppressWarnings( { "rawtypes", "unchecked", "ReplaceInefficientStreamCount" } )
private void preprocessRemoveProperty(
@Nonnull final OpenAPI openAPI,
@Nonnull final String schemaName,
@Nonnull final String propertyName )
{
final var schema = openAPI.getComponents().getSchemas().get(schemaName);
if( schema == null ) {
log.error("Could not find schema {} to remove property {} from.", schemaName, propertyName);
return;
}
boolean removed = false;

final Predicate<Schema> remove =
s -> s != null && s.getProperties() != null && s.getProperties().remove(propertyName) != null;
final var schemasQueued = new LinkedList<Schema>();
final var schemasDone = new HashSet<Schema>();
schemasQueued.add(schema);

while( !schemasQueued.isEmpty() ) {
final var s = schemasQueued.remove();
if( s == null || !schemasDone.add(s) ) {
continue;
}
// check removal of direct schema property
removed |= remove.test(s);

// check for allOf, anyOf, oneOf
for( final List<Schema> list : Arrays.asList(s.getAllOf(), s.getAnyOf(), s.getOneOf()) ) {
if( list != null ) {
schemasQueued.addAll(list);
}
}
}
if( !removed ) {
log.error("Could not remove property {} from schema {}.", propertyName, schemaName);
}
}

/**
* Remove unused schema components.
*
* @param openAPI
* The OpenAPI specification to update.
*/
@SuppressWarnings( { "rawtypes", "unchecked" } )
private void preprocessRemoveRedundancies( @Nonnull final OpenAPI openAPI )
{
final var queue = new LinkedList<Schema>();
final var done = new HashSet<Schema>();
final var refs = new LinkedHashSet<String>();
final var pattern = Pattern.compile("\\$ref: #/components/schemas/(\\w+)");

// find and queue schemas nested in paths
for( final var path : openAPI.getPaths().values() ) {
final var m = pattern.matcher(path.toString());
while( m.find() ) {
final var name = m.group(1);
final var schema = openAPI.getComponents().getSchemas().get(name);
queue.add(schema);
refs.add(m.group(0).split(" ")[1]);
}
}

while( !queue.isEmpty() ) {
final var s = queue.remove();
if( s == null || !done.add(s) ) {
continue;
}

// check for $ref attribute
final var ref = s.get$ref();
if( ref != null ) {
refs.add(ref);
final var refName = ref.substring(ref.lastIndexOf('/') + 1);
queue.add(openAPI.getComponents().getSchemas().get(refName));
}

// check for direct properties
if( s.getProperties() != null ) {
for( final var s1 : s.getProperties().values() ) {
queue.add((Schema) s1);
}
}

// check for array items
if( s.getItems() != null ) {
queue.add(s.getItems());
}

// check for allOf, anyOf, oneOf
for( final List<Schema> list : Arrays.asList(s.getAllOf(), s.getAnyOf(), s.getOneOf()) ) {
if( list != null ) {
queue.addAll(list);
}
}
}

// remove all schemas that have not been marked "used"
openAPI.getComponents().getSchemas().keySet().removeIf(schema -> {
if( !refs.contains("#/components/schemas/" + schema) ) {
log.info("Removing unused schema {}", schema);
return true;
}
return false;
});
}

/**
* Use JsonCreator for interface sub-types in case there are any primitives.
*
* @param m
* The model to update.
*/
private void useCreatorsForInterfaceSubtypes( @Nonnull final CodegenModel m )
{
if( m.discriminator != null ) {
return;
}
boolean useCreators = false;
for( final Set<String> candidates : List.of(m.anyOf, m.oneOf) ) {
int nonPrimitives = 0;
final var candidatesSingle = new HashSet<String>();
final var candidatesMultiple = new HashSet<String>();

for( final String candidate : candidates ) {
if( candidate.startsWith("List<") ) {
final var c1 = candidate.substring(5, candidate.length() - 1);
candidatesMultiple.add(c1);
useCreators = true;
} else {
candidatesSingle.add(candidate);
useCreators |= PRIMITIVES.contains(candidate);
if( !PRIMITIVES.contains(candidate) ) {
nonPrimitives++;
}
}
}
if( useCreators ) {
if( nonPrimitives > 1 ) {
final var msg =
"Generating interface with mixed multiple non-primitive and primitive sub-types: {}. Deserialization may not work.";
log.warn(msg, m.name);
}
candidates.clear();
final var monads = Map.of("single", candidatesSingle, "multiple", candidatesMultiple);
m.vendorExtensions.put("x-monads", monads);
m.vendorExtensions.put("x-is-one-of-interface", true); // enforce template usage
}
}
chainedContextVoid(
GeneratorCustomization.UpdateModelForComposedSchema.class,
( context, m1, schema1, allDef1 ) -> super.updateModelForComposedSchema(m1, schema1, allDef1),
context -> context.get().updateModelForComposedSchema(context, m, schema, allDefinitions));
}

@SuppressWarnings( { "rawtypes", "RedundantSuppression" } )
@Override
protected void updateModelForObject( @Nonnull final CodegenModel m, @Nonnull final Schema schema )
{
// Disable additional attributes to prevent model classes from extending "HashMap"
// SAP Cloud SDK offers custom field APIs to handle additional attributes already
schema.setAdditionalProperties(Boolean.FALSE);
super.updateModelForObject(m, schema);
chainedContextVoid(
GeneratorCustomization.UpdateModelForObject.class,
( context, m1, schema1 ) -> super.updateModelForObject(m1, schema1),
context -> context.get().updateModelForObject(context, m, schema));
}
}
Loading
Loading