
io.github.wistefan.mapping.JavaObjectMapper Maven / Gradle / Ivy
package io.github.wistefan.mapping;
import io.github.wistefan.mapping.annotations.AttributeGetter;
import io.github.wistefan.mapping.annotations.AttributeType;
import io.github.wistefan.mapping.annotations.DatasetId;
import io.github.wistefan.mapping.annotations.EntityId;
import io.github.wistefan.mapping.annotations.EntityType;
import io.github.wistefan.mapping.annotations.RelationshipObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.fiware.ngsi.model.AdditionalPropertyVO;
import org.fiware.ngsi.model.EntityVO;
import org.fiware.ngsi.model.GeoPropertyVO;
import org.fiware.ngsi.model.PropertyListVO;
import org.fiware.ngsi.model.PropertyVO;
import org.fiware.ngsi.model.RelationshipListVO;
import org.fiware.ngsi.model.RelationshipVO;
import javax.inject.Singleton;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Mapper to handle translation from Java-Objects into NGSI-LD entities.
*/
@Slf4j
@Singleton
@RequiredArgsConstructor
public class JavaObjectMapper extends Mapper {
private static final String DEFAULT_CONTEXT = "https://smartdatamodels.org/context.jsonld";
public static final String NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE = "No mapping defined for method %s";
public static final String WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE = "Was not able invoke method %s on %s";
/**
* Translate the given object into an Entity.
*
* @param entity the object representing the entity
* @param class of the entity
* @return the NGIS-LD entity objet
*/
public EntityVO toEntityVO(T entity) {
isMappingEnabled(entity.getClass())
.orElseThrow(() -> new UnsupportedOperationException(String.format("Generic mapping to NGSI-LD entities is not supported for object %s", entity)));
List entityIdMethod = new ArrayList<>();
List entityTypeMethod = new ArrayList<>();
List propertyMethods = new ArrayList<>();
List propertyListMethods = new ArrayList<>();
List relationshipMethods = new ArrayList<>();
List relationshipListMethods = new ArrayList<>();
List geoPropertyMethods = new ArrayList<>();
List geoPropertyListMethods = new ArrayList<>();
Arrays.stream(entity.getClass().getMethods()).forEach(method -> {
if (isEntityIdMethod(method)) {
entityIdMethod.add(method);
} else if (isEntityTypeMethod(method)) {
entityTypeMethod.add(method);
} else {
getAttributeGetter(method.getAnnotations()).ifPresent(annotation -> {
switch (annotation.value()) {
case PROPERTY -> propertyMethods.add(method);
// We handle property lists the same way as properties, since it is mapped as a property which value is a json array.
// A real NGSI-LD property list would require a datasetId, that is not provided here.
case PROPERTY_LIST -> propertyMethods.add(method);
case GEO_PROPERTY -> geoPropertyMethods.add(method);
case RELATIONSHIP -> relationshipMethods.add(method);
case GEO_PROPERTY_LIST -> geoPropertyListMethods.add(method);
case RELATIONSHIP_LIST -> relationshipListMethods.add(method);
default -> throw new UnsupportedOperationException(String.format("Mapping target %s is not supported.", annotation.value()));
}
});
}
});
if (entityIdMethod.size() != 1) {
throw new MappingException(String.format("The provided object declares %s id methods, exactly one is expected.", entityIdMethod.size()));
}
if (entityTypeMethod.size() != 1) {
throw new MappingException(String.format("The provided object declares %s type methods, exactly one is expected.", entityTypeMethod.size()));
}
return buildEntity(entity, entityIdMethod.get(0), entityTypeMethod.get(0), propertyMethods, propertyListMethods, geoPropertyMethods, relationshipMethods, relationshipListMethods);
}
/**
* Build the entity from its declared methods.
*/
private EntityVO buildEntity(T entity, Method entityIdMethod, Method entityTypeMethod, List propertyMethods, List propertyListMethods, List geoPropertyMethods, List relationshipMethods, List relationshipListMethods) {
EntityVO entityVO = new EntityVO();
// TODO: Check if we need that configurable
entityVO.setAtContext(DEFAULT_CONTEXT);
// TODO: include extraction via annotation for all well-known attributes
entityVO.setOperationSpace(null);
entityVO.setObservationSpace(null);
entityVO.setLocation(null);
try {
Object entityIdObject = entityIdMethod.invoke(entity);
if (!(entityIdObject instanceof URI)) {
throw new MappingException(String.format("The entityId method does not return a valid URI for entity %s.", entity));
}
entityVO.id((URI) entityIdObject);
Object entityTypeObject = entityTypeMethod.invoke(entity);
if (!(entityTypeObject instanceof String)) {
throw new MappingException("The entityType method does not return a valid String.");
}
entityVO.setType((String) entityTypeObject);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, "unknown-method", entity), e);
}
Map additionalProperties = new LinkedHashMap<>();
additionalProperties.putAll(buildProperties(entity, propertyMethods));
additionalProperties.putAll(buildPropertyList(entity, propertyListMethods));
additionalProperties.putAll(buildGeoProperties(entity, geoPropertyMethods));
Map relationshipVOMap = buildRelationships(entity, relationshipMethods);
Map relationshipListVOMap = buildRelationshipList(entity, relationshipListMethods);
// we need to post-process the relationships, since orion-ld only accepts dataset-ids for lists > 1
relationshipVOMap.entrySet().stream().forEach(e -> e.getValue().setDatasetId(null));
relationshipListVOMap.entrySet().stream().forEach(e -> {
if (e.getValue().size() == 1) {
e.getValue().get(0).setDatasetId(null);
}
});
additionalProperties.putAll(relationshipVOMap);
additionalProperties.putAll(relationshipListVOMap);
additionalProperties.forEach(entityVO::setAdditionalProperties);
return entityVO;
}
/**
* Check if the given method defines the entity type
*/
private boolean isEntityTypeMethod(Method method) {
return Arrays.stream(method.getAnnotations()).anyMatch(EntityType.class::isInstance);
}
/**
* Check if the given method defines the entity id
*/
private boolean isEntityIdMethod(Method method) {
return Arrays.stream(method.getAnnotations()).anyMatch(EntityId.class::isInstance);
}
/**
* Build a relationship from the declared methods
*/
private Map buildRelationships(T entity, List relationshipMethods) {
return relationshipMethods.stream()
.map(method -> methodToRelationshipEntry(entity, method))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
/**
* Build a list of relationships from the declared methods
*/
private Map buildRelationshipList(T entity, List relationshipListMethods) {
return relationshipListMethods.stream()
.map(relationshipMethod -> methodToRelationshipListEntry(entity, relationshipMethod))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
/*
* Build a list of properties from the declared methods
*/
private Map buildPropertyList(T entity, List propertyListMethods) {
return propertyListMethods.stream()
.map(propertyListMethod -> methodToPropertyListEntry(entity, propertyListMethod))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
/**
* Build geoproperties from the declared methods
*/
private Map buildGeoProperties(T entity, List geoPropertyMethods) {
return geoPropertyMethods.stream()
.map(geoPropertyMethod -> methodToGeoPropertyEntry(entity, geoPropertyMethod))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
/**
* Build properties from the declared methods
*/
private Map buildProperties(T entity, List propertyMethods) {
return propertyMethods.stream()
.map(propertyMethod -> methodToPropertyEntry(entity, propertyMethod))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
/**
* Return method defining the object of the relationship for the given entity, if exists.
*/
private Optional getRelationshipObjectMethod(T entity) {
return Arrays.stream(entity.getClass().getMethods()).filter(this::isRelationShipObject).findFirst();
}
/**
* Return method defining the datasetid for the given entity, if exists.
*/
private Optional getDatasetIdMethod(T entity) {
return Arrays.stream(entity.getClass().getMethods()).filter(this::isDatasetId).findFirst();
}
/**
* Get all methods declared as attribute getters.
*/
private List getAttributeGettersMethods(T entity) {
return Arrays.stream(entity.getClass().getMethods()).filter(m -> getAttributeGetterAnnotation(m).isPresent()).toList();
}
/**
* return the {@link AttributeGetter} annotation for the method if there is such.
*/
private Optional getAttributeGetterAnnotation(Method m) {
return Arrays.stream(m.getAnnotations()).filter(AttributeGetter.class::isInstance).findFirst().map(AttributeGetter.class::cast);
}
/**
* Find the attribute getter from all the annotations.
*/
private Optional getAttributeGetter(Annotation[] annotations) {
return Arrays.stream(annotations).filter(AttributeGetter.class::isInstance).map(AttributeGetter.class::cast).findFirst();
}
/**
* Check if the given method is declared to be used as object of a relationship
*/
private boolean isRelationShipObject(Method m) {
return Arrays.stream(m.getAnnotations()).anyMatch(RelationshipObject.class::isInstance);
}
/**
* Check if the given method is declared to be used as datasetId
*/
private boolean isDatasetId(Method m) {
return Arrays.stream(m.getAnnotations()).anyMatch(DatasetId.class::isInstance);
}
/**
* Build a property entry from the given method on the entity
*/
private Optional> methodToPropertyEntry(T entity, Method method) {
try {
Object propertyObject = method.invoke(entity);
if (propertyObject == null) {
return Optional.empty();
}
AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(() -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
PropertyVO propertyVO = new PropertyVO();
propertyVO.setValue(propertyObject);
return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), propertyVO));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
}
}
/**
* Build a geo-property entry from the given method on the entity
*/
private Optional> methodToGeoPropertyEntry(T entity, Method method) {
try {
Object o = method.invoke(entity);
if (o == null) {
return Optional.empty();
}
AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(() -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
GeoPropertyVO geoPropertyVO = new GeoPropertyVO();
geoPropertyVO.setValue(o);
return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), geoPropertyVO));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
}
}
/**
* Build a relationship entry from the given method on the entity
*/
private Optional> methodToRelationshipEntry(T entity, Method method) {
try {
Object relationShipObject = method.invoke(entity);
if (relationShipObject == null) {
return Optional.empty();
}
RelationshipVO relationshipVO = getRelationshipVO(method, relationShipObject);
AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(() -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), relationshipVO));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
}
}
/**
* Build a relationship list entry from the given method on the entity
*/
private Optional> methodToRelationshipListEntry(T entity, Method method) {
try {
Object o = method.invoke(entity);
if (o == null) {
return Optional.empty();
}
if (!(o instanceof List)) {
throw new MappingException(String.format("Relationship list method %s::%s did not return a List.", entity, method));
}
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy