Chapter 8. Tapping into ReportServer

8. Tapping into ReportServer

ReportServer makes heavy use of a subscriber-notifier pattern which we call hooking. That is, a central HookHandlerService plays the role of registry where so called Hooks can be registered. These registered code snippets are then notified, when certain code is executed (for example, when a report is about to be executed) or to provide functionality (for example, to handle the authentication process or export a report into a specified format). In this section we explain the basics of registering scripts as Hooks and discuss some of the more common Hook interfaces. Other examples of customization using Hooks are given in the appendix.

Besides extending ReportServer via hooks, ReportServer has an event mechanism that can be used to be notified upon certain events, such as changes to entities (for example, one can be notified whenever a report is changed) or on errors.

8.1. ReportServer Hooks Basics

A ReportServer hook is simply an interface that inherits the net.datenwerke.hookhandler.shared.hookhandler.interfaces.hook interface. Hooks are used throughout ReportServer to provide mechanisms to easily extend the ReportServer functionality. For example, hooks can be used to register additional database drivers, to add report output formats, to allow for customized authentication (for example, LDAP) and much more. To register a hook with ReportServer you will usually use the callbackRegistry which is available via the GLOBALS object. The callbackRegistry offers two methods to register a new hook:

  • attachHook() takes as input a class object, to specify the hook interface and an object from type hook (usually called hooker) which contains the actual code. The method returns a unique name for the hook which can be used to deregister it again.
  • attachHook(name) same as before, but one additionally specifies the name as first parameter. This makes it easy to remove or update the hooker by simply accessing it via the specified name. That is, if this method is called twice with the same name, then only one hook is registered.

To deregister hooks you can use the method detachHook(name) which takes the name of a previously registered hook as input. To get a list (or rather a map) of all the registered hooks use the method getRegisteredHooks which returns a map (name -> hook).

The easiest way to learn how to work with ReportServer hooks is to actually implement one. In the following we will create a simple hook that is notified before every report execution and denies the execution in case the request is not within the normal working hours. For this we will use the ReportExecutionNotificationHook or more precisely the (net.datenwerke.rs.core.service.reportmanager.hooks.ReportExecutionNotificationHook). Following is the basic outline of a script that registers a hook:

def HOOK_NAME = "THE NAME OF MY HOOK" 

def callback = [
 	/* implementation of hook */
] as TypeOfHook

GLOBALS.services.callbackRegistry.attachHook(HOOK_NAME, TypeOfHook.class, callback)

In our case, this could look as follows

import net.datenwerke.rs.core.service.reportmanager.exceptions.* 
import net.datenwerke.rs.core.service.reportmanager.hooks.*

def HOOK_NAME = "PROHIBIT_EXECUTION" 

def callback = [
    notifyOfReportExecution : { report, parameterSet, user, outputFormat, configs ->  },
    notifyOfReportsSuccessfulExecution : { compiledReport, report, parameterSet, user,
outputFormat, configs -> },
    notifyOfReportsUnsuccessfulExecution : { e, report, parameterSet, user, outputFormat,
configs -> },
	doVetoReportExecution: { report, parameterSet, user, outputFormat, configs -> 
		def cal = Calendar.instance
		def hour = cal.get(Calendar.HOUR_OF_DAY)
		if(hour > 17 || hour < 9)
			throw new ReportExecutorException("Please come back during working hours.");
    }
] as ReportExecutionNotificationHook

GLOBALS.services.callbackRegistry.attachHook(HOOK_NAME, ReportExecutionNotificationHook.
class, callback)

We have implemented the interface ReportExecutionNotificationHook which has four methods. We did this using ''closure coercion'' as explained here: https://docs.groovy-lang.org/latest/html/documentation/core-semantics.html#closure-coercion. In our case we wanted to stop the execution of a report execution. For this, the interface specifies that we should throw a ReportExecutorException.

All that is left is to execute the above script.

reportserver$ exec denyexecutionhook.groovy
PROHIBIT_EXECUTION
Tip: When implementing hooks you need to take care to properly implement the interface. Bugs that lead to an exception within your implementation could otherwise easily lead to unexpected behavior.
8.1.1. Getting a List of all Registered Hooks

It may be useful to list all the registered hooks, or all the registered hooks of a specific type. For this we can use the method "getRegisteredHooks" provided by the callbackRegistry. The following simple script simply outputs the name of all registered hooks together with the date, when the hook has been registered.

GLOBALS.services.callbackRegistry.getRegisteredHooks().each { key, value ->
	tout.println(key + ": " + value.getDate())
}

""
8.2. Registering Hooks on Start-up and on Login

On start-up and when a user logs in ReportServer executes a specially named script or all scripts in a named folder, respectively. This is the place to register your hooks in case you permanently want to adapt certain ReportServer functionality. The location of these scripts is configured in the config file scripting/scripting.cf (for further information refer to ReportServer Configuration Guide). The config could, for example, look like

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
   <scripting>
      <enable>true</enable>
      <restrict>
         <location>bin</location>
      </restrict>
      <startup>
         <login>fileserver/bin/onlogin.d</login>
         <rs>fileserver/bin/onstartup.d</rs>
      </startup>
   </scripting>
</configuration>

Assuming that onstartup.d is a directory, ReportServer would execute all scripts within this directory on start-up. Note that ReportServer will not recursively traverse directories.

If something goes wrong during a start-up script ReportServer will record this event in its audit log. To access the audit log simply create a dynamic list pointing to the database that hosts ReportServer. The table is RS_AUDIT_LOG. The action is named STARTUP_SCRIPT_FAILED. More information on the audit log can be found in the administrator's manual.

Should you find yourself locked out of ReportServer, you can disable any scripts via the rs.scripting.disable property in the reportserver.properties configuration file. If present and set to true, scripts will not be executed and you can thus login correctly to ReportServer.

8.3. Which Hooks can I use?

The interface net.datenwerke.hookhandler.shared.hookhandler.interfaces.Hook is implemented by all ReportServer Hooks. Thus, a good starting point is the Javadoc documentation of the source. In principle any such hook can be implemented. Here you can find a list of all hooks of the latest ReportServer version: https://reportserver.net/api/latest/hooks.html. For hooks that were especially designed to be implemented by scripts we have added an adapter class that provides a dummy implementation for all methods required by the hook. You will find the corresponding adapter in the subpackage adapter. Other than the source directly the examples in the appendix of this manual should provide a good starting point.