Skip to main content

How to use Atoti Server Extension to create your own measures

In this guide, we show the steps required to take full advantage of Atoti Server Extension to define and use your own measures and post-processors.

1. Add atoti-server-extension-parent to your pom.xml

For a correct use of Atoti Server Extension, you need to add the atoti-server-extension-parent as a parent in the pom.xml file of your runtime extension project.

<parent>
<groupId>io.atoti</groupId>
<artifactId>atoti-server-extension-parent</artifactId>
</parent>

You can use 6.1.11-SNAPSHOT to specify the version of Atoti Server Extension you want to use.

2. Create a basic custom measure and register it

In this section, we show you how to create a custom measure and register it in the Atoti application. The steps are the same for every custom measure not requiring an underlying post-processor.

2.1 Create an argument class for your measure

Create a class that implements the empty interface io.atoti.runtime.api.measures.IMeasureDefinition, this class should contain the arguments needed to compute the measure and optionally, the measure's plugin key.
Arguments can be of any Java primitive type, or any types of identifier defined in Atoti (e.g. com.activeviam.activepivot.core.intf.api.cube.metadata.LevelIdentifier, com.activeviam.activepivot.core.intf.api.cube.metadata.HierarchyIdentifier, etc.).

/** Arguments for a measure increment its underlying measure. */
public record IncrementArguments(
String underlyingMeasureName, LevelIdentifier perLevel, long increment)
implements IMeasureDefinition {
public static final String PLUGIN_KEY = "MY_INCREMENT";
@Override
public String getPluginKey() {
return PLUGIN_KEY;
}
}

The arguments passed to the custom measures inside a IMeasureDefinition from Python are of the following types:

  • Any Java primitive type (e.g. int, double, boolean, etc.)
  • LevelIdentifier
  • HierarchyIdentifier
  • StoreField
  • Measure name as a String
  • Column name as a String
  • Dimension name as a String
  • Any array/tuple and map of the above types

2.2 Create your measure's registration class

Once an argument class is defined, you are now able to create a io.atoti.server.common.api.plugins.MeasureRegistration object specific to your measure, that will then be used to register the measure in the Atoti application.

private static MeasureRegistration<IncrementArguments> createIncrementRegistration() {
return MeasureRegistration.<IncrementArguments>builder()
.measureKey(IncrementArguments.PLUGIN_KEY)
.definitionClass(IncrementArguments.class)
.factory(
(definition, environment) ->
environment
.getMeasure(definition.underlyingMeasureName())
.getFormula()
.plus(Copper.constant(definition.increment()))
.per(Copper.level(definition.perLevel()))
.sum())
.build();
}

note

You can also specify a formatter when creating your custom measure formula by overriding IFormulaFactory.computeFormatter()

Example

/** Arguments class for multiply by two measure. */
public record MultiplyByTwoArguments(String underlyingMeasureName, LevelIdentifier perLevel)
implements IMeasureDefinition {
public static final String PLUGIN_KEY = "INCREMENT_BY_ONE";
@Override
public String getPluginKey() {
return PLUGIN_KEY;
}
}
private MeasureRegistration<MultiplyByTwoArguments> createMultiplyByTwoRegistration() {
return MeasureRegistration.<MultiplyByTwoArguments>builder()
.measureKey(MultiplyByTwoArguments.PLUGIN_KEY)
.definitionClass(MultiplyByTwoArguments.class)
.factory(
new IFormulaFactory<>() {
@Override
public CopperMeasure buildFormula(
final MultiplyByTwoArguments definition,
final IFormulaFactoryEnvironment environment) {
return environment
.getMeasure(definition.underlyingMeasureName())
.getFormula()
.multiply(Copper.constant(2))
.per(Copper.level(definition.perLevel()))
.sum();
}
@Override
public Optional<String> computeFormatter(
final MultiplyByTwoArguments definition, final IFieldType outputType) {
if (outputType.isPrimitive() && outputType.getContentType() == ContentType.DOUBLE) {
return Optional.of("DOUBLE[#,###.###]");
} else {
throw new IllegalStateException(
"Underlying value should be of type double, but got " + outputType);
}
}
})
.build();
}

2.3 Register your measure in the Atoti application

Once the measure registration class is ready, you can register your measure in the Atoti application using the io.atoti.server.common.api.plugins.IPluginSetup bean.
You can do this in a Spring configuration class by defining a bean that returns a io.atoti.server.common.api.plugins.PluginMeasureRegistrations object.

@Bean
@Lazy(false)
public PluginMeasureRegistrations pluginMeasureRegistrations(final IPluginSetup pluginInit) {
return pluginInit.contributeMeasures().addMeasure(createIncrementRegistration()).build();
}

note

If you need to register multiple measures, they can all be added to the PluginMeasureRegistrations object returned by the contributeMeasures() method of the IPluginSetup bean.

Example:

return pluginInit
.contributeMeasures()
.addMeasure(createAddOneRegistration())
.addMeasure(createEverythingRegistration())
.addMeasure(createEverythingMapRegistration())
.build();

Once this is done, the measure is available on the start of your application. To know how to use it with Atoti Python SDK, head to the Atoti Python SDK documentation.

3. Create a custom measure relying on a custom post-processor

In this section, we show you how to create a custom measure whose calculations rely on a custom post-processor.

3.1 Create a custom post-processor

You can create a custom post-processor by extending any class which already extends AAdvancedPostProcessor.
For example:

Creation of a basic post-processor

/** A post processor that adds one to the aggregated value. */
@AtotiExtendedPluginValue(intf = IPostProcessor.class, key = AddOnePostProcessor.PLUGIN_KEY)
public class AddOnePostProcessor extends ABasicPostProcessor {
public static final String PLUGIN_KEY = "ADD_ONE";
public AddOnePostProcessor(
final String name, final IPostProcessorCreationContext creationContext) {
super(name, creationContext);
}
@Override
public void init(final Properties properties) throws ActiveViamException {
properties.setProperty(OUTPUT_TYPE_PROPERTY, ILiteralType.DOUBLE);
super.init(properties);
}
@Override
public void evaluate(
final ILocation location,
final IRecordReader aggregatedMeasures,
final IWritableCell resultCell) {
resultCell.writeDouble(aggregatedMeasures.readDouble(0) + 1);
}
@Override
public String getType() {
return PLUGIN_KEY;
}
}

3.2 Create a custom measure relying on the created post-processor

Once the post-processor is created, you can create a measure that relies on it by using very similar code to the one used in the Basic Custom Measure section:

  • You need to create an argument class that implementing IMeasureDefinition.

/** Arguments */
public class AddOneAndMultiplyArguments implements IMeasureDefinition {
@Getter private final String underlyingMeasureName;
@Getter private final double multiplier;
public static final String PLUGIN_KEY = "ADD_ONE_AND_MULTIPLY";
/** Creates a new instance of the arguments. */
public AddOneAndMultiplyArguments(final String underlyingMeasureName, final double multiplier) {
this.underlyingMeasureName = underlyingMeasureName;
this.multiplier = multiplier;
}
@Override
public String getPluginKey() {
return PLUGIN_KEY;
}
}

  • You need to create a MeasureRegistration object specifying the new post-processor that will be used to register the measure in the Atoti application, for example:

private static MeasureRegistration<AddOneAndMultiplyArguments> createAddOneRegistration() {
return MeasureRegistration.<AddOneAndMultiplyArguments>builder()
.measureKey(AddOneAndMultiplyArguments.PLUGIN_KEY)
.definitionClass(AddOneAndMultiplyArguments.class)
.factory(
new IFormulaFactory<>() {
@Override
public CopperMeasure buildFormula(
final AddOneAndMultiplyArguments definition,
final IFormulaFactoryEnvironment environment) {
return Copper.newPostProcessor(AddOnePostProcessor.PLUGIN_KEY)
.withUnderlyingMeasures(
environment.getMeasure(definition.getUnderlyingMeasureName()).getFormula())
.withType(Types.TYPE_DOUBLE)
.multiply(Copper.constant(definition.getMultiplier()));
}
})
.build();
}

3.3 Register the post-processor

One the post-processor and the measure are created, you need to manually register the created post-processor in the Atoti application registry:

/** Registers the percentage post processor. */
public void registerAddOnePostProcessor() {
Registry.getExtendedPlugin(IPostProcessor.class)
.add(AddOnePostProcessor.PLUGIN_KEY, "Add one post processor", AddOnePostProcessor.class);
}

3.4 Register the measure

Finally, you can register the measure in the Atoti application using the IPluginSetup bean as shown in the Basic Custom Measure section. The post-processor must also be registered in the PluginMeasureRegistrations bean definition.

Once this is done, the measure will be available on the start of your application, to know how to use it with Atoti Python SDK, head to the Atoti Python SDK plugin_measure documentation.