package net.rrm.ehour.backup.service.restore; import net.rrm.ehour.backup.common.BackupConfig; import net.rrm.ehour.backup.common.BackupEntityType; import net.rrm.ehour.backup.domain.ImportException; import net.rrm.ehour.backup.domain.ParseSession; import net.rrm.ehour.backup.domain.ParserUtil; import net.rrm.ehour.backup.service.restore.structure.FieldDefinition; import net.rrm.ehour.backup.service.restore.structure.FieldMap; import net.rrm.ehour.backup.service.restore.structure.FieldMapFactory; import net.rrm.ehour.domain.DomainObject; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import javax.persistence.*; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; import java.io.Serializable; import java.lang.reflect.Field; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; /** * Not thread safe * * @author thies (Thies Edeling - thies@te-con.nl) * Created on: Nov 16, 2010 - 11:18:59 PM */ public class EntityParser { private EntityParserDao parserDao; private XMLEventReader reader; private static final Logger LOG = Logger.getLogger(EntityParser.class); private static final Map<Class<?>, TypeTransformer<?>> transformerMap = new HashMap<>(); private ParseSession status; private PrimaryKeyCache keyCache; private final BackupConfig backupConfig; static { transformerMap.put(Integer.class, new IntegerTransformer()); transformerMap.put(Float.class, new FloatTransformer()); transformerMap.put(Date.class, new DateTransformer()); transformerMap.put(Boolean.class, new BooleanTransformer()); } public EntityParser(XMLEventReader reader, EntityParserDao parserDao, PrimaryKeyCache keyCache, BackupConfig backupConfig) { this.parserDao = parserDao; this.reader = reader; this.keyCache = keyCache; this.backupConfig = backupConfig; } public <PK extends Serializable, T extends DomainObject<PK, ?>> List<T> parse(Class<T> clazz, JoinTables joinTables, ParseSession status) throws IllegalAccessException, InstantiationException, XMLStreamException, ImportException { FieldMap fieldMap = FieldMapFactory.buildFieldMapForEntity(clazz); this.status = status; return parseDomainObjects(clazz, fieldMap, joinTables, status); } /** * Parse domain object with reader pointing on the table name tag */ private <PK extends Serializable, T extends DomainObject<PK, ?>> List<T> parseDomainObjects(Class<T> clazz, FieldMap fieldMap, JoinTables joinTables, ParseSession status) throws XMLStreamException, IllegalAccessException, InstantiationException, ImportException { List<T> domainObjects = new ArrayList<>(); while (reader.hasNext()) { XMLEvent event = reader.nextTag(); if (event.isStartElement()) { T domainObject = parseAndPersistDomainObject(clazz, fieldMap, joinTables); domainObjects.add(domainObject); BackupEntityType backupEntityType = backupConfig.entityForClass(clazz); status.addInsertion(backupEntityType); } else if (event.isEndElement()) { break; } } return domainObjects; } @SuppressWarnings("unchecked") private <PK extends Serializable, T extends DomainObject<PK, ?>> T parseAndPersistDomainObject(Class<T> clazz, FieldMap fieldMap, JoinTables joinTables) throws XMLStreamException, IllegalAccessException, InstantiationException, ImportException { T targetObject = clazz.newInstance(); Map<Class<?>, Object> embeddables = new HashMap<>(); while (reader.hasNext()) { XMLEvent event = reader.nextTag(); if (event.isEndElement()) { break; } StartElement startElement = event.asStartElement(); String dbField = startElement.getName().getLocalPart(); FieldDefinition fieldDefinition = fieldMap.get(dbField.toLowerCase()); Field targetField = fieldDefinition.getField(); Class<? extends Serializable> type = (Class<? extends Serializable>) targetField.getType(); String columnValue = ParserUtil.parseNextEventAsCharacters(reader); Object parsedColumnValue = parseColumn(type, columnValue, fieldDefinition.isIgnorable()); if (parsedColumnValue != null) { fieldDefinition.process(targetObject, embeddables, parsedColumnValue); } } boolean hasCompositeKey = replaceEmbeddablesInEntity(fieldMap, targetObject, embeddables); addManyToManies(fieldMap, targetObject, joinTables); PK originalKey = targetObject.getPK(); resetId(fieldMap, targetObject); Serializable primaryKey = parserDao.persist(targetObject); if (!hasCompositeKey) { Serializable casted = castOrLog(originalKey, primaryKey); keyCache.putKey(targetObject.getClass(), originalKey, casted); } return targetObject; } private <PK extends Serializable> Serializable castOrLog(PK originalKey, Serializable primaryKey) throws ImportException { try { if (primaryKey.getClass().equals(originalKey.getClass())) { return originalKey.getClass().cast(primaryKey); } else { LOG.error("This should only happen while running junit tests."); return null; } } catch (ClassCastException cce) { throw new ImportException("can't cast " + primaryKey.toString() + " to " + originalKey.toString()+ ": " + cce.getMessage(), cce); } } @SuppressWarnings("unchecked") private <T> void addManyToManies(FieldMap fieldMap, T targetEntity, JoinTables joinTables) throws IllegalAccessException { for (FieldDefinition fieldDefinition : fieldMap.fieldDefinitions()) { if (!fieldDefinition.isPartOfXML()) { // find the original ID of this entity FieldDefinition idFieldDef = fieldMap.getId(); Field idField = idFieldDef.getField(); String id = idField.get(targetEntity).toString(); // discover the table name Field field = fieldDefinition.getField(); JoinTable joinTableAnnotation = field.getAnnotation(JoinTable.class); String tableName = joinTableAnnotation.name().toLowerCase(); // find the type of the fk entity ManyToMany manyToManyAnnotation = field.getAnnotation(ManyToMany.class); Class fkType = manyToManyAnnotation.targetEntity(); // discover the type of the fk id JoinColumn[] joinColumns = joinTableAnnotation.inverseJoinColumns(); String fkIdDatabaseColumnName = joinColumns[0].name(); FieldMap fkFieldMap = FieldMapFactory.buildFieldMapForEntity(fkType); FieldDefinition fkIdFieldDefinition = fkFieldMap.get(fkIdDatabaseColumnName.toLowerCase()); // iterate over all the foreign tables this entity has a relation with List<String> targetFkIds = joinTables.getTarget(tableName, id); if (targetFkIds != null) { for (String fkId : targetFkIds) { Serializable fkTransformedId; if (fkIdFieldDefinition.getField().getAnnotation(GeneratedValue.class) != null) { Class<?> fkIdType = fkIdFieldDefinition.getField().getType(); fkTransformedId = keyCache.getKey(fkType, fkIdType == Integer.class ? Integer.parseInt(fkId) : fkId); } else { fkTransformedId = fkId; } Serializable fk = parserDao.find(fkTransformedId, fkType); Collection o = (Collection) field.get(targetEntity); o.add(fk); } } } } } private <T> boolean replaceEmbeddablesInEntity(FieldMap fieldMap, T targetEntity, Map<Class<?>, Object> embeddables) throws IllegalAccessException { boolean hasCompositeKey = false; for (FieldDefinition fieldDefinition : fieldMap.fieldDefinitions()) { Field field = fieldDefinition.getField(); Class<?> fieldType = field.getType(); if (fieldType.isAnnotationPresent(Embeddable.class)) { field.set(targetEntity, embeddables.get(fieldType)); hasCompositeKey = true; } } return hasCompositeKey; } private <T> void resetId(FieldMap fieldMap, T domainObject) throws IllegalAccessException { FieldDefinition fieldDef = fieldMap.getGeneratedId(); if (fieldDef != null) { fieldDef.getField().setAccessible(true); fieldDef.getField().set(domainObject, null); } } @SuppressWarnings("unchecked") private Serializable parseColumn(Class<? extends Serializable> columnType, String value, boolean canBeIgnored) throws IllegalAccessException, InstantiationException { Serializable parsedValue = null; if (columnType.isAnnotationPresent(Entity.class)) { Serializable castToFk = castToFkType(columnType, value); Serializable persistedKey = keyCache.getKey(columnType, castToFk); if (persistedKey != null) { parsedValue = parserDao.find(persistedKey, columnType); } if (parsedValue == null && !canBeIgnored) { status.addError(backupConfig.entityForClass(columnType), "ManyToOne relation not resolved"); } } else if (columnType == String.class) { parsedValue = value; } else if (columnType.isEnum()) { parsedValue = Enum.valueOf((Class<Enum>) columnType, value); } else { if (transformerMap.containsKey(columnType)) { parsedValue = transformerMap.get(columnType).transform(value); } else { status.addError(backupConfig.entityForClass(columnType), "unknown type: " + columnType); LOG.error("no transformer for type " + columnType); } } return parsedValue; } @SuppressWarnings("unchecked") private Serializable castToFkType(Class<?> fkObjectType, String value) throws InstantiationException, IllegalAccessException { Field[] fields = fkObjectType.getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(Id.class)) { return parseColumn((Class<? extends Serializable>) field.getType(), value, false); } } return value; } PrimaryKeyCache getKeyCache() { return keyCache; } private interface TypeTransformer<T extends Serializable> { T transform(String value); } private static class IntegerTransformer implements TypeTransformer<Integer> { @Override public Integer transform(String value) { return StringUtils.isNotBlank(value) ? Integer.parseInt(value) : null; } } private static class BooleanTransformer implements TypeTransformer<Boolean> { @Override public Boolean transform(String value) { return "y".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value) || "1".equals(value); } } private static class FloatTransformer implements TypeTransformer<Float> { @Override public Float transform(String value) { return StringUtils.isNotBlank(value) ? Float.parseFloat(value) : null; } } private static class DateTransformer implements TypeTransformer<Date> { @Override public Date transform(String value) { try { return new SimpleDateFormat("yyyy-MM-dd").parse(value); } catch (ParseException e) { LOG.error("Failed to parse date: " + value); return null; } } } }