foo + bar
MidPoint Expression Language Specification
|
MidPoint Expression Language feature
This page describes configuration of MidPoint Expression Language midPoint feature.
Please see the feature page for more details.
|
MidPoint Expression Language (MEL) is an expression language designed to support customization of midPoint functionality in an easy-to-use and secure way.
This page provides detailed description of the language. It is meant to be a reference documentation of the language.
| See MEL Introduction for a step-by-step introduction to MEL language. If you are used to create Groovy scripts, MEL Migration Guide may be a good start. |
Literals
| Literal | Prefix / Delimiter / Values | Example | Notes |
|---|---|---|---|
String |
|
|
|
Numeric |
none |
|
|
Boolean |
|
|
|
Bytes |
|
|
|
Null |
|
|
|
List |
|
|
|
Map |
|
|
|
Timestamp |
|
|
|
Duration |
|
|
Operators
Following operators can be used in MEL expressions:
| Operator | Meaning | Notes |
|---|---|---|
|
Equality |
|
|
Inequality |
|
|
Greater than |
|
|
Greater than or equal |
|
|
Less than |
|
|
Less than or equal |
|
|
Addition, concatenation |
Can be used for algebraic addition or string concatenation. |
|
Negation, subtraction |
|
|
Multiplication |
|
|
Division |
|
|
Remainder |
|
|
Logical NOT |
|
|
Logical AND |
|
|
Logical OR |
|
|
Grouping |
|
|
Inclusion test |
|
|
Conditional |
|
|
Selection |
Used to resolve structured data, assumes non-null operand. |
|
Optional selection |
Used to resolve structured data, can handle null operand. |
|
Index |
Used to retrieve specific value from a list, map or structured data. Assumes non-null operand. |
|
Optional Index |
Used to retrieve specific value from a list, map or structured data. Can handle null operand. |
Variables
Variables are demoted without any prefix.
E.g. following expression is adding values of two variables, foo and bar:
The usual expression and mapping variables are available for MEL expressions. However, there are few exceptions:
-
Deprecated variables
user,accountandshadoware not present in MEL expressions. Use correct variables names instead (focusandprojection). -
Variables that represent services for Java-like languages (
prismContext,localizationService) do not make sense in MEL environment, therefore these are not present. -
Some script expression environments expose built-in extension libraries in a form of variables (
basic,midpoint,log). These variables are not present in MEL environment. Equivalent functionality is available to MEL expressions in a form of native MEL language extensions, e.g. asnorm,log.debug(…)ormidpoint.getObject(…)functions. See migration guide for more details.
There is additional variable available to all MEL expressions:
-
Variable
nowcontains current timestamp. It is a fixed timestamp of an approximate moment of the script execution. Value of the variable does not change during script execution.
Functions and Macros
There are two types of functions: global functions and member functions.
-
Global functions are specified by their name, providing all the inputs to the function as parameters.
size(focus.assignment) > 0 matches(focus.telephoneNumber, '^[0-9]+$') -
Member functions are executed with respect to some value, usually a variable. E.g. the
containsfunction in the example below is a member function, which is executed on a string value provided in variables.s.contains('foo')
Some functions are available in both global and member versions, such as isEmpty() function:
foo.isEmpty() isEmpty(foo)
Following sections provide description of functions available to all MEL expressions.
all
list(A).all(A, predicate(A)) → bool
map(A, B).all(A, predicate(A)) → bool
Macro which tests whether all elements in the input list or all keys in a map satisfy the given predicate.
// Returns true if all assignments in a focus have targetRef
focus.assignment.all(a, has(a.targetRef))
ascii
ascii(any) → string
Converts the argument to plain ASCII (ASCII7), all non-ASCII characters and all non-printable characters are removed. ASCII characters with diacritical marks are converted to their basic variants (removing the marks).
// Returns 'Cortuv hrad, tam Strasa!'
ascii('Čórtův hrád, tam Strašá!')
charAt
string.charAt(int) → string
Returns the character at the given position (starting at 0). If the position is negative, or greater than the length of the string, the function will produce an error.
'midpoint'.charAt(1) // Returns 'i'
contains
string.contains(substring) → bool
Function that tests whether the string contains the substring. Works on strings as well as polystrings.
// Checks whether the full name contains substring 'Mr.'
focus.fullName.contains('Mr.')
containsIgnoreCase
string.containsIgnoreCase(substring) → bool
Function that tests whether the string contains the substring without regard to character case. Works on strings as well as polystrings.
// Checks whether the full name contains substring 'Mr.'
focus.fullName.contains('Mr.')
debugDump
debugDump(any) → string
Returns formatted, human-friendly, multi-line dump of a complex data structure.
// Returns formatted, multi-line dump of the data structure in focus variable
debugDump(focus)
This function can be especially useful when combined with logging functions:
// Returns formatted, multi-line dump of the data structure in focus variable
log.trace('Focus object: {}', debugDump(focus))
default
default(any, defaultValue) → any
Returns a value provided as a first argument as long as that value is non-null.
If null (or equivalent) value is provided, it returns default value (second argument) instead.
'Hello ' + default(foo, 'world') // Returns 'Hello world' if value of foo is null
endsWith
string.endsWith(suffix) → bool
Function that tests whether the string ends with a specified suffix. Works on strings as well as polystrings.
// Checks whether the full name ends with 'PhD.' suffix
focus.fullName.endsWith('PhD.')
exists
list(A).exists(A, predicate(A)) → bool
map(A,B).exists(A, predicate(A)) → bool
Macro which tests whether any value in the list or any key in the map satisfies the predicate expression.
// Returns true if any assignment in a focus contains a policy rule
focus.assignment.exists(a, has(a.policyRule))
exists_one
list(A).exists_one(A, predicate(A)) → bool
map(A,B).exists_one(A, predicate(A)) → bool
Macro which tests whether exactly one value in the list or any key in the map satisfies the predicate expression.
// Returns true if exactly one assignment in a focus contains a policy rule
focus.assignment.exists_one(a, has(a.policyRule))
filter
list(A).filter(A, predicate(A)) → list(A)
map(A,B).filter(A, predicate(A)) → list(A)
Macro which returns a list containing only the elements from the input list that satisfy the given predicate.
// Returns lists of assignments that have targetRef specified
focus.assignment.filter(a, has(a.targetRef))
format
string.format(list) → string
Format strings according to specified template, filling in data from the arguments.
Format specification follows Java formatter conventions.
// Returns 'Jack has 3 apples'
'%s has %d apples'.format(['Jack', 3])
It is strongly recommended to use format() function instead of add operator (+) for string concatenation.
Unlike the add operator (+), the format() function can work reliably with all data types and values, including null values.
has
has(variable.item) → bool
Macro that checks whether the item exists within a data structure.
In MEL, it is recommended to use isPresent() function instead of has().
has(focus.activation.administrativeStatus)
indexOf
string.indexOf(substring) → int
string.indexOf(substring, offset) → int
Returns the integer index of the first occurrence of the search string. If offset is specified, the search begins at the offset. If the search string is not found the function returns -1.
'midpoint is in the middle'.indexOf('id') // Returns 1
'midpoint is in the middle'.indexOf('id', 5) // Returns 20
isBlank
isBlank(string) → bool
string.isBlank() → bool
Returns true if string is blank (has zero length or contains only white characters).
'foo'.isBlank() // Returns false
' '.isBlank() // Returns true
''.isBlank() // Returns true
isEmpty
isEmpty(string) → bool
isEmpty(list) → bool
string.isEmpty() → bool
Returns true if string is empty (has zero length), or if it is null. Also returns true if the argument is an empty list.
'foo'.isEmpty() // Returns false
' '.isEmpty() // Returns false
''.isEmpty() // Returns true
isEmpty([]) // Returns true
isNull
isNull(any) → bool
Returns true if the argument is null or any of its equivalents.
This function works reliably in all the cases when a test for "nullness" would be done.
E.g. it works with variables as well as optionals that are result of .? operator use.
Use of this function is strongly recommended for null checks in all places.
isNull(null) // Returns true
isNull('foo') // Returns false
Function isNull() can be used with structured data as well.
The following expression returns false only if (non-null) value of validFrom property is present.
It returns true in all other cases, such as when focus variable contains null, if the object in focus variable does not have activation container, if the container does not contain validFrom property or if that property is null.
isNull(focus.?activation.?validFrom)
It is strongly recommended to use isNull() function instead of null equality check (e.g. foo == null) in all cases.
isPresent
isPresent(any) → bool
Returns true if the argument is present, i.e. if it is not null nor any of its equivalents.
This function works reliably in all the cases when a test for presence would be done.
E.g. it works with variables as well as optionals that are result of .? operator use.
Use of this function is strongly recommended for presence checks in all places.
isPresent(null) // Returns false
isPresent('foo') // Returns true
Function isPresent() can be used with structured data as well.
The following expression returns true only if (non-null) value of validFrom property is present.
It returns false in all other cases, such as when focus variable contains null, if the object in focus variable does not have activation container, if the container does not contain validFrom property or if that property is null.
isPresent(focus.?activation.?validFrom)
It is strongly recommended to use isPresent() function instead of null inequality check (e.g. foo != null) in all cases.
join
list(string).join() → string
list(string).join(separator) → string
Returns a new string where the elements of string list are concatenated using the separator. If no separator is specified, empty string is used as separator.
['foo','bar','baz'].join() // Returns 'foobarbaz'
['foo','bar','baz'].join(',') // Returns 'foo,bar,baz'
lastIndexOf
string.lastIndexOf(substring) → int
string.lastIndexOf(substring, offset) → int
Returns the integer index of the last occurrence of the search string. If offset is specified, the search begins at the offset. If the search string is not found the function returns -1.
'midpoint is in the middle'.lastIndexOf('id') // Returns 20
'midpoint is in the middle'.lastIndexOf('id', 5) // Returns 1
lc
string.lc() → string
Returns a new string where all characters are lower-cased.
'Čórtúv Hrád'.lc() // Returns 'čórtúv hrád'
list
list(scalar) → list
list(list) → list
Returns list composed of specified argument. If a scalar argument is provided, it returns a single-element list containing that argument. If a list is provided as argument, the same list is returned.
list('foo') // Returns ['foo']
list(['foo']) // Returns ['foo']
list(['foo','bar']) // Returns ['foo','bar']
lowerAscii
string.lowerAscii() → string
Returns a new string where all ASCII characters are lower-cased. This function does not perform Unicode case-mapping for characters outside the ASCII range.
'Čórtúv Hrád'.lowerAscii() // Returns 'Čórtúv hrád'
This is s stock CEL function, it is mostly provided just for compatibility.
As this function is not mapping non-ASCII characters, it is not suitable for international unvironments.
Use lc() function instead.
|
map
list(A).map(A, transform(A)) → list(A)
list(A).map(A, predicate(A), transform(A)) → list(A)
map(A,B).map(A, transform(A)) → list(A)
map(A,B).map(A, predicate(A), transform(A)) → list(A)
Macro returns a list where each element is the result of applying the transform expression to the corresponding input list element or input map key.
There are two forms of the map macro:
-
The two argument form transforms all elements.
-
The three argument form transforms only elements which satisfy the predicate.
The three-argument form of the macro is equivalent to combined filter / map operations.
// Returns lists of descriptions retrieved from all the assignments
focus.assignment.map(a, a.description)
// Returns lists of target OIDs retrieved from assignments that have targetRefs
focus.assignment.map(a, has(a.targetRef) a.targetRef.oid)
matches
string.matches(regex) → bool
matches(string, regex) → bool
Function that tests whether the string matches a specified RE2 regular expression. Works on strings as well as polystrings.
The match function tests for a partial match of regular expression.
I.e. the function returns true if any substring of the specified string matches specified regular expression.
If a different behavior is needed, the regular expression needs to be anchored, e.g. by using ^ and $ anchors.
RE2 expressions are a subset of Perl/PCRE regular expressions. See also RE2 syntax definition.
// Checks whether the telephone number contains numbers only
focus.telephoneNumber.matches('^[0-9]+$')
// Checks whether the full name contains a number anywhere in the string
focus.fullName.matches('[0-9]+')
norm
norm(string) → string
Returns string in a normalized form. It follows the default normalization algorithm, the same algorithm as used for PolyString values. This function works on strings as well as polystrings.
norm(' Semančík') // Returns 'semancik'
replace
string.replace(search, replacement) → string
string.replace(search, replacement, limit) → string
Returns a new string based on the target, which replaces the occurrences of a search string with a replacement string if present. The function accepts an optional limit on the number of substring replacements to be made. When the replacement limit is 0, the result is the original string. When the limit is a negative number, the function behaves the same as replace all.
'sailpoint'.replace('sail','mid') // Returns 'midpoint'
single
single(scalar) → scalar
single(list) → scalar
Function that returns a single value from a list.
If a list with a single element is provided as function argument, that element is returned.
If empty list is provided to the function, it returns null.
If a list with more than one element is provided, the function issues an error.
If a simple scalar value is presented to the function, the same value is returned.
// Returns 'foo'
single(['foo'])
// Returns 'foo'
single('foo')
// Error
single(['foo','bar'])
size
string.size() → int
size(string) → int
bytes.size() → int
size(bytes) → int
list.size() → int
size(list) → int
map.size() → int
size(map) → int
Function that determines length of a string, number of bytes in a sequence or size of a list or map.
// Returns number of characters in a full name
focus.fullName.size()
// Returns number of assignments in a focus
size(focus.assignment)
split
string.split(separator) → list(string)
string.split(separator, limit) → list(string)
Returns a list of strings split from the input by the given separator. Limit on the number of substrings produced by the split can be specified.
'foo,bar,baz'.split(',') // Returns ['foo','bar','baz']
startsWith
string.startsWith(prefix) → bool
Function that tests whether the string starts with a specified prefix. Works on strings as well as polystrings.
// Checks whether the full name ends with 'Ing.' prefix
focus.fullName.startsWith('Ing.')
str
str(any) → string
Converts any value provided as an argument to string.
This function is nullable.
If a null argument is provided as an input, null value is returned.
This function is useful for type conversion and unification (e.g. converting string, polystring and numbers to basic string).
For a function suitable for string formatting see stringify().
str(['foo','bar']) // Returns '["foo","bar"]'
str(3) // Returns '3'
str(null) // Returns null
stringify
stringify(any) → string
stringify(any, default)
Formats any value provided as an argument to string.
The value is converted to string in a sensitive way.
E.g. the function tries to detect collections and returns the first element (if there is only one).
This function always provides non-null output.
If null value is presented as an argument, the function returns the default value instead.
If no default value is explicitly specified, empty string is returned.
This function is useful for string formatting and diagnostics.
For type conversion function see str().
stringify(['foo','bar']) // Returns '["foo","bar"]'
stringify(3) // Returns '3'
stringify(null, '?') // Returns '*'
substring
string.substring(beginIndex) → string
string.substring(beginIndex, endIndex) → string
Returns a string that is a substring of this string.
The substring begins at the specified beginIndex and extends to the character at index endIndex - 1.
Thus the length of the substring is endIndex - beginIndex.
If endIndex is not specified, the substring extends to the end of original string.
Empty string is returned in case that the indexes extend beyond the original string.
'midpoint'.substring(0,3) // Returns 'mid'
The substring function does not fail in case the indexes extend beyond the original string.
This behavior differs from stock behavior of Google CEL string extensions.
This behavior change is very intentional, enabling creation of simpler expressions.
|
trim
string.trim() → string
Returns a new string which removes the leading and trailing whitespace in the target string. The trim function uses the Unicode definition of whitespace which does not include the zero-width spaces.
' midpoint '.trim() // Returns 'midpoint'
upperAscii
string.upperAscii() → string
Returns a new string where all ASCII characters are upper-cased. This function does not perform Unicode case-mapping for characters outside the ASCII range.
'Čórtúv Hrád'.upperAscii() // Returns 'ČóRTúV HRáD'
This is s stock CEL function, it is mostly provided just for compatibility.
As this function is not mapping non-ASCII characters, it is not suitable for international unvironments.
Use uc() function instead.
|
uc
string.uc() → string
Returns a new string where all characters are upper-cased.
'Čórtúv Hrád'.uc() // Returns 'ČÓRTÚV HRÁD'
Language Extensions
Common expression language (CEL) which was a foundation for MidPoint Expression Language (MEL) is quite limited. Therefore, MEL provides additional language extensions that implement functions commonly used in midPoint environment. Many MEL language extensions fit naturally into the language and are usually not noticed at all (e.g. see polystrings below). However, other extensions are explicitly distinguished, to make their purpose clear and to avoid conflicts with native functionality of the language. Most of the extensions have explicit prefix in function names.
Format Extension
Prefix: format.
Format extension is used to format, parse and otherwise process string-formatted data.
format.concatName
format.concatName(list) → string
Concatenates a user-friendly name from the list of provided components. Each argument is converted to string (stringified), trimmed and the result is concatenated by spaces.
// Returns 'Radovan Semančík'
format.concatName(['Radovan', 'Semančík'])
format.strftime
format.strftime(timestamp, format) → string
timestamp.strftime(format) → string
Formats provided timestamp to string, using a format template specified in POSIX notation.
This is POSIX-compatible function, using a POSIX time formatting conventions. This format is used by other variants of Common expression language (CEL). Use of this function is generally recommended instead of Java-like variants below.
// Returns formatted time, e.g. `11/02/26 09:32:15`
format.strftime(ts, '%d/%m/%Y %H:%M:%S')
This function can also be invoked as a member function on a timestamp:
// Returns formatted time, e.g. '11/02/26 09:32:15'
ts.strftime('%d/%m/%Y %H:%M:%S')
format.strptime
format.strptime(string, format) → timestamp
string.strptime(format) → timestamp
Parses provided string to timestamp, using a format template specified in POSIX notation.
This is POSIX-compatible function, using a POSIX time formatting conventions. This format is used by other variants of Common expression language (CEL). Use of this function is generally recommended instead of Java-like variants below.
// Returns a timestamp
format.strptime('11/02/26 09:32:15', '%d/%m/%Y %H:%M:%S')
This function can also be invoked as a member function on a string:
// Returns a timestamp
'11/02/26 09:32:15'.strptime('%d/%m/%Y %H:%M:%S')
format.formatDateTime
format.formatDateTime(timestamp, format) → string
timestamp.formatDateTime(format) → string
Formats provided timestamp to string, using a format template specified in Java SimpleDateFormat notation.
This is Java-like function, compatible with original midPoint 3.x basic expression functions.
This function is mostly provided for users that are accustomed to Java conventions and for the purposed of easier migration of Groovy scripts to MEL.
Use of this function is generally not recommended.
Use strftime function instead.
// Returns formatted time, e.g. `11/02/26 09:32:15`
format.formatDateTime(ts, 'dd/MM/YY HH:mm:ss')
This function can also be invoked as a member function on a timestamp:
// Returns formatted time, e.g. '11/02/26 09:32:15'
ts.formatDateTime('dd/MM/YY HH:mm:ss')
format.parseDateTime
format.parseDateTime(string, format) → timestamp
string.parseDateTime(format) → timestamp
Parses provided string to timestamp, using a format template specified in Java SimpleDateFormat notation.
This is Java-like function, compatible with original midPoint 3.x basic expression functions.
This function is mostly provided for users that are accustomed to Java conventions and for the purposed of easier migration of Groovy scripts to MEL.
Use of this function is generally not recommended.
Use strptime function instead.
// Returns a timestamp
format.parseDateTime('11/02/26 09:32:15', 'dd/MM/YY HH:mm:ss')
This function can also be invoked as a member function on a string:
// Returns a timestamp
'11/02/26 09:32:15'.parseDateTime('dd/MM/YY HH:mm:ss')
format.parseGivenName
format.parseGivenName(string) → string
string.parseGivenName() → string
Parses provided string as a person’s full name, returning the given name (first name) part.
This functions makes some assumptions regarding common structure of full names (e.g. the given name comes first and family name comes last), trying to match individual name components.
// Returns 'Radovan'
format.parseGivenName('Ing. Radovan Semančík, PhD.')
This function can also be invoked as a member function on a string or polystring:
// Returns 'Radovan'
'Ing. Radovan Semančík, PhD.'.parseGivenName()
format.parseFamilyName
format.parseFamilyName(string) → string
string.parseFamilyName() → string
Parses provided string as a person’s full name, returning the family name (last name) part.
This functions makes some assumptions regarding common structure of full names (e.g. the given name comes first and family name comes last), trying to match individual name components.
// Returns 'Semančík'
format.parseFamilyName('Ing. Radovan Semančík, PhD.')
This function can also be invoked as a member function on a string or polystring:
// Returns 'Semančík'
'Ing. Radovan Semančík, PhD.'.parseFamilyName()
format.parseAdditionalName
format.parseAdditionalName(string) → string
string.parseAdditionalName() → string
Parses provided string as a person’s full name, returning the additional name (middle name) part.
This functions makes some assumptions regarding common structure of full names (e.g. the given name comes first and family name comes last), trying to match individual name components.
// Returns 'Clerk'
format.parseAdditionalName('James Clerk Maxwell')
This function can also be invoked as a member function on a string or polystring:
// Returns 'Clerk'
'James Clerk Maxwell'.parseAdditionalName()
format.parseNickName
format.parseNickName(string) → string
string.parseNickName() → string
Parses provided string as a person’s full name, returning the nickname part.
This functions makes some assumptions regarding common structure of full names (e.g. the given name comes first and family name comes last), trying to match individual name components.
// Returns 'The King'
format.parseNickName('Elvis "The King" Presley')
This function can also be invoked as a member function on a string or polystring:
// Returns 'The King'
'Elvis "The King" Presley'.parseNickName()
format.parseHonorificPrefix
format.parseHonorificPrefix(string) → string
string.parseHonorificPrefix() → string
Parses provided string as a person’s full name, returning the honorific prefix (titles before the name) part.
This functions makes some assumptions regarding common structure of full names (e.g. the given name comes first and family name comes last), trying to match individual name components.
// Returns 'Ing.'
format.parseHonorificPrefix('Ing. Radovan Semančík, PhD.')
This function can also be invoked as a member function on a string or polystring:
// Returns 'Ing.'
'Ing. Radovan Semančík, PhD.'.parseHonorificPrefix()
format.parseHonorificSuffix
format.parseHonorificSuffix(string) → string
string.parseHonorificSuffix() → string
Parses provided string as a person’s full name, returning the honorific suffix (titles after the name) part.
This functions makes some assumptions regarding common structure of full names (e.g. the given name comes first and family name comes last), trying to match individual name components.
// Returns 'PhD.'
format.parseHonorificSuffix('Ing. Radovan Semančík, PhD.')
This function can also be invoked as a member function on a string or polystring:
// Returns 'PhD.'
'Ing. Radovan Semančík, PhD.'.parseHonorificSuffix()
Log Extension
Prefix: log.
Log extension is used to record diagnostic information in system logs.
Functions of the log extensions can be used to record errors, as well as troubleshoot the expressions.
See also debugDump function, which can be useful when diagnosing content of a complex data structures.
log.info
log.info(format, any) → any
log.info(format, list) → list
Records formatted message in system logs at INFO level.
The arguments are formatted into a log message.
Curly brackets ({}) act as placeholder for arguments in the message.
The function returns its argument as its output, which enables placing of log functions into complex expression without breaking their flow.
// Logs message 'The system is broken', returns 'system' as its output
log.info('The {} is broken', 'system')
// Logs message 'The system is broken due to a bug', returns ['system', 'bug'] as its output
log.info('The {} is broken due to a {}', [ 'system', 'bug' ])
log.error
log.error(format, any) → any
log.error(format, list) → list
Records formatted message in system logs at ERROR level.
The arguments are formatted into a log message.
Curly brackets ({}) act as placeholder for arguments in the message.
The function returns its argument as its output, which enables placing of log functions into complex expression without breaking their flow.
// Logs message 'The system is broken', returns 'system' as its output
log.error('The {} is broken', 'system')
// Logs message 'The system is broken due to a bug', returns ['system', 'bug'] as its output
log.error('The {} is broken due to a {}', [ 'system', 'bug' ])
log.warn
log.warn(format, any) → any
log.warn(format, list) → list
Records formatted message in system logs at WARN level.
The arguments are formatted into a log message.
Curly brackets ({}) act as placeholder for arguments in the message.
The function returns its argument as its output, which enables placing of log functions into complex expression without breaking their flow.
// Logs message 'The system is broken', returns 'system' as its output
log.warn('The {} is broken', 'system')
// Logs message 'The system is broken due to a bug', returns ['system', 'bug'] as its output
log.warn('The {} is broken due to a {}', [ 'system', 'bug' ])
log.debug
log.debug(format, any) → any
log.debug(format, list) → list
Records formatted message in system logs at DEBUG level.
The arguments are formatted into a log message.
Curly brackets ({}) act as placeholder for arguments in the message.
The function returns its argument as its output, which enables placing of log functions into complex expression without breaking their flow.
// Logs message 'The system is broken', returns 'system' as its output
log.debug('The {} is broken', 'system')
// Logs message 'The system is broken due to a bug', returns ['system', 'bug'] as its output
log.debug('The {} is broken due to a {}', [ 'system', 'bug' ])
log.trace
log.trace(format, any) → any
log.trace(format, list) → list
Records formatted message in system logs at TRACE level.
The arguments are formatted into a log message.
Curly brackets ({}) act as placeholder for arguments in the message.
The function returns its argument as its output, which enables placing of log functions into complex expression without breaking their flow.
// Logs message 'The system is broken', returns 'system' as its output
log.trace('The {} is broken', 'system')
// Logs message 'The system is broken due to a bug', returns ['system', 'bug'] as its output
log.trace('The {} is broken due to a {}', [ 'system', 'bug' ])
LDAP Extension
Prefix: ldap.
LDAP extension is used to process LDAP-specific data formats, such distinguished name (DN).
ldap.composeDn
ldap.composeDn(list) → string
Creates a valid LDAP distinguished name from the wide range of components. The resulting DN is well-formatted and properly escaped. Note: the DN is not normalized. The case of the attribute names and white spaces are preserved.
// Returns 'cn=foo,ou=baz,o=bar'
ldap.composeDn(['cn','foo','ou','baz','o','bar'])
ldap.composeDnWithSuffix
ldap.composeDnWithSuffix(list) → string
Creates a valid LDAP distinguished name from the wide range of components, including a DN suffix at the end. The resulting DN is well-formatted and properly escaped. Note: the DN is not normalized. The case of the attribute names and white spaces are preserved.
// Returns 'cn=foo,ou=baz,dc=example,dc=com'
ldap.composeDnWithSuffix(['cn','foo','ou','baz','dc=example,dc=com'])
ldap.hashPassword
ldap.hashPassword(clearString, algorithm) → string
ldap.hashPassword(clearBytes, algorithm) → string
Hashes cleartext password in an (unofficial) LDAP password format. The cleartext password can be provided as a string, polystring, or as bytes. Supported algorithms: SSHA, SHA and MD5.
// Returns '{SSHA}rxNYgQODi95h2bsjYXuBqvYz+I1gjgMkF9f0tA=='
ldap.hashPassword('password','SSHA')
ldap.determineSingleAttributeValue
ldap.determineSingleAttributeValue(dn, attributeName, list(values)) → string
Selects single value from many LDAP values, based on object DN.
E.g. value bar is selected from list of values ['foo','bar','baz'] because that value is present in DN uid=bar,o=example.
// Returns 'bar'
ldap.determineSingleAttributeValue('uid=bar,o=example','uid', ['foo','bar','baz'])
Secret Extension
Prefix: secret.
Secret extension is used to interact with secret providers.
secret.resolveBinary
secret.resolveBinary(provider, key) → string
Resolves a secret specified by the key, using a provider specified by its name. Returns the secret in binary form (bytes).
secret.resolveBinary('prov1', 'midpoint-ldap-admin')
secret.resolveString
secret.resolveString(provider, key) → string
Resolves a secret specified by the key, using a provider specified by its name. Returns the secret in string form.
secret.resolveString('prov1', 'midpoint-ldap-admin')
secret.resolveProtectedString
secret.resolveProtectedString(provider, key) → string
Resolves a secret specified by the key, using a provider specified by its name. Returns the secret in protected string form for additional protection of the secret.
secret.resolveProtectedString('prov1', 'midpoint-ldap-admin')
Object Extension
Object extensions provide convenience functions that allow easier processing of midPoint objects, such as resources or shadows.
Functions in this language extension does not have a prefix. They are invoked as a member functions on appropriate value or variable containing midPoint object.
find
object.find(path) → item
Returns an item to which the specified item path refers.
// Returns a value of administrativeStatus property
focus.find('activation/administrativeStatus')
isEffectivelyEnabled
object.isEffectivelyEnabled() → bool
Returns true if the object is effectively enabled.
If the object is not a FocusType, the method returns true (as there is no activation there).
For FocusType objects, it assumes that the object underwent standard computation and
activation/effectiveStatus is set.
If the activation/effectiveStatus is not present, the return value of the method is undefined.
focus.isEffectivelyEnabled()
connectorConfiguration
resource.connectorConfiguration(propertyName) → list
Returns list of values of a resource connector configuration property specified by the argument. The function is invoked as a member method on a variable containing midPoint resource.
Following expression returns a list containing values that are configured in a connector configuration property host in resource that is stored in the resource variable.
resource.connectorConfiguration('host')
As most connector configuration properties as single-valued, this function is often combined with single() function:
// Returns configured hostname as string, e.g. 'localhost'
single(resource.connectorConfiguration('host'))
primaryIdentifiers
shadow.primaryIdentifiers() → list
Returns list of values of shadow primary identifier. The function is invoked as a member method on a variable containing a shadow.
// Returns list of values, e.g. ['381870aa-0731-11f1-b8d0-1baca68e8763']
shadow.primaryIdentifiers()
As most identifiers as single-valued, this function is often combined with single() function:
// Returns string value, e.g. '381870aa-0731-11f1-b8d0-1baca68e8763'
single(shadow.primaryIdentifiers())
secondaryIdentifiers
shadow.secondaryIdentifiers() → list
Returns list of values of shadow secondary identifier. The function is invoked as a member method on a variable containing a shadow.
// Returns list of values, e.g. ['uid=foo,ou=People,dc=example,dc=com']
shadow.secondaryIdentifiers()
As most identifiers as single-valued, this function is often combined with single() function:
// Returns string value, e.g. 'uid=foo,ou=People,dc=example,dc=com'
single(shadow.secondaryIdentifiers())
Timestamps and Durations
Timestamps can be constructed by using the timestamp() function.
The timestamp() constructor expect a string argument, specifying a timestamp formatted according to RFC 3339/ISO8601 format.
timestamp('2026-03-15T12:34:56Z')
Similarly, there is a duration() constructor:
duration('1h30m')
duration('-300ms')
The duration string used by CEL is not following any standardized format.
It is built on common conventions, where characters h, m, s, ms, us and ns specifying units of time.
CEL timestamps follow protocol buffer convention, which means that the timestamps are fixed to UTC (Z) timezone.
Functions that work with timestamps in a timezone-specific manner (e.g. atStartOfDay()) assume system timezone as default, allowing optional explicit specification of a time zone.
Timezone specification follows IANA Time Zone Database (a.k.a Olson or tz database).
Timestamps and durations can be used in some arithmetic and comparison operations:
timestamp('2026-03-15T12:34:56Z') + duration('1h30m')
timestamp('2026-03-15T12:34:56Z') < timestamp('2026-03-15T14:30:00Z')
Timestamp referring to the "current" time instant is available in variable now.
The value of this variable refers to an approximate moment of the start of expression evaluation.
Value of this variable is fixed through the evaluation of the expression.
atStartOfDay
timestamp.atStartOfDay() → timestamp
timestamp.atStartOfDay(timezone) → timestamp
Modifies the timestamp to refer to the start of the day. Resulting timestamp refers to the same day as the argument timestamp, however the time is reset to the earliest time at that day.
Optional time zone can be specified. If no timezone is specified, system default timezone is assumed.
Following example assume that system default time zone is UTC.
timestamp('2026-02-11T12:34:56Z').atStartOfDay() // Returns timestamp 2026-02-11T00:00:00Z
timestamp('2026-02-11T12:34:56Z').atStartOfDay('Z') // Returns timestamp 2026-02-11T00:00:00Z
atEndOfDay
timestamp.atEndOfDay() → timestamp
timestamp.atEndOfDay(timezone) → timestamp
Modifies the timestamp to refer to the end of the day. Resulting timestamp refers to the same day as the argument timestamp, however the time is set to the latest time at that day.
Optional time zone can be specified. If no timezone is specified, system default timezone is assumed.
Following example assume that system default time zone is UTC.
timestamp('2026-02-11T12:34:56Z').atEndOfDay() // Returns timestamp 2026-02-11T23:59:59Z
timestamp('2026-02-11T12:34:56Z').atEndOfDay('Z') // Returns timestamp 2026-02-11T23:59:59Z
getDate
timestamp.getDate() → int
timestamp.getDate(timezone) → int
Returns the day of the month (starting with 1) from a timestamp.
Optional time zone can be specified. If no timezone is specified, UTC is assumed (because CEL stores timestamps in UTC).
timestamp('2026-02-11T12:34:56Z').getDate() // Returns 11
timestamp('2026-02-11T12:34:56Z').getDate('Z') // Returns 11
getDayOfMonth
timestamp.getDayOfMonth() → int
timestamp.getDayOfMonth(timezone) → int
Returns the day of the month (starting with 0) from a timestamp.
Optional time zone can be specified. If no timezone is specified, UTC is assumed (because CEL stores timestamps in UTC).
timestamp('2026-02-11T12:34:56Z').getDayOfMonth() // Returns 10
timestamp('2026-02-11T12:34:56Z').getDayOfMonth('Z') // Returns 10
getDayOfWeek
timestamp.getDayOfWeek() → int
timestamp.getDayOfWeek(timezone) → int
Returns the day of the week (starting with 0 for Sunday) from a timestamp.
Optional time zone can be specified. If no timezone is specified, UTC is assumed (because CEL stores timestamps in UTC).
timestamp('2026-02-11T12:34:56Z').getDayOfWeek() // Returns 3
timestamp('2026-02-11T12:34:56Z').getDayOfWeek('Z') // Returns 3
getDayOfYear
timestamp.getDayOfYear() → int
timestamp.getDayOfYear(timezone) → int
Returns the day of the year (starting with 0) from a timestamp.
Optional time zone can be specified. If no timezone is specified, UTC is assumed (because CEL stores timestamps in UTC).
timestamp('2026-02-11T12:34:56Z').getDayOfYear() // Returns 40
timestamp('2026-02-11T12:34:56Z').getDayOfYear('Z') // Returns 40
getFullYear
timestamp.getFullYear() → int
timestamp.getFullYear(timezone) → int
Returns the year from a timestamp.
Optional time zone can be specified. If no timezone is specified, UTC is assumed (because CEL stores timestamps in UTC).
timestamp('2026-02-11T12:34:56Z').getFullYear() // Returns 2026
timestamp('2026-02-11T12:34:56Z').getFullYear('Z') // Returns 2026
getHours
timestamp.getHours() → int
timestamp.getHours(timezone) → int
duration.getHours() → int
Returns hours from a timestamp or converts a duration to hours.
Optional time zone can be specified. If no timezone is specified, UTC is assumed (because CEL stores timestamps in UTC).
timestamp('2026-02-11T12:34:56Z').getHours() // Returns 12
timestamp('2026-02-11T12:34:56Z').getHours('Z') // Returns 12
duration('2h').getHours() // Returns 2
getMilliseconds
timestamp.getMilliseconds() → int
timestamp.getMilliseconds(timezone) → int
duration.getMilliseconds() → int
Returns milliseconds from a timestamp or duration.
Optional time zone can be specified. If no timezone is specified, UTC is assumed (because CEL stores timestamps in UTC).
timestamp('2026-02-11T12:34:56.789Z').getMilliseconds() // Returns 789
timestamp('2026-02-11T12:34:56.789Z').getMilliseconds('Z') // Returns 789
duration('1.234s').getMilliseconds() // Returns 234
getMinutes
timestamp.getMinutes() → int
timestamp.getMinutes(timezone) → int
duration.getMinutes() → int
Returns minutes from a timestamp or converts a duration to minutes.
Optional time zone can be specified. If no timezone is specified, UTC is assumed (because CEL stores timestamps in UTC).
timestamp('2026-02-11T12:34:56Z').getMinutes() // Returns 34
timestamp('2026-02-11T12:34:56Z').getMinutes('Z') // Returns 34
duration('2h').getMinutes() // Returns 120
getMonth
timestamp.getMonth() → int
timestamp.getMonth(timezone) → int
Returns the month (starting with 0 for January) from a timestamp.
Optional time zone can be specified. If no timezone is specified, UTC is assumed (because CEL stores timestamps in UTC).
timestamp('2026-02-11T12:34:56Z').getMonth() // Returns 1
timestamp('2026-02-11T12:34:56Z').getMonth('Z') // Returns 1
getSeconds
timestamp.getSeconds() → int
timestamp.getSeconds(timezone) → int
duration.getSeconds() → int
Returns seconds from a timestamp or converts a duration to seconds.
Optional time zone can be specified. If no timezone is specified, UTC is assumed (because CEL stores timestamps in UTC).
timestamp('2026-02-11T12:34:56Z').getSeconds() // Returns 56
timestamp('2026-02-11T12:34:56Z').getSeconds('Z') // Returns 56
duration('2m').getSeconds() // Returns 120
timestamp.longAgo
timestamp.longAgo() → timestamp
Returns timestamp that is referring to a time long, long ago, too far in the past. This timestamp can be used to make sure that no practical timestamp in the system is earlier that this moment in time.
Current implementation returns the start of the epoch used by the CEL implementation, which is 1st January 1970.
However, as different time schemes are based on different mechanisms and epochs, the value returned by timestamp.longAgo() may change in the future, to be sure that the returned timestamp is always earlier than all other practical timestamps in the system.
timestamp.longAgo() // Returns timestamp 1970-01-01T00:00:00Z
Polystrings
Polystrings are midPoint data structured used to store string values, together with their normalized and localized variants.
Polystrings are represented as simple structured objects in MEL. Individual parts of polystring can be accessed by using the usual dot notation:
polystring.orig
polystring.norm
In MEL, polystrings behave just like strings. E.g. polystrings can be easily compared or concatenated with strings:
polystring == 'foo'
'Hello ' + polystring
In such cases, the orig part of the polystring is used in these string-like operations.
If the normalized part of polystring is to be used, it must be explicitly referenced:
'X' + polystring.norm
| The localization functionality of polystrings is not available in MEL at this time. |
Protected Strings
Protected strings are midPoint data structure meant to provide extra protection to string data. They are usually used to contain passwords, API keys and similar sensitive data.
Unlike polystrings, protected strings do not behave like strings in MEL. This is an intentional behavior to provide additional protection against unintentional leakage of the data, e.g. in logfiles. We do not want string functions (such as substring) to unintentionally reveal content of a protected string. If you need to do string operations on a protected string, you have to explicitly decrypt it first.
protectedString.decrypt()
Ordinary string value can be encrypted, to gain additional protection of a protected string:
string.endecrypt()
decrypt
protectedString.decrypt() → string
Decrypts value of protected string, returning cleartext value as string.
encrypt
string.encrypt() → protectedString
Encrypts a string value, creating protected string. Works with strings as well as polystrings.
QNames
MEL can work with QNames as simple structured objects.
QNames can be constructed using qname() function:
qname(namespace, localPart)
qname(localPart)
Parts of a qname can be accessed using the dot notation:
x.namespaceURI
x.localPart
QNames can be used instead of strings to access structured midPoint data when using index operator ([]):
focus.extension[qname('http://example.com/ns', 'ship')]
qname
qname(namespaceURI, localPart) → qname
qname(localPart) → qname
Creates a qname value.
encrypt
string.encrypt() → protectedString
Encrypts a string value, creating protected string. Works with strings as well as polystrings.
Structured Data
Object structure can be accessed in mel using the selection operator (.) (a.k.a. dot convention).
Following expression returns value of name property of the object stored in focus variable.
focus.name
Access to structured data can be nested.
Following code returns value of administrativeStatus property located inside activation container, located in user object stored in variable focus.
focus.activation.administrativeStatus
Alternatively, index operator ([]) can be used to access structured data:
focus['name']
Structure of the data is dictated by midPoint schema.
Access to extension items of objects is facilitated by an extension container.
E.g. extension attribute ship of the object stored in the focus variable can be accessed as follows:
focus.extension.ship
focus.extension['ship']
When working with midPoint (prism) objects, both strings and qnames can be used as indexes.
The has() macro can be used for tests regarding presence of items in structured objects and maps:
has(focus.fullName)
However, the has() macro is not null-safe.
E.g. the expression above fails in case the focus variable is null.
It is recommended to use isNull() and isPresent() functions instead (see below).
Optional Values in Structured Data
CEL interpreter that evaluates MEL expressions assumes that the data structures accessed by the expression exist when the expression is evaluated.
E.g. the following expression assumes that variable focus contains structured data value, which has field activation, which has field administrativeStatus.
The expression fails if any of that assumptions is false, e.g. if variable focus does not contain a value (is null).
focus.activation.administrativeStatus
However, the data are seldom complete.
In usual case, the data are only partially populated.
E.g. a focus may not have activation at all, and objects with administrativeStatus explicitly defined are quite rare.
MEL allows specification of optional selection by adding question mark (?) after the dot when working with incomplete data.
focus.?activation.?administrativeStatus
The expression above does not fail when variable focus does not contain any value, or in case that there is no activation container or administrativeStatus property.
In such cases the expression evaluates to an empty optional, which is supposed to be equivalent to null.
MidPoint interprets the value correctly when it is a result value of an expression.
Most of MEL functions can interpret the value correctly as well.
However, interpretation of some built-in CEL operators (e.g. == and +) can be confusing.
Therefore, it is recommended to use functions isPresent() and isNull() for checks, and format() for string formatting when dealing with optional values.
E.g. the isPresent() function can be used to determine whether a particular item is present in data:
isPresent(focus.?activation.?administrativeStatus)
See Null and Optional Values section below.
Item QNames
QNames can be used instead of strings to access the data when using index operator ([]):
focus.extension[qname('http://example.com/ns', 'ship')]
Explicit use of QNames is usually not necessary. However, it may be required in some cases, e.g. when several schema extensions are using items with the same local name.
Item Path
Item path can be used to locate an item in an object by using the find() function:
focus.find('activation/administrativeStatus')
Null and Optional Values
MEL expressions often need to deal with null and optional values, values which may or may not be present.
This applies to script variables, which may have value in some cases, and may not have value (null) in others.
Then there are structured data (midPoint/prism objects), which have many optional items.
CEL language provides some mechanisms to deal with such optional values. Unfortunately, there are two mechanisms to represent value which is not present: null values and optionals. These mechanisms are not perfectly aligned, which may cause a lot of confusion.
When it comes to structured types, CEL assumes that all the items in a structured data referenced by an expression exist, and are present in the data.
E.g. following expression fails in case that the object in the focus variable does not have activation container, that container does not have administrativeStatus property, or even if focus variable has null value.
focus.activation.administrativeStatus
The method used in expression above is not recommended in MEL expressions.
Such expressions are reliable only if you are absolutely sure that all the referenced items are present in the data, which usually means that the expressions have explicitly checked their existence before.
In general case, the use of .? operator is recommended instead:
focus.?activation.?administrativeStatus
The .? operator is using optionals, which means it does not fail even if there is no activation container, that container does not have administrativeStatus property, or even if focus variable is null.
In all such cases the expression evaluates to empty optional, which is supposed to have the same meaning as null value.
However, it is not completely equivalent to null, as empty optional is formally a non-null value.
This means that CEL/MEL has two mechanisms for representing values that may or may not be present.
Therefore, simple equality check for null is not reliable:
foo == null
This equality operator checks whether the value is null.
As optional are always non-null, the expression above returns non-intuitive result (false) for empty optionals.
The expression above is likely to work correctly when used with variables which contain primitive values, but it will not work with optionals.
This can be counter-intuitive when dealing with structured data:
focus.?activation.?administrativeStatus == null
The expression above never evaluates to false, as optionals are always non-null values, even if the item that they reference does not exist.
As a rule of thumb, it is recommended to always avoid use of null equality check (foo == null).
MEL provides a special purpose functions isNull() and isPresent() which can be reliably used in all the cases.
All the expressions below will be evaluated correctly, providing results that the user would intuitively expect.
isNull(foo)
isNull(focus.?activation.?validFrom)
isPresent(focus.?activation.?administrativeStatus)
There is a has() macro in CEL, which can be used to check for presence of a particular element in structured data.
However, the has() macro has limitations, which makes it difficult to use in some cases.
The use of isNull() and isPresent() functions is recommended instead of use of has() macro, especially for new users.
|
Moreover, there is a default() function which is very handy, and it reliably detects present or missing value in all the cases:
default(focus.?lifecycleState, 'active')
The expression above will always return a valid string value, even in case that focus variable is null, or that the object present in focus variable does not have lifecycleState property.
While MEL provides functions that work reliably (isNull(), isPresent()) and even some convenience functions (default()) which make it easy to work with null and optional values, there is still one drawback given by the nature of CEL.
While the .? operator works reliably with structured data, it cannot be used for function invocation.
E.g. the following expression is invalid:
foo.?contains('#')
Assuming that foo is a string variable, which may either contain a string or null, the expression above is specifying execution of contains() function on that variable.
However, the use of .? operator in this way is illegal in CEL, and currently there is no practical way for MEL extensions to implement this behavior.
Therefore, the only practical option in this case is to explicitly check for null value using a conditional operator (?:):
isPresent(foo) ? foo.contains('#') : null
When working with strings, stringify() function can be very handy.
As stringify() never returns null, any string function can be safely invoked on stringified value:
stringify(foo).contains('#')
Such use of stringify() can be very handy in case of string formatting as well.
If your intent is to return empty string instead of null
Custom Function Libraries
Functions from custom expression function libraries can be easily invoked from MEL.
The function can be invoked by combining name of the library with name of the function.
Following expression is invoking function determineOrg() from function library mylib, using foo as arguments.
mylib.determineOrg(foo)
The expression above is using simplified notation, which works for custom function that has at most one argument. As arguments of functions in custom function libraries are referencing the parameters by name rather than their position, functions that have more than one argument need to use map to specify parameter values:
mylib.generateIdentifier({ 'prefix': 'X', 'length': 10 })
The expression above invokes custom function generateIdentifier() with two parameters.
Parameter prefix with value X and parameter length with value 10.
| Functional expression languages such as MEL/CEL are based on an assumption that functions as well as other operations are free of any side effects. I.e. the functions are supposed to be idempotent: no matter how many times they are executed, they produce the same result. This feature is important for consistency of expression evaluation, as the sequence of evaluation of individual functions or other operations is determined by CEL runtime. It is important to keep that in mind when creating custom function libraries that are meant to be invoked by MEL expressions. As a rule of thumb, the library function should not make any modifications or change the state of midPoint repository. |
Miscellaneous Notes
-
MEL expression can span multiple lines, which provides opportunity for indentation to increase readability.
-
Two slashes (
//) can be used to add comments in expressions. The slashes start a comment, which ends at the end of line. -
Deprecated variables such as
user,accountandshadoware not available in MEL. Use variables such asfocusandprojectioninstead.
Expression Profiles
Expression profiles are applied to MEL expressions in the same way as they are applied to any other expression in midPoint.
E.g. the following expression profile configuration can be used to deny access to all scripting expression evaluators except for MEL, applying script-mel-restricted permission profile to MEL expressions:
<systemConfiguration>
...
<expressions>
<expressionProfile>
...
<evaluator>
<type>script</type>
<decision>deny</decision>
<script>
<language>http://midpoint.evolveum.com/xml/ns/public/expression/language#mel</language>
<decision>allow</decision>
<typeChecking>true</typeChecking>
<permissionProfile>script-mel-restricted</permissionProfile>
</script>
</evaluator>
...
</expressionProfile>
...
</expressions>
...
</systemConfiguration>
MEL expression language is considered to be secure in such a way that no MEL expression can compromise the platform on which it is running.
However, this does not mean that all the functionality available to MEL expressions is harmless.
Some MEL language extensions and custom function libraries may reveal sensitive information.
E.g. function secret.resolveString() has access to secret providers, which include credential vaults.
Functions in the midpoint.* language extension have read access to midPoint repository, which may reveal sensitive information.
Therefore, common recommendation is to use permission profile to limit access to MEL language extensions that are not strictly necessary.
The package clause of a permission profile can be used to control access to individual MEL language extensions, using their name (prefix).
Following configuration allows access to all MEL extensions, except for midpoint and secret extensions.
<systemConfiguration>
...
<expressions>
...
<permissionProfile>
<identifier>script-mel-restricted</identifier>
<decision>allow</decision>
<package>
<name>midpoint</name>
<description>midpoint.* MEL language extension</description>
<decision>deny</decision>
</package>
<package>
<name>secret</name>
<description>secret.* MEL language extension</description>
<decision>deny</decision>
</package>
</permissionProfile>
...
</expressions>
...
</systemConfiguration>
MEL and CEL
MidPoint Expression Language (MEL) is based on Common Expression Language, extended with midPoint-specific features, functions and libraries. MEL is using basic CEL syntax, operators, basic functions and even some extensions (e.g. regexp). However, as CEL is oriented towards the protocol buffers world, MEL provides access to midPoint objects that are based on Prism. Moreover, MEL is extending the basic CEL environment with midPoint data types such as polystring and protected string as well as variety of functions that adapt the language for midPoint environment.
Simple expressions in MEL and CEL are going to be the same. However, MEL is expanding on CEL, especially with convenience functions and much better support for nullable values, MEL expressions have a very distinct "midPointish" look and feel.
MEL and Java/Groovy
MEL is a secure expression language with a distinct functional character. This makes MEL very unlike Java and Groovy. MEL is not suitable for implementing complex algorithms. This is not a purpose of MEL. MEL is meant to create mappings and customizations, including quite complex customization. However, the way MEL is doing that is very different from the imperative Java/Groovy world, and it will require some time for adjustments. See MEL Migration Guide for more information.
MEL is meant to create expressions that are secure, which means that MEL needs to be limited by design. Java classes and libraries are intentionally not exposed to MEL expressions. MEL expressions are living in their own safe little world.