package org.mapfish.print.processor.jasper;
import com.google.common.annotations.Beta;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.vividsolutions.jts.util.Assert;
import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.data.JRMapCollectionDataSource;
import net.sf.jasperreports.engine.design.JRDesignField;
import org.mapfish.print.config.Configuration;
import org.mapfish.print.config.ConfigurationException;
import org.mapfish.print.config.ConfigurationObject;
import org.mapfish.print.output.Values;
import org.mapfish.print.processor.AbstractProcessor;
import org.mapfish.print.processor.CustomDependencies;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* <p>This processor combines DataSources and individual processor outputs (or attribute values) into a
* single DataSource which can be used in a jasper report's detail section.</p>
* <p>
* An example use case is where we might have zero or many of tables and zero or many legends. You can
* configure the template with a detail section that contains a subreport, the name of which is a field in
* the DataSources and the DataSources for the sub-template another field. Then you can merge the legend
* and the tables into a single DataSources. This way the report will nicely expand depending on if you
* have a legend and how many tables you have in your report.
* </p>
* [[examples=merged_datasource]]
*/
@Beta
public final class MergeDataSourceProcessor
extends AbstractProcessor<MergeDataSourceProcessor.In, MergeDataSourceProcessor.Out>
implements CustomDependencies {
private List<Source> sources = Lists.newArrayList();
/**
* Constructor.
*/
protected MergeDataSourceProcessor() {
super(Out.class);
}
/**
* <p>The <em>source</em> to add to the merged DataSource.</p>
* <p>Each <em>source</em> indicates if it should be treated
* as a datasource or as a single item to add to the merged DataSource. If the source indicates that it
* is a {@link org.mapfish.print.processor.jasper.MergeDataSourceProcessor.SourceType#DATASOURCE} the
* object each row in the datasource will be used to form a row in the merged DataSource. If the
* source type is {@link org.mapfish.print.processor.jasper.MergeDataSourceProcessor.SourceType#SINGLE}
* the object will be a single row even if it is in fact a DataSource.</p>
*
* <p>See also: <a href="configuration.html#!mergeSource">!mergeSource</a></p>
*
* @param sources the source objects to merge
*/
public void setSources(final List<Source> sources) {
this.sources = sources;
}
@Override
protected void extraValidation(final List<Throwable> validationErrors, final Configuration config) {
if (this.sources == null || this.sources.isEmpty()) {
validationErrors.add(new ConfigurationException(getClass().getSimpleName() + " needs to have at minimum a single source. " +
"Although logically it should have more"));
return;
}
for (int i = 0; i < this.sources.size(); i++) {
Source source = this.sources.get(i);
if (source.type == null) {
validationErrors.add(new ConfigurationException(
"The " + indexString(i) + " source in " + getClass().getSimpleName() + " needs to " +
"have a 'type' parameter defined."));
} else {
source.type.validate(i, validationErrors, source);
}
}
}
@Nullable
@Override
public In createInputParameter() {
return new In();
}
@Nullable
@Override
public Out execute(final In values, final ExecutionContext context) throws Exception {
List<Map<String, ?>> rows = Lists.newArrayList();
for (Source source : this.sources) {
source.type.add(rows, values.values, source);
}
JRDataSource mergedDataSource = new JRMapCollectionDataSource(rows);
return new Out(mergedDataSource);
}
@Nonnull
@Override
public Collection<String> getDependencies() {
HashSet<String> sourceKeys = Sets.newHashSet();
for (Source source : this.sources) {
source.type.addValuesKeys(source, sourceKeys);
}
return sourceKeys;
}
private static String indexString(final int i) {
switch (i + 1) {
case 1:
return "1st";
case 2:
return "2nd";
default:
return (i + 1) + "th";
}
}
/**
* The input object for {@link org.mapfish.print.processor.jasper.MergeDataSourceProcessor}.
*/
public static class In {
/**
* The values used to look up the values to merge together.
*/
public Values values;
}
/**
* The output object for {@link org.mapfish.print.processor.jasper.MergeDataSourceProcessor}.
*/
public static class Out {
/**
* The resulting datasource.
*/
public final JRDataSource mergedDataSource;
/**
* Constructor.
*
* @param mergedDataSource the merged datasource
*/
public Out(final JRDataSource mergedDataSource) {
this.mergedDataSource = mergedDataSource;
}
}
/**
* <p>Describes the objects used as sources for a merged data source
* (see <a href="processors.html#!mergeDataSources">!mergeDataSources</a> processor).</p>
* [[examples=merged_datasource]]
*/
public static final class Source implements ConfigurationObject {
String key;
SourceType type;
Map<String, String> fields = Maps.newHashMap();
/**
* The key to use when looking for the object among the attributes and the processor output values.
*
* @param key the look up key
*/
public void setKey(final String key) {
this.key = key;
}
/**
* The type of source. See {@link org.mapfish.print.processor.jasper.MergeDataSourceProcessor.SourceType} for the options.
*
* @param type the type of source
*/
public void setType(final SourceType type) {
this.type = type;
}
/**
* The names of each field in the DataSource. See {@link org.mapfish.print.processor.jasper.MergeDataSourceProcessor.SourceType}
* for instructions on how to declare the fields
*
* @param fields the field names
*/
public void setFields(final Map<String, String> fields) {
this.fields = fields;
}
static Source createSource(final String key, final SourceType type) {
Source source = new Source();
source.key = key;
source.type = type;
return source;
}
static Source createSource(final String key, final SourceType type, final Map<String, String> fields) {
Source source = new Source();
source.key = key;
source.type = type;
source.fields = fields;
return source;
}
@Override
public void validate(final List<Throwable> validationErrors, final Configuration config) {
// validation is done in MergeDataSourceProcessor
}
}
/**
* An enumeration of the different <em>types</em> of source objects. Essentially this describes how the source should be merged
* into the final merged DataSource.
*/
public enum SourceType {
/**
* Creates a single row from a set of values from the output and attribute objects.
* <p>
* In this case the key is not required, only the fields. Each field key will be the
* look up key to find the object from the set of processor output and attributes. The field
* value will be the column name for that value in the created row
* </p>
*/
SINGLE {
@Override
void add(final List<Map<String, ?>> rows, final Values values, final Source source) {
Map<String, Object> row = Maps.newHashMap();
for (Map.Entry<String, String> entry : source.fields.entrySet()) {
final Object object = values.getObject(entry.getKey(), Object.class);
row.put(entry.getValue(), object);
}
rows.add(row);
}
@Override
void validate(final int rowIndex, final List<Throwable> validationErrors, final Source source) {
if (source.key != null) {
validationErrors.add(new ConfigurationException(
"The 'key' property is not required for source with the type " + name() + ". The " + indexString(rowIndex) +
" source has a key property configured when it should not"));
}
if (source.fields.isEmpty()) {
validationErrors.add(new ConfigurationException(
"The " + indexString(rowIndex) + " source in " + getClass().getSimpleName() + " has an invalid 'fields' " +
"parameter defined. There should be at least most one field defined"));
}
}
@Override
public void addValuesKeys(final Source source, final HashSet<String> sourceKeys) {
sourceKeys.addAll(source.fields.keySet());
}
},
/**
* Indicates that the object is a DataSource and each row in it should be expanded to be a row in the output table.
* <p>
* If the datasource does not exist or is null then this source will be skipped
* </p>
* <p>
* The fields parameter of the source should contain all the fields to pull from the source DataSource. Not all
* Fields need to be declared. For example if the source has 5 fields not all of them need to be in the resulting
* merged datasource.
* </p>
*/
DATASOURCE {
@Override
void add(final List<Map<String, ?>> rows, final Values values, final Source source) throws JRException {
JRDataSource dataSource = values.getObject(source.key, JRDataSource.class);
Assert.isTrue(dataSource != null, "The Datasource object referenced by key: " + source.key + " does not exist. Check" +
" that the key is correctly spelled in the config.yaml file.\n\t This is one of the" +
" sources for the !mergeDataSources.");
JRDesignField jrField = new JRDesignField();
while (dataSource.next()) {
Map<String, Object> row = Maps.newHashMap();
for (Map.Entry<String, String> field : source.fields.entrySet()) {
jrField.setName(field.getKey());
row.put(field.getValue(), dataSource.getFieldValue(jrField));
}
rows.add(row);
}
}
@Override
void validate(final int rowIndex, final List<Throwable> validationErrors, final Source source) {
if (source.key.isEmpty()) {
validationErrors.add(new ConfigurationException(
"The " + indexString(rowIndex) + " source in " + MergeDataSourceProcessor.class.getSimpleName() +
" needs to have a 'key' parameter defined."));
}
if (source.fields.isEmpty()) {
validationErrors.add(new ConfigurationException(
"The " + indexString(rowIndex) + " source in " + MergeDataSourceProcessor.class.getSimpleName() +
" needs to have a 'fields' parameter defined."));
}
}
@Override
public void addValuesKeys(final Source source, final HashSet<String> sourceKeys) {
sourceKeys.add(source.key);
}
};
abstract void add(List<Map<String, ?>> rows, Values values, Source source) throws JRException;
abstract void validate(int rowIndex, List<Throwable> validationErrors, Source source);
abstract void addValuesKeys(Source source, HashSet<String> sourceKeys);
}
}