The Struts framework depends on one or more configuration files to be able to load and create the necessary application-specific components at startup. The configuration files allow the behavior of the framework components to be specified declaratively, rather than having the information and behavior hardcoded. This gives developers the flexibility to provide their own extensions, which the framework can discover dynamically.
The configuration file is based on the XML format and can be validated against the Struts DTD struts-config_1_1.dtd. Although there are some similarities between the 1.0 and 1.1 versions of the framework with respect to the configuration file, there are at least as many differences. Fortunately, the designers of the framework have made backward compatibility a goal of the Struts 1.1 release; therefore, your 1.0 applications should continue to work properly with the new version.
Application modules were mentioned briefly in Chapter 3, but we haven’t yet fully introduced this new feature. With application modules, you can define multiple Struts configuration files, one for each supported module. Each application module can provide its own configuration information, including message resources, and be completely independent from other modules.
Application modules allow a single Struts application to be split into separate projects, making parallel development easier to accomplish. Although the functionality for application modules exists in the framework, you are not required to implement more than one (the default application module). We’ll discuss application modules further in Chapter 5, Chapter 6, and Chapter 7. For now, we’ll concentrate on configuring the default application; we’ll see how easy it is to add additional modules later.
The
org.apache.struts.config
package was added to Struts 1.1. The framework uses JavaBeans at
runtime to hold the configuration information it reads from the
Struts configuration files. Figure 4-4 shows the
essential classes from the config
package.
Each class in the config
package holds information
from a specific section of the configuration file. After the
configuration file has been validated and parsed, the Struts
framework uses instances of these beans to represent in-memory
versions of the information that has been declared in the
configuration file. These classes act as runtime containers of the
configuration information and are used by the framework components as
needed.
The
org.apache.struts.config.ConfigRuleSet
class shown in Figure 4-4 has a slightly different, but related,
job—it contains the set of rules that are required to parse a
Struts configuration file. Its job is to construct instances of the
configuration JavaBeans mentioned in the previous paragraph when the
application is started.
The
org.apache.struts.config.ApplicationConfig
class deserves a special introduction, as
it plays a very important role in the framework. As Figure 4-4 indicates, it is central to the entire
config
package and holds onto the configuration
information that describes an entire Struts application. If multiple
application modules are being used, there is one
ApplicationConfig
object for each module. The
ApplicationConfig
class will surface throughout
the remainder of our discussion of the framework.
As the web application’s DTD is used to validate the web.xml file, the Struts DTD is used to validate the Struts configuration file.
Tip
A complete struts-config.xml file is shown later, in Example 4-5. It may help to refer to that example following the discussion of these elements.
The following Struts DTD declaration indicates that the
struts-config
element is the root element for the
XML file and that it has eight child elements:
<!ELEMENT struts-config (data-sources?, form-beans?, global-exceptions?, global- forwards?, action-mappings?, controller?, message-resources*, plug-in*) >
The
data-sources
element
allows you to set up a rudimentary data source that you can use from
within the Struts framework. A data source acts as a
factory[7] for database connections and
provides a single point of control. Many data source implementations
use a connection-pooling mechanism to improve performance and
scalability.
Many vendors provide their own implementations of data source
objects. The Java language provides the
javax.sql.DataSource
interface, which all implementations must implement. Most application
servers and some web containers provide built-in data source
components. All of the major database vendors also provide data
source implementations.
The data-sources
element can contain zero or more
data-source
elements:
<!ELEMENT data-sources (data-source*)>
The data-source
element allows for multiple
set-property
elements to be specified:
<!ELEMENT data-source (set-property*)>
The set-property
element allows you to configure
properties that are specific to your data source implementation.
Throughout the discussion of the Struts configuration elements in the
rest of this chapter, you will notice a child element called
set-property
in many of the major elements of the
configuration file. The
set-property
element
specifies the name and value of an additional JavaBeans configuration
property whose setter method will be called on the object that
represents the surrounding element. This element is especially useful
for passing additional property information to an extended
implementation class. The set-property
element is
optional, and you will use it only if you need to pass additional
properties to a configuration class.
Tip
The set-property
element defines three attributes,
including the id
attribute, which is seldom used.
The property
attribute is the name of the
JavaBeans property whose setter method will be called. The
value
attribute is a string representing the value
that will be passed to the setter method after proper conversion.
This section provides an example of using the
set-property
element. The same format is
replicated wherever the set-property
element is
declared.
The attributes for the data-source
element are
listed in Table 4-3.
Table 4-3. Attributes of the data-source element
Warning
The
GenericDataSource
class
included with the Struts framework has been deprecated in favor of
the
Database Connection Pool (DBCP)
project from Jakarta or an implementation from your container.
The following code illustrates how to configure a data source within the Struts configuration file:
<data-sources> <data-source> <set-property property="autoCommit" value="true"/> <set-property property="description" value="MySql Data Source"/> <set-property property="driverClass" value="com.caucho.jdbc.mysql.Driver"/> <set-property property="maxCount" value="10"/> <set-property property="minCount" value="2"/> <set-property property="user" value="admin"/> <set-property property="password" value="admin"/> <set-property property="url" value="jdbc:mysql-caucho://localhost:3306/storefront"/> </data-source> </data-sources>
This code illustrates a data-source
element
configured to connect to a MySQL database using a JDBC driver from
Caucho Technology, the developers of the Resin™
servlet/EJB container.
You can specify multiple data sources within the configuration file, assign each one a unique key, and access a particular data source in the framework by its key. This gives you the ability to access multiple databases if necessary.There are several other popular data source implementations you can use. Table 4-4 lists a few of the more popular alternative implementations.
The
form-beans
element
allows you to configure multiple ActionForm
classes that are used by the views. Within the
form-beans
section, you can configure zero or more
form-bean
child elements. Each
form-bean
element also has several child elements.
<!ELEMENT form-bean (icon?, display-name?, description?, set-property*, form- property*) >
Each form-bean
element also has four attributes
that you can specify. Table 4-5 lists the
attributes.
Table 4-5. Attributes of the form-bean element
Warning
Be careful when configuring the value for the type
attribute. It must be the fully qualified name of the
ActionForm
implementation class. If you misspell
the name, it can be very hard to debug this problem.
As mentioned in Chapter 3, a
form bean is
a JavaBeans class that extends the
org.apache.struts.action.ActionForm
class. The
following code shows how the form-beans
element
can be configured in the Struts configuration file:
<struts-config> <form-beans> <form-bean name="loginForm" type="org.apache.struts.action.DynaActionForm"> <form-property name="username" type="java.lang.String"/> <form-property name="password" type="java.lang.String"/> </form-bean> <form-bean name="shoppingCartForm" type="com.oreilly.struts.order.ShoppingCartForm"/> </form-beans> </struts-config>
One of the form-bean
elements in this code uses a
feature new in Struts 1.1, called
dynamic action
forms
. Dynamic action forms were discussed
briefly in Chapter 3 and will be discussed in
detail in Chapter 7.
You can pass one or more dynamic properties to an instance of the
org.apache.struts.action.DynaActionForm
class using the
form-property
element. It is supported only when
the type
attribute of the surrounding
form-bean
element is
org.apache.struts.action.DynaActionForm
, or a
descendant class.
Each form-property
element also has four
attributes that you can specify. Table 4-6 lists
the attributes allowed in the
form-property
element.
Table 4-6. Attributes of the form-property element
The following
form-bean
fragment illustrates the use of the form-property
element:
<form-bean name="checkoutForm" type="org.apache.struts.action.DynaActionForm"> <form-property name="firstName" type="java.lang.String"/> <form-property name="lastName" type="java.lang.String"/> <form-property name="age" type="java.lang.Integer" initial="18"/> </form-bean>
The global-exceptions
section allows you to
configure exception handlers declaratively. The
global-exceptions
element can contain zero or more
exception
elements:
<!ELEMENT global-exceptions (exception*)>
Later in this chapter, when action mappings are discussed, you will
see that the exception
element also can be
specified in the action
element. If an
exception
element is configured for the same type
of exception both in the global- exceptions
element and in the action
element, the
action
level will take precedence. If no
exception
element mapping is found at the
action
level, the framework will look for
exception mappings defined for the exception’s
parent class. Eventually, if a handler is not found, a
ServletException
or IOException
will be thrown, depending on the type of the original exception.
Chapter 10 deals with both declarative and
programmatic exception handling in detail. This section illustrates
how to configure declarative exception handling for your
applications.
The exception
element describes a mapping between
a Java exception that may occur during processing of a request and an
instance of org.apache.struts.action.ExceptionHandler
that is responsible for dealing
with the thrown exception. The declaration of the
exception
element illustrates that it also has
several child elements:
<!ELEMENT exception (icon? display-name? description? set-property*)>
Probably more important than the child elements are the attributes
that can be specified in the exception
element.
The attributes are listed in Table 4-7.
Table 4-7. Attributes of the exception element
The following is an example of a global-exceptions
element:
<global-exceptions> <exception key="global.error.invalidlogin" path="/security/signin.jsp" scope="request" type="com.oreilly.struts.framework.exceptions.InvalidLoginException"/> </global-exceptions>
Every action that is executed finishes by forwarding or redirecting
to a view. This view is a JSP page or static HTML page, but might be
another type of resource. Instead of referring to the view directly,
the Struts framework uses the concept of a forward to associate a
logical name with the resource. So, instead of referring to
login.jsp directly, a Struts application may
refer to this resource as the login
forward, for
example.
The
global-forwards
section allows you to configure forwards that can be used by all
actions within an application. The global-forwards
section consists of zero or more forward
elements:
<!ELEMENT global-forwards (forward*)>
The forward
element maps a logical name to an
application-relative URI. The application can then perform a forward
or redirect, using the logical name rather than the literal URI. This
helps to decouple the controller and model logic from the view. The
forward
element can be defined in both the
global-forwards
and action
elements. If a forward with the same name is defined in both places,
the action
level will take precedence.
The declaration of the
forward
element
illustrates that it also has child elements:
<!ELEMENT forward(icon?, display-name?, description, set-property*)>
As with the exception
element, the attributes
probably are more interesting than the child elements. The attributes
for the forward
element are shown in Table 4-8.
Table 4-8. Attributes of the forward element
Here’s an example of a
global-forwards
element from the
Storefront application:
<global-forwards> <forward name="Login" path="/security/signin.jsp" redirect="true"/> <forward name="SystemFailure" path="/common/systemerror.jsp"/> <forward name="SessionTimeOut" path="/common/sessiontimeout.jsp" redirect="true"/> <forward name="Welcome" path="/viewsignin"/> </global-forwards>
The
org.apache.struts.action.ActionForward
class is used to hold the information
configured in the controller
element (discussed
later). The ActionForward
class now extends
org.apache.struts.config.ForwardConfig
for backward compatibility.
The
action-mappings
element contains a set of zero or more action
elements for a Struts application:
<!ELEMENT action-mappings (action*)>
The action
element describes a mapping from a
specific request path to a corresponding Action
class. The controller selects a particular mapping by matching the
URI path in the request with the path
attribute in
one of the action
elements. The
action
element
contains the following child elements:
<!ELEMENT action (icon?, display-name?, description, set-property*, exception*, forward*)>
Two child elements should stand out in the list of children for the
action
element, because you’ve
already seen them earlier in this chapter:
exception
and
forward
.
We talked about the exception
element when we
discussed the global-exceptions
element. We
mentioned then that exception
elements could be
defined at the global or at the action level. The
exception
elements defined within the
action
element take precedence over any of the
same type defined at the global level. The syntax and attributes are
the same, regardless of where they are defined.
We introduced the forward
element when we
discussed the global-forwards
element. As with
exception
elements, a forward
element can be defined both at the global level and at the action
level. The action level takes precedence if the same forward is
defined in both locations. The action
element
contains quite a few attributes, shown in Table 4-9.
Table 4-9. Attributes of the action element
Name |
Description |
---|---|
attribute |
The name of the request- or session-scope attribute under which the
form bean for this action can be accessed. A value is allowed here
only if there is a form bean specified in the |
className |
The implementation class of the configuration bean that will hold the
action information. The |
forward |
The application-relative path to a servlet or JSP resource that will
be forwarded to, instead of instantiating and calling the
|
include |
The application-relative path to a servlet or JSP resource that will
be included with the response, instead of instantiating and calling
the |
input |
Module-relative path of the action or other resource to which control should be returned if a validation error is encountered. Valid only when “name” is specified. Required if “name” is specified and the input bean returns validation errors. Optional if “name” is specified and the input bean does not return validation errors |
name |
The name of the form bean associated with this action. This value
must be the |
path |
The application-relative path to the submitted request, starting with
a “/” character and without the
filename extension if extension mapping is used. In other words, this
is the name of the action—for example,
|
parameter |
A general-purpose configuration parameter that can be used to pass
extra information to the action instance selected by this action
mapping. The core framework does not use this value. If you provide a
value here, you can obtain it in your |
prefix |
Used to match request parameter names to form bean property names.
For example, if all of the properties in a form bean begin with
“pre_”, you can set the
|
roles |
A comma-delimited list of security role names allowed to invoke this
|
scope |
Used to identify the scope in which the form bean is
placed—either |
suffix |
Used to match request parameter names to form bean property names.
For example, if all of the properties in a form bean end with
“_foo”, you can set the
|
type |
A fully qualified Java class name that extends the
|
unknown |
A Boolean value indicating whether this action should be configured
as the default for this application. If this attribute is set to
|
validate |
A Boolean value indicating whether the |
The following is an example of the
“signin” action
element from the Storefront application:
<action path="/signin" type="com.oreilly.struts.storefront.security.LoginAction" scope="request" name="loginForm" validate="true" input="/security/signin.jsp"> <forward name="Success" path="/index.jsp" redirect="true"/> <forward name="Failure" path="/security/signin.jsp" redirect="true"/> </action>
The
controller
element is
new to Struts 1.1. Prior to Version 1.1, the
ActionServlet
contained the controller
functionality, and you had to extend that class to override the
functionality. In Version 1.1, however, Struts has moved most of the
controller functionality to the RequestProcessor
class. The ActionServlet
still receives the
requests, but it delegates the request handling to an instance of the
RequestProcessor
. This allows you to declaratively
assign the processor class and modify its functionality.
If you’re familiar with Version 1.0,
you’ll notice that many of the parameters that were
configured in the web.xml file for the
controller servlet now are configured using the
controller
element. Because the controller and its
attributes are defined in the struts-config.xml
file, you can define a separate controller
element
for each module. The controller
element has a
single child element:
<!ELEMENT controller (set-property*)>
The controller
element can contain zero or more
set-property
elements and many different
attributes. The attributes are shown in Table 4-10.
Table 4-10. Attributes of the controller element
The
org.apache.struts.config.ControllerConfig
class is used to represent the
information configured in the controller
element
in memory. The following fragment shows an example of how to
configure the controller
element:
<controller contentType="text/html;charset=UTF-8" locale="true" nocache="true" processorClass="com.oreilly.struts.framework.CustomRequestProcessor"/>
The
message-resources
element specifies characteristics of the message resource bundles
that contain the localized messages for an application. Each Struts
configuration file can define one or more message resource bundles;
therefore, each module can define its own bundles. The
message-resources
element contains only a
set-property
element:
<!ELEMENT message-resources (set-property*)>
Table 4-11 lists the attributes supported by the
message-resources
element.
Table 4-11. Attributes of the message-resources element
The following example shows how to configure multiple
message-resources
elements for a single
application. Notice that the second element had to specify the
key
attribute, because only one can be stored with
the default key:
<message-resources null="false" parameter="StorefrontMessageResources"/> <message-resources key="IMAGE_RESOURCE_KEY" null="false" parameter="StorefrontImageResources"/>
The concept of a
plug-in
was added in Struts 1.1. This powerful feature allows your Struts
applications to discover resources dynamically at startup. For
example, if you need to create a connection to a remote system at
startup and you didn’t want to hardcode this
functionality into the application, you can use a plug-in, and the
Struts application will discover it dynamically. To use a plug-in,
create a Java class that implements the
org.apache.struts.action.PlugIn
interface and add
a
plug-in
element to
the configuration file. The PlugIn
mechanism
itself will be discussed further in Chapter 9.
The plug-in
element specifies a fully qualified
class name of a general-purpose application plug-in module that
receives notification of application startup and shutdown events. An
instance of the specified class is created for each element; the
init()
method is called when the application is
started, and the destroy()
method is called when
the application is stopped. The class specified here must implement
the org.apache.struts.action.PlugIn
interface and
implement the init()
and destroy( )
methods.
The plug-in
element may contain zero or more
set-property
elements, so that extra configuration
information may be passed to your PlugIn
class:
<!ELEMENT plug-in (set-property*)>
The allowed attribute for the plug-in
element is
shown in Table 4-12.
The following fragment shows two plug-in
elements
being used:
<plug-in className="com.oreilly.struts.storefront.service.StorefrontServiceFactory"/> <plug-in className="org.apache.struts.validator.ValidatorPlugIn"> <set-property property="pathnames" value="/WEB-INF/validator-rules.xml,/WEB-INF/validation.xml"/> </plug-in>
Tip
The ValidatorPlugIn
shown in the second
plug-in
element displays how the Struts framework
initializes the Validator. The Validator framework is discussed in
Chapter 11.
Up to this point, you haven’t seen a complete example of a Struts configuration file. Example 4-5 provides a complete listing.
Example 4-5. A complete Struts configuration file
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN" "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd"> <struts-config> <data-sources> <data-source> <set-property property="autoCommit" value="true"/> <set-property property="description" value="Resin Data Source"/> <set-property property="driverClass" value="com.caucho.jdbc.mysql.Driver"/> <set-property property="maxCount" value="10"/> <set-property property="minCount" value="2"/> <set-property property="user" value="admin"/> <set-property property="password" value="admin"/> <set-property property="url" value="jdbc:mysqlcaucho://localhost:3306/storefront"/> </data-source> </data-sources> <form-beans> <form-bean name="loginForm" type="com.oreilly.struts.storefront.security.LoginForm"/> <form-bean name="itemDetailForm" type="org.apache.struts.action.DynaActionForm"> <form-property name="view" type="com.oreilly.struts.catalog.view.ItemView"/> </form-bean> </form-beans> <global-exceptions> <exception key="global.error.invalidlogin" path="/security/signin.jsp" scope="request" type="com.oreilly.struts.framework.exceptions.InvalidLoginException"/> </global-exceptions> <global-forwards> <forward name="Login" path="/security/signin.jsp" redirect="true"/> <forward name="SystemFailure" path="/common/systemerror.jsp"/> <forward name="SessionTimeOut" path="/common/sessiontimeout.jsp" redirect="true"/> </global-forwards> <action-mappings> <action path="/viewsignin" parameter="/security/signin.jsp" type="org.apache.struts.actions.ForwardAction" scope="request" name="loginForm" validate="false" input="/index.jsp"> </action> <action path="/signin" type="com.oreilly.struts.storefront.security.LoginAction" scope="request" name="loginForm" validate="true" input="/security/signin.jsp"> <forward name="Success" path="/index.jsp" redirect="true"/> <forward name="Failure" path="/security/signin.jsp" redirect="true"/> </action> <action path="/signoff" type="com.oreilly.struts.storefront.security.LogoutAction" scope="request" validate="false" input="/security/signin.jsp"> <forward name="Success" path="/index.jsp" redirect="true"/> </action> <action path="/home" parameter="/index.jsp" type="org.apache.struts.actions.ForwardAction" scope="request" validate="false"> </action> <action path="/viewcart" parameter="/order/shoppingcart.jsp" type="org.apache.struts.actions.ForwardAction" scope="request" validate="false"> </action> <action path="/cart" type="com.oreilly.struts.storefront.order.ShoppingCartActions" scope="request" input="/order/shoppingcart.jsp" validate="false" parameter="method"> <forward name="Success" path="/action/viewcart" redirect="true"/> </action> <action path="/viewitemdetail" name="itemDetailForm" input="/index.jsp" type="com.oreilly.struts.storefront.catalog.GetItemDetailAction" scope="request" validate="false"> <forward name="Success" path="/catalog/itemdetail.jsp"/> </action> <action path="/begincheckout" input="/order/shoppingcart.jsp" type="com.oreilly.struts.storefront.order.CheckoutAction" scope="request" validate="false"> <forward name="Success" path="/order/checkout.jsp"/> </action> <action path="/getorderhistory" input="/order/orderhistory.jsp" type="com.oreilly.struts.storefront.order.GetOrderHistoryAction" scope="request" validate="false"> <forward name="Success" path="/order/orderhistory.jsp"/> </action> </action-mappings> <controller contentType="text/html;charset=UTF-8" locale="true" nocache="true" processorClass="com.oreilly.struts.framework.CustomRequestProcessor"/> <message-resources parameter="StorefrontMessageResources" null="false"/> <message-resources key="IMAGE_RESOURCE_KEY" parameter="StorefrontImageResources" null="false"/> <plug-in className="com.oreilly.struts.storefront.service.StorefrontServiceFactory"/> <plug-in className="org.apache.struts.validator.ValidatorPlugIn"> <set-property property="pathnames" value="/WEB-INF/validator-rules.xml,/WEB-INF/validation.xml"/> </plug-in> </struts-config>
Now that you’ve seen how to configure the default application for Struts, the last step is to discuss how you include multiple application modules. With Struts 1.1, you have the ability to set up multiple Struts configuration files. Although the application modules are part of the same web application, they act independently of one another. You also can switch back and forth between application modules if you like.
Using multiple application modules allows for better organization of the components within a web application. For example, you can assemble and configure one application module for everything that deals with catalogs and items, while another module can be organized with the configuration information for a shopping cart and ordering. Separating an application into components in this way facilitates parallel development.
The first step is to create the additional Struts configuration
files. Suppose we created a second configuration file named
struts-order-config.xml. We must modify the
web.xml file for the application and add an
additional init-param
element for the new module.
This was shown earlier in the chapter, but it’s
repeated here for convenience. Example 4-6 shows the
servlet instance mapping from before with an additional
init-param
for the second Struts configuration
file.
Example 4-6. A partial web.xml file that illustrates how to configure multiple modules
<servlet> <servlet-name>storefront</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <init-param> <param-name>config/order</param-name> <param-value>/WEB-INF/struts-order-config.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>
Notice that the param-name
value for the
nondefault application module in Example 4-6 begins
with config/
. All nondefault application
modules’ param- name
elements
must begin with config/
; the default
application’s param-name
element
contains the config
value alone. The part that
comes after config/
is known as the
application module prefix and is used throughout
the framework for intercepting requests and returning the correct
resources.
Tip
With the current version of the Struts framework, only extension mapping is supported when using multiple application modules. Path mapping is not yet supported.
Pay special attention to the configuration attributes available in the various Struts XML elements. Some of them, as mentioned in this chapter, have a profound effect on how an application operates in a multiapplication module environment.
To ensure that your Struts configuration file is valid, it can and
should be validated against the Struts DTD. To do this, you must
include the
DOCTYPE
element at
the beginning of your Struts configuration XML file:
<?xml version="1.0" encoding="ISO-8859-1" ?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN" "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">
In earlier versions of the framework, there were some issues with applications not being able to start up if they couldn’t get to the Jakarta site and access the DTD from there. This is no longer the case, as Struts now provides local copies of the DTDs.
Some users prefer to specify a SYSTEM DOCTYPE
tag,
rather than a PUBLIC
one. This allows you to
specify an absolute path instead of a relative one. Although this may
solve a short-term problem, it creates more long-term ones. You
can’t always guarantee the directory structure from
one target environment to another. Also, different containers act
differently when using a SYSTEM DOCTYPE
tag. You
probably are better off not using it. However, if you decide that you
need to do so, it should look something like the following:
<?xml version="1.0" encoding="ISO-8859-1" ?> <!DOCTYPE struts-config SYSTEM "file:///c:/dtds/struts-config_1_1.dtd"> <struts-config> <!--The rest of the struts configuration file goes next -->
As you can see, the location of the DTD is an absolute path. If the path of the target environment is not the same, you’ll have to modify the XML file. This is why this approach is not recommended.
[7] See the discussion of the Abstract Factory pattern in the Gang of Four’s Design Patterns: Elements of Reusable Object-Oriented Software (Addison Wesley).
Get Programming Jakarta Struts, Second Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.