レベル: 中級 Andrei Cioroianu, Senior Java Developer and Consultant, Devsphere
2007年 10月 09日 この連載の第 1 回では、Java™ 開発者である著者の Andrei Cioroianu が Ajax (Asynchronous JavaScript + XML) によって Web フォームのユーザー入力を送信し、JSF (JavaServer Faces) によって Ajax リクエストを処理する方法を説明しました。続く第 2 回では、サーバー側でのデータ管理を取り上げ、自動的に保存されたフォーム・データを維持するためのデータ・リポジトリーを紹介しました。3 回からなる連載の最終回となるこの記事で説明するのは、おそらく読者が想像する以上に巧妙な JSF フォーム・データのリストア方法です。興味深い JSF 手法として、JSF コンポーネントの immediate および onclick 属性を使用する方法、JSF リクエスト処理のライフ・サイクルのいくつかのフェーズを省略する方法、隠しフォーム要素を使って JSF リスナーを起動する方法を学んでください。この記事ではさらに、JSP/JSF 式を JavaScript コードに組み込む方法や JSF コンポーネントのレンダラーで生成された HTML フォーム要素と併せて JavaScript を使用する方法、そしてアプリケーション Bean のシリアライズおよびデシリアライズを行うサーブレット・コンテキスト・リスナーを実装する方法も説明します。
連載の概要
全 3 回からなるこの連載では一貫して 1 つの Web アプリケーションを取り上げ、毎回の記事でアプリケーションの機能強化を進めています。このセクションで、このサンプル・アプリケーションの概要を簡単に説明しておきます。
第 1 回では典型的な JSF フォームである SupportForm.jsp を出発点として、ユーザー入力を定期的かつ透過的に自動保存できるように Ajax でフォーム・データを取得、エンコード、送信する一連の再利用可能な JavaScript 関数を紹介しました。これらのJavaScript 関数のソース・コードは、サンプル・アプリケーションの AutoSaveScript.js ファイルに記載されています。第 1 回では、Ajax リクエストを処理するための AutoSaveListener という JSF フェーズ・リスナーを構築する方法も説明しました。このリスナー・クラスは第 2 回で変更しました。
第 2 回では、現行 JSF ビューのデータをデータ・リポジトリーに保管する方法を説明しました。この DataMapRepository クラスがデータ・マップを含む Map です。それぞれのデータ・マップには単一フォーム・インスタンスのユーザー入力が保持されます。データ・リポジトリーにアクセスするには RepositoryWrapper というスレッド・セーフなラッパー・クラスを使用し、シャットダウンの前にリポジトリーをファイルにシリアライズして起動後にリポジトリーの状態をリストアするには DataMapPersistence というサーブレット・コンテキスト・リスナーを使用します。この第 2 回では、ブラウザー・セッション間で匿名のユーザーを識別するために使うサーブレット・フィルター、BrowserIdFilter も紹介しました。
連載最終回となるこの第 3 回目の記事では、ユーザーがブラウザーを閉じて再び開いたときにデータがリストアされるように SupportForm.jsp ページを変更します。リストア・リクエストを処理する JSF リスナー・メソッドは、ViewRestorer クラスが提供します。第 3 回のサンプル・コードは、JSF リクエスト処理のライフ・サイクルについての知識がないと理解しにくいかもしれません。この JSF メカニズムの概要は第 1 回を参照してください。また、連載のすべての記事では使用する JSF 機能をできる限り詳しく説明しています。リクエスト処理のライフ・サイクルについての詳細は、JSF 仕様にも記載されています。
リストア・リクエスト・ハンドラーのビルド
このセクションでは、JSF フォームのデータをリストアするために使用する Java メソッドを紹介します。これらのメソッドは、後で説明する複数の JavaScript 関数および JSF コンポーネントと組み合わせて使用します。
現行ビューのデータをリストアする
第 2 回では、JSF コンポーネント・ツリーをトラバースして入力コンポーネントの値を取得するという手段で現行ビューのフォーム・データを保管する仕組みを説明しました。この操作は DataMapRepository クラスの saveValues() メソッドで行われます。このクラスにはまた、入力コンポーネントの値をリストアするためのメソッドも含まれています。それが、restoreValues() メソッド (リスト 1 を参照) です。このメソッドは EditableValueHolder インターフェースを実装するコンポーネントごとに、前に保管された値を指定のデータ・マップから取得して JSF コンポーネントの value プロパティーを設定し、submittedValue プロパティーをクリアします。
リスト 1. 入力コンポーネントの値のリストア
package autosave;
...
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
...
public class DataMapRepository ... {
...
public static void restoreValues(UIComponent comp,
Map<String, Object> dataMap) {
if (comp == null || dataMap == null)
return;
if (comp instanceof EditableValueHolder) {
// Input component. Get its value from the data map
// and clear any submitted value
EditableValueHolder evh = (EditableValueHolder) comp;
evh.setValue(dataMap.get(comp.getId()));
evh.setSubmittedValue(null);
}
// Iterate over the children of the current component
Iterator children = comp.getChildren().iterator();
while (children.hasNext()) {
UIComponent child = (UIComponent) children.next();
// Recursive call
restoreValues(child, dataMap);
}
}
}
|
第 2 回では、フォームの自動保存リクエストを処理する AutoSaveListener クラスの saveCurrentView() メソッドも紹介しました。このセクションではサンプル・アプリケーションの ViewRestorer クラスについて説明します。このクラスには、restoreCurrentView() という名前のメソッドと 2 つの JSF イベント・リスナーが含まれています。これらのメソッドとイベント・リスナーが、現行 JSF ビューのデータをリストアするリクエスト (リストア・リクエスト) を処理します。
リスト 2 の restoreCurrentView() メソッドは、ラッパー Bean によってリポジトリーにアクセスし、現行のユーザーとビューの組み合わせに対応するデータ・マップを取得してから、restoreValues() メソッドを呼び出して入力コンポーネントの値をリストアします。その後、restoreCurrentView() は faces コンテキストの renderResponse() メソッドを呼び出し、JSF フレームワークに Render Response (レスポンス生成) フェーズに進むよう通知します。renderResponse() 呼び出しは、この後説明するように immediate 属性と併せて使用します。
リスト 2. 現行 JSF ビューのデータのリストア
package autosave;
import java.util.Map;
...
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
public class ViewRestorer implements java.io.Serializable {
public void restoreCurrentView() {
// Get the faces context of the current request.
FacesContext ctx = FacesContext.getCurrentInstance();
// Get the root component of the current view.
UIViewRoot root = ctx.getViewRoot();
// Get the managed bean instance wrapping the data repository.
RepositoryWrapper wrapper = RepositoryWrapper.getManagedBean(ctx);
// Get the data map for the current context.
Map<String, Object> dataMap = wrapper.getDataMap(ctx);
// Use the data map to restore the values of the JSF components.
DataMapRepository.restoreValues(root, dataMap);
// Signal the JSF framework to go to the Render Response phase.
ctx.renderResponse();
}
...
}
|
JSF リスナーを実装する
ViewRestorer の actionListener() および valueChangeListener() メソッド (リスト 3 を参照) をそれぞれ JSF コンポーネントの actionListener 属性と valueChangeListener 属性で使用すると、現行ビューのリストアを起動できます。この 2 つのリスナー・メソッドを使用する方法は、次のセクションで実演します。
リスト 3. リストア・イベントの JSF リスナー
package autosave;
...
import javax.faces.event.ActionEvent;
import javax.faces.event.ValueChangeEvent;
public class ViewRestorer implements java.io.Serializable {
...
public void actionListener(ActionEvent e) {
restoreCurrentView();
}
public void valueChangeListener(ValueChangeEvent e) {
restoreCurrentView();
}
...
}
|
isCurrentViewRestorable() メソッドは、リポジトリーに現行ビューと現行ユーザーのデータ・マップが含まれる場合には true を返します (リスト 4 を参照)。
リスト 4. 現行ビューのデータをリストアできるかどうかの検証
package autosave;
...
public class ViewRestorer implements java.io.Serializable {
...
public boolean isCurrentViewRestorable() {
FacesContext ctx = FacesContext.getCurrentInstance();
RepositoryWrapper wrapper = RepositoryWrapper.getManagedBean(ctx);
return wrapper.hasDataMap(ctx);
}
}
|
リスト 5 に、ViewRestorer クラスを管理対象 Bean として構成し、SupportForm.jsp ページの JSF コンポーネントがリスナー・メソッドを使えるようにする方法を示します。
リスト 5. ビュー・リストア用 Bean の構成
<faces-config>
...
<managed-bean>
<managed-bean-name>viewRestorer</managed-bean-name>
<managed-bean-class>autosave.ViewRestorer</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
...
</faces-config>
|
リストア・リクエストの送信
SupportForm.jsp ページは、第 1 回で紹介した AutoSaveScript.js ファイルの setAutoSaving() 関数を使ってフォームの自動保存機能を有効にします。このセクションでは、現行ビューのデータをリストアできるように JSF ページを変更する方法を説明します。
リストア・リクエストに隠しトリガーを使用する
ViewRestorer Bean の valueChangeListener() メソッドに対して ValueChangeEvent を起動するのに最も簡単な方法は、SupportForm.jsp ページに隠し要素を追加することです。リスト 6 に、JSF ページに追加するコードを記載します。
リスト 6. リストア・イベントの隠しトリガー
<h:form id="supportForm">
<h:inputHidden id="restoreTrigger" value="default"
valueChangeListener="#{viewRestorer.valueChangeListener}"
immediate="true"/>
...
</h:form>
|
フォームのリストアを起動するのにどうして隠しコンポーネントを使わなければならないのか疑問に思うことでしょう。フォームがレンダリングされるときに単純にリストアするだけにはできない理由を理解するには、リストアしなければならない値を持つコンポーネントのツリーを JSF フレームワークがどのように作成するかを把握する必要があります。ここで忘れてはならないのは、ユーザーはアプリケーションを終了した後 (あるいはブラウザーが異常終了した後)、GET リクエストを使用してフォーム・ページに戻るという点です。
JSF フレームワークが上記のシナリオで GET リクエストを処理する場合、たいていはリクエスト処理ライフ・サイクルの最初のフェーズ (Restore View (ビューのリストア)) では JSF コンポーネント・ツリーをリストアできないはずです。JSF 実装ではこれが可能だとしても、ユーザーは単にアプリケーションに戻っているだけなので、リクエストにはパラメーターが含まれません。
JSF 仕様によると、ビューをリストアできない場合、あるいはリクエストに照会用パラメーターも POST データも含まれない場合、JSF 実装は Restore View フェーズで faces コンテキストの renderResponse() メソッドを呼び出すことになっています。この場合、リクエスト処理は最後のフェーズ (Render Response) にジャンプし、このフェーズで HTML 出力が生成されると同時に、前の段階でリストアされなかったコンポーネント・ツリーも作成されることになります。これはつまり、アプリケーションには GET リクエストの処理中にコンポーネント・ツリーを更新するチャンスがなく、更新したときにはすでに出力がユーザーのブラウザーに送信されてしまっているということです。
POST リクエストを送信して JSF フォームをリストアする
上記の問題を解決する方法は、JSF フレームワークに GET リクエストを処理させてから、新しい POST リクエストを送信することです。こうすれば、アプリケーションが JSF コンポーネント・ツリーを処理してコンポーネントの値をリストアできるようになります。POST リクエストは、フォーム・オブジェクトの JavaScript submit() メソッドで送信することができます。この場合には XMLHttpRequest を使いません。それは、ページの更新は POST リクエストの後に行わなければならないためです。
フォーム・ページの HTML 出力をよく見ると、JSF が隠し要素に supportForm:restoreTrigger ID を生成していることに気付くはずです。したがって、Web ページでフォーム要素オブジェクトを見つけるためには、getFormElement() (リスト 7 を参照) のような JavaScript 関数が必要になります。
リスト 7. JSF コンポーネントによってレンダリングされたフォーム要素の検出
function getFormElement(formId, elemId) {
return document.getElementById(formId + ":" + elemId);
}
|
リスト 8 に示すのは、フォーム・データのリストア・リクエストを送信するもう 1 つの JavaScript 関数です。この submitRestoreRequest() 関数は、supportForm オブジェクトの submit() メソッドを呼び出す前に隠し要素の値を変更し、リクエストがサーバー側で処理されるときに ViewRestorer Bean の valueChangeListener() メソッドが起動されるようにします。submitRestoreRequest() 関数は SupportForm.jsp ファイルに記載されています。
リスト 8. フォーム・データのリストア・リクエストの送信
function submitRestoreRequest() {
var restoreTrigger
= getFormElement("supportForm", "restoreTrigger");
restoreTrigger.value = "restore";
var supportForm = document.getElementById("supportForm");
supportForm.submit();
}
|
フォームがリストア可能かどうかを検証する
submitRestoreRequest() によってサーバーに送信されるリストア・リクエストは、POST を使用します。POST はあらゆる JSF フォームで指定されている HTTP メソッドだからです。しかし無限ループを防ぐには、GET リクエストのレスポンスとして現行ページが生成された場合にのみリストア・リクエストが送信されるようにしなければなりません。さらに、リポジトリーには現行のユーザーとフォームの組み合わせに対して保管されたデータが含まれる必要もあります。この両方の条件は isRestorable() 関数内で検証されます。これにより、サーバー側で評価された 2 つの JSP/JSF EL 式の値が含まれるクライアント側の JavaScript 式が評価されます(リスト 9 を参照)。
リスト 9. リストア・リクエストを送信できるかどうかの検証
function isRestorable() {
return "${pageContext.request.method}".toUpperCase() == "GET"
&& <h:outputText value="#{viewRestorer.currentViewRestorable}"/>;
}
|
生成された JavaScript 関数の式の結果が true となるのは、ページが GET リクエストの後に生成され、なおかつ ViewRestorer Bean の isCurrentViewRestorable() メソッドがサーバー側で true を返した場合です (リスト 10 を参照)。
リスト 10. GET リクエストの後に生成された JavaScript コード
function isRestorable() {
return "GET".toUpperCase() == "GET"
&& true;
}
|
リストア・リクエストが submitRestoreRequest() で送信された場合や、ユーザーが Submit ボタンをクリックした場合には、サーバーが POST リクエストを受信し、isRestorable() が false を返します (リスト 11 を参照)。
リスト 11. POST リクエストの後に生成された JavaScript コード
function isRestorable() {
return "POST".toUpperCase() == "GET"
&& true;
}
|
リストア・リクエストの処理方法について
このセクションでは、サンプル・アプリケーションの ViewRestorer クラスでの immediate 属性の役割、そして restoreCurrentView() メソッドから renderResponse() を呼び出す目的について説明します。
JSF コンポーネントの immediate 属性を使用する
前のセクションで使用した <h:inputHidden> コンポーネントでは、immediate 属性を true に設定して、JSF リクエスト処理のライフ・サイクルのきわめて早い段階でリスナー・メソッドを呼び出せるようにしています。正確に言うと、リスナー・メソッドはApply Request Values (リクエスト値の適用) フェーズで呼び出されます。immediate 属性は、アクション・メソッドを Invoke Application (アプリケーション起動) フェーズになるまで待たずに Apply Request Values フェーズで呼び出さなければならないコマンド・ボタンに対しても、true に設定することができます。例えば Restore ボタンを JSF フォームに追加する場合は、immediate 属性を true に設定した <h:commandButton> タグを使用することができます (リスト 12 を参照)。
リスト 12. フォーム・データをリストアするためのコマンド・ボタン
<h:form id="supportForm">
...
<h:commandButton id="restoreButton" value="Restore"
actionListener="#{viewRestorer.actionListener}"
immediate="true"/>
...
</h:form>
|
要するに、SupportForm.jsp ページの restoreTrigger および restoreButton コンポーネントの immediate 属性は true に設定されているため、ViewRestorer Bean の valueChangeListener() メソッドと actionListener() メソッドは Apply Request Values フェーズで呼び出されます。
一部の JSF リクエスト処理フェーズを省略する
 | 前のセクションで説明した renderResponse() 呼び出しは、ここで分析している renderResponse() とは何の関係もありません。前のセクションで説明したのは JSF フレームワークが Restore View フェーズでこのメソッドを呼び出す場合についてで、このセクションで説明するのはサンプル・アプリケーションが JSF リクエスト処理ライフ・サイクルの Apply Request Values フェーズで呼び出す場合についてです。 |
|
ViewRestorer クラスの 2 つのリスナー・メソッドは同じクラスにある restoreCurrentView() メソッドを呼び出し、それによって faces コンテキストの renderResponse() メソッドが呼び出されます。したがって、リクエスト処理は Apply Request Values フェーズから直接 Render Response フェーズに移り、Process Validations (検証処理)、Update Model Values (モデル値更新)、および Invoke Application フェーズは省略されます。
ここで、ViewRestorer クラスの restoreCurrentView() メソッドから renderResponse() を呼び出すことの意味を分析してみましょう。このアプリケーションで何よりも必要なのは、submitRestoreRequest() 関数でフォームが送信されるときに、送信されるデータをすべて無視し、ViewRestorer の valueChangeListener() を起動することです。そのために、DataMapRepository クラスの restoreValues() メソッドはそれぞれの入力コンポーネントの submittedValue プロパティーをクリアします。通常、値の欠落は多くの検証エラーの原因となるものですが、このサンプル・アプリケーションの場合には該当しません。renderResponse() が呼び出されることで、リストア・リクエストには Process Validations フェーズが実行されないためです。
renderResponse() によって Process Validations、Update Model Values、Invoke Application フェーズを省略するということは、リストア・リクエストによる副次作用を心配する必要がないという意味でもあります。したがって、検証フェーズの後にフェーズ・イベントをリッスンする AutoSaveListener クラスにはリストア・リクエストによる影響がないため、このクラスを変更する必要はありません。その上、リストア・リクエストの後にアプリケーションのデータ・モデルが更新されることも、アクション・メソッドが呼び出されることもありません。このように、同じ原則がリストア・リクエストと自動保存リクエストの両方に適用されると、リクエストの処理は透過的に行われ、アプリケーション・ロジックに干渉することもありません。
ユーザーによるフォームの保存とリストアの制御
フォームのリストアを開始する手段は、restoreTrigger 隠しコンポーネントと submitRestoreRequest() JavaScript 関数が提供しています。リストア処理が開始されると、後はサーバー側のコードによって、自動保存されたデータでリストアされたフォームが生成されます。また一方で、いくつかのボタンを追加してユーザーがフォームの保存やリストアを制御できるようにすることも可能です。実際のアプリケーションでユーザー・インターフェースを簡潔にして、保存とリストアの両方を透過的かつ自動的に行いたい場合には、このようなボタンの追加はお勧めしません。
その一方、実際のアプリケーションで自動保存機能を拡張すれば、ユーザーはまるでデスクトップ・アプリケーションで文書を保存/ロードするように Web フォームを保存/ロードできるようになります。この場合、ユーザーが Web フォームをリストアし、一部のデータを変更してフォームを Web サーバーに再送信することも可能です。一例として、月々の請求書を送信するためのフォームがあるとします。請求日、請求書番号、そしておそらく請求項目など、一部の入力は月によって変わることがあっても、入力済みの仕入先と買い手をそのまま使えるとしたら、フォーム・データの入力に費やす時間を節約することができます。
Save ボタンを追加する
前のセクションでは Restore ボタンを追加する方法を説明しましたが、フォームには Save ボタンを含めることもできます。このボタンのコードはリスト 13 のとおりです。サーバー側のメソッドを action 属性や actionListener 属性で指定する代わりに、この Save ボタンではユーザーがボタンをクリックしたときに submitSaveRequest() JavaScript 関数を呼び出すために onclick 属性を使っています。
リスト 13. フォーム・データを保存するためのコマンド・ボタン
<h:form id="supportForm">
...
<h:commandButton id="saveButton" value="Save"
onclick="return submitSaveRequest()"/>
...
</h:form>
|
リスト 14 に、SupportForm.jsp ページの submitSaveRequest() 関数を記載します。この JavaScript 関数は、AutoSaveScript.js ファイルの submitAllForms() 関数 (連載第 1 回にコードを記載) を使ってフォーム・データをサーバーに送信します。それから submitSaveRequest() は false を返し、Save ボタンの onclick 式が false を返せるようにします。つまり、Web ブラウザーがフォームをサーバーに送信しないようにします。
リスト 14. サーバーに保存する現行ユーザー入力の送信
function submitSaveRequest() {
submitAllForms();
return false;
}
|
return キーワードが onclick 属性から削除されたり、あるいは false ではなく true が返されると、フォーム・データは 2 回送信されることになります。1 回目は Ajax を使用する submitAllForms() 関数によって、そして 2 回目は Save ボタンを通常の Submit ボタンとして扱う Web ブラウザーによってです。ブラウザーの送信によりページは更新され、おそらく検証エラーという結果になります。onclick 属性内に false が返されれば、Web ブラウザーがフォーム・データを再び送信することはないので、ユーザー入力は submitAllForms() 関数によってのみ保存され、ページの更新は行われません。
自動保存チェック・ボックスを追加する
フォーム・ページには、ユーザーが現行ページに対して自動保存機能を有効または無効に設定するためのチェック・ボックスを含めることもできます (リスト 15 を参照)。setAutoSaving() 関数を呼び出すのは、Auto-Save Form チェック・ボックスの onclick 属性内です。このチェック・ボックスは、SupportForm.jsp ファイルで <h:selectBooleanCheckbox> を使って作成します。一方、AutoSaveScript.js ファイルの setAutoSaving() 関数では、JavaScript API の setInterval() 関数を使用して Web ブラウザーに指定のミリ秒間隔で submitAllForms() を呼び出すように指示します。setAutoSaving() 関数に 0 が渡されると、自動保存機能は無効に設定されます。
リスト 15. 自動保存機能を有効、無効にするためのチェック・ボックス
<h:form id="supportForm">
...
<h:selectBooleanCheckbox id="autoSaveCheckbox"
onclick="setAutoSaving(this.checked ? autoSaveInterval : 0)"/>
<h:outputLabel value="Auto-Save Form" for="autoSaveCheckbox"/>
...
</h:form>
|
autoSaveInterval 変数は、SupportForm.jsp では 10000 に設定されています。
クライアント側を初期化する
SupportForm.jsp の <body> タグは onload 属性を使用して、Web ブラウザーがページをロードした後に init() 関数 (リスト 16 を参照) を呼び出します。この関数は、フォーム・データがリストア可能かどうかを検証してから、ユーザーに自動保存されたフォームをリストアするかどうかを確認します。ユーザーが確認ダイアログの OK ボタンをクリックすると、init() は submitRestoreRequest() を呼び出します。OK ボタンがクリックされない場合は、autoSaveCheckbox にチェック・マークが付いていれば、init() は setAutoSaving() 関数を呼び出します。
リスト 16. ユーザーの同意によるフォーム・データのリストア
var autoSaveInterval = 10000;
function init() {
if (isRestorable())
if (confirm("Do you want to restore the auto-saved form?")) {
submitRestoreRequest();
return;
}
var autoSaveCheckbox
= getFormElement("supportForm", "autoSaveCheckbox");
if (autoSaveCheckbox.checked)
setAutoSaving(autoSaveInterval);
} |
まとめ
この連載では、3 回の記事をとおして以下の手順を完了しました。
- JSF ベースの Web アプリケーションにフォームの自動保存機能を実装する
- Ajax を利用して Web ページを更新せずにフォームを保存する
- サーバー側でフォーム・データを管理する
- JSF フォームのデータをリストアする
これまでに学んだ JavaScript および JSF 手法は以下のとおりです。
- JavaScript によるフォーム・データのエンコードと送信
-
XMLHttpRequest オブジェクトの削除による Web ブラウザーでのメモリー・リークの回避
- JSF フレームワークによる Ajax リクエストの処理
- ブラウザー・セッション間で匿名ユーザーを識別するためのブラウザー ID の設定
- JSF コンポーネント・ツリーの再帰的トラバース
- JSF コンポーネントの
immediate および onclick 属性の使用
- faces コンテキストの
renderResponse() および responseComplete() メソッド呼び出し
独自の Ajax/JSF アプリケーションにフォームの自動保存機能や同様の機能を実装する際には、遠慮なく、この連載に付属のサンプル・コードを利用してください。
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| Sample application for this article | wa-aj-jsf3.zip | 9KB | HTTP |
|---|
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | |  | Andrei Cioroianu は、カスタム Java EE 開発および Ajax/JSF コンサルティング・サービスのプロバイダー、Devsphere のシニア Java 開発者兼コンサルタントです。彼への連絡には、www.devsphere.com に用意されたコンタクト・フォームを利用してください。 |
記事の評価
|