3.6. 制約を追加する¶
項目
標準で提供されている制約では不足している場合、制約そのものを追加できます。
例として、日付書式であることを制約として追加してみます。 (正規表現制約を使えば同じことができますが、実装例として参考にしてください)
制約の項目として
- 日付書式 (変数名:format)
を持つものとします。
3.6.1. 制約の実装¶
制約を増やすには、サーバ側とクライアント側の両方の実装が必要です。
- クライアント側
- Vue.js のコンポーネント
- サーバ側
- 制約を表現するクラスをインスタンス化するクラス
- 制約を表現するクラス
- インポート・エクスポート時の設定、処理を行うクラス
- 設定ファイル
- 影響範囲検出のためのクラス
それぞれの実装で共通となる ID が必要です。この ID はシステムで一意とならなければなりません。 未知の制約が追加されることを考慮し、FQDN やモジュールIDなど競合しない文字列を検討してください。
この例では簡単のため sample_restriction を制約のIDとします。
3.6.1.1. クライアント側の実装¶
クライアント側は、Vue.js のコンポーネントとして実装します。 「Vue.js のコンポーネント」の詳細は Vue.js の Web サイトを参照してください。
制約のコンポーネントとして必要なのは以下の項目です。
- id
- 上記「制約のID」を指定します。この文字列でサーバ側とクライアント側を紐づけます。
- label
- 制約の選択肢となるセレクトボックスに表示する名前を定義します。
- sortOrder
- 制約の選択肢となるセレクトボックスに表示するソート順を定義します。
- inject
- 親コンポーネントから注入される関数、プロパティを定義します。
- template
- 画面に表示する HTML のテンプレートを定義します。
- computed
- 入力データ
- computed のゲッター、セッターを定義します。
- セッターでは、このコンポーネントを呼び出している親コンポーネントにデータの変更を伝えます。
- また、制約のデータは id をキーに持つオブジェクトとして定義します。
これらを踏まえて実装します。
src/main/public/im_repository/sample_restriction.js
// // 日付書式制約のコンポーネントを定義します。 // const SampleRestriction = { // 「制約のID」を指定します。この文字列でサーバ側とクライアント側を紐づけます。 id: 'sample_restriction', // 制約の選択肢に表示する文字列 label: '日付書式', // 制約の選択肢のソート番号 sortOrder: 2000, // 親コンポーネントから注入される関数 inject: ['isEditable', 'getRestriction'], // 画面に表示する HTML のテンプレートを定義します。 template: // デフォルトの制約と同じような見た目で制約を表示します。 '<restriction ' + // HTML の id 属性です。 <div id="odd_or_even_restriction"...> のように展開されます。 'id="sample_restriction" ' + // 種類です。 ':type="restrictionLabel" ' + // 制約IDです。 ':restrictionId="restrictionId"> ' + // 制約のプロパティを表示します。 '<restriction-property ' + // 制約IDです。 ':restrictionId="restrictionId"' + // 制約のプロパティのIDです。 <tr id="option"..> のように展開されます。 'id="format"' + // 制約のプロパティのキーです。 'property-key="format">' + // 制約のプロパティのタイトルです。 // '<th slot="tableHeader">日付書式</th>' + // ここからは <td> の内部として展開されます。 '<td slot="propertyCell">' + '<label>書式:</label>' + '<input ' + 'v-if="isEditable()" ' + 'v-model="format" ' + 'name="format" ' + 'type="text">' + '<label v-else>{{ format }}</label>' + '</td>' + '</restriction-property>' + '</restriction>', data() { return { restrictionId: SampleRestriction.id, restrictionLabel: SampleRestriction.label } }, props: { dictionaryItem: { type: Object, required: true } }, computed: { // この制約の項目 format: { get() { // 辞書項目から制約用途を取得、制約用途の中から this.restrictionId の制約を取得、その成約の中から format プロパティを取得する return _.get(this.getRestriction(this.dictionaryItem, this.restrictionId), 'format', '') }, set(value) { this.$emit('merge', { id: this.restrictionId, format: value }) } } } } // // コンポーネントを追加します // if (window.imRepository && window.imRepository.restrictions) { window.imRepository.restrictions.push(SampleRestriction) } else if (window.imRepository) { window.imRepository.restrictions = [ SampleRestriction ] } else { window.imRepository = { restrictions: [SampleRestriction] } }
3.6.1.1.1. 親コンポーネントから渡される情報¶
親コンポーネントから props へ渡されるものは次のものです。 上記例では dictionaryItem しか使用していません。
名前 | 型 | 説明 |
---|---|---|
dictionaryItem | Object | 辞書項目またはエイリアス |
anotherItem | Object | 差分表示時に使用される辞書項目またはエイリアス |
readonly | Boolean | 表形式や差分表示など読み込み専用を示すフラグ |
親コンポーネントから provide される関数は 親コンポーネントから渡される情報 に加えて次のものがあります。
名前 | 引数 | 返り値 | 説明 | |
---|---|---|---|---|
hasDifferencePropert(restrictionId, key) | 指定した制約IDと制約データのキーに該当するデータに 差分があるかどうかを返す関数です。 | |||
{String} restrictionId | | 制約ID | |||
{String} key | 制約データのキー | |||
Boolean | 差がある場合 true を、差がない場合 false を返します。 | |||
getRestriction(item, restrictionId) | 指定した制約IDの制約を取得する関数です。 | |||
{Object} item | 辞書項目またはエイリアス | |||
{String} restrictionId | 制約ID | |||
Object | 制約のオブジェクトを返します。 指定した制約IDの制約が存在しない場合 nullを返します。 |
また、この他に親コンポーネントから provide されるオブジェクトも存在します。内部で使用しています。
名前 | 型 | 説明 |
---|---|---|
dictionaryItem | Object | 辞書項目またはエイリアス |
anotherItem | Object | 差分表示時に使用される辞書項目またはエイリアス |
readonly | Boolean | 表形式や差分表示など読み込み専用を示すフラグ |
eventHub | Object | 削除ボタンをクリックしたときのイベントをやりとりするオブジェクト |
3.6.1.1.2. 親コンポーネントで定義されているコンポーネント¶
3.6.1.1.2.1. restriction コンポーネント¶
このコンポーネントは、標準の制約と同じ見た目を提供するものです。 div タグとして展開されます。また、内部に form, table タグを内包しています。
restriction コンポーネントでは以下のプロパティを要求します。これらの値を指定してください。
名前 | 型 | 説明 |
---|---|---|
id | String | div の id。div の内部に生成される form の id にも使用されます。form の id は、この id に _form をつけたものです。 |
type | String | この制約の種類の表示名を指定します。 |
restrictionId | String | 制約IDを指定します。 |
3.6.1.1.2.2. restriction-property コンポーネント¶
このコンポーネントは、制約のプロパティをテーブルの行として表示するためのものです。 tr タグとして展開されます。また、内部に差分の有無を示すセル、行のヘッダを示すセル、制約のプロパティを表示するセルの3つのセルを内包しています。
restriction コンポーネントでは以下のプロパティを要求します。これらの値を指定してください。
名前 | 型 | 説明 |
---|---|---|
id | String | tr の id。 |
property-key | String | この制約のプロパティのキーを指定します。 |
restrictionId | String | 制約IDを指定します。 |
3.6.1.2. サーバ側の実装¶
サーバ側の実装で必要なのは以下の4つです。
- 制約を表現するクラスをインスタンス化するクラス
- 制約を表現するクラス
- インポート・エクスポート時の設定、処理を行うクラス
- 設定ファイル
- 影響範囲検出のためのクラス
3.6.1.2.1. 制約を表現するクラスをインスタンス化するクラス¶
このクラスの役割は、
- システムに前述のクライアント側のスクリプトパスを知らせること
- 制約用途のインスタンス化の際に、制約のインスタンスを生成すること
です。 jp.co.intra_mart.foundation.repository.metadata.dictionary.restriction.RestrictionFactory を実装したクラスとして実装します。
ScriptPath アノテーションを使い、クライアント側のスクリプトを配置するパスを指定します。
この例では im_repository/sample_restriction.js というファイルを使用します。
@ScriptPath(value = "im_repository/sample_restriction.js")
制約のインスタンスを生成する際、制約用途は制約の情報を制約のコンストラクタに渡します。 上記のクライアン側の例では
return { id: this.restrictionId, format: value }
としました。
クライアント側からサーバ側に渡される情報を、Map<String, Object> として扱います。 逆にサーバ側からクライアント側へ渡す情報は、Restriction として扱います。
src/main/java/sample/restriction/SampleDateFormatRestrictionFactory.java
// クライアント側のスクリプトパスを指定します。 @ScriptPath(value = "im_repository/sample_restriction.js") public class SampleDateFormatRestrictionFactory implements RestrictionFactory { @Override public String getId() { return SampleDateFormatRestriction.ID; } // クライアント側からサーバ側へ渡す情報を生成します。 @Override public Restriction create(final Map<String, Object> restrictionData) throws DictionaryServiceException { final String format = (String) restrictionData.get("format"); return new SampleDateFormatRestriction(format); } // サーバ側からクライアント側へ渡す情報を生成します。 @Override public Map<String, Object> create(final Restriction restriction) throws DictionaryServiceException { final SampleDateFormatRestriction r = (SampleDateFormatRestriction) restriction; final Map<String, Object> map = new HashMap<>(); map.put("id", this.getId()); map.put("format", r.getFormat()); return map; } }
3.6.1.2.2. 制約を表現するクラス¶
このクラスの役割は、Factory から渡されたパラメータを保持し、そのパラメータで制約の確認を行うことです。
src/main/java/sample/restriction/SampleDateFormatRestriction.java
public class SampleDateFormatRestriction extends AbstractRestriction { /** 制約ID */ public static final String ID = "sample_restriction"; private final String format; /** * 空のコンストラクタ。 * ServiceLoader でインスタンス化するのに必須です。 */ public SampleDateFormatRestriction() { this.format = StringUtil.EMPTY_STRING; } /** * コンストラクタ * @param format 日付書式 */ public SampleDateFormatRestriction(final String format) { this.format = format; } @Override public String getId() { return ID; } @Override public String getName() { return "日付書式"; } // value がこの制約に適合するかどうかを確認します。 // 制約は複数個チェックされる可能性があるので、Boolean 等を返さず、適合しないものを ValidateResult に追加する、という方法を採っています。 @Override protected void doValidate(final String name, final Object value, final ValidateResult result) { if (!(value instanceof CharSequence)) { result.getInvalidParams().add(new InvalidParam(name, MetadataMsg.MSG_E_IWP_MESSAGE_REPOSITORY_METADATA_DICTIONARY_RESTRICTION_NOT_COVERED.get())); return; } final String str = value.toString(); final Pattern pattern = Pattern.compile(this.format); final Matcher matcher = pattern.matcher(str); if (matcher.matches()) { result.getInvalidParams().add(new InvalidParam(name, "書式に適合しません")); return; } return; } /** * 日付書式を返します。 * @return 日付書式 */ public String getFormat() { return format; } }
3.6.1.2.3. インポート・エクスポート時の設定、処理を行うクラス¶
インポート、エクスポートで利用されるクラスです。 jp.co.intra_mart.system.repository.metadata.dictionary.import_export.StandardRestrictionSerialize を継承したクラスとして実装します。
3.6.1.2.3.1. エクスポート処理¶
制約の内容は1つのセルに出力されます。項目が複数ある場合は、カンマなどで結合した文字列として出力してください。 今回の例では制約の項目が1つのみで空白入力を許しています。 制約が追加されたことを示すboolean値と、制約の項目を結合した文字列を出力します。
public String serialize(final Restriction restriction) { final SampleDateFormatRestriction sample = (SampleDateFormatRestriction) restriction; final StringBuilder param = new StringBuilder(); // このメソッドが呼び出されるのは、この制約が有効になっているためです。 // ですので出力する有効・無効フラグは true です。 param.append(Boolean.TRUE.toString()); param.append(CONCAT_STRING); param.append(sample.getFormat() == null ? StringUtil.EMPTY_STRING : sample.getFormat()); return param.toString(); }
3.6.1.2.3.2. インポート処理¶
インポート内容から各フィールドの値を取得し、制約を作成してください。 エクスポート処理で出力された内容を解析する必要がある場合は、それらの処理を行ってください。
public Restriction deserialize(final Class<?> element, final Restriction restriction, final ImportDataModel data, final Item refItem) throws DictionaryServiceException { // インポート内容から日付書式のデータを取得します。 final ImportExcelDataModel importData = data.get(SampleDateFormatRestriction.ID); if (importData == null) { return null; } SampleDateFormatRestriction sample = null; if (element == Item.class) { // 辞書項目の場合はインポート内容から制約を作成します。 final String value = importData.getValue(); if (!value.isEmpty()) { final String[] splitValue = value.split(CONCAT_STRING); if (Boolean.valueOf(splitValue[0].trim()) && splitValue.length == 2) { sample = new SampleDateFormatRestriction(splitValue[1].trim()); } else { sample = new SampleDateFormatRestriction(); } } } else if (element == Alias.class) { // エイリアスの場合は参照先辞書項目の内容を利用します。 final RestrictionUsage usage = refItem.getUsages().get(UsageId.of(RestrictionUsage.ID)); sample = usage == null ? null : (SampleDateFormatRestriction) usage.getRestriction(SampleDateFormatRestriction.ID); return sample; } return sample; }
3.6.1.2.3.3. 検証処理¶
validateメソッドはインポートファイルに記載された値を検証します。 検証した結果、処理を続行できない場合はエラーメッセージをセットしてください。 処理は続行できるけれど、何らかの警告を表示したい場合は警告メッセージをセットしてください。
エラーメッセージをセットするには、表示したいエラーメッセージを引数に setErrorMessage を呼び出してください。 警告メッセージをセットするには、表示したい警告メッセージを引数に setWarningMessage を呼び出してください。
// インポート内容から日付書式のデータを取得します。 final ImportExcelDataModel data = importData.get(SampleDateFormatRestriction.ID); if (data == null) { return; } if (!data.getValue().isEmpty()) { if (element == Item.class) { final String[] values = data.getValue().split(CONCAT_STRING, -1); if (PARAM_LENGTH != values.length) { // ワーニングメッセージの追加例 setWarningMessage("要素数が2ではありません。 (Sheet : dictionary, Row : " + data.getRow() + ", Col : " + data.getCol() + ", 要素数 : " + values.length + ")"); return; } if (!values[1].trim().isEmpty()) { try { new SimpleDateFormat(values[1].trim()); } catch (final NullPointerException | IllegalArgumentException e) { // エラーメッセージの追加例 setErrorMessage("書式が不正です。 (Sheet : dictionary, Row : " + data.getRow() + ", Col : " + data.getCol() + ", 書式 : " + values[1].trim() + ")"); } } } }
エクスポート処理、インポート処理、検証処理をまとめます。
src/main/java/sample/restriction/SampleDateFormatRestrictionSerializer.java
public class SampleDateFormatRestrictionSerializer extends StandardRestrictionSerializer { // インポートファイルのパラメータ数 static final int PARAM_LENGTH = 2; /** * 制約タイプを返却します。 制約のclassを指定してください。 */ @SuppressWarnings("unchecked") @Override public Class<Restriction> getSupportType() { return (Class<Restriction>) (Object) SampleDateFormatRestriction.class; } /** * エクスポート時の処理 制約の内容は1セルに出力されます。 項目が複数ある場合は項目を連結して出力してください。 * @param restriction 制約 */ @Override public String serialize(final Restriction restriction) { final SampleDateFormatRestriction sample = (SampleDateFormatRestriction) restriction; final StringBuilder param = new StringBuilder(); param.append(Boolean.TRUE.toString()); param.append(CONCAT_STRING); param.append(sample.getFormat() == null ? StringUtil.EMPTY_STRING : sample.getFormat()); return param.toString(); } /** * インポート時の処理 * @param element 辞書項目またはエイリアス * @param restriction 制約 * @param data インポート内容 * @param refItem 参照先辞書項目 */ @Override public Restriction deserialize(final Class<?> element, final Restriction restriction, final ImportDataModel data, final Item refItem) throws DictionaryServiceException { // インポート内容から日付書式のデータを取得します。 final ImportExcelDataModel importData = data.get(SampleDateFormatRestriction.ID); if (importData == null) { return null; } SampleDateFormatRestriction sample = null; if (element == Item.class) { // 辞書項目の場合はインポート内容から制約を作成します。 final String value = importData.getValue(); if (!value.isEmpty()) { final String[] splitValue = value.split(CONCAT_STRING); if (Boolean.valueOf(splitValue[0].trim()) && splitValue.length == 2) { sample = new SampleDateFormatRestriction(splitValue[1].trim()); } else { sample = new SampleDateFormatRestriction(); } } } else if (element == Alias.class) { // エイリアスの場合は参照先辞書項目の内容を利用します。 final RestrictionUsage usage = refItem.getUsages().get(UsageId.of(RestrictionUsage.ID)); sample = usage == null ? null : (SampleDateFormatRestriction) usage.getRestriction(SampleDateFormatRestriction.ID); return sample; } return sample; } /** * インポート時のバリデーションを行います。 */ @Override public void validate(final Class<?> element, final ImportDataModel importData) { // インポート内容から日付書式のデータを取得します。 final ImportExcelDataModel data = importData.get(SampleDateFormatRestriction.ID); if (data == null) { return; } if (!data.getValue().isEmpty()) { if (element == Item.class) { final String[] values = data.getValue().split(CONCAT_STRING, -1); if (PARAM_LENGTH != values.length) { // ワーニングメッセージの追加例 setWarningMessage("要素数が2ではありません。 (Sheet : dictionary, Row : " + data.getRow() + ", Col : " + data.getCol() + ", 要素数 : " + values.length + ")"); return; } if (!values[1].trim().isEmpty()) { try { new SimpleDateFormat(values[1].trim()); } catch (final NullPointerException | IllegalArgumentException e) { // エラーメッセージの追加例 setErrorMessage("書式が不正です。 (Sheet : dictionary, Row : " + data.getRow() + ", Col : " + data.getCol() + ", 書式 : " + values[1].trim() + ")"); } } } } } }
3.6.1.2.4. 実装したクラスをシステムに読み込ませるための設定ファイルの追加¶
次に、実装したクラスをシステムに読み込ませるための設定ファイルを追加します。 設定ファイルは java.util.ServiceLoader で読み込まれます。
jp.co.intra_mart.foundation.repository.metadata.dictionary.restriction.RestrictionFactory
sample.restriction.SampleDateFormatRestrictionFactoryjp.co.intra_mart.foundation.repository.metadata.dictionary.restriction.Restriction
sample.restriction.SampleDateFormatRestrictionjp.co.intra_mart.system.repository.metadata.dictionary.import_export.restriction.RestrictionSerializer
sample.restriction.SampleDateFormatRestrictionSerializer
3.6.1.2.5. インポート・エクスポートで使用する設定ファイル¶
下記の例は sample_restriction の入出力設定です。
src/main/conf/im-repository-dictionary-config-type/sample-restriction.xml
<?xml version="1.0" encoding="UTF-8"?> <im-repository-dictionary-config-type xmlns="http://www.intra-mart.jp/repository/im-repository-dictionary-config-type" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.intra-mart.jp/repository/im-repository-dictionary-config-type im-repository-dictionary-config-type.xsd "> <sheetConfig kind="dictionary"> <excelConfig kind="restriction_usage" usage="child"> <columnData> <key>sample_restriction</key> <index>49000</index> <validationAddress></validationAddress> <editDisable>Alias</editDisable> <line no="1"> <name></name> <color>LIGHT_TURQUOISE</color> <borderTop>THIN</borderTop> <borderBottom>THIN</borderBottom> <borderLeft></borderLeft> <borderRight>THIN</borderRight> </line> <line no="2"> <name>サンプル</name> <color>LIGHT_TURQUOISE</color> <borderTop>THIN</borderTop> <borderBottom>THIN</borderBottom> <borderLeft>THIN</borderLeft> <borderRight>THIN</borderRight> </line> </columnData> </excelConfig> </sheetConfig > </im-repository-dictionary-config-type>
3.6.1.2.5.1. タグの説明¶
- sheetConfigタグ
Excel ファイルのシートを表現します。 kind属性で出力するシート名を指定します。 指定できる値は category または dictionary のいずれかです。 今回の例ではdictionaryシートに出力したいので、dictionary を指定します。
<sheetConfig kind="dictionary">
- excelConfigタグ
kind 属性を指定することで、列のグループを表現します。 kind属性 restriction_usage は制約用途を表す列のグループです。 今回の例では、追加したsample_restriction を出力するための設定を追加します。
<excelConfig kind="restriction_usage" usage="child">
- columnDataタグ
- Excel に対する出力設定を行うタグの親タグです。 属性情報はありません。
- keyタグ
追加した制約のIDを設定します。
<key>sample_restriction</key>
- indexタグ
出力位置を示す値です。 excelConfigタグ内での位置を示す値です。
<index>49000</index>
- validationAddressタグ
Excel での入力規則を示す値です。 Excel 上で利用する値を設定してください。
<validationAddress>inputlist!$B$1:$B$3</validationAddress>
入力規則が必要ない場合は値を空にしてください。
<validationAddress></validationAddress>
- editDisableタグ
Excel での編集不可を示す値です。 編集不可の場合はAliasを指定してください。編集可能な場合は、空のタグを指定するか、タグ自体を書かないでください。
<editDisable>Alias</editDisable>
- lineタグ
Excel でのヘッダ行の出力位置を指定します。ヘッダ行は2行で構成されています。 1行目に出力したい場合は 1 を、2行目に出力したい場合は 2 を指定してください。
<line no="1">
- nameタグ
カラムの名称を指定してください。
<name>サンプル</name>
多言語化が必要な場合は、多言語対応 を参考にメッセージプロパティを定義した上でメッセージコードを指定してください。
<name>I18N.MESSAGE.EXAMPLE</name>
- colorタグ
セルの背景色を設定できます。 指定できる値は org.apache.poi_v3_9.ss.usermodel.IndexedColors の値です。
<color>LIGHT_TURQUOISE</color>
背景色が必要ない場合は値を空にしてください。
<color></color>
- borderTopタグ、borderBottomタグ、borderLeftタグ、borderRightタグ
セルの罫線を設定できます。 指定できる値は org.apache.poi_v3_9.ss.usermodel.CellStyle の値です。
<borderTop>THIN</borderTop> <borderBottom>THIN</borderBottom> <borderLeft>THIN</borderLeft> <borderRight>THIN</borderRight>
罫線が必要ない場合は値を空にしてください。
<borderTop></borderTop> <borderBottom></borderBottom> <borderLeft></borderLeft> <borderRight></borderRight>
これらの実装を行い、モジュールのビルドや war ファイルのデプロイを行うと、次のような画面が現れ、制約が追加されていることがわかります。