Pular para o conteúdo

Extensões: funções e formatters

O Mondrian dá aos autores de schema vários pontos de extensão para personalizar como funções MDX funcionam, como valores de célula são exibidos e como captions de membro aparecem entre idiomas. Esta página cobre os pontos de extensão que você é mais provável a usar no dia a dia.

User-defined functions

Uma user-defined function (UDF) deixa você chamar uma função Java (ou scripted) customizada de qualquer expressão MDX. O Mondrian a chama durante a avaliação da consulta exatamente como chamaria uma função embutida.

Escrever uma UDF em Java

Sua classe deve ter um construtor público sem argumentos e implementar a interface mondrian.spi.UserDefinedFunction. O exemplo a seguir adiciona um ao seu 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 uma UDF no seu schema

Declare a UDF dentro do seu elemento <Schema>, após seus cubos:

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

Usar a UDF em MDX

Uma vez registrada, chame a função de qualquer statement 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]

Compartilhar uma classe entre múltiplos nomes de função

Se você der à sua classe um construtor público que aceita um único argumento String, o Mondrian passa o nome da função para esse construtor. Isso deixa uma classe sustentar múltiplas 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 os nomes apontando para a mesma classe:

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

Auto-descoberta via mecanismo service-provider

Você pode empacotar suas implementações de UDF em um JAR e incluir um arquivo em META-INF/services/mondrian.spi.UserDefinedFunction listando um nome de classe totalmente qualificado por linha. O Mondrian descobre esses automaticamente e os torna disponíveis para todo schema carregado naquela JVM — sem declaração por schema necessária.

UDFs scripted (JavaScript)

Para lógica simples que você prefere não compilar, embute a implementação dentro de um filho <Script> de <UserDefinedFunction>. As seguintes funções devem estar presentes no script:

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

getName(), getDescription(), getReservedWords() e getSyntax() são opcionais e default para valores sensatos com base no atributo name.

Aqui está uma função fatorial escrita em 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>

Cell formatter

Um cell formatter sobrescreve como o Mondrian formata o valor bruto de uma célula de medida para exibição. Controla o que Cell.getFormattedValue() retorna. Sua classe deve implementar mondrian.spi.CellFormatter.

Anexar um formatter a uma medida

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

Cell formatter scripted

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

O script recebe uma variável value correspondendo ao valor bruto da célula. O fragmento pode ter múltiplos statements mas deve terminar com um statement return.

Formatter em um calculated member

<CellFormatter> também funciona em 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;"

Formatter via property CELL_FORMATTER (MDX)

Para medidas calculadas definidas na cláusula WITH MEMBER de uma consulta MDX, defina a property 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 um formatter scripted inline em MDX, use CELL_FORMATTER_SCRIPT e 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]

Member formatter

Um member formatter sobrescreve como o Mondrian formata o caption de um membro. Controla o que Member.getCaption() retorna. Sua classe deve implementar mondrian.spi.MemberFormatter.

Anexar um formatter a um level

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

Member formatter scripted

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

O script recebe uma variável member — o Member sendo formatado. O fragmento deve terminar com um statement return.


Property formatter

Um property formatter sobrescreve como o Mondrian formata o valor de uma member property. Controla o que Property.getPropertyFormattedValue() retorna. Sua classe deve implementar mondrian.spi.PropertyFormatter.

Anexar um formatter a uma property

No Mondrian 4, o elemento <Property> vive dentro de uma definição <Attribute>:

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

Property formatter scripted

<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>

O script recebe variáveis member, propertyName e propertyValue. O fragmento deve terminar com um statement return.


Localizar seu schema

O Mondrian deixa você expor seu schema em múltiplos idiomas sem manter arquivos de schema separados. A abordagem combina duas coisas: valores específicos de locale para caption e descrição armazenados como elementos <Annotation>, e a classe LocalizingDynamicSchemaProcessor que reescreve o XML do schema na hora para cada locale.

Usar <Annotation> para metadados específicos de locale

Todo objeto de schema (<Schema>, <Cube>, <Dimension>, <Hierarchy>, <Level>, <Measure>, named set e assim por diante) suporta um bloco filho <Annotations>. A convenção do Mondrian é usar nomes de annotation qualificados com ponto para metadados 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"

Ferramentas cliente que entendem essa convenção podem então exibir captions no locale do usuário. O formato de schema YAML suporta essas annotations diretamente sob a chave annotations de qualquer elemento — veja Referência de schemas YAML para a sintaxe completa.

Substituição por arquivo de propriedades com LocalizingDynamicSchemaProcessor

Para substituição total de locale do lado do servidor, escreva seu schema usando placeholders %{key} em atributos caption, description, allMemberCaption e 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>

Forneça um arquivo de propriedades default (locale.properties) e um por locale suportado (locale_fr.properties, locale_de.properties e assim por diante). Cada arquivo mapeia as chaves de placeholder para strings traduzidas:

# 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

O LocalizingDynamicSchemaProcessor é habilitado pela connection string do Mondrian. No Saiku Cloud isso é configurado no nível do data-source pelo seu administrador. Se você precisa de substituição de locale no nível do schema, contate o suporte.


Avançado: pontos de extensão self-hosted

Os três pontos de extensão abaixo se aplicam apenas a implantações self-hosted ou embedded do Mondrian. Eles exigem configuração no nível da implantação (connection strings, declarações de servlet web.xml) e tipicamente não são necessários ao usar o Saiku Cloud.

  • Dynamic schema processor (mondrian.spi.DynamicSchemaProcessor): Intercepta a carga do schema para filtrar ou gerar o XML do schema em cada conexão ROLAP. Usado para injetar conteúdo específico de locale ou tenant no momento da carga.
  • Data source change listener (mondrian.spi.DataSourceChangeListener): Plugue no caminho de invalidação de cache do Mondrian. Quando chamado, sinaliza se dados subjacentes de dimensão ou fato mudaram, disparando um flush de cache.
  • Dynamic datasource XMLA servlet (mondrian.xmla.impl.DynamicDatasourceXmlaServlet): Um servlet XMLA alternativo que recarrega datasources.xml em cada requisição do cliente e seletivamente limpa caches de catálogo para entradas mudadas.

Se você está rodando Mondrian embedded e precisa implementar qualquer um desses SPIs, consulte o Javadoc do Mondrian para mondrian.spi.


Adaptado do guia de schema do projeto Mondrian (EPL v1.0).