Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to customize classes schemas in properties. #840

Merged
merged 1 commit into from
Nov 16, 2022
Merged
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
6 changes: 3 additions & 3 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
projectVersion=4.6.1-SNAPSHOT
projectVersion=4.7.0-SNAPSHOT
projectGroup=io.micronaut.openapi

micronautDocsVersion=2.0.0
micronautVersion=3.7.2
micronautVersion=3.7.3
micronautTestVersion=3.7.0
groovyVersion=3.0.13
spockVersion=2.3-groovy-3.0
Expand All @@ -13,7 +13,7 @@ projectUrl=https://micronaut.io
githubSlug=micronaut-projects/micronaut-openapi
developers=Puneet Behl,Álvaro Sánchez-Mariscal,Iván López

githubCoreBranch=3.7.x
githubCoreBranch=3.8.x

bomProperty=micronautOpenapiVersion
bomProperties=swaggerVersion
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ managed-slf4j = "1.7.36"
# Versions beyond 0.62.2 require Java 11
managed-html2md-converter = "0.62.2"

kotlin = "1.7.10"
kotlin = "1.7.21"

[libraries]
# Duplicated to keep catalog compatibility with 3.4.x. Can be removed for 4.0.0
Expand Down
2 changes: 1 addition & 1 deletion gradle/license.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ license {
java = 'SLASHSTAR_STYLE'
groovy = 'SLASHSTAR_STYLE'
}
ext.year = '2017-2020'
ext.year = '2017-2022'

exclude "**/transaction/**"
exclude '**/*.txt'
Expand Down
4 changes: 4 additions & 0 deletions openapi/openapi-custom-schema-for-class.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
micronaut.openapi.schema.io.micronaut.openapi.ObjectId=java.lang.String
micronaut.openapi.schema.io.micronaut.openapi.JAXBElement=io.micronaut.openapi.MyJaxbElement
micronaut.openapi.schema.io.micronaut.openapi.JAXBElement<test.XmlElement2>=io.micronaut.openapi.MyJaxbElement2
micronaut.openapi.schema.io.micronaut.openapi.JAXBElement<test.XmlElement3>=io.micronaut.openapi.MyJaxbElement3

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
Expand All @@ -50,6 +51,7 @@
import io.micronaut.context.env.Environment;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.naming.conventions.StringConvention;
import io.micronaut.core.type.Argument;
import io.micronaut.core.type.GenericArgument;
import io.micronaut.core.util.CollectionUtils;
Expand Down Expand Up @@ -98,7 +100,6 @@
OpenApiApplicationVisitor.MICRONAUT_OPENAPI_TARGET_FILE,
OpenApiApplicationVisitor.MICRONAUT_OPENAPI_ADDITIONAL_FILES,
OpenApiApplicationVisitor.MICRONAUT_OPENAPI_CONFIG_FILE,

})
public class OpenApiApplicationVisitor extends AbstractOpenApiVisitor implements TypeElementVisitor<OpenAPIDefinition, Object> {

Expand Down Expand Up @@ -179,6 +180,24 @@ public class OpenApiApplicationVisitor extends AbstractOpenApiVisitor implements
private static final String MICRONAUT_ENVIRONMENT_CREATED = "micronaut.environment.created";
private static final String MICRONAUT_OPENAPI_PROPERTIES = "micronaut.openapi.properties";
private static final String MICRONAUT_OPENAPI_ENDPOINTS = "micronaut.openapi.endpoints";

/**
* Properties prefix to set custom schema implementations for selected clases.
* For example, if you want to set simple 'java.lang.String' class to some complex 'org.somepackage.MyComplexType' class you need to write:
* <p>
* micronaut.openapi.schema.org.somepackage.MyComplexType=java.lang.String
*
* Also, you can set it in your application.yml file like this:
* <p>
* micronaut:
* openapi:
* schema:
* org.somepackage.MyComplexType: java.lang.String
* org.somepackage.MyComplexType2: java.lang.Integer
* ...
*/
private static final String MICRONAUT_OPENAPI_SCHEMA = "micronaut.openapi.schema";
private static final String MICRONAUT_CUSTOM_SCHEMAS = "micronaut.internal.custom.schemas";
/**
* Loaded expandable properties. Need to save them to reuse in diffferent places.
*/
Expand Down Expand Up @@ -247,6 +266,108 @@ public void visitClass(ClassElement element, VisitorContext context) {
classElement = element;
}

public static ClassElement getCustomSchema(String className, Map<String, ClassElement> typeArgs, VisitorContext context) {

Map<String, CustomSchema> customSchemas = (Map<String, CustomSchema>) context.get(MICRONAUT_CUSTOM_SCHEMAS, Map.class).orElse(null);
if (customSchemas != null) {
String key = getClassNameWithGenerics(className, typeArgs);

CustomSchema customSchema = customSchemas.get(key);
if (customSchema != null) {
return customSchema.classElement;
}
customSchema = customSchemas.get(className);

return customSchema != null ? customSchema.classElement : null;
}

customSchemas = new HashMap<>();

// first read system properties
Properties sysProps = System.getProperties();
readCustomSchemas(sysProps, customSchemas, context);

// second read openapi.properties file
Properties fileProps = readOpenApiConfigFile(context);
readCustomSchemas(fileProps, customSchemas, context);

// third read environments properties
Environment environment = getEnv(context);
for (Map.Entry<String, Object> entry : environment.getProperties(MICRONAUT_OPENAPI_SCHEMA, StringConvention.RAW).entrySet()) {
String configuredClassName = entry.getKey();
String targetClassName = (String) entry.getValue();
readCustomSchema(configuredClassName, targetClassName, customSchemas, context);
}

context.put(MICRONAUT_CUSTOM_SCHEMAS, customSchemas);

if (customSchemas.isEmpty()) {
return null;
}

String key = getClassNameWithGenerics(className, typeArgs);

CustomSchema customSchema = customSchemas.get(key);
if (customSchema != null) {
return customSchema.classElement;
}
customSchema = customSchemas.get(className);

return customSchema != null ? customSchema.classElement : null;
}

private static String getClassNameWithGenerics(String className, Map<String, ClassElement> typeArgs) {
StringBuilder key = new StringBuilder(className);
if (!typeArgs.isEmpty()) {
key.append('<');
boolean isFirst = true;
for (ClassElement typeArg : typeArgs.values()) {
if (!isFirst) {
key.append(',');
}
key.append(typeArg.getName());
isFirst = false;
}
key.append('>');
}
return key.toString();
}

private static void readCustomSchemas(Properties props, Map<String, CustomSchema> customSchemas, VisitorContext context) {

for (String prop : props.stringPropertyNames()) {
if (!prop.startsWith(MICRONAUT_OPENAPI_SCHEMA)) {
continue;
}
String className = prop.substring(MICRONAUT_OPENAPI_SCHEMA.length() + 1);
String targetClassName = props.getProperty(prop);
readCustomSchema(className, targetClassName, customSchemas, context);
}
}

private static void readCustomSchema(String className, String targetClassName, Map<String, CustomSchema> customSchemas, VisitorContext context) {
if (customSchemas.containsKey(className)) {
return;
}
ClassElement targetClassElement = context.getClassElement(targetClassName).orElse(null);
if (targetClassElement == null) {
context.warn("Can't find class " + targetClassName + " in classpath. Skip it.", null);
return;
}

List<String> configuredTypeArgs = null;
int genericNameStart = className.indexOf('<');
if (genericNameStart > 0) {
String[] generics = className.substring(genericNameStart + 1, className.indexOf('>')).split(",");
configuredTypeArgs = new ArrayList<>();
for (String generic : generics) {
configuredTypeArgs.add(generic.trim());
}
}

customSchemas.put(className, new CustomSchema(configuredTypeArgs, targetClassElement));
}

public static String getConfigurationProperty(String key, VisitorContext context) {
String value = System.getProperty(key, readOpenApiConfigFile(context).getProperty(key));
if (value != null) {
Expand Down Expand Up @@ -807,4 +928,23 @@ public String translate(String propertyName) {
}

}

static final class CustomSchema {

private final List<String> typeArgs;
private final ClassElement classElement;

private CustomSchema(List<String> typeArgs, ClassElement classElement) {
this.typeArgs = typeArgs;
this.classElement = classElement;
}

public List<String> getTypeArgs() {
return typeArgs;
}

public ClassElement getClassElement() {
return classElement;
}
}
}
174 changes: 174 additions & 0 deletions openapi/src/test/groovy/io/micronaut/openapi/JAXBElement.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package io.micronaut.openapi;

import java.io.Serializable;

import javax.xml.namespace.QName;

/**
* This class is copy of jakarta.xml.bind.api.JAXBElement
*/
public class JAXBElement<T> implements Serializable {

/**
* xml element tag name
*/
final protected QName name;

/**
* Java datatype binding for xml element declaration's type.
*/
final protected Class<T> declaredType;

final protected Class<?> scope;

/**
* xml element value.
* Represents content model and attributes of an xml element instance.
*/
protected T value;

/**
* true iff the xml element instance has xsi:nil="true".
*/
protected boolean nil = false;

/**
* Designates global scope for an xml element.
*/
public static final class GlobalScope {

private GlobalScope() {
}
}

/**
* <p>Construct an xml element instance.</p>
*
* @param name Java binding of xml element tag name
* @param declaredType Java binding of xml element declaration's type
* @param scope Java binding of scope of xml element declaration.
* Passing null is the same as passing {@code GlobalScope.class}
* @param value Java instance representing xml element's value.
*
* @see #getScope()
* @see #isTypeSubstituted()
*/
public JAXBElement(QName name,
Class<T> declaredType,
Class<?> scope,
T value) {
if (declaredType == null || name == null) {
throw new IllegalArgumentException();
}
this.declaredType = declaredType;
if (scope == null) {
scope = GlobalScope.class;
}
this.scope = scope;
this.name = name;
this.value = value;
}

/**
* Construct a xml element instance.
* <p>
* This is just a convenience method for {@code new JAXBElement(name,declaredType,GlobalScope.class,value)}
*/
public JAXBElement(QName name, Class<T> declaredType, T value) {
this(name, declaredType, GlobalScope.class, value);
}

/**
* Returns the Java binding of the xml element declaration's type attribute.
*/
public Class<T> getDeclaredType() {
return declaredType;
}

/**
* Returns the xml element tag name.
*/
public QName getName() {
return name;
}

/**
* <p>Set the content model and attributes of this xml element.</p>
*
* <p>When this property is set to {@code null}, {@code isNil()} must by {@code true}.
* Details of constraint are described at {@link #isNil()}.</p>
*
* @see #isTypeSubstituted()
*/
public void setValue(T t) {
value = t;
}

/**
* <p>Return the content model and attribute values for this element.</p>
*
* <p>See {@link #isNil()} for a description of a property constraint when
* this value is {@code null}</p>
*/
public T getValue() {
return value;
}

/**
* Returns scope of xml element declaration.
*
* @return {@code GlobalScope.class} if this element is of global scope.
*
* @see #isGlobalScope()
*/
public Class<?> getScope() {
return scope;
}

/**
* <p>Returns {@code true} iff this element instance content model
* is nil.</p>
*
* <p>This property always returns {@code true} when {@link #getValue()} is null.
* Note that the converse is not true, when this property is {@code true},
* {@link #getValue()} can contain a non-null value for attribute(s). It is
* valid for a nil xml element to have attribute(s).</p>
*/
public boolean isNil() {
return (value == null) || nil;
}

/**
* <p>Set whether this element has nil content.</p>
*
* @see #isNil()
*/
public void setNil(boolean value) {
nil = value;
}

/* Convenience methods
* (Not necessary but they do unambiguously conceptualize
* the rationale behind this class' fields.)
*/

/**
* Returns true iff this xml element declaration is global.
*/
public boolean isGlobalScope() {
return scope == GlobalScope.class;
}

/**
* Returns true iff this xml element instance's value has a different
* type than xml element declaration's declared type.
*/
public boolean isTypeSubstituted() {
if (value == null) {
return false;
}
return value.getClass() != declaredType;
}

private static final long serialVersionUID = 1L;
}
Loading