diff --git a/iplass-web/src/main/java/org/iplass/mtp/impl/webapi/openapi/schema/OpenApiComponentClassReusableSchemaFactory.java b/iplass-web/src/main/java/org/iplass/mtp/impl/webapi/openapi/schema/OpenApiComponentClassReusableSchemaFactory.java index 8689708e51..510078e78a 100644 --- a/iplass-web/src/main/java/org/iplass/mtp/impl/webapi/openapi/schema/OpenApiComponentClassReusableSchemaFactory.java +++ b/iplass-web/src/main/java/org/iplass/mtp/impl/webapi/openapi/schema/OpenApiComponentClassReusableSchemaFactory.java @@ -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; @@ -42,11 +43,87 @@ * @author SEKIGUCHI Naoya */ public class OpenApiComponentClassReusableSchemaFactory implements OpenApiComponentReusableSchemaFactory>, OpenApiComponentTarget { + /** 再利用可能なスキーマの参照プレフィックス */ + private static final String DEFS = "$defs"; + /** ログ */ private Logger logger = LoggerFactory.getLogger(OpenApiComponentClassReusableSchemaFactory.class); /** JSON Schema ジェネレータ */ private ClassSchemaGenerator schemaGenerator; + /** + * スキーマタイプ検出器 + *

+ * Schema オブジェクトから配列型やオブジェクト型を判定します。 + *

+ */ + 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; @@ -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; @@ -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()) { @@ -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) jsonSchemaMap.get("$defs"); + var defsMap = (Map) jsonSchemaMap.get(DEFS); jsonSchema = getDefsSchema(defsMap, jsonSchema, mapper); } - // - if (null == jsonSchema.getProperties()) { + if (jsonSchema.getProperties() == null) { // プロパティが null の場合はオブジェクトをそのまま返却する return jsonSchema; } @@ -138,79 +217,48 @@ 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 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)) { @@ -218,11 +266,12 @@ private Schema createObjectSchemaFromClass(Class clazz, OpenAPI openApi, O 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); } } }); @@ -230,9 +279,41 @@ private Schema createObjectSchemaFromClass(Class clazz, OpenAPI openApi, O 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); } + } + + /** + * 配列型プロパティのスキーマを更新します。 + *

+ * 配列の要素クラス(コンポーネント型またはジェネリクス型)に応じて、適切なスキーマを設定します。 + *

+ * + * @param props プロパティマップ(更新対象) + * @param propKey 更新対象のプロパティ名 + * @param propSchema 更新対象のプロパティスキーマ + * @param extractClassFromArray 配列から抽出された要素クラス(配列の場合はコンポーネント型、コレクションの場合はジェネリクス型) + * @param standardClassSchemaResolver 標準クラススキーマリゾルバ + * @param openApi OpenAPI オブジェクト + * @param schemaType スキーマタイプ(REQUEST/RESPONSE) + */ + @SuppressWarnings("rawtypes") + private void updateArrayPropertySchema(Map 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); + } } /** @@ -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); } @@ -254,7 +335,7 @@ private Components getOpenApiComponents(OpenAPI openApi) { } /** - * 再利用可能なスキーマ名を取得する + * 再利用可能なスキーマ名を取得します * @param clazz スキーマに変換するクラス * @param schemaType スキーマタイプ * @return 再利用可能なスキーマ名 @@ -265,16 +346,33 @@ private String getReusableSchemaName(Class clazz, OpenApiJsonSchemaType schem /** * JsonSchema の $defs からスキーマを取得します + *

注意点

+ *
    + *
  • 本メソッドは、{@link io.swagger.v3.oas.models.media.Schema#get$ref()} が非nullであることを確認後に実行してください。
  • + *
+ * * @param defsMap $defs のマップ * @param schema $refs が設定されているスキーマ * @param mapper オブジェクトマッパー * @return $refs が設定されているスキーマの定義 - * @throws JsonProcessingException + * @throws JsonProcessingException $defs からスキーマの取得に失敗した場合 */ private Schema getDefsSchema(Map 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); @@ -282,8 +380,8 @@ private Schema getDefsSchema(Map defsMap, Schema schema, O if (StringUtil.isNotEmpty(defSchema.get$ref())) { // $ref が設定されている場合は、再帰的に取得する return getDefsSchema(defsMap, defSchema, mapper); - } else { - return defSchema; } + + return defSchema; } } diff --git a/iplass-web/src/main/java/org/iplass/mtp/impl/webapi/openapi/schema/PropertyDescriptorParser.java b/iplass-web/src/main/java/org/iplass/mtp/impl/webapi/openapi/schema/PropertyDescriptorParser.java index 4d3f7180b6..716dd15d5b 100644 --- a/iplass-web/src/main/java/org/iplass/mtp/impl/webapi/openapi/schema/PropertyDescriptorParser.java +++ b/iplass-web/src/main/java/org/iplass/mtp/impl/webapi/openapi/schema/PropertyDescriptorParser.java @@ -24,6 +24,8 @@ import java.beans.Introspector; import java.beans.PropertyDescriptor; +import org.iplass.mtp.SystemException; + /** * プロパティ記述子を解析するクラス * @author SEKIGUCHI Naoya @@ -40,15 +42,16 @@ class PropertyDescriptorParser { */ public PropertyDescriptorParser(Class clazz) { this.clazz = clazz; - this.descriptors = getPropertyDesctiptors(clazz); + this.descriptors = getPropertyDescriptors(clazz); } /** * プロパティ名に一致する PropertyDescriptor インスタンスを取得します * @param propertyName プロパティ名 * @return PropertyDescriptor インスタンス + * @throws SystemException プロパティが見つからない場合 */ - public PropertyDescriptor getPropertyDesctiptor(String propertyName) { + public PropertyDescriptor getPropertyDescriptor(String propertyName) { for (PropertyDescriptor descriptor : descriptors) { if (descriptor.getName() .equals(propertyName)) { @@ -56,16 +59,17 @@ public PropertyDescriptor getPropertyDesctiptor(String propertyName) { } } // プロパティが見つからない場合は例外をスロー - throw new RuntimeException("Property '" + propertyName + "' not found in class " + clazz.getName()); + throw new SystemException("Property '" + propertyName + "' not found in class " + clazz.getName()); } /** * クラスのプロパティ記述子を取得します。 * @param clazz クラス * @return プロパティ記述子の配列 + * @throws SystemException クラスのプロパティ記述子の取得に失敗した場合 */ - private PropertyDescriptor[] getPropertyDesctiptors(Class clazz) { - if (null == clazz) { + private PropertyDescriptor[] getPropertyDescriptors(Class clazz) { + if (clazz == null) { throw new IllegalArgumentException("Class must not be null."); } @@ -74,7 +78,7 @@ private PropertyDescriptor[] getPropertyDesctiptors(Class clazz) { return beanInfo.getPropertyDescriptors(); } catch (IntrospectionException e) { - throw new RuntimeException("Failed to get property descriptor for " + clazz.getName() + " class.", e); + throw new SystemException("Failed to get property descriptor for " + clazz.getName() + " class.", e); } }