Extensions : fonctions et formatters
Mondrian offre aux auteurs de schémas plusieurs points d’extension pour adapter le fonctionnement des fonctions MDX, l’affichage des valeurs de cellules et l’apparence des captions de membres dans différentes langues. Cette page couvre les points d’extension que vous êtes le plus susceptible d’utiliser au quotidien.
Fonctions définies par l’utilisateur
Une fonction définie par l’utilisateur (User-Defined Function, UDF) vous permet d’appeler une fonction Java (ou scriptée) personnalisée depuis n’importe quelle expression MDX. Mondrian l’appelle pendant l’évaluation de la requête exactement comme il appellerait une fonction intégrée.
Écrire une UDF Java
Votre classe doit avoir un constructeur public sans argument et implémenter l’interface mondrian.spi.UserDefinedFunction. L’exemple suivant ajoute un à son argument numérique :
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; }}Enregistrer une UDF dans votre schéma
Déclarez l’UDF à l’intérieur de votre élément <Schema>, après vos cubes :
<Schema ...> ... <UserDefinedFunction name="PlusOne" className="com.example.PlusOneUdf"/></Schema>Utiliser l’UDF en MDX
Une fois enregistrée, appelez la fonction depuis n’importe quelle instruction MDX :
WITH MEMBER [Measures].[Unit Sales Plus One] AS 'PlusOne([Measures].[Unit Sales])'SELECT {[Measures].[Unit Sales]} ON COLUMNS, {[Gender].MEMBERS} ON ROWSFROM [Sales]Partager une classe entre plusieurs noms de fonctions
Si vous donnez à votre classe un constructeur public qui accepte un seul argument String, Mondrian passe le nom de la fonction à ce constructeur. Cela permet à une classe de servir plusieurs UDFs enregistrées :
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; }}Enregistrez les deux noms pointant vers la même classe :
<Schema ...> ... <UserDefinedFunction name="PlusOne" className="com.example.PlusOrMinusOneUdf"/> <UserDefinedFunction name="MinusOne" className="com.example.PlusOrMinusOneUdf"/></Schema>Découverte automatique via le mécanisme de service-provider
Vous pouvez packager vos implémentations d’UDF dans une JAR et inclure un fichier à META-INF/services/mondrian.spi.UserDefinedFunction listant un nom de classe complètement qualifié par ligne. Mondrian les découvre automatiquement et les rend disponibles à chaque schéma chargé dans cette JVM — sans déclaration par schéma.
UDFs scriptées (JavaScript)
Pour une logique simple que vous préférez ne pas compiler, embarquez l’implémentation dans un enfant <Script> de <UserDefinedFunction>. Les fonctions suivantes doivent être présentes dans le script :
getParameterTypes()getReturnType(parameterTypes)execute(evaluator, arguments)
getName(), getDescription(), getReservedWords() et getSyntax() sont optionnelles et prennent des valeurs par défaut sensées basées sur l’attribut name.
Voici une fonction factorielle écrite 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>Cell Formatter
Un cell formatter surcharge la façon dont Mondrian formate la valeur brute d’une cellule de mesure pour l’affichage. Il contrôle ce que retourne Cell.getFormattedValue(). Votre classe doit implémenter mondrian.spi.CellFormatter.
Attacher un formatter à une mesure
<Measure name="Revenue"> <CellFormatter className="com.example.MyCellFormatter"/></Measure>Cell formatter scripté
<Measure name="Revenue"> <CellFormatter> <Script language="JavaScript"> var s = value.toString(); while (s.length() < 20) { s = "0" + s; } return s; </Script> </CellFormatter></Measure>Le script reçoit une variable value correspondant à la valeur brute de la cellule. Le fragment peut avoir plusieurs instructions mais doit se terminer par une instruction return.
Formatter sur un membre calculé
<CellFormatter> fonctionne aussi sur les éléments <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 la propriété CELL_FORMATTER (MDX)
Pour les mesures calculées définies dans la clause WITH MEMBER d’une requête MDX, définissez la propriété 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]Pour un formatter scripté inline en MDX, utilisez CELL_FORMATTER_SCRIPT et 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
Un member formatter surcharge la façon dont Mondrian formate le caption d’un membre. Il contrôle ce que retourne Member.getCaption(). Votre classe doit implémenter mondrian.spi.MemberFormatter.
Attacher un formatter à un niveau
<Level name="Store Name" column="store_name"> <MemberFormatter className="com.example.MyMemberFormatter"/></Level>Member formatter scripté
<Level name="Store Name" column="store_name"> <MemberFormatter> <Script language="JavaScript"> return member.getName().toUpperCase(); </Script> </MemberFormatter></Level>Le script reçoit une variable member — le Member en cours de formatage. Le fragment doit se terminer par une instruction return.
Property Formatter
Un property formatter surcharge la façon dont Mondrian formate la valeur d’une propriété de membre. Il contrôle ce que retourne Property.getPropertyFormattedValue(). Votre classe doit implémenter mondrian.spi.PropertyFormatter.
Attacher un formatter à une propriété
Dans Mondrian 4, l’élément <Property> vit à l’intérieur d’une définition <Attribute> :
<Attribute name="My Attribute" keyColumn="attributeColumn" uniqueMembers="true"> <Property name="My Property" column="propColumn"> <PropertyFormatter className="com.example.MyPropertyFormatter"/> </Property></Attribute>Property formatter 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>Le script reçoit les variables member, propertyName et propertyValue. Le fragment doit se terminer par une instruction return.
Localiser votre schéma
Mondrian vous permet d’exposer votre schéma dans plusieurs langues sans maintenir de fichiers de schéma séparés. L’approche combine deux choses : des valeurs de caption et de description spécifiques à la locale stockées comme éléments <Annotation>, et la classe LocalizingDynamicSchemaProcessor qui réécrit le XML du schéma à la volée pour chaque locale.
Utiliser <Annotation> pour les métadonnées spécifiques à la locale
Chaque objet du schéma (<Schema>, <Cube>, <Dimension>, <Hierarchy>, <Level>, <Measure>, named sets, etc.) supporte un bloc enfant <Annotations>. La convention Mondrian est d’utiliser des noms d’annotations qualifiés par des points pour les métadonnées 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>Les outils clients qui comprennent cette convention peuvent alors afficher les captions dans la locale de l’utilisateur. Le format YAML supporte ces annotations directement sous la clé annotations de n’importe quel élément — voir la référence des schémas YAML pour la syntaxe complète.
Substitution par fichier de propriétés avec LocalizingDynamicSchemaProcessor
Pour une substitution de locale complète côté serveur, écrivez votre schéma en utilisant des placeholders %{key} dans les attributs caption, description, allMemberCaption et 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>Fournissez un fichier de propriétés par défaut (locale.properties) et un par locale supportée (locale_fr.properties, locale_de.properties, etc.). Chaque fichier mappe les clés de placeholder à des chaînes traduites :
# 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 dbLocalizingDynamicSchemaProcessor est activé via la chaîne de connexion Mondrian. Sur Saiku Cloud, cela est configuré au niveau de la source de données par votre administrateur. Si vous avez besoin d’une substitution de locale au niveau du schéma, contactez le support.
Avancé : points d’extension auto-hébergés
Les trois points d’extension ci-dessous s’appliquent uniquement aux déploiements Mondrian auto-hébergés ou embarqués. Ils nécessitent une configuration au niveau du déploiement (chaînes de connexion, déclarations de servlet web.xml) et ne sont généralement pas nécessaires lors de l’utilisation de Saiku Cloud.
- Dynamic schema processor (
mondrian.spi.DynamicSchemaProcessor) : Intercepte le chargement du schéma pour filtrer ou générer le XML du schéma à chaque connexion ROLAP. Utilisé pour injecter du contenu spécifique à la locale ou au tenant au moment du chargement. - Data source change listener (
mondrian.spi.DataSourceChangeListener) : Se branche sur le chemin d’invalidation du cache de Mondrian. Quand il est appelé, il signale si les données de dimension ou de fait sous-jacentes ont changé, déclenchant un flush du cache. - Dynamic datasource XMLA servlet (
mondrian.xmla.impl.DynamicDatasourceXmlaServlet) : Un servlet XMLA alternatif qui rechargedatasources.xmlà chaque requête client et vide sélectivement les caches de catalogue pour les entrées modifiées.
Si vous exécutez Mondrian embarqué et avez besoin d’implémenter l’une de ces SPIs, consultez le Javadoc Mondrian pour mondrian.spi.
Adapté du guide des schémas du projet Mondrian (EPL v1.0).