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 ROWSFROM [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() < 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;"<CalculatedMember name="Double Sales" dimension="Measures"> <Formula>[Measures].[Unit Sales] * 2</Formula> <CellFormatter> <Script language="JavaScript"> var s = value.toString(); while (s.length() < 20) { s = "0" + s; } return s; </Script> </CellFormatter></CalculatedMember>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 ROWSFROM [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 ROWSFROM [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"<Cube name="Sales" caption="Sales"> <Annotations> <Annotation name="caption.de_DE">Verkaufen</Annotation> <Annotation name="caption.fr_FR">Ventes</Annotation> <Annotation name="description.fr_FR">Cube des ventes</Annotation> <Annotation name="description.de_AT">Cube den Verkaufen</Annotation> </Annotations> ...</Cube>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=Measuresfoodmart.dimension.store.caption=Storefoodmart.dimension.store.country.caption=Store Countryfoodmart.dimension.store.state.caption=Store Statefoodmart.dimension.store.city.caption=Store Cityfoodmart.dimension.store.allmember.caption=All Storesfoodmart.cube.sales.caption=Salesfoodmart.cube.sales.measure.unitsales.caption=Unit Sales# locale_hu.properties — Hungarianfoodmart.measures.caption=Hungarian Measuresfoodmart.dimension.store.caption=Áruházfoodmart.dimension.store.country.caption=Országfoodmart.dimension.store.state.caption=Állam/Megyefoodmart.dimension.store.city.caption=Városfoodmart.dimension.store.allmember.caption=Minden Áruházfoodmart.cube.sales.caption=Forgalomfoodmart.cube.sales.measure.unitsales.caption=Eladott dbO 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 recarregadatasources.xmlem 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).