Hook Classes
Entry points for the backend logic are the methods in hook classes. A hook class is generated for each class that is created through the Dashboard. The hook methods are called when an operation is performed on the concerned class by ApiOmat. This applies to
-
Post (Create a new object)
-
Get (Get an object by ID)
-
Put (Update an object by ID)
-
Delete (Delete an object by ID)
as well as to
-
GetAll (Get all objects by a query)
-
DeleteAll (Delete objects by a query))
-
PostRef (Create a reference to an object)
-
GetRef (Get an existing reference)
-
DeleteRef (Delete an existing reference to an object)
-
PostData (Attach data (image/file) to an object)
-
GetData (Get the attached data from an object)
-
DeleteData (Delete the attached data from an object)
Content:
Available Methods
Supposed, we declared a class named DataClass via Dashboard, the corresponding hook class generated by ApiOmat until version 2.1 is called DataClassHooks.java. In newer versions, starting with ApiOmat 2.2, the methods are split up into two different hook-classes DataClassHooksTransient.java and DataClassHooksNonTransient.java. These classes reflect the state of the transient flag. If your class is a transient class, the hooks from DataClassHooksTransient.java will be called, or the DataClassHooksNonTransient.java otherwise. The classes contain the following methods:
public
String doPost( DataClass obj, Request r )
public
void
doPut( DataClass obj, Request r )
public
DataClass doGet( String foreignId, Request r )
public
boolean
doDelete( String foreignId, Request r )
public
boolean
doDeleteAll( String query, Request r )
public
List<DataClass> doGetAll( String query, Request r )
public
long
doCountAll( String query, Request r )
public
void
doPostRef( Object referencedObject, String referenceName, Request r )
public
void
doDeleteRef( String refName, String refForeignId, Request r )
public
<Z
extends
AbstractClientDataModel> List<Z> doGetRef( String refName, String query, Request r )
public
String doPostData(
final
String attributeName,
final
DataWrapper dataWrapper,
final
Request request )
public
DataWrapper doGetData(
final
String dataId,
final
String attributeName,
final
TranscodingConfiguration transcodingConfig,
final
Request request )
public
boolean
doDeleteData(
final
String attributeName,
final
String dataId,
final
Request request )
public
void
beforePost( DataClass obj, Request r )
public
void
afterPost( DataClass obj, Request r )
public
void
beforeGet( String obj, Request r )
public
void
afterGet( DataClass obj, Request r )
public
boolean
beforePut( DataClass objFromDB, DataClass obj, Request r )
public
void
afterPut( DataClass obj, Request r )
public
boolean
beforeDelete( DataClass obj, Request r )
public
String beforeGetAll( String query, Request r )
public
List<DataClass> afterGetAll( List<DataClass> objects, String query, Request r )
public
boolean
beforePostRef( DataClass obj, Object referencedObject, String referenceName, Request r )
public
void
afterPostRef( DataClass obj, Object referencedObject, String referenceName, Request r )
public
boolean
beforeDeleteRef( DataClass obj, Object referencedObject, String referenceName, Request r )
public
void
afterDeleteRef( DataClass obj, Object referencedObject, String referenceName, Request r )
public
String beforeGetAllReferences( String query, String referenceName, Request request )
public
<Z
extends
AbstractClientDataModel> List<Z> afterGetAllReferences( List<Z> objects, String query, String referenceName, Request request )
public
boolean
beforePostData(
final
T obj,
final
String attributeName,
final
DataWrapper dataWrapper,
final
Request request )
public
void
afterPostData(
final
T obj,
final
String attributeName,
final
DataWrapper dataWrapper,
final
Request request )
public
String beforeGetData(
final
String dataId,
final
String attributeName,
final
TranscodingConfiguration transcodingConfig,
final
Request request )
public
void
afterGetData(
final
String dataId,
final
String attributeName,
final
DataWrapper dataWrapper,
final
TranscodingConfiguration transcodingConfig,
final
Request request )
public
boolean
beforeDeleteData(
final
T obj,
final
String attributeName,
final
DataWrapper dataWrapper,
final
Request request )
public
void
afterDeleteData(
final
T obj,
final
String attributeName,
final
DataWrapper dataWrapper,
final
Request request )
The logic to be executed has to be placed into the suitable method body and is executed when the specified action is called.
It is necessary to distinguish whether the class is marked as transient or not. In the case of a transient class, the methods starting with "do..." (first block of the listing above) are usable, whereas the remaining (second block) are applicable if the class is non-transient.
This distinction is caused by the fact that the internal doPost, doGet, .. methods are replaced entirely by the hook methods of the respective module if it is transient. The developer is responsible for responding to operations, especially for assigning an ID to objects on creation.
However in the case of a non-transient class, it is possible to execute code before and after the execution of internal methods by means of the hook methods starting with "before..." and "after...". The execution is comparable to Server Code.
Since it is impossible to set static variables (e.g. a consistent logging object) for technical reasons, the belonging object is injected into the hook class by the setCallingModel method during the initialization of the object. This initializes the class variable model of the hook class which provides access to static methods such as searching, saving or deleting objects.
As of version 2.5, the old beforeGet method has been renamed to afterGet, as it is called after the object has been loaded from the database. The old beforeGet method currently still gets invoked, but it is marked as deprecated and we strongly recommend you to update this. Additionally, there is a new beforeGet method, which is really called before the object is loaded. We've also added a beforeGetAll and beforeGetAllReferences where you can modify the query which is passed to the database.
Using references
Regarding the behaviour of referenced classes, it is necessary to distinguish between transient and non-transient classes.
In transient classes there is no built-in way to access references to other classes, as there is no locally stored data for the transient class. In order to use objects' references, the generated method doGetRef needs to be implemented fetching the referenced objects. The doGetRef method is called in two cases. The first case is the GET call over the REST-API on /yambas/rest/apps/{appName}/{moduleName}/{modelName}/{dataModelId}/{refName} , when the referencing class is transient. The second case, as of ApiOmat version 2.5.6 and later, is the call of the obj.get[RefName]() method in NativeModule code. Both cases will internally first call the doGet method of the data model with the given id first (to have the values for this.model filled) and then call the doGetRef method.
If using a non-transient class, the hook-methods enable you to access referenced objects by using obj.get[RefName](), except for methods in which no reference is present (beforePost, afterPost, beforePostRef, afterDeleteRef). The referenced Ids or foreignIds are stored on the non-transient class object. If the object has a non-transient reference, it will simply load the object from our internal storage. If the object has a transient reference, it will get the stored foreignId and call the doGet method of the referenced class to load the reference.
Recursive Call
It is possible to create an endless loop here. See section on recursive calls for details.
Using back references
Every non-transient class instance which is referenced to another object contains a back reference to the other object. This comes in handy if you want to do some changes to the parent object before a referenced class instance e.g. gets deleted.
Imagine you have an object called 'base' with a reference to an object called 'ref'. So, object 'ref' has a back reference to object 'base'. If someone triggers to delete object 'ref' the beforeDelete hook method of object 'ref' is called.
Within the hook method you now have the ability to load all back references of object 'ref' via ref.loadBackReferences() .
The following example shows how you may use the loadBackReferences() method to set a default reference to the base object if the reference object gets deleted:
@Override
public
boolean
beforeDelete( com.apiomat.nativemodule.testaom48772.MyUser obj, com.apiomat.nativemodule.Request r )
{
final
List<ReferenceWrapper> backReferences = obj.loadBackReferences( );
for
( ReferenceWrapper referenceWrapper : backReferences )
{
/* get some details about the backreference if needed */
final
String applicationName = referenceWrapper.getApplicationName( );
final
String moduleName = referenceWrapper.getModuleName( );
final
String modelName = referenceWrapper.getModelName( );
final
String refName = referenceWrapper.getRefName( );
System.out.println(
"found Backreferences: "
+ applicationName +
" "
+ moduleName +
" "
+ modelName +
" "
+ refName );
/* do operations on specific base model instances */
for
( IModel<?> iModel : referenceWrapper )
{
if
( iModel
instanceof
MyGroup )
{
MyGroup myGroup = ( MyGroup ) iModel;
/* E.g. manually remove MyUser object from MyGroup */
myGroup.removeUsers( obj );
/* E.g. post default MyUser object as new reference to MyGroup */
final
MyUser defaultMyUser = ( MyUser ) obj.findById(
"5b1f7f3f02f83b2630b973e6"
, r );
myGroup.postUsers( defaultMyUser );
/* do some other stuff with the parent object ... */
}
}
}
return
false
;
}
Using binary data
Since ApiOmat 2.6.0 there are new hook methods for handling binary data.
In older versions a client had to send two requests to attach a file to an object: First send a POST request to the backend to create the file, then read the location header of the response, add the URL to an object and then send a PUT request with the changed object. Now there are new REST endpoints. For example for posting a file:
/yambas/rest/apps/{appName}/files/{moduleName}/{modelName}/{dataModelId}/{refName}
When a POST request gets sent to this REST endpoint, the doPostData() (transient) or beforePostData() + afterPostData() (non-transient) hook methods are called.
Recursive Call
It is possible to create an endless loop here. See section on recursive calls for details.
Transient
When implementing the doPostData() method, you have to take care of the two things that were previously split in two requests:
-
Save the data (image/file) somewhere and
-
Add the URL to the object where the data shall be attached
-
The URL must look like /yambas/rest/apps/{appName}/files/{moduleName}/{modelName}/{dataModelId}/{refName}/{dataId}. Note the added dataID. This is necessary, so that when a GET request is sent and your doGetData() hook method gets called, you know which file to fetch and return.
-
The same goes for doDeleteData(): Delete the file as well as the "<attributeName>URL" attribute from the object.
It's easier for doGetData(): You only need to fetch and return the data.
Non-transient
As with the other non-transient hook methods, you don't need to implement the methods, but you have the possibility to alter the default behavior and data. For example, when an image gets loaded in a GET request, you can implement afterGetData() to inspect the image, to change the size or file format of the image or to completely replace the image by another one.
Authentication
Hook classes can overwrite a method that's defined in the IModelHooksTransient and IModelHooksNonTransient interfaces:
@Override
public
boolean
auth( String httpVerb, String moduleName, String modelName,
String modelForeignId, String userNameOrEmail, String passwordOrToken,
com.apiomat.nativemodule.Request request )
Implementing this class leads to the hook class being available as authentication class, which you can add to your backend to be used for authentication on every request to any object of any class that's in a module that's added to your backend.
Please read more about authentication on the pages Authentication Classes and Authentication checks in Native Modules.
Update to new Hook Structure
If you have an old module, which contains and uses the old hook-class, the new hooks will be added to your modules' files after the up- and download on a new version. But nevertheless, ApiOmat will use the old hook-file until you update the @Model annotation in your DataClass.
Assume that you've the following @Model annotation in your DataClass:
@Model
( moduleName =
"MyModule"
,
hooksClassName =
"com.apiomat.nativemodule.mymodule.DataClassHooks"
,
isTransient =
false
, requiredUserRoleCreate=UserRole.User, requiredUserRoleRead=UserRole.User,
requiredUserRoleWrite=UserRole.Owner, restrictResourceAccess=
false
,
allowedRolesCreate={}, allowedRolesRead={},
allowedRolesWrite={}, allowedRolesGrant={})
public
class
DataClass
extends
AbstractClientDataModel
implements
IModel<DataClass>
{
...
You have to replace the hook information in that annotation with the information where to find the new hook-methods:
@Model
( moduleName =
"MyModule"
,
hooksClassNameTransient =
"com.apiomat.nativemodule.mymodule.DataClassTransientHooks"
,
hooksClassNameNonTransient =
"com.apiomat.nativemodule.mymodule.DataClassNonTransientHooks"
,
isTransient =
false
, requiredUserRoleCreate=UserRole.User, requiredUserRoleRead=UserRole.User,
requiredUserRoleWrite=UserRole.Owner, restrictResourceAccess=
false
,
allowedRolesCreate={}, allowedRolesRead={},
allowedRolesWrite={}, allowedRolesGrant={})
public
class
DataClass
extends
AbstractClientDataModel
implements
IModel<DataClass>
{
...
You can then simply copy the relevant parts of the code from your old hook class to the new hook classes.
Inheritance
It will also affect generated hook classes if inheritance is used. Here the topmost parent class lists all hook methods whereas the subclasses declare none of them. Thus, the subclasses behave as their parent classes. If this behaviour is to be changed, the respective hook methods have to be overridden using the @Override annotation.
Example: Class A is the class at the top of a hierarchy and class B inherits from it. AHooksNonTransient and AHooksTransient declare all hook methods. doGet keeps the default behaviour and returns null, doDelete and the remaining methods are filled with own code.
B should now behave identically but use the default behaviour in doDelete and a user-defined behaviour in doGet. Thus, only these two methods have to be declared and annotated with @Override in BHooksTransient.
Furthermore, inheritance of hook classes requires the particular parent class to be defined within a native module. If the inheriting module was dynamic before, you may have to add inheritance manually.
Example: ClassA is defined in ModuleA, while ClassB is defined in ModuleB. ModuleA is a dynamic module, which means it was never down- and uploaded before. If you now set ClassB inheritance from ClassA and start downloading ModuleB, the generated hook methods are not inheriting from ClassA's ones, as ModuleA is still dynamic and not native. If you are down+uploading ModuleA now (or making it native via the Dashboard button) and download ModuleB again, the hooks will be regenerated (if ModuleB is still dynamic) and now inheriting from ClassA's ones. If ModuleB is already native and the hooks are not inheriting from ClassA's ones, you will have to add the inheritance on your own.
When hook inheritance was generated successfully, you'll see, that ClassB's hook class is inheriting from ClassA's one:
public
class
ClassBHooksNonTransient<T
extends
ClassB>
extends
ClassAHooksNonTransient<ClassB>
{
}
Distinguish Customer and User requests
In versions 2.2.1 and higher the request handed to any hook-method holds an attribute isAccountRequest. This attribute tells you whether the requester is authorized through an AbstractAccount. This indicates that it is a customer requesting. Otherwise the request originates from an "ordinary" user of your backend.
Special notes regarding Hook methods
beforePost
When you have a non-transient class and use the beforePost method, you can call setters on the object that gets passed as parameter (as obj). Do not call save() afterwards! Saving the object is automatically done after the beforePost internally; calling manually save() would lead to a duplicate ID exception during the internal save() call.
beforePut
In the beforePut method, you can validate the values which have been set for your object or set additional values for specific fields. If the given object contains values which are assumed to be invalid, you can interrupt and abort the update process by returning true in the update process.
doGet
There is a special case for transient classes that inherit from the User class regarding the loadMe method in the frontend SDKs. Due to the lack of class and id information on the /apps/{appName}/models/me endpoint, the loadMe method will be overridden for the transient class that inherits from user and make a GET request to the /apps/{appName}/models/{moduleName}/{dataModelName}/{dataModelId} endpoint, handing the username as datamodelId. For that case, the doGet method is called and contains the username as foreignId parameter.
Invoking other modules' hooks
If you are using other modules within your Native Modules (see: Using other modules ) you may wish to have its hook-methods to be invoked if you are performing the respective actions on the module's objects. In order to achieve this you have to set the respective flag to the request prior to calling object-methods:
r.setHooksInUsedModulesEnabled(
true
);
Recursive calls
As it is possible to create endless loops within hook classes, we introduced a configurable limit for the call depth of hook methods. If this limit is exeeded the invocation of the hook will be aborted and a custom exception is thrown.
The limit can be set in the apiomat.yaml using the key yambas.limits.maxHookCallDepth.
For example, the system will end in such an endless loop, if you call the obj.post[RefName](refObject) method within the doGet method of the object. As the post[RefName] internally calls the doGet of the object itself, to get the latest data of the object to post the reference on it (while the doGet call then calls the post[RefName] again, which will then call the doGet again, which will...).
Another example: When you call obj.addReferencedData() in a doGet() method and obj is an object of a transient class, this leads to the execution of doPostData(), which internally calls doGet() to properly set this.model.
@Override
public boolean beforeDelete( com.apiomat.nativemodule.testaom48772.MyUser obj, com.apiomat.nativemodule.Request r )
{
final List<ReferenceWrapper> backReferences = obj.loadBackReferences( );
for ( ReferenceWrapper referenceWrapper : backReferences )
{
String applicationName = referenceWrapper.getApplicationName( );
String moduleName = referenceWrapper.getModuleName( );
String modelName = referenceWrapper.getModelName( );
String refName = referenceWrapper.getRefName( );
System.out.println(
"found Backreference: " + applicationName + " " + moduleName + " " + modelName + " " + refName );
for ( IModel<?> iModel : referenceWrapper )
{
if ( iModel instanceof MyGroup )
{
MyGroup myGroup = ( MyGroup ) iModel;
myGroup.removeUsers( obj );
final MyUser defaultMyUser = ( MyUser ) obj.findById( "5b1f7f3f02f83b2630b973e6", r );
myGroup.postUsers( defaultMyUser );
}
}
}
return false;
}