org.molgenis.api.data.v1.RestController Maven / Gradle / Ivy
package org.molgenis.api.data.v1;
import static java.util.Objects.requireNonNull;
import static org.molgenis.data.meta.AttributeType.CATEGORICAL;
import static org.molgenis.data.meta.AttributeType.CATEGORICAL_MREF;
import static org.molgenis.data.meta.AttributeType.COMPOUND;
import static org.molgenis.data.meta.AttributeType.DATE;
import static org.molgenis.data.meta.AttributeType.DATE_TIME;
import static org.molgenis.data.meta.AttributeType.FILE;
import static org.molgenis.data.meta.AttributeType.MREF;
import static org.molgenis.data.meta.AttributeType.ONE_TO_MANY;
import static org.molgenis.data.meta.AttributeType.XREF;
import static org.molgenis.data.meta.model.AttributeMetadata.ATTRIBUTE_META_DATA;
import static org.molgenis.data.security.auth.UserMetadata.USER;
import static org.molgenis.data.util.EntityUtils.getTypedValue;
import static org.molgenis.security.core.runas.RunAsSystemAspect.runAsSystem;
import static org.molgenis.security.twofactor.auth.TwoFactorAuthenticationSetting.ENABLED;
import static org.molgenis.security.twofactor.auth.TwoFactorAuthenticationSetting.ENFORCED;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.NO_CONTENT;
import static org.springframework.http.HttpStatus.OK;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import cz.jirutka.rsql.parser.RSQLParserException;
import io.micrometer.core.annotation.Timed;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import org.apache.commons.lang3.StringUtils;
import org.molgenis.api.ApiNamespace;
import org.molgenis.api.data.RestService;
import org.molgenis.data.DataService;
import org.molgenis.data.Entity;
import org.molgenis.data.EntityCollection;
import org.molgenis.data.Fetch;
import org.molgenis.data.MolgenisDataAccessException;
import org.molgenis.data.MolgenisDataException;
import org.molgenis.data.Query;
import org.molgenis.data.QueryRule;
import org.molgenis.data.Repository;
import org.molgenis.data.Sort;
import org.molgenis.data.UnknownAttributeException;
import org.molgenis.data.UnknownEntityException;
import org.molgenis.data.UnknownEntityTypeException;
import org.molgenis.data.meta.AttributeType;
import org.molgenis.data.meta.model.Attribute;
import org.molgenis.data.meta.model.EntityType;
import org.molgenis.data.security.auth.User;
import org.molgenis.data.security.auth.UserMetadata;
import org.molgenis.data.support.DefaultEntityCollection;
import org.molgenis.data.support.QueryImpl;
import org.molgenis.data.validation.MolgenisValidationException;
import org.molgenis.security.core.UserPermissionEvaluator;
import org.molgenis.security.core.token.TokenService;
import org.molgenis.security.core.token.UnknownTokenException;
import org.molgenis.security.settings.AuthenticationSettings;
import org.molgenis.security.token.TokenParam;
import org.molgenis.security.user.UserAccountService;
import org.molgenis.validation.ConstraintViolation;
import org.molgenis.web.ErrorMessageResponse;
import org.molgenis.web.ErrorMessageResponse.ErrorMessage;
import org.molgenis.web.rsql.MolgenisRSQL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.convert.ConversionException;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
/**
* Rest endpoint for the DataService
*
* Query, create, update and delete entities.
*
*
If a repository isn't capable of doing the requested operation an error is thrown.
*
*
Response is json.
*
* @author erwin
*/
@org.springframework.web.bind.annotation.RestController
@RequestMapping(RestController.BASE_URI)
@Timed(value = "rest.v1", description = "Timing information for the REST API v1.", histogram = true)
public class RestController {
private static final Logger LOG = LoggerFactory.getLogger(RestController.class);
static final String API_VERSION = "v1";
static final String BASE_URI = ApiNamespace.API_PATH + '/' + API_VERSION;
private static final Pattern PATTERN_EXPANDS = Pattern.compile("([^\\[^\\]]+)(?:\\[(.+)\\])?");
private final AuthenticationSettings authenticationSettings;
private final DataService dataService;
private final TokenService tokenService;
private final AuthenticationManager authenticationManager;
private final UserPermissionEvaluator permissionService;
private final UserAccountService userAccountService;
private final MolgenisRSQL molgenisRSQL;
private final RestService restService;
public RestController(
AuthenticationSettings authenticationSettings,
DataService dataService,
TokenService tokenService,
AuthenticationManager authenticationManager,
UserPermissionEvaluator permissionService,
UserAccountService userAccountService,
MolgenisRSQL molgenisRSQL,
RestService restService) {
this.authenticationSettings = requireNonNull(authenticationSettings);
this.dataService = requireNonNull(dataService);
this.tokenService = requireNonNull(tokenService);
this.authenticationManager = requireNonNull(authenticationManager);
this.userAccountService = requireNonNull(userAccountService);
this.permissionService = requireNonNull(permissionService);
this.molgenisRSQL = requireNonNull(molgenisRSQL);
this.restService = requireNonNull(restService);
}
/** Checks if an entity exists. */
@GetMapping(value = "/{entityTypeId}/exist", produces = APPLICATION_JSON_VALUE)
public boolean entityExists(@PathVariable("entityTypeId") String entityTypeId) {
return dataService.hasRepository(entityTypeId);
}
/**
* Gets the metadata for an entity
*
*
Example url: /api/v1/person/meta
*
* @return EntityType
*/
@GetMapping(value = "/{entityTypeId}/meta", produces = APPLICATION_JSON_VALUE)
public EntityTypeResponse retrieveEntityType(
@PathVariable("entityTypeId") String entityTypeId,
@RequestParam(value = "attributes", required = false) String[] attributes,
@RequestParam(value = "expand", required = false) String[] attributeExpands) {
Set attributeSet = toAttributeSet(attributes);
Map> attributeExpandSet = toExpandMap(attributeExpands);
EntityType meta = dataService.getEntityType(entityTypeId);
return new EntityTypeResponse(
meta, attributeSet, attributeExpandSet, permissionService, dataService);
}
/**
* Same as retrieveEntityType (GET) only tunneled through POST.
*
* Example url: /api/v1/person/meta?_method=GET
*
* @return EntityType
*/
@PostMapping(
value = "/{entityTypeId}/meta",
params = "_method=GET",
produces = APPLICATION_JSON_VALUE)
public EntityTypeResponse retrieveEntityTypePost(
@PathVariable("entityTypeId") String entityTypeId,
@Valid @RequestBody EntityTypeRequest request) {
Set attributesSet = toAttributeSet(request != null ? request.getAttributes() : null);
Map> attributeExpandSet =
toExpandMap(request != null ? request.getExpand() : null);
EntityType meta = dataService.getEntityType(entityTypeId);
return new EntityTypeResponse(
meta, attributesSet, attributeExpandSet, permissionService, dataService);
}
/**
* Example url: /api/v1/person/meta/emailaddresses
*
* @return EntityType
*/
@GetMapping(value = "/{entityTypeId}/meta/{attributeName}", produces = APPLICATION_JSON_VALUE)
public AttributeResponse retrieveEntityAttributeMeta(
@PathVariable("entityTypeId") String entityTypeId,
@PathVariable("attributeName") String attributeName,
@RequestParam(value = "attributes", required = false) String[] attributes,
@RequestParam(value = "expand", required = false) String[] attributeExpands) {
Set attributeSet = toAttributeSet(attributes);
Map> attributeExpandSet = toExpandMap(attributeExpands);
return getAttributePostInternal(entityTypeId, attributeName, attributeSet, attributeExpandSet);
}
/**
* Same as retrieveEntityAttributeMeta (GET) only tunneled through POST.
*
* @return EntityType
*/
@PostMapping(
value = "/{entityTypeId}/meta/{attributeName}",
params = "_method=GET",
produces = APPLICATION_JSON_VALUE)
public AttributeResponse retrieveEntityAttributeMetaPost(
@PathVariable("entityTypeId") String entityTypeId,
@PathVariable("attributeName") String attributeName,
@Valid @RequestBody EntityTypeRequest request) {
Set attributeSet = toAttributeSet(request != null ? request.getAttributes() : null);
Map> attributeExpandSet =
toExpandMap(request != null ? request.getExpand() : null);
return getAttributePostInternal(entityTypeId, attributeName, attributeSet, attributeExpandSet);
}
/**
* Get's an entity by it's id
*
* Examples:
*
*
/api/v1/person/99 Retrieves a person with id 99
*/
@GetMapping(value = "/{entityTypeId}/{id}", produces = APPLICATION_JSON_VALUE)
public Map retrieveEntity(
@PathVariable("entityTypeId") String entityTypeId,
@PathVariable("id") String untypedId,
@RequestParam(value = "attributes", required = false) String[] attributes,
@RequestParam(value = "expand", required = false) String[] attributeExpands) {
Set attributesSet = toAttributeSet(attributes);
Map> attributeExpandSet = toExpandMap(attributeExpands);
EntityType meta = dataService.getEntityType(entityTypeId);
Object id = getTypedValue(untypedId, meta.getIdAttribute());
Entity entity = dataService.findOneById(entityTypeId, id);
if (entity == null) {
throw new UnknownEntityException(meta, id);
}
return getEntityAsMap(entity, meta, attributesSet, attributeExpandSet);
}
/** Same as retrieveEntity (GET) only tunneled through POST. */
@PostMapping(
value = "/{entityTypeId}/{id}",
params = "_method=GET",
produces = APPLICATION_JSON_VALUE)
@ResponseBody
public Map retrieveEntity(
@PathVariable("entityTypeId") String entityTypeId,
@PathVariable("id") String untypedId,
@Valid @RequestBody EntityTypeRequest request) {
Set attributesSet = toAttributeSet(request != null ? request.getAttributes() : null);
Map> attributeExpandSet =
toExpandMap(request != null ? request.getExpand() : null);
EntityType meta = dataService.getEntityType(entityTypeId);
Object id = getTypedValue(untypedId, meta.getIdAttribute());
Entity entity = dataService.findOneById(entityTypeId, id);
if (entity == null) {
throw new UnknownEntityException(meta, untypedId);
}
return getEntityAsMap(entity, meta, attributesSet, attributeExpandSet);
}
/**
* Get's an XREF entity or a list of MREF entities
*
* Example:
*
*
/api/v1/person/99/address
*/
@GetMapping(value = "/{entityTypeId}/{id}/{refAttributeName}", produces = APPLICATION_JSON_VALUE)
public Object retrieveEntityAttribute(
@PathVariable("entityTypeId") String entityTypeId,
@PathVariable("id") String untypedId,
@PathVariable("refAttributeName") String refAttributeName,
@Valid EntityCollectionRequest request,
@RequestParam(value = "attributes", required = false) String[] attributes,
@RequestParam(value = "expand", required = false) String[] attributeExpands) {
Set attributesSet = toAttributeSet(attributes);
Map> attributeExpandSet = toExpandMap(attributeExpands);
return retrieveEntityAttributeInternal(
entityTypeId, untypedId, refAttributeName, request, attributesSet, attributeExpandSet);
}
/**
* Get's an XREF entity or a list of MREF entities
*
* Example:
*
*
/api/v1/person/99/address
*/
@PostMapping(
value = "/{entityTypeId}/{id}/{refAttributeName}",
params = "_method=GET",
produces = APPLICATION_JSON_VALUE)
public Object retrieveEntityAttributePost(
@PathVariable("entityTypeId") String entityTypeId,
@PathVariable("id") String untypedId,
@PathVariable("refAttributeName") String refAttributeName,
@Valid @RequestBody EntityCollectionRequest request) {
Set attributesSet = toAttributeSet(request.getAttributes());
Map> attributeExpandSet = toExpandMap(request.getExpand());
return retrieveEntityAttributeInternal(
entityTypeId, untypedId, refAttributeName, request, attributesSet, attributeExpandSet);
}
/**
* Do a query
*
* Returns json
*/
@GetMapping(value = "/{entityTypeId}", produces = APPLICATION_JSON_VALUE)
public EntityCollectionResponse retrieveEntityCollection(
@PathVariable("entityTypeId") String entityTypeId,
@Valid EntityCollectionRequest request,
@RequestParam(value = "attributes", required = false) String[] attributes,
@RequestParam(value = "expand", required = false) String[] attributeExpands) {
Set attributesSet = toAttributeSet(attributes);
Map> attributeExpandSet = toExpandMap(attributeExpands);
return retrieveEntityCollectionInternal(
entityTypeId, request, attributesSet, attributeExpandSet);
}
/**
* Same as retrieveEntityCollection (GET) only tunneled through POST.
*
* Example url: /api/v1/person?_method=GET
*
*
Returns json
*/
@PostMapping(value = "/{entityTypeId}", params = "_method=GET", produces = APPLICATION_JSON_VALUE)
public EntityCollectionResponse retrieveEntityCollectionPost(
@PathVariable("entityTypeId") String entityTypeId,
@Valid @RequestBody EntityCollectionRequest request) {
Set attributesSet = toAttributeSet(request.getAttributes());
Map> attributeExpandSet = toExpandMap(request.getExpand());
return retrieveEntityCollectionInternal(
entityTypeId, request, attributesSet, attributeExpandSet);
}
/**
* Does a rsql/fiql query, returns the result as csv
*
* Parameters:
*
*
q: the query
*
*
attributes: the attributes to return, if not specified returns all attributes
*
*
start: the index of the first row, default 0
*
*
num: the number of results to return, default 100, max 10000
*
*
*
*
Example: /api/v1/csv/person?q=firstName==Piet&attributes=firstName,lastName&start=10&num=100
*/
@GetMapping(value = "/csv/{entityTypeId}", produces = "text/csv")
@ResponseBody
public EntityCollection retrieveEntityCollection(
@PathVariable("entityTypeId") String entityTypeId,
@RequestParam(value = "attributes", required = false) String[] attributes,
HttpServletRequest req,
HttpServletResponse resp)
throws IOException {
final Set attributesSet = toAttributeSet(attributes);
Repository repo;
Iterable entities;
try {
repo = dataService.getRepository(entityTypeId);
Query q =
new QueryStringParser(repo, molgenisRSQL).parseQueryString(req.getParameterMap());
String[] sortAttributeArray = req.getParameterMap().get("sortColumn");
if (sortAttributeArray != null
&& sortAttributeArray.length == 1
&& StringUtils.isNotEmpty(sortAttributeArray[0])) {
String sortAttribute = sortAttributeArray[0];
String sortOrderArray[] = req.getParameterMap().get("sortOrder");
Sort.Direction order = Sort.Direction.ASC;
if (sortOrderArray != null
&& sortOrderArray.length == 1
&& StringUtils.isNotEmpty(sortOrderArray[0])) {
String sortOrder = sortOrderArray[0];
switch (sortOrder) {
case "ASC":
order = Sort.Direction.ASC;
break;
case "DESC":
order = Sort.Direction.DESC;
break;
default:
throw new RuntimeException("unknown sort order");
}
}
q.sort().on(sortAttribute, order);
}
if (q.getPageSize() == 0) {
q.pageSize(EntityCollectionRequest.DEFAULT_ROW_COUNT);
}
if (q.getPageSize() > EntityCollectionRequest.MAX_ROWS) {
resp.sendError(
HttpServletResponse.SC_BAD_REQUEST,
"Num exceeded the maximum of " + EntityCollectionRequest.MAX_ROWS + " rows");
return null;
}
entities = () -> dataService.findAll(entityTypeId, q).iterator();
} catch (ConversionFailedException
| RSQLParserException
| UnknownAttributeException
| IllegalArgumentException
| UnsupportedOperationException
| UnknownEntityException e) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
return null;
} catch (MolgenisDataAccessException e) {
resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return null;
}
// Check attribute names
EntityType meta = repo.getEntityType();
Iterable attributesIterable =
Iterables.transform(
meta.getAtomicAttributes(), attribute -> attribute.getName().toLowerCase());
if (attributesSet != null) {
SetView diff = Sets.difference(attributesSet, Sets.newHashSet(attributesIterable));
if (!diff.isEmpty()) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unknown attributes " + diff);
return null;
}
}
attributesIterable = Iterables.transform(meta.getAtomicAttributes(), Attribute::getName);
if (attributesSet != null) {
attributesIterable =
Iterables.filter(
attributesIterable, attribute -> attributesSet.contains(attribute.toLowerCase()));
}
return new DefaultEntityCollection(entities, attributesIterable);
}
/** Creates a new entity from a html form post. */
@Transactional
@PostMapping(
value = "/{entityTypeId}",
headers = "Content-Type=application/x-www-form-urlencoded")
public void createFromFormPost(
@PathVariable("entityTypeId") String entityTypeId,
HttpServletRequest request,
HttpServletResponse response) {
Map paramMap = new HashMap<>();
for (String param : request.getParameterMap().keySet()) {
String[] values = request.getParameterValues(param);
String value = values != null ? StringUtils.join(values, ',') : null;
if (StringUtils.isNotBlank(value)) {
paramMap.put(param, value);
}
}
createInternal(entityTypeId, paramMap, response);
}
/** Creates a new entity from a html form post. */
@Transactional
@PostMapping(value = "/{entityTypeId}", headers = "Content-Type=multipart/form-data")
public void createFromFormPostMultiPart(
@PathVariable("entityTypeId") String entityTypeId,
MultipartHttpServletRequest request,
HttpServletResponse response) {
Map paramMap = new HashMap<>();
for (String param : request.getParameterMap().keySet()) {
String[] values = request.getParameterValues(param);
String value = values != null ? StringUtils.join(values, ',') : null;
if (StringUtils.isNotBlank(value)) {
paramMap.put(param, value);
}
}
// add files to param map
for (Entry> entry : request.getMultiFileMap().entrySet()) {
String param = entry.getKey();
List files = entry.getValue();
if (files != null && files.size() > 1) {
throw new IllegalArgumentException("Multiple file input not supported");
}
paramMap.put(param, files != null && !files.isEmpty() ? files.get(0) : null);
}
createInternal(entityTypeId, paramMap, response);
}
@Transactional
@PostMapping("/{entityTypeId}")
public void create(
@PathVariable("entityTypeId") String entityTypeId,
@RequestBody Map entityMap,
HttpServletResponse response) {
if (entityMap == null) {
throw new IllegalArgumentException("Missing entity in body");
}
createInternal(entityTypeId, entityMap, response);
}
/**
* Updates an entity using PUT
*
* Example url: /api/v1/person/99
*/
@Transactional
@PutMapping("/{entityTypeId}/{id}")
@ResponseStatus(OK)
public void update(
@PathVariable("entityTypeId") String entityTypeId,
@PathVariable("id") String untypedId,
@RequestBody Map entityMap) {
updateInternal(entityTypeId, untypedId, entityMap);
}
/**
* Updates an entity by tunneling PUT through POST
*
* Example url: /api/v1/person/99?_method=PUT
*/
@Transactional
@PostMapping(value = "/{entityTypeId}/{id}", params = "_method=PUT")
@ResponseStatus(OK)
public void updatePost(
@PathVariable("entityTypeId") String entityTypeId,
@PathVariable("id") String untypedId,
@RequestBody Map entityMap) {
updateInternal(entityTypeId, untypedId, entityMap);
}
@PutMapping(value = "/{entityTypeId}/{id}/{attributeName}")
@ResponseStatus(OK)
public void updateAttributePut(
@PathVariable("entityTypeId") String entityTypeId,
@PathVariable("attributeName") String attributeName,
@PathVariable("id") String untypedId,
@RequestBody(required = false) Object paramValue) {
updateAttribute(entityTypeId, attributeName, untypedId, paramValue);
}
// TODO alternative for synchronization, for example by adding updatAttribute methods to the REST
// api
@PostMapping(value = "/{entityTypeId}/{id}/{attributeName}", params = "_method=PUT")
@ResponseStatus(OK)
public synchronized void updateAttribute(
@PathVariable("entityTypeId") String entityTypeId,
@PathVariable("attributeName") String attributeName,
@PathVariable("id") String untypedId,
@RequestBody(required = false) Object paramValue) {
EntityType entityType = dataService.getEntityType(entityTypeId);
Object id = getTypedValue(untypedId, entityType.getIdAttribute());
Entity entity = dataService.findOneById(entityTypeId, id);
if (entity == null) {
throw new UnknownEntityException(entityType, id);
}
Attribute attr = entityType.getAttribute(attributeName);
if (attr == null) {
throw new UnknownAttributeException(entityType, attributeName);
}
if (attr.isReadOnly()) {
throw new MolgenisDataAccessException(
"Attribute '" + attributeName + "' of entity '" + entityTypeId + "' is readonly");
}
Object value = this.restService.toEntityValue(attr, paramValue, id);
entity.set(attributeName, value);
dataService.update(entityTypeId, entity);
}
/**
* Updates an entity from a html form post.
*
* Tunnels PUT through POST
*
*
Example url: /api/v1/person/99?_method=PUT
*/
@Transactional
@PostMapping(
value = "/{entityTypeId}/{id}",
params = "_method=PUT",
headers = "Content-Type=multipart/form-data")
@ResponseStatus(NO_CONTENT)
public void updateFromFormPostMultiPart(
@PathVariable("entityTypeId") String entityTypeId,
@PathVariable("id") String untypedId,
MultipartHttpServletRequest request) {
Map paramMap = new HashMap<>();
for (String param : request.getParameterMap().keySet()) {
String[] values = request.getParameterValues(param);
String value = values != null ? StringUtils.join(values, ',') : null;
paramMap.put(param, value);
}
// add files to param map
for (Entry> entry : request.getMultiFileMap().entrySet()) {
String param = entry.getKey();
List files = entry.getValue();
if (files != null && files.size() > 1) {
throw new IllegalArgumentException("Multiple file input not supported");
}
paramMap.put(param, files != null && !files.isEmpty() ? files.get(0) : null);
}
updateInternal(entityTypeId, untypedId, paramMap);
}
/**
* Updates an entity from a html form post.
*
* Tunnels PUT through POST
*
*
Example url: /api/v1/person/99?_method=PUT
*/
@Transactional
@PostMapping(
value = "/{entityTypeId}/{id}",
params = "_method=PUT",
headers = "Content-Type=application/x-www-form-urlencoded")
@ResponseStatus(NO_CONTENT)
public void updateFromFormPost(
@PathVariable("entityTypeId") String entityTypeId,
@PathVariable("id") String untypedId,
HttpServletRequest request) {
Map paramMap = new HashMap<>();
for (String param : request.getParameterMap().keySet()) {
String[] values = request.getParameterValues(param);
String value = values != null ? StringUtils.join(values, ',') : null;
paramMap.put(param, value);
}
updateInternal(entityTypeId, untypedId, paramMap);
}
/** Deletes an entity by it's id */
@Transactional
@DeleteMapping("/{entityTypeId}/{id}")
@ResponseStatus(NO_CONTENT)
public void deleteDelete(
@PathVariable("entityTypeId") String entityTypeId, @PathVariable("id") String untypedId) {
delete(entityTypeId, untypedId);
}
/**
* Deletes an entity by it's id but tunnels DELETE through POST
*
* Example url: /api/v1/person/99?_method=DELETE
*/
@PostMapping(value = "/{entityTypeId}/{id}", params = "_method=DELETE")
@ResponseStatus(NO_CONTENT)
public void deletePost(
@PathVariable("entityTypeId") String entityTypeId, @PathVariable("id") String untypedId) {
delete(entityTypeId, untypedId);
}
private void delete(String entityTypeId, String untypedId) {
EntityType entityType = dataService.getEntityType(entityTypeId);
Object id = getTypedValue(untypedId, entityType.getIdAttribute());
if (ATTRIBUTE_META_DATA.equals(entityTypeId)) {
dataService.getMeta().deleteAttributeById(id);
} else {
dataService.deleteById(entityTypeId, id);
}
}
/** Deletes all entities for the given entity name */
@DeleteMapping("/{entityTypeId}")
@ResponseStatus(NO_CONTENT)
public void deleteAll(@PathVariable("entityTypeId") String entityTypeId) {
dataService.deleteAll(entityTypeId);
}
/** Deletes all entities for the given entity name but tunnels DELETE through POST */
@PostMapping(value = "/{entityTypeId}", params = "_method=DELETE")
@ResponseStatus(NO_CONTENT)
public void deleteAllPost(@PathVariable("entityTypeId") String entityTypeId) {
dataService.deleteAll(entityTypeId);
}
/** Deletes all entities and entity meta data for the given entity name */
@DeleteMapping(value = "/{entityTypeId}/meta")
@ResponseStatus(NO_CONTENT)
public void deleteMeta(@PathVariable("entityTypeId") String entityTypeId) {
deleteMetaInternal(entityTypeId);
}
/**
* Deletes all entities and entity meta data for the given entity name but tunnels DELETE through
* POST
*/
@PostMapping(value = "/{entityTypeId}/meta", params = "_method=DELETE")
@ResponseStatus(NO_CONTENT)
public void deleteMetaPost(@PathVariable("entityTypeId") String entityTypeId) {
deleteMetaInternal(entityTypeId);
}
private void deleteMetaInternal(String entityTypeId) {
dataService.getMeta().deleteEntityType(entityTypeId);
}
/**
* Login to the api.
*
*
Returns a json object with a token on correct login else throws an AuthenticationException.
* Clients can use this token when calling the api.
*
*
Example:
*
*
Request: {username:admin,password:xxx}
*
*
Response: {token: b4fd94dc-eae6-4d9a-a1b7-dd4525f2f75d}
*/
@PostMapping(value = "/login", produces = APPLICATION_JSON_VALUE)
@ResponseBody
public LoginResponse login(@Valid @RequestBody LoginRequest login, HttpServletRequest request) {
if (login == null) {
throw new HttpMessageNotReadableException("Missing login");
}
if (isUser2fa()) {
throw new BadCredentialsException(
"Login using /api/v1/login is disabled, two factor authentication is enabled");
}
return runAsSystem(
() -> {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(login.getUsername(), login.getPassword());
authToken.setDetails(new WebAuthenticationDetails(request));
// Authenticate the login
Authentication authentication = authenticationManager.authenticate(authToken);
if (!authentication.isAuthenticated()) {
throw new BadCredentialsException("Unknown username or password");
}
User user =
dataService.findOne(
USER,
new QueryImpl().eq(UserMetadata.USERNAME, authentication.getName()),
User.class);
if (user.isChangePassword()) {
throw new BadCredentialsException(
"Unable to log in because a password reset is required. Sign in to the website to reset your password.");
}
// User authenticated, log the user in
SecurityContextHolder.getContext().setAuthentication(authentication);
// Generate a new token for the user
String token =
tokenService.generateAndStoreToken(authentication.getName(), "REST API login");
return new LoginResponse(
token, user.getUsername(), user.getFirstName(), user.getLastName());
});
}
private boolean isUser2fa() {
return authenticationSettings.getTwoFactorAuthentication() == ENFORCED
|| (authenticationSettings.getTwoFactorAuthentication() == ENABLED
&& userAccountService.getCurrentUser().isTwoFactorAuthentication());
}
@PostMapping("/logout")
@ResponseStatus(OK)
public void logout(@TokenParam(required = true) String token, HttpServletRequest request) {
tokenService.removeToken(token);
SecurityContextHolder.getContext().setAuthentication(null);
if (request.getSession(false) != null) {
request.getSession().invalidate();
}
}
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(BAD_REQUEST)
public ErrorMessageResponse handleHttpMessageNotReadableException(
HttpMessageNotReadableException e) {
LOG.error("", e);
return new ErrorMessageResponse(new ErrorMessage(e.getMessage()));
}
@ExceptionHandler(UnknownTokenException.class)
@ResponseStatus(NOT_FOUND)
public ErrorMessageResponse handleUnknownTokenException(UnknownTokenException e) {
LOG.debug("", e);
return new ErrorMessageResponse(new ErrorMessage(e.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(BAD_REQUEST)
public ErrorMessageResponse handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
LOG.debug("", e);
List messages = Lists.newArrayList();
for (ObjectError error : e.getBindingResult().getAllErrors()) {
messages.add(new ErrorMessage(error.getDefaultMessage()));
}
return new ErrorMessageResponse(messages);
}
@ExceptionHandler(MolgenisValidationException.class)
@ResponseStatus(BAD_REQUEST)
public ErrorMessageResponse handleMolgenisValidationException(MolgenisValidationException e) {
LOG.info("", e);
List messages = Lists.newArrayList();
for (ConstraintViolation violation : e.getViolations()) {
messages.add(new ErrorMessage(violation.getMessage()));
}
return new ErrorMessageResponse(messages);
}
@ExceptionHandler(ConversionException.class)
@ResponseStatus(BAD_REQUEST)
public ErrorMessageResponse handleConversionException(ConversionException e) {
LOG.info("", e);
return new ErrorMessageResponse(new ErrorMessage(e.getMessage()));
}
@ExceptionHandler(MolgenisDataException.class)
@ResponseStatus(BAD_REQUEST)
public ErrorMessageResponse handleMolgenisDataException(MolgenisDataException e) {
LOG.error("", e);
return new ErrorMessageResponse(new ErrorMessage(e.getMessage()));
}
@ExceptionHandler(AuthenticationException.class)
@ResponseStatus(UNAUTHORIZED)
public ErrorMessageResponse handleAuthenticationException(AuthenticationException e) {
LOG.info("", e);
// workaround for https://github.com/molgenis/molgenis/issues/4441
String message = e.getMessage();
String messagePrefix =
"org.springframework.security.core.userdetails.UsernameNotFoundException: ";
if (message.startsWith(messagePrefix)) {
message = message.substring(messagePrefix.length());
}
return new ErrorMessageResponse(new ErrorMessage(message));
}
@ExceptionHandler(MolgenisDataAccessException.class)
@ResponseStatus(UNAUTHORIZED)
public ErrorMessageResponse handleMolgenisDataAccessException(MolgenisDataAccessException e) {
LOG.info("", e);
return new ErrorMessageResponse(new ErrorMessage(e.getMessage()));
}
private void updateInternal(
String entityTypeId, String untypedId, Map entityMap) {
EntityType meta = dataService.getEntityType(entityTypeId);
if (meta.getIdAttribute() == null) {
throw new IllegalArgumentException(entityTypeId + " does not have an id attribute");
}
Object id = getTypedValue(untypedId, meta.getIdAttribute());
Entity existing =
dataService.findOneById(
entityTypeId, id, new Fetch().field(meta.getIdAttribute().getName()));
if (existing == null) {
throw new UnknownEntityException(meta, id);
}
Entity entity = this.restService.toEntity(meta, entityMap);
dataService.update(entityTypeId, entity);
restService.updateMappedByEntities(entity, existing);
}
private void createInternal(
String entityTypeId, Map entityMap, HttpServletResponse response) {
EntityType entityType = dataService.getEntityType(entityTypeId);
Entity entity = this.restService.toEntity(entityType, entityMap);
if (ATTRIBUTE_META_DATA.equals(entityTypeId)) {
dataService.getMeta().addAttribute(new Attribute(entity));
} else {
dataService.add(entityTypeId, entity);
}
restService.updateMappedByEntities(entity);
Object id = entity.getIdValue();
if (id != null) {
response.addHeader("Location", UriUtils.createEntityUriPath(entityTypeId, id));
}
response.setStatus(HttpServletResponse.SC_CREATED);
}
private AttributeResponse getAttributePostInternal(
String entityTypeId,
String attributeName,
Set attributeSet,
Map> attributeExpandSet) {
EntityType meta = dataService.getEntityType(entityTypeId);
Attribute attribute = meta.getAttribute(attributeName);
if (attribute != null) {
return new AttributeResponse(
entityTypeId,
meta,
attribute,
attributeSet,
attributeExpandSet,
permissionService,
dataService);
} else {
throw new UnknownEntityTypeException(entityTypeId);
}
}
private Object retrieveEntityAttributeInternal(
String entityTypeId,
String untypedId,
String refAttributeName,
EntityCollectionRequest request,
Set attributesSet,
Map> attributeExpandSet) {
EntityType meta = dataService.getEntityType(entityTypeId);
Object id = getTypedValue(untypedId, meta.getIdAttribute());
// Check if the entity has an attribute with name refAttributeName
Attribute attr = meta.getAttribute(refAttributeName);
if (attr == null) {
throw new UnknownAttributeException(meta, refAttributeName);
}
// Get the entity
Entity entity = dataService.findOneById(entityTypeId, id);
if (entity == null) {
throw new UnknownEntityException(meta, id);
}
String attrHref =
UriUtils.createEntityAttributeUriPath(meta.getId(), entity.getIdValue(), refAttributeName);
switch (attr.getDataType()) {
case COMPOUND:
Map entityHasAttributeMap = new LinkedHashMap<>();
entityHasAttributeMap.put("href", attrHref);
@SuppressWarnings("unchecked")
Iterable attributeParts = (Iterable) entity.get(refAttributeName);
for (Attribute attribute : attributeParts) {
String attrName = attribute.getName();
entityHasAttributeMap.put(attrName, entity.get(attrName));
}
return entityHasAttributeMap;
case CATEGORICAL_MREF:
case MREF:
case ONE_TO_MANY:
List mrefEntities = new ArrayList<>();
for (Entity e : entity.getEntities((attr.getName()))) mrefEntities.add(e);
int count = mrefEntities.size();
int toIndex = request.getStart() + request.getNum();
mrefEntities = mrefEntities.subList(request.getStart(), toIndex > count ? count : toIndex);
List