Saltearse al contenido

Extensiones: funciones y formateadores

Mondrian ofrece a los autores de schema varios puntos de extensión para adaptar cómo funcionan las funciones MDX, cómo se muestran los valores de celda y cómo aparecen las leyendas de miembros en distintos idiomas. Esta página cubre los puntos de extensión que probablemente use en el día a día.

Funciones definidas por el usuario

Una función definida por el usuario (UDF) le permite llamar a una función Java (o con script) personalizada desde cualquier expresión MDX. Mondrian la llama durante la evaluación de la consulta exactamente como llamaría a una función incorporada.

Escribir una UDF en Java

Su clase debe tener un constructor público sin argumentos e implementar la interfaz mondrian.spi.UserDefinedFunction. El siguiente ejemplo añade uno a su argumento numérico:

package com.example;
import mondrian.olap.*;
import mondrian.olap.type.*;
import mondrian.spi.UserDefinedFunction;
/**
* A simple user-defined function which adds one to its argument.
*/
public class PlusOneUdf implements UserDefinedFunction {
// public constructor
public PlusOneUdf() {
}
public String getName() {
return "PlusOne";
}
public String getDescription() {
return "Returns its argument plus one";
}
public Syntax getSyntax() {
return Syntax.Function;
}
public Type getReturnType(Type[] parameterTypes) {
return new NumericType();
}
public Type[] getParameterTypes() {
return new Type[] { new NumericType() };
}
public Object execute(Evaluator evaluator, Exp[] arguments) {
final Object argValue = arguments[0].evaluateScalar(evaluator);
if (argValue instanceof Number) {
return new Double(((Number) argValue).doubleValue() + 1);
} else {
// Argument might be a RuntimeException indicating that
// the cache does not yet have the required cell value.
// The function will be called again when the cache is loaded.
return null;
}
}
public String[] getReservedWords() {
return null;
}
}

Registrar una UDF en su schema

Declare la UDF dentro de su elemento <Schema>, tras sus cubos:

<Schema ...>
...
<UserDefinedFunction name="PlusOne" className="com.example.PlusOneUdf"/>
</Schema>

Usar la UDF en MDX

Una vez registrada, llame a la función desde cualquier sentencia MDX:

WITH MEMBER [Measures].[Unit Sales Plus One]
AS 'PlusOne([Measures].[Unit Sales])'
SELECT
{[Measures].[Unit Sales]} ON COLUMNS,
{[Gender].MEMBERS} ON ROWS
FROM [Sales]

Compartir una clase entre varios nombres de función

Si proporciona a su clase un constructor público que acepte un único argumento String, Mondrian pasa el nombre de la función a ese constructor. Esto permite que una clase respalde varias UDFs registradas:

public class PlusOrMinusOneUdf implements UserDefinedFunction {
private final String name;
private final boolean isPlus;
public PlusOrMinusOneUdf(String name) {
this.name = name;
if (name.equals("PlusOne")) {
isPlus = true;
} else if (name.equals("MinusOne")) {
isPlus = false;
} else {
throw new IllegalArgumentException("Unexpected name " + name);
}
}
public String getName() { return name; }
// ... getDescription, getSyntax, getReturnType, getParameterTypes as before ...
public Object execute(Evaluator evaluator, Exp[] arguments) {
final Object argValue = arguments[0].evaluateScalar(evaluator);
if (argValue instanceof Number) {
if (isPlus) {
return new Double(((Number) argValue).doubleValue() + 1);
} else {
return new Double(((Number) argValue).doubleValue() - 1);
}
} else {
return null;
}
}
public String[] getReservedWords() { return null; }
}

Registre ambos nombres apuntando a la misma clase:

<Schema ...>
...
<UserDefinedFunction name="PlusOne" className="com.example.PlusOrMinusOneUdf"/>
<UserDefinedFunction name="MinusOne" className="com.example.PlusOrMinusOneUdf"/>
</Schema>

Autodescubrimiento mediante el mecanismo service-provider

Puede empaquetar sus implementaciones de UDF en un JAR e incluir un archivo en META-INF/services/mondrian.spi.UserDefinedFunction listando un nombre de clase completamente cualificado por línea. Mondrian las descubre automáticamente y las pone disponibles para cada schema cargado en esa JVM — sin declaración por schema requerida.

UDFs con script (JavaScript)

Para lógica simple que prefiere no compilar, incruste la implementación dentro de un hijo <Script> de <UserDefinedFunction>. Las siguientes funciones deben estar presentes en el script:

  • getParameterTypes()
  • getReturnType(parameterTypes)
  • execute(evaluator, arguments)

getName(), getDescription(), getReservedWords() y getSyntax() son opcionales y por defecto tienen valores razonables basados en el atributo name.

Aquí hay una función factorial escrita en JavaScript:

<UserDefinedFunction name="Factorial">
<Script language="JavaScript">
function getParameterTypes() {
return new Array(new mondrian.olap.type.NumericType());
}
function getReturnType(parameterTypes) {
return new mondrian.olap.type.NumericType();
}
function execute(evaluator, arguments) {
var n = arguments[0].evaluateScalar(evaluator);
return factorial(n);
}
function factorial(n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
</Script>
</UserDefinedFunction>

Formateador de celda

Un formateador de celda sobrescribe cómo Mondrian formatea el valor crudo de una celda de medida para mostrar. Controla lo que devuelve Cell.getFormattedValue(). Su clase debe implementar mondrian.spi.CellFormatter.

Adjuntar un formateador a una medida

<Measure name="Revenue">
<CellFormatter className="com.example.MyCellFormatter"/>
</Measure>

Formateador de celda con script

<Measure name="Revenue">
<CellFormatter>
<Script language="JavaScript">
var s = value.toString();
while (s.length() &lt; 20) {
s = "0" + s;
}
return s;
</Script>
</CellFormatter>
</Measure>

El script recibe una variable value correspondiente al valor crudo de la celda. El fragmento puede tener varias sentencias pero debe terminar con una sentencia return.

Formateador en un miembro calculado

<CellFormatter> también funciona en elementos <CalculatedMember>:

calculated_members:
- name: "Double Sales"
dimension: "Measures"
formula: "[Measures].[Unit Sales] * 2"
cell_formatter:
script:
body: "var s = value.toString();\nwhile (s.length() < 20) { s = \"0\" +\
\ s; }\nreturn s;"

Formateador mediante propiedad CELL_FORMATTER (MDX)

Para medidas calculadas definidas en la cláusula WITH MEMBER de una consulta MDX, defina la propiedad CELL_FORMATTER:

WITH MEMBER [Measures].[Foo]
AS '[Measures].[Unit Sales] * 2',
CELL_FORMATTER='com.example.MyCellFormatter'
SELECT {[Measures].[Unit Sales], [Measures].[Foo]} ON COLUMNS,
{[Store].Children} ON ROWS
FROM [Sales]

Para un formateador con script en línea en MDX, use CELL_FORMATTER_SCRIPT y CELL_FORMATTER_SCRIPT_LANGUAGE:

WITH MEMBER [Measures].[Foo]
AS '[Measures].[Unit Sales] * 2',
CELL_FORMATTER_SCRIPT_LANGUAGE='JavaScript',
CELL_FORMATTER_SCRIPT='var s = value.toString(); while (s.length() < 20) s = "0" + s; return s;'
SELECT {[Measures].[Unit Sales], [Measures].[Foo]} ON COLUMNS,
{[Store].Children} ON ROWS
FROM [Sales]

Formateador de miembro

Un formateador de miembro sobrescribe cómo Mondrian formatea la leyenda de un miembro. Controla lo que devuelve Member.getCaption(). Su clase debe implementar mondrian.spi.MemberFormatter.

Adjuntar un formateador a un nivel

<Level name="Store Name" column="store_name">
<MemberFormatter className="com.example.MyMemberFormatter"/>
</Level>

Formateador de miembro con script

<Level name="Store Name" column="store_name">
<MemberFormatter>
<Script language="JavaScript">
return member.getName().toUpperCase();
</Script>
</MemberFormatter>
</Level>

El script recibe una variable member — el Member que se está formateando. El fragmento debe terminar con una sentencia return.


Formateador de propiedad

Un formateador de propiedad sobrescribe cómo Mondrian formatea el valor de una propiedad de miembro. Controla lo que devuelve Property.getPropertyFormattedValue(). Su clase debe implementar mondrian.spi.PropertyFormatter.

Adjuntar un formateador a una propiedad

En Mondrian 4 el elemento <Property> vive dentro de una definición de <Attribute>:

<Attribute name="My Attribute" keyColumn="attributeColumn" uniqueMembers="true">
<Property name="My Property" column="propColumn">
<PropertyFormatter className="com.example.MyPropertyFormatter"/>
</Property>
</Attribute>

Formateador de propiedad con script

<Attribute name="Store Name" keyColumn="store_name">
<Property name="Store Type" column="store_type">
<PropertyFormatter>
<Script language="JavaScript">
return member.getName().toUpperCase();
</Script>
</PropertyFormatter>
</Property>
</Attribute>

El script recibe las variables member, propertyName y propertyValue. El fragmento debe terminar con una sentencia return.


Localizar su schema

Mondrian le permite exponer su schema en varios idiomas sin mantener archivos de schema separados. El enfoque combina dos cosas: valores de leyenda y descripción específicos por locale almacenados como elementos <Annotation>, y la clase LocalizingDynamicSchemaProcessor que reescribe el XML del schema al vuelo para cada locale.

Usar <Annotation> para metadatos específicos por locale

Cada objeto del schema (<Schema>, <Cube>, <Dimension>, <Hierarchy>, <Level>, <Measure>, named set, etc.) admite un bloque hijo <Annotations>. La convención de Mondrian es usar nombres de anotación cualificados con puntos para los metadatos de locale:

cubes:
Sales:
caption: "Sales"
annotations:
caption.de_DE: "Verkaufen"
caption.fr_FR: "Ventes"
description.fr_FR: "Cube des ventes"
description.de_AT: "Cube den Verkaufen"

Las herramientas cliente que entienden esta convención pueden entonces mostrar leyendas en el locale del usuario. El formato YAML de schema admite estas anotaciones directamente bajo la clave annotations de cualquier elemento — consulte la referencia de schema YAML para la sintaxis completa.

Sustitución mediante archivo de propiedades con LocalizingDynamicSchemaProcessor

Para sustitución de locale completa del lado del servidor, escriba su schema usando placeholders %{key} en los atributos caption, description, allMemberCaption y measuresCaption:

<Schema name="FoodMart" measuresCaption="%{foodmart.measures.caption}">
<Dimension name="Store" caption="%{foodmart.dimension.store.caption}"
description="%{foodmart.dimension.store.description}">
<Hierarchy hasAll="true"
allMemberName="All Stores"
allMemberCaption="%{foodmart.dimension.store.allmember.caption}"
primaryKey="store_id">
<Table name="store"/>
<Level name="Store Country" column="store_country" uniqueMembers="true"
caption="%{foodmart.dimension.store.country.caption}"
description="%{foodmart.dimension.store.country.description}"/>
<Level name="Store State" column="store_state" uniqueMembers="true"
caption="%{foodmart.dimension.store.state.caption}"/>
<Level name="Store City" column="store_city" uniqueMembers="false"
caption="%{foodmart.dimension.store.city.caption}"/>
</Hierarchy>
</Dimension>
<Cube name="Sales" caption="%{foodmart.cube.sales.caption}"
description="%{foodmart.cube.sales.description}">
...
<MeasureGroup table="sales_fact_1997">
<Measures>
<Measure name="Unit Sales" column="unit_sales"
caption="%{foodmart.cube.sales.measure.unitsales.caption}"
description="%{foodmart.cube.sales.measure.unitsales.description}"/>
</Measures>
</MeasureGroup>
</Cube>
</Schema>

Proporcione un archivo de propiedades por defecto (locale.properties) y uno por locale soportado (locale_fr.properties, locale_de.properties, etc.). Cada archivo mapea las claves de placeholder a cadenas traducidas:

# locale.properties — default (English)
foodmart.measures.caption=Measures
foodmart.dimension.store.caption=Store
foodmart.dimension.store.country.caption=Store Country
foodmart.dimension.store.state.caption=Store State
foodmart.dimension.store.city.caption=Store City
foodmart.dimension.store.allmember.caption=All Stores
foodmart.cube.sales.caption=Sales
foodmart.cube.sales.measure.unitsales.caption=Unit Sales
# locale_hu.properties — Hungarian
foodmart.measures.caption=Hungarian Measures
foodmart.dimension.store.caption=Áruház
foodmart.dimension.store.country.caption=Ország
foodmart.dimension.store.state.caption=Állam/Megye
foodmart.dimension.store.city.caption=Város
foodmart.dimension.store.allmember.caption=Minden Áruház
foodmart.cube.sales.caption=Forgalom
foodmart.cube.sales.measure.unitsales.caption=Eladott db

El LocalizingDynamicSchemaProcessor se habilita mediante la cadena de conexión de Mondrian. En Saiku Cloud esto lo configura su administrador a nivel del datasource. Si necesita sustitución de locale a nivel de schema, contacte con soporte.


Avanzado: puntos de extensión autoalojados

Los tres puntos de extensión siguientes aplican solo a despliegues de Mondrian autoalojados o incrustados. Requieren configuración a nivel de despliegue (cadenas de conexión, declaraciones de servlet en web.xml) y normalmente no se necesitan al usar Saiku Cloud.

  • Dynamic schema processor (mondrian.spi.DynamicSchemaProcessor): Intercepta la carga del schema para filtrar o generar el XML del schema en cada conexión ROLAP. Se usa para inyectar contenido específico de locale o tenant en tiempo de carga.
  • Data source change listener (mondrian.spi.DataSourceChangeListener): Se conecta a la ruta de invalidación de caché de Mondrian. Cuando se invoca, señala si los datos de dimensión o hechos subyacentes han cambiado, disparando un flush de caché.
  • Dynamic datasource XMLA servlet (mondrian.xmla.impl.DynamicDatasourceXmlaServlet): Un servlet XMLA alternativo que recarga datasources.xml en cada petición del cliente y limpia selectivamente las cachés de catálogo de las entradas cambiadas.

Si está ejecutando Mondrian incrustado y necesita implementar alguna de estas SPIs, consulte el Javadoc de Mondrian para mondrian.spi.


Adaptado de la guía del schema del proyecto Mondrian (EPL v1.0).