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)を利用するため、記述ファイルに制限を掛けています。例:

Note: The tags attribute here is important. The value matches the component type that the path either works on or returns. 

また、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で実行)、その結果を考察します。

@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画面に表示する
@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に介して上記アプリケーションに紐付ける

なので、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
関連記事
customize

RANKING
2021.1.08
2020.12.28
2020.12.01
2020.10.30
2020.12.18