Incorporated documentation material on the shippingrate and numberguess sample applications.
This commit is contained in:
@@ -11,7 +11,29 @@
|
||||
<para>
|
||||
<orderedlist>
|
||||
<listitem>
|
||||
<para>Phonebook - the original sample demonstrating most features (including subflows).</para>
|
||||
<para>
|
||||
<ulink url="http://www.ervacon.com/products/swf/intro/index.html">Phonebook</ulink> - the original sample demonstrating most features (including subflows).
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<link linkend="fileupload-sample">Fileupload</link> - demonstrates multipart file upload.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<link linkend="birthdate-sample">Birthdate</link> - demonstrates Struts integration and the MultiAction.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<link linkend="shippingrate-sample">Shippingrate</link> - demonstrates Spring Web Flow together with Ajax technology.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<link linkend="numberguess-sample">NumberGuess</link> - demonstrates use of stateful middle-tier components to carry out business logic.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>Sellitem - demonstrates a wizard with conditional transitions, flow scope, flow execution redirects, and continuations.</para>
|
||||
@@ -22,22 +44,6 @@
|
||||
<listitem>
|
||||
<para>Itemlist - demonstrates REST-style URLs and inline flows.</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>Shippingrate - demonstrates Spring Web Flow together with Ajax technology.</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>NumberGuess - demonstrates use of stateful middle-tier components to carry out business logic.</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<link linkend="birthdate-sample">Birthdate</link> - demonstrates Struts integration and the MultiAction.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<link linkend="fileupload-sample">Fileupload</link> - demonstrates multipart file upload.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>Phonebook-Portlet - the phonebook sample in a Portlet environment (notice how the flow definitions do not change).</para>
|
||||
</listitem>
|
||||
@@ -504,4 +510,465 @@ public BirthDateFormAction() {
|
||||
</para>
|
||||
</sect2>
|
||||
</sect1>
|
||||
<sect1 id="shippingrate-sample">
|
||||
<title>Shippingrate Example</title>
|
||||
<sect2>
|
||||
<title>Overview</title>
|
||||
<para>
|
||||
The Shippingrate sample demonstrates the use of Spring Web Flow in combination with
|
||||
Ajaxian techniques. It consists of several wizard-style steps executed
|
||||
with Ajax requests and refreshing a portion of the page.
|
||||
The input is collected from the user in incremental steps. It is stored
|
||||
in a flow-scoped object and is then used to calcualte a shipping rate.
|
||||
The example also demonstrates invocation of a service-layer bean
|
||||
defined in a Spring context to perform calculations and
|
||||
to provide reference data such as countries and package types.
|
||||
</para>
|
||||
</sect2>
|
||||
<sect2>
|
||||
<title>Web.xml</title>
|
||||
<para>
|
||||
The web.xml configuration maps requests for "*.htm" to the
|
||||
shippingrate servlet - a regular Spring MVC DispatcherServlet:
|
||||
<programlisting>
|
||||
<servlet>
|
||||
<servlet-name>shippingrate</servlet-name>
|
||||
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>shippingrate</servlet-name>
|
||||
<url-pattern>*.htm</url-pattern>
|
||||
</servlet-mapping>
|
||||
</programlisting>
|
||||
The web.xml also ensures the following Spring context file is loaded
|
||||
at runtime from the web application classpath:
|
||||
<programlisting>
|
||||
<context-param>
|
||||
<param-name>contextConfigLocation</param-name>
|
||||
<param-value>
|
||||
classpath:org/springframework/webflow/samples/shippingrate/domain/services.xml
|
||||
</param-value>
|
||||
</context-param>
|
||||
</programlisting>
|
||||
The services.xml Spring context defines a "rateService" bean providing
|
||||
operations for making shipping rate calculations and for retrieving
|
||||
reference data required for display in the JSP pages of the application.
|
||||
</para>
|
||||
</sect2>
|
||||
<sect2>
|
||||
<title>Spring MVC Context</title>
|
||||
<para>
|
||||
The Spring MVC servlet context for the shippingrate servlet (WEB-INF/shippingrate-servlet.xml)
|
||||
defines one controller bean:
|
||||
<programlisting>
|
||||
<bean name="/rates.htm" class="org.springframework.webflow.executor.mvc.FlowController">
|
||||
<property name="flowExecutor" ref="flowExecutor" />
|
||||
</bean>
|
||||
</programlisting>
|
||||
FlowController is a Web Flow controller. It is the main point of integration between Spring MVC
|
||||
and Spring Web Flow routing requests to one or more managed web flow executions. The
|
||||
FlowController is injected with flowExecutor and flowRegistry beans:
|
||||
<programlisting>
|
||||
<!-- Launches new flow executions and resumes existing executions. -->
|
||||
<flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="simple"/>
|
||||
|
||||
<!-- Creates the registry of flow definitions for this application -->
|
||||
<flow:registry id="flowRegistry">
|
||||
<flow:location path="/WEB-INF/flows/**/*-flow.xml" />
|
||||
</flow:registry>
|
||||
</programlisting>
|
||||
The flowExecutor and the flowRegistry beans collectively configure
|
||||
the FlowController with one web flow - the getRate-flow defined in
|
||||
/WEB-INF/flows/getRate-flow.xml. The flowExecutor uses a "simple"
|
||||
repository, which manages execution state in the user session.
|
||||
</para>
|
||||
<para>
|
||||
Given the above definitions the following URI can be used to initiate
|
||||
the getRate-flow:
|
||||
<programlisting>
|
||||
/swf-shippingrate/rates.htm?_flowId=getRate-flow
|
||||
</programlisting>
|
||||
</para>
|
||||
</sect2>
|
||||
<sect2>
|
||||
<title>Ajax Requests</title>
|
||||
<para>
|
||||
The shippingrate example consists of several wizard-style steps.
|
||||
After the initial index.jsp subsequent pages are
|
||||
loaded in an Ajax manner without reloading the entire page.
|
||||
</para>
|
||||
<warning>
|
||||
<para>
|
||||
This sample has been tested successfully on Internet Explorer 6 and 7,
|
||||
Firefox 2.0, and Safari 2. There are known Javascript issues
|
||||
with use on Firefox 1.5.
|
||||
</para>
|
||||
</warning>
|
||||
<para>
|
||||
The Ajax requests are done with the help of the
|
||||
<ulink url="http://prototype.conio.net">Prototype</ulink>
|
||||
framework and a thin JavaScript layer over it providing
|
||||
convenient functions for processing Ajax form and get requests.
|
||||
The required Javascript libraries are included in index.jsp as follows:
|
||||
<programlisting>
|
||||
<script src="prototype.js" type="text/javascript"></script>
|
||||
<script src="swf_ajax.js" type="text/javascript"></script>
|
||||
</programlisting>
|
||||
</para>
|
||||
<para>
|
||||
When index.jsp is loaded the following JavaScript invokes the getRate-flow
|
||||
and replaces the content of the getRateWizard div tag with the response
|
||||
returned from the server:
|
||||
<programlisting>
|
||||
<div id="getRateWizard">
|
||||
<script type="text/javascript">
|
||||
window.onload = function() {
|
||||
new SimpleRequest('getRateWizard', 'rates.htm', 'get', '_flowId=getRate-flow');
|
||||
};
|
||||
</script>
|
||||
</div>
|
||||
</programlisting>
|
||||
Functions are first-class citizens and a type in JavaScript.
|
||||
The script above creates an instance of
|
||||
the SimpleRequest function defined in swf_ajax.js. This function invokes
|
||||
Prototype's Ajax.Updater with the specified URL and request parameters. On success
|
||||
the content of the getRateWizard div is replaced with the response returned
|
||||
from the server. On failure such as an HTTP response code other 200 (OK)
|
||||
an error message is displayed.
|
||||
</para>
|
||||
<para>
|
||||
The next few pages are form-based JSP's - selectCustomer.jsp, selectReceiver.jsp,
|
||||
etc. Each of them contains the following JavaScript call at the bottom:
|
||||
<programlisting>
|
||||
<script type="text/javascript">
|
||||
formRequest('selectCustomerTypeForm');
|
||||
</script>
|
||||
</programlisting>
|
||||
The formRequest function is also defined in swf_ajax.js
|
||||
and it uses Prototype to register a handler for the form submit event:
|
||||
<programlisting>
|
||||
function formRequest(formElementId) {
|
||||
Event.observe(formElementId, 'submit', handleSubmitEvent, true);
|
||||
}
|
||||
</programlisting>
|
||||
The handleSubmitEvent function extracts the form parameters, stops the
|
||||
submit event, and posts an AJAX request via XMLHttpRequest. On success
|
||||
the results returned form the server replace the content of the form.
|
||||
On failure such as an HTTP response code other 200 (OK) an error
|
||||
message is displayed.
|
||||
</para>
|
||||
<para>
|
||||
Although not demonstrated in this example a back button can be
|
||||
implemented in parallel with the Next button used to advance from
|
||||
one screen to the next. This would be necessary because the browser
|
||||
back button - a common issue in Ajax applications, contrary to user
|
||||
expectation returns to the page prior to the first Ajax request.
|
||||
</para>
|
||||
<para>
|
||||
As a result of the Ajax requests the entire wizard is able to
|
||||
function within a portion of the page without refresing
|
||||
the remaining information on it.
|
||||
</para>
|
||||
</sect2>
|
||||
<sect2>
|
||||
<title>getRate Web Flow</title>
|
||||
<para>
|
||||
The getRate-flow (/WEB-INF/jsp/flows/getRate-flow.xml) defines the following start state:
|
||||
<programlisting>
|
||||
<view-state id="selectCustomerType" view="selectCustomer">
|
||||
<transition on="submit" to="selectSender">
|
||||
<action bean="formAction" method="bind" />
|
||||
</transition>
|
||||
</view-state>
|
||||
</programlisting>
|
||||
This is a view state, which will display the initial form using the
|
||||
JSP page /WEB-INF/jsp/selectCustomer.jsp. Notice, the use of a start action
|
||||
executed immediately before the JSP is displayed:
|
||||
<programlisting>
|
||||
<start-actions>
|
||||
<action bean="formAction" method="setupForm" />
|
||||
</start-actions>
|
||||
</programlisting>
|
||||
The "formAction" bean is defined in the Spring servlet context
|
||||
(/WEB-INF/shippingrate-servlet.xml). It specifies a form object
|
||||
and a validator to use for form data binding and validation:
|
||||
<programlisting>
|
||||
<!-- Performs "form backing object" data binding and validation on input submit -->
|
||||
<bean id="formAction" class="org.springframework.webflow.action.FormAction">
|
||||
<property name="formObjectName" value="rateCriteria" />
|
||||
<property name="formObjectClass" value="org.springframework.webflow.samples.shippingrate.domain.RateCriteria" />
|
||||
<property name="formObjectScope" value="FLOW" />
|
||||
<property name="validator">
|
||||
<bean class="org.springframework.webflow.samples.shippingrate.domain.RateCriteriaValidator" />
|
||||
</property>
|
||||
</bean>
|
||||
</programlisting>
|
||||
The form object of type RateCriteria will be used to collect data
|
||||
from the user in several steps. The form object will be stored in FLOW scope
|
||||
and will not be re-created with each request as long
|
||||
as the flow hasn't reached its end state. The actual binding of
|
||||
html form fields to the RateCriteria object is based on
|
||||
Spring's data binding mechanism. Html form fields are surrounded
|
||||
with the <spring:bind> tag containing the path
|
||||
nested property field. FormAction's bindAndValidate method
|
||||
will initiate the actual binding on the server side
|
||||
between HTTP request parameters and RateCriteria data fields.
|
||||
</para>
|
||||
<para>
|
||||
When the selectCustomer.jsp submits back to the FlowController via
|
||||
"/swf-shippingrate/rate.htm" it uses a submit button named
|
||||
"_eventId_submit". This indicates to Web Flow a transition to
|
||||
the "selectSender" view state. This view state is defined as follows:
|
||||
<programlisting>
|
||||
<view-state id="selectSender" view="selectSender">
|
||||
<render-actions>
|
||||
<bean-action bean="rateService" method="getCountries">
|
||||
<method-result name="countries" />
|
||||
</bean-action>
|
||||
</render-actions>
|
||||
<transition on="submit" to="selectReceiver">
|
||||
<action bean="formAction" method="bindAndValidate">
|
||||
<attribute name="validatorMethod" value="validateSender" />
|
||||
</action>
|
||||
</transition>
|
||||
</view-state>
|
||||
</programlisting>
|
||||
The selectSender view state has a render action:
|
||||
the "rateService" bean that was loaded through the services.xml context referenced
|
||||
in web.xml. The purpose of the render action is to load data required
|
||||
to render the JSP. In this case the rateService bean has a method called
|
||||
getCountries that returns a list of countries to be displayed in a drop-down
|
||||
by the JSP.
|
||||
</para>
|
||||
<para>
|
||||
The "selectSender" view state also defines one transition: on event with
|
||||
id of "submit" a transition to the "selectReceiver" view state occurs.
|
||||
A pre-requisite for the transition to occur is the successful completion of
|
||||
formAction bean's bindAndValidate method. The attribute "validatorMethod" on
|
||||
the bean specifies the name of the method to invoke on the Validator object
|
||||
specifically for the fields of the current screen.
|
||||
If the bindAndValidate method does not succeed the transition does not take
|
||||
place and the flow remains in the "selectSender" view
|
||||
state where the user can review the errors and modify the selection.
|
||||
</para>
|
||||
<para>
|
||||
The next two states in the flow - selectReceiver and selectPackageDetails use similar
|
||||
mechnisms. The rateSevice bean is used to retrieve countries and package types for
|
||||
use in the JSP. The form backing object RateCriteria stored in FLOW scope
|
||||
is used to collect user input with each form submit.
|
||||
</para>
|
||||
<para>
|
||||
The "findRate" action state occurs after all user input has been provided.
|
||||
It is defined as follows:
|
||||
<programlisting>
|
||||
<action-state id="findRate">
|
||||
<bean-action bean="rateService" method="getRate">
|
||||
<method-arguments>
|
||||
<argument expression="flowScope.rateCriteria" />
|
||||
</method-arguments>
|
||||
<method-result name="rate" />
|
||||
</bean-action>
|
||||
<transition on="success" to="showRate" />
|
||||
</action-state>
|
||||
</programlisting>
|
||||
Logic for the action state is provided by the getRate method of
|
||||
the rateService bean. The RateCriteria object stored in FLOW scope
|
||||
and containing the user input is passed to the rateService bean.
|
||||
The result of the method is exposed in request scope under
|
||||
the name "rate".
|
||||
</para>
|
||||
<para>
|
||||
The next and final state "showRate" is a JSP page, which accesses the calculated rate
|
||||
information and displays it to the user.
|
||||
</para>
|
||||
</sect2>
|
||||
</sect1>
|
||||
<sect1 id="numberguess-sample">
|
||||
<title>Numberguess Example</title>
|
||||
<sect2>
|
||||
<title>Overview</title>
|
||||
<para>
|
||||
Numberguess uses Web Flow to implement two number guessing games.
|
||||
For each game the user can enter multiple guesses and depending
|
||||
on the answer either transition back to the same screen or
|
||||
advance to the final screen. Logic for the guessing games is
|
||||
provided through FLOW-scoped beans, which also maintain state
|
||||
such as the total number of guesses. The example defines transitions
|
||||
using event pattern matching and custom exception handlers.
|
||||
</para>
|
||||
</sect2>
|
||||
<sect2>
|
||||
<title>Web.xml</title>
|
||||
<para>
|
||||
The web.xml configuration maps "*.htm" requests to the numberguess servlet -
|
||||
a regular Spring MVC DispatcherServlet:
|
||||
<programlisting>
|
||||
<servlet>
|
||||
<servlet-name>numberguess</servlet-name>
|
||||
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
|
||||
<init-param>
|
||||
<param-name>contextConfigLocation</param-name>
|
||||
<param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
|
||||
</init-param>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>numberguess</servlet-name>
|
||||
<url-pattern>*.htm</url-pattern>
|
||||
</servlet-mapping>
|
||||
</programlisting>
|
||||
The Spring web context is loaded from a file called
|
||||
/WEB-INF/dispatcher-servlet.xml.
|
||||
</para>
|
||||
</sect2>
|
||||
<sect2>
|
||||
<title>Spring MVC Context</title>
|
||||
<para>
|
||||
The Spring MVC web context (WEB-INF/dispatcher-servlet.xml)
|
||||
defines one controller bean:
|
||||
<programlisting>
|
||||
<bean name="/play.htm" class="org.springframework.webflow.executor.mvc.FlowController">
|
||||
<property name="flowExecutor" ref="flowExecutor" />
|
||||
</bean>
|
||||
</programlisting>
|
||||
FlowController is a Web Flow controller. It is the main point of
|
||||
integration between Spring MVC and Spring Web Flow routing requests
|
||||
to one or more managed web flow executions. The FlowController is
|
||||
injected with flowExecutor and flowRegistry beans:
|
||||
<programlisting>
|
||||
<!-- Launches new flow executions and resumes existing executions. -->
|
||||
<flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="singlekey"/>
|
||||
|
||||
<!-- Creates the registry of flow definitions for this application -->
|
||||
<flow:registry id="flowRegistry">
|
||||
<flow:location path="/WEB-INF/higherlower.xml" />
|
||||
<flow:location path="/WEB-INF/mastermind.xml" />
|
||||
</flow:registry>
|
||||
</programlisting>
|
||||
The flowExecutor and the flowRegistry beans collectively configure
|
||||
the FlowController with two web flows - higherlower and mastermind.
|
||||
This flowExecutor is configured with a simple repository that assigns
|
||||
a single flow execution key per conversation. The key, once assigned,
|
||||
never changes for the duration of the conversation.
|
||||
</para>
|
||||
<para>
|
||||
Given the above definitions the following URI's can be used to initiate
|
||||
each of the two flows:
|
||||
<programlisting>
|
||||
/swf-numberguess/play.htm?_flowId=higherlower
|
||||
/swf-numberguess/play.htm?_flowId=mastermind
|
||||
</programlisting>
|
||||
</para>
|
||||
<para>
|
||||
The Spring MVC servlet context also defines a view resolver bean for
|
||||
resolving logical view names. In general Web Flow does not aim
|
||||
to replace the flexibility of Spring MVC for view resolution.
|
||||
It focuses on the C in MVC.
|
||||
</para>
|
||||
</sect2>
|
||||
<sect2>
|
||||
<title>Higherlower Flow</title>
|
||||
<para>
|
||||
The Higherlower flow (/WEB-INF/higherlower.xml) starts with the following
|
||||
flow variable declaration:
|
||||
<programlisting>
|
||||
<var name="game" class="org.springframework.webflow.samples.numberguess.HigherLowerGame"/>
|
||||
</programlisting>
|
||||
This variable is automatically created when an execution of the flow
|
||||
begins and will exist in FLOW scope throughout its duration.
|
||||
</para>
|
||||
<para>
|
||||
The start state for the flow is defined as follows:
|
||||
<programlisting>
|
||||
<view-state id="enterGuess" view="higherlower.enterGuess">
|
||||
<transition on="submit" to="makeGuess"/>
|
||||
</view-state>
|
||||
</programlisting>
|
||||
The view resolver bean of Spring MVC will resolve "higherlower.enterGuess"
|
||||
to /WEB-INF/jsp/higherlower.enterGuess.jsp.
|
||||
This JSP has a form with one input field for the guess number.
|
||||
The "game" variable referenced throughout the JSP
|
||||
is the FLOW-scoped variable that was declared at the top of
|
||||
the flow definition.
|
||||
</para>
|
||||
<para>
|
||||
The name of the form submit button "_eventId_submit" indicates the
|
||||
event id to use for deciding where to transition to next. Given an
|
||||
event with id of "submit" the "enterGuess" view state transitions
|
||||
to the "makeGuess" action state defined as follows:
|
||||
<programlisting>
|
||||
<action-state id="makeGuess">
|
||||
<evaluate-action expression="flowScope.game.makeGuess(requestParameters.guess)">
|
||||
<evaluation-result name="guessResult"/>
|
||||
</evaluate-action>
|
||||
<transition on="CORRECT" to="showAnswer"/>
|
||||
<transition on="*" to="enterGuess"/>
|
||||
<transition on-exception="java.lang.NumberFormatException" to="enterGuess"/>
|
||||
</action-state>
|
||||
</programlisting>
|
||||
</para>
|
||||
<para>
|
||||
The makeGuess action state consists of one evaluate action and three
|
||||
transitions. Evaluate actions are used to invoke logic encapsulated
|
||||
in a FLOW-scoped object - in this case the game bean.
|
||||
The makeGuess method of the game bean returns one of several enum
|
||||
values it defines:
|
||||
<programlisting>
|
||||
enum GuessResult {
|
||||
TOO_HIGH, TOO_LOW, CORRECT, INVALID
|
||||
}
|
||||
</programlisting>
|
||||
Web Flow detects the returned result from the makeGuess method
|
||||
is a JDK 1.5 enum type and
|
||||
creates an Event with a String id matching the enum value. If the
|
||||
makeGuess method returns CORRECT a transition to the final
|
||||
showAnswer state occurs. For any other event (defined with the event
|
||||
pattern on="*") Web Flow returns to the enterGuess
|
||||
state. The makeGuess state also defines one on-exception transition
|
||||
demonstrating how specific Exceptions can be incorporated into
|
||||
flow transition logic.
|
||||
</para>
|
||||
<para>
|
||||
The end-state showAnswer resolves to the JSP page
|
||||
/WEB-INF/jsp/higherlower.showAnswer.jsp, which simply shows the
|
||||
correct guess. At this point the flow has ended and the "game" bean
|
||||
is no longer in scope.
|
||||
</para>
|
||||
</sect2>
|
||||
<sect2>
|
||||
<title>Mastermind Flow</title>
|
||||
<para>
|
||||
The mastermind flow uses a similar flow definition to implement a 4-digit
|
||||
guessing game:
|
||||
<programlisting>
|
||||
<var name="game" class="org.springframework.webflow.samples.numberguess.MastermindGame"/>
|
||||
|
||||
<start-state idref="enterGuess"/>
|
||||
|
||||
<view-state id="enterGuess" view="mastermind.enterGuess">
|
||||
<transition on="submit" to="makeGuess"/>
|
||||
</view-state>
|
||||
|
||||
<action-state id="makeGuess">
|
||||
<evaluate-action expression="flowScope.game.makeGuess(requestParameters.guess)">
|
||||
<evaluation-result name="guessResult"/>
|
||||
</evaluate-action>
|
||||
<transition on="CORRECT" to="showAnswer"/>
|
||||
<transition on="*" to="enterGuess"/>
|
||||
</action-state>
|
||||
|
||||
<end-state id="showAnswer" view="mastermind.showAnswer"/>
|
||||
</programlisting>
|
||||
The MastermindGame class encapsulates the logic for the game and
|
||||
is stored as a FLOW-scoped bean.
|
||||
It returns one of three possible enum values -
|
||||
WRONG, CORRECT, or INVALID, which Web Flow converts to events with
|
||||
id's matching the enum values. If the guess is INVALID the JSP page
|
||||
/WEB-INF/jsp/mastermind.enterGuess.jsp will print an error message.
|
||||
If the guess is CORRECT the flow will transition to the showAnswer
|
||||
end state and complete the flow.
|
||||
</para>
|
||||
</sect2>
|
||||
</sect1>
|
||||
</chapter>
|
||||
|
||||
Reference in New Issue
Block a user