VisiBroker for C++ Developer’s Guide : Using VisiBroker Interceptors

Using VisiBroker Interceptors
This section provides an overview of the VisiBroker Interceptors (Interceptors) framework, walks through a Interceptor example, and describes some advanced features such as Interceptor factories and chaining Interceptors. This section also covers the expected behaviors when both Portable Interceptors and VisiBroker Interceptors are used in the same service.
Interceptors overview
Similar to Portable Interceptors, VisiBroker Interceptors offers VisiBroker ORB services a mechanism to intercept normal flow of execution of the ORB. There are two kinds of VisiBroker Interceptors:
Client Interceptors are system-level Interceptors which are called when a method is invoked on a client object.
Server Interceptors are system-level Interceptors which are called when a method is invoked on a server object.
To use VisiBroker Interceptors, you declare a class which implements one of the Interceptor interfaces. Once you have instantiated an Interceptor object, you register it with its corresponding Interceptor manager. Your Interceptor object is then notified by its manager when, for example, an object has had one of its methods invoked or its parameters marshaled or demarshaled.
An important difference between VisiBroker interceptors and Portable interceptors is that VisiBroker interceptors do not get invoked for co-located calls. Therefore, users have to make a cautious decision when choosing which interceptor to use.
Note
If you want to intercept an operation request before it is marshaled on the client side or if you want to intercept an operation request before it is processed on the server side, use object wrappers, described in “Using object wrappers”.
Interceptor interfaces and managers
Interceptor developers derive classes from one or more of the following base Interceptor API classes which are defined and implemented by the VisiBroker.
Client Interceptors
There are currently two kinds of client Interceptor and their respective managers:
BindInterceptor and BindInterceptorManager
ClientRequestInterceptor and ClientRequestInterceptorManager
For more details about client Interceptors, see “Using Portable Interceptors”.
BindInterceptor
A BindInterceptor object is a global Interceptor which is called on the client side before and after binds.
class _VISEXPORT BindInterceptor : public virtual VISPseudoInterface {
public:
virtual IOP::IORValue_ptr bind(IOP::IORValue_ptr ior,
CORBA_Object_ptr obj,
CORBA::Boolean rebind,
VISClosure& closure) = 0;
virtual IOP::IORValue_ptr bind_failed(IOP::IORValue_ptr ior,
CORBA_Object_ptr object,
VISClosure& closure) = 0;
virtual void bind_succeeded(IOP::IORValue_ptr ior,
CORBA_Object_ptr object,
CORBA::Long profile_index,
interceptor::InterceptorManagerControl_ptr control,
VISClosure& closure) = 0;
virtual void exception_occurred(IOP::IORValue_ptr ior,
CORBA_Object_ptr object,
CORBA_Environment_ptr env,
VISClosure& closure) = 0;
};
ClientRequestInterceptor
A ClientRequestInterceptor object may be registered during a bind_succeeded call of a BindInterceptor object, and it remains active for the duration of the connection. Two of its methods are called before the invocation on the client object, one (preinvoke_premarshal) before the parameters are marshaled and the other (preinvoke_postmarshal) after they are. The third method (postinvoke) is called after the request has completed.
class _VISEXPORT ClientRequestInterceptor : public virtual VISPseudoInterface {
public:
virtual void preinvoke_premarshal(CORBA::Object_ptr target,
const char* operation,
IOP::ServiceContextList& servicecontexts,
VISClosure& closure) = 0;
virtual void preinvoke_postmarshal(CORBA::Object_ptr target,
CORBA_MarshalInBuffer& payload,
VISClosure& closure) = 0;
virtual void postinvoke(CORBA::Object_ptr target,
const IOP::ServiceContextList& service_contexts,
CORBA_MarshalInBuffer& payload,
CORBA::Environment_ptr env,
VISClosure& closure) = 0;
virtual void exception_occurred(CORBA::Object_ptr target,
CORBA::Environment_ptr env,
VISClosure& closure) = 0;
};
Server Interceptors
There are the following kinds of server Interceptors:
POALifeCycleInterceptor and POALifeCycleInterceptorManager
ActiveObjectLifeCycleInterceptor and ActiveObjectLifeCycleInterceptorManager
ServerRequestInterceptor and ServerRequestInterceptorManager
IORCreationInterceptor and IORCreationInterceptorManager
For more details about server Interceptors see , “Using Portable Interceptors.”
POALifeCycleInterceptor
A POALifeCycleInterceptor object is a global Interceptor which is called every time a POA is created (using the create method) or destroyed (using the destroy method).
class _VISEXPORT POALifeCycleInterceptor : public virtual VISPseudoInterface {
public:
virtual void create(PortableServer::POA_ptr _poa,
CORBA::PolicyList& _policies,
IOP::IORValue*& _iorTemplate,
interceptor::InterceptorManagerControl_ptr _poaAdmin) = 0;
virtual void destroy(PortableServer::POA_ptr _poa) = 0;
};
ActiveObjectLifeCycleInterceptor
An ActiveObjectLifeCycleInterceptor object is called whenever an object is added to the Active Object Map (using the create method) or after an object has been deactivated and etherealized (using the destroy method). The Interceptor may be registered by a POALifeCycleInterceptor on a per-POA basis at POA creation time. This Interceptor may only be registered if the POA has the RETAIN policy.
class _VISEXPORT ActiveObjectLifeCycleInterceptor : public virtual VISPseudoInterface {
public:
virtual void create(const PortableServer::ObjectId& _oid,
PortableServer_ServantBase* _servant,
PortableServer::POA_ptr _adapter) = 0;
virtual void destroy(const PortableServer::ObjectId& _oid,
PortableServer_ServantBase* _servant,
PortableServer::POA_ptr _adapter) = 0;
};
ServerRequestInterceptor
A ServerRequestInterceptor object is called at various stages in the invocation of a server implementation of a remote object before the invocation (using the preinvoke method) and after the invocation both before and after the marshaling of the reply (using the postinvoke_premarshal and postinvoke_premarshal methods respectively). This Interceptor may be registered by a POALifeCycleInterceptor object at POA creation time on a per-POA basis.
class _VISEXPORT ServerRequestInterceptor : public virtual VISPseudoInterface {
public:
virtual void preinvoke(CORBA::Object_ptr _target, const char* _operation,
const IOP::ServiceContextList& _service_contexts,
CORBA_MarshalInBuffer& _payload,
VISClosure& _closure) = 0;
virtual void postinvoke_premarshal(CORBA::Object_ptr _target,
IOP::ServiceContextList& _service_contexts,
CORBA::Environment_ptr _env,
VISClosure& _closure) = 0;
virtual void postinvoke_postmarshal(CORBA::Object_ptr _target,
CORBA_MarshalOutBuffer& _payload,
VISClosure& _closure) = 0;
virtual void exception_occurred(CORBA::Object_ptr _target,
CORBA::Environment_ptr _env,
VISClosure& _closure) = 0;
};
Note
If a CORBA::SystemException or any sub-classes (for example CORBA::NO_PERMISSION) is raised on the server side, the exception should not be encrypted. This is because the ORB uses some of these exceptions internally (for example TRANSIENT for doing automatic rebind).
IORCreationInterceptor
An IORCreationInterceptor object is called whenever a POA creates an object reference (using the create method). This Interceptor may be registered by a POALifeCycleInterceptor at POA creation time on a per-POA basis.
class _VISEXPORT IORCreationInterceptor : public virtual VISPseudoInterface {
public:
virtual void create(PortableServer::POA_ptr _poa,
IOP::IORValue*& _ior) = 0;
};
Service Resolver Interceptor
This Interceptor is used to install a user service that you can then dynamically load.
class _VISEXPORT interceptor::ServiceResolverInterceptor :public virtual VISPsuedoInterface {
public:
virtual CORBA::Object_ptr resolve(const char* _name) = 0;
};
class _VISEXPORT ServiceResolverInterceptorManager :public virtual InterceptorManager,
public virtual VISPseudoInterface {
public:
virtual void add(const char* _name, ServiceResolverInterceptor_ptr _interceptor) =
0;
virtual void remove(const char* _name) = 0;
};
When you call resolve_initial_references, the resolve on all installed services gets called. The resolve then can return the appropriate object.
To write service initializers, you must obtain a ServiceResolver after getting an InterceptorManagerControl to be able to add your services.
Registering Interceptors with the VisiBroker ORB
Each Interceptor interface has a corresponding Interceptor manager interface which is used to register your Interceptor objects with the VisiBroker ORB. The following steps are necessary to register an Interceptor:
1
Get a reference to an InterceptorManagerControl object by calling the resolve_initial_references method on an ORB object with the parameter VisiBrokerInterceptorControl.
2
Call the get_manager method on the InterceptorManagerControl object with one of the String values in the following table which shows the String values to pass to the get_manager method of the InterceptorManagerControl object. (Be sure to cast the object reference to its corresponding Interceptor manager interface.)
3
4
5
Creating Interceptor objects
Finally, you need to implement a factory class which creates instances of your Interceptors and registers them with the VisiBroker ORB. Your factory class must derive from the VISInit class.
// in the vinit.h file
class _VISEXPORT VISInit {
public:
VISInit();
VISInit(CORBA::Long init_priority);
virtual ~VISInit();
// ORB_init is called toward the beginning of CORBA::ORB_init()
virtual void ORB_init(int& /*argc*/,
char* const* /*argv*/,
CORBA_ORB* /*orb*/)
{}
// ORB_initialized is called at the end of CORBA::ORB_init()
virtual void ORB_initialized(CORBA_ORB* /*orb*/) {}
// shutdown is called when CORBA::ORB::shutdown() was called
// or process shutdown is detected
virtual void ORB_shutdown() {}
. . .
};
Note
You can also create new instances of your Interceptors and register them with the VisiBroker ORB from within other Interceptors as in the examples in “Example Interceptors”.
Loading Interceptors
To load your interceptor, simply instantiate the factory before the call to CORBA::ORB_init in your application.
Example Interceptors
The example Interceptor in this section uses all of the Interceptor API methods (listed in “Using Portable Interceptors”) so that you can see how these methods are used, and when they are invoked.
Example code
In “Code listings”, each of the Interceptor API methods is simply implemented to print informational messages to the standard output.
The following example applications are located in the directory:
<install_dir>\examples\vbe\interceptors\
Client-server Interceptors example
To run the example, compile the files as you normally would. Then start up the server and the client as follows:
prompt>Server
prompt>Client John
Note
The ServiceInit class used in VisiBroker 3.x is replaced by implementing two interfaces: ServiceLoader and ServiceResolverInterceptor. For an example of how to do this, see “SampleServerLoader”.
The results of executing the example Interceptor are shown in the following table. The execution by the client and server is listed in sequence.
Since the OAD is not running, the bind call fails and the server proceeds. The client binds to the account object, and then calls the balance method. This request is received by the server, processed, and results are returned to the client. The client prints the results.
As demonstrated by the example code and results, the Interceptors for both the client and server are installed when the respective process starts. Information about registering an interceptor is covered in “Registering Interceptors with the VisiBroker ORB”.
Code listings
SampleServerLoader
The SampleServerLoader object is responsible for loading the POALifeCycleInterceptor class and instantiating an object. This class is linked to the VisiBroker ORB dynamically by vbroker.orb.dynamicLibs. The SampleServerLoader class contains the init method which is called by the VisiBroker ORB during initialization. Its sole purpose is to install a POALifeCycleInterceptor object by creating it and registering it with the InterceptorManager.
#include <iostream.h>
#include "vinit.h"
#include "SamplePOALifeCycleInterceptor.h"

class POAInterceptorLoader : VISInit {
private:
short int _poa_interceptors_installed;
public:
POAInterceptorLoader(){
_poa_interceptors_installed = 0;
}
void ORB_init(int& argc, char* const* argv, CORBA::ORB_ptr orb) {
if( _poa_interceptors_installed) return;
cout << "Installing POA interceptors" << endl;
SamplePOALifeCycleInterceptor *interceptor = new SamplePOALifeCycleInterceptor;
// Get the interceptor manager control
CORBA::Object *object =
orb->resolve_initial_references("VisiBrokerInterceptorControl");
interceptor::InterceptorManagerControl_var control =
interceptor::InterceptorManagerControl::_narrow(object);
// Get the POA manager
interceptor::InterceptorManager_var manager =
control->get_manager("POALifeCycle");
PortableServerExt::POALifeCycleInterceptorManager_var poa_mgr =
PortableServerExt::POALifeCycleInterceptorManager::_narrow(manager);
// Add POA interceptor to the list
poa_mgr->add( (PortableServerExt::POALifeCycleInterceptor*)interceptor);
cout << "POA interceptors installed" << endl;
_poa_interceptors_installed = 1;
}
};
SamplePOALifeCycleInterceptor
The SamplePOALifeCycleInterceptor object is invoked every time a POA is created or destroyed. Because we have two POAs in the client_server example, this Interceptor is invoked twice, first during rootPOA creation and then at the creation of myPOA. We install the SampleServerInterceptor only at the creation of myPOA.
#include "interceptor_c.hh"
#include "PortableServerExt_c.hh"
#include "IOP_c.hh"
#include "SampleServerInterceptor.h"

class SamplePOALifeCycleInterceptor : PortableServerExt::POALifeCycleInterceptor {
public:
void create( PortableServer::POA_ptr poa,
CORBA_PolicyList& policies,
IOP::IORValue_ptr& iorTemplate,
interceptor::InterceptorManagerControl_ptr control) {
if( strcmp( poa->the_name(),"bank_agent_poa") == 0 ) {
// Add the Request-level interceptor
SampleServerInterceptor* interceptor =
new SampleServerInterceptor("MyServerInterceptor");
// Get the ServerRequest interceptor manager
interceptor::InterceptorManager_var generic_manager =
control->get_manager("ServerRequest");
interceptor::ServerRequestInterceptorManager_var manager =
interceptor::ServerRequestInterceptorManager::_narrow(
generic_manager);
// Add the interceptor
manager->add( (interceptor::ServerRequestInterceptor*)interceptor);
cout <<"============>In POA " << poa->the_name() <<
", 1 ServerRequest interceptor installed"<< endl;
} else
cout << "============>In POA " << poa->the_name() <<
". Nothing to do." << endl;
}
void destroy( PortableServer::POA_ptr poa) {
// To be a trace!
cout << "============> SamplePOALifeCycleInterceptor destroy" <<
poa->the_name() << endl;
}
};
SampleServerInterceptor
The SampleServerInterceptor object is invoked every time a request is received at or a reply is made by the server.
#include <iostream.h>
#include "vclosure.h"
#include "interceptor_c.hh"
#include "IOP_c.hh"
// USE_STD_NS is a define setup by VisiBroker to use the std namespace
USE_STD_NS

class SampleServerInterceptor : interceptor::ServerRequestInterceptor {
private:
char * _id;
public:
SampleServerInterceptor( const char* id) {
_id = new char[ strlen(id)];
strcpy( _id,id);
}

~SampleServerInterceptor() { _id = NULL;}
void preinvoke( CORBA_Object* target,
const char* operation,
const IOP::ServiceContextList& service_contexts,
CORBA_MarshalInBuffer& payload,
VISClosure& closure) {
closure.data = new char[ strlen(_id)];
strcpy( (char*)(closure.data), _id);
cout << "============> SampleServerInterceptor id " <<
(char*)(closure.data) <<
" preinvoke => " << operation << endl;
}
void postinvoke_premarshal( CORBA_Object* target,
IOP::ServiceContextList& service_contexts,
CORBA::Environment_ptr env,
VISClosure& closure) {
cout << "============> SampleServerInterceptor id " <<
(char*)(closure.data) <<
" postinvoke_premarshal " << endl;
}
void postinvoke_postmarshal(CORBA_Object* target,
CORBA_MarshalOutBuffer& payload,
VISClosure& closure) {
cout << "============> SampleServerInterceptor id " <<
(char*)(closure.data) <<
" postinvoke_postmarshal " << endl;
}
void exception_occurred( CORBA_Object* target,
CORBA::Environment_ptr env,
VISClosure& closure) {
cout << "============> SampleServerInterceptor id " <<
(char*)(closure.data) <<
" exception_occurred" << endl;
}
};
SampleClientInterceptor
The SampleClientInterceptor is invoked every time a request is made by or a reply is received at the client.
#include <iostream.h>
#include "interceptor_c.hh"
#include "IOP_c.hh"
#include "vclosure.h"

class SampleClientInterceptor : public interceptor::ClientRequestInterceptor {
private:
char * _id;
public:
SampleClientInterceptor( char * id) {
_id = new char[ strlen(id)+1];
strcpy(_id,id);
}

void preinvoke_premarshal(CORBA::Object_ptr target,
const char* operation,
IOP::ServiceContextList& servicecontexts,
VISClosure& closure) {
closure.data = new char[ strlen(_id)];
strcpy( (char*)(closure.data), _id);
cout << "SampleClientInterceptor id " << closure.data
<< "=================> preinvoke_premarshal "
<< operation << endl;
}
void preinvoke_postmarshal(CORBA::Object_ptr target,
CORBA_MarshalInBuffer& payload,
VISClosure& closure) {
cout << "SampleClientInterceptor id " << closure.data
<< "=================> preinvoke_postmarshal "
<< endl;
}
void postinvoke(CORBA::Object_ptr target,
const IOP::ServiceContextList& service_contexts,
CORBA_MarshalInBuffer& payload,
CORBA::Environment_ptr env,
VISClosure& closure) {
cout << "SampleClientInterceptor id " << closure.data
<< "=================> postinvoke "
<< endl;
}
void exception_occurred(CORBA::Object_ptr target,
CORBA::Environment_ptr env,
VISClosure& closure) {
cout << "SampleClientInterceptor id " << closure.data
<< "=================> exception_occurred "
<< endl;
}
};
SampleClientLoader
The SampleClientLoader is responsible for loading BindInterceptor objects. This class is linked to the VisiBroker ORB dynamically by vbroker.orb.dynamicLibs. The SampleClientLoader class contains the bind and bind_succeeded methods. These methods are called by the ORB during object binding. When the bind succeeds,bind_succeeded will be called by the ORB and a BindInterceptor object is installed by creating it and registering it the InterceptorManager.
#include <iostream.h>
#include "vinit.h"
#include "SampleBindInterceptor.h"

class BindInterceptorLoader : VISInit {
private:
short int _bind_interceptors_installed;
public:
BindInterceptorLoader() {
_bind_interceptors_installed = 0;
}

void ORB_init(int& argc, char* const* argv, CORBA::ORB_ptr orb) {
if( _bind_interceptors_installed) return;
cout << "Installing Bind interceptors" << endl;
SampleBindInterceptor *interceptor =
new SampleBindInterceptor;
// Get the interceptor manager control
CORBA::Object *object =
orb->resolve_initial_references("VisiBrokerInterceptorControl");
interceptor::InterceptorManagerControl_var control =
interceptor::InterceptorManagerControl::_narrow(object);
// Get the Bind manager
interceptor::InterceptorManager_var manager =
control->get_manager("Bind");
interceptor::BindInterceptorManager_var bind_mgr =
interceptor::BindInterceptorManager::_narrow(manager);
// Add Bind interceptor to the list
bind_mgr->add( (interceptor::BindInterceptor*)interceptor);
cout << "Bind interceptors installed" << endl;
_bind_interceptors_installed = 1;
}
};
SampleBindInterceptor
The SampleBindInterceptor is invoked when the client attempts to bind to an object. The first step on the client side after ORB initialization is to bind to an AccountManager object. This bind invokes the SampleBindInterceptor and a SampleClientInterceptor is installed when the bind succeeds.
#include <iostream.h>
#include "interceptor_c.hh"
#include "IOP_c.hh"
#include "vclosure.h"
#include "SampleClientInterceptor.h"

class SampleBindInterceptor : public interceptor::BindInterceptor {
public:
IOP::IORValue_ptr bind(IOP::IORValue_ptr ior,
CORBA_Object_ptr obj,
CORBA::Boolean rebind,
VISClosure& closure) {
cout << "SampleBindInterceptor-----> bind" << endl;
return NULL;
}
IOP::IORValue_ptr bind_failed(IOP::IORValue_ptr ior,
CORBA_Object_ptr object,
VISClosure& closure) {
cout << "SampleBindInterceptor-----> bind_failed" << endl;
return NULL;
}

void bind_succeeded(IOP::IORValue_ptr ior,
CORBA_Object_ptr object,
CORBA::Long profile_index,
interceptor::InterceptorManagerControl_ptr control,
VISClosure& closure) {
cout << "SampleBindInterceptor-----> bind_succeeded"
<< endl;
// Add the Request-level interceptor
interceptor::ClientRequestInterceptor_var interceptor =
new SampleClientInterceptor((char*)"MyClientInterceptor");
// Get the ClientRequest interceptor manager
interceptor::InterceptorManager_var generic_manager =
control->get_manager("ClientRequest");
interceptor::ClientRequestInterceptorManager_var manager =
interceptor::ClientRequestInterceptorManager::_narrow(
generic_manager);
// Add the interceptor
manager->add( (interceptor::ClientRequestInterceptor*)interceptor);
cout <<"============>In bind_succeeded, 1 "
<<"ClientRequest interceptor installed"<< endl;
}
void exception_occurred(IOP::IORValue_ptr ior,
CORBA_Object_ptr object,
CORBA_Environment_ptr env,
VISClosure& closure) {
cout << "SampleBindInterceptor-----> exception_occured"
<< endl;
}
};
Passing information between your Interceptors
Closure objects are created by the ORB at the beginning of certain sequences of Interceptor calls. The same Closure object is used for all calls in that particular sequence. The Closure object contains a single public data field object of type java.lang.Object which may be set by the Interceptor to keep state information. The sequences for which Closure objects are created vary depending on the Interceptor type. In the ClientRequestInterceptor, a new Closure is created before calling preinvoke_premarshal and the same Closure is used for that request until the request completes, successfully or not. Likewise, in the ServerInterceptor, a new Closure is created before calling preinvoke, and that Closure is used for all Interceptor calls related to processing that particular request.
For an example of how Closure is used, see the examples in the following directory:
<install_dir>/examples/vbe/interceptors/client_server
The Closure object can be cast to ExtendedClosure to obtain response_expected and request_id as follows:
CORBA::Boolean my_response_expected =
((ExtendedClosure)closure).reqInfo.response_expected;
CORBA::ULong my_request_id =
((ExtendedClosure)closure).reqInfo.request_id;
Using both Portable Interceptors and VisiBroker Interceptors simultaneously
Both Portable Interceptors and VisiBroker Interceptors can be installed simultaneously with the VisiBroker ORB. However, as they have different implementations, there are several rules of flow and constrains that developers need to understand when using both Interceptors, as described in the following.
Order of invocation of interception points
The order of invocation of interception points follows the interception point ordering rules of individual versions of Interceptors, regardless of whether the developer actually chooses to install one of more than one version.
Client side Interceptors
When both Portable Interceptors and VisiBroker client side Interceptors are installed, the order of events, (assuming no Interceptor throws an exception) is:
1
send_request (Portable Interceptor), followed by preinvoke_premarshal (Interceptors)
2
3
preinvoke_postmarshal (Interceptor)
4
5
postinvoke (Interceptor), followed by received_reply/receive_exception/receive_other (Portable Interceptor) depending on the type of reply.
Server side Interceptors
When both Portable Interceptors and VisiBroker server side Interceptors are installed, the order of events is received (locate requests do not fire Interceptors, which is the same as VisiBroker behavior), assuming no Interceptor throws an exception, is:
1
received_request_service_contexts (Portable Interceptor), followed by preinvoke (Interceptor)
2
servantLocator.preinvoke (if using servant locator)
3
receive_request (Portable Interceptor)
4
5
postinvoke_premarshal (Interceptor)
6
servantLocator.postinvoke (if using servant locator)
7
send_reply/send_exception/send_other, depending on the outcome of the request
8
Order of ORB events during POA creation
The order of ORB events during creation of a POA is listed as follows:
1
2
An Interceptors' POA life cycle Interceptors' create() method is invoked. This method can potentially add new policies or modify the IOR template created in the previous step.
3
A Portable Interceptor's IORInfo object is created and the IORInterceptors' establish_components() method is invoked. This interception point allows the Interceptor to query the policies passed to create_POA() and those added in the previous step, and also add components to the IOR template based on those policies.
4
An object reference factory and object reference template for the POA are created, and the Portable Interceptor's IORInterceptors' components_established() method is invoked. This interception point allows the Interceptor to change the POA's object reference factory, which will be used to manufacture object references.
Order of ORB events during object reference creation
The following events occur during calls to POA that create object reference, such as create_reference(), create_reference_with_id().
1
Call the object reference factory's make_object() method to create the object reference (this does not call the VisiBroker IOR creation Interceptors, and the factory may be user -supplied). If there are no VisiBroker IOR creation Interceptors installed, this should be the object reference returned to the application; otherwise, proceed to step 2.
2
3
IOR from step 2 is returned as the object reference to the caller of create_reference(), create_reference_with_id().