/**
* AnalyzerBeans
* Copyright (C) 2014 Neopost - Customer Information Management
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.eobjects.analyzer.result;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.metamodel.util.NumberComparator;
/**
* Abstract reducer class for {@link CrosstabResult}s that are two dimensional
* and the dimensions are the same on all slave results. This scenario is quite
* common since a lot of analyzers produce crosstabs with measures on one
* dimension and column names on another.
*/
public abstract class AbstractCrosstabResultReducer<R extends CrosstabResult> implements AnalyzerResultReducer<R> {
@Override
public R reduce(Collection<? extends R> results) {
final Crosstab<Serializable> masterCrosstab = createMasterCrosstab(results);
final Class<?> valueClass = masterCrosstab.getValueClass();
final CrosstabDimension dimension1 = masterCrosstab.getDimension(0);
final CrosstabDimension dimension2 = masterCrosstab.getDimension(1);
final CrosstabNavigator<Serializable> masterNav = masterCrosstab.navigate();
for (String category1 : dimension1) {
masterNav.where(dimension1.getName(), category1);
for (String category2 : dimension2) {
masterNav.where(dimension2.getName(), category2);
final String[] categories = new String[] { category1, category2 };
final List<ResultProducer> slaveResultProducers = new ArrayList<ResultProducer>();
final List<Object> slaveValues = new ArrayList<Object>(results.size());
for (R result : results) {
final Crosstab<?> slaveCrosstab = result.getCrosstab();
try {
final Object slaveValue = slaveCrosstab.getValue(categories);
slaveValues.add(slaveValue);
final ResultProducer resultProducer = slaveCrosstab.explore(categories);
if (resultProducer != null) {
slaveResultProducers.add(resultProducer);
}
} catch (IllegalArgumentException e) {
// ignore this value - it was not present in one of the
// slaves
}
}
final Serializable masterValue = reduceValues(slaveValues, category1, category2, results, valueClass);
masterNav.put(masterValue);
if (!slaveResultProducers.isEmpty()) {
final ResultProducer masterResultProducer = reduceResultProducers(slaveResultProducers, category1,
category2, valueClass, masterValue);
if (masterResultProducer != null) {
masterNav.attach(masterResultProducer);
}
}
}
}
return buildResult(masterCrosstab, results);
}
/**
* Builds the master crosstab, including all dimensions and categories that
* will be included in the final result.
*
* By default this method will use the first result's crosstab dimensions
* and categories, assuming that they are all the same.
*
* Subclasses can override this method to build the other dimensions.
*
* @param results
* @return
*/
protected Crosstab<Serializable> createMasterCrosstab(Collection<? extends R> results) {
final R firstResult = results.iterator().next();
final Crosstab<?> firstCrosstab = firstResult.getCrosstab();
final Class<?> valueClass = firstCrosstab.getValueClass();
final CrosstabDimension dimension1 = firstCrosstab.getDimension(0);
final CrosstabDimension dimension2 = firstCrosstab.getDimension(1);
@SuppressWarnings({ "unchecked", "rawtypes" })
final Crosstab<Serializable> masterCrosstab = new Crosstab(valueClass, dimension1, dimension2);
return masterCrosstab;
}
protected ResultProducer reduceResultProducers(List<ResultProducer> slaveResultProducers, String category1,
String category2, Class<?> valueClass, Serializable masterValue) {
for (ResultProducer resultProducer : slaveResultProducers) {
AnalyzerResult result = resultProducer.getResult();
if (result instanceof AnnotatedRowsResult) {
if (((AnnotatedRowsResult) result).getAnnotatedRowCount() > 0) {
// just return the first annotated rows result - these are
// anyways "just" samples
return resultProducer;
}
}
}
return null;
}
protected abstract Serializable reduceValues(List<Object> slaveValues, String category1, String category2,
Collection<? extends R> results, Class<?> valueClass);
protected abstract R buildResult(final Crosstab<?> crosstab, final Collection<? extends R> results);
/**
* Helper method to get a sum of values (values will be checked whether they
* are integers only, or else a double will be returned).
*
* @param slaveValues
* @return
*/
protected static Number sum(List<?> slaveValues) {
for (Object slaveValue : slaveValues) {
if (slaveValue != null) {
final Class<? extends Object> cls = slaveValue.getClass();
if (!(cls == Integer.class || cls == Short.class || cls == Byte.class)) {
return sumAsDouble(slaveValues);
}
}
}
return sumAsInteger(slaveValues);
}
/**
* Helper method to get a sum of values (sum will be calculated as an
* integer)
*
* @param slaveValues
* @return
*/
protected static Integer sumAsInteger(List<?> slaveValues) {
int sum = 0;
for (Object slaveValue : slaveValues) {
Number value = (Number) slaveValue;
if (value != null) {
sum += value.intValue();
}
}
return sum;
}
/**
* Helper method to get a sum of values (sum will be calculated as a double)
*
* @param slaveValues
* @return
*/
protected static Double sumAsDouble(List<?> slaveValues) {
double sum = 0;
for (Object slaveValue : slaveValues) {
Number value = (Number) slaveValue;
if (value != null) {
sum += value.doubleValue();
}
}
return sum;
}
/**
* Helper method to get the maximum of all values (must be numbers)
*
* @param slaveValues
* @return
*/
protected static Number maximum(List<?> slaveValues) {
Number max = null;
for (Object slaveValue : slaveValues) {
if (max == null) {
max = (Number) slaveValue;
} else {
Comparable<Object> comparable = NumberComparator.getComparable(max);
if (comparable.compareTo(slaveValue) < 0) {
max = (Number) slaveValue;
}
}
}
return max;
}
/**
* Helper method to get the minimum of all values (must be numbers)
*
* @param slaveValues
* @return
*/
protected static Number minimum(List<?> slaveValues) {
Number min = null;
for (Object slaveValue : slaveValues) {
if (min == null) {
min = (Number) slaveValue;
} else {
Comparable<Object> comparable = NumberComparator.getComparable(min);
if (comparable.compareTo(slaveValue) > 0) {
min = (Number) slaveValue;
}
}
}
return min;
}
}