/*
* Copyright [2014] [Christian Loehnert, krampenschiesser@gmail.com]
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.ks.idnadrev.expimp.xls;
import de.ks.idnadrev.expimp.DependencyGraph;
import de.ks.idnadrev.expimp.xls.result.XlsxImportSheetResult;
import de.ks.idnadrev.expimp.xls.sheet.ImportSheetHandler;
import de.ks.idnadrev.expimp.xls.sheet.ImportValue;
import de.ks.persistence.PersistentWork;
import de.ks.persistence.entity.IdentifyableEntity;
import de.ks.reflection.ReflectionUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.PluralAttribute;
import javax.persistence.metamodel.SingularAttribute;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class SingleSheetImport implements Callable<Void> {
private static final Logger log = LoggerFactory.getLogger(SingleSheetImport.class);
protected final ColumnProvider columnProvider;
private final Class<?> clazz;
private final InputStream sheetStream;
private final DependencyGraph graph;
private final XlsxImportSheetResult result;
private final XlsImportCfg importCfg;
private final EntityType<?> entityType;
private final XSSFReader reader;
protected final LinkedList<Runnable> runAfterImport = new LinkedList<>();
public SingleSheetImport(Class<?> clazz, InputStream sheetStream, DependencyGraph graph, XSSFReader reader, XlsxImportSheetResult result, XlsImportCfg importCfg) {
this.clazz = clazz;
this.sheetStream = sheetStream;
this.graph = graph;
this.result = result;
this.importCfg = importCfg;
this.entityType = graph.getEntityType(clazz);
this.reader = reader;
columnProvider = new ColumnProvider(graph);
}
@Override
public Void call() throws Exception {
try {
XMLReader parser = XMLReaderFactory.createXMLReader();
ImportSheetHandler importSheetHandler = new ImportSheetHandler(clazz, reader.getSharedStringsTable(), columnProvider, this::importEntity);
parser.setContentHandler(importSheetHandler);
InputSource inputSource = new InputSource(sheetStream);
parser.parse(inputSource);
} catch (SAXException | IOException | InvalidFormatException e) {
result.generalError("Failed to parse sheet " + clazz.getName(), e);
throw new RuntimeException(e);
} finally {
try {
sheetStream.close();
} catch (IOException e) {
result.generalError("Could not close sheet stream " + clazz.getName(), e);
throw new RuntimeException(e);
}
return null;
}
}
public void importEntity(List<ImportValue> importValues) {
if (importValues.isEmpty()) {
return;
}
Object idProperty;
if (IdentifyableEntity.class.isAssignableFrom(clazz)) {
String idPropertyName = graph.getIdentifierProperty(clazz);
Optional<ImportValue> first = importValues.stream().filter(v -> v.getColumnDef().getIdentifier().equals(idPropertyName)).findFirst();
idProperty = first.get().getValue();
} else {
idProperty = null;
}
PersistentWork.run(em -> {
Object instance;
boolean keepExisting = importCfg.isKeepExisting();
boolean exists;
if (idProperty != null) {
String idPropertyName = graph.getIdentifierProperty(clazz);
@SuppressWarnings("unchecked") IdentifyableEntity loaded = PersistentWork.findByIdentification((Class<IdentifyableEntity>) clazz, idPropertyName, idProperty);
if (loaded != null) {
instance = loaded;
exists = true;
} else {
exists = false;
instance = ReflectionUtil.newInstance(clazz);
}
} else {
exists = false;
instance = ReflectionUtil.newInstance(clazz);
}
if (exists && keepExisting) {
result.success("Ignored " + clazz.getName() + " with identifier " + idProperty, importValues.get(0).getCellId());
return;
}
List<ToOneRelationAssignment> manadatoryRelations = getManadatoryRelations(importValues, instance);
List<ToOneRelationAssignment> optionalRelations = getOptionalRelations(importValues);
List<ToManyRelationAssignment> toManyRelations = getToManyRelations(importValues);
try {
manadatoryRelations.forEach(r -> {
r.run();
});
importValues.forEach(v -> {
XlsxColumn columnDef = v.getColumnDef();
Object value = v.getValue();
columnDef.setValue(instance, value);
});
optionalRelations.forEach(r -> {
r.ownerResolver = () -> {
Object identifier = getIdentifier(instance);
String identifierProperty = graph.getIdentifierProperty(clazz);
return resolveEntity(clazz, identifierProperty, identifier);
};
});
if (!exists) {
em.persist(instance);
}
result.success(instance.toString(), importValues.get(0).getCellId());
toManyRelations.forEach(r -> {
r.ownerResolver = () -> {
Object identifier = getIdentifier(instance);
String identifierProperty = graph.getIdentifierProperty(instance.getClass());
return resolveEntity(clazz, identifierProperty, identifier);
};
});
runAfterImport.addAll(optionalRelations);
runAfterImport.addAll(toManyRelations);
} catch (Exception e) {
result.error("Could not persist entity " + instance, e, importValues.get(0).getCellId());
throw e;
}
});
}
private List<ToOneRelationAssignment> getManadatoryRelations(List<ImportValue> importValues, Object instance) {
List<SingularAttribute<?, ?>> mandatoryRelations = entityType.getSingularAttributes().stream().filter(a -> a.isAssociation() && !a.isOptional()).collect(Collectors.toList());
return mandatoryRelations.stream().map(r -> {
Optional<ImportValue> found = importValues.stream().filter(v -> v.getColumnDef().getIdentifier().equals(r.getName())).findFirst();
if (!found.isPresent()) {
log.warn("Could not import {} no column definition for {}.{} found. Values: {}", clazz.getName(), r.getJavaType().getName(), r.getName(), importValues);
result.warn("Could not import " + clazz.getName() + " no column definition for " + r.getJavaType().getName() + "." + r.getName() + " found. Values: " + importValues, null);
return null;
}
ImportValue importValue = found.get();
importValues.remove(importValue);
String identifierProperty = graph.getIdentifierProperty(r.getJavaType());
Consumer<Object> resultWriter = o -> result.warn("could not find association of '" + o + "' via '" + r.getName() + "' to '" + importValue.getValue() + "'", importValue.getCellId());
return new ToOneRelationAssignment(resultWriter, () -> instance, r, importValue.getColumnDef(), identifierProperty, importValue.getValue());
}).collect(Collectors.toList());
}
private List<ToOneRelationAssignment> getOptionalRelations(List<ImportValue> importValues) {
List<SingularAttribute<?, ?>> optionalRelations = entityType.getSingularAttributes().stream().filter(a -> a.isAssociation() && a.isOptional()).collect(Collectors.toList());
return optionalRelations.stream().map(r -> {
Optional<ImportValue> found = importValues.stream().filter(v -> v.getColumnDef().getIdentifier().equals(r.getName())).findFirst();
if (!found.isPresent()) {
return null;
}
ImportValue importValue = found.get();
importValues.remove(importValue);
Consumer<Object> resultWriter = o -> result.warn("could not find association of '" + o + "' via '" + r.getName() + "' to '" + importValue.getValue() + "'", importValue.getCellId());
String identifierProperty = graph.getIdentifierProperty(r.getJavaType());
return new ToOneRelationAssignment(resultWriter, null, r, importValue.getColumnDef(), identifierProperty, importValue.getValue());
}).filter(r -> r != null).collect(Collectors.toList());
}
private List<ToManyRelationAssignment> getToManyRelations(List<ImportValue> importValues) {
List<PluralAttribute<?, ?, ?>> pluralAttributes = entityType.getPluralAttributes().stream().filter(a -> a.isAssociation()).collect(Collectors.toList());
return pluralAttributes.stream().map(r -> {
Optional<ImportValue> found = importValues.stream().filter(v -> v.getColumnDef().getIdentifier().equals(r.getName())).findFirst();
if (!found.isPresent()) {
return null;
}
ImportValue importValue = found.get();
importValues.remove(importValue);
String identifierProperty = graph.getIdentifierProperty(r.getElementType().getJavaType());
return new ToManyRelationAssignment(importValue.getColumnDef(), r, identifierProperty, (String) importValue.getValue());
}).filter(r -> r != null).collect(Collectors.toList());
}
static Object getIdentifier(Object instance) {
if (IdentifyableEntity.class.isAssignableFrom(instance.getClass())) {
return ((IdentifyableEntity) instance).getIdValue();
}
return null;
}
static Object resolveEntity(Class<?> type, String identifierProperty, Object identifier) {
if (IdentifyableEntity.class.isAssignableFrom(type)) {
@SuppressWarnings("unchecked") IdentifyableEntity identifyableEntity = PersistentWork.findByIdentification((Class<IdentifyableEntity>) type, identifierProperty, identifier);
return identifyableEntity;
}
return null;
}
static List<Object> resolveToManyRelation(Class<?> type, String identifierProperty, List<String> singleIdentifiers) {
LinkedList<Object> retval = new LinkedList<>();
for (String identifier : singleIdentifiers) {
Object entity = resolveEntity(type, identifierProperty, identifier);
retval.add(entity);
}
return retval;
}
static class ToOneRelationAssignment implements Runnable {
private final Consumer<Object> resultWriter;
XlsxColumn ownerColumn;
Supplier<Object> ownerResolver;
Supplier<Object> relationResolver;
ToOneRelationAssignment(Consumer<Object> resultWriter, Supplier<Object> ownerResolver, SingularAttribute<?, ?> relation, XlsxColumn ownerColumn, String identifierPropertyName, Object relationIdentifier) {
this.resultWriter = resultWriter;
this.ownerColumn = ownerColumn;
this.ownerResolver = ownerResolver;
relationResolver = () -> resolveEntity(relation.getJavaType(), identifierPropertyName, relationIdentifier);
}
@Override
public void run() {
PersistentWork.wrap(() -> {
Object owner = ownerResolver.get();
Object value = relationResolver.get();
if (value == null) {
resultWriter.accept(owner);
} else {
ownerColumn.setValue(owner, value);
}
});
}
}
static class ToManyRelationAssignment implements Runnable {
XlsxColumn ownerColumn;
Supplier<Object> ownerResolver;
Supplier<List<Object>> relationResolver;
ToManyRelationAssignment(XlsxColumn ownerColumn, PluralAttribute relation, String identifierPropertyName, String relationString) {
this.ownerColumn = ownerColumn;
String[] split = StringUtils.split(relationString, "|");
ArrayList<String> singleIdentifiers = new ArrayList<>(split.length);
for (String string : split) {
String id = StringUtils.replace(string, ToManyColumn.SEPARATOR, ToManyColumn.SEPARATOR_REPLACEMENT);
singleIdentifiers.add(id);
}
relationResolver = () -> resolveToManyRelation(relation.getElementType().getJavaType(), identifierPropertyName, singleIdentifiers);
}
@Override
@SuppressWarnings("unchecked")
public void run() {
PersistentWork.wrap(() -> {
Object owner = ownerResolver.get();
List<Object> values = relationResolver.get();
Collection original = (Collection) ReflectionUtil.getFieldValue(owner, ownerColumn.getIdentifier());
original.addAll(values);
});
}
}
public LinkedList<Runnable> getRunAfterImport() {
return runAfterImport;
}
}