487 lines
25 KiB
XML
487 lines
25 KiB
XML
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!--
|
|
/*
|
|
* Copyright 2002-2010 the original author or authors.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
-->
|
|
<chapter xml:id="remoting" xmlns="http://docbook.org/ns/docbook" version="5">
|
|
<title>.NET Remoting</title>
|
|
|
|
<section xml:id="remoting-introduction">
|
|
<title>Introduction</title>
|
|
|
|
<para>Spring's .NET Remoting support allows you to export a 'plain CLR
|
|
object' as a .NET Remoted object. By "plain CLR object" we mean classes
|
|
that do not inherit from a specific infrastructure base class such as
|
|
MarshalByRefObject. On the server side, Spring's .NET Remoting exporters
|
|
will automatically create a proxy that implements MarshalByRefObject. You
|
|
register SAO types as either SingleCall or Singleton and also configure on
|
|
a per-object basis lifetime and leasing parameters. On the client side you
|
|
can obtain CAO references to server proxy objects in a manner that
|
|
promotes interface based design best practices when developing .NET
|
|
remoting applications. The current implementation requires that your plain
|
|
.NET objects implements a business service interface. Additionally you can
|
|
add AOP advice to both SAO and CAO objects.</para>
|
|
|
|
<para>You can leverage the IoC container to configure the exporter and
|
|
service endpoints. A remoting specific xml-schema is provided to simplify
|
|
the remoting configuration but you can still use the standard
|
|
reflection-like property based configuration schema. You may also opt to
|
|
not use the IoC container to configure the objects and use Spring's .NET
|
|
Remoting classes Programatically, as you would with any third party
|
|
library.</para>
|
|
|
|
<para>A sample application, often referred to in this documentation, is in
|
|
the distribution under the directory "examples\Spring\Spring.Calculator"
|
|
and may also be found via the start menu by selecting the 'Calculator'
|
|
item.</para>
|
|
</section>
|
|
|
|
<section xml:id="remoting-publishsao">
|
|
<title>Publishing SAOs on the Server</title>
|
|
|
|
<para>Exposing a Singleton SAO service can be done in two ways. The first
|
|
is through programmatic or administrative type registration that makes
|
|
calls to
|
|
<literal>RemotingConfiguration.RegisterWellKnownServiceType</literal>.
|
|
This method has the limitation that you must use a default constructor and
|
|
you can not easily configure the singleton state at runtime since it is
|
|
created on demand. The second way is to publish an object instance using
|
|
<literal>RemotingServices.Marshal</literal>. This method overcomes the
|
|
limitations of the first method. Example server side code for publishing
|
|
an SAO singleton object with a predefined state is shown below
|
|
<programlisting language="csharp">AdvancedMBRCalculator calc = new AdvancedMBRCalculator(217);
|
|
RemotingServices.Marshal(calc, "MyRemotedCalculator");</programlisting></para>
|
|
|
|
<para>The class AdvancedMBRCalculator used above inherits from
|
|
MarshalByRefObject.</para>
|
|
|
|
<para>If your design calls for configuring a singleton SAO, or using a
|
|
non-default constructor, you can use the Spring IoC container to create
|
|
the SAO instance, configure it, and register it with the .NET remoting
|
|
infrastructure. The <literal>SaoExporter</literal> class performs this
|
|
task and most importantly, will automatically create a proxy class that
|
|
inherits from MarshalbyRefObject if your business object does not already
|
|
do so. The following XML taken from the <link
|
|
linkend="remoting-quickstart">Remoting QuickStart</link> demonstrates its
|
|
usage to an SAO Singleton object</para>
|
|
|
|
<section xml:id="sao-singleton">
|
|
<title>SAO Singleton</title>
|
|
</section>
|
|
|
|
<programlisting language="myxml"><object id="singletonCalculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services">
|
|
<constructor-arg type="int" value="217"/>
|
|
</object>
|
|
|
|
<!-- Registers the calculator service as a SAO in 'Singleton' mode. -->
|
|
<object name="saoSingletonCalculator" type="Spring.Remoting.SaoExporter, Spring.Services">
|
|
<property name="TargetName" value="singletonCalculator" />
|
|
<property name="ServiceName" value="RemotedSaoSingletonCalculator" />
|
|
</object></programlisting>
|
|
|
|
<para>This XML fragment shows how an existing object "singletonCalculator"
|
|
defined in the Spring context is exposed under the url-path name
|
|
"RemotedSaoSingletonCalculator". (The fully qualified url is
|
|
tcp://localhost:8005/RemotedSaoSingleCallCalculator using the standard
|
|
.NET channel configuration shown further below.)
|
|
<literal>AdvancedCalculator</literal> class implements the business
|
|
interface <literal>IAdvancedCalculator</literal>. The current proxy
|
|
implementation requires that your business objects implement an interface.
|
|
The interfaces' methods will be the ones exposed in the generated .NET
|
|
remoting proxy. The initial memory of the calculator is set to 217 via the
|
|
constructor. The class <literal>AdvancedCalculator</literal>
|
|
<emphasis>does not</emphasis> inherit from
|
|
<literal>MarshalByRefObject</literal>. Also note that the exporter
|
|
sets the lifetime of the SAO Singleton to infinite so that the singleton
|
|
will not be garbage collected after 5 minutes (the .NET default lease
|
|
time). If you would like to vary the lifetime properties, they are
|
|
InitialLeaseTime, RenewOnCallTime, and SponsorshipTimeout.</para>
|
|
|
|
<para>A custom schema is provided to make the object declaration even
|
|
easier and with intellisense support for the attributes. This is shown
|
|
below<programlisting language="myxml"><objects xmlns="http://www.springframework.net"
|
|
xmlns:r="http://www.springframework.net/remoting">
|
|
|
|
<r:saoExporter targetName="singletonCalculator"
|
|
serviceName="RemotedSaoSingletonCalculator" />
|
|
|
|
... other object definitions
|
|
|
|
</objects></programlisting>Refer to the end of this chapter for more
|
|
information on Spring's .NET custom schema.</para>
|
|
|
|
<section xml:id="sao-singlecall">
|
|
<title>SAO SingleCall</title>
|
|
</section>
|
|
|
|
<para>The following XML fragment shows how to expose the calculator
|
|
service in SAO 'SingleCall' mode. <programlisting language="myxml"><object id="prototypeCalculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"
|
|
singleton="false">
|
|
<constructor-arg type="int" value="217"/>
|
|
</object>
|
|
|
|
<object name="saoSingleCallCalculator" type="Spring.Remoting.SaoExporter, Spring.Services">
|
|
<property name="TargetName" value="prototypeCalculator" />
|
|
<property name="ServiceName" value="RemotedSaoSingleCallCalculator" />
|
|
</object></programlisting></para>
|
|
|
|
<para>Note that we change the singleton attribute of the plain CLR object
|
|
as configured by Spring in the <object> definition and not an
|
|
attribute on the SaoExporter. The object referred to in the
|
|
<literal>TargetName</literal> parameter can be an AOP proxy to a business
|
|
object. For example, if we were to apply some simple logging advice to the
|
|
singleton calculator, the following standard AOP configuration is used to
|
|
create the target for the SaoExporter</para>
|
|
|
|
<programlisting language="myxml"><object id="singletonCalculatorWeaved" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">
|
|
<property name="target" ref="singletonCalculator"/>
|
|
<property name="interceptorNames">
|
|
<list>
|
|
<value>Log4NetLoggingAroundAdvice</value>
|
|
</list>
|
|
</property>
|
|
</object>
|
|
|
|
<object name="saoSingletonCalculatorWeaved" type="Spring.Remoting.SaoExporter, Spring.Services">
|
|
<property name="TargetName" value="singletonCalculatorWeaved" />
|
|
<property name="ServiceName" value="RemotedSaoSingletonCalculatorWeaved" />
|
|
</object></programlisting>
|
|
|
|
<note>As generally required with a .NET Remoting application, the
|
|
arguments to your service methods should be Serializable.</note>
|
|
|
|
<sect2 xml:id="remoting-configuration">
|
|
<title>Console Application Configuration</title>
|
|
|
|
<para>When using <literal>SaoExporter</literal> you can still use
|
|
the standard remoting administration section in the application
|
|
configuration file to register the channel.
|
|
<literal>ChannelServices</literal> as shown below</para>
|
|
|
|
<programlisting language="myxml"><system.runtime.remoting>
|
|
<application>
|
|
<channels>
|
|
<channel ref="tcp" port="8005" />
|
|
</channels>
|
|
</application>
|
|
</system.runtime.remoting></programlisting>
|
|
|
|
<para>A console application that will host this Remoted object needs to
|
|
initialize the .NET Remoting infrastructure with a call to
|
|
RemotingConfiguration (since we are using the .config file for channel
|
|
registration) and then start the Spring application context. This is
|
|
shown below <programlisting language="csharp">RemotingConfiguration.Configure("RemoteApp.exe.config");
|
|
|
|
IApplicationContext ctx = ContextRegistry.GetContext();
|
|
|
|
Console.Out.WriteLine("Server listening...");
|
|
|
|
Console.ReadLine();
|
|
</programlisting></para>
|
|
|
|
<para>You can also put in the configuration file an instance of the
|
|
object <literal>Spring.Remoting.RemotingConfigurer</literal> to make
|
|
the RemotingConfiguration call show above on your behalf during
|
|
initialization of the IoC container. The
|
|
<literal>RemotingConfigurer</literal> implements the
|
|
<literal>IObjectFactoryPostProcessor</literal> interface,
|
|
which gets called after all object definitions have been loaded but
|
|
before they have been instantiated, (See<xref
|
|
linkend="objects-factory-customizing-factory-postprocessors" /> for more
|
|
information). The RemotingConfigurer has two properties you can
|
|
configure. <literal>Filename</literal>, that specifies the filename
|
|
to load the .NET remoting configuration from (if null the default file
|
|
name is used) and <literal>EnsureSecurity</literal> which makes sure
|
|
the channel in encrypted (available only on .NET 2.0). As a convenience,
|
|
the custom Spring remoting schema can be used to define an instance of
|
|
this class as shown below, taken from the <link
|
|
linkend="remoting-quickstart">Remoting QuickStart</link>
|
|
<programlisting language="myxml"><objects xmlns="http://www.springframework.net"
|
|
xmlns:r="http://www.springframework.net/remoting">
|
|
|
|
<r:configurer filename="Spring.Calculator.RemoteApp.exe.config" />
|
|
|
|
</objects></programlisting></para>
|
|
|
|
<para>The ReadLine prevents the console application from exiting. You
|
|
can refer to the code in RemoteApp in the <link
|
|
linkend="remoting-quickstart">Remoting QuickStart</link> to see this
|
|
code in action.</para>
|
|
</sect2>
|
|
|
|
<section xml:id="iis-application">
|
|
<title>IIS Application Configuration</title>
|
|
|
|
<para>If you are deploying a .NET remoting application inside IIS there
|
|
is a <ulink
|
|
url="http://forum.springframework.net/showthread.php?t=469">sample
|
|
project </ulink> that demonstrates the necessary configuration using
|
|
Spring.Web.</para>
|
|
|
|
<para>Spring.Web ensures the application context is initialized, but if
|
|
you don't use Spring.Web the idea is to start the initialization of the
|
|
Spring IoC container inside the application start method defined in
|
|
Global.asax, as shown below</para>
|
|
|
|
<programlisting language="csharp"> void Application_Start(object sender, EventArgs e)
|
|
{
|
|
// Code that runs on application startup
|
|
|
|
// Ensure Spring has loaded configuration registering context
|
|
Spring.Context.IApplicationContext ctx = new Spring.Context.Support.XmlApplicationContext(
|
|
HttpContext.Current.Server.MapPath("Spring.Config"));
|
|
Spring.Context.Support.ContextRegistry.RegisterContext(ctx);
|
|
}</programlisting>
|
|
|
|
<para>In this example, the Spring configuration file is named
|
|
Spring.Config. Inside Web.config you add a standard
|
|
<system.runtime.remoting> section. Note that you do not need to
|
|
specify the port number of your channels as they will use the port
|
|
number of your web site. Ambiguous results have been reported if you do
|
|
specify the port number. Also, in order for IIS to recognize the
|
|
remoting request, you should add the suffix '.rem' or '.soap' to the
|
|
target name of your exported remote object so that the correct IIS
|
|
handler can be invoked.</para>
|
|
</section>
|
|
</section>
|
|
|
|
<section xml:id="remoting-clientsao">
|
|
<title>Accessing a SAO on the Client</title>
|
|
|
|
<para>Administrative type registration on the client side lets you easily
|
|
obtain a reference to a SAO object. When a type is registered on the
|
|
client, using the new operator or using the reflection API will return a
|
|
proxy to the remote object instead of a local reference. Administrative
|
|
type registration on the client for a SAO object is performed using the
|
|
<literal>wellknown</literal> element in the client configuration section.
|
|
However, this approach requires that you expose the implementation of the
|
|
class on the client side. Practically speaking this would mean linking in
|
|
the server assembly to the client application, a generally recognized bad
|
|
practice. This dependency can be removed by developing remote services
|
|
based on a business interface. Aside from remoting considerations, the
|
|
separation of interface and implementation is considered a good practice
|
|
when designing OO systems. In the context of remoting, this means that the
|
|
client can obtain a proxy to a specific implementation with
|
|
<emphasis>only</emphasis> a reference to the interface assembly. To
|
|
achieve the decoupling of client and server, a separate assembly
|
|
containing the interface definitions is created and shared between the
|
|
client and server applications.</para>
|
|
|
|
<para>There is a simple means for following this design when the remote
|
|
object is a SAO object. A call to <literal>Activator.GetObject</literal>
|
|
will instantiate a SAO proxy on the client. For CAO objects another
|
|
mechanism is used and is discussed later. The code to obtain the SAO proxy
|
|
is shown below <programlisting language="csharp">ICalculator calc = (ICalculator)Activator.GetObject (
|
|
typeof (ICalculator),
|
|
"tcp://localhost:8005/MyRemotedCalculator");</programlisting></para>
|
|
|
|
<para>To obtain a reference to a SAO proxy within the IoC container, you
|
|
can use the object factory <literal>SaoFactoryObject</literal> in the
|
|
Spring configuration file. The following XML taken from the <link
|
|
linkend="remoting-quickstart"> Remoting QuickStart</link> demonstrates its
|
|
usage.</para>
|
|
|
|
<programlisting language="myxml"><object id="calculatorService" type="Spring.Remoting.SaoFactoryObject, Spring.Services">
|
|
<property name="ServiceInterface" value="Spring.Calculator.Interfaces.IAdvancedCalculator, Spring.Calculator.Contract" />
|
|
<property name="ServiceUrl" value="tcp://localhost:8005/RemotedSaoSingletonCalculator" />
|
|
</object></programlisting>
|
|
|
|
<para>The ServiceInterface property specifies the type of proxy to create
|
|
while the ServiceUrl property creates a proxy bound to the specified
|
|
server and published object name.</para>
|
|
|
|
<para>Other objects in the IoC container that depend on an implementation
|
|
of the interface <literal>ICalculator</literal> can now refer to the
|
|
object "calculatorService", thereby using a remote implementation of this
|
|
interface. The exposure of dependencies among objects within the IoC
|
|
container lets you easily switch the implementation of
|
|
<literal>ICalculator</literal>. By using the IoC container changing
|
|
the application to use a local instead of remote implementation is a
|
|
configuration file change, not a code change. By promoting interface based
|
|
programing, the ability to switch implementation makes it easier to unit
|
|
test the client application, since unit testing can be done with a mock
|
|
implementation of the interface. Similarly, development of the client can
|
|
proceed independent of the server implementation. This increases
|
|
productivity when there are separate client and server development teams.
|
|
The two teams agree on interfaces before starting development. The client
|
|
team can quickly create a simple, but functional implementation and then
|
|
integrate with the server implementation when it is ready.</para>
|
|
</section>
|
|
|
|
<section xml:id="remoting-cao-introduction">
|
|
<title>CAO best practices</title>
|
|
|
|
<para>Creating a client activated object (CAO) is typically done by
|
|
administrative type registration, either Programatically or via the
|
|
standard .NET remoting configuration section. The registration process
|
|
allows you to use the 'new' operator to create the remote object and
|
|
requires that the implementation of the object be distributed to the
|
|
client. As mentioned before, this is not a desirable approach to
|
|
developing distributed systems. The best practice approach that avoids
|
|
this problem is to create an SAO based factory class on the server that
|
|
will return CAO references to the client. In a manner similar to how
|
|
Spring's generic object factory can be used as a replacement creating a
|
|
factory per class, we can create a generic SAO object factory to return
|
|
CAO references to objects defined in Spring's application context. This
|
|
functionality is encapsulated in Spring's
|
|
<literal>CaoExporter</literal> class. On the client side a reference
|
|
is obtained using <literal>CaoFactoryObject</literal>. The client side
|
|
factory object supports creation of the CAO object using constructor
|
|
arguments. In addition to reducing the clutter and tedium around creating
|
|
factory classes specific to each object type you wish to expose in this
|
|
manner, this approach has the additional benefit of not requiring any type
|
|
registration on the client or server side. This is because the act of
|
|
returning an instance of a class that inherits from MarshalByRefObject
|
|
across a remoting boundary automatically returns a CAO object reference.
|
|
For more information on this best-practice, refer to the last section,
|
|
<xref linkend="remoting-additional" />, for some links to additional
|
|
resources.</para>
|
|
</section>
|
|
|
|
<section xml:id="remoting-publishcao">
|
|
<title>Registering a CAO object on the Server</title>
|
|
|
|
<para>To expose an object as a CAO on the server you should declare an
|
|
object in the standard Spring configuration that is a 'prototype', that is
|
|
the singleton property is set to false. This results in a new object being
|
|
created each time it is retrieved from Spring's IoC container. An
|
|
implementation of <literal>ICaoRemoteFactory</literal> is what
|
|
is exported via a call to RemotingServices.Marshal. This implementation
|
|
uses Spring's IoC container to create objects and then dynamically create
|
|
a .NET remoting proxy for the retrieved object. Note that the default
|
|
lifetime of the remote object is set to infinite (null is returned from
|
|
the implementation of InitializeLifetimeService()).</para>
|
|
|
|
<para>This is best shown using an example from the Remoting Quickstart
|
|
application. Here is the definition of a simple calculator object,</para>
|
|
|
|
<para><programlisting language="myxml"><object id="prototypeCalculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"
|
|
singleton="false">
|
|
<constructor-arg type="int" value="217" />
|
|
</object></programlisting>To export this as a CAO object we can declare
|
|
the <literal>CaoExporter</literal> object directly in the server's XML
|
|
configuration file, as shown below</para>
|
|
|
|
<programlisting language="myxml"><object id="caoCalculator" type="Spring.Remoting.CaoExporter, Spring.Services">
|
|
<property name="TargetName" value="prototypeCalculator" />
|
|
<property name="Infinite" value="false" />
|
|
<property name="InitialLeaseTime" value="2m" />
|
|
<property name="RenewOnCallTime" value="1m" />
|
|
</object></programlisting>
|
|
|
|
<para>Note the property 'TargetName' is set to the name, not the
|
|
reference, of the non-singleton declaration of the 'AdvancedCalculator'
|
|
class.</para>
|
|
|
|
<para>Alternatively, you can use the remoting schema and declare the CAO
|
|
object as shown below</para>
|
|
|
|
<programlisting language="myxml"><r:caoExporter targetName="prototypeCalculator" infinite="false">
|
|
<r:lifeTime initialLeaseTime="2m" renewOnCallTime="1m" />
|
|
</r:caoExporter></programlisting>
|
|
|
|
<para></para>
|
|
|
|
<section>
|
|
<title>Applying AOP advice to exported CAO objects</title>
|
|
|
|
<para>Applying AOP advice to exported CAO objects is done by referencing
|
|
the adviced object name to the CAO exporter. Again, taking an example
|
|
from the Remoting QuickStart, a calculator with logging around advice is
|
|
defined as shown below.</para>
|
|
|
|
<programlisting language="myxml"><object id="prototypeCalculatorWeaved" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">
|
|
<property name="targetSource">
|
|
<object type="Spring.Aop.Target.PrototypeTargetSource, Spring.Aop">
|
|
<property name="TargetObjectName" value="prototypeCalculator" />
|
|
</object>
|
|
</property>
|
|
<property name="interceptorNames">
|
|
<list>
|
|
<value>ConsoleLoggingAroundAdvice</value>
|
|
</list>
|
|
</property>
|
|
</object></programlisting>
|
|
|
|
<para>If this declaration is unfamiliar to you, please refer to <xref
|
|
linkend="aop" /> for more information. The CAO exporter then references
|
|
with the name 'prototypeCalculatorWeaved' as shown below.</para>
|
|
|
|
<programlisting language="myxml"><r:caoExporter targetName="prototypeCalculatorWeaved" infinite="false">
|
|
<r:lifeTime initialLeaseTime="2m" renewOnCallTime="1m" />
|
|
</r:caoExporter></programlisting>
|
|
</section>
|
|
</section>
|
|
|
|
<section xml:id="remoting-clientcao">
|
|
<title>Accessing a CAO on the Client</title>
|
|
|
|
<para>On the client side a CAO reference is obtained by using the
|
|
<literal>CaoFactoryObject</literal> as shown below</para>
|
|
|
|
<programlisting language="myxml"><object id="calculatorService" type="Spring.Remoting.CaoFactoryObject, Spring.Services">
|
|
<property name="RemoteTargetName" value="prototypeCalculator" />
|
|
<property name="ServiceUrl" value="tcp://localhost:8005" />
|
|
</object></programlisting>
|
|
|
|
<para>This definition corresponds to the exported calculator from the
|
|
previous section. The property 'RemoteTargetName' identifies the object on
|
|
the server side. Using this approach the client can obtain an reference
|
|
though standard DI techniques to a remote object that implements the
|
|
<literal>IAdvancedCalculator</literal> interface. (As always,
|
|
that doesn't mean the client should treat the object as if it was an
|
|
in-process object).</para>
|
|
|
|
<para>Alternatively, you can use the Remoting schema to shorten this
|
|
definition and provide intellisense code completion</para>
|
|
|
|
<programlisting language="myxml"><r:caoFactory id="calculatorService"
|
|
remoteTargetName="prototypeCalculator"
|
|
serviceUrl="tcp://localhost:8005" /></programlisting>
|
|
|
|
<section>
|
|
<title>Applying AOP advice to client side CAO objects.</title>
|
|
|
|
<para>Applying AOP advice to a client side CAO object is done just like
|
|
any other object. Simply use the id of the object created by the
|
|
<literal>CaoFactoryObject</literal> as the AOP target, i.e.
|
|
'calculatorService' in the previous example.</para>
|
|
</section>
|
|
</section>
|
|
|
|
<section xml:id="remoting-schema">
|
|
<title>XML Schema for configuration</title>
|
|
|
|
<para>Please install the XSD schemas into VS.NET as described in <xref
|
|
linkend="vsnet" />. XML intellisense for the attributes of the
|
|
saoExporter, caoExporter and caoFactory should be self explanatory as they
|
|
mimic the standard property names used to configure .NET remote
|
|
objects.</para>
|
|
</section>
|
|
|
|
<section xml:id="remoting-additional">
|
|
<title>Additional Resources</title>
|
|
|
|
<para>Two articles that describe the process of creating a standard SAO
|
|
factory for returning CAO objects are <ulink
|
|
url="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpatterns/html/ImpBrokerClient.asp">Implementing
|
|
Broker with .NET Remoting Using Client-Activated Objects</ulink> on MSDN
|
|
and <ulink
|
|
url="http://www.glacialcomponents.com/ArticleDetail/CAOGuide.aspx">Step by
|
|
Step guide to CAO creation through SAO class factories</ulink> on Glacial
|
|
Components website.</para>
|
|
</section>
|
|
</chapter> |