null Swagger-CodegenでLiferay Headless REST APIを作ってみた(2)

こんにちは。

前回はLiferayのHeadless APIのソースコードの構造を考察しました。今回はその構造を踏まえて、OpenAPIのオフィシャルツールが作成したコードをLiferayに取り込んでみます。

Swagger-Codegen

ひとまず、OpenAPIのオフィシャルコード作成ツールSwagger-Codegenをインストールしましょう。Macの場合、homebrewを利用すれば brew install swagger-codegen で手軽に完成できます。インストール後、swagger-codegen -h を実行し、以下の出力ができればOKです。

$ swagger-codegen -h
commands:
  Command                additional help
    generate             generate
    config-help          config-help
    meta                 meta
    langs                langs
    version              version

目標API仕様

今回はOpenAPIの仕様を試したいので、以下のポリモーフィズムAPIを目標として置きましょう。

  • パスとメソッド:
    • GET /vehicle?id=xxx
  • ポリモーフィズムレスポンス
    • type=sedanの場合、定員数を返却する
{
  id:
  type: sedan
  max_passengers: 5
}
    • type=truckの場合、最大積載量を返却する
{
  id:
  type: truck
  max_load: 1500
}
    • type=sportsの場合、最大スピードを返却する
{
  id:
  type: sports
  max_speed: 200
}

API記述ファイル

OpenAPIのポリモーフィズムレスポンスを利用し、上記仕様のSchema定義は以下になります。

components:
  schemas:
    Vehicle:
      type: object
      discriminator:
        propertyName: type
        mapping:
          Sedan: '#/components/schemas/Sedan'
          Truck: '#/components/schemas/Truck'
          Sports: '#/components/schemas/Sports'
      properties:
        name:
          type: string
        type:
          type: string
          enum:
          - Sedan
          - Truck
          - Sports
    Sedan:
      description: 'a sedan'
      allOf:
      - $ref: '#/components/schemas/Vehicle'
      - type: object
        properties:
          max_passengers:
            type: integer
    Truck:
      description: 'a truck'
      allOf:
      - $ref: '#/components/schemas/Vehicle'
      - type: object
        properties:
          max_load:
            type: integer
    Sports:
      description: 'a sports car'
      allOf:
      - $ref: '#/components/schemas/Vehicle'
      - type: object
        properties:
          max_speed:
            type: integer

フルyaml設定ファイルはappendix Aを参照してください。

Swagger-Codegenでのソースコード作成

Swagger-Codegenの基本的な使い方は以下になります:

$swagger-codegen generate -i <input_file> -l <language> -o <output>

-l <language>でいくつの言語を選択できますが、前回の内容を踏まえて、LiferayのHeadless APIはJAX-RSベースであり、OSGIのJAX-RSホワイトボードはCXFベースで実装されているので、今回はjaxrs-cxfを指定します(Swagger-Codegenがサポートする言語リストはAppendix Bを参考してください)。

$swagger-codegen generate -i vehicle.yaml -l jaxrs-cxf -o vehicle-src

そしで、vehicle-srcの下に作成されたソースコードを確認してみましょう。

  • Schema(モデル)定義(/modelの下)
    • Vehicle:ベースクラス
    • Sedan, SportsとTruck:それぞれはVehicleを継承するサブクラスになる
public class Vehicle   {

  @JsonProperty("name")
  public String getName() {
    return name;
  }

  @JsonProperty("type")
  public String getType() {
    return type.getValue();
  }

  ...
}

@Schema(description="a sedan")
public class Sedan extends Vehicle  {

  ...
  @JsonProperty("max_passengers")
  public Integer getMaxPassengers() {
    return maxPassengers;
  }

  ...
}

@Schema(description="a sports car")
public class Sports extends Vehicle  {

  ...
  @JsonProperty("max_speed")
  public Integer getMaxSpeed() {
    return maxSpeed;
  }

  ...
}

@Schema(description="a truck")
public class Truck extends Vehicle  {

  ...
  @JsonProperty("max_load")
  public Integer getMaxLoad() {
    return maxLoad;
  }

  ...
}
  • ベースAPIインタフェース(/apiの下)
    • /vehicleに、パラメータ=id、戻り値=VehicleクラスのJAX-RS APIが作成される
@Path("/")
public interface DefaultApi  {

    @GET
    @Path("/vehicle")
    @Produces({ "application/json" })
    @Operation(summary = "Get a vehicle", tags={  })
    @ApiResponses(value = { 
        @ApiResponse(
          responseCode = "200", 
          description = "Vehicle Request", 
          content = @Content(
            mediaType = "application/json", 
            schema = @Schema(implementation = Vehicle.class))) })

    public Vehicle vehicleGet(@QueryParam("id") Integer id);
}

Liferayへ移行

それでは、以上のコードをLiferayに移行しましょう。前回の考察により、以下のコンポーネントが必要になります。(*) JAVAパッケージ名はvehicle.headless.rest.*と指定しています。

  • Liferay.Vulcanに登録するJAX-RS Applicationクラス
    • アプリ名をvehicle.appと指定する
    • osgi.jaxrs.extension.selectに、当該アプリをLiferay.Vulcanに紐付ける
    • ホワイトボードのbase-pathにLiferayのホワイトボードベースである/oに対するパスを指定する(今回はvehicle-appに設定する)
    • 以上の設定により、当該APIのベースパスは {hostname}:{port}/o/vehicle-app になる
@Component(
    property = {
        "liferay.jackson=false",
        "osgi.jaxrs.application.base=/vehicle-app",
        "osgi.jaxrs.extension.select=(osgi.jaxrs.name=Liferay.Vulcan)",
        "osgi.jaxrs.name=vehicle.app"
    },
    service = Application.class
)
@Generated("")
public class VehicleApplication extends Application {
}
  • APIインタフェースの変更
    • APIインタフェース名をDefaultApiからVehicleHeadlessRestに変更する
    • buildRESTに合わせてAPIパスをv1.0に指定する
@Path("/v1.0")
public interface VehicleHeadlessRest {

    ...
    public Vehicle vehicleGet(@QueryParam("id") Integer id);

}
  • APIインタフェースの実装
    • osgiに介して上記Applicationに紐付ける
    • サンプルのため、ダミーエンティティを導入する
@Component(
    property = {
        "api.version=v1.0",
        "osgi.jaxrs.application.select=(osgi.jaxrs.name=vehicle.app)",
        "osgi.jaxrs.resource=true"
    },
    scope = ServiceScope.PROTOTYPE,
    service = VehicleHeadlessRest.class
)
public class VehicleHeadlessRestImpl implements VehicleHeadlessRest {

    @Override
    public Vehicle vehicleGet(Integer id) {
        return vehicles.get(id);
    }

    private static final Map<Integer, Vehicle> vehicles = new HashMap<>(); static {
        vehicles.put(1, (new Sedan()).maxPassengers(5).type(TypeEnum.SEDAN).name("a sedan"));
        vehicles.put(2, (new Truck()).maxLoad(10).type(TypeEnum.TRUCK).name("a truck"));
        vehicles.put(3, (new Sports()).maxSpeed(250).type(TypeEnum.SPORTS).name("a sports car"));
    }
}

以上のソースコードをビルドしLiferayにデプロイすると、Headless REST api {hostname}:{port}/o/vehicle-app/v1.0/vehicleが作成できます。早速、試してみましょう。

現時点ではまだLiferay API Explorer UIを利用できないため、まず外部からアクセスするための認証を解除します:

  • 公式手順に従って、Control Panel → Security → Service Access Policy に新規エンティティを作成する
  • サービスクラス:API実装クラス名 vehicle.headless.rest.impl.VehicleHeadlessRestImpl(パッケージ名は適宜変更)
  • メソッド名:実装クラスのメソッド vehicleGet

curlを利用し、上記APIパスにアクセスすると、以下のようにHeadless APIが正しく作成されることを確認できます:

$ curl -X 'GET'   'http://localhost:8080/o/vehicle-app/v1.0/vehicle?id=1'   -H 'accept: application/json'
{
  "max_passengers" : 5,
  "name" : "a sedan",
  "type" : "Sedan"
}

$ curl -X 'GET'   'http://localhost:8080/o/vehicle-app/v1.0/vehicle?id=3'   -H 'accept: application/json'
{
  "max_speed" : 250,
  "name" : "a sports car",
  "type" : "Sports"
}

Liferay API Explorer UIの登録

ここまでの実装で、LiferayのOSGIホワイトボードにREST APIを登録しましたが、作成されたvehicle-appがLiferayのAPI Explorer UIでは表示されていません。前回考察した通り、API Explorerで表示する場合、/vehicle-app/v1.0/openapi.jsonにAPIを作成し、VehicleHeadlessRestインタフェースの実装クラスをLiferay API Explorer UIへ提供する必要があります。そのため:

  • OpenAPIResourceImplクラスを実装するJAX-RSサービスを作成する
  • /openapi.jsonまたは.yamlパスにGETメソッドを実装し、当該メソッドはVehicleHeadlessRestImplクラスを返却する
  • osgi.jaxrs.application.selectにvehicle.appを紐付ける
@Component(
    property = {
        "api.version=v1.0",
        "osgi.jaxrs.application.select=(osgi.jaxrs.name=vehicle.app)",
        "osgi.jaxrs.resource=true"
    },
    service = OpenAPIResourceImpl.class
)
@OpenAPIDefinition(
    info = @Info(description = "Vehicle APP", ...)
@Path("/v1.0")
public class OpenAPIResourceImpl {

    @GET
    @Path("/openapi.{type:json|yaml}")
    @Produces({MediaType.APPLICATION_JSON, "application/yaml"})
    public Response getOpenAPI(@PathParam("type") String type) throws Exception {
    ...
    }

    ...
    private final Set<Class<?>> _resourceClasses = new HashSet<Class<?>>() {
        {
            add(VehicleHeadlessRestImpl.class);
            add(OpenAPIResourceImpl.class);
        }
    };
}

このクラスをデプロイすると、Liferay API Explorerにvehicle-appが表示されます。では、早速試しましょう。

  • Liferay API Explorer UIに登録されたことを確認する

  • id=1(sedan)のサンプルリクエスト

  • id=2(truck)のサンプルリクエスト

 

まとめ

今回は前回の成果を踏まえて、OpenAPIの標準仕様の記述ファイルを用いて、OpenAPIのソースコード生成ツールに作成されたコードをそのままLiferayのHeadless REST API化しました。作成されたAPIは一般的なCXFベースのJAXRSウェブサービスと見られています。

今回は検証のため、buildRESTが作成するAPIの中に存在するLiferayシステムサービスアクセス部分を実装していません。LiferayシステムサービスもAPIメソッドに組み込みたい場合、buildRESTのサンプルまたはテンプレート(portal-tools-rest-builder/resource.ftlportal-tools-rest-builder/resource_factory_impl.ftl)をを参考にすればよいでしょう。

appendix A: yamlファイル

  • vehicle.yaml
components:
  schemas:
    Vehicle:
      type: object
      discriminator:
        propertyName: type
        mapping:
          Sedan: '#/components/schemas/Sedan'
          Truck: '#/components/schemas/Truck'
          Sports: '#/components/schemas/Sports'
      properties:
        name:
          type: string
        type:
          type: string
          enum:
          - Sedan
          - Truck
          - Sports
    Sedan:
      description: 'a sedan'
      allOf:
      - $ref: '#/components/schemas/Vehicle'
      - type: object
        properties:
          max_passengers:
            type: integer
    Truck:
      description: 'a truck'
      allOf:
      - $ref: '#/components/schemas/Vehicle'
      - type: object
        properties:
          max_load:
            type: integer
    Sports:
      description: 'a sports car'
      allOf:
      - $ref: '#/components/schemas/Vehicle'
      - type: object
        properties:
          max_speed:
            type: integer
paths:
  /vehicle:
    get:
      summary: Get a vehicle
      parameters:
        - in: query
          name: id
          schema:
            type: integer
      responses:
        '200':
          description: 'Vehicle Request'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Vehicle'
openapi: 3.0.1
info:
  description: "API for Get a Vehicle"
  license:
        name: "Apache 2.0"
        url: "http://www.apache.org/licenses/LICENSE-2.0.html"
  title: "Get Vehicle"
  version: v1.0

appendix B: Swagger-Codegenの対応言語一覧

aspnetcore
csharp
csharp-dotnet2
go
go-server
dynamic-html
html
html2
java
jaxrs-cxf-client
jaxrs-cxf
inflector
jaxrs-cxf-cdi
jaxrs-spec
jaxrs-jersey
jaxrs-di
jaxrs-resteasy-eap
jaxrs-resteasy
micronaut
spring
nodejs-server
openapi
openapi-yaml
kotlin-client
kotlin-server
php
python
python-flask
r
ruby
scala
scala-akka-http-server
swift3
swift4
swift5
typescript-angular
typescript-axios
typescript-fetch
javascript
関連記事
customize

RANKING
2021.1.08
2020.12.28
2020.12.01
2020.10.30
2020.12.18