Method and system for assembling software components6256780Abstract An assembly method and system for assembling components into an assembly. An assembly provides connectors through which the components can be exported and through which an external entity can be imported. An exported component can be connected to another assembly by importing that exported component into that other assembly. A connector can both expose components to be exported and expose connections through which an external entity can be imported. The assembly provides a mechanism through which the connectors can be exposed to external entities. An external entity can use the connector providing mechanism to retrieve a connector and then use the retrieved connector to export components and establish connections by importing external entities. In addition, the use of a standard connector and of a standard connector providing mechanism allows predefined components to be assembled into assemblies that can expose certain behavior of the components through the connectors in a predefined manner. Claims What is claimed is: Description TECHNICAL FIELD
IConnector
interface IConnector : IUnknown
{
GetElement (LONG dwindex, REFIID riid, void **ppvElement);
Connect (LONG dwRoleld, IUnknown *punkOther);
GetConnect (LONG dwRoleld, REFIID riid, void **ppvConnectee);
BatchConnect (DWORD cGet, // number of Gets
LONG rgidxGet[] // array of indices to get
IID rgiidGet[], // corresponding iids
IUnknown *rgpunkGet[], // array of elements returned
DWORD rgfGOptional[], // flag gets that can fail
DWORD cConnU, // number of Connects
LONG dwrole[], // array of roles to connect
IUnknown *rgpunkOther[], // array of elts to connect
DWORD rgfCOptional[], // flag connections that can fail
HRESULT rghr[], // array of hresults for individual
connections
);
};
The IConnector interface of a connector object provides methods for retrieving exposed components, connecting an external entity through an exposed connection, and retrieving reference to the external entity connected through a certain connection. As an optimization, the IConnector interface also provides a method through which multiple exposed components can be retrieved and multiple connections established through a single invocation of the method. IConnector::GetElement The method GetElement is used by an external entity to retrieve a reference to an exposed component (which may be either an element or a sub-assembly). This method is passed the index of a component and an interface identifier of the component and returns a reference to the identified interface of the indexed component. IConnector::Connect The method Connect is used to connect an external entity through an exposed connection to a component of the assembly. The method is passed the role of the connection and a reference to the external entity. If Connect is called more than once, each call replaces the previous connection with a new one. The connection can be disconnected by passing the role of the connection along with a NULL-valued reference. IConnector::GetConnected The method GetConnected is used to retrieve a reference to the external entity that is connected through a connection. The method is passed a role and an interface identifier, and returns a reference to the identified interface of the external entity connected to that role. IConnector::BatchConnect The method BatchConnect is used to perform retrieval of references to multiple exposed components and to establish multiple connections with a single invocation. To retrieve the references, the method is passed an array of indexes and an array interface identifiers, and returns an array of references to the identified interfaces of the indexed components. To establish the connections, the method is passed an array of roles and an array of references to external entities to be connected to the connections identified by the roles. The method is also passed an array for the retrieved components and an array for the established connections that indicates whether the retrieval or connecting for that component or connection is optional. If it is not optional, then when a failure occurs when retrieving that component or establishing that connection, the method returns without returning any components and without establishing any connections.
IInitConnector
interface IInitConnector : IUnknown
{
Init(DWORD cInit, // total count of connector
interfaces on this connector
REFIID conniid, // connector IID being initialized
DWORD cGet, // number of implemented Gets
LONG rgidxGet[], // implemented Get indices
IUnknown *rgpunkGet[], // corresponding elements
DWORD cConnU, // number of connects implemented by
unary connectors
LONG rgdwroleU[], // connect roles implemented by
unary connectors
IID rgiidconnDel[], // iids for corresponding delegatees
IConnector *rgpconnDel[], // corresponding delegatees
LONG rgdwroleDel[], // corresponding delegatee roles
DWORD cConnB, // number of connects implemented by
binary connectors
LONG rgdwroleB[], // connect roles implemented by
binary connectors
IBinaryConnector *rgpbconnDel[], // corresponding delegatees
IUnknown *rgpunkOther[], // corresponding other
DWORD rgfLeftRight[], // if true connect(other,.) else
connect(.,other)
);
Uninit ();
};
The IInitConnector interface of a connector object provides methods for initializing and uninitializing of the connector object. A connector is initialized by registering the components and the connections of the assembly that are to be exposed by the connector. To register components to be exposed, the method Init is passed an array of indexes and an array of references to the components to be identified by those indexes. The method Init is also passed information for registering unary connections that includes an array of roles for connections to be exposed, an array of interface identifiers of the component objects (i.e., delegatees), an array of IConnector interfaces for the delegatees, and an array of roles for the delegatees. The method Init is also passed information for binary connections that include an array of roles, an array of IBinaryConnector interfaces for the delegatees, an array of references to component objects to be connected as part of the binary connection, and an array indicating whether the component object should be connected to the left or right of the binary connection. The method Uninit unregisters all components and connections that have been registered. IInitFromTemplateStream Interface IInitFromTemplateStream: IUnknown { HRESULT InitFromTemplate (IStream *pstm); } The IInitFromTemplateStream interface of an assembly object has one method that controls the initialization of the assembly object from a passed stream. The initialization data is static configuration data along with the initialization data for assembly parameters. Other assembly data, such as ambient properties, can be passed to an assembly via a connection. One format for the static configuration data and the initialization data for assembly parameters is described below in detail. The initialization data for the assembly parameters is passed in the stream and the static configuration data can be available through the class identifier of the assembly. The assembly can be customized through the parameters. IBinaryConnector Interface IBinaryConnector: IUnknown { Connect (Unknown punkSource, IUnknown punkTarget); } The IBinaryConnector interface is used when neither end of a desired connection has the needed connection behavior. The method Connect is passed references to the two ends of the connection and performs the behavior necessary to connect them. The IBinaryConnector interface is implemented by intermediary objects and invoked by connector objects to establish binary connections. FIG. 5 is a more detailed block diagram of a sample implementation of assembly3 of FIG. 3. In this figure, the various interfaces used by the assembly system are illustrated along with example data structures used to support the exposing of components and connections. The assembly 500 comprises assembly object 501, connector 503, element 506, and element 507. The assembly object 501 exposes the IConnectorProvider interface through which an external entity can retrieve a reference to the connector 503 of the assembly and exposes the IInitFromTemplateStream interface through which the assembly can be initialized. The assembly object includes connector table 502. The connector table contains the name of the connector along with a reference to the IUnknown interface of the connector object. The connector 503 exposes the IInitConnector interface and the IConnector interface. The IInitConnector interface is used during initialization of the assembly to register components and connections to be exposed. The IConnector interface is used to retrieve references to exposed components and to establish connections through exposed connections. The connector contains an index table 504 and a unary connection table 505. The index table contains an entry for each component that is exposed through the connector. Each entry contains the index of the component and a reference to the component. The unary connection table contains an entry for each unary connection. Each entry contains the identification of the role of the connection and a pointer to an interface of a delegatee object to which the connection process is to be delegated. In this example, the interface of the delegate object is the IConnector interface. In one embodiment, each entry of the unary connection table may contain the role of the connection, the interface identifier of the delegatee object, a reference to the delegates object, and a role for the delegatee object. Although not needed in this example, the connector object may also include a binary connection table. FIG. 6 is a low-level flow diagram of the process of initializing an assembly3 of FIG. 5. In step 601, the process invokes the function CoCreateInstance passing the class identifier of the assembly. The function creates an instance of an assembly object 501 of the passed class identifier and returns a reference to the assembly object. In step 602, the process invokes the function CoCreateInstance passing the class identifier of the first element. This function creates an instance of the passed class identifier and returns a reference to the element. In step 603, if the first element supports the IInitFromTemplateStream interface, then the processes invokes the method Init of that interface passing a reference to the string that contains the initialization data for the element. Steps 604 and 605 are similar to steps 602 and 603 except that they are applied to the second element of the assembly. In step 606, the process invokes the function CoCreateInstance to create an instance of the standard connector and to return a reference to the IInitConnector interface. In step 607, the process invokes the method Init of the IInitConnector interface to initialize the connector with the components and connections to be exposed. In step 608, the process adds an entry for the connector to the connector table of the assembly object. FIG. 7 illustrates the data structures and interfaces of an assembly object and its connector objects in one embodiment. As illustrated, the assembly object 700 provides IConnectorProvider interface, and the connector object provides the IConnector interface. The assembly object includes connector table 701 that contains an entry for each connector within the assembly. Each entry contains the name of the connector along with a reference to the connector. The connector object 702 contains an index table 703, a unary connection table 704, and a binary connection table 705. The index table contains an entry for each component that is exposed by the connector. Each entry contains the index of the component along with a reference to an exposed interface of the component. The unary connection table contains an entry for each unary connection exposed by the connector. Each entry identifies the name of the role of the connection and information relating to the delegatee object that is responsible for performing the connection. The delegatee information includes the interface identification (iidDel) of the delegatee object, a reference to the delegatee object (pDel), and the role of the delegatee object (roleDel). The binary connection table contains an entry for each binary connection exposed by the connector. Each entry identifies the name of the role of the connection and information relating to the intermediary object with the IBinaryConnector interface and the component object with which the binary connection is to be established. The information includes a reference to the intermediary object (pDel), a reference to the component object (pOther), and an indication whether the component object is to be on the left or right of the binary connection. FIG. 8 is a flow diagram of one possible implementation of the method GetConnector of the IConnectorProvider interface. This method is passed the name of a connector and the identifier of an interface, scans the connector table for any entry with that name, and returns a reference to the identified interface of the connector with name. In step 801, the method selects the next entry in the connector table starting with the first entry. In step 802, if all the entries have already been selected, then no entry with the passed name is in the connector table and the method returns an error indicator, else the method continues at step 803. In step 803, if the name of the selected entry matches the passed name, then the method continues at step 804, else the method loops to step 801 to select the next entry. In step 804, the method retrieves the pointer to the named connector from the selected entry. In step 805, the method queries the interface of the connector to retrieve a reference to the passed interface identifier. In step 806, if the query interface was successful, then the method returns a success indicator, else the method returns an error indicator. FIG. 9 is a flow diagram of one possible implementation of the method Init of the IInitConnector interface. This method is passed a list of the components, unary connections, and the binary connections to be exposed by the connector. The method initializes the internal tables of the connector. In steps 901-903, the method loops adding an entry in the index table for each component that is to be exposed. In step 901, the method selects the next component to be exposed, starting with the first component. In step 902, if all the components have already been selected, then the method continues at step 904, else the method continues at step 903. In step 903, the method stores the index and a reference to the selected component in an entry of the index table and loops to step 901 to select next component. In steps 904-906, the method loops adding an entry for each unary connection to the unary connection table. In step 904, the method selects the next unary connection to be exposed starting with the first. In step 905, if all the unary connection have already been selected, then the method continues at step 907, else the method continues at step 906. In step 906, the method stores the role, the interface identifier of the delegates object, a pointer to the delegatee object, and the role of the delegates object in an entry of the unary connection table and loops to step 604 to select the next unary connection. In step 907, the method loops adding an entry to the binary connection table for each binary connection to be exposed. In step 907, the method selects the next binary connection to be exposed starting with the first. In step 908, if all the binary connections have already been selected, then the method returns, else the method continues at step 909. In step 909, the method stores the role, the reference to the delegatee object, the reference to the component object, and the indication as to left or right in an entry of the binary connection table and loops step 907 to select the next binary connection. FIG. 10 is a flow diagram of one possible implementation of the method GetElement of the IConnector interface. This method is passed the index of an exposed component along with an interface identifier and returns the reference to the identified interface of the exposed component. In steps 1001-1003, the method loops searching for an entry in the index table that matches the passed index. In step 1001, the method selects the next entry in the index table starting with the first. In step 1002, if all the entries have already been selected, then there is no entry with the passed index in the index table and the method returns an error indicator, else the method continues at step 1003. In step 1003, if the index of the selected entry matches the passed index, then the method continues at step 1004, else the method loops to step 1001 to select next entry. In step 1004, the method retrieves the reference in the selected entry. In step and 1005, the method retrieves a reference to the passed interface identifier for the component by invoking the method QueryInterface of the indexed component. In step 1006, if the retrieval of the reference to the identified interface was successful, then the method returns a success indicator, else the method returns an error indicator. FIG. 11 is a flow diagram of one possible implementation of the method Connect of the IConnector interface. This method is passed the role of a connection along with a reference to the external entity to be connected. The method effects the connection by delegating responsibility of a unary connection to a component object and a binary connection to an intermediary object. In steps 1101-1103, the method loops determining whether the passed role is in the unary connection table. In step 1101, the method selects the next entry in the unary connection table starting with the first. In step 1102, if all entries have already been selected, then the method continues at step 1105, else the method continues at step 1103. In step 1103, if the role of the selected entry matches the passed role, then the method continues at step 1104, else the method loops to step 101 to select the next entry. In step 1104, the method delegates the connection to the component object referenced in the selected entry and returns. In steps 1105-1110, the method loops determining whether the passed role is in the binary connection table. In step 1105, the method selects the next entry in the binary connection table starting with the first. In step 1106, if all the entries in the binary connection table have already been selected, then the method returns an error indicator, else the method continues at step 1107. In step 1107, if the role of the selected entry matches the passed role, then the method continues at step 1108, else the method loops step 1105 to select the next entry. In step 1108, if the left/right flag indicates left, then the method continues at step 1110, else the function continues at step 1109. In step 1109-1110, the method delegates connection to the intermediary object and returns. FIG. 12 is a block diagram illustrating one possible embodiment an assembly which contains two components that are elements and one component that is a sub-assembly. Assembly 1200 contains sub-assembly 1201, elements 1202, 1203, and connectors 1204, 1205. The sub-assembly corresponds to assembly3 of FIG. 1. Connector 1204 exposes element 1202 and a connection to element 1203. Connector 1205 exposes sub-assembly 1201, element 1208 of the sub-assembly, and a connection to element 1208 of the sub-assembly. Element 1203 exposes a connection that is internally connected (within assembly 1200) to element 1207 of the sub-assembly. In one embodiment, the assembly system provides a PCODE interpreter that inputs pseudo-code that describes the initialization of an assembly. The PCODE interpreter is implemented by the method InitFromTemplateStream of the IInitFromTemplateStream interface of the assembly object. The PCODE interpreter initializes the assembly object in accordance with the pseudo-code for the assembly object as modified by assembly parameters passed via the stream to the method. Table 1 provides a description of the pseudo-code in one embodiment of the PCODE interpreter. The table contains one entry for each pseudo-code instruction (e.g., "Create Instance"). The entry identifies the operation code for the instruction (e.g., "10") along with its operands (e.g., "<result-index>" and "<param-index>"). The entry also contains a general description of the interpretation performed by the PCODE interpreter for that instruction. The array "object [ ]" referenced in Table 1 logically is an array maintained by the interpreter to keep track of the components and connectors of the assembly. The variable "param-index" refers to an internal parameter that is static or an external parameter this dynamic and is passed in the stream. The array "OA [ ]" is an array of indexes into the object [ ]. Each operation has a corresponding batch version in which multiple executions of the operation can be indicated by a single instruction. The format of the batch instruction is the batch operation code followed by the number of executions, followed by an array of the first operands, followed by an array of the second operands, and so on.
TABLE 1
Create Instance
code: 10
instruction:
10 <result-index><param-index>
interpretation:
object[<result-index >] = CoCreateInstance(clsid of param-index)
object[<result-index>] ->
IInitFromTemplateStream::InitFromTemplate (substream of param-index)
Create Aggregate Part
code: 30
instruction:
30 <result-index > <param-index> <outer-index>
interpretation:
object[<result-index> = CocreateInstance(object[<outer-index>],
clsid of param-index)
object[<result-index >] ->
IInitFromTemplateStream::InitFromTemplate (substream of param-index)
Initialize Connector
code: 50
instruction:
50 <index> <get-count> <get-index-array> <get-elt-index-array>
<uconn-count> <role-array> <del-index-array>
<del-role-array> <bconn-count> <brole-array>
<bdel-index-array> <other-index-array>
<left-right-array>
interpretation:
object[<index>] ->IInitConnector::Init(<get-count>,
<get-index-array>, OA[<get-elt-index-array>],
<uconn-count>, <urole-array>, <iid-array>,
OA[<udel-index-array>], <del-role-array>,
<bconn-count>, <brole-array>,
OA[<bdel-index-array>], OA[<other-index-array>],
<left-right-array>)
Name Connector
code: 70
instruction:
70 <index> <connector-uid>
interpretation:
correlates the <connector-guid> in the assembly object so that
object[<index>] ->
iConnectorProvider::GetConnector(<connector-guid>, iid, ppv) returns the
result of
QueryIntenface for iid for <connector-uid>
Get Connector
code: 90
instruction:
90 <result-index> <provider-index> <connector-uid>
<connector-iid>
interpretation:
object[<result-index>] object[<provider-index>] ->
IConnectorProvider::GetConnector(<connector-guid>,
<connector-iid>, ppv)
Get Connector Element
code: 110
instruction:
110 <result-index> <connector-index> <connector-iid> <index>
<iid>
interpretation:
object[<result-index>] = object[<connector-index>] ->
QueryInterface(<connector-iid>) ->
IConnector::GetElement(<index>, <iid>)
Unary Connect
code: 130
instruction:
130 <connector-index> <connector-iid> <role-id> <other-index>
interpretation:
object[<connector-index>] -> QueryInterface(<connector-iid>) ->
IConnector::Connect((<role-id>,
object[<other-index>])
Unary Batch Connect
code: 150
instruction:
150 <connector-index> <connector-iid> <get-count> <index-array>
<iid array> <optional get-flag-array>
<result-index-array> <connect-count>
<role-array> <other-index-array> <optional connect-
flag-array>
interpretation:
object[<connector-index>] -> QueryInterface(<connector-iid>) ->
IConnector::BatchConnect((<get-count>,
<index-array> <iid-array>, <optional
get-flag-array>, &rgpunkGet, <connect-count> <role-
array> OA[<other-index-array>] <optional
connect-flag-array>, &rghr)
The interpreter assigns the rgpunkGet results to
<result-index-array> slots in the object array. The array rghr of
HRESULTs returned from IConnector::BatchConnect
is handled by the interpreter. It is
not visible in the PCODE.
Binary Connect
code: 170
instruction: 170 <connector-index> <source-index> <dest-index>
interpretation:
object[<connector-index>] ->
IBinaryConnector::Connect(object[<source-index>], object[<dest-index>])
End
code: 0
instruction:
0
interpretation:
end instruction
FIG. 13 is a block diagram illustrating an assembly that is initialized by the PCODE interpreter. The assembly A11300 contains assembly object SA11301, connector Av11302, element A 1303, and sub-assembly A21304. Sub-assembly A21304 contains assembly object SA21305, connector Av21306, element B 1307 and element C 1308. Element B exposes a connection (role "iBC"). Sub-assembly A2 exposes element B (index "iB") and element C (index "iC") and a connection to element C (role "rCA") through connector Av2. Element A exposes a connection (role "rAB"). Assembly A1 exposes element A (index "iA") through connector Av1. Element C is connected to (imported to) element B via connection rBC and element B of sub-assembly A2 is connected to (imported to) element A via connection rAB. Table 2 describes the creation and initialization of the assembly of FIG. 14. This creation and initialization can be either programmed directly by a developer or specified as pseudo-code that is executed by the PCODE interpreter. The indentation of the statements of Table 2 reflect processing performed by the invoked method. For example, statement 7 contains a statement that invokes the method Init of the IID_InitFromTemplateStream of the subassembly SA2, and statements 8-20 represent the processing performed during that invocation. Once assembly A1 is developed, the assembly is created and initialized by executing statements 1 and 2. The method Init of the IID_InitFromStreamTemplate interface can either be developed to include the code representing statements 3-33 and using any external parameters specified in the passed stream or be developed to retrieve the internal parameter for the class of assembly A1 and invoke the PCODE interpreter passing internal and external parameters.
TABLE 2
1 CoCreateInstance(CLSID_SA1, IID_IUknown, &pSA1)
2 PSA1->QueryInterface(IIDIinitFromTeplateStream)->Init(pstm)
3 CoCreateInstance(CLSID_A, IID_IUknown, &pA)
4 pA->QueryInterface(IID_IinitFromTeplateStream)->Init(pstm)
5
6 CoCreateInstance(CLSID_SA2, IID_IUnknown &pSA2)
7 pSA2->QueryInterface(IID_IinitFromTeplateStream)->Init(pstm) //
load p-code for SA2
8
9 CoCreateInstance(CLSID_B, IID_IUnknown, &pB)
10 pB->QueryInterface(IID_IinitFromTeplateStream)->Init(pstm)
11
12 CoCreateInstance(CLSID_C, IID_IUnknown, &pC)
13 pC->QueryInterface(IID_IinitFromTeplateStream)->Init(pstm)
14
15 pB->QueryInterface(IID_IConnector)->Connect(rBC, pC) //
connects B to C
16
17 CoCreateInstance(CLSID_StdConnector, IID_IInitConnector, &pAv2)
// create connector for SA2
18 pAv2 ->Init(2, {iB, iC}, {pB, pC}, 1, {rCA}, {pC}, {rCA}, 0, 0,
0, 0, 0) // populate the connector
19
20 correlate connector pAv2 and GUID_Av2 with assembly pSA2 //
Add connector to SA2
21
22 pV =
pSA2->QueryInterface(IID_IConnectorProvider)->GetConnector(GUID_Av2,
IID_Iconnector)
23
24 // Connect A to B
25 pB = pV->GetElement(iB, IID_IUnknown)
26 pA->QueryInterface(IID_IConnector)->Connect(rAB, pB)
27
28 // Connect C to A
29 pV ->Connect(rCA, pA)
30
31 CoCreateInstance(CLSID_StdConnector, IID_IInitConnector &pAv1) //
create connector for SA1
32 pAv1 ->Init(1, iA, pA, 0, 0, 0, 0, 0, 0, 0, 0, 0) //
populate the connector
33 correlate connector pAv1 and GUID_Av1 with assembly pSA1 //
Add connector to SA1
FIG. 14 is a block diagram indicating the internal data structures in possible embodiment of the assembly of FIG. 13. Table 3 lists the pseudo-code for the creation of assembly A1 and assembly A2. When the PCODE interpreter encounters each PCODE instruction it performs the interpretation as shown in Table 2. The pseudo-code for assembly A1 uses three parameters as indicated by the "{0, 1, 2}" of the "pcBatchCreateInstance," which is the batch form of the CreateInstance operation. The external parameters may be specified by higher index numbers (i.e., parameter index 2). Thus, the type of the A element can specified at the time the assembly is created to customize the assembly.
TABLE 3
A1
pcBatchCreateInstance 3 {SA2, Av1, A} {0, 1, 2}
pcGetConnector V, GUID_Av2, IID_Iunknown
pcUaryBatchConnect V, 1, {iB}, {IID_Iunknown}, {false}, {B}, 1,
{rCA}, {A}, {false}
pcUaryConnect A, IID_IConnector, rAB, B
pcInitizeConnector Av1, IID_Unknown, 1, {iA},
{A}, 0, 0, 0, 0, 0, 0, 0, 0, 0
pcNameConnector Av1, GUID_Av1
pcEnd
A2
pcBatchCreateInstance 3 {Av2, B, C} {0, 1, 2}
pcUaryConnect B, IID_IConnector, rBC, C
pcInitizeConnector Av2, IID_Unknown, 2, {iB, iC}, {B, C}, 1,
{rCA}, {C}, {rCA}, 0, 0, 0, 0, 0
pcNameConnector Av2, GUID_Av2
pcEnd
FIG. 15 is a block diagram illustrating one embodiment of a data structure that holds information that describes and is used for initialization assembly. This PCODE assembly structure includes a Header table 1501, a Blob table 1502, and a PCODE table 1503. The Header table contains the GUID of the assembly, a version number, the class identifier of the assembly, an external parameter count, the size of the object array, and the count of the number of connectors provided by the assembly. The Blob table contains a Blob header 1502a and various Blobs 1502b. (A "Blob" refers to any collection of data.) The Blob header contains a GUID of the Blob, a count of the number of Blobs in the Blob table, and a pointer to each Blob. A Blob contains the class identifier of the object represented by the Blob, a flag indicating whether this Blob contains Blob data or a table of external parameters for the Blob, the length of the Blob, and the Blob data. Each Blob corresponds to an object that can be instantiated by the pseudo-code when the assembly is created. The Blob data is passed to the instantiated object to initialize it. Each Blob corresponds to an internal parameter. When the object to be instantiated is itself an assembly the Blob data is a parameter index table 1502c that contains the parameter indexes of the Blobs that are to be passed to the assembly as external parameters. External parameters are passed in a Blob table. Thus, the PCODE interpreter creates a Blob table containing the Blobs for the parameters indexed by the parameter Blob table. The PCODE table includes a PCODE header 1503a and various PCODE segments 1503b that contain the PCODE. From the foregoing it will be appreciated that, although specific embodiments of the invention have been described herein for purposes of illustration, various modifications may be made without deviating from the spirit and scope of the invention. Accordingly, the invention is not limited except as by the appended claims.
|
Same subclass Same class Consider this |
||||||||||
