Swagger-CodegenでLiferay Headless REST APIを作ってみた(1) - Swagger-CodegenでLiferay Headless REST APIを作ってみた(1) - aegif Labo Blog Liferay
null Swagger-CodegenでLiferay Headless REST APIを作ってみた(1)
こんにちは。
みなさまのご存知通り、Liferayは7.3からOpenAPI仕様に準ずるHeadless REST API機能を提供しています。そして、blade/LiferayIDE/DeveloperStudioのbuildRESTコマンドを利用すれば、OpenAPI仕様のapi定義ファイル(rest-openapi.yaml)からソースコードテンプレートを作成できます。具体的な手順は公式ブログをご参考ください。
OpenAPIとLiferay buildRESTとは?
もともと、OpenAPIはウェブAPIの記述フォーマットであります。OpenAPIスペックのjsonまたはyamlフォーマットの記述ファイルは、目標APIのパスとレスポンスの構造を含んています。簡単な例として、以下のyamlファイルは、/vehicleにGETメソッドを用いて{name: 'a sedan', type: 'Sedan'}
のようなレスポンスを取得できるAPIを記述しています。
components:
schemas:
Vehicle:
type: object
properties:
name:
type: string
type:
type: string
paths:
/vehicle:
get:
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Vehicle'
ですが、記述だけではそのAPIの実装はできません。API実装の自動化をするために、記述ファイルからソースコードを作成できるツールが開発されています。Swagger-Codegen、buildRESTはそれぞれOpenAPI公式とLiferayのソースコード生成ツールであります。Liferay公式例はbladeツールのbuildRESTコマンドを利用しています。
ただし、buildRESTは独自のyaml解析ツール(YAMLUtil.java)を利用するため、記述ファイルに制限を掛けています。例:
また、buildRESTは一部のOpenAPI仕様を実装できません。例えば、Liferay仕様の記述ファイルのSchemaがポリモーフィズム構造のある場合、buildRESTは正しくSchemaのモデルクラスを作成できません:
- 以下のポリモーフィズムSchemaを作成する
components:
schemas:
Vehicle:
type: object
discriminator:
propertyName: type
mapping:
Sedan: '#/components/schemas/Sedan'
Truck: '#/components/schemas/Truck'
Sports: '#/components/schemas/Sports'
Sedan:
description: 'a sedan'
allOf:
- $ref: '#/components/schemas/Vehicle'
- type: object
properties:
...
- 上記記述ファイルに基づいてLiferayのbuildRESTを実行すると、作成されたSedanクラスの中身は以下になる:
- VehicleはSedanクラスの属性になる
- ポリモーフィズムはSedan extends Vehicleを要求しているため実装できない
@JsonFilter("Liferay.Vulcan")
@XmlRootElement(name = "Sedan")
public class Sedan implements Serializable {
...
@Schema
public Integer getMax_passengers() {
return max_passengers;
}
...
@Schema
@Valid
public Vehicle getVehicle() {
return vehicle;
}
...
@JsonProperty(access = JsonProperty.Access.READ_WRITE)
protected Vehicle vehicle;
...
}
もともとOpenAPI記述ファイルの目的は、異なる実装言語でも同じファイルを利用できることなのに、buildRESTを利用すると一般性は失われてしまいます。そのため、今回はOpenAPIのオフィシャルツールであるSwagger-Codegenを利用してJavaのソースコードを作成し、当該ソースコードをLiferayに組み込む形で、Liferay Headless APIを作ってみたいと思います。
Liferay Headless REST APIの構造
OpenAPIのコードをLiferayに利用するために、まずはLiferayのbuildRESTによって作成されたプロジェクトを考察しましょう。今回はREST APIに注目したいため、公式ブログのサンプルのコンフィグレーションファイルrest-config.yamlに以下の設定を追加します
generateBatch: false
generateGraphQL: false
それでは、公式ブログのサンプルrest-openapi.yaml(フルファイルは本記事の最後を参考にしてください)を利用し、buildRESTを実行し(公式ブログが利用しているプロジェクトはbladeに作成されたものなので./gradlew buildREST
で実行)、その結果を考察します。
- ..../jaxrs/application/HeadlessVitaminsApplication.java
- OSGIのJAX-RSホワイトボードスペックにお馴染みの方はすでにお気づきかもしれませんが、Vitamins APIの本体は、Liferay.VulcanというJAX-RSホワイトボードアプリケーションに紐づいてるJAX-RSサービスです
- ホワイトボードのルートからのコンテキストパスは/headless-vitaminsである(すなわち/o/headless-vitaminsになる)
- Schema毎にAPIメソッドを実装するため、Applicationの中にはメソッドが入っていない
- OSGIのJAX-RSホワイトボードスペックにお馴染みの方はすでにお気づきかもしれませんが、Vitamins APIの本体は、Liferay.VulcanというJAX-RSホワイトボードアプリケーションに紐づいてるJAX-RSサービスです
@Component(
property = {
"liferay.jackson=false",
"osgi.jaxrs.application.base=/headless-vitamins",
"osgi.jaxrs.extension.select=(osgi.jaxrs.name=Liferay.Vulcan)",
"osgi.jaxrs.name=dnebinger.Headless.Vitamins"
},
service = Application.class
)
@Generated("")
public class HeadlessVitaminsApplication extends Application {
}
- .../resource/v1_0/BaseVitaminResourceImpl.java
- OpenAPIのSchema毎に(/vitamins: GET, POST)の基本実装が入っている
- javax.ws.rs.ドメインのアノテーションがJAX-RS Webサービスと同等にする役割を果たす
- io.swagger.v3.oas.ドメインからOpenAPI用情報提供アノテーションをインポートしている
@javax.ws.rs.Path("/v1.0")
public abstract class BaseVitaminResourceImpl implements VitaminResource {
@io.swagger.v3.oas.annotations.Operation(description = "")
@io.swagger.v3.oas.annotations.Parameters(value = { ... })
@io.swagger.v3.oas.annotations.tags.Tags(value = { ... })
...
@Override
public Page<Vitamin> getVitaminsPage(...)
throws Exception {
....
}
...
}
- .../resource/v1_0/VitaminResourceImpl.java
- 上記ベース実装を拡張し、OSGIコンポーネント化をする
- src/main/resources/OSGI-INF/liferay/rest/v1_0/の下のSchema毎のOSGI Componentアノテーションの属性ファイルを読み取る
osgi.jaxrs.application.select=(osgi.jaxrs.name=dnebinger.Headless.Vitamins)
により、当該OSGIコンポーネントは上記JAX-RSサービスに紐付けられる
@Component(
properties = "OSGI-INF/liferay/rest/v1_0/vitamin.properties",
scope = ServiceScope.PROTOTYPE, service = VitaminResource.class
)
public class VitaminResourceImpl extends BaseVitaminResourceImpl {
}
ここまでご覧いただいたコードによって、Vitamin APIはすでにdnebinger.Headless.VitaminsというJAX-RSアプリケーションとして、http://{host}:{port}/o/headless-vitamins/v1.0/vitaminsに登録されます。ただし、実はこれだけではLiferayのAPI Explorer UIは当該APIを認識できません。API Explorer UIに認識させるため、以下のコードも自動生成されています。
- .../resource/v1_0/OpenAPIResourceImpl.java
- src/main/resources/OSGI-INF/liferay/rest/v1_0/の下のopenapi.propertiesからOSGI Componentアノテーションの属性ファイルを読み取る
osgi.jaxrs.application.select=(osgi.jaxrs.name=dnebinger.Headless.Vitamins)
により、当該OSGIコンポーネントは上記JAX-RSサービスに紐付けられる- その結果、当クラスは/o/headless-vitamin/v1.0/openapi.jsonというAPIを追加した
- 当該APIが呼ばれる場合、自身とSchema毎のAPI実装クラスをまとめてリターンする
- LiferayのAPI Explorer UI(/o/api)でheadless-vitaminを選択する際、headless-discovery/APIGUI.jsが上記APIを呼び出し、Schema毎に定義されたAPIの情報を取り出し、API Explorer UI画面に表示する
- src/main/resources/OSGI-INF/liferay/rest/v1_0/の下のopenapi.propertiesからOSGI Componentアノテーションの属性ファイルを読み取る
@Component(
properties = "OSGI-INF/liferay/rest/v1_0/openapi.properties",
service = OpenAPIResourceImpl.class
)
@OpenAPIDefinition(...)
@Path("/v1.0")
public class OpenAPIResourceImpl {
@GET
@Path("/openapi.{type:json|yaml}")
public Response getOpenAPI(@PathParam("type") String type)
throws Exception {
...
}
...
private final Set<Class<?>> _resourceClasses = new HashSet<Class<?>>() {
{
add(QueryUserInfoImpl.class);
add(OpenAPIResourceImpl.class);
}
};
}
その他
LiferayのbuildRESTの成果物のAPIインタフェースに多くのメソッドが含まれています。
- APIの中でもLiferayのシステムサービスまたはHttpContextを利用できる
public interface VitaminResource {
...
public void setContextAcceptLanguage(AcceptLanguage contextAcceptLanguage);
public void setContextCompany(com.liferay.portal.kernel.model.Company contextCompany);
public void setContextHttpServletRequest();
...
public void setGroupLocalService(GroupLocalService groupLocalService);
public void setResourceActionLocalService(ResourceActionLocalService resourceActionLocalService);
public void setResourcePermissionLocalService(ResourcePermissionLocalService resourcePermissionLocalService);
public void setRoleLocalService(RoleLocalService roleLocalService);
...
}
まとめ
ここまでの考察をまとめると、LiferayのHeadless APIの構造を理解できます:
- rest-config.yamlのbaseURLに指定されたパス(上例:/headless-vitamins)に、OGSIのJAX-RSアプリケーションを登録し(上例:HeadlessVitaminsApplication.java)、Liferay.Vulcanに紐付ける
- OpenAPI定義yamlファイルのSchema毎にAPIメソッドを実装し、osgi.jaxrs.application.selectに介して上記アプリケーションに紐付ける
- OpenAPIResourceサービスを作成し、osgi.jaxrs.application.selectに介して上記アプリケーションに紐付ける
- buildRESTのテンプレートを参考して/openapi.json APIを実装し、上記Schemaの実装クラスをリターンする
なので、OpenAPIの公式コード作成ツールによって作成されたソースコードを上記構造に従って改造すれば、LiferayのHeadless APIに登録できます。この部分は次回の記事にまとめます。
appendix: 公式サンプルのrest-openapi.yamlファイル
components:
schemas:
Vitamin:
description: Contains all of the data for a single vitamin or mineral.
properties:
name:
description: The vitamin or mineral name.
type: string
id:
description: The vitamin or mineral internal ID.
type: string
chemicalNames:
description: The chemical names of the vitamin or mineral if it has some.
items:
type: string
type: array
properties:
description: The chemical properties of the vitamin or mineral if it has some.
items:
type: string
type: array
group:
description: The group the vitamin or mineral belongs to, i.e. the B group or A group.
type: string
description:
description: The description of the vitamin or mineral.
type: string
articleId:
description: A journal articleId if there is a web content article for this vitamin.
type: string
type:
description: The type of the vitamin or mineral.
enum: [Vitamin, Mineral, Other]
type: string
attributes:
description: Health properties attributed to the vitamin or mineral.
items:
type: string
type: array
risks:
description: Risks associated with the vitamin or mineral.
items:
type: string
type: array
symptoms:
description: Symptoms associated with the vitamin or mineral deficiency.
items:
type: string
type: array
type: object
paths:
"/vitamins":
get:
tags: ["Vitamin"]
description: Retrieves the list of vitamins and minerals. Results can be paginated, filtered, searched, and sorted.
parameters:
- in: query
name: filter
schema:
type: string
- in: query
name: page
schema:
type: integer
- in: query
name: pageSize
schema:
type: integer
- in: query
name: search
schema:
type: string
- in: query
name: sort
schema:
type: string
responses:
200:
description: ""
content:
application/json:
schema:
items:
$ref: "#/components/schemas/Vitamin"
type: array
application/xml:
schema:
items:
$ref: "#/components/schemas/Vitamin"
type: array
post:
tags: ["Vitamin"]
description: Create a new vitamin/mineral.
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
responses:
200:
description: ""
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
openapi: 3.0.1
info:
description: "API for accessing Vitamin details."
license:
name: "Apache 2.0"
url: "http://www.apache.org/licenses/LICENSE-2.0.html"
title: "Something title"
version: v1.0