コンテキスト依存コンポーネントモデルを補完するものとして、Seamアプリケーションの特徴と なっている極度の疎結合を促進させる2つの基本概念が存在します。 最初のものは、イベントがJSFライクなメソッド結合式(method binding expression)を介して イベントリスナーへマップできるような強力なイベントモデルです。 二番目のものは、ビジネスロジックを実装するコンポーネントに対して横断的関心事 (cross-cutting concerns)を適用するためにアノテーションやインターセプタを広範囲に 使用しているということです。
Seamコンポーネントモデルはイベント駆動アプリケーション で使うために開発されました。特に、細粒度イベントモデル(fine-grained eventing model) での細粒度かつ疎結合コンポーネント開発を可能にします。 Seamでのイベントは、すでにご存知のように、いくつかのタイプがあります。
JSFイベント
jBPM状態遷移イベント
Seamページアクション
Seamコンポーネント駆動イベント
Seamコンテキスト依存イベント
これらの多様なイベントすべてはJSF ELメソッド結合式を介してSeamコンポーネント へマップされます。JSFイベントは、次のJSFテンプレートで定義されます。
<h:commandButton value="Click me!" action="#{helloWorld.sayHello}"/>jBPM遷移イベントは、jBPMプロセス定義またはページフロー定義で規定されます。
<start-page name="hello" view-id="/hello.jsp">
<transition to="hello">
<action expression="#{helloWorld.sayHello}"/>
</transition>
</start-page>JSFイベントやjPBMイベントの情報をいたるところで見つかります。 ここからは、Seamによって定義される、さらなる2種類のイベントに集中しましょう。
Seamページアクションはページのレンダリングの直前に発生するイベントです。 ページアクションはWEB-INF/pages.xmlで宣言します。 特定のJSFビューidのためのページアクションを定義することも可能です。
<pages>
<page view-id="/hello.jsp" action="#{helloWorld.sayHello}"/>
</pages>あるいは、ワイルドカードに一致するすべてのビューIDに適用するアクションを指定するようにパターンを使うことも可能です。
<pages>
<page view-id="/hello/*" action="#{helloWorld.sayHello}"/>
</pages>複数のワイルドカード化されたページアクションがカレントビューidに一致するなら、 Seamは曖昧な指定から明確な指定への順(least-specific to most-specific)で、 それらすべてのアクションを呼び出します。
ページアクションのメソッドはJSF outcomeを返すことができます。もしも、そのoutcome が非nullなら、Seamはビューをナビゲートするためその定義済みナビゲーション規則を使います。
さらに、<page>要素で指定されたビューidは、実際のJSPやFacelets に対応する必要はないのです! そこで、ページアクションを使用したStrutsやWebWorkのような 伝統的なアクション指向フレームワークの機能を再現することもできます。
TODO: translate struts action into page action
これはnon-facesリクエスト(たとえば、HTTP Getリクエスト)へのレスポンスで複雑な処理をしたいのであればとても便利です。
JSF facesリクエスト(フォーム送信)は「アクション」(メソッド結合)と「パラメータ」(入力値結合)の両方をカプセル化します。ページアクションはパラメータを取ることも可能なのです!
GETリクエストはブックマーク可能なので、ページパラメータは人間が読めるリクエスト パラメータとして引き渡されます(これはJSFフォーム入力とは異なります!)。
Seamでは、名前付きリクエストパラメータとしてモデルオブジェクトの属性を対応させる値結合が可能です。
<pages>
<page view-id="/hello.jsp" action="#{helloWorld.sayHello}">
<param name="firstName" value="#{person.firstName}"/>
<param name="lastName" value="#{person.lastName}"/>
</page>
</pages><param>宣言は双方向で、まさにJSF入力の値結合のようです。
指定されたビューidに対するnon-faces(GET)リクエストが発生するとき、 Seamは、適切な型変換を施した後に、名前付きパラメータの値をそのモデルオブジェクトに設定します。
任意の<s:link> や <s:button> は 透過的にリクエストパラメータを含みます。パラメータ値は、( <s:link>が レンダリングされるとき)レンダリングフェーズの間に値結合を評価することによって決定されます。
ビューidに対する<redirect/>の任意のナビゲーションルールは リクエストパラメータを透過的に含みます。パラメータの値はアプリケーションフェーズの 最後に値結合を評価することで決定されます。
The value is transparently propagated with any JSF form submission for the page with the given view id. (This means that view parameters behave like PAGE-scoped context variables for faces requests.
The essential idea behind all this is that however we get from any other page to /hello.jsp (or from /hello.jsp back to /hello.jsp), the value of the model attribute referred to in the value binding is "remembered", without the need for a conversation (or other server-side state).
This all sounds pretty complex, and you're probably wondering if such an exotic construct is really worth the effort. Actually, the idea is very natural once you "get it". It is definitely worth taking the time to understand this stuff. Page parameters are the most elegant way to propagate state across a non-faces request. They are especially cool for problems like search screens with bookmarkable results pages, where we would like to be able to write our application code to handle both POST and GET requests with the same code. Page parameters eliminate repetitive listing of request parameters in the view definition and make redirects much easier to code.
Note that you don't need an actual page action method binding to use a page parameter. The following is perfectly valid:
<pages>
<page view-id="/hello.jsp">
<param name="firstName" value="#{person.firstName}"/>
<param name="lastName" value="#{person.lastName}"/>
</page>
</pages>You can even specify a JSF converter:
<pages>
<page view-id="/calculator.jsp" action="#{calculator.calculate}">
<param name="x" value="#{calculator.lhs}"/>
<param name="y" value="#{calculator.rhs}"/>
<param name="op" converterId="com.my.calculator.OperatorConverter" value="#{calculator.op}"/>
</page>
</pages><pages>
<page view-id="/calculator.jsp" action="#{calculator.calculate}">
<param name="x" value="#{calculator.lhs}"/>
<param name="y" value="#{calculator.rhs}"/>
<param name="op" converter="#{operatorConverter}" value="#{calculator.op}"/>
</page>
</pages>Seamアプリケーションではfaces-config.xmlで定義される標準の JSFナビゲーション規則を使用できます。しかし、JSFナビゲーション規則は厄介な制限があります。
リダイレクトで使われるときにリクエストパラメータを指定できません。
規則から対話(conversation)の開始や終了ができません。
規則はアクションメソッドの戻り値の評価によって動作します。 つまり、任意のEL式を評価することはできません。
さらに問題なのは組合せ(orchestration)のロジックがpages.xmlと faces-config.xmlの間に分散してしまうことです。 このロジックはpages.xml側に統合した方がよいでしょう。
このJSFナビゲーション規則は、
<navigation-rule>
<from-view-id>/editDocument.xhtml</from-view-id>
<navigation-case>
<from-action>#{documentEditor.update}</from-action>
<from-outcome>success</from-outcome>
<to-view-id>/viewDocument.xhtml</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>次のように書き直すことができます。
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule if-outcome="success">
<redirect view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page>しかし、もし 文字列の値を持つ戻り値(JSF outcome)でDocumentEditorコンポーネントを汚染させる必要がなかったならば、より良かったでしょう。そこでSeamでは次のように書けるようにしています。
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}"
evaluate="#{documentEditor.errors.size}">
<rule if-outcome="0">
<redirect view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page>または、次のようにすら書くことができます。
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule if="#{documentEditor.errors.empty}">
<redirect view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page>最初の形式は後続の規則によって使用されるようにcoutcomの値を決定する値結合を評価します。 二番目のアプローチはoutcomeを無視し、各々の規則の値結合を評価します。
もちろん、更新が成功したなら、現在の対話(conversation)を終了させたいことでしょう。 これには、次のようにします。
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule if="#{documentEditor.errors.empty}">
<end-conversation/>
<redirect view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page>しかし、対話(conversation)の終了は、私たちの現在の興味の対象であるドキュメントを含む、 その対話に関連する状態を失うことにつながります。 1つの解はリダイレクトの代わりにすぐにrenderを使うことでしょう。
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule if="#{documentEditor.errors.empty}">
<end-conversation/>
<render view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page>しかし、正解はリクエストパラメータとしてドキュメントidを渡すことです。
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule if="#{documentEditor.errors.empty}">
<end-conversation/>
<redirect view-id="/viewDocument.xhtml">
<param name="documentId" value="#{documentEditor.documentId}"/>
</redirect>
</rule>
</navigation>
</page>Null outcomeはJSFでは特別なケースです。null coucomeは「そのページを再表示する」 という意味に解釈されます。次のナビゲーション規則は非nullのoutcomeに適合しますが null outcomeでは適合しないです。
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule>
<render view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page>null outcomeが発生したときにナビゲーションをしたいのであれば、 変わりに次の形式を使います。
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<render view-id="/viewDocument.xhtml"/>
</navigation>
</page>もしも、大量のページアクション、ページパラメータ、ナビゲーション規則が あるなら、それらの定義を複数のファイルに分割したいことでしょう。 ビューid/calc/calculator.jspのアクションやパラメータは calc/calculator.page.xml という名前のリソースに定義可能です。 この場合のルート要素は<page>要素で、ビューidは暗に指定されます。
<page action="#{calculator.calculate}">
<param name="x" value="#{calculator.lhs}"/>
<param name="y" value="#{calculator.rhs}"/>
<param name="op" converter="#{operatorConverter}" value="#{calculator.op}"/>
</page>Seamコンポーネント同士は互いのメソッドを呼ぶことだけでやりとりができます。 ステートフルコンポーネントはobserver/observableパターンを実装することすらできます。 しかし、コンポーネントが互いにメソッドを直接呼ぶとき、より疎結合な方法でやりとり できるために、Seamはコンポーネント駆動イベントを提供します。
イベントリスナー(observers)をcomponents.xmlに指定します。
<components>
<event type="hello">
<action expression="#{helloListener.sayHelloBack}"/>
<action expression="#{logger.logHello}"/>
</event>
</components>ここでevent type は単なる任意の文字列です。
イベントが発生するとき、そのイベント用に登録されたアクションはcomponents.xml に出現した順番で呼び出されます。コンポーネントがどのようにイベントを発行するか知りたいですか? Seamはこのために組み込みコンポーネントを提供します。
@Name("helloWorld")
public class HelloWorld {
public void sayHello() {
FacesMessages.instance().add("Hello World!");
Events.instance().raiseEvent("hello");
}
}あるいは、アノテーションを使うことも可能です。
@Name("helloWorld")
public class HelloWorld {
@RaiseEvent("hello")
public void sayHello() {
FacesMessages.instance().add("Hello World!");
}
}このイベント供給者はイベント消費者になんら依存していないことに注意してください。 そのイベントリスナーはまったく供給者と依存関係がないように実装できるのです。
@Name("helloListener")
public class HelloListener {
public void sayHelloBack() {
FacesMessages.instance().add("Hello to you too!");
}
}
@Name("helloListener")
public class HelloListener {
@Observer("hello")
public void sayHelloBack() {
FacesMessages.instance().add("Hello to you too!");
}
}この議論の中でイベントオブジェクトに関してまったく言及していなかった理由について 疑問に思われる方もいらっしゃることでしょう。Seamでは、イベント供給者とリスナーの間で 状態を伝播するのにイベントオブジェクトの必要はありません。 状態はSeamコンテキスト上に保持されて、コンポーネント間で共有されるのです。 しかしながら、もしイベントオブジェクトの引渡しを望むのであれば、それも可能です。
@Name("helloWorld")
public class HelloWorld {
private String name;
public void sayHello() {
FacesMessages.instance().add("Hello World, my name is #0.", name);
Events.instance().raiseEvent("hello", name);
}
}@Name("helloListener")
public class HelloListener {
@Observer("hello")
public void sayHelloBack(String name) {
FacesMessages.instance().add("Hello #0!", name);
}
}Seamは特殊なフレームワークの統合のためにアプリケーションが利用可能な多くの組み込みイベントを定義します。そのイベントとは次のようなものです。
org.jboss.seam.validationFailed — called when JSF validation fails
org.jboss.seam.noConversation — called when there is no long running conversation and a long running conversation is required
org.jboss.seam.preSetVariable.<name> — called when the context variable <name> is set
org.jboss.seam.postSetVariable.<name> — called when the context variable <name> is set
org.jboss.seam.preRemoveVariable.<name> — called when the context variable <name> is unset
org.jboss.seam.postRemoveVariable.<name> — called when the context variable <name> is unset
org.jboss.seam.preDestroyContext.<SCOPE> — called before the <SCOPE> context is destroyed
org.jboss.seam.postDestroyContext.<SCOPE> — called after the <SCOPE> context is destroyed
org.jboss.seam.beginConversation — called whenever a long-running conversation begins
org.jboss.seam.endConversation — called whenever a long-running conversation ends
org.jboss.seam.beginPageflow.<name> — called when the pageflow <name> begins
org.jboss.seam.endPageflow.<name> — called when the pageflow <name> ends
org.jboss.seam.createProcess.<name> — called when the process <name> is created
org.jboss.seam.endProcess.<name> — called when the process <name> ends
org.jboss.seam.initProcess.<name> — called when the process <name> is associated with the conversation
org.jboss.seam.initTask.<name> — called when the task <name> is associated with the conversation
org.jboss.seam.startTask.<name> — called when the task <name> is started
org.jboss.seam.endTask.<name> — called when the task <name> is ended
org.jboss.seam.postCreate.<name> — called when the component <name> is created
org.jboss.seam.preDestroy.<name> — called when the component <name> is destroyed
org.jboss.seam.beforePhase — called before the start of a JSF phase
org.jboss.seam.afterPhase — called after the end of a JSF phase
org.jboss.seam.postAuthenticate.<name> — called after a user is authenticated
org.jboss.seam.preAuthenticate.<name> — called before attempting to authenticate a user
org.jboss.seam.notLoggedIn — called there is no authenticated user and authentication is required
org.jboss.seam.rememberMe — occurs when Seam security detects the username in a cookie
Seamコンポーネントは、他のコンポーネント駆動イベントを観察する(observe)のとまったく同様に これらのどのイベントでも観察することが可能です。
EJB 3.0 introduced a standard interceptor model for session bean components. To add an interceptor to a bean, you need to write a class with a method annotated @AroundInvoke and annotate the bean with an @Interceptors annotation that specifies the name of the interceptor class. For example, the following interceptor checks that the user is logged in before allowing invoking an action listener method:
public class LoggedInInterceptor {
@AroundInvoke
public Object checkLoggedIn(InvocationContext invocation) throws Exception {
boolean isLoggedIn = Contexts.getSessionContext().get("loggedIn")!=null;
if (isLoggedIn) {
//the user is already logged in
return invocation.proceed();
}
else {
//the user is not logged in, fwd to login page
return "login";
}
}
}To apply this interceptor to a session bean which acts as an action listener, we must annotate the session bean @Interceptors(LoggedInInterceptor.class). This is a somewhat ugly annotation. Seam builds upon the interceptor framework in EJB3 by allowing you to use @Interceptors as a meta-annotation. In our example, we would create an @LoggedIn annotation, as follows:
@Target(TYPE)
@Retention(RUNTIME)
@Interceptors(LoggedInInterceptor.class)
public @interface LoggedIn {}We can now simply annotate our action listener bean with @LoggedIn to apply the interceptor.
@Stateless
@Name("changePasswordAction")
@LoggedIn
@Interceptors(SeamInterceptor.class)
public class ChangePasswordAction implements ChangePassword {
...
public String changePassword() { ... }
}If interceptor ordering is important (it usually is), you can add @Interceptor annotations to your interceptor classes to specify a partial order of interceptors.
@Interceptor(around={BijectionInterceptor.class,
ValidationInterceptor.class,
ConversationInterceptor.class},
within=RemoveInterceptor.class)
public class LoggedInInterceptor
{
...
}You can even have a "client-side" interceptor, that runs around any of the built-in functionality of EJB3:
@Interceptor(type=CLIENT)
public class LoggedInInterceptor
{
...
}EJB interceptors are stateful, with a lifecycle that is the same as the component they intercept. For interceptors which do not need to maintain state, Seam lets you get a performance optimization by specifying @Interceptor(stateless=true).
Much of the functionality of Seam is implemented as a set of built-in Seam interceptors, including the interceptors named in the previous example. You don't have to explicitly specify these interceptors by annotating your components; they exist for all interceptable Seam components.
You can even use Seam interceptors with JavaBean components, not just EJB3 beans!
EJB defines interception not only for business methods (using @AroundInvoke), but also for the lifecycle methods @PostConstruct, @PreDestroy, @PrePassivate and @PostActive. Seam supports all these lifecycle methods on both component and interceptor not only for EJB3 beans, but also for JavaBean components (except @PreDestroy which is not meaningful for JavaBean components).
JSF is surprisingly limited when it comes to exception handling. As a partial workaround for this problem, Seam lets you define how a particular class of exception is to be treated by annotating the exception class, or declaring the exception class in an XML file. This facility is meant to be combined with the EJB 3.0-standard @ApplicationException annotation which specifies whether the exception should cause a transaction rollback.
EJB specifies well-defined rules that let us control whether an exception immediately marks the current transaction for rollback when it is thrown by a business method of the bean: system exceptions always cause a transaction rollback, application exceptions do not cause a rollback by default, but they do if @ApplicationException(rollback=true) is specified. (An application exception is any checked exception, or any unchecked exception annotated @ApplicationException. A system exception is any unchecked exception without an @ApplicationException annotation.)
Note that there is a difference between marking a transaction for rollback, and actually rolling it back. The exception rules say that the transaction should be marked rollback only, but it may still be active after the exception is thrown.
Seam applies the EJB 3.0 exception rollback rules also to Seam JavaBean components.
But these rules only apply in the Seam component layer. What about an exception that is uncaught and propagates out of the Seam component layer, and out of the JSF layer? Well, it is always wrong to leave a dangling transaction open, so Seam rolls back any active transaction when an exception occurs and is uncaught in the Seam component layer.
To enable Seam's exception handling, we need to make sure we have the master servlet filter declared in web.xml:
<filter>
<filter-name>Seam Filter</filter-name>
<filter-class>org.jboss.seam.servlet.SeamFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>Seam Filter</filter-name>
<url-pattern>*.seam</url-pattern>
</filter-mapping>You may also need to disable Facelets development mode in web.xml and Seam debug mode in components.xml if you want your exception handlers to fire.
The following exception results in a HTTP 404 error whenever it propagates out of the Seam component layer. It does not roll back the current transaction immediately when thrown, but the transaction will be rolled back if it the exception is not caught by another Seam component.
@HttpError(errorCode=404)
public class ApplicationException extends Exception { ... }This exception results in a browser redirect whenever it propagates out of the Seam component layer. It also ends the current conversation. It causes an immediate rollback of the current transaction.
@Redirect(viewId="/failure.xhtml", end=true)
@ApplicationException(rollback=true)
public class UnrecoverableApplicationException extends RuntimeException { ... }Note that @Redirect does not work for exceptions which occur during the render phase of the JSF lifecycle.
This exception results in a redirect, along with a message to the user, when it propagates out of the Seam component layer. It also immediately rolls back the current transaction.
@Redirect(viewId="/error.xhtml", message="Unexpected error")
public class SystemException extends RuntimeException { ... }Since we can't add annotations to all the exception classes we are interested in, Seam also lets us specify this functionality in pages.xml.
<pages>
<exception class="javax.persistence.EntityNotFoundException">
<http-error error-code="404"/>
</exception>
<exception class="javax.persistence.PersistenceException">
<end-conversation/>
<redirect view-id="/error.xhtml">
<message>Database access failed</message>
</redirect>
</exception>
<exception>
<end-conversation/>
<redirect view-id="/error.xhtml">
<message>Unexpected failure</message>
</redirect>
</exception>
</pages>The last <exception> declaration does not specify a class, and is a catch-all for any exception for which handling is not otherwise specified via annotations or in pages.xml.
You can also access the handled exception instance through EL, Seam places it in the conversation context, e.g. to access the message of the exception:
...
throw new AuthorizationException("You are not allowed to do this!");
<pages>
<exception class="org.jboss.seam.security.AuthorizationException">
<end-conversation/>
<redirect view-id="/error.xhtml">
<message severity="WARN">#{handledException.message}</message>
</redirect>
</exception>
</pages>