blob: 317598dafd90a7f9d257d54730bb542864f726d3 [file] [log] [blame]
/********************************************************************************
* Copyright (c) 2015-2018 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
********************************************************************************/
package org.eclipse.mdm.businessobjects.service;
import static io.vavr.API.$;
import static io.vavr.API.Case;
import static io.vavr.API.Tuple;
import static io.vavr.Predicates.instanceOf;
import static org.eclipse.mdm.businessobjects.boundary.ResourceConstants.ENTITYATTRIBUTE_NAME;
import static org.eclipse.mdm.businessobjects.boundary.ResourceConstants.ENTITYATTRIBUTE_NUMBER_OF_VALUES;
import static org.eclipse.mdm.businessobjects.utils.Decomposer.decompose;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.inject.Inject;
import org.eclipse.mdm.api.base.adapter.Attribute;
import org.eclipse.mdm.api.base.adapter.ChildrenStore;
import org.eclipse.mdm.api.base.adapter.EntityStore;
import org.eclipse.mdm.api.base.adapter.EntityType;
import org.eclipse.mdm.api.base.model.ContextComponent;
import org.eclipse.mdm.api.base.model.ContextRoot;
import org.eclipse.mdm.api.base.model.ContextType;
import org.eclipse.mdm.api.base.model.Deletable;
import org.eclipse.mdm.api.base.model.Entity;
import org.eclipse.mdm.api.base.model.EnumRegistry;
import org.eclipse.mdm.api.base.model.EnumerationValue;
import org.eclipse.mdm.api.base.model.Environment;
import org.eclipse.mdm.api.dflt.EntityManager;
import org.eclipse.mdm.api.dflt.model.CatalogAttribute;
import org.eclipse.mdm.api.dflt.model.CatalogComponent;
import org.eclipse.mdm.api.dflt.model.CatalogSensor;
import org.eclipse.mdm.api.dflt.model.EntityFactory;
import org.eclipse.mdm.api.dflt.model.TemplateAttribute;
import org.eclipse.mdm.api.dflt.model.TemplateComponent;
import org.eclipse.mdm.api.dflt.model.TemplateRoot;
import org.eclipse.mdm.api.dflt.model.TemplateSensor;
import org.eclipse.mdm.api.dflt.model.TemplateTest;
import org.eclipse.mdm.api.dflt.model.TemplateTestStepUsage;
import org.eclipse.mdm.api.dflt.model.ValueList;
import org.eclipse.mdm.api.dflt.model.ValueListValue;
import org.eclipse.mdm.businessobjects.control.I18NActivity;
import org.eclipse.mdm.businessobjects.control.MDMEntityAccessException;
import org.eclipse.mdm.businessobjects.control.NavigationActivity;
import org.eclipse.mdm.businessobjects.control.SearchActivity;
import org.eclipse.mdm.businessobjects.entity.MDMAttribute;
import org.eclipse.mdm.businessobjects.entity.MDMEntity;
import org.eclipse.mdm.businessobjects.entity.MDMEntityResponse;
import org.eclipse.mdm.businessobjects.entity.MDMRelation;
import org.eclipse.mdm.businessobjects.entity.MDMRelation.RelationType;
import org.eclipse.mdm.businessobjects.entity.SearchAttribute;
import org.eclipse.mdm.businessobjects.utils.Decomposer;
import org.eclipse.mdm.businessobjects.utils.EntityNotFoundException;
import org.eclipse.mdm.businessobjects.utils.ReflectUtil;
import org.eclipse.mdm.businessobjects.utils.RequestBody;
import org.eclipse.mdm.businessobjects.utils.Serializer;
import org.eclipse.mdm.businessobjects.utils.ServiceUtils;
import org.eclipse.mdm.connector.boundary.ConnectorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import io.vavr.API;
import io.vavr.CheckedFunction0;
import io.vavr.Function0;
import io.vavr.Tuple;
import io.vavr.Value;
import io.vavr.collection.HashMap;
import io.vavr.collection.List;
import io.vavr.collection.Map;
import io.vavr.collection.Seq;
import io.vavr.collection.Set;
import io.vavr.collection.Stream;
import io.vavr.control.Option;
import io.vavr.control.Try;
/**
* Class providing basic data access methods to {@link Entity}s.
*
* @author Alexander Nehmer, science+computing AG Tuebingen (Atos SE)
*
*/
@Stateless
public class EntityService {
@Inject
protected ConnectorService connectorService;
@EJB
private SearchActivity searchActivity;
@EJB
private NavigationActivity navigationActivity;
@EJB
private I18NActivity i18nActivity;
private static final Logger LOGGER = LoggerFactory.getLogger(EntityService.class);
/**
* Converts a {@code value} into a {@link Value}. If {@code value} is
* {@code null}, {@code Value.isEmpty() == true}.
*
* @param value the value to wrap in a {@link Value}
* @return the created {@link Value}
*/
// TODO anehmer on 2017-11-26: rename to toValue()?
public static <T> Value<T> V(T value) {
return Option.of(value);
}
/**
* Converts the given string values into a {@link Seq} of {@link Value}s
* wrapping the value.
*
* @param value the {@link Value}s to put in a {@link Seq}
* @return the created {@link Seq} of {@link Value}s
*/
public static Seq<String> SL(String... values) {
return List.of(values);
}
/**
* Converts the given string {@link Value}s into a {@link Seq} of string
* {@link Value}s.
*
* @param value the {@link Value}s to put in a {@link Seq}
* @return the created {@link Seq} of {@link Value}s
*/
@SafeVarargs
public static Seq<Value<String>> SL(Value<String>... values) {
return List.of(values);
}
/**
* Converts the given {@link Value}s into a {@link Seq} of {@link Value}s.
*
* @param value the {@link Value}s to put in a {@link Seq}
* @return the created {@link Seq} of {@link Value}s
*/
@SafeVarargs
public static Seq<Value<?>> L(Value<?>... values) {
return List.of(values);
}
/**
* @see #find(String, Class, Value, Value, Value...)
*/
public <T extends Entity> Try<T> find(String sourceName, Class<T> entityClass, String id) {
return find(sourceName, entityClass, id, (Value<ContextType>) null, null);
}
/**
* @see #find(String, Class, Value, Value, Value...)
*/
public <T extends Entity> Try<T> find(String sourceName, Class<T> entityClass, String id,
Seq<String> parentIdSuppliers) {
return find(sourceName, entityClass, id, (Value<ContextType>) null, parentIdSuppliers);
}
/**
* @see #find(String, Class, Value, Value, Value...)
*/
public <T extends Entity> Try<T> find(String sourceName, Class<T> entityClass, String id,
Value<ContextType> contextTypeSupplier) {
return find(sourceName, entityClass, id, contextTypeSupplier, null);
}
/**
* Returns the specified entity by given {@code entityClass} and given
* {@code id}. If the {@code entityClass} is either {@link CatalogAttribute},
* {@link TemplateCompont}, {@link TemplateAttributeAttribute} or
* {@link ContextComponent} the respective root entities
* {@link CatalogComponent}, {@link TemplateRoot} or {@link ContextRoot} are
* used to get the entity to find.
*
* @param sourceName name of the source (MDM {@link Environment} name)
* @param entityClass entityType
* @param id id of entity to find
* @param contextTypeSupplier {@link Value} with the contextType of entity to
* find. Can be {@code null} if {@code EntityType}
* has no {@code ContextType}.
* @param parentIds {@link Value}s with the id(s) of parent(s). For
* {@link CatalogAttribute} the parentId must be the
* id of the {@link CatalogComponent}, for
* {@link TemplateComponent} it must be the id of the
* {@link TemplateRoot}, for
* {@link TemplateAttribute} it must be the id of the
* {@link TemplateRoot} first and also the
* {@link TemplateComponent}, for a nested
* {@link TemplateComponent} it must be the id of the
* {@link TemplateRoot} first and the id of the
* parent {@link TemplateComponent} second, for a
* {@link TemplateAttribute} within a nested
* {@link TemplateComponent} it must be the id of the
* {@link TemplateRoot} first, the id of the parent
* {@link TemplateComponent} second and the id of the
* {@link TemplateComponent} last and for
* {@link ContextComponent} it must be the id of the
* {@link ContextRoot}.
* @return {@link Try} with the found entity
*/
// TODO anehmer on 2017-11-22: complete javadoc for ValueListValue and
// TemplateTestStepUsage
// TODO anehmer on 2017-11-22: add comment for parentIds for TplSensors and
// nested TplSensors as well as for TplSensorAttrs and nested TplSensorAttrs
@SuppressWarnings("unchecked")
public <T extends Entity> Try<T> find(String sourceName, Class<T> entityClass, String id,
Value<ContextType> contextTypeSupplier, Seq<String> parentIds) {
// validate parentIds count
Map<Class<?>, Integer> minParentsForEntity = HashMap.empty();
minParentsForEntity = minParentsForEntity.put(Tuple(CatalogAttribute.class, 1))
.put(Tuple(CatalogSensor.class, 1)).put(Tuple(TemplateComponent.class, 1))
.put(Tuple(TemplateAttribute.class, 2)).put(Tuple(TemplateSensor.class, 2))
.put(Tuple(ValueListValue.class, 1)).put(Tuple(TemplateTestStepUsage.class, 1));
// return failure if number of parentIds do not correspond with the minimu
// required by the entity type
Option<Integer> minParents = minParentsForEntity.get(entityClass);
// TODO anehmer on 2017-11-25: add entity types to message
if (minParents.isDefined() && (parentIds == null || minParents.get() > parentIds.size())) {
return Try.failure(new IllegalArgumentException("ParentId(s) of " + entityClass.getSimpleName()
+ " not set appropriately. Expected minimum: " + minParents.get()));
}
// if the find is contextType specific
if (contextTypeSupplier != null && !contextTypeSupplier.isEmpty()) {
if (entityClass.equals(CatalogAttribute.class)) {
// get CatalogAttribute from CatalogComponent
if (parentIds.size() == 1) {
return find(sourceName, CatalogComponent.class, parentIds.get(0), contextTypeSupplier)
.map(catComp -> (T) getChild(CatalogAttribute.class, id, catComp::getCatalogAttributes));
}
// get the CatalogAttribute from a CatalogSensor
else if (parentIds.size() == 2) {
return find(sourceName, CatalogSensor.class, parentIds.get(1), contextTypeSupplier,
parentIds.dropRight(1)).map(
catComp -> (T) getChild(CatalogAttribute.class, id, catComp::getCatalogAttributes));
}
}
// get CatalogSensor from CatalogComponent
else if (entityClass.equals(CatalogSensor.class)) {
return find(sourceName, CatalogComponent.class, parentIds.get(0), contextTypeSupplier)
.map(catComp -> (T) getChild(CatalogSensor.class, id, catComp::getCatalogSensors));
}
// get TemplateComponent from TemplateRoot or parent TemplateComponent(s)
else if (entityClass.equals(TemplateComponent.class)) {
// if nested TplComp has to be found
if (parentIds.size() > 1) {
return find(sourceName, TemplateComponent.class, parentIds.get(parentIds.size() - 1),
contextTypeSupplier, parentIds.dropRight(1))
.map(tplComp -> (T) getChild(TemplateComponent.class, id,
tplComp::getTemplateComponents));
}
// if non-nested TplComp has to be found: exit condition of recursive call
return find(sourceName, TemplateRoot.class, parentIds.get(0), contextTypeSupplier)
.map(tplRoot -> (T) getChild(TemplateComponent.class, id, tplRoot::getTemplateComponents));
}
// get TemplateAttributes from TemplateComponent
else if (entityClass.equals(TemplateAttribute.class)) {
Try<TemplateComponent> tplCompTry = find(sourceName, TemplateComponent.class,
parentIds.get(parentIds.size() - 1), contextTypeSupplier, parentIds.dropRight(1));
// if TemplateSensorAttribute has to be found
// TODO anehmer on 2019-11-18: another criteria to switch to TemplateSensor
// search should be used
if (!tplCompTry.isFailure()) {
return tplCompTry
.map(tplComp -> (T) getChild(TemplateAttribute.class, id, tplComp::getTemplateAttributes));
} else {
return find(sourceName, TemplateSensor.class, parentIds.get(parentIds.size() - 1),
contextTypeSupplier, parentIds.dropRight(1))
.map(tplComp -> (T) getChild(TemplateAttribute.class, id,
tplComp::getTemplateAttributes));
}
}
// get TemplateSensor from TemplateComponent
else if (entityClass.equals(TemplateSensor.class)) {
return find(sourceName, TemplateComponent.class, parentIds.get(parentIds.size() - 1),
contextTypeSupplier, parentIds.dropRight(1))
.map(tplComp -> (T) getChild(TemplateSensor.class, id, tplComp::getTemplateSensors));
}
// get ContextComponent from ContextRoot
else if (entityClass.equals(ContextComponent.class)) {
// TODO anehmer on 2017-11-09: implement (also for nested ContextComponents)
throw new RuntimeException("NOT IMPLEMENTED YET");
}
// get root nested entities (CatalogComponent, TemplateRoot, ContextRoot)
return getEntityManager(sourceName).mapTry(em -> em.load(entityClass, contextTypeSupplier.get(), id));
}
// get ValueListValue from ValueList
else if (entityClass.equals(ValueListValue.class)) {
return find(sourceName, ValueList.class, parentIds.get(0))
.map(valueList -> (T) getChild(ValueListValue.class, id, valueList::getValueListValues));
}
// get TemplateTestStepUsage from TemplateTest
else if (entityClass.equals(TemplateTestStepUsage.class)) {
return find(sourceName, TemplateTest.class, parentIds.get(0))
.map(tplTest -> (T) getChild(TemplateTestStepUsage.class, id, tplTest::getTemplateTestStepUsages));
}
// for all other cases
return getEntityManager(sourceName).map(em -> em.load(entityClass, id));
}
/**
* Gets the child with the given {@code childId} or an EntityNotFoundException
* if the child was not found
*
* @param childClass class of child to construct exception on failure
* @param childId supplier of the id of child to find
* @param childSupplier function that gets all children
* @return the found child
*/
private <T extends Entity> T getChild(Class<T> childClass, String childId,
Function0<java.util.List<T>> childSupplier) {
return Stream.ofAll(childSupplier.apply()).find(childEntity -> childEntity.getID().equals(childId))
.getOrElseThrow(() -> new EntityNotFoundException(childClass, childId));
}
/**
* Returns a {@link Try} of all {@link Entity}s if no filter is available.
*
* @param sourceName name of the source (MDM {@link Environment} name)
* @param entityClass class of the {@link Entity} to find
* @param filter filter string to filter the {@link Entity} result. Can be
* null.
* @return a {@link Try} of the list of found {@link Entity}s
*/
// TODO anehmer on 2017-11-26: make filter Value<String>
public <T extends Entity> Try<List<T>> findAll(String sourceName, Class<T> entityClass, String filter) {
return findAll(sourceName, entityClass, filter, null);
}
/**
* Returns a {@link Try} of the matching {@link Entity}s of the given
* contextType using the given filter or all {@link Entity}s of the given
* contextType provided by the {@code contextTypeSupplier} if no filter is
* available.
*
* @param sourceName name of the source (MDM {@link Environment} name)
* @param entityClass class of the {@link Entity} to find
* @param filter filter string to filter the {@link Entity} result
* @param contextTypeSupplier a {@link Value} with the contextType of entity to
* find. Can be {@code null} if {@code EntityType}
* has no {@code ContextType}.
* @return a {@link Try} of the list of found {@link Entity}s
*/
// TODO anehmer on 2017-11-26: make filter Value<String>
public <T extends Entity> Try<List<T>> findAll(String sourceName, Class<T> entityClass, String filter,
Value<ContextType> contextTypeSupplier) {
// TODO anehmer on 2017-11-22: do we need to implement the navigationActivity
// filter shortcut like in ChannelGroupService.getChannelGroups()
if (filter == null || filter.trim().length() <= 0) {
return Try
.of(getLoadAllEntitiesMethod(getEntityManager(sourceName).get(), entityClass, contextTypeSupplier))
.map(javaList -> List.ofAll(javaList));
} else {
// TODO anehmer on 2017-11-15: not tested
return Try.of(() -> this.searchActivity.search(connectorService.getContextByName(sourceName), entityClass,
filter)).map(javaList -> List.ofAll(javaList));
}
}
/**
* Returns the method to load all entities of type {@code entityClass}. If a
* {@code ContextType} is given the appropriate method in
* {@link org.eclipse.mdm.api.dflt.EntityManager} is used
*
* @param entityManager entityManager to load entities with
* @param entityClass class of entites to load
* @param contextType {@link ContextType} of entities of null if none
* @return the appropriate loadAllEntities() method
*/
private <T extends Entity> CheckedFunction0<java.util.List<T>> getLoadAllEntitiesMethod(EntityManager entityManager,
Class<T> entityClass, Value<ContextType> contextType) {
// if contextType is specified
if (contextType != null && !contextType.isEmpty()) {
return (() -> entityManager.loadAll(entityClass, contextType.get()));
}
return (() -> entityManager.loadAll(entityClass));
}
/**
* Creates a new {@link Entity} of type entityClass. The method searches the
* {@link EntityFactory} for a suitable create() method by matching the return
* parameter and the given entity class. If more than one method is found, the
* first one is taken. The argument are provided by {@link Try<Object>}s so that
* any exceptions thrown throughout retrieval will be wrapped in the returned
* {@link Try}.
*
* @param sourceName name of the source (MDM {@link Environment} name)
* @param entityClass class of the {@link Entity} to create
* @param arguments arguments for corresponding EntityFactory.createX()
* @return a {@link Try} with the created {@link Entity}
*/
@SuppressWarnings("unchecked")
public <T extends Entity> Try<T> create(String sourceName, Class<T> entityClass, Seq<Value<?>> argumentSuppliers) {
// get corresponding create method for entityClass from EntityFactory
return Try.of(() -> connectorService.getContextByName(sourceName).getEntityFactory())
.mapTry(factory -> (T) Stream.of(EntityFactory.class.getMethods())
// find method with the return type matching entityClass
.filter(m -> m.getReturnType().equals(entityClass))
.filter(m -> satisfiesParameters(m.getParameterTypes(),
argumentSuppliers.flatMap(s -> s.map(Object::getClass))))
.getOrElseThrow(
() -> new NoSuchMethodException("No matching create()-method found for EntityType "
+ entityClass.getSimpleName() + " taking the parameters "
+ argumentSuppliers.flatMap(s -> s.map(a -> a.getClass().getName()))
.collect(Collectors.joining(", "))))
// invoke with given arguments
.invoke(factory.get(), argumentSuppliers.map(Value::get).toJavaArray()))
.mapFailure(Case($(instanceOf(InvocationTargetException.class)),
t -> ((InvocationTargetException) t).getTargetException()), Case($(), t -> t))
.map(e -> (T) DataAccessHelper
.execute(getEntityManager(sourceName).get(), Tuple.of(e, DataAccessHelper.CREATE))
.toArray()[0]);
}
/**
* Checks if the given parameter types are all assignable from the required
* parameters
*
* @param parameterTypes
* @param requiredParameters
* @return true if the given parameter types are all assignable from the
* required parameters
*/
@VisibleForTesting
boolean satisfiesParameters(Class<?>[] parameterTypes, Seq<Class<?>> requiredParameters) {
boolean result = parameterTypes.length == requiredParameters.length();
result &= List.of(parameterTypes).zip(requiredParameters).filter(t -> !ReflectUtil.isAssignable(t._2(), t._1()))
.isEmpty();
return result;
}
/**
* Extract arguments from request defaulting to extract MDMEntity.name see also
* {@link #extractArgumentsFromRequestBody(String, MDMEntityResponse, List, List)}
*/
public Seq<Value<?>> extractArgumentsFromRequestBody(String sourceName, MDMEntityResponse body) {
return extractArgumentsFromRequestBody(sourceName, body, List.of("Name"), List.empty());
}
/**
* Extract arguments from request body to use for {@link EntityFactory} create
* methods. If and argument given by attributeArguments or relationArguments
* cannot be found, they are skipped.
*
* @param sourceName name of the source (MDM {@link Environment} name)
* @param body deserialized request to extract arguments from
* @param attributeArguments list of names of attributes to extract values from
* @param relationArguments classes of related entities to extract from
* @return the extracted arguments as a {@link Seq} of {@link Value}.
*/
public final Seq<Value<?>> extractArgumentsFromRequestBody(String sourceName, MDMEntityResponse body,
List<String> attributeArguments, List<Class<? extends Entity>> relationArguments) {
List<Try<?>> extractedArguments = List.empty();
// if there arguments to be extracted from MDMEntity.attributes
if (attributeArguments != null) {
// although the name of the entity to be created is an attribute, it could also
// be given as MDMEntity.name instead of MDMEntity.attributes("Name")
extractedArguments = extractedArguments.appendAll(
attributeArguments.filter(attrName -> attrName.equalsIgnoreCase("name")).map(attrName -> Try
.of(() -> decompose(body::getData).<MDMEntity>getAt(0).get(MDMEntity::getName)).get()));
extractedArguments = extractedArguments
// ignore name attr if already processed
.appendAll(attributeArguments.filter(attrName -> !attrName.equalsIgnoreCase("name"))//
.map(attrName -> Try.of(() -> //
decompose(body::getData) //
.<MDMEntity>getAt(0) //
.<MDMAttribute, String>getFiltered(MDMEntity::getAttributes, MDMAttribute::getName,
attrName) //
.map(attr -> {
Object value = attr.getValue().toString();
if (attr.getDataType().equals("ENUMERATION")) {
value = EnumRegistry.getInstance().get(EnumRegistry.VALUE_TYPE)
.valueOf(value.toString());
}
return value;
}).get())));
}
// wrap find in Try to handle non-existing relation (not all entityClasses must
// exist as a relation)
if (relationArguments != null) {
extractedArguments = extractedArguments
.appendAll(relationArguments.map(clazz -> Try.of(() -> find(sourceName, clazz, //
// get IDs of related entities
decompose(body::getData).<MDMEntity>getAt(0) //
.<MDMRelation, String>getFiltered(MDMEntity::getRelations,
MDMRelation::getEntityType, clazz.getSimpleName()) //
.get(MDMRelation::getIds).getValueAt(0),
// get ContextType; if not present, find() ignores null value
Try.of(() -> decompose(body::getData).<MDMEntity>getAt(0) //
.<MDMRelation, String>getFiltered(MDMEntity::getRelations,
MDMRelation::getEntityType, clazz.getSimpleName()) //
.get(MDMRelation::getContextType)).getOrElse((Decomposer<ContextType>) null)) //
.get())));
}
// log failures as warnings
extractedArguments.filter(Try::isFailure)
.forEach(failure -> LOGGER.warn("Argument to extract not found: {}", failure.getCause().getMessage()));
return extractedArguments.filter(Try::isSuccess).map(Try::toOption);
}
/**
* Updates the given {@link Entity} with the values of the given map provided by
* the {@code valueMapSupplier}.
*
* @param sourceName name of the source (MDM {@link Environment} name)
* @param entity the entity to update
* @param valueMapSupplier {@link Supplier<Map<String, Object>> of a map of
* values to update the entity with according to matching attribute values by
* name case sensitive
* @return a {@link Try} of the updated entity
*/
// TODO "anehmer" on 2019-03-29: remove method
@SuppressWarnings("unchecked")
public <T extends Entity> Try<T> update(String sourceName, Try<T> entity,
Value<Map<String, Object>> valueMapSupplier) {
// return updated entity
return
// update entity values
entity.map(e -> updateEntityValues(sourceName, e, valueMapSupplier.get()))
// persist entity
.map(e -> (T) DataAccessHelper
.execute(getEntityManager(sourceName).get(), Tuple.of(e.get(), DataAccessHelper.UPDATE))
.toArray()[0]);
}
/**
* Updates the given {@link Entity} with the values of the given
* {@link MDMEntity}
*
* @param sourceName name of the source (MDM {@link Environment} name)
* @param entity the entity to update
* @param mdmEntity the update source
* @return a {@link Try} of the updated entity
*/
@SuppressWarnings("unchecked")
public <T extends Entity> Try<T> update(String sourceName, Try<T> entity, MDMEntity mdmEntity) {
// return updated entity
return
// update entity values
entity.map(e -> updateEntity(e, mdmEntity))
// persist entity
.map(e -> (T) DataAccessHelper
.execute(getEntityManager(sourceName).get(), Tuple.of(e.get(), DataAccessHelper.UPDATE))
.toArray()[0]);
}
/**
* Deletes the given {@link Entity} {@code valueMapSupplier}.
*
* @param sourceName name of the source (MDM {@link Environment} name)
* @param entity the entity to delete
* @return a {@link Try} of the deleted entity
*/
@SuppressWarnings("unchecked")
public <T extends Entity> Try<T> delete(String sourceName, Try<T> entity) {
return entity.map(e -> (T) DataAccessHelper
.execute(getEntityManager(sourceName).get(), Tuple.of(e, DataAccessHelper.DELETE)).toArray()[0]);
}
/**
* Returns a {@link Try} of the the {@link SearchAttribute}s for the given
* entityClass
*
* @param sourceName name of the source (MDM {@link Environment} name)
* @param entityClass class of the {@link Entity} to get the
* {@link SearchAttribute}s for
*
* @return a {@link Try} with the {@link SearchAttribute}s
*/
public <T extends Entity> Try<List<SearchAttribute>> getSearchAttributesSupplier(String sourceName,
Class<T> entityClass) {
return Try.of(() -> List.ofAll(this.searchActivity
.listAvailableAttributes(connectorService.getContextByName(sourceName), entityClass)));
}
/**
* Returns a {@link Try} of the localized {@link Entity} type name
*
* @param sourceName name of the source (MDM {@link Environment} name)
* @param entityClass class of the {@link Entity} to be localized
*
* @return a {@link Try} with the localized {@link Entity} type name
*/
public <T extends Entity> Try<Map<EntityType, String>> getLocalizeTypeSupplier(String sourceName,
Class<T> entityClass) {
return Try.of(() -> HashMap.ofAll(this.i18nActivity.localizeType(sourceName, entityClass)));
}
/**
* Returns a {@link Try} of the localized {@link Entity} attributes
*
* @param sourceName name of the source (MDM {@link Environment} name)
* @param entityClass class of the {@link Entity} to be localized
* @return a {@link Try} with the the localized {@link Entity} attributes
*/
public <T extends Entity> Try<Map<Attribute, String>> getLocalizeAttributesSupplier(String sourceName,
Class<T> entityClass) {
return Try.of(() -> HashMap.ofAll(this.i18nActivity.localizeAttributes(sourceName, entityClass)));
}
/**
* Returns a {@link Try} of an {@link EnumerationValue} for the name supplied by
* the {@code enumValueNameSupplier}
*
* @param enumValueNameSupplier supplies the name of the
* {@link EnumerationValue} to get
* @return a {@link Try} with the resolved {@link EnumerationValue}
*/
public Try<EnumerationValue> getEnumerationValueSupplier(Try<?> enumValueNameSupplier) {
return Try.of(() -> EnumRegistry.getInstance().get(EnumRegistry.VALUE_TYPE)
.valueOf(enumValueNameSupplier.get().toString()));
}
/**
* Gets the EntityManager from the ConnectorService with the given source name
*
* @param sourceName the name of the datasource to get EntityManager for
* @return the found EntityManager. Throws {@link MDMEntityAccessException} if
* not found.
*/
public Try<EntityManager> getEntityManager(String sourceName) {
return Try.of(() -> this.connectorService.getContextByName(sourceName).getEntityManager()
.orElseThrow(() -> new MDMEntityAccessException("Entity manager not present")));
}
/**
* Updates the given {@link Entity} with the values from the given valueMap. All
* matching attributes (case sensitive) are updated as well as the referenced
* relations by the id of the given
* {@link org.eclipse.mdm.api.base.model.Entity} and the simple class name as
* the key (the data model attribute name is the reference, case sensitive).
*
* @param entity the entity to update
* @param valueMap values to update the entity with according to matching
* attribute names. The keys are compared case sensitive.
* @return a {@link Try} with the the updated entity
*/
// TODO "anehmer" on 2019-03-29: remove method
@SuppressWarnings("unchecked")
public <T extends Entity> Try<T> updateEntityValues(String sourceName, T entity, Map<String, Object> valueMap) {
HashMap<String, org.eclipse.mdm.api.base.model.Value> entityValues = HashMap.ofAll(entity.getValues());
// normalized value map (convert all attribute names to upper camel case)
valueMap = valueMap.bimap(k -> k.substring(0, 1).toUpperCase() + k.substring(1), v -> v);
// update primitive values where the key from the valueMap has a matching entity
// value and collect the updated keys
Set<String> updatedPrimitiveValues = valueMap
.filter((valueMapEntryKey, valueMapEntryValue) -> entityValues.containsKey(valueMapEntryKey)
&& !(valueMapEntryValue instanceof java.util.Map))
.map((entityValueEntryKey, entityValueEntryValue) -> {
entityValues.get(entityValueEntryKey)
.forEach(value -> Serializer.applyValue(value, entityValueEntryValue));
return Tuple.of(entityValueEntryKey, entityValueEntryValue);
}).keySet();
// update enumeration values
Set<String> updatedEnumerationValues = valueMap
.filter((valueMapEntryKey, valueMapEntryValue) -> entityValues.containsKey(valueMapEntryKey)
&& (valueMapEntryValue instanceof java.util.Map))
.map((entityValueEntryKey, entityValueEntryValue) -> {
entityValues.get(entityValueEntryKey).forEach(value -> {
// get key-value-pairs that identify the enum und enumValue
String enumName = ((java.util.Map<String, String>) entityValueEntryValue).get("Enumeration");
String enumValueName = ((java.util.Map<String, String>) entityValueEntryValue)
.get("EnumerationValue");
if (enumName == null || enumValueName == null) {
throw new IllegalArgumentException("EnumerationValue is set by providing a map "
+ "containing the keys 'Enumeration' and 'EnumerationValue' "
+ "and the respective names as the values");
}
// find enumeration and the enumeration value
Option.of(EnumRegistry.getInstance()
// get enum
.get(enumName)).onEmpty(() -> {
throw new IllegalArgumentException("Enumeration [" + enumName + "] not found");
})
// get enumValue
.map(enumeration -> enumeration.valueOf(enumValueName))
// if enumValue is not found, null is returned
.filter(Objects::nonNull).onEmpty(() -> {
throw new IllegalArgumentException("EnumerationValue [" + enumValueName
+ "] not found in Enumeration [" + enumName + "]");
})
// set enumValue
.map(enumValue -> {
value.set(enumValue);
return enumValue;
});
});
return Tuple.of(entityValueEntryKey, entityValueEntryValue);
}).keySet();
// update the relations and gather the updated keys
// use only those keys that have not been updated yet and can be resolved as
// class names. If so, try to update accordingly named relation with the entity
// found by its id given as the value
Set<String> updatedRelations = valueMap
.filter((valueMapEntryKey, valueMapEntryValue) -> !updatedPrimitiveValues.contains(valueMapEntryKey)
&& !updatedEnumerationValues.contains(valueMapEntryKey))
.filter((relatedEntityClassName, relatedEntityId) -> {
EntityStore store = ServiceUtils.getCore(entity).getMutableStore();
ContextType contextType = null;
// determine if class has a context type
for (ContextType ct : ContextType.values()) {
int index = relatedEntityClassName.toUpperCase().indexOf(ct.name());
if (index > 0) {
contextType = ct;
// cut out ContextType
relatedEntityClassName = relatedEntityClassName.substring(0, index)
+ relatedEntityClassName.substring(index + ct.name().length());
}
}
// to have final variables for Try
final String processedRelatedEntityClassName = relatedEntityClassName;
final ContextType contextTypeIfPresent = contextType;
// load class from model packages
Try<Class<Entity>> updateTry = Try
.of(() -> (Class<Entity>) Class
.forName("org.eclipse.mdm.api.base.model." + processedRelatedEntityClassName))
.orElse(Try.of(() -> (Class<Entity>) Class
.forName("org.eclipse.mdm.api.dflt.model." + processedRelatedEntityClassName)))
// update related entity by first finding the related entity by its id
// use find and store.set() with ContextType if needed
.andThenTry(entityClass -> {
if (contextTypeIfPresent == null) {
store.set(find(sourceName, entityClass, relatedEntityId.toString())
.onFailure(e -> LOGGER.error(e.getMessage())).get());
} else {
store.set(
find(sourceName, entityClass, relatedEntityId.toString(),
V(contextTypeIfPresent))
.onFailure(e -> LOGGER.error(e.getMessage())).get(),
contextTypeIfPresent);
}
}).onFailure(e -> LOGGER.error("Entity of type [" + processedRelatedEntityClassName
+ "] and ID " + relatedEntityId + " not found", e));
return updateTry.isSuccess() ? true : false;
}).keySet();
// return Try.Failure if there are keys that are not present in the entity and
// thus are not updated
String unmappedKeys = valueMap.filterKeys(key -> !updatedPrimitiveValues.contains(key)
&& !updatedEnumerationValues.contains(key) && !updatedRelations.contains(key)).map(Tuple::toString)
.collect(Collectors.joining(", "));
if (unmappedKeys != null && !unmappedKeys.isEmpty()) {
return Try.failure(
new IllegalArgumentException("ValueMap to update entity contains the following keys that either "
+ "have no match in the entity vaues or relations to update "
+ "or an error occurred while finding the related entity: " + unmappedKeys));
} else {
return Try.of(() -> entity);
}
}
/**
* Updates the given {@link Entity} with the values from the given
* {@link MDMEntity}. Values are ignored if value attribute is empty. Lists are
* fully replaced as no remove-from-related-entities-list-operation is
* available.
*
* @param entity the entity to update
* @param valueMap values to update the entity (attributes and related entities)
* according to matching MDMAttribute and MDMRelation data
* @return a {@link Try} with the the updated entity
*/
@SuppressWarnings("unchecked")
public <T extends Entity> Try<T> updateEntity(T entity, MDMEntity mdmEntity) {
// update attributes
// take all given attributes
return Try.of(() -> List.ofAll(mdmEntity.getAttributes())
// and filter out those which dont't have values
.filter(Objects::nonNull)
// update the single values of the entity
// call applyValue() on entity for every given attribute. If an error occurs
// the returned Try holds the Failure
.foldLeft(Try.of(() -> entity), //
(updateTry, attr) -> updateTry.andThen(() -> //
// encapsulate applyValue in a Try to get attribute specific error
Try.of(() -> {
Serializer.applyValue(entity.getValue(attr.getName()), attr.getValue());
return null;
}).mapFailure(
Case($(),
t -> new MDMEntityAccessException("Error while updating attribute ["
+ attr.getName() + "]: " + t.getMessage(), t)))
.get()))
// update relations
.transform(updateTry ->
// take all given relations
List.ofAll(mdmEntity.getRelations())
// call this::updateRelation with every relation on the entity
.foldLeft(updateTry, this::updateRelation))
.get());
}
/**
* Updates the entity's relation with the given relation
*
* @param entityTry entity to update
* @param relation relation to update corresponding entity relation with
*/
private <T extends Entity> Try<T> updateRelation(Try<T> entityTry, MDMRelation relation) {
return entityTry.map(e -> API.unchecked(() -> {
RelationType type = relation.getType();
if (type == RelationType.CHILDREN) {
Class<Deletable> entityClass = getEntityClass(relation.getEntityType());
ChildrenStore childrenStore = ServiceUtils.getCore(e).getChildrenStore();
List<Deletable> relatedEntities = List
// children can be empty
.ofAll(childrenStore.getCurrent().getOrDefault(entityClass, new ArrayList<Deletable>()));
return List.ofAll(relation.getIds())
// delete all entities not present in given relations
.transform(relationList -> {
relatedEntities.filter(relatedEntity -> !relationList.contains(relatedEntity.getID()))
.forEach(childrenStore::remove);
return relationList;
})
// process new related entities; get all ids not already in relatedEntities
.filter(id -> !relatedEntities.map(Entity::getID).contains(id))
// find entity for id
.foldLeft(entityTry,
// find the entity by the given id and the loaded class from the relation
(findTry, id) -> {
// use diffenrent find method if ContextType is present
Try.of(relation::getContextType)
// if ContextType is given, find with ContextType
.map(contextType -> find(e.getSourceName(), entityClass, id, V(contextType),
SL(e.getID())))
// find without contextType
.getOrElse(find(e.getSourceName(), entityClass, id, SL(e.getID())))
// add to children store
.andThen(childrenStore::add).get();
return entityTry;
});
} else if (type == RelationType.MUTABLE) {
EntityStore mutableStore = ServiceUtils.getCore(e).getMutableStore();
List<Entity> relatedEntities = List.ofAll(mutableStore.getCurrent())
.filter(re -> re.getTypeName().equals(relation.getEntityType()));
Class<Entity> entityClass = getEntityClass(relation.getEntityType());
return List.ofAll(relation.getIds())
// delete all entities not present in given relations
.transform(relationList -> {
relatedEntities.filter(relatedEntity -> !relationList.contains(relatedEntity.getID()))
.forEach(relatedEntity ->
// diff if ContextType
mutableStore.remove(relatedEntity.getClass()));
return relationList;
})
// process new related entities; get all ids not already in relatedEntities
.filter(id -> !relatedEntities.map(Entity::getID).contains(id))
// find entity for id
.foldLeft(entityTry,
// find the entity by the given id and the loaded class from the relation
(findTry, id) -> {
// safe use of get() as any exception thrown is covered by the enclosing Try
ContextType ct = relation.getContextType();
if (ct != null) {
mutableStore.set(find(e.getSourceName(), entityClass, id, V(ct)).get(), ct);
} else {
mutableStore.set(find(e.getSourceName(), entityClass, id).get());
}
return entityTry;
});
}
// return updated entity
return entityTry;
}).apply()).get();
}
@SuppressWarnings("unchecked")
private <T extends Entity> Class<T> getEntityClass(String type) throws ClassNotFoundException {
return Try.of(() -> (Class<T>) Class.forName("org.eclipse.mdm.api.base.model." + type))
.orElse(Try.of(() -> (Class<T>) Class.forName("org.eclipse.mdm.api.dflt.model." + type)))
.getOrElseThrow(e -> new ClassNotFoundException(
"No corresponding class with name [" + type + "] found in base.model or dflt.model"));
}
// TODO: Clarify which input should be handled.
// TODO: Improve error msg?
public Seq<Value<?>> extractRequestBody(String body, String sourceName,
List<Class<? extends Entity>> entityClasses) {
RequestBody requestBody = RequestBody.create(body);
List<Try<?>> name = List.of(requestBody.getStringValueSupplier(ENTITYATTRIBUTE_NAME));
// Mandatory in ChannelGroup.
Try<?> numberOfValues = requestBody.getStringValueSupplier(ENTITYATTRIBUTE_NUMBER_OF_VALUES)
.map(val -> Integer.parseInt(val));
if (numberOfValues.isSuccess()) {
name = name.append(numberOfValues);
}
return entityClasses
.map(clazz -> find(sourceName, clazz, requestBody.getStringValueSupplier(clazz.getSimpleName()).get()))
.foldLeft(name, (l, e) -> l.append(e)).filter(Try::isSuccess).map(Try::toOption);
}
public MDMEntity removeVirtualAttributes(MDMEntity entity) {
java.util.List<MDMAttribute> attributes = entity.getAttributes().stream()
.filter(attr -> !attr.getName().startsWith("@")).collect(Collectors.toList());
return new MDMEntity(entity.getName(), entity.getId(), entity.getType(), entity.getSourceType(),
entity.getSourceName(), attributes, entity.getRelations());
}
}