Click here for general information about scripting in Intrexx.
2. Intrexx Standard Library
In the Groovy script editor,
you can access the Intrexx Standard Library on the
Libraries tab.
If you select an entry in the library, you can access these buttons at the
bottom right of this area:
Show description
Shows the description of the currently selected function with an example script.
Open link
Links to the corresponding page that provides more information.
The page that opens also shows the classes, interfaces, methods or properties
that the currently selected function can be used for.
2.1. Application structure
2.1.1. Application property from data record object
Reads application properties from a data record object. Replace
"myMethode()" in the example script with the desired method.
All standard text elements used in Intrexx are defined in the
global language constants.
Click
here
for more information about this subject.
2.2.1. Access global language constant
Returns the value that the global language constant has for the
default portal language.
Replace "PORTAL_CONST_NAME" in the example script with the name of the desired language constant.
Example
def strValue = g_i18n.BUTTON_NEXT
Returns "Next" (label for
the "Next" button that can be found in many applications - in this
case it is returned in English, the default language of this example portal).
2.2.2. Access global language constant in a specific language
Returns the value that the global language constant has for the specified
language.
Replace "PORTAL_CONST_NAME" in the example script with the name of the desired language constant.
Example
def lang = g_i18n.language("en")
Returns the value that the global language constant has for English.
Snippet
def lang = g_i18n.language("language code")
def strValue = lang.PORTAL_CONST_NAME
2.2.4. Access application language constant in a specific language
Returns the value that the application language constant
has for the specified language.
Replace "APP_CONST_NAME" in the example script with the name of the desired language constant.
Replace the language code
with the two-letter ISO code, e.g. "en" for English.
Object for generating and performing database queries.
Snippet
//Prepared statement structure
def stmt = g_dbQuery.prepare(conn, "SELECT * FROM MYTABLE WHERE LID = ?")
stmt.setInt(1, iLid)
stmt.executeQuery()
stmt.close()
//Query a single value
def iMax = g_dbQuery.executeAndGetScalarValue(conn, "SELECT MAX(LID) FROM MYTABLE", 0)
Access the process's temporary working directory on the server.
The directory is available to all subsequent process elements until the
process is ended.
Only available in
processes
- contains event that triggered the current workflow.
Please note: comparisons to the current event (see script example)
should always be performed to interfaces and never with specific classes.
Snippet
import de.uplanet.lucy.server.workflow.event.*
if (g_event instanceof IGlobalTimerWorkflowEvent)
//Insert script that responds to the timer event
Contains the exception that was caught. This needs to be examined so the ErrorHandler
can decide whether it is responsible for it. Common criteria for this include:
- The exception was triggered by an exception of a specific type.
- The exception was triggered by an exception of a specific type
and the error message has specific content (starts with "my-custom-prefix", for example).
See the readme.txt and handler_50_xxx.groovy.example
portal directory internal/system/vm/html/errorhandler/custom
for more information.
Intrexx provides a wide range of extended options for process-controlled user
management. With access objects and methods, new users, among other things,
can be created and managed in Groovy actions. The globally available,
pre-initialized object "g_om" serves as an entry point for the portal
organizational structure in processes. With this object, principle use
cases such as searching for a user or generating a new password can be
implemented. These methods can be used as "normal" method calls.
All available methods can be found in our JavaDocs:
// Change the password of the stated user.
g_om.changePassword(g_session.user, "SECRET")
// Generate and send a new password to the stated user.
g_om.generateAndSendPassword(g_session.user)
// Get a user using a GUID.
def user = g_om.getUser("C10579449052F85D9C3FF3C2824348FCE020A22E")
// Classify a list of GUIDs. A list with GUIDs can
// be transferred or a text (e.g. content of a multiple selection data field).
def guids = ["AE39A904172F5867DA23DE289D1D6B7967420DC0", "6AA80844C3C99EF93BF4536EB18605BF86FDD3C5", g_session.user.guid]
def classified = g_om.classifyGuids(guids) //guids = Collection or String
//The result is a map with the categorized GUIDs.
//e.g. {containers=[], users=[7312F993D0DA4CECCA9AE5A9D865BE142DE413EA],
// unclassified=[AE39A904172F5867DA23DE289D1D6B7967420DC0],
// sets=[6AA80844C3C99EF93BF4536EB18605BF86FDD3C5]}
println(classified)
//Additional filters for specific object types
println(classified.users)
println(classified.containers)
println(classified.sets)
println(classified.unclassified)
//Returns the number of non-anonymous sesssions
g_om.getNonAnonymousActiveSessionCount()
In addition, the object "g_om" has access to more methods that can be called
in conjunction with a closure to implement subsequent actions. These methods
can be recognized by the stated parameter in the
JavaDocs
with the type "groovy.lang.Closure", such as "GroovyOrgBuilder.createUser(groovy.lang.Closure p_closure)".
Within such a closure, methods of any class can be called that are documented
in each of the defined links.
The following methods can be called with a closure:
g_om.createUser(groovy.lang.Closure p_closure)
A new Intrexx user can be created with this. At the same time, the properties
listed below can be set within the closure. The properties "name" and "loginName"
are mandatory and the others are optional.
Property
Data type
birthday
Date
city
String
container
Object
country
String
defaultLanguage
String
defaultLayout
String
deletable
Boolean
deleted
Boolean
description
String
disabled
Boolean
dn
String
emailBiz
String
emailHome
String
employeeNo
String
enterDate
Date
externalLogin1
String
externalLogin2
String
externalLogin3
String
externalPassword1
String
externalPassword2
String
externalPassword3
String
externalPrimaryGroupId
Integer
female
Boolean
firstName
String
fullName
String
gender
Integer
guid
String
id
Integer
internalUsn
Integer
lastName
String
loginDomain
String
loginDomainLwr
String
loginName
String
loginNameLwr
String
male
Boolean
memberOf
Collection<?>
middleName
String
name
String
password
String
passwordChangedDate
Date
passwordExpires
Boolean
passwordHash
String
phoneBiz
String
phoneFax
String
phoneHome
String
phoneMobileBiz
String
phoneMobileHome
String
phonePager
String
poBox
String
postalCode
String
priority
Integer
rplGuid
String
salt
String
showUser
boolean
state
String
street
String
timeZone
TimeZone
title
String
userImageContentType
String
userImageFile
File
userImageMetaInfo
String
For the properties "container" and "memberOf", you can choose whether
to enter the GUIDs or the unique names of the containers, roles, sets, groups
and paths in the organizational structure.
Returns all currently logged-in users. "p_bIncludeAnonymous" allows you to define
whether anonymous sessions should be included in the result. Additional filters
can be applied via the Groovy closure.
Returns all currently logged-in, non-anonymous users without double entries.
Additional filters can be applied via the Groovy closure in the same way as
"g_om.getLoggedOnUsers(boolean p_bIncludeAnonymous, groovy.lang.Closure".
Allows you to work in the portal organizational structure.
Example
g_om.withOrgStructure {
println("9DABA9EE4F9F6F771704F75C79A1C3A124FF399C".isUser())
}
The following methods can be called within the OrgStructure closure:
isUser()
isSet()
isGroup()
isDistList()
isContainer()
isOrganization()
isOrgUnit()
g_om.getMembers(java.lang.Object,boolean)
Generates a list of the GUIDs of all users that belong to a specific or multiple groups.
"00A303288634E154D755732E478F2BE0D9AD36F7" is the GUID of the group copied from the from the
Users module.
Multiple group GUIDs or set GUIDs can be entered. More information is available here:
This class is intended to check and query permissions.
The usage is demonstrated in the template "JSON response"
for the corresponding use case.
checkPermission(Permission) and check(Closure) check whether the desired
permissions have been granted. Otherwise, a java.security.AccessControlException
is thrown.
hasPermission(Permission) and has(Closure) function in the same way but return
a Boolean value that states whether the requested permissions have been granted
or not. The closure methods delegate to the above-mentioned
GroovyIxAccessControllerDelegate object and demand a map that assigns
actions to the object names.
Example
Check application permissions for the application <application GUID>,
read permission for the data group <data group GUID> and write permission
for the data group <another data group GUID>.
g_permissions.check {
application("<application GUID>": "access")
dataGroup("<data group GUID>": "read", "<another data group GUID>": "write,create")
}
Access the portlet pool that
should be filtered (collection of portlets), on the one hand really in the portlet
pool and on the other hand when rendering the portlet.
Access the current request, e.g. to read request variables in the process.
This variable is only defined if the script was called through a web request.
RtCache - Access to data groups, applications, fields etc.
Example
//Find all data groups of the application with the GUID 68C97BF4D89E8466BDE08AF03A4EF95F5B23AF72
def datagroups = g_rtCache.dataGroups.findAll {it.appGuid == "68C97BF4D89E8466BDE08AF03A4EF95F5B23AF72"}
With this object, values can be read from a system data group.
Example
//Replace the GUID with the GUID of the system data field
def strValue = g_sysDg['C1BFDD165EBFD0713D306D3E2B124E80021E613F']
def strValueByFieldGuid = g_sysDg.getValueByFieldGuid('C1BFDD165EBFD0713D306D3E2B124E80021E613F')
def vhByFieldGuid = g_sysDg.getValueHolderByFieldGuid('C1BFDD165EBFD0713D306D3E2B124E80021E613F')
Returns the entire StackTrace of an occurred error as a string.
The closure "getStackTraceString()" requires a parameter with the type
java.lang.Throwable, i.e. an exception.
So that you don't have to use a static data group
name in SQL statements, thus avoiding problems when importing or making
changes to the data group, a data group can be referenced using its GUID
instead of its name with the aid of this client function.
This is only available via the Intrexx database API.
Example
g_dbQuery.executeAndGetScalarValue(conn, "SELECT COUNT(LID) FROM DATAGROUP('DAF7CECF66481FCABE50E529828116EAFE906962')")
Snippet
def strName = g_rtCache.dataGroups["<Data group GUID>"].name
2.6.2.7. Prepared statement with DELETE (with closure)
Executes a DELETE statement with a closure as a prepared query.
Example
def conn = g_dbConnections.systemConnection
g_dbQuery.executeUpdate(conn, "DELETE FROM DATAGROUP('7AFAF7CB5DE281D35F05D96FCD96CE27692C110F') WHERE ID > ?"){
setInt(1, 5)
}
Please note:
If the statement should be used multiple times, e.g. within a loop, the prepared statement version without a closure is more efficient.
Snippet
def conn = g_dbConnections.systemConnection
g_dbQuery.executeUpdate(conn, "DELETE FROM DATAGROUP('<DATAGROUP_GUID>') WHERE <CONDITION>"){
//setInt(1, 1)
//setString(2, "Example text")
//setTimestamp(3, now().withoutFractionalSeconds)
//setBoolean(4, true)
2.6.2.8. Read a single value from a database query
Reads a single value from a database query. If the result set is empty, or the
value is null, the fallbackValue is returned.
If the return data type value should be defined explicitly, typified method calls such as
executeAndGetScalarBooleanValue(...) can be used.
Example
def value = g_dbQuery.executeAndGetScalarValue(conn, "SELECT MAX(ID) FROM DATAGROUP('7AFAF7CB5DE281D35F05D96FCD96CE27692C110F')", 0)
or with a prepared statement
def value = g_dbQuery.executeAndGetScalarValue(conn, "SELECT MAX(ID) FROM DATAGROUP('7AFAF7CB5DE281D35F05D96FCD96CE27692C110F') WHERE DTEDIT < ?", 0) {
setTimestamp(1, now().withoutFractionalSeconds)
Snippet
def conn = g_dbConnections.systemConnection
def value = g_dbQuery.executeAndGetScalarValue(conn, "SELECT <COLUMNS> FROM DATAGROUP('<DATAGROUP_GUID>') WHERE <CONDITION>", <FALLBACK_VALUE>) {
//setString(1, "Example text.")
}
2.6.2.9. Read a single value using a prepared database query
Reads a single value using a prepared database query. If the result set is
empty, or the value is null, the fallbackValue is returned.
If the return data type value should be defined explicitly, typified method calls such as
executeAndGetScalarBooleanValue(...) can be used.
Example
def stmt = g_dbQuery.prepare(conn, "SELECT MAX(LID) FROM DATAGROUP('7AFAF7CB5DE281D35F05D96FCD96CE27692C110F')")
def value = stmt.executeAndGetScalarValue(0)
stmt = Safely.close(stmt)
Calls the underlying web service. Only valid for scripts defined within a web service.
Snippet
g_ws.invoke()
2.7.2. Read web service input values
Read values defined as input parameters for a web service. Use the name
of the control, which contains the value, as the variable name.
Example
g_ctx.requestVars.textcontrolD72A9620
Snippet
g_ctx.requestVars.
2.7.3. Read web service response values
Read values defined as response parameters from a web service. Use the name of
the control, where the response value will be written, as the variable name.
Example
g_ctx.bpeeVars.textvcontrol72EF4A0B.value
Snippet
g_ctx.bpeeVars..value
2.8. Case differentiation
2.8.1. switch statement for Groovy condition
Usable in processes. The returned values correspond to the outgoing
connections (connection ID)
of the Groovy condition.
Snippet
switch (g_record[""].value)
{
case "expected value 1":
return connectionId1
case "expected value 2":
return connectionId2
default:
return connectionId3
}
import de.uplanet.lucy.server.workflow.event.IAfterCreateDataGroupWorkflowEvent
import de.uplanet.lucy.server.workflow.event.IAfterUpdateDataGroupWorkflowEvent
import de.uplanet.lucy.server.workflow.event.IBeforeDeleteDataGroupWorkflowEvent
import de.uplanet.lucy.server.workflow.event.INotifyDataGroupWorkflowEvent
switch (g_event)
{
case IAfterCreateDataGroupWorkflowEvent:
g_log.info("A new record was inserted.")
break
case IAfterUpdateDataGroupWorkflowEvent:
g_log.info("A record was updated.")
break
case IBeforeDeleteDataGroupWorkflowEvent:
g_log.info("A record will be deleted.")
break
case INotifyDataGroupWorkflowEvent:
g_log.info("A timer resubmitted a record.")
break
default:
g_log.warn("Unhandled event ${g_event}.")
break
}
Distinguish the first record from the subsequent records that are delivered
by a global data group timer.
Snippet
if (g_sharedState["wasHere${g_guidSelf}"])
{
// we were here before
}
else
{
// we are here for the first time
g_sharedState["wasHere${g_guidSelf}"] = true
}
2.9. Mathematical calculations
2.9.1. Round half away from zero
Round half away from zero with the option of defining the number
of decimal places.
Here you can find script for formatting date values.
Snippet
import java.text.SimpleDateFormat
// specify the time zone
def tz = g_session.user.timeZone
//def tz = de.uplanet.lucy.server.DefaultTimeZone.get()
//def tz = TimeZone.getTimeZone("Europe/Berlin")
assert tz != null
// the date/time to be formatted
def dt = new Date()
// the date format
// https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/text/SimpleDateFormat.html
def fmt = new SimpleDateFormat("yyyy-MM-dd")
fmt.setTimeZone(tz)
println(fmt.format(dt))
A data record contains data for a start and an end date. The duration between
these two date values is calculated. Please note that time zones are not considered.
Snippet
def conn = g_dbConnections.systemConnection
def dtStart = g_record["GUID"].value // datafield startDate
def dtEnd = g_record["GUID"].value // datafield endDate
def iID = g_record["GUID"].value // datafield (PK) (S) ID
use (groovy.time.TimeCategory)
{
def duration = dtEnd - dtStart
def stmt = g_dbQuery.prepare(conn, "UPDATE DATAGROUP('<DATAGROUP_GUID>') SET <COLUMN_DURATION> = ? WHERE LID = ?")
stmt.setInt(1, duration.days)
stmt.setInt(2, iId)
stmt.executeUpdate()
stmt.close()
}
2.10.3. Parse ISO date string
Parses an ISO formatted string into an "java.util.Date" object.
import de.uplanet.lucy.server.mail.GroovyMailBuilder
def mail = new GroovyMailBuilder().composeMail {
from = "sender@example.org"
to = "recipient@example.org"
subject = "Insert the subject here"
body << "Hello world"
}
mail.drop()
2.11.2. Groovy email
Create an HTML email with inline images and attachment.
Snippet
import de.uplanet.lucy.server.mail.GroovyMailBuilder
def fileImg = new File(g_dirWorkflow, "theImage.png")
def fileAttachment = new File(g_dirWorkflow, "document.pdf")
def mail = new GroovyMailBuilder().composeMail {
headers = [
"Importance": "High",
"X-Priority": "1"
]
from = "sender@example.org"
to = ["recipient-1@example.org", "recipient-2@example.org"]
subject = "Insert the subject here"
contentType = "text/html; charset=UTF-8"
body << """<html>
<body>
<h1>Hello World</h1>
<p>
Look at this image
<br>
<img src="${srcInlineImage(fileImg)}">
<br>
Nice.
</p>
</body>
</html>"""
// note: name is an optional parameter
attachFile(file: fileAttachment, name: "nameOfTheAttachment.pdf", contentType: "application/pdf")
}
mail.drop()
2.11.3. Send email via Ant
Snippet
def strFromAddr = g_session?.user?.emailBiz
def strFromName = g_session?.user?.name
def strToAddr = "" // insert the recipient here
new AntBuilder().mail(mailhost:"localhost", messagemimetype:"text/plain", subject:"Hello World") {
from(address:"${strFromName} <${strFromAddr}>")
to(address:addrTo)
message("""Here goes the message text.
With kind regards
${strFromName}
""")
}
2.12. Files and directories
2.12.1. Identify disk space
Identifies the free and total disk space.
Snippet
// the partition where the Intrexx portal is stored
File partition = new File(".")
long totalSpace = partition.totalSpace
long freeSpace = partition.freeSpace
Access a temporary workflow directory. The directory is also available to
subsequent workflow objects. It is deleted as soon as the workflow
has been fully executed.
Add a file to an existing record. With "copyFileToIntrexx(...)" the source
file is copied and remains at its storage location. An analog call can be
made with "moveFileToIntrexx(...)". In this case, the source file will
be moved to Intrexx.
Parameters:
p_ctx - Current processing context (g_context)
p_fileSrc - File to be added to record
p_strFieldGUID - Data field GUID
p_recId - ID of record that the file should be added to
p_bTriggerWorkflow - Should a workflow be triggered that responds to data record changes?
g_om.createUser {
container = "System"
name = "user-${now().withoutFractionalSeconds}"
loginName = "UserU-${now().withoutFractionalSeconds}"
password = "secret"
emailBiz = "user@example.org"
description = "User created with Groovy at ${now().withoutFractionalSeconds}"
}
2.14.2. Create a user
Snippet
/ provide copy of user image (optional)
def fileTemplImage = new File(g_dirWorkflow, "user.jpg")
def fileUserImage = new File(g_dirWorkflowTmp, "user.jpg")
de.uplanet.io.IOHelper.copyFile(fileUserImage, fileTemplImage)
def user = g_om.createUser {
container = "System"
name = "user-${now().withoutFractionalSeconds}"
loginName = "UserU-${now().withoutFractionalSeconds}"
password = "secret"
emailBiz = "user@example.org"
description = "User created with Groovy at ${now().withoutFractionalSeconds}"
nickName = "dodo" // this is a custom user field
userImageFile = fileUserImage
// provide a list of sets (group, role, dist list, ...) the user should be member of
// allowed are GUIDs, unique names, paths, or org structure set nodes
memberOf = ["importantRole", "36B3BFD54A57BE5D1EE51288D920CDA9B20A67A4"]
}
// create a new password and send it to the user's emailBiz address (optional)
g_om.generateAndSendPassword(user, g_defaultLanguage)
2.14.3. Classify GUIDs
Classify GUIDs by
user
container (organization, organizational unit, ...)
set (group, rol, DistList, ...)
unclassified GUID
Snippet
def classified = g_om.classifyGuids(/* a list of GUIDs or text that contains GUIDs */)
// do something with the classified GUIDs
classified.users
classified.containers
classified.sets
classified.unclassified
2.15. Categories
2.15.1. Date / Time
Snippet
use (groovy.time.TimeCategory)
{
}
2.15.2. IValueHolder
Snippet
use (de.uplanet.lucy.server.scripting.groovy.GroovyIntrexxValueHolderCategory)
{
}
Please note that a permissions check (IxAccessController)
is required before any action that is triggered by a user.
Snippet
response.json()
// define an error handler
response.onError = {e, err ->
//err.type = "default"
// either title/description, ...
err.title = "my error"
err.description = e.message
err.showEmbedded = true
// ... or a redirect
// err.redirectUrl = "http://www.example.org/"
// err.redirectDelay = 1500 // milliseconds
}
// check permissions
g_permissions.check {
// application("${application.guid}": "access")
// dataGroup("<data group GUID>": "read", "<another data group GUID>": "write,create")
}
// create some JSON content
writeJSON("person": [
givenName: "Donald",
lastName: "Duck",
age: 78,
nephews: ["Huey", "Dewey", "Louie"]
])
Possible variations of the script above
Closure without or with exactly one parameter
If the closure declares a parameter, the exception that occurred is transferred
to the closure in the first parameter (e).
Snippet
// define an error handler
response.onError = {e ->
writeJSON("error": "Something bad happened.")
}
Closure with two parameters
The exception that occurred is transferred to the closure in
the first parameter (e) and a
ErrorResponseData object
is transferred in the second parameter (err).
The ErrorResponseData object can be configured in the closure according to the
respective requirements.
Snippet
// define an error handler
response.onError = {e, err ->
//err.type = "default"
// either title/description, ...
err.title = "my error"
err.description = e.message
err.showEmbedded = true
// ... or a redirect
// err.redirectUrl = "http://www.example.org/"
// err.redirectDelay = 1500 // milliseconds
}
With a string
The stated string is sent to the client in the response body.
Snippet
response.onError = '{"error": "Something bad happened."}'
Assign another object
In a JSON response scenario, an attempt is made to generate JSON from the object.
If the response is not JSON, an attempt is made to output the object in a suitable
manner as text.
Snippet
response.onError = [
"error": "Something bad happened.",
"solace": "But things could be worse."
]
12.19. Velocity
2.19.1. Create text from a Velocity template
Snippet
import de.uplanet.lucy.server.scripting.velocity.VelocityContextUtil
import org.apache.velocity.app.Velocity
def fileVm = new File(g_dirWorkflow, "") // the Velocity input file
def vc = VelocityContextUtil.createDefaultContext(g_context)
// add additional variables to the Velocity context
// vc.put("PreparedQuery", g_dbQuery)
// vc.put("variableName", variableValue)
def template = Velocity.getTemplate(fileVm.path)
def writer = new StringWriter(4096) // 4 KiB initial buffer
template.merge(vc, writer)
g_log.info(writer.toString())
2.19.2. Create a file from a Velocity template
For security reasons, Velocity files can only be executed if they are stored in one of the following subfolders:
Writes an INFO log entry to the portal log file (portal.log).
This method can be used if the log file of the current processing context
is not portal.log, but portal.log should be used.
Writes a WARN log entry to the portal log file (portal.log).
This method can be used if the log file of the current processing context is
not the portal.log, but portal.log should be used.
Writes an ERROR log entry to the portal log file (portal.log).
This method can be used if the log file of the current processing context
is not the portal.log, but portal.log should be used.
import de.uplanet.lucy.server.monitor.log.GroovyLogReader
// flush queued log entries to disk
GroovyLogReader.flushLogQueue()
// collect the log files
def logFiles = []
new File("internal/statistics").eachFile {
if (!it.name.startsWith(".")) // TODO check additional criteria
logFiles << it
}
// read the selected log files
logFiles.each { file ->
GroovyLogReader.readLog(file) { entry ->
// TODO do something with the entry
println("Time in millis = ${entry.time}, targetGuid = ${entry.targetGuid}")
}
}
2.23. Connector for Microsoft Exchange
2.23.1. Current Exchange connection
Returns the current connection to Microsoft Exchange.
Creates an appointment for the current Exchange user.
Parameters:
dtStartDate - Start date of the appointment
dtEndDate - End date of the appointment
strSubject - Subject of the appointment
strBody - Description of the appointment
If additional properties are set or updated with set()-methods after creating the appointment, they have to be saved with appointment.save() afterwards.
2.24.2. Restrict portlets in the portlet container
To restrict portlets in the portlet container, a list with portlet objects must be returned.
You can retrieve all available portlet objects with "g_portletPool".
In this example, only the portlet with the GUID entered instead of the placeholder would be available.
2.27. Automatic logout of implicitly generated sessions
If a Groovy endpoint is called and the client does not send a valid session ID, then an anonymous session is implicitly generated on the server.
The following script allows you to prevent these sessions from staying on the server until the timeout of the anonymous session.
The criterion for the automatic logout is that the client has not sent a co_SId session cookie.
Snippet
import de.uplanet.server.transaction.TransactionManager
// if the client did not send a session cookie we logout
// the session after the current transaction has finished
if (!g_request.get('co_SId'))
TransactionManager.addAfterCommitAction {g_session.logout()}