package nl.ipo.cds.etl.attributemapping;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import nl.idgis.commons.jobexecutor.Job;
import nl.idgis.commons.jobexecutor.JobLogger;
import nl.idgis.commons.jobexecutor.JobLogger.LogLevel;
import nl.ipo.cds.attributemapping.MapperContext;
import nl.ipo.cds.attributemapping.MappingDestination;
import nl.ipo.cds.attributemapping.MappingSource;
import nl.ipo.cds.attributemapping.executer.Executer;
import nl.ipo.cds.attributemapping.executer.MappingValidationException;
import nl.ipo.cds.attributemapping.executer.OperationExecutionException;
import nl.ipo.cds.attributemapping.operations.Operation;
import nl.ipo.cds.attributemapping.operations.OperationInput;
import nl.ipo.cds.attributemapping.operations.OperationType;
import nl.ipo.cds.dao.attributemapping.OperationDTO;
import nl.ipo.cds.domain.FeatureType;
import nl.ipo.cds.etl.FeatureOutputStream;
import nl.ipo.cds.etl.GenericFeature;
import nl.ipo.cds.etl.PersistableFeature;
import nl.ipo.cds.etl.attributemapping.AttributeMappingValidator.MessageKey;
import nl.ipo.cds.etl.featurecollection.FeatureCollection;
import nl.ipo.cds.etl.log.LogStringBuilder;
import nl.ipo.cds.etl.theme.AttributeDescriptor;
import nl.ipo.cds.etl.theme.ObjectDescriptor;
import nl.ipo.cds.etl.theme.ThemeConfig;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.deegree.commons.tom.ows.CodeType;
public class AttributeMapper<T extends PersistableFeature> {
private static final Log technicalLog = LogFactory.getLog (AttributeMapper.class);
private final Job job;
private final ThemeConfig<T> themeConfig;
private final FeatureType featureType;
private final Map<AttributeDescriptor<?>, OperationDTO> attributeMappings;
private final LogStringBuilder<AttributeMappingValidator.MessageKey> logger;
private final Mapping mapping;
public AttributeMapper (final Job job,
final ThemeConfig<T> themeConfig,
final FeatureType featureType,
final Map<AttributeDescriptor<?>, OperationDTO> attributeMappings,
final JobLogger jobLogger,
final Properties loggerProperties) {
if (job == null) {
throw new NullPointerException ("job is null");
}
if (themeConfig == null) {
throw new NullPointerException ("themeConfig is null");
}
if (featureType == null) {
throw new NullPointerException ("featureType is null");
}
if (attributeMappings == null) {
throw new NullPointerException ("attributeMappings is null");
}
if (jobLogger == null) {
throw new NullPointerException ("stringLogger is null");
}
if (loggerProperties == null) {
throw new NullPointerException ("loggerProperties is null");
}
this.job = job;
this.themeConfig = themeConfig;
this.featureType = featureType;
this.attributeMappings = new HashMap<AttributeDescriptor<?>, OperationDTO> (attributeMappings);
// Create a logger:
logger = new LogStringBuilder<AttributeMappingValidator.MessageKey> ();
logger.setJobLogger (jobLogger);
logger.setProperties (loggerProperties);
// Build attribute mapping pipelines for each attribute:
this.mapping = buildAttributeMappings ();
}
public boolean isValid () {
return mapping != null;
}
public ThemeConfig<T> getThemeConfig () {
return themeConfig;
}
public FeatureType getFeatureType () {
return featureType;
}
public Map<AttributeDescriptor<?>, OperationDTO> getAttributeMappings () {
return Collections.unmodifiableMap (attributeMappings);
}
public void processFeatures (final FeatureCollection featureCollection, final FeatureOutputStream<T> outputStream) {
// Validate:
if (featureCollection == null) {
throw new NullPointerException ("featureCollection is null");
}
if (outputStream == null) {
throw new NullPointerException ("outputStream is null");
}
if (!isValid ()) {
throw new IllegalStateException ("The attribute mapping is not valid");
}
final MappingAttributeInfo[] attributes = mapping.attributes;
final Object[] instances = new Object[mapping.objectClasses.length];
final Executer[] executers = new Executer[attributes.length];
final MappingDestination[] destinations = new MappingDestination[mapping.attributes.length];
// Create mapping destinations:
for (int i = 0; i < attributes.length; ++ i) {
destinations[i] = createMappingDestination (attributes[i], instances);
executers[i] = attributes[i].executer;
}
// Initialize an instance of each object:
initializeInstances (instances);
// Initialize values for keys:
// TODO: Implement key checks.
// Loop over features in the feature collection:
for (final GenericFeature feature: featureCollection) {
final MappingSource mappingSource = createMappingSource (feature);
// Apply mapping to each known attribute:
for (int i = 0; i < executers.length; ++ i) {
try {
executers[i].execute (mappingSource, destinations[i]);
} catch (OperationExecutionException e) {
technicalLog.error ("Error while executing mapping operation", e);
logger.logEvent (
job,
MessageKey.ATTRIBUTE_MAPPING_RUNTIME_ERROR,
LogLevel.ERROR,
attributes[i].attributeDescriptor.getDescription (Locale.getDefault ()),
e.getLocalizedMessage ()
);
}
}
// Compare key values:
// TODO: Implement key checks.
// Store instances:
// TODO: Simplified, only works for themes that use a single simple feature type during import (like Protected Sites).
for (int i = 0; i < instances.length; ++ i) {
if (instances[i] instanceof PersistableFeature) {
((PersistableFeature)instances[i]).setId (feature.getId ());
}
outputStream.writeFeature (convert (instances[i]));
instances[i] = createInstance (mapping.objectClasses[i]);
}
}
// TODO: Finish writing instances.
}
@SuppressWarnings("unchecked")
private T convert (final Object feature) {
final PersistableFeature persistable = (PersistableFeature)feature;
return (T)persistable;
}
private MappingSource createMappingSource (final GenericFeature feature) {
return new MappingSource() {
@Override
public boolean hasAttribute (final String name) {
return feature.hasProperty (name);
}
@Override
public Object getAttributeValue (final String name) {
return feature.get (name);
}
};
}
private Object createInstance (final Class<?> cls) {
try {
return cls.newInstance ();
} catch (InstantiationException e) {
reportException (e);
throw new RuntimeException (e);
} catch (IllegalAccessException e) {
reportException (e);
throw new RuntimeException (e);
}
}
private void initializeInstances (final Object[] instances) {
for (int i = 0; i < instances.length; ++ i) {
try {
instances[i] = mapping.objectClasses[i].newInstance ();
} catch (InstantiationException e) {
reportException (e);
throw new RuntimeException (e);
} catch (IllegalAccessException e) {
reportException (e);
throw new RuntimeException (e);
}
}
}
private MappingDestination createMappingDestination (final MappingAttributeInfo attribute, final Object[] instances) {
final Method setterMethod = attribute.setterMethod;
final int index = attribute.objectIndex;
final MappingDestination destination = new MappingDestination() {
@Override
public void setValue (final Object value) {
try {
setterMethod.invoke (instances[index], value);
} catch (IllegalArgumentException e) {
reportException (e);
throw new RuntimeException (e);
} catch (IllegalAccessException e) {
reportException (e);
throw new RuntimeException (e);
} catch (InvocationTargetException e) {
reportException (e);
throw new RuntimeException (e);
}
}
};
// Convert code types:
if (attribute.attributeDescriptor.getAttributeType ().equals (CodeType.class) && attribute.attributeDescriptor.getCodeSpace () != null) {
final String codeSpace = attribute.attributeDescriptor.getCodeSpace ();
return new MappingDestination () {
@Override
public void setValue (final Object value) {
final CodeType codeType = (CodeType)value;
if (codeType != null && codeType.getCodeSpace () == null) {
final CodeType newCodeType = codeType != null ? new CodeType (codeType.getCode (), codeSpace) : null;
destination.setValue (newCodeType);
} else {
destination.setValue (codeType);
}
}
};
}
return destination;
}
private void reportException (final Exception e) {
final String message = e.getLocalizedMessage ();
logger.logEvent (job, MessageKey.ATTRIBUTE_MAPPING_TECHNICAL_ERROR, LogLevel.ERROR, message != null ? message : e.getClass ().getCanonicalName ());
}
private Mapping buildAttributeMappings () {
final Set<AttributeDescriptor<?>> attributeDescriptors = themeConfig.getAttributeDescriptors ();
final List<Class<?>> beanClasses = new ArrayList<Class<?>> ();
final List<MappingAttributeInfo> attributes = new ArrayList<AttributeMapper.MappingAttributeInfo> ();
for (final Map.Entry<AttributeDescriptor<?>, OperationDTO> entry: attributeMappings.entrySet ()) {
final ObjectDescriptor<?> objectDescriptor = entry.getKey ().getObjectDescriptor ();
// Locate the bean class:
final Class<?> objectClass = objectDescriptor.getObjectClass ();
int objectClassIndex = beanClasses.indexOf (objectClass);
if (objectClassIndex < 0) {
objectClassIndex = beanClasses.size ();
beanClasses.add (objectClass);
}
final MappingAttributeInfo attributeInfo = buildAttributeMapping (entry.getKey (), objectClassIndex, entry.getValue (), attributeDescriptors);
if (attributeInfo == null) {
continue;
}
attributes.add (attributeInfo);
}
if (attributes.size () != attributeMappings.size ()) {
return null;
}
return new Mapping (beanClasses.toArray (new Class<?>[beanClasses.size ()]), attributes.toArray (new MappingAttributeInfo[attributes.size ()]));
}
private MappingAttributeInfo buildAttributeMapping (final AttributeDescriptor<?> attributeDescriptor, final int objectIndex, final OperationDTO operation, final Set<AttributeDescriptor<?>> attributeDescriptors) {
if (!attributeDescriptors.contains (attributeDescriptor)) {
throw new IllegalStateException (String.format ("Attribute descriptor for %s not in theme %s", attributeDescriptor.getName (), themeConfig.getThemeName ()));
}
// Validate the attribute mapping:
final AttributeMappingValidator validator = new AttributeMappingValidator (attributeDescriptor, featureType, logger);
if (!validator.isValid (job, operation)) {
return null;
}
// Create the root operation:
final List<OperationInput> inputs = new ArrayList<OperationInput> ();
inputs.add (new OperationInput() {
@Override
public Operation getOperation() {
return operation;
}
});
final Operation rootOperation = new Operation() {
@Override
public OperationType getOperationType() {
return attributeDescriptor;
}
@Override
public Object getOperationProperties() {
return null;
}
@Override
public List<OperationInput> getInputs() {
return inputs;
}
};
// Create an executer for the attribute mapping:
final Executer executer;
try {
executer = new Executer (rootOperation, new MapperContext ());
} catch (MappingValidationException e) {
// Log a technical error if the executer fails to initialize:
logger.logEvent (job, MessageKey.ATTRIBUTE_MAPPING_TECHNICAL_ERROR, LogLevel.ERROR, e.getLocalizedMessage ());
return null;
} catch (OperationExecutionException e) {
logger.logEvent (job, MessageKey.ATTRIBUTE_MAPPING_TECHNICAL_ERROR, LogLevel.ERROR, e.getLocalizedMessage ());
return null;
}
return new MappingAttributeInfo (
attributeDescriptor,
objectIndex,
attributeDescriptor.getPropertyDescriptor ().getWriteMethod (),
executer
);
}
private static class Mapping {
public final Class<?>[] objectClasses;
public final MappingAttributeInfo[] attributes;
public Mapping (final Class<?>[] objectClasses, final MappingAttributeInfo[] attributes) {
this.objectClasses = Arrays.copyOf (objectClasses, objectClasses.length);
this.attributes = Arrays.copyOf (attributes, attributes.length);
}
}
private static class MappingAttributeInfo {
public final AttributeDescriptor<?> attributeDescriptor;
public final int objectIndex;
public final Method setterMethod;
public final Executer executer;
public MappingAttributeInfo (
final AttributeDescriptor<?> attributeDescriptor,
final int objectIndex,
final Method setterMethod,
final Executer executer) {
this.attributeDescriptor = attributeDescriptor;
this.setterMethod = setterMethod;
this.objectIndex = objectIndex;
this.executer = executer;
}
}
}