package fr.openwide.core.imports.table.common.mapping;
import java.text.Collator;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import org.apache.commons.lang3.Validate;
import com.google.common.base.Equivalence;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import fr.openwide.core.commons.util.functional.Predicates2;
import fr.openwide.core.imports.table.common.event.TableImportEvent;
import fr.openwide.core.imports.table.common.event.TableImportEvent.ExcelImportErrorEvent;
import fr.openwide.core.imports.table.common.event.TableImportEvent.ExcelImportInfoEvent;
import fr.openwide.core.imports.table.common.event.ITableImportEventHandler;
import fr.openwide.core.imports.table.common.event.exception.TableImportContentException;
import fr.openwide.core.imports.table.common.event.exception.TableImportMappingException;
import fr.openwide.core.imports.table.common.location.TableImportLocation;
import fr.openwide.core.imports.table.common.location.TableImportLocationContext;
import fr.openwide.core.imports.table.common.location.ITableImportNavigator;
import fr.openwide.core.imports.table.common.mapping.column.ITableImportColumnDefinition;
import fr.openwide.core.imports.table.common.mapping.column.ITableImportColumnDefinition.IMappedExcelImportColumnDefinition;
import fr.openwide.core.imports.table.common.mapping.column.MappedTableImportColumnDefinitionImpl;
import fr.openwide.core.imports.table.common.mapping.column.builder.AbstractTableImportColumnBuilder;
import fr.openwide.core.imports.table.common.mapping.column.builder.ITableImportColumnMapper;
import fr.openwide.core.imports.table.common.mapping.column.builder.MappingConstraint;
import fr.openwide.core.imports.table.common.mapping.column.builder.state.TypeState;
/**
* The central class of this Excel import framework.
* See TestApachePoiExcelImporter for an example on how to use this class.
* @author yrodiere
*/
public abstract class AbstractTableImportColumnSet<TTable, TRow, TCell, TCellReference> {
private static final Comparator<? super String> DEFAULT_HEADER_LABEL_COLLATOR;
static {
Collator collator = Collator.getInstance(Locale.ROOT);
collator.setStrength(Collator.IDENTICAL);
DEFAULT_HEADER_LABEL_COLLATOR = Ordering.from(collator).nullsFirst();
}
private final Comparator<? super String> defaultHeaderLabelCollator;
private final AbstractTableImportColumnBuilder<TTable, TRow, TCell, TCellReference> builder;
private final Collection<Column<?>> columns = Lists.newArrayList();
public AbstractTableImportColumnSet(AbstractTableImportColumnBuilder<TTable, TRow, TCell, TCellReference> builder) {
this(builder, DEFAULT_HEADER_LABEL_COLLATOR);
}
public AbstractTableImportColumnSet(AbstractTableImportColumnBuilder<TTable, TRow, TCell, TCellReference> builder, Comparator<? super String> defaultHeaderLabelCollator) {
super();
this.builder = builder;
this.defaultHeaderLabelCollator = defaultHeaderLabelCollator;
}
public final TypeState<TTable, TRow, TCell, TCellReference> withHeader(String headerLabel) {
return withHeader(headerLabel, MappingConstraint.REQUIRED);
}
public final TypeState<TTable, TRow, TCell, TCellReference> withHeader(String headerLabel, Comparator<? super String> collator) {
return withHeader(headerLabel, collator, 0, MappingConstraint.REQUIRED);
}
public final TypeState<TTable, TRow, TCell, TCellReference> withHeader(String headerLabel, MappingConstraint mappingConstraint) {
return withHeader(headerLabel, 0, mappingConstraint);
}
public final TypeState<TTable, TRow, TCell, TCellReference> withHeader(String headerLabel, int indexAmongMatchedColumns, MappingConstraint mappingConstraint) {
return withHeader(headerLabel, defaultHeaderLabelCollator, indexAmongMatchedColumns, mappingConstraint);
}
public final TypeState<TTable, TRow, TCell, TCellReference> withHeader(String headerLabel, Equivalence<? super String> headerEquivalence, int indexAmongMatchedColumns, MappingConstraint mappingConstraint) {
return builder.withHeader(this, headerLabel, headerEquivalence.equivalentTo(headerLabel), indexAmongMatchedColumns, mappingConstraint);
}
public final TypeState<TTable, TRow, TCell, TCellReference> withHeader(String headerLabel, Comparator<? super String> collator, int indexAmongMatchedColumns, MappingConstraint mappingConstraint) {
return builder.withHeader(this, headerLabel, Predicates2.comparesEqualTo(headerLabel, collator), indexAmongMatchedColumns, mappingConstraint);
}
public final TypeState<TTable, TRow, TCell, TCellReference> withIndex(int index) {
return builder.withIndex(this, index);
}
/**
* The actual column implementation.
* <p>This class is implemented as an inner class in order to get rid of the <TSheet, TRow, TCellReference, TCell, TValue> generic
* parameters when the client references columns.
*/
public class Column<TValue> implements ITableImportColumnDefinition<TTable, TRow, TCell, TCellReference, TValue> {
private final ITableImportColumnMapper<TTable, TRow, TCell, TCellReference> mapper;
private final Function<? super TCell, ? extends TValue> cellToValueFunction;
private final Predicate<? super TValue> mandatoryValuePredicate;
public Column(ITableImportColumnMapper<TTable, TRow, TCell, TCellReference> mapper,
Function<? super TCell, ? extends TValue> cellToValueFunction, Predicate<? super TValue> mandatoryValuePredicate) {
super();
this.mapper = mapper;
this.cellToValueFunction = cellToValueFunction;
this.mandatoryValuePredicate = mandatoryValuePredicate;
// Register the new column
AbstractTableImportColumnSet.this.columns.add(this);
}
@Override
public IMappedExcelImportColumnDefinition<TTable, TRow, TCell, TCellReference, TValue> map(TTable sheet, ITableImportNavigator<TTable, TRow, TCell, TCellReference> navigator,
ITableImportEventHandler eventHandler) throws TableImportMappingException {
Function<? super TRow, ? extends TCellReference> rowToCellReferenceFunction = mapper.tryMap(sheet, navigator, eventHandler);
return new MappedTableImportColumnDefinitionImpl<TTable, TRow, TCell, TCellReference, TValue>(sheet, rowToCellReferenceFunction, navigator, cellToValueFunction, mandatoryValuePredicate);
}
}
public final TableContext map(TTable sheet, ITableImportNavigator<TTable, TRow, TCell, TCellReference> navigator, ITableImportEventHandler eventHandler) throws TableImportMappingException {
return new TableContext(sheet, navigator, eventHandler);
}
public class TableContext extends TableImportLocationContext implements Iterable<RowContext> {
private final TTable table;
private final ITableImportNavigator<TTable, TRow, TCell, TCellReference> navigator;
private final ITableImportEventHandler eventHandler;
private final Map<Column<?>, IMappedExcelImportColumnDefinition<TTable, TRow, TCell, TCellReference, ?>> mappings;
private TableContext(TTable table, ITableImportNavigator<TTable, TRow, TCell, TCellReference> navigator, ITableImportEventHandler eventHandler)
throws TableImportMappingException {
super(eventHandler);
Validate.notNull(table, "table must not be null");
this.table = table;
this.navigator = navigator;
this.eventHandler = eventHandler;
Map<Column<?>, IMappedExcelImportColumnDefinition<TTable, TRow, TCell, TCellReference, ?>> mutableMappings = Maps.newHashMap();
for (Column<?> columnDefinition : columns) {
mutableMappings.put(columnDefinition, columnDefinition.map(table, navigator, eventHandler));
}
this.mappings = Collections.unmodifiableMap(mutableMappings);
this.eventHandler.checkNoMappingErrorOccurred();
}
public TTable getTable() {
return table;
}
@Override
public Iterator<RowContext> iterator() {
return toRowContexts(navigator.rows(table));
}
protected Iterator<RowContext> toRowContexts(Iterator<TRow> rows) {
return Iterators.transform(rows, new Function<TRow, RowContext>() {
@Override
public RowContext apply(TRow input) {
return row(input);
}
});
}
public Iterable<RowContext> nonEmptyRows() {
return new Iterable<RowContext>() {
@Override
public Iterator<RowContext> iterator() {
return toRowContexts(navigator.nonEmptyRows(table));
}
};
}
@SuppressWarnings("unchecked")
private <TValue> IMappedExcelImportColumnDefinition<TTable, TRow, TCell, TCellReference, TValue> getMappedColumn(
ITableImportColumnDefinition<TTable, TRow, TCell, TCellReference, TValue> columnDefinition) {
IMappedExcelImportColumnDefinition<TTable, TRow, TCell, TCellReference, TValue> mappedColumn =
(IMappedExcelImportColumnDefinition<TTable, TRow, TCell, TCellReference, TValue>) mappings.get(columnDefinition);
if (mappedColumn == null) {
throw new IllegalStateException("Column " + columnDefinition
+ " was not properly registered, hence it has not been mapped. Please use AbstractColumns.add() before using AbstractColumns.newMapping().");
}
return mappedColumn;
}
public RowContext row(TRow row) {
return new RowContext(this, row);
}
public <TValue> ColumnContext<TValue> column(Column<TValue> columnDefinition) {
return new ColumnContext<TValue>(this, columnDefinition);
}
public <TValue> CellContext<TValue> cell(TRow row, Column<TValue> columnDefinition) {
return row(row).cell(columnDefinition);
}
/**
* @see TableImportLocationContext The event recording methods error(), info(), etc. defined in the superclass.
*/
@Override
public TableImportLocation getLocation() {
return navigator.getLocation(table, null, null);
}
public void event(ExcelImportErrorEvent event, String message, TRow row, Object ... args) throws TableImportContentException {
event(event, message, row, null, (Object[])args);
}
public void event(ExcelImportInfoEvent event, String message, TRow row, Object ... args) {
event(event, message, row, null, (Object[])args);
}
public void event(ExcelImportErrorEvent event, String message, TRow row, TCellReference cellReference, Object ... args) throws TableImportContentException {
eventHandler.event(event, navigator.getLocation(table, row, cellReference), message, (Object[])args);
}
public void event(ExcelImportInfoEvent event, String message, TRow row, TCellReference cellReference, Object ... args) {
eventHandler.event(event, navigator.getLocation(table, row, cellReference), message, (Object[])args);
}
}
public final class RowContext extends TableImportLocationContext {
private final TableContext tableContext;
private final TRow row;
private RowContext(TableContext sheetContext, TRow row) {
super(sheetContext.eventHandler);
this.tableContext = sheetContext;
this.row = row;
}
public boolean hasContent() {
return tableContext.navigator.rowHasContent(row);
}
public <TValue> CellContext<TValue> cell(Column<TValue> columnDefinition) {
return new CellContext<>(this, tableContext.getMappedColumn(columnDefinition));
}
/**
* @see TableImportLocationContext The event recording methods error(), info(), etc. defined in the superclass.
*/
@Override
public TableImportLocation getLocation() {
return tableContext.navigator.getLocation(tableContext.table, row, null);
}
public void event(ExcelImportErrorEvent event, String message, TCellReference cellReference, Object ... args) throws TableImportContentException {
tableContext.event(event, message, row, cellReference, (Object[])args);
}
public void event(ExcelImportInfoEvent event, String message, TCellReference cellReference, Object ... args) {
tableContext.event(event, message, row, cellReference, (Object[])args);
}
}
public final class ColumnContext<TValue> {
private final TableContext tableContext;
private final Column<TValue> columnDefinition;
private ColumnContext(TableContext sheetContext, Column<TValue> columnDefinition) {
super();
this.tableContext = sheetContext;
this.columnDefinition = columnDefinition;
}
public CellContext<TValue> cell(TRow row) {
return tableContext.row(row).cell(columnDefinition);
}
public boolean exists() {
return tableContext.getMappedColumn(columnDefinition).isBound();
}
}
public final class CellContext<T> extends TableImportLocationContext {
private final RowContext rowContext;
private final IMappedExcelImportColumnDefinition<TTable, TRow, TCell, TCellReference, T> mappedColumn;
private CellContext(RowContext rowContext, IMappedExcelImportColumnDefinition<TTable, TRow, TCell, TCellReference, T> mappedColumn) {
super(rowContext.tableContext.eventHandler);
this.rowContext = rowContext;
this.mappedColumn = mappedColumn;
}
public T get() {
return mappedColumn.getValue(rowContext.row);
}
public T getMandatory(String message, Object ... args) throws TableImportContentException {
return getMandatory(TableImportEvent.ERROR, message, (Object[])args);
}
public T getMandatory(ExcelImportErrorEvent event, String message, Object ... args) throws TableImportContentException {
T value = mappedColumn.getMandatoryValue(rowContext.row);
if (value == null) {
event(event, message, (Object[])args);
}
return value;
}
public T getMandatory(ExcelImportInfoEvent event, String message, Object ... args) {
T value = mappedColumn.getMandatoryValue(rowContext.row);
if (value == null) {
event(event, message, (Object[])args);
}
return value;
}
public boolean hasContent() {
return mappedColumn.hasContent(rowContext.row);
}
/**
* @deprecated Use {@link #error(String, Object...)} instead.
*/
@Deprecated
public void missingValue(String message) throws TableImportContentException {
error(message);
}
/**
* @see TableImportLocationContext The event recording methods error(), info(), etc. defined in the superclass.
*/
@Override
public TableImportLocation getLocation() {
return rowContext.tableContext.navigator.getLocation(rowContext.tableContext.table, rowContext.row, mappedColumn.getCellReference(rowContext.row));
}
}
}