package org.molgenis.data.rest.service; import org.apache.commons.lang3.StringUtils; import org.molgenis.data.DataService; import org.molgenis.data.Entity; import org.molgenis.data.EntityManager; import org.molgenis.data.MolgenisDataException; import org.molgenis.data.meta.AttributeType; import org.molgenis.data.meta.model.Attribute; import org.molgenis.data.meta.model.EntityType; import org.molgenis.data.populate.IdGenerator; import org.molgenis.file.FileDownloadController; import org.molgenis.file.FileStore; import org.molgenis.file.model.FileMeta; import org.molgenis.file.model.FileMetaFactory; import org.molgenis.util.MolgenisDateFormat; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.IOException; import java.text.ParseException; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Stream; import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import static java.util.stream.StreamSupport.stream; import static org.molgenis.data.EntityManager.CreationMode.POPULATE; import static org.molgenis.data.meta.AttributeType.ONE_TO_MANY; import static org.molgenis.file.model.FileMetaMetaData.FILE_META; import static org.molgenis.util.MolgenisDateFormat.getDateFormat; import static org.molgenis.util.MolgenisDateFormat.getDateTimeFormat; @Service public class RestService { private final DataService dataService; private final IdGenerator idGenerator; private final FileStore fileStore; private final FileMetaFactory fileMetaFactory; private final EntityManager entityManager; @Autowired public RestService(DataService dataService, IdGenerator idGenerator, FileStore fileStore, FileMetaFactory fileMetaFactory, EntityManager entityManager) { this.dataService = requireNonNull(dataService); this.idGenerator = requireNonNull(idGenerator); this.fileStore = requireNonNull(fileStore); this.fileMetaFactory = requireNonNull(fileMetaFactory); this.entityManager = requireNonNull(entityManager); } /** * Creates a new entity based from a HttpServletRequest. For file attributes persists the file in the file store * and persist a file meta data entity. * * @param meta entity meta data * @param request HTTP request parameters * @return entity created from HTTP request parameters */ public Entity toEntity(final EntityType meta, final Map<String, Object> request) { final Entity entity = entityManager.create(meta, POPULATE); for (Attribute attr : meta.getAtomicAttributes()) { if (attr.getExpression() == null) { String paramName = attr.getName(); if (request.containsKey(paramName)) { final Object paramValue = request.get(paramName); final Object value = this.toEntityValue(attr, paramValue); entity.set(attr.getName(), value); } } } return entity; } /** * Converts a HTTP request parameter to a entity value of which the type is defined by the attribute. For file * attributes persists the file in the file store and persist a file meta data entity. * * @param attr attribute * @param paramValue HTTP parameter value * @return Object */ public Object toEntityValue(Attribute attr, Object paramValue) { // Treat empty strings as null if (paramValue != null && (paramValue instanceof String) && ((String) paramValue).isEmpty()) { paramValue = null; } Object value; AttributeType attrType = attr.getDataType(); switch (attrType) { case BOOL: value = convertBool(attr, paramValue); break; case EMAIL: case ENUM: case HTML: case HYPERLINK: case SCRIPT: case STRING: case TEXT: value = convertString(attr, paramValue); break; case CATEGORICAL: case XREF: value = convertRef(attr, paramValue); break; case CATEGORICAL_MREF: case MREF: case ONE_TO_MANY: value = convertMref(attr, paramValue); break; case DATE: value = convertDate(attr, paramValue); break; case DATE_TIME: value = convertDateTime(attr, paramValue); break; case DECIMAL: value = convertDecimal(attr, paramValue); break; case FILE: value = convertFile(attr, paramValue); break; case INT: value = convertInt(attr, paramValue); break; case LONG: value = convertLong(attr, paramValue); break; case COMPOUND: throw new RuntimeException(format("Illegal attribute type [%s]", attrType.toString())); default: throw new RuntimeException(format("Unknown attribute type [%s]", attrType.toString())); } return value; } private static Long convertLong(Attribute attr, Object paramValue) { Long value; if (paramValue != null) { if (paramValue instanceof String) { value = Long.valueOf((String) paramValue); } // javascript number converted to double else if (paramValue instanceof Number) { value = ((Number) paramValue).longValue(); } else { throw new MolgenisDataException( format("Attribute [%s] value is of type [%s] instead of [%s] or [%s]", attr.getName(), paramValue.getClass().getSimpleName(), String.class.getSimpleName(), Number.class.getSimpleName())); } } else { value = null; } return value; } private static Integer convertInt(Attribute attr, Object paramValue) { Integer value; if (paramValue != null) { if (paramValue instanceof String) { value = Integer.valueOf((String) paramValue); } // javascript number converted to double else if ((paramValue instanceof Number)) { value = ((Number) paramValue).intValue(); } else { throw new MolgenisDataException( format("Attribute [%s] value is of type [%s] instead of [%s] or [%s]", attr.getName(), paramValue.getClass().getSimpleName(), String.class.getSimpleName(), Number.class.getSimpleName())); } } else { value = null; } return value; } private FileMeta convertFile(Attribute attr, Object paramValue) { FileMeta value; if (paramValue != null) { if (!(paramValue instanceof MultipartFile)) { throw new MolgenisDataException( format("Attribute [%s] value is of type [%s] instead of [%s]", attr.getName(), paramValue.getClass().getSimpleName(), MultipartFile.class.getSimpleName())); } MultipartFile multipartFile = (MultipartFile) paramValue; String id = idGenerator.generateId(); try { fileStore.store(multipartFile.getInputStream(), id); } catch (IOException e) { throw new MolgenisDataException(e); } FileMeta fileEntity = fileMetaFactory.create(id); fileEntity.setFilename(multipartFile.getOriginalFilename()); fileEntity.setContentType(multipartFile.getContentType()); fileEntity.setSize(multipartFile.getSize()); fileEntity.setUrl(ServletUriComponentsBuilder.fromCurrentRequest() .replacePath(FileDownloadController.URI + '/' + id).replaceQuery(null).build().toUriString()); dataService.add(FILE_META, fileEntity); value = fileEntity; } else { value = null; } return value; } private static Double convertDecimal(Attribute attr, Object paramValue) { Double value; if (paramValue != null) { if (paramValue instanceof String) { value = Double.valueOf((String) paramValue); } // javascript number converted to double else if (paramValue instanceof Number) { value = ((Number) paramValue).doubleValue(); } else { throw new MolgenisDataException( format("Attribute [%s] value is of type [%s] instead of [%s] or [%s]", attr.getName(), paramValue.getClass().getSimpleName(), String.class.getSimpleName(), Number.class.getSimpleName())); } } else { value = null; } return value; } private static Date convertDateTime(Attribute attr, Object paramValue) { Date value; if (paramValue != null) { if (paramValue instanceof Date) { value = (Date) paramValue; } else if (paramValue instanceof String) { String paramStrValue = (String) paramValue; try { value = getDateTimeFormat().parse(paramStrValue); } catch (ParseException e) { throw new MolgenisDataException( format("Attribute [%s] value [%s] does not match date format [%s]", attr.getName(), paramStrValue, MolgenisDateFormat.getDateTimeFormat().toPattern())); } } else { throw new MolgenisDataException( format("Attribute [%s] value is of type [%s] instead of [%s] or [%s]", attr.getName(), paramValue.getClass().getSimpleName(), String.class.getSimpleName(), Date.class.getSimpleName())); } } else { value = null; } return value; } private static Date convertDate(Attribute attr, Object paramValue) { Date value; if (paramValue != null) { if (paramValue instanceof Date) { value = (Date) paramValue; } else if (paramValue instanceof String) { String paramStrValue = (String) paramValue; try { value = getDateFormat().parse(paramStrValue); } catch (ParseException e) { throw new MolgenisDataException( format("Attribute [%s] value [%s] does not match date format [%s]", attr.getName(), paramStrValue, MolgenisDateFormat.getDateFormat().toPattern())); } } else { throw new MolgenisDataException( format("Attribute [%s] value is of type [%s] instead of [%s]", attr.getName(), paramValue.getClass().getSimpleName(), String.class.getSimpleName())); } } else { value = null; } return value; } private List<?> convertMref(Attribute attr, Object paramValue) { List<?> value; if (paramValue != null) { List<?> mrefParamValues; if (paramValue instanceof String) { mrefParamValues = asList(StringUtils.split((String) paramValue, ',')); } else if (paramValue instanceof List<?>) { mrefParamValues = (List<?>) paramValue; } else { throw new MolgenisDataException( format("Attribute [%s] value is of type [%s] instead of [%s] or [%s]", attr.getName(), paramValue.getClass().getSimpleName(), String.class.getSimpleName(), List.class.getSimpleName())); } EntityType mrefEntity = attr.getRefEntity(); Attribute mrefEntityIdAttr = mrefEntity.getIdAttribute(); value = mrefParamValues.stream().map(mrefParamValue -> toEntityValue(mrefEntityIdAttr, mrefParamValue)) .map(mrefIdValue -> entityManager.getReference(mrefEntity, mrefIdValue)).collect(toList()); } else { value = emptyList(); } return value; } private Object convertRef(Attribute attr, Object paramValue) { Object value; if (paramValue != null) { Object idValue = toEntityValue(attr.getRefEntity().getIdAttribute(), paramValue); value = entityManager.getReference(attr.getRefEntity(), idValue); } else { value = null; } return value; } private static String convertString(Attribute attr, Object paramValue) { String value; if (paramValue != null) { if (paramValue instanceof String) { value = (String) paramValue; } else { throw new MolgenisDataException( format("Attribute [%s] value is of type [%s] instead of [%s]", attr.getName(), paramValue.getClass().getSimpleName(), String.class.getSimpleName())); } } else { value = null; } return value; } private static Boolean convertBool(Attribute attr, Object paramValue) { Boolean value; if (paramValue != null) { if (paramValue instanceof String) { value = Boolean.valueOf((String) paramValue); } else if (paramValue instanceof Boolean) { value = (Boolean) paramValue; } else { throw new MolgenisDataException( format("Attribute [%s] value is of type [%s] instead of [%s] or [%s]", attr.getName(), paramValue.getClass().getSimpleName(), String.class.getSimpleName(), Boolean.class.getSimpleName())); } } else { // boolean false is not posted (http feature), so if null and required, should be false value = !attr.isNillable() ? false : null; } return value; } /** * For entities with attributes that are part of a bidirectional relationship update the other side of the relationship. * * @param entity created entity */ public void updateMappedByEntities(@Nonnull Entity entity) { updateMappedByEntities(entity, null); } /** * For entities with attributes that are part of a bidirectional relationship update the other side of the relationship. * * @param entity created or updated entity * @param existingEntity existing entity */ public void updateMappedByEntities(@Nonnull Entity entity, @Nullable Entity existingEntity) { entity.getEntityType().getMappedByAttributes().forEach(mappedByAttr -> { AttributeType type = mappedByAttr.getDataType(); switch (type) { case ONE_TO_MANY: updateMappedByEntitiesOneToMany(entity, existingEntity, mappedByAttr); break; default: throw new RuntimeException( format("Attribute [%s] of type [%s] can't be mapped by another attribute", mappedByAttr.getName(), type.toString())); } }); } /** * For entities with the given attribute that is part of a bidirectional one-to-many relationship update the other side of the relationship. * * @param entity created or updated entity * @param existingEntity existing entity * @param attr bidirectional one-to-many attribute */ private void updateMappedByEntitiesOneToMany(@Nonnull Entity entity, @Nullable Entity existingEntity, @Nonnull Attribute attr) { if (attr.getDataType() != ONE_TO_MANY || !attr.isMappedBy()) { throw new IllegalArgumentException( format("Attribute [%s] is not of type [%s] or not mapped by another attribute", attr.getName(), attr.getDataType().toString())); } // update ref entities of created/updated entity Attribute refAttr = attr.getMappedBy(); Stream<Entity> stream = stream(entity.getEntities(attr.getName()).spliterator(), false); if (existingEntity != null) { // filter out unchanged ref entities Set<Object> refEntityIds = stream(existingEntity.getEntities(attr.getName()).spliterator(), false) .map(Entity::getIdValue).collect(toSet()); stream = stream.filter(refEntity -> !refEntityIds.contains(refEntity.getIdValue())); } List<Entity> updatedRefEntities = stream.map(refEntity -> { if (refEntity.getEntity(refAttr.getName()) != null) { throw new MolgenisDataException( format("Updating [%s] with id [%s] not allowed: [%s] is already referred to by another [%s]", attr.getRefEntity().getName(), refEntity.getIdValue().toString(), refAttr.getName(), entity.getEntityType().getName())); } refEntity.set(refAttr.getName(), entity); return refEntity; }).collect(toList()); // update ref entities of existing entity if (existingEntity != null) { Set<Object> refEntityIds = stream(entity.getEntities(attr.getName()).spliterator(), false) .map(Entity::getIdValue).collect(toSet()); List<Entity> updatedRefEntitiesExistingEntity = stream( existingEntity.getEntities(attr.getName()).spliterator(), false) .filter(refEntity -> !refEntityIds.contains(refEntity.getIdValue())).map(refEntity -> { refEntity.set(refAttr.getName(), null); return refEntity; }).collect(toList()); updatedRefEntities = Stream.concat(updatedRefEntities.stream(), updatedRefEntitiesExistingEntity.stream()) .collect(toList()); } if (!updatedRefEntities.isEmpty()) { dataService.update(attr.getRefEntity().getName(), updatedRefEntities.stream()); } } }