Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import java.util.Map;

import org.iplass.mtp.SystemException;
import org.iplass.mtp.impl.util.ClassUtil;
import org.iplass.mtp.impl.webapi.openapi.OpenApiService;
import org.iplass.mtp.spi.ServiceRegistry;
Expand All @@ -42,11 +43,87 @@
* @author SEKIGUCHI Naoya
*/
public class OpenApiComponentClassReusableSchemaFactory implements OpenApiComponentReusableSchemaFactory<Class<?>>, OpenApiComponentTarget {
/** 再利用可能なスキーマの参照プレフィックス */
private static final String DEFS = "$defs";

/** ログ */
private Logger logger = LoggerFactory.getLogger(OpenApiComponentClassReusableSchemaFactory.class);
/** JSON Schema ジェネレータ */
private ClassSchemaGenerator schemaGenerator;

/**
* スキーマタイプ検出器
* <p>
* Schema オブジェクトから配列型やオブジェクト型を判定します。
* </p>
*/
private static class SchemaTypeDetector {
/** スキーマタイプ: object */
private static final String OBJECT = "object";
/** スキーマタイプ: array */
private static final String ARRAY = "array";

/** 検出対象のスキーマ */
private final Schema<?> schema;

/**
* コンストラクタ
* @param schema 検出対象のスキーマ
*/
SchemaTypeDetector(Schema<?> schema) {
this.schema = schema;
}

/**
* 配列型かどうかを判定します
* @return 配列型の場合 true
*/
boolean isArray() {
if (ARRAY.equals(schema.getType())) {
// type が array の場合
return true;
}

if (schema.getTypes() != null && schema.getTypes()
.contains(ARRAY)) {
// types に array が含まれている場合
return true;
}

if (schema.getItems() != null && StringUtil.isNotEmpty(schema.getItems()
.get$ref())) {
// items に $ref が設定されている場合は、配列型の可能性があるため true を返す
return true;
}

return false;
}

/**
* オブジェクト型かどうかを判定します
* @return オブジェクト型の場合 true
*/
boolean isObject() {
if (OBJECT.equals(schema.getType())) {
// type が object の場合
return true;
}

if (schema.getTypes() != null && schema.getTypes()
.contains(OBJECT)) {
// types に object が含まれている場合
return true;
}

if (StringUtil.isNotEmpty(schema.get$ref())) {
// $ref が設定されている場合は、オブジェクト型の可能性があるため true を返す
return true;
}

return false;
}
}

@Override
public boolean isTarget(Object object) {
return object instanceof Class;
Expand All @@ -68,7 +145,7 @@ public String addReusableSchema(Class<?> clazz, OpenAPI openApi, OpenApiJsonSche

var components = getOpenApiComponents(openApi);

if (null != components.getSchemas() && components.getSchemas()
if (components.getSchemas() != null && components.getSchemas()
.containsKey(name)) {
// すでに定義済みの場合は再利用
return ref;
Expand Down Expand Up @@ -97,22 +174,25 @@ public void setClassSchemaGenerator(ClassSchemaGenerator schemaGenerator) {
* クラスから ObjectSchema を生成します。
* @param clazz スキーマに変換するクラス
* @param openApi OpenAPI オブジェクト
* @param schemaType JSONスキーマタイプ
* @return 生成された ObjectSchema
*/
@SuppressWarnings("unchecked")
private Schema<?> createObjectSchemaFromClass(Class<?> clazz, OpenAPI openApi, OpenApiJsonSchemaType schemaType) {
var openApiService = ServiceRegistry.getRegistry()
.getService(OpenApiService.class);
var standardClassSchemaResolver = openApiService.getStandardClassSchemaResolver();

try {
// Class to JsonSchema
String jsonSchemaString = schemaGenerator.generate(clazz);
logger.trace("Class to JsonSchema. class={}, jsonSchema={}", clazz, jsonSchemaString);
// JsonSchema to OpenAPI Schema
var resolver = ServiceRegistry.getRegistry()
.getService(OpenApiService.class)
.getOpenApiResolver();
var openApiVersion = openApi.getOpenapi()
.split("\\.");
var seriesVersion = openApiVersion[0] + "." + openApiVersion[1];
var mapper = resolver.getObjectMapper(OpenApiFileType.JSON, OpenApiVersion.fromSeriesVersion(seriesVersion));
var mapper = openApiService.getOpenApiResolver()
.getObjectMapper(OpenApiFileType.JSON, OpenApiVersion.fromSeriesVersion(seriesVersion));
var jsonSchema = mapper.readValue(jsonSchemaString, Schema.class);

if (clazz.isEnum()) {
Expand All @@ -123,13 +203,12 @@ private Schema<?> createObjectSchemaFromClass(Class<?> clazz, OpenAPI openApi, O
if (StringUtil.isNotEmpty(jsonSchema.get$ref())) {
// 解析した結果が $ref になっている場合は、$defs に指定されているスキーマを取得してそれを対象スキーマとして設定する。
var jsonSchemaMap = mapper.readValue(jsonSchemaString, Map.class);
var defsMap = (Map<String, Object>) jsonSchemaMap.get("$defs");
var defsMap = (Map<String, Object>) jsonSchemaMap.get(DEFS);

jsonSchema = getDefsSchema(defsMap, jsonSchema, mapper);
}

//
if (null == jsonSchema.getProperties()) {
if (jsonSchema.getProperties() == null) {
// プロパティが null の場合はオブジェクトをそのまま返却する
return jsonSchema;
}
Expand All @@ -138,101 +217,103 @@ private Schema<?> createObjectSchemaFromClass(Class<?> clazz, OpenAPI openApi, O
openApiSchema.setProperties(jsonSchema.getProperties());
openApiSchema.setTitle(schemaType.name() + " Class Schema " + clazz.getSimpleName());

var standardClassSchemaResolver = ServiceRegistry.getRegistry()
.getService(OpenApiService.class)
.getStandardClassSchemaResolver();
@SuppressWarnings("rawtypes")
Map<String, Schema> props = openApiSchema.getProperties();
var parser = new PropertyDescriptorParser(clazz);
props.forEach((key, value) -> {
var isContainsArray = null != value.getTypes() && value.getTypes()
.contains("array");
var isTypeArray = "array".equals(value.getType());
var isNotEmptyArrayRef = StringUtil.isNotEmpty(value.getItems() != null ? value.getItems()
.get$ref() : null);
var isArray = isContainsArray || isTypeArray || isNotEmptyArrayRef;

var isContainsObject = null != value.getTypes() && value.getTypes()
.contains("object");
var isTypeObject = "object".equals(value.getType());
var isNotEmptyObjectRef = StringUtil.isNotEmpty(value.get$ref());
var isObject = isContainsObject || isTypeObject || isNotEmptyObjectRef;
props.forEach((key, schema) -> {
var detector = new SchemaTypeDetector(schema);

if (logger.isTraceEnabled()) {
logger.trace("class={}, property={}, isArray={}, isObject={}, schema={}", clazz, key, isArray, isObject, value.toString());
// 引数を toString しているため、isTraceEnable でガードする
logger.trace("class={}, property={}, isArray={}, isObject={}, schema={}",
clazz, key, detector.isArray(), detector.isObject(), schema.toString());
}

if (isArray) {
// type が array の場合は、標準データ型を判定
var descriptor = parser.getPropertyDesctiptor(key);
if (detector.isArray()) {
// type が array の場合は、配列クラスもしくは、コレクション派生クラスとなる。標準データ型を判定
var descriptor = parser.getPropertyDescriptor(key);
var propType = descriptor.getPropertyType();

logger.trace("array type. class={}, property={}, propertyType={}", clazz, key, propType);
if (propType.isArray()) {
// クラス配列の場合
var extractClass = propType.getComponentType();
logger.trace("array type. class={}, property={}, propertyType={}, extractClass={}", clazz, key, propType, extractClass);

if (standardClassSchemaResolver.canResolve(propType)) {
// 解決可能な場合は、スキーマを解決して設定
props.put(key, value.items(standardClassSchemaResolver.resolve(propType, schemaType)));
updateArrayPropertySchema(props, key, schema, extractClass, standardClassSchemaResolver, openApi, schemaType);

} else if (propType.isAssignableFrom(java.util.List.class) || propType.isAssignableFrom(java.util.Set.class)) {
// List, Set 派生の場合は、 generics を取得する
} else {
// List, Set 派生の場合は、generics を取得する
var genericReturnType = descriptor.getReadMethod()
.getGenericReturnType();
if (genericReturnType instanceof java.lang.reflect.ParameterizedType pt) {
// List, Set なので必ず Genericsタイプは1つ
var genericType = pt.getActualTypeArguments()[0];
var genericClass = ClassUtil.forName(genericType.getTypeName());

logger.trace("array generic type. class={}, property={}, genericType={}", clazz, key, genericClass);

if (standardClassSchemaResolver.canResolve(genericClass)) {
// 解決可能な場合は、スキーマを解決して設定
props.put(key, value.items(standardClassSchemaResolver.resolve(genericClass, schemaType)));
logger.trace("collection type. class={}, property={}, propertyType={}, genericType={}", clazz, key, propType, genericType);

} else {
var genericClass = ClassUtil.forName(genericType.getTypeName());

// 解決できない場合は、再利用可能なスキーマとして追加
var refName = addReusableSchema(genericClass, openApi, schemaType);
value.getItems()
.set$ref(refName);
value.getItems()
.properties(null);
}
updateArrayPropertySchema(props, key, schema, genericClass, standardClassSchemaResolver, openApi, schemaType);
}

} else {
// 解決できない場合は、再利用可能なスキーマとして追加
var refName = addReusableSchema(propType, openApi, schemaType);
value.getItems()
.set$ref(refName);
value.getItems()
.properties(null);
}

} else if (isObject) {
} else if (detector.isObject()) {
// type が object の場合は、標準データ型を判定
var propType = parser.getPropertyDesctiptor(key)
var propType = parser.getPropertyDescriptor(key)
.getPropertyType();
logger.trace("object type. class={}, property={}, propertyType={}", clazz, key, propType);
if (standardClassSchemaResolver.canResolve(propType)) {
// 解決可能な場合は、スキーマを解決して設定
props.put(key, standardClassSchemaResolver.resolve(propType, schemaType));

} else {
// 解決できない場合は、再利用可能なスキーマとして追加
// カスタムクラスの場合は、components/schemas に再利用可能なスキーマとして追加
var refName = addReusableSchema(propType, openApi, schemaType);
value.set$ref(refName);
// 配下のプロパティは null を設定
value.setProperties(null);
// schema に参照($ref)を設定
schema.set$ref(refName);
// $ref を使用する場合、OpenAPI仕様上 properties は不要なため null を設定
schema.setProperties(null);
}
}
});

return openApiSchema;

} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to create ObjectSchema for class: " + clazz.getName(), e);
throw new SystemException("Failed to create ObjectSchema for class: " + clazz.getName(), e);
}
}

/**
* 配列型プロパティのスキーマを更新します。
* <p>
* 配列の要素クラス(コンポーネント型またはジェネリクス型)に応じて、適切なスキーマを設定します。
* </p>
*
* @param props プロパティマップ(更新対象)
* @param propKey 更新対象のプロパティ名
* @param propSchema 更新対象のプロパティスキーマ
* @param extractClassFromArray 配列から抽出された要素クラス(配列の場合はコンポーネント型、コレクションの場合はジェネリクス型)
* @param standardClassSchemaResolver 標準クラススキーマリゾルバ
* @param openApi OpenAPI オブジェクト
* @param schemaType スキーマタイプ(REQUEST/RESPONSE)
*/
@SuppressWarnings("rawtypes")
private void updateArrayPropertySchema(Map<String, Schema> props, String propKey, Schema<?> propSchema, Class<?> extractClassFromArray,
OpenApiStandardClassSchemaResolver standardClassSchemaResolver, OpenAPI openApi, OpenApiJsonSchemaType schemaType) {

if (standardClassSchemaResolver.canResolve(extractClassFromArray)) {
// 標準クラス(String, Integer, Long 等)の場合は、対応する型スキーマを items に設定
props.put(propKey, propSchema.items(standardClassSchemaResolver.resolve(extractClassFromArray, schemaType)));

} else {
// カスタムクラスの場合は、components/schemas に再利用可能なスキーマとして追加
var refName = addReusableSchema(extractClassFromArray, openApi, schemaType);
var items = propSchema.getItems();
// items に参照($ref)を設定
items.set$ref(refName);
// $ref を使用する場合、OpenAPI仕様上 properties は不要なため null を設定
items.properties(null);
}
}

/**
Expand All @@ -245,7 +326,7 @@ private Schema<?> createObjectSchemaFromClass(Class<?> clazz, OpenAPI openApi, O
*/
private Components getOpenApiComponents(OpenAPI openApi) {
var components = openApi.getComponents();
if (null == components) {
if (components == null) {
components = new Components();
openApi.setComponents(components);
}
Expand All @@ -254,7 +335,7 @@ private Components getOpenApiComponents(OpenAPI openApi) {
}

/**
* 再利用可能なスキーマ名を取得する
* 再利用可能なスキーマ名を取得します
* @param clazz スキーマに変換するクラス
* @param schemaType スキーマタイプ
* @return 再利用可能なスキーマ名
Expand All @@ -265,25 +346,42 @@ private String getReusableSchemaName(Class<?> clazz, OpenApiJsonSchemaType schem

/**
* JsonSchema の $defs からスキーマを取得します
* <h3>注意点</h3>
* <ul>
* <li>本メソッドは、{@link io.swagger.v3.oas.models.media.Schema#get$ref()} が非nullであることを確認後に実行してください。</li>
* </ul>
*
* @param defsMap $defs のマップ
* @param schema $refs が設定されているスキーマ
* @param mapper オブジェクトマッパー
* @return $refs が設定されているスキーマの定義
* @throws JsonProcessingException
* @throws JsonProcessingException $defs からスキーマの取得に失敗した場合
*/
private Schema<?> getDefsSchema(Map<String, Object> defsMap, Schema<?> schema, ObjectMapper mapper) throws JsonProcessingException {
var defsKey = schema.get$ref()
.substring(schema.get$ref()
.lastIndexOf('/') + 1);
var ref = schema.get$ref();
if (StringUtil.isEmpty(ref)) {
throw new SystemException("Schema $ref is empty");
}

var lastSeparatorIndex = ref.lastIndexOf('/');
if (lastSeparatorIndex < 0) {
throw new SystemException("Invalid schema $ref format: " + ref);
}

var defsKey = ref.substring(lastSeparatorIndex + 1);
if (!defsMap.containsKey(defsKey)) {
throw new SystemException("Schema definition not found for key: " + defsKey + " in " + DEFS);
}

var defSchemaObject = defsMap.get(defsKey);
var defSchemaString = mapper.writeValueAsString(defSchemaObject);
var defSchema = mapper.readValue(defSchemaString, Schema.class);

if (StringUtil.isNotEmpty(defSchema.get$ref())) {
// $ref が設定されている場合は、再帰的に取得する
return getDefsSchema(defsMap, defSchema, mapper);
} else {
return defSchema;
}

return defSchema;
}
}
Loading
Loading