JHipster によるエンタープライズアプリケーションの構築(6) Blueprint でコード生成処理をカスタマイズする 2

カバー

JHipster Blueprint の紹介記事で、JHipster のソースコード生成機能をオーバーライドし、プロジェクトにあったソースコードを生成するための Blueprint という仕組みについて紹介しました。

この記事では Blueprint に含まれる Side-by-Side Blueprint, Custom CLI, LocalBlueprint などの機能や、テンプレートの作成、文字列の操作をはじめとした実際のコード生成処理について、使い方や具体例を中心に解説していきます。

この記事では、JHipster Blueprint の紹介記事で定義した用語を使用します。当該用語については、初出のタイミングで記事へのリンクをつけておりますので、ご参照ください。

JHipster8 と Blueprint

2023 年 11 月に Spring Framework6 / Spring Boot3 に対応した JHipster8 がリリースされましたので、この記事では JHipster 8.0.0 を採用しています。

Blueprint の解説に入る前に、JHipster8 で Blueprint を作成するにあたって、注意すべき変更点を紹介します。

サブジェネレータの構成変更

client, entity-client 等のサブジェネレータについて、JHipster8 では構成が変更されました。特に大きな変更について紹介します。

  • フロントエンドの client, entity-client サブジェネレータからフレームワーク別の機能が独立し、サブジェネレータ angular, react, vue が作成されました。
  • entity-client, entity-server, entity-i18n 等のエンティティ単位のサブジェネレータが廃止され、client, server, languages サブジェネレータに統合されました。

Blueprint を作成する際には、このサブジェネレータの構成に従って配置する必要があるため、注意が必要です。最新のサブジェネレータの構成については、公式の Github を参考にしてください。

テンプレートのパス変更

サブジェネレータの構成変更に伴って、テンプレートのファイル構成にも変更が入っています。一例として RESTController のファイルパスを紹介します。

  • 変更前:generators/entity-server/templates/ src/main/java/package/web/rest/EntityResource.java.ejs
  • 変更後:generators/server/templates/entity/ src/main/java/package/web/rest/_EntityClass_Resource.java.ejs

Blueprint では同名のファイルを配置することで、テンプレートを差し替えることが可能なため、ファイル名やパスについては、最新版のサブジェネレータの内容にあわせて対応が必要です。

ジェネレータで実行されるタスクの変更

ジェネレータの generator.mjs で実行されるエンティティを処理するためのタスクとして writingEntitiespostWritingEntities などが追加されています。JHipster7 までは entity-client 等のエンティティを処理するためのジェネレータがありましたが、JHipster8 で廃止されました。エンティティ単位でコードの生成処理を行いたい場合は、writingEntitiespostWritingEntities 上に記述することになります。

これらの変更により、JHipster Blueprint の紹介記事で、エンティティの PDF ファイルを出力する Blueprint を作成しましたが、そのままでは JHipster8 で動作しません。これから紹介する Blueprint の機能と実例の中で、JHipster8 に対応したサンプルも掲載しております。

Blueprint の機能・コード生成処理

Side-by-Side Blueprint

Side-by-Side Blueprint を有効化したBlueprintを作成すると、既存のサブジェネレータの生成処理が動作した上で、Blueprint 側で定義したコード生成処理が動作するようになります。

開発においては JHipster 標準のサブジェネレータの動作結果を活用しないケースは少なく、出力結果をカスタマイズしたり、一部のテンプレートを差し替えたりすることが多いため、基本的には Side-by-Side Blueprint は有効化した状態で Blueprint を作成することになります。

逆に JHipster 標準のサブジェネレータによる出力結果を利用しない場合や、標準にはないサブジェネレータを新しく追加する場合は、Side-by-Side Blueprint を使用せず Blueprint を作成します。テンプレートやコードを生成する処理を全て自作する必要があるので、注意が必要です。

Side-by-Side Blueprint の利用方法

Side-by-Side Blueprint の有効化は、Blueprint プロジェクト作成時にサブジェネレータ単位で選択できます。

? Is client generator a side-by-side blueprint? Yes

後から Side-by-Side Blueprint の有効・無効を変更したい場合は、generator.mjs のコンストラクタで設定できます。

export default class extends BaseApplicationGenerator {
  constructor(args, opts, features) {
    super(args, opts, { ...features, sbsBlueprint: true });
  }

Custom CLI

Custom CLI は Blueprint に対して専用の CLI を作成できます。

Blueprint が my-blueprint の場合は jhipster-my-blueprint のようなコマンドが作成されます。

Custom CLI を使用すると Blueprint に依存関係のあるバージョンの JHipster を利用したり、カスタムジェネレータを指定して実行できます。特に作成によるデメリットもないため、Custom CLI については Blueprint 作成時は有効化しておいて問題ありません。

カスタムジェネレータは clientserver などの JHipster 標準のサブジェネレータをオーバーライドするのではなく、Blueprint 側だけで作成した新しい名称のサブジェネレータです。カスタムジェネレータについては、この記事の後半で個別に紹介します。

Custom CLI の利用方法

Custom CLI は jhipster generate-blueprint コマンドによる Blueprint プロジェクト作成時に選択できます。

? Add a cli? Yes

Custom CLI を有効化すると、Blueprint プロジェクトで npm installnpm link を行った際に、/usr/local/bin/ などにシンボリックリンクが作成され、コマンドが実行できるようになります。

また、カスタムジェネレータを追加し、Custom CLI に含める設定も行えます。以下は csv, report というカスタムジェネレータを追加する場合の例です。

? Comma separated additional sub-generators. csv,report
? Is csv generator a cli command? Yes
? What task do you want do implement at csv generator? writing, writingEntities
? Is report generator a cli command? Yes
? What task do you want do implement at report generator? writing, writingEntities

追加したカスタムジェネレータは以下のようなコマンドで実行でき、それぞれのジェネレータに定義したコード生成処理のみが動作します。

jhipster-my-blueprint csv
jhipster-my-blueprint report

LocalBlueprint

LocalBlueprint は Blueprint を JHipster のプロジェクト配下に作成するための機能です。

LocalBlueprint は JHipster プロジェクト内のフォルダ .blueprint に作成され、jhipster コマンド時に自動的に適用されるため、通常の Blueprint プロジェクトのように、npm リポジトリへの登録や、npm install, npm link の実行等、利用するための準備が不要となります。

LocalBlueprint は開発中の JHipster プロジェクトにおいて、プロジェクト固有の Blueprint を作成したい場合に活用できます。プロジェクト配下のフォルダに Blueprint も格納されるため、プロジェクト全体のソースコードを一元管理できるメリットがあります。

逆にプロジェクト固有ではなく、複数のプロジェクトで使うことを想定した Blueprint は専用の Blueprint プロジェクトを作成することになります。

開発時は LocalBlueprint でプロジェクト固有のコード生成処理を定義し、他のプロジェクトにも活用できそうなものであれば、個別の Blueprint プロジェクトを作成し、移植する等の活用が考えらえます。

LocalBlueprint の利用方法

LocalBlueprint は jhipster generate-blueprint コマンドによる Blueprint 作成時に選択できます。
事前に JHipster プロジェクトを作成し、そのフォルダ配下で実行する必要があります。

? Do you want to generate a local blueprint inside your application? Yes

.blueprint フォルダ配下に、Blueprint 関連ファイルが作成されます。

このフォルダ配下のコード生成処理をカスタマイズした後、jhipster コマンドで反映できます。

テンプレートの置き換え

JHipster オリジナルのジェネレータ内に定義されているテンプレートとなるファイルを、Blueprint で用意したファイルに置き換えできます。

オリジナルのジェネレータに含まれるロゴや favicon 等の画像、画面デザイン、サーバの処理などを差し替えたい場合に、テンプレートの置き換えを活用することになります。

テンプレートを変更してしまうと JHipster のバージョンアップ等で元のテンプレートが大きく変更された場合に、追従が大変になる場合があります。テンプレートを大きく改変するケースはやむを得ませんが、軽微なコード変更の場合は、後述の文字列の操作を活用したほうがよいです。

テンプレート置き換えの利用方法・サンプル

テンプレートの置き換えについては、JHipster オリジナルのジェネレータ内に定義されているテンプレートのファイルパスと同一となるファイルを、Blueprint 側にも配置するだけで利用できます。generator.mjs でコードの生成処理などを作成する必要はありません。

JHipster オリジナルのテンプレートは、各ジェネレータの templates フォルダ配下に存在しています。
npm install した node_modules/generator-jhipster 配下から各ジェネレータのテンプレートのファイルパスを確認して、配置してください。

例えば、ロゴや favicon を置き換える場合は client ジェネレータに以下のファイルを用意します。

  • client/templates/src/main/webapp/favicon.ico
  • client/templates/src/main/webapp/content/images/logo-jhipster.png

また、フッターのファイルを変更し、コピーライトを入れる場合は react ジェネレータに以下のファイルを用意します。

  • react/templates/src/main/webapp/app/shared/layout/footer/footer.tsx.ejs
import './footer.scss';

import React from 'react';
import { Translate } from 'react-jhipster';
import { Col, Row } from 'reactstrap';

const Footer = () => (
<div className="footer page-content">
  <Row>
    <Col md="12">
      <p>
        © 2023 ALPHA SYSTEMS INC.
      </p>
    </Col>
  </Row>
</div>
);

export default Footer;

以下のようにロゴやフッター等が置き換えられます。

テンプレートの追加

JHipster オリジナルのジェネレータ内に定義されていないテンプレートを準備し、Blueprint でコード生成できます。

JHipster オリジナルでは用意されていない機能や画面、画像ファイルなどを追加する場合に活用します。

テンプレート追加の利用方法・サンプル

Blueprint 内のフォルダにテンプレートを配置し、コード生成処理を generator.mjs に記述することで利用できます。

JHipster Blueprint の紹介記事で エンティティの内容を PDF ファイルとしてダウンロードする機能を追加しましたが、そちらではテンプレートの追加を活用しています。

PDF ダウンロード用の RestController を作成するため、server ジェネレータの templates 配下に以下のファイルを用意します。

  • src/main/java/package/web/rest/report/ReportEntityResource.java.ejs
  • src/main/resources/reports/report_entity.jrxml.ejs
コードの詳細(クリックして開く/閉じる
  • ReportEntityResource.java.ejs
package <%= entityAbsolutePackage %>.web.rest.report;

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
    
import <%= entityAbsolutePackage %>.domain.<%= entityClass %>;
import <%= entityAbsolutePackage %>.repository.<%= entityClass %>Repository;
import <%= entityAbsolutePackage %>.web.rest.errors.BadRequestAlertException;

@RestController
@RequestMapping("/api")
public class Report<%= entityClass %>Resource {

    private static final String ENTITY_NAME = "<%= entityInstance %>";

    @Value("${jhipster.clientApp.name}")
    private String applicationName;

    private final <%= entityClass %>Repository <%= entityInstance %>Repository;

    public Report<%= entityClass %>Resource(<%= entityClass %>Repository <%= entityInstance %>Repository ) {
        this.<%= entityInstance %>Repository = <%= entityInstance %>Repository;
    }

    private byte[] exportReportToPdf(Resource resource, Map<String, Object> parameters, List<?> fields)
            throws IOException, JRException {
        try (InputStream in = resource.getInputStream();) {
            return JasperExportManager.exportReportToPdf(
                JasperFillManager.fillReport(JasperCompileManager.compileReport(in), parameters, new JRBeanCollectionDataSource(fields))
            );
        }
    }
    
    @GetMapping("/<%= entityApiUrl %>/report/{id}")
    public ResponseEntity<byte[]> get<%= entityClass %>(@PathVariable <%= primaryKey.type %> id) throws IOException, JRException {
        Optional<<%= entityClass %>> <%= entityInstance %> = <%= entityInstance %>Repository.findById(id);
        if (<%= entityInstance %>.isEmpty()) {
            throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound");
        }
        Resource resource = new ClassPathResource("./reports/report_<%= entityClass %>.jrxml");
        byte[] bytes = exportReportToPdf(resource, new HashMap<String, Object>(), Arrays.asList(<%= entityInstance %>.get()));
        return ResponseEntity.ok().body(bytes);
    }
}
  • report_entity.jrxml.ejs
<?xml version="1.0" encoding="UTF-8"?>
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="emproyee" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="e666857c-a4c1-4758-9020-f97c81327097">
	<queryString>
	</queryString>
<%_ for (const field of fields) {
	const fieldName = field.fieldName; _%>
	<%_ if (field.fieldTypeUUID) { _%>
	<field name="<%= fieldName %>" class="java.util.UUID"/>
	<%_ } else if (field.fieldTypeLong) { _%>
	<field name="<%= fieldName %>" class="java.lang.Long"/>
	<%_ } else if (field.fieldTypeInteger) { _%>
	<field name="<%= fieldName %>" class="java.lang.Integer"/>
	<%_ } else if (field.fieldTypeFloat) { _%>
	<field name="<%= fieldName %>" class="java.lang.Float"/>
	<%_ } else if (field.fieldTypeDouble) { _%>
	<field name="<%= fieldName %>" class="java.lang.Double"/>
	<%_ } else if (field.fieldTypeBigDecimal) { _%>
	<field name="<%= fieldName %>" class="java.math.BigDecimal"/>
	<%_ } else if (field.fieldTypeLocalDate) { _%>
	<field name="<%= fieldName %>" class="java.time.LocalDate"/>    
	<%_ } else if (field.fieldTypeInstant) { _%>
	<field name="<%= fieldName %>" class="java.time.Instant"/>
	<%_ } else if (field.fieldTypeDuration) { _%>
	<field name="<%= fieldName %>" class="java.time.Duration"/>    
	<%_ } else if (field.fieldTypeZonedDateTime) { _%>
	<field name="<%= fieldName %>" class="java.time.ZonedDateTime"/>    
	<%_ } else if (field.fieldTypeBoolean) { _%>
	<field name="<%= fieldName %>" class="java.lang.Boolean"/>
	<%_ } else if (field.fieldTypeString) { _%>
	<field name="<%= fieldName %>" class="java.lang.String"/>
	<%_ } else if (field.fieldIsEnum) { _%>
	<field name="<%= fieldName %>" class="<%= entityAbsolutePackage %>.domain.enumeration.<%= field.javaFieldType %>"/>
	<%_ } _%>
<%_ } _%>
	<pageHeader>
		<band height="30">
		<staticText>
			<reportElement x="0" y="0" width="550" height="30"/>
			<textElement textAlignment="Center" verticalAlignment="Middle">
				<font size="20"/>
			</textElement>
			<text><%= entityClass %></text>
		</staticText>
		</band>
	</pageHeader>
	<detail>
		<band height="750" splitType="Stretch">
<%_ var counter = 0; 
	var height = Math.min( Math.floor( 750 / fields.length ), 60 ); _%>
<%_ for (const field of fields) {
	const fieldName = field.fieldName; _%>
		<textField textAdjust="ScaleFont">
			<reportElement x="0" y="<%= counter * height %>" width="200" height="<%= height %>"/>
			<box>
				<topPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
				<leftPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
				<bottomPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
				<rightPen lineWidth="2.0" lineStyle="Double" lineColor="#000000"/>
			</box>
			<textElement textAlignment="Center" verticalAlignment="Middle">
				<font size="16"/>
			</textElement>
			<textFieldExpression><![CDATA["<%= fieldName %>"]]></textFieldExpression>
		</textField>
		<textField textAdjust="ScaleFont" isBlankWhenNull="true">
			<reportElement x="200" y="<%= counter * height %>" width="350" height="<%= height %>"/>
			<box>
				<topPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
				<bottomPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
				<rightPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/>
			</box>
			<textElement textAlignment="Left" verticalAlignment="Middle">
				<font size="14"/>
			</textElement>
			<textFieldExpression><![CDATA[$F{<%= fieldName %>}]]></textFieldExpression>
		</textField>
	<% counter++; %>
<%_ } _%>  
		</band>
	</detail>
</jasperReport>

追加したテンプレートを元に実際のコード生成を行う場合は、server ジェネレータの generator.mjs に以下のように記述します。

  get [BaseApplicationGenerator.WRITING_ENTITIES]() {
    return this.asWritingEntitiesTaskGroup({
      async writingEntitiesTemplateTask({ entities }) {
        for (const entity of entities.filter(entity => (!entity.skipServer && !entity.builtIn))) {
          await this.writeFiles({
            sections: {
              files: [
                {
                  path: 'src/main/java/',
                  templates: [
                    {
                      file: 'package/web/rest/report/ReportEntityResource.java',
                      renameTo: generator => `${generator.entityAbsoluteFolder}/web/rest/report/Report${generator.entityClass}Resource.java`
                    },
                  ],
                },
                {
                  path: 'src/main/resources/',
                  templates: [
                    {
                      file: 'reports/report_entity.jrxml',
                      renameTo: generator => `reports/report_${generator.entityClass}.jrxml`
                    },
                  ],
                },
              ],
            },
            context: entity,
          });
        }
      }
    });
  }

前回の記事と比較して、JHipster8 となったことでコード生成処理の記述先が get [BaseApplicationGenerator.WRITING_ENTITIES]() になり、for 文によりエンティティ単位で処理する記述に変更となっています。

ファイルの削除

JHipster オリジナルのジェネレータで生成されたファイルやフォルダを削除できます。

開発の際に不要となるファイルを予め削除したり、未使用となる機能を削除するなどの利用が考えられます。

ファイル削除の利用方法・サンプル

ファイルの削除は各ジェネレータの generator.mjsdeleteDestination 等を使用することで利用できます。

ホーム画面に表示されるキャラクター画像を削除する場合は、client ジェネレータの generator.mjs に以下のように記述します。

images 配下の特定のファイル名を含むファイルを削除しています。

  get [BaseApplicationGenerator.POST_WRITING]() {
    return this.asPostWritingTaskGroup({
      async postWritingTemplateTask() {},
      async deleteImages() {
        this.deleteDestination('src/main/webapp/content/images/jhipster_family_member_*');
      },
    });
  }

以下のように指定した名称に合致するファイルが削除されます。

文字列の操作

生成されたコードに対して、文字列の操作を行えます。これはオリジナルのジェネレータの生成結果と Blueprint による生成結果のどちらのコードにも適用できます。

画面上の文言変更、設定ファイルへの項目追加、ソースコード上の import やアノテーションの追加等、文字列の操作で行えることは多岐にわたります。
前述のテンプレートの置き換えではファイルごと差し替える必要があるため、JHipster のバージョンアップによるテンプレートの変更に追従し辛いデメリットがありますが、文字列の操作の場合は元となる文字列に変更がなければ、継続して利用できます。

文字列の操作の利用方法・サンプル

文字列の操作は各ジェネレータの generator.mjseditFile に対象のファイルと文字列を指定することで利用できます。指定した文字列に対して replaceslice 等を使って、必要なコードの挿入や置換を実施できます。

画面で使用するアイコン画像を追加する場合は、react ジェネレータの generator.mjs に以下のように記述します。

icon-loader.tsFont Awesome の PDF アイコンを追加設定しています。

  get [BaseApplicationGenerator.POST_WRITING]() {
    return this.asPostWritingTaskGroup({
      async postWritingTemplateTask() {},
      async editIconLoaders() {
        this.editFile(
          `src/main/webapp/app/config/icon-loader.ts`,
          contents => contents.includes('faFilePdf')
            ? contents : contents.replaceAll(
              `faWrench,`, `faWrench,faFilePdf,`)
        )
      },
    });
  }

追加したアイコン画像を React のコンポーネントで使用する場合は以下のように指定します。

<FontAwesomeIcon icon="file-pdf" /> <span className="d-none d-md-inline" >PDF</span>

JHipster Blueprint の紹介記事で PDF ダウンロードボタンを追加した方法も、文字列の操作を活用しています。

  get [BaseApplicationGenerator.POST_WRITING_ENTITIES]() {
    return this.asPostWritingEntitiesTaskGroup({
      async postWritingEntitiesTemplateTask({ entities }) {
        for (const entity of entities.filter(entity => !entity.skipClient && !entity.builtIn)) {
          const primaryKeyName = entity.primaryKey.name;
          const entityInstance = entity.entityInstance;
          const entityApiUrl = entity.entityApiUrl;
          const entityFolderName = entity.entityFolderName;
          const entityFileName = entity.entityFileName;
          this.editFile(
            `src/main/webapp/app/entities/` + entityFolderName + `/` + entityFileName + `.reducer.ts`,
            contents => contents.includes('downloadEntitiesPdf')
              ? contents : contents.replace(
                `// slice`,
                `export const downloadEntitiesPdf = createAsyncThunk(
                  '${entityInstance}/fetch_entity_pdf',
                  async (id? : number) => {
                    return await axios.get(\`\${apiUrl}/report/\${id ? id : ''}\`, { responseType: 'blob' }).then(response => {
                      const link = document.createElement('a');
                      link.href = URL.createObjectURL(new Blob([response.data]));
                      link.download = "${entityApiUrl}.pdf";
                      link.click();
                      URL.revokeObjectURL(link.href);
                    })
                  },
                  { serializeError: serializeAxiosError }
                );
                // slice`),
          );
          this.editFile(
            `src/main/webapp/app/entities/` + entityFolderName + `/` + entityFileName + `.tsx`,
            contents => contents.includes('downloadEntitiesPdf')
              ? contents : contents.replace(
                `getEntities`,
                `getEntities, downloadEntitiesPdf`
              ).replace(
                `const handleSyncList = () => {`,
                `const handleDownloadPdf = (${primaryKeyName}?: number) => { dispatch(downloadEntitiesPdf(${primaryKeyName})); };
                 const handleSyncList = () => {`
              ).replace(
                `<div className="btn-group flex-btn-group-container">`,
                `<div className="btn-group flex-btn-group-container">
                 <Button color="warning" size="sm" data-cy="entityReportsButton" onClick={() => void handleDownloadPdf(${entityInstance}.${primaryKeyName})}>
                 <FontAwesomeIcon icon="save" /> <span className="d-none d-md-inline" >PDF</span>
                 </Button>`)
          );
        }
      },
    });
  }

前回の記事と比較して JHipster8 となったことでコード生成処理の記述先が generators/react/generator.mjsget [BaseApplicationGenerator.POST_WRITING_ENTITIES]() になり、for 文によりエンティティ単位で処理する記述に変更となっています。

ライブラリの追加

フロントエンド、サーバサイドそれぞれで使用するライブラリを追加できます。

新しい機能を追加する Blueprint を作成する場合に、追加機能で使用するライブラリを build.gradlepackage.json に追加する際に利用します。

ライブラリ追加の利用方法・サンプル

ライブラリの追加は各ジェネレータの generator.mjsaddGradleMavenRepository, addGradleDependency, packageJson.merge 等を使用することで利用できます。

build.gradle に JasperReports の設定を追加するには、server ジェネレータの generator.mjs に以下のように記述します。

  get [BaseApplicationGenerator.POST_WRITING]() {
    return this.asPostWritingTaskGroup({
      async postWritingTemplateTask() {},
      async addGradleJasperreports({ source }) {
        source.addGradleMavenRepository({
          url: 'https://jaspersoft.jfrog.io/jaspersoft/third-party-ce-artifacts/'
        });
        source.addGradleDependency({
          groupId: 'net.sf.jasperreports',
          artifactId: 'jasperreports',
          version: 'latest.release',
          scope: 'implementation'
        });
      }
    });
  }

package.json に exceljs の設定を追加するには、client ジェネレータの generator.mjs に以下のように記述します。

  get [BaseApplicationGenerator.POST_WRITING]() {
    return this.asPostWritingTaskGroup({
      async postWritingTemplateTask() {},
      async mergePackageJson() {
        this.packageJson.merge({
          dependencies: {
            'exceljs': '4.3.0',
          },
        });
      },
    });
  }

カスタムジェネレータの追加

Custom CLI でも紹介したカスタムジェネレータは JHipster 標準のサブジェネレータをオーバーライドするのではなく、Blueprint 側だけで作成した新しい名称のサブジェネレータです。

CSV 出力や帳票出力などのまとまった機能をカスタムジェネレータとして Blueprint を作成しておき、その機能が必要な場合のみ、Custom CLI を使用してプロジェクトに機能を追加できます。

Blueprint を実装する際に JHipster 標準のサブジェネレータにオーバーライドする対象が存在しない場合や、機能を一つのサブジェネレータに集約したい場合、コマンドラインオプション等でサブジェネレータを個別に実行したい場合に、カスタムジェネレータとして実装します。

カスタムジェネレータの利用方法・サンプル

カスタムジェネレータは jhipster generate-blueprint コマンドによる Blueprint プロジェクト作成時に選択できます。

additional sub-generators の設問で、任意のジェネレータ名を指定します。以下は report ジェネレータを追加する例です。

? Comma separated additional sub-generators. report
? Is report generator a cli command? Yes
? What task do you want do implement at report generator? writing, writingEntities

追加したカスタムジェネレータ配下に、今まで紹介した Blueprint の機能を活用し、テンプレートの追加や generator.mjs によるコード生成処理を定義します。

JHipster オリジナルに存在しないジェネレータのため、Side-by-Side Blueprint が有効化できない点に注意が必要です。通常のジェネレータで動作していたコード生成処理が、カスタムジェネレータで動作しないケースもあります。

例えば、build.gradle に JasperReports の設定を追加する処理を行うには、gradle のジェネレータが含まれている必要があります。

以下のように generator.mjsget [BaseApplicationGenerator.COMPOSING]() を定義し、composeWithJHipster(GENERATOR_GRADLE) を追加することで、report ジェネレータ実行時に、gradle ジェネレータも実行され、build.gradle に JasperReports の設定を追加する処理が動作可能になります。

  get [BaseApplicationGenerator.COMPOSING]() {
    return this.asComposingTaskGroup({
      async composingTemplateTask() { },
      async composeWithJHipster() {
        await this.composeWithJHipster(GENERATOR_GRADLE);
      },
    });
  }

あわせてテンプレートの追加文字列の操作で紹介した PDF ダウンロードに関するコード生成処理を、カスタムジェネレータ report 内にまとめて定義します。クライアント側、サーバ側の区別も特に必要ありません。

追加したカスタムジェネレータは以下のようにコマンラインパラメータで指定して実行できます。

jhipster-my-blueprint report --force

実行するとカスタムジェネレータに関連したコードのみを生成できます。複数のカスタムジェネレータをもった Blueprint プロジェクトから、必要な機能のみを適用したい場合に利用します。

    force gradlew
    force .yo-rc.json
    force .jhipster/Employee.json
    force gradlew.bat
    force gradle/wrapper/gradle-wrapper.jar
    force gradle/wrapper/gradle-wrapper.properties
   create src/main/resources/reports/report_Employee.jrxml
    force build.gradle
   create src/main/java/com/mycompany/myapp/web/rest/report/ReportEmployeeResource.java
    force src/main/webapp/app/entities/employee/employee.reducer.ts
    force src/main/webapp/app/entities/employee/employee.tsx

また、app ジェネレータで composeWithJHipster の設定を行うことで、デフォルトでカスタムジェネレータを動作させることもできます。カスタムジェネレータとして実装した機能を、Blueprint の適応時に自動的に反映させたい場合に利用します。

  get [BaseApplicationGenerator.COMPOSING]() {
    return this.asComposingTaskGroup({
      async composeWithJHipster() {
        await this.composeWithJHipster(`jhipster-my-blueprint:report`);
      },
    });
  }

おわりに

本記事では、Blueprint のコード生成機能の使い方や具体例について詳しく解説してきました。ここまでの内容を組み合わせることで、さまざまな機能を簡単に実装できることを実感していただけたのではないでしょうか。

Blueprint で行えることは、この他にもありますので、JHipster 公式の Blueprint で実装されているものも参考にしてみてください。

今回の記事が、JHipster と Blueprint を試してみるきっかけとなれば幸いです。

JHipster は 8.0.0 がリリースされたばかりですが、Blueprint はバージョンアップの影響を受けやすい側面がありますので、新しいバージョンでの検証なども続けていきたいと思います。

JHipster 関連記事

参考文献


TOP
アルファロゴ 株式会社アルファシステムズは、ITサービス事業を展開しています。このブログでは、技術的な取り組みを紹介しています。X(旧Twitter)で更新通知をしています。