Automation Anywhere A2019 の共通開発部品であるパッケージを作成する #2～SDKサンプルを変更してオリジナルパッケージを構築！

Automation Anywhere A2019では、「パッケージ」という共通開発部品を作成、配布することでBot作成を効率化することができます。

先日オンラインで開催された[Automation Anywhere社主催Developer Meetup Vol.2](https://rpasouken.com/event-2/webinar/2488/)でも解説されていた内容を参考にしながら概要をまとめてみました。Javaのビルド環境やSDK関連ファイルのダウンロードとセットアップは、前回の記事『[Automation Anywhere A2019 の共通開発部品であるパッケージを作成する #1～まずはSDKサンプルをそのままビルドして使う](https://qiita.com/RPAbot/items/3ea4216f1b94f76df0b3)』を参照してください。この記事では、これらのセットアップは完了してビルドもできる環境になっていることを前提として進めます。

この記事ではオリジナルアクションとして、文字列の中の半角文字を全角文字に変換するアクションを作成します。


**「Automation Anywhere A2019 の共通開発部品であるパッケージを作成する」シリーズの記事**

* [#1～まずはSDKサンプルをそのままビルドして使う](https://qiita.com/RPAbot/items/3ea4216f1b94f76df0b3)
* **#2～SDKサンプルを変更してオリジナルパッケージを構築！** (この記事)
* #3～作成したBotやパッケージをBot Storeにアップロードしよう (今後の予定)

# SDKサンプルのフォルダーの構造
IDEでSDKサンプルのフォルダーを開くと、以下のような構造をしています。「src」のフォルダーの下がソースコードの本体で、後は共通ライブラリ (Bot Runtime Framework)、ドキュメント、設定ファイルなどから構成されています。

(それぞれの項目のアイコンの意味はIntelliJ IDEAの製品ドキュメント『[アイコン参照](https://pleiades.io/help/idea/symbols.html)』を見てください)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479715/0f550dca-d74b-8d45-574d-89cd08d2a5e7.png)

srcの下をより詳しく見てみると、以下のような構造になっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479715/4bae6f0a-cff2-8d6a-67e5-a6f3380622f9.png)

ちなみに、実際にファイルが置いてある場所に新しくファイルを作成したり、ファイルを削除したりといった操作を行うと、IDE側のツリー構造にも自動的に反映されます。

# アクションパッケージを作るためのハウツーガイドの場所

製品ドキュメントの以下の場所にいろいろあります。最新情報は英語で提供されるため、なるべく英語で見ることをお勧めします。

**[ハウツーガイド](https://docs.automationanywhere.com/bundle/enterprise-v2019/page/enterprise-cloud/topics/developer/cloud-how-to-examples.html?_LANG=jpjp)**

* [アクションから値を返す](https://docs.automationanywhere.com/bundle/enterprise-v2019/page/enterprise-cloud/topics/developer/cloud-return-value-from-action.html?_LANG=jpjp)
* [アクションをプロパティとして公開する](https://docs.automationanywhere.com/bundle/enterprise-v2019/page/enterprise-cloud/topics/developer/cloud-expose-action-property.html?_LANG=jpjp)
* [If 条件に条件を追加する](https://docs.automationanywhere.com/bundle/enterprise-v2019/page/enterprise-cloud/topics/developer/cloud-add-condition-custom-package.html?_LANG=jpjp)
* [ループ アクションに反復子を追加する](https://docs.automationanywhere.com/bundle/enterprise-v2019/page/enterprise-cloud/topics/developer/cloud-add-iterator-custom-package.html?_LANG=jpjp)
* [bot_launcher.log ファイルにデバッグ ログを追加する](https://docs.automationanywhere.com/bundle/enterprise-v2019/page/enterprise-cloud/topics/developer/cloud-add-debug-log-custom-packages.html?_LANG=jpjp)
* [カスタム パッケージのセッションの処理](https://docs.automationanywhere.com/bundle/enterprise-v2019/page/enterprise-cloud/topics/developer/cloud-handle-session-custom-package.html?_LANG=jpjp)

これらの内容はサンプルソースコードの中に実装されているので、基本的なものは後で見ていきましょう。

# アノテーション (注釈)とは
Javaでコードを書く際にアノテーションという概念が出てきます。これは、Javaを書くときに大切な情報をコメントとして入れ込んでおくことができるもので、**Javaのコード上では”@(アットマーク)”からはじまり**、コードでは表現しきれない情報を、補足としてつけ加えられます。実際にコードを書くところにルールを書いておくことで、ミスを避ける、コンパイラの警告メッセージを出なくする、プログラムを実行する環境に合わせて、動作を変える、など様々な機能があります。ここでは詳細は延べませんので、Javaのアノテーションに詳しくない方で詳細を知りたい場合は『[Javaの「アノテーション」とは？使い方・作り方を解説](https://ec-orange.jp/ec-media/?p=17791)』などより詳しいドキュメントを参照してください。

アノテーションは自作することもでき、アクションパッケージを作成するSDKでも自前で定義されています。これらをおまじないのように所定の場所に記載します。(com.automationanywhere.commandsdk.annotationsで定義されています)

パッケージを開発するにあたり、以下のようなカスタムアノテーションを利用します。結構数が多く、リファレンスもありますが、サンプルコードを改変していくことで必要なものだけ使い方を覚えておけばよいでしょう。

**[アノテーション](https://docs.automationanywhere.com/bundle/enterprise-v2019/page/enterprise-cloud/topics/developer/cloud-annotations.html)**

* [Creation and functionアノテーション](https://docs.automationanywhere.com/bundle/enterprise-v2019/page/enterprise-cloud/topics/developer/cloud-creation-and-function-annotations.html): `@BotCommand`, `@CommandPkg`, `@ConditionTest`, `@Execute`, `@GlobalSessionContext`, `@HasNext`, `@Idx`, `@Idx.Option`, `@Inject`, `@Next`, `@Pkg`
* [Validation アノテーション](https://docs.automationanywhere.com/bundle/enterprise-v2019/page/enterprise-cloud/topics/developer/cloud-validation-annotations.html):`@CodeType`, `@CredentialOnly`, `@Equals`, `@FileExtension`, `@GreaterThan`, `@GreaterThanEqualTo`, `@LessThan`, `@LessThanEqualTo`, `@LocalFile`, `@MatchesRegex`, `@NotEmpty`, `@NotEquals`, `@NotMatchesRegex`, `@NumberInteger`, `@RepositoryFile`, `@VariableNotPackage`, `@VariablePackage`, `@VariableSubType`, `@VariableType`, `@VariableUserDefined`

パッケージ開発におけるカスタムアノテーションには「Creation and function」と「Validation」の2種類あります。前者はパッケージの中のアクションの詳細 (名前、引数など)を定義するもので、Validationはアクションの詳細でインプットされたデータの検証に使われます。


# サンプルソースコードを改変してみよう
それでは、サンプルソースを変更していくことで、パッケージがどのように変わるかを少しずつ見ていくことにしましょう！

### 1. 日本語のUI表示用リソースを追加してみよう
まず、日本語でUIを作るのに必要な処理を行ってみましょう。Localeのフォルダーの中に、以下のようなen_US.jsonというファイルがあり、この中に英語のテキストが入っています。

```json:en_US.json
{
	"label": "A2019DemoPackage",
	"description": "Provides actions for A2019DemoPackage operations.",
	"Concatenate.label": "Concatenate",
	"Concatenate.description": "Concatenates two strings",
	"Concatenate.node_label": "{{firstString}} with {{secondString}}",
	"Concatenate.return_label": "Assign the output to variable",
	"Concatenate.firstString.label": "First string",
	"Concatenate.secondString.label": "Second string",
	"Uppercase.label": "Uppercase",
	"Uppercase.description": "Uppercase",
	"Uppercase.node_label": "{{caseType}} of {{sourceString}}",
	"Uppercase.return_label": "Assign the output to variable",
	"Uppercase.sourceString.label": "Source string",
	"Uppercase.caseType.label": "Provide which part of string to upper case",
	"Uppercase.caseType.2.1.label": "Make whole string uppercase",
	"Uppercase.caseType.2.1.node_label": "all",
	"Uppercase.caseType.2.2.label": "Make only first character of the string uppercase",
	"Uppercase.caseType.2.2.node_label": "first character",
	"CredentialTypeDemo.label": "Credential Demo",
	"CredentialTypeDemo.description": "Sample action showing how crendtials are used.",
	"CredentialTypeDemo.credentials.label": "Provide the credentials for the user",
	"SessionDemo.label": "Sessions Demo",
	"SessionDemo.description": "Sample action showing how sessions are used.",
	"SessionDemo.name.label": "Please provide the session name",
	"TextTypeDemo.label": "Text Demo",
	"TextTypeDemo.description": "Sample action showing how to display a textbox and accept value from it",
	"TextTypeDemo.firstName.label": "Please enter the first name",
	"TextTypeDemo.lastName.label": "Please enter the last name",
	"SelectTypeDemo.label": "Select Demo",
	"SelectTypeDemo.description": "This example shows how to create a dropdown with predefined values",
	"SelectTypeDemo.region.label": "Select an appropriate region",
	"SelectTypeDemo.region.1.1.label": "US East",
	"SelectTypeDemo.region.1.2.label": "US West",
	"SelectTypeDemo.region.1.3.label": "US central",
	"NumberTypeDemo.label": "Number Demo",
	"NumberTypeDemo.description": "Sample action showing how to display a textbox with numbers and accept value from it",
	"NumberTypeDemo.first.label": "Please enter a number",
	"NumberTypeDemo.second.label": "Please enter a number to add to first",
	"CheckBoxTypeDemo.label": "CheckBox Demo",
	"CheckBoxTypeDemo.description": "Sample action showing how to display a checkbox and accept boolean value from it",
	"CheckBoxTypeDemo.isChecked.label": "Flag",
	"RadioTypeDemo.label": "Radio Demo",
	"RadioTypeDemo.description": "This example shows how to create a radio group with predefined values",
	"RadioTypeDemo.region.label": "Select an appropriate region",
	"RadioTypeDemo.region.1.1.label": "US East",
	"RadioTypeDemo.region.1.2.label": "US West",
	"RadioTypeDemo.region.1.3.label": "US central",
	"BooleanTypeDemo.label": "Boolean Demo",
	"BooleanTypeDemo.description": "Sample action showing how to accept true, false or boolean variable",
	"BooleanTypeDemo.toInvert.label": "Please enter the boolean value to revert",
	"FileTypeDemo.label": "File Demo",
	"FileTypeDemo.description": "Sample action showing how to accept files",
	"FileTypeDemo.anyFile.label": "Please enter file name from either respository or local",
	"FileTypeDemo.localFile.label": "Please enter a local file",
	"FileTypeDemo.repositoryFile.label": "Please enter a repository",
	"TextAreaTypeDemo.label": "TextArea Demo",
	"TextAreaTypeDemo.description": "Sample action showing how to display a textarea and accept value from it",
	"TextAreaTypeDemo.firstName.label": "Please enter the first name",
	"TextAreaTypeDemo.lastName.label": "Please enter the last name",
	"DictionaryTypeDemo.label": "Dictionary Demo",
	"DictionaryTypeDemo.description": "Sample action showing how to use a dictionary",
	"DictionaryTypeDemo.keyValuePair.label": "Please provide the dictionary",
	"DictionaryTypeDemo.key.label": "Please enter the key",
	"ListTypeDemo.label": "List Demo",
	"ListTypeDemo.description": "Sample action showing how to display a List",
	"ListTypeDemo.sourceList.label": "Please enter the source",
	"ListTypeDemo.itemPositionNumber.label": "Please enter the index",
	"TaskBotTypeDemo.label": "Taskbot Demo",
	"TaskBotTypeDemo.description": "Sample action showing how to run another bot from parent bot"
}
```
日本語のテキストを保有するファイルja_JP.jsonを同じフォルダーに作成して、以下の内容で上書きしてみましょう。(何かの構成ファイルにja_JP.jsonの追加を記載する必要はありません。)

```json:ja_JP.json
{
	"label": "A2019デモパッケージ",
	"description": "A2019デモパッケージ操作のアクションを提供します。",
	"Concatenate.label": "連結",
	"Concatenate.description": "2つの文字列を連結する",
	"Concatenate.node_label": "{{firstString}}と{{secondString}}",
	"Concatenate.return_label": "出力を変数に割り当てる",
	"Concatenate.firstString.label": "1 番目の文字列",
	"Concatenate.secondString.label": "2 番目の文字列",
	"Uppercase.label": "大文字",
	"Uppercase.description": "大文字",
	"Uppercase.node_label": "{{sourceString}}の{{caseType}}",
	"Uppercase.return_label": "出力を変数に割り当てる",
	"Uppercase.sourceString.label": "ソース文字列",
	"Uppercase.caseType.label": "文字列のどの部分を大文字にするか指定する",
	"Uppercase.caseType.2.1.label": "文字列全体を大文字にする",
	"Uppercase.caseType.2.1.node_label": "すべて",
	"Uppercase.caseType.2.2.label": "文字列の最初の文字のみを大文字にする",
	"Uppercase.caseType.2.2.node_label": "最初の文字",
	"CredentialTypeDemo.label": "資格情報のデモ",
	"CredentialTypeDemo.description": "資格情報の使用方法を示すサンプルアクション",
	"CredentialTypeDemo.credentials.label": "ユーザーの資格情報を指定する",
	"SessionDemo.label": "セッションデモ",
	"SessionDemo.description": "セッションの使用方法を示すサンプル アクション",
	"SessionDemo.name.label": "セッション名を入力してください",
	"TextTypeDemo.label": "テキスト入力デモ",
	"TextTypeDemo.description": "テキストボックスを表示し、値入力を受ける方法を示すサンプルアクション",
	"TextTypeDemo.firstName.label": "名を入力してください",
	"TextTypeDemo.lastName.label": "姓を入力してください",
	"SelectTypeDemo.label": "選択デモ",
	"SelectTypeDemo.description": "定義済みの値を持つドロップダウンを作成する例",
	"SelectTypeDemo.region.label": "適切な地域を選択してください",
	"SelectTypeDemo.region.1.1.label": "米国東部",
	"SelectTypeDemo.region.1.2.label": "米国西部",
	"SelectTypeDemo.region.1.3.label": "米国中部",
	"NumberTypeDemo.label": "数字デモ",
	"NumberTypeDemo.description": "数字入りのテキスト ボックスを表示し、値入力を受ける方法を示すサンプル アクション",
	"NumberTypeDemo.first.label": "数字を入力してください",
	"NumberTypeDemo.second.label": "先ほどの数字に足す数字を入力してください",
	"CheckBoxTypeDemo.label": "チェックボックスデモ",
	"CheckBoxTypeDemo.description": "チェック ボックスを表示し、そこからブール値を受け取る方法を示すサンプル アクション",
	"CheckBoxTypeDemo.isChecked.label": "フラグ",
	"RadioTypeDemo.label": "ラジオボタンデモ",
	"RadioTypeDemo.description": "事前定義された値を持つラジオグループを作成する方法を示しています",
	"RadioTypeDemo.region.label": "適切な地域を選択してください",
	"RadioTypeDemo.region.1.1.label": "米国東部",
	"RadioTypeDemo.region.1.2.label": "米国西部",
	"RadioTypeDemo.region.1.3.label": "米国中部",
	"BooleanTypeDemo.label": "ブール値デモ",
	"BooleanTypeDemo.description": "真、偽、といったブール値を受け取る方法を示すサンプル アクション",
	"BooleanTypeDemo.toInvert.label": "反転するブール値を入力してください",
	"FileTypeDemo.label": "ファイルデモ",
	"FileTypeDemo.description": "ファイルを受け取る方法を示すサンプルアクション",
	"FileTypeDemo.anyFile.label": "リポジトリまたはローカルのファイル名を入力してください",
	"FileTypeDemo.localFile.label": "ローカルのファイル名を入力してください",
	"FileTypeDemo.repositoryFile.label": "リポジトリのファイル名を入力してください",
	"TextAreaTypeDemo.label": "テキスト領域デモ",
	"TextAreaTypeDemo.description": "テキスト領域を表示し、値入力を受け取る方法を示すサンプル アクション",
	"TextAreaTypeDemo.firstName.label": "名を入力してください",
	"TextAreaTypeDemo.lastName.label": "姓を入力してください",
	"DictionaryTypeDemo.label": "ディクショナリ型デモ",
	"DictionaryTypeDemo.description": "ディクショナリ型の使い方を示すサンプルアクション",
	"DictionaryTypeDemo.keyValuePair.label": "ディクショナリを指定してください",
	"DictionaryTypeDemo.key.label": "キーを入力してください",
	"ListTypeDemo.label": "リストデモ",
	"ListTypeDemo.description": "リストの表示方法を示すサンプルアクション",
	"ListTypeDemo.sourceList.label": "ソースを入力してください",
	"ListTypeDemo.itemPositionNumber.label": "インデックスを入力してください",
	"TaskBotTypeDemo.label": "Taskbot デモ",
	"TaskBotTypeDemo.description": "親のBotから別のBotを実行する方法を示すサンプル アクション"
}
```
これでビルドしてパッケージをControl Roomにアップロードしてみてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479715/d0c46b6a-84c3-c48a-86be-c6c241de9d25.png)

Control RoomのUI言語を日本語にすると、日本語で画面が表示されることがわかると思います。(ただし構造上、すべてのアクションが多言語対応のつくりになっているわけではないようですが。) UI言語を英語にすると、もとの英語表示に戻ります。言語を切り替え可能な形で実装を行うには、en_US.jsonとja_JP.json (もしくは必要に応じて他のロケール言語も)にそれぞれの言語のテキストを保持するようにすればいいことがわかりましたね。

日本語
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479715/0601c9f7-0c71-185a-85c6-05851798712b.png)

英語
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479715/5aeae628-a07f-ff07-5bba-2d292a2828d3.png)

### 2. 不要なアクションを削除してみよう

SDKサンプルには28ものアクションが含まれていますが、不要なアクションを削除してみましょう。
具体的には、`com\automationanywhere\botcommand\samples`フォルダーの下で`commands\basic\types`フォルダーに入っているすべてのアクション、そして`commands\ui`、`conditional`、`iterator`、`trigger`、`variable`フォルダーの中のアクションも、ひとまず不要と考えてみます。アクションを消すには実態のファイルを削除すればOKです。これらのフォルダーを削除してビルドしてみましょう。

パッケージをアップロードしてみると、4つのアクションのみに絞られていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479715/e8c50137-50a8-e0f8-30f4-713fadda338e.png)

これらはパッケージプロジェクトの中で残っているクラスに対応しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479715/da0dc2c6-e727-0d71-0906-8c060b2fbf65.png)

実際にBotの構築画面でもアクションの数が4つに減っていますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479715/e63ab817-c015-ce46-0056-83af40dbdcec.png)



### 3. 外部ライブラリを使ってオリジナルのアクションを作成してみよう
Uppercaseという、アルファベットを大文字に変換するアクションがありますが、これを真似て、半角文字を全角文字に変換する (Half2Full) アクションを、外部ライブラリであるICU4Jを使って構築してみましょう。
http://site.icu-project.org/download

まずはUppercase.javaの構造を見てみましょう。(コメントは翻訳済み。)

```Java:Uppercase.java
package com.automationanywhere.botcommand.samples.commands.basic;

import com.automationanywhere.botcommand.data.Value;
import com.automationanywhere.botcommand.data.impl.StringValue;
import com.automationanywhere.botcommand.exception.BotCommandException;
import com.automationanywhere.commandsdk.annotations.BotCommand;
import com.automationanywhere.commandsdk.annotations.CommandPkg;
import com.automationanywhere.commandsdk.annotations.Execute;
import com.automationanywhere.commandsdk.annotations.Idx;
import com.automationanywhere.commandsdk.annotations.Pkg;
import com.automationanywhere.commandsdk.annotations.rules.NotEmpty;
import com.automationanywhere.commandsdk.i18n.Messages;
import com.automationanywhere.commandsdk.i18n.MessagesFactory;
import com.automationanywhere.commandsdk.model.DataType;

import static com.automationanywhere.commandsdk.model.AttributeType.RADIO;
import static com.automationanywhere.commandsdk.model.AttributeType.TEXT;
import static com.automationanywhere.commandsdk.model.DataType.STRING;

//BotCommand によりクラスはアクションとして認識される
@BotCommand

//CommandPks はGUI上で表示するための必要な情報を追記する
@CommandPkg(
		//表示用にパッケージ内で重複しないnameとlabelを指定する
		name = "uppercase", label = "[[Uppercase.label]]",  
		node_label = "[[Uppercase.node_label]]",  description = "[[Uppercase.description]]", icon = "pkg.svg", 
		
		//型情報を返す。return_typeによりUI上で適切な変数のみ表示されるようにする 
		return_label = "[[Uppercase.return_label]]", return_type = STRING, return_required = true)
public class Uppercase {
	
	//指定されたファイル名から、国際化対応の専用プロパティに格納されたメッセージを読み込む
	private static final Messages MESSAGES = MessagesFactory
			.getMessages("com.automationanywhere.botcommand.samples.messages");

	//アクションのエントリポイント。文字列が返ってくるため戻り値はValue<String>に設定 
	@Execute
	public Value<String> action(
			//Idx 1 のついた入力用テキストボックスが最初に表示される
			@Idx(index = "1", type = TEXT) 
			//1つめのコントロールのUIラベル
			@Pkg(label = "[[Uppercase.sourceString.label]]") 
			//値が空の時は検証エラーが発生するようにする
			@NotEmpty 
			String sourceString,
			
			//ラジオボタンで2つの選択肢を提示するので、typeをRADIOにする
			//RADIO は @NotEmpty なので、ここでは検証は必要ない
			//RADIO は常にStringを返す
			@Idx(index = "2", type = RADIO, 
				
				//RADIOが複数の選択肢を返す 
				options = {
					//選択肢のPkgをIdx.Optionの内部で指定する
					//Pkg内のvalueは選択されたときに返す値を指定する
					@Idx.Option(index = "2.1", pkg = @Pkg(node_label = "[[Uppercase.caseType.2.1.node_label]]", label = "[[Uppercase.caseType.2.1.label]]", value = "ALL")),
					@Idx.Option(index = "2.2", pkg = @Pkg(node_label = "[[Uppercase.caseType.2.2.node_label]]", label = "[[Uppercase.caseType.2.2.label]]", value = "FIRST_CHAR"))
			})
			//2つめのコントロール (caseType) のUIラベル
			@Pkg(label = "[[Uppercase.caseType.label]]", default_value = "ALL", default_value_type = DataType.STRING)
		    String caseType) {
		
		//内部的に検証を行い、空の文字列を許さないようにする。firstStringはNotEmptyであるため、Null値のチェックは必要ない。
		if ("".equals(sourceString.trim()))
			throw new BotCommandException(MESSAGES.getString("emptyInputString", "sourceString"));
		
		if ("".equals(caseType.trim()))
			throw new BotCommandException(MESSAGES.getString("emptyInputString", "caseType"));

		//このアクションのビジネスロジック
		String result = "ALL".equals(caseType) ? sourceString.toUpperCase() : (sourceString.substring(0, 1).toUpperCase() + sourceString.substring(1));
		
		//StringValueを戻り値として返す
		return new StringValue(result);
	}
}

```
やっていることですが、もうちょっと単純化すると以下のような構造となっています。

```Java:Uppercase.java(単純化)
@BotCommand

@CommandPkg( ... )
public class Uppercase {
	@Execute
	public Value<String> action( @Idx()@Pkg() String sourceString, @Idx()@Pkg()String caseType) {

... ビジネスロジック ...

		return new StringValue(result);
	}
}
```
どうでしょうか。だいぶ構造が見えてきましたか？`@BotCommand`、`@CommandPkg`、`@Execute`あたりはおまじないと思ってい所定の場所に入れておきましょう。それぞれ「Javaクラスの種類 (単純なアクションなのかIf/Loopアクションの拡張なのか)」「パッケージの概要」「クラス内のエントリポイントになる関数の指定」を行います。エントリポイントになる関数の引数が、「アクションの詳細」における入力フィールドに対応します。`@Idx`、`@Pkg`やValidationアノテーションで詳細を指定します。

いま、エントリポイントの関数で文字列引数が1つのHalf2Fullクラスを同様に作成しましょう。
Half2Full.javaをUppercase.javaと同じ場所に作成して、以下のように内容をコーディングします。

```Java:Half2Full.java
package com.automationanywhere.botcommand.samples.commands.basic;

import com.automationanywhere.botcommand.data.Value;
import com.automationanywhere.botcommand.data.impl.StringValue;
import com.automationanywhere.botcommand.exception.BotCommandException;
import com.automationanywhere.commandsdk.annotations.BotCommand;
import com.automationanywhere.commandsdk.annotations.CommandPkg;
import com.automationanywhere.commandsdk.annotations.Execute;
import com.automationanywhere.commandsdk.annotations.Idx;
import com.automationanywhere.commandsdk.annotations.Pkg;
import com.automationanywhere.commandsdk.annotations.rules.NotEmpty;
import com.automationanywhere.commandsdk.i18n.Messages;
import com.automationanywhere.commandsdk.i18n.MessagesFactory;
import com.automationanywhere.commandsdk.model.DataType;

import static com.automationanywhere.commandsdk.model.AttributeType.TEXT;
import static com.automationanywhere.commandsdk.model.DataType.STRING;

import com.ibm.icu.text.Transliterator;

//BotCommand makes a class eligible for being considered as an action.
@BotCommand

//CommandPks adds required information to be dispalable on GUI.
@CommandPkg(
		//Unique name inside a package and label to display.
		name = "Half2Full", label = "[[Half2Full.label]]",  
		node_label = "[[Half2Full.node_label]]",  description = "[[Half2Full.description]]", icon = "pkg.svg", 
		
		//Return type information. return_type ensures only the right kind of variable is provided on the UI. 
		return_label = "[[Half2Full.return_label]]", return_type = STRING, return_required = true)
public class Half2Full {
	
	//Messages read from full qualified property file name and provide i18n capability.
	private static final Messages MESSAGES = MessagesFactory
			.getMessages("com.automationanywhere.botcommand.samples.messages");

	//Identify the entry point for the action. Returns a Value<String> because the return type is String. 
	@Execute
	public Value<String> action(
			//Idx 1 would be displayed first, with a text box for entering the value.
			@Idx(index = "1", type = TEXT) 
			//UI labels.
			@Pkg(label = "[[Half2Full.sourceString.label]]") 
			//Ensure that a validation error is thrown when the value is null.
			@NotEmpty 
			String sourceString) {
		
		//Internal validation, to disallow empty strings. No null check needed as we have NotEmpty on firstString.
		if ("".equals(sourceString.trim()))
			throw new BotCommandException(MESSAGES.getString("emptyInputString", "sourceString"));


		//Business logic
                Transliterator tr = Transliterator.getInstance("Halfwidth-Fullwidth");

		String result = tr.transliterate(sourceString);

		//Return StringValue.
		return new StringValue(result);
	}
}
```
また、ja_JP.json とen_US.jsonに以下のエントリを追加します。

```json:ja_JP.json
...
	"Half2Full.label": "全角文字に変換",
	"Half2Full.node_label": "{{sourceString}}を全角文字に変換",
	"Half2Full.description": "半角文字を全角文字に変換します",
	"Half2Full.return_label": "出力を変数に割り当てる",
	"Half2Full.sourceString.label": "ソース文字列",
...
```

```json:en_US.json
...
	"Half2Full.label": "Full width",
	"Half2Full.node_label": "Full width of {{sourceString}}",
	"Half2Full.description": "Convert half width to full width characters",
	"Half2Full.return_label": "Assign the output to variable",
	"Half2Full.sourceString.label": "Source string",
...
```

あわせて、`build.gradle`に以下を追加します。

```:build.gradle
...
	dependencies {
		compileOnly name: 'command-annotations', version: '1+'
		compileOnly name: 'bot-runtime', version: '1+'
		compileOnly name: 'bot-api', version: '1+'
		compileOnly name: 'common-security', version: '1+'
		implementation name: 'i18n-api', version: '1+'
		implementation name: 'icu4j', version: '67_1' 👈👈👈 これを追加。バージョン番号はicu4j-XX_X.jarの "XX_X" の部分の文字列を記載
		apt name: 'command-processor', version: '1+'
		compileOnly group: 'org.apache.logging.log4j', name: 'log4j-core', version: "$loggerVersion"
		testImplementation "org.testng:testng:$testNgVersion"
		testImplementation name: 'bot-runtime', version: '1+'
		testImplementation name: 'bot-api', version: '1+'
		implementation group: 'net.java.dev.jna', name: 'jna-platform', version: "$jnaVersion"		
	}
...
```
そして、ICU4Jのサイトからダウンロードした`icu4j-XX_X.jar`ファイル (12MBくらいのファイル) を`libs`に置きます。

これでビルドしてパッケージを読み込んでみてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479715/85512b4b-68f9-77d9-dfd1-50162e0a1f93.png)

半角文字を全角文字に変換するアクションができあがりました。
UIで表示されるラベルと、(locale).jsonファイルの中にあるリソーステキストとの関係も記しておきました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479715/1205d10e-0583-fe9f-f5d5-3c2fbd5ae113.png)


# アイコンの作成
アイコンはSVG形式のものが使えます。これはベクター形式で、XMLとしてテキストエディターで開くことができます。たとえば、サンプルで使われている`sample.svg`はメモ帳で開くと以下のような中身になっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479715/86fc7f52-0827-395a-d0e9-7d56bb26b62f.png)

```XML:sample.svg
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
	 viewBox="0 0 18 18" style="enable-background:new 0 0 18 18;" xml:space="preserve">
<style type="text/css">
	.st0{display:none;}
	.st1{display:inline;opacity:0.5;}
	.st2{fill:#6FCBDC;}
	.st3{display:inline;}
	.st4{opacity:0.5;fill:none;stroke:#A8A7A8;stroke-width:0.05;stroke-miterlimit:10;enable-background:new    ;}
	.st5{fill:none;stroke:#A8A7A8;stroke-width:0.05;stroke-miterlimit:10;}
	.st6{display:inline;opacity:0.5;fill:none;stroke:#6FCBDC;stroke-width:0.2;stroke-miterlimit:10;enable-background:new    ;}
	.st7{fill:#4C6A8D;}
</style>
<g id="Grid" class="st0">
	<g class="st1">
		<path class="st2" d="M18.8-0.8L18.8-0.8v19.6H-0.8V-0.8H18.8 M19-1H-1v20h20V-0.7V-1L19-1z"/>
	</g>
	<g class="st3">
		<line class="st4" x1="-2" y1="-3" x2="-2" y2="21"/>
		<line class="st4" x1="-1" y1="-3" x2="-1" y2="21"/>
		<line class="st4" x1="0" y1="-3" x2="0" y2="21"/>
		<line class="st4" x1="1" y1="-3" x2="1" y2="21"/>
		<line class="st4" x1="2" y1="-3" x2="2" y2="21"/>
		<line class="st4" x1="3" y1="-3" x2="3" y2="21"/>
		<line class="st4" x1="4" y1="-3" x2="4" y2="21"/>
		<line class="st4" x1="5" y1="-3" x2="5" y2="21"/>
		<line class="st4" x1="6" y1="-3" x2="6" y2="21"/>
		<line class="st4" x1="7" y1="-3" x2="7" y2="21"/>
		<line class="st4" x1="8" y1="-3" x2="8" y2="21"/>
		<line class="st4" x1="9" y1="-3" x2="9" y2="21"/>
		<line class="st4" x1="10" y1="-3" x2="10" y2="21"/>
		<line class="st4" x1="11" y1="-3" x2="11" y2="21"/>
		<line class="st4" x1="12" y1="-3" x2="12" y2="21"/>
		<line class="st4" x1="13" y1="-3" x2="13" y2="21"/>
		<line class="st4" x1="14" y1="-3" x2="14" y2="21"/>
		<line class="st4" x1="15" y1="-3" x2="15" y2="21"/>
		<line class="st4" x1="16" y1="-3" x2="16" y2="21"/>
		<line class="st4" x1="17" y1="-3" x2="17" y2="21"/>
		<line class="st4" x1="18" y1="-3" x2="18" y2="21"/>
		<line class="st4" x1="19" y1="-3" x2="19" y2="21"/>
		<line class="st4" x1="20" y1="-3" x2="20" y2="21"/>
		<line class="st4" x1="21" y1="-2" x2="-3" y2="-2"/>
		<line class="st4" x1="21" y1="-1" x2="-3" y2="-1"/>
		<line class="st4" x1="21" y1="0" x2="-3" y2="0"/>
		<line class="st4" x1="21" y1="1" x2="-3" y2="1"/>
		<line class="st4" x1="21" y1="2" x2="-3" y2="2"/>
		<line class="st4" x1="21" y1="3" x2="-3" y2="3"/>
		<line class="st4" x1="21" y1="4" x2="-3" y2="4"/>
		<line class="st4" x1="21" y1="5" x2="-3" y2="5"/>
		<line class="st4" x1="21" y1="6" x2="-3" y2="6"/>
		<line class="st4" x1="21" y1="7" x2="-3" y2="7"/>
		<line class="st4" x1="21" y1="8" x2="-3" y2="8"/>
		<line class="st4" x1="21" y1="9" x2="-3" y2="9"/>
		<line class="st4" x1="21" y1="10" x2="-3" y2="10"/>
		<line class="st4" x1="21" y1="11" x2="-3" y2="11"/>
		<line class="st4" x1="21" y1="12" x2="-3" y2="12"/>
		<line class="st4" x1="21" y1="13" x2="-3" y2="13"/>
		<line class="st4" x1="21" y1="14" x2="-3" y2="14"/>
		<line class="st4" x1="21" y1="15" x2="-3" y2="15"/>
		<line class="st4" x1="21" y1="16" x2="-3" y2="16"/>
		<line class="st4" x1="21" y1="17" x2="-3" y2="17"/>
		<line class="st4" x1="21" y1="18" x2="-3" y2="18"/>
		<line class="st4" x1="21" y1="19" x2="-3" y2="19"/>
		<line class="st4" x1="21" y1="20" x2="-3" y2="20"/>
		<rect x="-3" y="-3" class="st5" width="24" height="24"/>
	</g>
	<line class="st6" x1="-3" y1="-3" x2="21" y2="21"/>
	<line class="st6" x1="21" y1="-3" x2="-3" y2="21"/>
	<line class="st6" x1="-3" y1="9" x2="21" y2="9"/>
	<line class="st6" x1="-3" y1="3" x2="21" y2="3"/>
	<line class="st6" x1="-3" y1="14" x2="21" y2="14"/>
	<circle class="st6" cx="9" cy="9" r="10"/>
	<line class="st6" x1="15" y1="-3" x2="15" y2="21"/>
	<line class="st6" x1="3" y1="-3" x2="3" y2="21"/>
	<line class="st6" x1="9" y1="-3" x2="9" y2="21"/>
</g>

<path class="st7" d="M2.009,1.59c0,3.4,0,5.1,0,8.6v6.8H3v-7.1c4.9-1.3,7.894,2.169,12.994,0.269c0-3.4,0-5.1,0-8.6
	C10.593,3.659,7.509-0.41,2.009,1.59z"/>
</svg>
```
元々のファイルはAdobe Illustratorで作られたことを伺わせますが、有料のツールでなくても、SVG形式のファイルを直接編集できる無料ツールがあります。たとえばINKSCAPEを使うとよいでしょう。
https://inkscape.org/ja/

INKSCAPEはインストーラこそ英語であるものの、日本語UIにも対応しており、高機能でお勧めです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479715/8ab9bd9c-cc4d-b1cf-7194-9387808421ce.png)

以下のような編集画面となります。INKSCAPEの使い方自体はここでは詳しく述べませんが、使ってみてください！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479715/103ca197-1a5f-36f2-9c66-ca3d7149c929.png)


# 開発時のコツ、注意事項

この記事ですでに説明していることも含めて、改めて主なものをまとめてみました。

* アイコンはSVG形式で作成しresourceへ配置して、アノテーションで指定してください。
* 依存関係（外部パッケージ）については、利用するものを`build.gradle`へすべて記載すること。そうすることで、必要なライブラリファイルが出力される.jarファイルに取り込まれます。
* エラーメッセージはBot Runtime frameworkの i18n APIについている`BotCommandException()`を利用し、多言語対応させてください。他のUI要素も同様。
* インプットされたデータの検証にはValidationアノテーションを利用してください。
* ループはCPU負荷を上げやすく、ボットが応答しない原因となるため、極力避けてください。
* ログ出力には、Bot Runtime frameworkの共通ライブラリについているlog4Jを利用してください。

詳しくは製品ドキュメントの『[パッケージの開発のための標準のコーディング方法とガイドライン](https://docs.automationanywhere.com/bundle/enterprise-v2019/page/enterprise-cloud/topics/developer/cloud-standard-coding-practices-guidelines.html?_LANG=jpjp)』を参照してください。

ちなみに、jsonファイルなどの文法が間違っていても、ビルドは通ってしまうことがあります。パッケージのアップロード時に以下のようなエラーが出てアップロードできない場合は、データファイルの文法などがどこかで間違っていないか、もう一度確認してみてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479715/a205d944-d774-263d-7553-d1908a69fc75.png)


# おわりに
これで、Javaが書けるエンジニアには、自分独自のアクションパッケージを作成するための基本的情報がそろったのではないかと思います。ぜひ、自分だけのアクションパッケージを作成してみて、他の人にも配布してみましょう！

次回は作成したパッケージをBot Storeで公開する方法に触れたいと思います。

# この記事のシリーズ

* [Automation Anywhere A2019 の共通開発部品であるパッケージを作成する #1～まずはSDKサンプルをそのままビルドして使う](https://qiita.com/RPAbot/items/3ea4216f1b94f76df0b3)
* **Automation Anywhere A2019 の共通開発部品であるパッケージを作成する #2～SDKサンプルを変更してオリジナルパッケージを構築！** (この記事)
* Automation Anywhere A2019 の共通開発部品であるパッケージを作成する #3～作成したBotやパッケージをBot Storeにアップロードしよう (今後の予定)

# 参考資料

* [Developer Meetup Vol.2 資料:A2019 共通部品開発方法-パッケージとDLLとMetaBotの開発](https://automationanywhere.app.box.com/s/cowdu6uf607x1z1k2qqdb556gl7dsih7)
* [Automation Anywhere Enterprise Package Development Kit](https://docs.automationanywhere.com/bundle/enterprise-v2019/page/enterprise-cloud/topics/developer/cloud-create-package-overview.html) - 製品ドキュメント