| /******************************************************************************** |
| * 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()); |
| } |
| } |