Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
package io.github.wistefan.mapping;
import io.github.wistefan.mapping.annotations.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.fiware.ngsi.model.*;
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.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Mapper to handle translation from Java-Objects into NGSI-LD entities.
*/
@Slf4j
@Singleton
@RequiredArgsConstructor
public class JavaObjectMapper extends Mapper {
// name of the property containing the ID
private static final String ID_PROPERTY = "id";
private final MappingProperties mappingProperties;
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 attribute path for the given object into the path in the ngsi-ld model.
*
* @param attributePath the original path
* @param tClass class to use for translation
* @return the path in ngsi-ld and the type of the target attribute
*/
public static NgsiLdAttribute getNGSIAttributePath(List attributePath, Class tClass) {
List ngsiAttributePath = new ArrayList<>();
QueryAttributeType type = QueryAttributeType.STRING;
String currentAttribute = attributePath.get(0);
if(currentAttribute.equals(ID_PROPERTY)) {
ngsiAttributePath.add(ID_PROPERTY);
return new NgsiLdAttribute(ngsiAttributePath, QueryAttributeType.STRING);
}
if (isMappingEnabled(tClass).isPresent()) {
// for mapped properties, we have to climb down the property names
// we need the setter in case of type-erasure and the getter in all other cases
Optional setter = getSetterMethodByName(tClass, currentAttribute)
.filter(m -> getAttributeSetter(m.getAnnotations()).isPresent())
.findFirst();
Optional getter = getGetterMethodByName(tClass, currentAttribute)
.filter(m -> getAttributeGetter(m.getAnnotations()).isPresent())
.findFirst();
if (setter.isPresent() && getter.isPresent()) {
Method setterMethod = setter.get();
Method getterMethod = getter.get();
// no need to check again
AttributeSetter setterAnnotation = getAttributeSetter(setterMethod.getAnnotations()).get();
ngsiAttributePath.add(setterAnnotation.targetName());
type = fromClass(getterMethod.getReturnType());
if (attributePath.size() > 1) {
List subPaths = attributePath.subList(1, attributePath.size());
if (setterAnnotation.targetClass() != Object.class) {
NgsiLdAttribute subAttribute = getNGSIAttributePath(subPaths, setterAnnotation.targetClass());
ngsiAttributePath.addAll(subAttribute.path());
type = subAttribute.type();
} else {
NgsiLdAttribute subAttribute = getNGSIAttributePath(subPaths, getterMethod.getReturnType());
ngsiAttributePath.addAll(subAttribute.path());
type = subAttribute.type();
}
}
} else {
log.warn("No corresponding field does exist for attribute {} on {}.", currentAttribute,
tClass.getCanonicalName());
}
} else {
// we can use the "plain" object field-names, no additional mapping happens anymore
ngsiAttributePath.addAll(attributePath);
type = evaluateType(attributePath, tClass);
}
return new NgsiLdAttribute(ngsiAttributePath, type);
}
private static QueryAttributeType evaluateType(List path, Class> tClass) {
Class> currentClass = tClass;
for (String s : path) {
try {
Optional> optionalReturn = getGetterMethodByName(currentClass, s).findAny()
.map(Method::getReturnType);
if (optionalReturn.isPresent()) {
currentClass = optionalReturn.get();
} else {
currentClass = currentClass.getField(s).getType();
}
} catch (NoSuchFieldException e) {
throw new MappingException(String.format("No field %s exists for %s.", s, tClass.getCanonicalName()),
e);
}
}
return fromClass(currentClass);
}
private static QueryAttributeType fromClass(Class> tClass) {
if (Number.class.isAssignableFrom(tClass)) {
return QueryAttributeType.NUMBER;
} else if (Boolean.class.isAssignableFrom(tClass)) {
return QueryAttributeType.BOOLEAN;
}
return QueryAttributeType.STRING;
}
public static Stream getSetterMethodByName(Class tClass, String propertyName) {
return Arrays.stream(tClass.getMethods())
.filter(m -> getCorrespondingSetterFieldName(m.getName()).equals(propertyName));
}
public static Stream getGetterMethodByName(Class tClass, String propertyName) {
return Arrays.stream(tClass.getMethods())
.filter(m -> getCorrespondingGetterFieldName(m.getName()).equals(propertyName));
}
private static String getCorrespondingGetterFieldName(String methodName) {
var fieldName = "";
if (methodName.matches("^get[A-Z].*")) {
fieldName = methodName.replaceFirst("get", "");
} else if (methodName.matches("^is[A-Z].*")) {
fieldName = methodName.replaceFirst("is", "");
} else {
log.debug("The method {} is neither a get or is.", methodName);
return fieldName;
}
return fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
}
private static String getCorrespondingSetterFieldName(String methodName) {
var fieldName = "";
if (methodName.matches("^set[A-Z].*")) {
fieldName = methodName.replaceFirst("set", "");
} else if (methodName.matches("^is[A-Z].*")) {
fieldName = methodName.replaceFirst("is", "");
} else {
log.debug("The method {} is neither a set or is.", methodName);
return fieldName;
}
return fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
}
/**
* Translate the given object into an Entity.
*
* @param entity the object representing the entity
* @param class of the entity
* @return the NGSI-LD entity object
*/
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();
entityVO.setAtContext(mappingProperties.getContextUrl());
// 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 <
T> 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 static Optional getAttributeGetter(Annotation[] annotations) {
return Arrays.stream(annotations).filter(AttributeGetter.class::isInstance).map(AttributeGetter.class::cast)
.findFirst();
}
/**
* Find the attribute setter from all the annotations.
*/
private static Optional getAttributeSetter(Annotation[] annotations) {
return Arrays.stream(annotations).filter(AttributeSetter.class::isInstance).map(AttributeSetter.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 <
T> 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