Now that we have a better understanding of the various components of the CORBA architecture, let’s walk through the creation of CORBA objects using Java IDL. In order to distribute a Java object over the network using CORBA, you have to define your own CORBA-enabled interface and its implementation. This involves doing the following:
Writing an interface in the CORBA Interface Definition Language
Generating a Java base interface, plus a Java stub and skeleton class, using an IDL-to-Java compiler
Writing a server-side implementation of the Java base interface
We’ll walk through these steps one by one, starting with a quick primer on CORBA IDL, followed by the requirements for creating a Java implementation of an IDL-defined remote object.
The syntax of both Java and IDL were modeled to some extent on C++, so there are a lot of similarities between the two in terms of syntax. Interfaces in IDL are declared much like classes in C++ and, thus, classes or interfaces in Java. The major differences between IDL and Java are:
IDL is a declaration language. In IDL, you declare only the names and types for interfaces, data members, methods, method parameters, etc. Method implementations are created in the implementation language you choose (in this case Java), after you’ve used an IDL compiler to convert your IDL interface to your target language.
IDL, like C++, includes nonclass data structure definitions, like structs, unions, and enumerations.
Method parameters in IDL include modifiers that specify whether they are input, output, or input/output variables. In Java, all primitive data types are passed by value, and all object data types are passed by reference.
An IDL file can include multiple public interfaces. Only a single public class can be defined in a given Java file (although Java does allow for multiple inner classes within a single public class definition, and multiple nonpublic classes per file).
Modules, which are similar to Java packages, can be nested within other modules in the same IDL file, and interfaces in multiple distinct modules can be defined in the same IDL file. In Java, you can define a class only within a single package in a single Java file.
Modules
are declared in IDL using the module
keyword,
followed by a name for the module and an opening brace that starts
the module scope. Everything defined within the scope of this module
(interfaces, constants, other modules) falls within the module and is
referenced in other IDL modules using the syntax
modulename
::x
. Suppose
that you want all your classes to be contained in a module called
corba
, which is part of a larger module called
jent
(an abbreviation of the title of this
book). In IDL this is declared as follows:
// IDL module jent { module corba { interface NeatExample ... }; };
If you want to reference the NeatExample
interface in other IDL files, you use the syntax
jent::corba::NeatExample
, which may look
familiar to readers who have done C++ programming. Java programmers
should note the semicolons following the closing braces on the module
definitions, which are required in IDL but not in Java. A semicolon
is also required after the close of an interface
definition.
Interfaces declared in IDL are mapped into classes or interfaces in Java. As mentioned earlier, IDL is used only to declare modules, interfaces, and their methods. Methods on IDL interfaces are always left abstract, to be defined in the programming language you use to implement the interfaces.
The declaration of an interface includes an interface header and an interface body. The header specifies the name of the interface and the interfaces it inherits from (if any). Here is an IDL interface header:
interface PrintServer : Server { ...
This header starts the declaration of an interface called
PrintServer
that inherits all the methods and
data members defined in the Server
interface. An
IDL interface can inherit from multiple interfaces; simply separate
the interface names with commas in the inheritance part of the
header.
The interface body declares
all the data members (or attributes) and methods of an interface.
Data members are declared using the attribute
keyword. At a minimum, the declaration includes a name and a type
(see Chapter 14 for a complete list of the basic
data types available in IDL and the mapping to Java types). The
declaration can optionally specify whether the attribute is read-only
or not, using the readonly
keyword. By default,
every attribute you declare is readable and writable (for Java, this
means that the IDL compiler generates public read and write methods
for it). Here is an example declaration for a read-only
string
attribute:
readonly attribute string myString;
You declare a method by specifying its name, return type, and
parameters,
at a minimum. You can also optionally declare exceptions the method
might raise, the invocation semantics of the method, and the context
for the method call (see Chapter 14 for more
details). Here is the declaration for a simple method that returns a
string
:
string parseString(in string buffer);
This declares a method called parseString( )
that accepts a single string
argument and
returns a string
value.
Now let’s tie all these basic elements together. Here’s a complete IDL example that declares a module within another module, which itself contains several interfaces:
module OS { module services { interface Server { readonly attribute string serverName; boolean init(in string sName); }; interface Printable { boolean print(in string header); }; interface PrintServer : Server { boolean printThis(in Printable p); }; }; };
The first interface, Server
, has a single
read-only string
attribute and an
init( )
method that accepts a
string
and returns a
boolean
. The Printable
interface has a single print( )
method that
accepts a string header. Finally, the
PrintServer
interface extends the
Server
interface (hence inheriting all its
methods and attributes) and adds a printThis( )
method that accepts a Printable
object and
returns a boolean
. In all cases,
we’ve declared our method arguments as input-only
(i.e., pass-by-value), using the in
keyword.
Once you’ve described your remote interfaces in IDL, you need to generate Java classes that act as a starting point for implementing those remote interfaces in Java using an IDL-to-Java compiler. Every standard IDL-to-Java compiler (whether it’s a POA-compliant or pre-POA version) can generate the following Java classes from an IDL interface:
A Java interface with the same name as the IDL interface (e.g.,
Server
). This interface includes Java method declarations that are a mapping of the operations declared in the IDL interface. In later versions of Sun’s IDL-to-Java mapping (JDK 1.3 and later), aninterfaceName
Operations
interface (e.g.,ServerOperations
) is generated that contains these method declarations. The object’s mapped Java interface extends this interface. This “operations” interface was added to the standard CORBA IDL-to-Java mapping, and the IDL-to-Java compiler in JDK 1.3 was updated to reflect this. This same operations interface is extended by the server-side skeleton interfaces. In earlier versions of Java IDL (JDK 1.2), the object’s Java interface contains the method declarations directly, and there is no operations interface created.A helper class whose name is the name of the IDL interface with “Helper” appended to it (e.g.,
ServerHelper
). The primary purpose of this class is to provide a staticnarrow( )
method that can safely cast CORBAObject
references to the Java interface type. The helper class also provides other useful static methods, such asread( )
andwrite( )
methods that allow you to read and write an object of the corresponding type using I/O streams.A holder class whose name is the name of the IDL interface with “Holder” appended to it (e.g.,
ServerHolder
). This class is used when objects with this interface are used asout
orinout
arguments in remote CORBA methods. Instead of being passed directly into the remote method, the object is wrapped with its holder before being passed. When a remote method has parameters that are declared asout
orinout
, the server-side method has to be able to update the argument it is passed and return the updated value. The only way to guarantee this, even for primitive Java data types, is to forceout
andinout
arguments to be wrapped in Java holder classes, which are filled with the output value of the argument when the method returns. Also,inout
arguments are initialized with the desired input value before the remote method is called in Java.A client stub class, called
_
interface-name
Stub
, that acts as a client-side implementation of the interface. This stub class implements the generated Java interface for the object, but simply knows how to convert client method calls into ORB requests that are forwarded to the actual remote object. The stub class for an interface namedServer
is called_ServerStub
.
These classes
comprise the “outward-facing”
mapping of the CORBA object’s interface (the
interfaces that clients of the object use directly). The IDL-to-Java
compiler can also generate server-side skeleton classes you can use
to implement the server-side implementation of the remote CORBA
interface. In pre-POA versions of Java IDL, the IDL-to-Java compiler
(idltojava
in JDK 1.2,[13]
idlj
in JDK 1.3) is based on the
ImplBase
inheritance approach to creating server
implementations. The pre-POA Java IDL compilers generate the
server-side code in the form:
A server skeleton class called
_
interface-name
ImplBase
, which is a base class for a server-side implementation of the interface. The base class can accept requests for the object from the ORB and channel return values back through the ORB to the remote client. The skeleton class for an interface namedServer
is called_ServerImplBase
.
In POA-compliant versions of Sun’s
idlj
compiler, a similar
_interfaceName
Stub
client-side interface is generated, but instead of the server-side
ImplBase
inheritance scheme used in earlier
versions, the compiler generates:
A server skeleton class named
interfaceNamePOA
(e.g,ServerPOA
), which implements a generatedinterfaceName
Operations
interface and extends the POA-related server-side interfaces. TheinterfaceNameOperations
interface contains Java mappings of all the methods declared in the IDL definition. This class serves the same basic role as theImplBase
class generated in earlier versions of Java IDL, but it utilizes the standard POA interfaces to interact with the server-side ORB functions.
So, in addition to generating a client-side Java mapping of the IDL interface and some helper classes for the Java interface, the IDL-to-Java compiler also creates subclasses that act as an interface between a CORBA client and the ORB, between the server-side implementation and the ORB. Chapter 16 provides a complete reference for the JDK 1.3 and 1.4 versions of Sun’s idlj compiler. We will use this IDL-to-Java tool in the examples in this chapter. Remember, though, that any Java mapping of the CORBA standard should include its own IDL-to-Java compiler to generate these Java classes from the IDL interfaces you write. In addition, the Java that these tools generate should be compliant with the standard IDL mapping for Java, published by the OMG in the CORBA standard. Tools developed before the POA-compliant IDL-to-Java mapping will tend to use some non-standard server-side object adaptor styles, while those developed after the POA was introduced should generate POA-compliant server skeleton classes. So when using third-party Java-based CORBA tools and ORBs, it’s important to understand which version of the core CORBA spec, the IDL-to-Java mapping they support.
The scheme described above (and demonstrated in Example 4-1) for the generated server-side code for a
CORBA object is called an inheritance-based model. It
depends on the server-side implementation directly extending a
generated class (interfaceNamePOA
for
POA-compliant environments and
_interfaceNameImplBase
for pre-POA
environments). There is another option, called the
delegation model, available to you in terms of
how you “plug” your server code
into the CORBA environment.
The delegation model is based on a scheme in which an server-side
delegate is generated by the IDL compiler. This delegate extends the
generated skeleton class, and implements each of the mapped remote
methods by delegating the incoming method request to a delegate
object. This delegate object needs to implement the
interfaceNameOperations
interface generated by
the IDL compiler, but it doesn’t have to extend a
concrete or abstract base class. This can prove to be useful in cases
where you have a preexisting Java class with its own inheritance
scheme and want to “export” this
class through CORBA for remote access. Because Java prohibits
multiple inheritance, you don’t have the option of
extending both the existing class and the generated skeleton class.
With the delegation model, you can define a simple delegate class
that extends the preexisting Java class, and implements the
interfaceNameOperations
interface generated by
the compiler. You can then
“publish” an instance of the
original class by creating an instance of the new delegate class and
an instance of the generated delegation-based server object, and
associating the server object with your delegate.
We won’t provide full details of the delegation model here, but it’s important to realize that this option exists. It may prove useful in some situations. There are options available on the IDL-to-Java compiler to instruct it to generate delegation-based server objects (also referred to as ties). These compiler options are documented in Chapter 16.
The IDL interface shown in Example 4-1 is the IDL
equivalent of the Account
class we defined in
Example 3-1. The interface, named
Account
, is declared within some nested modules
(oreilly
, jent
,
corba
), and declares methods similar to the
Account
example in Chapter 3. Since this is IDL,
the various method argument and return value data types are
represented using IDL datatypes (e.g., string
instead of String
, etc.), and method arguments
are declared with in
,
inout
, or out
modifiers.
Example 4-1. An Account Interface Defined in IDL
// // [IDL] Account interface defined in IDL. // module oreilly { module jent { module corba { // Forward-declare the Account interface, for the typedefs below interface Account; // Declare some useful typedefs: a list of Accounts and of floats typedef sequence<Account> AccountList; typedef sequence<float> floatList; exception InsufficientFundsException {}; interface Account { // Get the name of the account owner string getName( ); // The account balance float getBalance( ); // Withdraw funds from the account void withdraw(in float amt) raises (InsufficientFundsException); // Deposit funds to the account void deposit(in float amt); // Transfer funds from the source account to this account void transfer(in float amt, in Account src) raises (InsufficientFundsException); // Similar to above, but perform transfers from a series of // source Accounts void transferBatch(in floatList amts, in AccountList srcs) raises (InsufficientFundsException); }; }; }; };
We can run the idlj compiler on this IDL interface using the following command line (Windows version):
C:\>idlj -fall Account.idl
This command creates the five Java classes described in the previous
sections: a Java version of the interface, a helper class, a holder
class, a client stub, and a server skeleton. The
-fall
option tells the compiler to generate both
client-side and server-side mapping interfaces (see Chapter 16 for complete details on the command-line
arguments for idlj).
The compiler creates the Java interface shown in Example 4-2, in a file named
Account.java
. This interface
doesn’t have much meat to it, because all of the
method declarations are generated in the
AccountOperations
interface are extended by this
interface, which is shown in Example 4-3. The
interface declaration in the IDL file is mapped directly to the
Account
Java interface declaration, with the
interface extending the AccountOperations
and
org.omg.CORBA.Object
interfaces. The module
declarations in the IDL file have been mapped into an
oreilly.jent.corba
package statement at the
beginning of all the generated Java files. The IDL data types have
been converted into the equivalent Java data types, and, since they
don’t require any special handling in a remote
method call, the in
method parameters in IDL are
mapped into regular Java input arguments.
Example 4-2. Java Mapping of the Account Interface
package oreilly.jent.corba; /** * oreilly/jent/corba/Account.java * Generated by the IDL-to-Java compiler (portable), version "3.1" * from ../Account.idl * Friday, July 6, 2001 8:11:50 AM EDT */ public interface Account extends AccountOperations, org.omg.CORBA.Object, org.omg.CORBA.portable.IDLEntity { } // interface Account
Example 4-3. AccountOperations Java Interface
package oreilly.jent.corba; /** * oreilly/jent/corba/AccountOperations.java * Generated by the IDL-to-Java compiler (portable), version "3.1" * from ../Account.idl * Friday, July 6, 2001 8:11:50 AM EDT */ public interface AccountOperations { // Get the name of the account owner String getName ( ); // The account balance float getBalance ( ); // Withdraw funds from the account void withdraw (float amt) throws oreilly.jent.corba.InsufficientFundsException; // Deposit funds to the account void deposit (float amt); // Transfer funds from the source account to this account void transfer (float amt, oreilly.jent.corba.Account src) throws oreilly.jent.corba.InsufficientFundsException; // source Accounts void transferBatch (float[] amts, oreilly.jent.corba.Account[] srcs) throws oreilly.jent.corba.InsufficientFundsException; } // interface AccountOperations
The compiler
also generates a helper class, called
AccountHelper
. We won’t provide
the full code listing for this generated class here, but it can be
found in the downloadable source code examples available for this
book, along with all of the other Java interfaces generated by the
IDL-to-Java compiler from this IDL interface.
The helper class is a standalone utility class that doesn’t extend any other interfaces:
abstract public class AccountHelper {
As mentioned earlier, the helper class has static methods that let
you read and write Account
objects to and from
CORBA I/O streams:
public static oreilly.jent.corba.Account read (org.omg.CORBA.portable.InputStream istream) public static void write (org.omg.CORBA.portable.OutputStream ostream, oreilly.jent.corba.Account value)
a type( )
method that provides the
TypeCode
for the mapped
Account
class:
synchronized public static org.omg.CORBA.TypeCode type ( )
and, most importantly, a narrow( )
method that
safely narrows a CORBA org.omg.CORBA.Object
reference into an Account
reference:
public static oreilly.jent.corba.Account narrow (org.omg.CORBA.Object obj)
Object narrowing is
CORBA’s equivalent to directly casting object
references -- we discuss why narrowing is necessary later, in Remote Object References and Narrowing. In the implementation of
the narrow( )
method, the helper class converts
a CORBA Object
reference to a reference to a
specific type. If the CORBA object can’t be narrowed
to the requested type (e.g., the passed object reference is a null
reference, the object’s
TypeCode
doesn’t match the
TypeCode
of the narrowed type, etc.), then the
narrow( )
method throws a
BAD_PARAM
exception.
The compiler generates a holder
class for the Account
class, which implements
the CORBA Streamable
interface.
public final class AccountHolder implements org.omg.CORBA.portable. Streamable
The holder class is a wrapper used when Account
objects are called for as out
or
inout
arguments in an IDL method. All holder
classes implement the Streamable
interface from
the org.omg.CORBA.portable
package, which
includes implementations of the _read( )
and
_write( )
methods of the
Streamable
interface:
public void _read (org.omg.CORBA.portable.InputStream i) public void _write (org.omg.CORBA.portable.OutputStream o)
This allows holders to be transmitted in remote method calls using
these _read( )
and _write( )
methods; these methods handle whatever serialization the
object needs. This functionality is similar to that provided by Java
serialization, but CORBA needs its own scheme because it is
independent of any particular language, and needs to provide this
serialization even if the target language doesn’t
provide this feature natively.
A holder contains a single
instance of the corresponding CORBA object (an
Account
, in this example) as a data member:
public oreilly.jent.corba.Account value = null;
This instance is initialized in the constructor of the holder:
public AccountHolder (oreilly.jent.corba.Account initialValue) { value = initialValue; }
When a holder object is passed into a remote method call as an
inout
argument, its _write( )
method is invoked. This method takes the object instance
contained by the holder class, serializes it, and streams it through
the ORB to the remote object server. When the remote method call
returns, the holder’s _read( )
method is invoked to read the (possibly updated) object from the
remote object server, and the holder object replaces its internal
value with the updated object.
As an example of using the holder
class, let’s define another IDL interface that
includes a method that uses an Account
as an
inout
parameter:
// IDL interface AccountManager { boolean updateAccount(inout Account account); };
The AccountManagerOperations
Java interface
generated from this IDL interface uses the
AccountHolder
class as the type for the
corresponding Java method parameter:
// Java public interface AccountManagerOperations { boolean updateAccount (AccountHolder account); } // interface AccountManagerOperations
The idlj
compiler generates a Java client stub
(_AccountStub
) for our CORBA interface. The
client stub implements the generated Account
Java interface and acts as a client-side proxy for a remote
Account
object:
public class _AccountStub extends org.omg.CORBA.portable.ObjectImpl implements oreilly.jent.corba.Account
When a client acquires a reference to a remote
Account
object (through any of the methods
we’ll describe later, in Finding and Using Remote Objects),
it actually receives an instance of this client stub class. The stub
has implementations of all the methods from the interface, as mapped
into the AccountOperations
interface that is the
parent of the Account
interface. The stub class
serves as a proxy for a remote server object, and each method
implementation in the stub generates a request to the ORB to make a
remote method call on the corresponding server-side object. The
method arguments are bundled up
(“marshalled”) and passed along
with the request to the ORB. We’re not going to go
into the details of the stub’s method
implementations, because you shouldn’t have to worry
much about them under normal conditions. But it is enlightening to
look at an example to see how your remote objects do what they do in
detail, using the core CORBA functions. As an example, here is the
generated client stub implementation of the getName( )
method from our Account
interface:
// Get the name of the account owner public String getName ( ) { org.omg.CORBA.portable.InputStream $in = null; try { org.omg.CORBA.portable.OutputStream $out = _request ("getName", true); $in = _invoke ($out); String $result = $in.read_string ( ); return $result; } catch (org.omg.CORBA.portable.ApplicationException $ex) { $in = $ex.getInputStream ( ); String _id = $ex.getId ( ); throw new org.omg.CORBA.MARSHAL (_id); } catch (org.omg.CORBA.portable.RemarshalException $rm) { return getName ( ); } finally { _releaseReply ($in); } } // getName
As mentioned, when a Java client gets
a reference to a remote Account
object, it is
given one of these stub objects. The client can make method calls on
the stub object, and the stub converts these calls into corresponding
requests to the ORB to invoke the methods on the remote object and
send back the results.
In pre-POA versions of Java IDL
(JDK 1.2 and 1.3), the IDL-to-Java compiler generates a server
implementation base class that follows the
ImplBase
scheme. An
_AccountImplBase
base class for the server
implementation is generated:
public abstract class _AccountImplBase extends org.omg.CORBA.portable.ObjectImpl implements oreilly.jent.corba.Account, org.omg.CORBA.portable.InvokeHandler
This base class provides the basic
“plumbing” for our server
implementation. As mentioned earlier in this section,
it’s principle purpose is to receive requests from
remote clients through the ORB and directs them to the proper method
on the server implementation class. In the
ImplBase
version, all this work is done by the
server skeleton’s _invoke( )
method, which is called by the ORB:
public org.omg.CORBA.portable.OutputStream _invoke (String method, org.omg.CORBA.portable.InputStream in, org.omg.CORBA.portable.ResponseHandler rh)
The _invoke( )
method figures out which method
is being called, unpacks the method arguments (if any) from the
request, and calls the method directly on itself.
Note that the server skeleton doesn’t have
implementations of the remote methods declared in the
Account
interface (also note that
it’s declared as an abstract class for this reason).
The idlj compiler doesn’t do
everything for you; you still need to create a server implementation
for your interface by extending this base class, and provide the
logic behind the remotely accessible operations on your CORBA object.
In JDK 1.4 and later, the idlj
compiler
generates server implementation skeleton classes that follow the POA
specification. In this model, the base class for the server
implementation of our Account
interface is
called AccountPOA
:
public abstract class AccountPOA extends org.omg.PortableServer.Servant implements oreilly.jent.corba.AccountOperations, org.omg.CORBA.portable.InvokeHandler
The structure of the class is very similar to the old
ImplBase
format (method requests are passed by the
ORB to the _invoke( )
method, where they are
delegated to the proper method on the implementation class), but the
particulars of the ORB interface follow the POA specification. This
helps to guarantee that your CORBA object implementations will
migrate easily between different CORBA ORB implementations.
So, we’ve written an
IDL interface and generated the Java interface and support classes
for it, including the client stub and the server skeleton. Now we
need to create concrete server-side implementations of all of the
methods on our interface. We do this by subclassing from the
server-side skeleton class generated by the idlj
compiler. For our example, we need to subclass either
_AccountImplBase
or
AccountPOA
(depending on whether
we’re using the POA or pre-POA versions of Java
IDL). We also need to implement the various methods defined on the
Account
IDL interface and mapped through the
idlj
compiler into the
AccountOperations
Java interface. The
AccountImplPOA
class in Example 4-4 shows the implementation for the POA-compliant
case (the pre-POA implementation is virtually identical, with a few
minor differences that we’ll mention at the end of
this section). The pre-POA version won’t be shown
here, but it is included in the source code examples for this book,
downloadable from the O’Reilly web site.
As you look through the implementation class, you’ll
notice that the method implementations are similar to the RMI version
of the Account
server implementation example in
Chapter 3. The only
real difference is that this AccountImpl
class
extends the generated AccountPOA
class (and
through it, the generated AccountOperations
interface and the CORBA Servant
class), while
the RMI server implementation implements the RMI
Account
interface and extends
java.rmi.server.UnicastRemoteObject
. So in the
same way that the two remote object schemes are analogous to each
other in terms of functionality, the particulars of the server
implementations in each case are analogous as well.
Example 4-4. Server Implementation of the Account CORBA Object
/** * AccountImplPOA: Implementation of the Account remote interface */ public class AccountImplPOA extends AccountPOA { // Our current balance private float mBalance = 0; // Name on account private String mName = ""; // Create a new account with the given name public AccountImplPOA(String name) { mName = name; } public String getName( ) { return mName; } public float getBalance( ) { return mBalance; } // Withdraw some funds public void withdraw(float amt) throws InsufficientFundsException { if (mBalance >= amt) { mBalance -= amt; // Log transaction... System.out.println("--> Withdrew " + amt + " from account " + getName( )); System.out.println(" New balance: " + getBalance( )); } else { throw new InsufficientFundsException("Withdrawal request of " + amt + " exceeds balance of " + mBalance); } } // Deposit some funds public void deposit(float amt) { mBalance += amt; // Log transaction... System.out.println("--> Deposited " + amt + " into account " + getName( )); System.out.println(" New balance: " + getBalance( )); } // Move some funds from another (remote) account into this one public void transfer(float amt, Account src) throws InsufficientFundsException { if (checkTransfer(src, amt)) { src.withdraw(amt); this.deposit(amt); // Log transaction... System.out.println("--> Transferred " + amt + " from account " + getName( )); System.out.println(" New balance: " + getBalance( )); } else { throw new InsufficientFundsException("Source account balance is less " + "than the requested transfer."); } } // Make several transfers from other (remote) accounts into this one public void transferBatch(float[] amts, Account[] srcs) throws InsufficientFundsException { // Iterate through the accounts and the amounts to be // transferred from each for (int i = 0; i < amts.length; i++) { float amt = amts[i]; Account src = srcs[i]; // Make the transaction this.transfer(amt, src); } } // Check to see if the transfer is possible, given the source account private boolean checkTransfer(Account src, float amt) { boolean approved = false; if (src.getBalance( ) >= amt) { approved = true; } return approved; } }
[13] Although Java IDL was a standard part of Java 1.2, Sun only offered the early-access version of its idltojava compiler, which you have to download separately from http://developer.java.sun.com/developer/earlyAccess/jdk12/idltojava.html.
Get Java Enterprise in a Nutshell, 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.