package org.molgenis.data.rest; 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 org.apache.commons.lang3.StringUtils; import org.molgenis.auth.User; import org.molgenis.auth.UserMetaData; import org.molgenis.data.*; import org.molgenis.data.i18n.LanguageService; import org.molgenis.data.meta.AttributeType; import org.molgenis.data.meta.model.Attribute; import org.molgenis.data.meta.model.EntityType; import org.molgenis.data.rest.service.RestService; import org.molgenis.data.rsql.MolgenisRSQL; import org.molgenis.data.support.DefaultEntityCollection; import org.molgenis.data.support.Href; import org.molgenis.data.support.QueryImpl; import org.molgenis.data.validation.ConstraintViolation; import org.molgenis.data.validation.MolgenisValidationException; import org.molgenis.security.core.MolgenisPermissionService; import org.molgenis.security.core.runas.RunAsSystem; import org.molgenis.security.core.token.TokenService; import org.molgenis.security.core.token.UnknownTokenException; import org.molgenis.security.token.TokenExtractor; import org.molgenis.util.ErrorMessageResponse; import org.molgenis.util.ErrorMessageResponse.ErrorMessage; import org.molgenis.util.MolgenisDateFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; 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.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.io.IOException; import java.sql.Date; import java.text.SimpleDateFormat; import java.util.*; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import static java.util.Objects.requireNonNull; import static org.molgenis.auth.UserMetaData.USER; import static org.molgenis.data.meta.AttributeType.*; import static org.molgenis.data.meta.model.AttributeMetadata.ATTRIBUTE_META_DATA; import static org.molgenis.data.rest.RestController.BASE_URI; import static org.molgenis.util.EntityUtils.getTypedValue; import static org.springframework.http.HttpStatus.*; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.web.bind.annotation.RequestMethod.*; /** * Rest endpoint for the DataService * <p> * Query, create, update and delete entities. * <p> * If a repository isn't capable of doing the requested operation an error is thrown. * <p> * Response is json. * * @author erwin */ @Controller @RequestMapping(BASE_URI) public class RestController { private static final Logger LOG = LoggerFactory.getLogger(RestController.class); static final String BASE_URI = "/api/v1"; private static final Pattern PATTERN_EXPANDS = Pattern.compile("([^\\[^\\]]+)(?:\\[(.+)\\])?"); private final DataService dataService; private final TokenService tokenService; private final AuthenticationManager authenticationManager; private final MolgenisPermissionService molgenisPermissionService; private final MolgenisRSQL molgenisRSQL; private final RestService restService; private final LanguageService languageService; @Autowired public RestController(DataService dataService, TokenService tokenService, AuthenticationManager authenticationManager, MolgenisPermissionService molgenisPermissionService, MolgenisRSQL molgenisRSQL, RestService restService, LanguageService languageService) { this.dataService = requireNonNull(dataService); this.tokenService = requireNonNull(tokenService); this.authenticationManager = requireNonNull(authenticationManager); this.molgenisPermissionService = requireNonNull(molgenisPermissionService); this.molgenisRSQL = requireNonNull(molgenisRSQL); this.restService = requireNonNull(restService); this.languageService = requireNonNull(languageService); } /** * Checks if an entity exists. */ @RequestMapping(value = "/{entityName}/exist", method = GET, produces = APPLICATION_JSON_VALUE) @ResponseBody public boolean entityExists(@PathVariable("entityName") String entityName) { try { dataService.getRepository(entityName); return true; } catch (UnknownEntityException e) { return false; } } /** * Gets the metadata for an entity * <p> * Example url: /api/v1/person/meta * * @param entityName * @return EntityType */ @RequestMapping(value = "/{entityName}/meta", method = GET, produces = APPLICATION_JSON_VALUE) @ResponseBody public EntityTypeResponse retrieveEntityType(@PathVariable("entityName") String entityName, @RequestParam(value = "attributes", required = false) String[] attributes, @RequestParam(value = "expand", required = false) String[] attributeExpands) { Set<String> attributeSet = toAttributeSet(attributes); Map<String, Set<String>> attributeExpandSet = toExpandMap(attributeExpands); EntityType meta = dataService.getEntityType(entityName); return new EntityTypeResponse(meta, attributeSet, attributeExpandSet, molgenisPermissionService, dataService, languageService); } /** * Same as retrieveEntityType (GET) only tunneled through POST. * <p> * Example url: /api/v1/person/meta?_method=GET * * @param entityName * @return EntityType */ @RequestMapping(value = "/{entityName}/meta", method = POST, params = "_method=GET", produces = APPLICATION_JSON_VALUE) @ResponseBody public EntityTypeResponse retrieveEntityTypePost(@PathVariable("entityName") String entityName, @Valid @RequestBody EntityTypeRequest request) { Set<String> attributesSet = toAttributeSet(request != null ? request.getAttributes() : null); Map<String, Set<String>> attributeExpandSet = toExpandMap(request != null ? request.getExpand() : null); EntityType meta = dataService.getEntityType(entityName); return new EntityTypeResponse(meta, attributesSet, attributeExpandSet, molgenisPermissionService, dataService, languageService); } /** * Example url: /api/v1/person/meta/emailaddresses * * @param entityName * @return EntityType */ @RequestMapping(value = "/{entityName}/meta/{attributeName}", method = GET, produces = APPLICATION_JSON_VALUE) @ResponseBody public AttributeResponse retrieveEntityAttributeMeta(@PathVariable("entityName") String entityName, @PathVariable("attributeName") String attributeName, @RequestParam(value = "attributes", required = false) String[] attributes, @RequestParam(value = "expand", required = false) String[] attributeExpands) { Set<String> attributeSet = toAttributeSet(attributes); Map<String, Set<String>> attributeExpandSet = toExpandMap(attributeExpands); return getAttributePostInternal(entityName, attributeName, attributeSet, attributeExpandSet); } /** * Same as retrieveEntityAttributeMeta (GET) only tunneled through POST. * * @param entityName * @return EntityType */ @RequestMapping(value = "/{entityName}/meta/{attributeName}", method = POST, params = "_method=GET", produces = APPLICATION_JSON_VALUE) @ResponseBody public AttributeResponse retrieveEntityAttributeMetaPost(@PathVariable("entityName") String entityName, @PathVariable("attributeName") String attributeName, @Valid @RequestBody EntityTypeRequest request) { Set<String> attributeSet = toAttributeSet(request != null ? request.getAttributes() : null); Map<String, Set<String>> attributeExpandSet = toExpandMap(request != null ? request.getExpand() : null); return getAttributePostInternal(entityName, attributeName, attributeSet, attributeExpandSet); } /** * Get's an entity by it's id * <p> * Examples: * <p> * /api/v1/person/99 Retrieves a person with id 99 * * @param entityName * @param untypedId * @param attributeExpands * @return * @throws UnknownEntityException */ @RequestMapping(value = "/{entityName}/{id:.+}", method = GET, produces = APPLICATION_JSON_VALUE) @ResponseBody public Map<String, Object> retrieveEntity(@PathVariable("entityName") String entityName, @PathVariable("id") String untypedId, @RequestParam(value = "attributes", required = false) String[] attributes, @RequestParam(value = "expand", required = false) String[] attributeExpands) { Set<String> attributesSet = toAttributeSet(attributes); Map<String, Set<String>> attributeExpandSet = toExpandMap(attributeExpands); EntityType meta = dataService.getEntityType(entityName); if (meta == null) { throw new UnknownEntityException(entityName + " not found"); } Object id = getTypedValue(untypedId, meta.getIdAttribute()); Entity entity = dataService.findOneById(entityName, id); if (entity == null) { throw new UnknownEntityException(entityName + " " + untypedId + " not found"); } return getEntityAsMap(entity, meta, attributesSet, attributeExpandSet); } /** * Same as retrieveEntity (GET) only tunneled through POST. * * @param entityName * @param untypedId * @param request * @return */ @RequestMapping(value = "/{entityName}/{id:.+}", method = POST, params = "_method=GET", produces = APPLICATION_JSON_VALUE) @ResponseBody public Map<String, Object> retrieveEntity(@PathVariable("entityName") String entityName, @PathVariable("id") String untypedId, @Valid @RequestBody EntityTypeRequest request) { Set<String> attributesSet = toAttributeSet(request != null ? request.getAttributes() : null); Map<String, Set<String>> attributeExpandSet = toExpandMap(request != null ? request.getExpand() : null); EntityType meta = dataService.getEntityType(entityName); Object id = getTypedValue(untypedId, meta.getIdAttribute()); Entity entity = dataService.findOneById(entityName, id); if (entity == null) { throw new UnknownEntityException(entityName + " " + untypedId + " not found"); } return getEntityAsMap(entity, meta, attributesSet, attributeExpandSet); } /** * Get's an XREF entity or a list of MREF entities * <p> * Example: * <p> * /api/v1/person/99/address * * @param entityName * @param untypedId * @param refAttributeName * @param request * @param attributeExpands * @return * @throws UnknownEntityException */ @RequestMapping(value = "/{entityName}/{id}/{refAttributeName}", method = GET, produces = APPLICATION_JSON_VALUE) @ResponseBody public Object retrieveEntityAttribute(@PathVariable("entityName") String entityName, @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<String> attributesSet = toAttributeSet(attributes); Map<String, Set<String>> attributeExpandSet = toExpandMap(attributeExpands); return retrieveEntityAttributeInternal(entityName, untypedId, refAttributeName, request, attributesSet, attributeExpandSet); } /** * Get's an XREF entity or a list of MREF entities * <p> * Example: * <p> * /api/v1/person/99/address * * @param entityName * @param untypedId * @param refAttributeName * @param request * @return * @throws UnknownEntityException */ @RequestMapping(value = "/{entityName}/{id}/{refAttributeName}", method = POST, params = "_method=GET", produces = APPLICATION_JSON_VALUE) @ResponseBody public Object retrieveEntityAttributePost(@PathVariable("entityName") String entityName, @PathVariable("id") String untypedId, @PathVariable("refAttributeName") String refAttributeName, @Valid @RequestBody EntityCollectionRequest request) { Set<String> attributesSet = toAttributeSet(request.getAttributes()); Map<String, Set<String>> attributeExpandSet = toExpandMap(request.getExpand()); return retrieveEntityAttributeInternal(entityName, untypedId, refAttributeName, request, attributesSet, attributeExpandSet); } /** * Do a query * <p> * Returns json * * @param entityName * @param request * @param attributeExpands * @return * @throws UnknownEntityException */ @RequestMapping(value = "/{entityName}", method = GET, produces = APPLICATION_JSON_VALUE) @ResponseBody public EntityCollectionResponse retrieveEntityCollection(@PathVariable("entityName") String entityName, @Valid EntityCollectionRequest request, @RequestParam(value = "attributes", required = false) String[] attributes, @RequestParam(value = "expand", required = false) String[] attributeExpands) { Set<String> attributesSet = toAttributeSet(attributes); Map<String, Set<String>> attributeExpandSet = toExpandMap(attributeExpands); return retrieveEntityCollectionInternal(entityName, request, attributesSet, attributeExpandSet); } /** * Same as retrieveEntityCollection (GET) only tunneled through POST. * <p> * Example url: /api/v1/person?_method=GET * <p> * Returns json * * @param request * @param entityName * @return */ @RequestMapping(value = "/{entityName}", method = POST, params = "_method=GET", produces = APPLICATION_JSON_VALUE) @ResponseBody public EntityCollectionResponse retrieveEntityCollectionPost(@PathVariable("entityName") String entityName, @Valid @RequestBody EntityCollectionRequest request) { Set<String> attributesSet = toAttributeSet(request.getAttributes()); Map<String, Set<String>> attributeExpandSet = toExpandMap(request.getExpand()); return retrieveEntityCollectionInternal(entityName, request, attributesSet, attributeExpandSet); } /** * Does a rsql/fiql query, returns the result as csv * <p> * Parameters: * <p> * q: the query * <p> * attributes: the attributes to return, if not specified returns all attributes * <p> * start: the index of the first row, default 0 * <p> * num: the number of results to return, default 100, max 10000 * <p> * <p> * Example: /api/v1/csv/person?q=firstName==Piet&attributes=firstName,lastName&start=10&num=100 * * @param entityName * @param attributes * @param req * @param resp * @return * @throws IOException */ @RequestMapping(value = "/csv/{entityName}", method = GET, produces = "text/csv") @ResponseBody public EntityCollection retrieveEntityCollection(@PathVariable("entityName") String entityName, @RequestParam(value = "attributes", required = false) String[] attributes, HttpServletRequest req, HttpServletResponse resp) throws IOException { final Set<String> attributesSet = toAttributeSet(attributes); EntityType meta; Iterable<Entity> entities; try { meta = dataService.getEntityType(entityName); Query<Entity> q = new QueryStringParser(meta, 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]; if (sortOrder.equals("ASC")) { order = Sort.Direction.ASC; } else if (sortOrder.equals("DESC")) { order = Sort.Direction.DESC; } else { 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(entityName, 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 Iterable<String> attributesIterable = Iterables .transform(meta.getAtomicAttributes(), attribute -> attribute.getName().toLowerCase()); if (attributesSet != null) { SetView<String> 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. * * @param entityName * @param request * @param response * @throws UnknownEntityException */ @RequestMapping(value = "/{entityName}", method = POST, headers = "Content-Type=application/x-www-form-urlencoded") public void createFromFormPost(@PathVariable("entityName") String entityName, HttpServletRequest request, HttpServletResponse response) { Map<String, Object> paramMap = new HashMap<String, Object>(); 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(entityName, paramMap, response); } /** * Creates a new entity from a html form post. * * @param entityName * @param request * @param response * @throws UnknownEntityException */ @RequestMapping(value = "/{entityName}", method = POST, headers = "Content-Type=multipart/form-data") public void createFromFormPostMultiPart(@PathVariable("entityName") String entityName, MultipartHttpServletRequest request, HttpServletResponse response) { Map<String, Object> paramMap = new HashMap<String, Object>(); 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<String, List<MultipartFile>> entry : request.getMultiFileMap().entrySet()) { String param = entry.getKey(); List<MultipartFile> 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(entityName, paramMap, response); } @RequestMapping(value = "/{entityName}", method = POST) public void create(@PathVariable("entityName") String entityName, @RequestBody Map<String, Object> entityMap, HttpServletResponse response) { if (entityMap == null) { throw new UnknownEntityException("Missing entity in body"); } createInternal(entityName, entityMap, response); } /** * Updates an entity using PUT * <p> * Example url: /api/v1/person/99 * * @param entityName * @param untypedId * @param entityMap */ @RequestMapping(value = "/{entityName}/{id}", method = PUT) @ResponseStatus(OK) public void update(@PathVariable("entityName") String entityName, @PathVariable("id") String untypedId, @RequestBody Map<String, Object> entityMap) { updateInternal(entityName, untypedId, entityMap); } /** * Updates an entity by tunneling PUT through POST * <p> * Example url: /api/v1/person/99?_method=PUT * * @param entityName * @param untypedId * @param entityMap */ @RequestMapping(value = "/{entityName}/{id}", method = POST, params = "_method=PUT") @ResponseStatus(OK) public void updatePost(@PathVariable("entityName") String entityName, @PathVariable("id") String untypedId, @RequestBody Map<String, Object> entityMap) { updateInternal(entityName, untypedId, entityMap); } @RequestMapping(value = "/{entityName}/{id}/{attributeName}", method = PUT) @ResponseStatus(OK) public void updateAttributePut(@PathVariable("entityName") String entityName, @PathVariable("attributeName") String attributeName, @PathVariable("id") String untypedId, @RequestBody Object paramValue) { updateAttribute(entityName, attributeName, untypedId, paramValue); } // TODO alternative for synchronization, for example by adding updatAttribute methods to the REST api @RequestMapping(value = "/{entityName}/{id}/{attributeName}", method = POST, params = "_method=PUT") @ResponseStatus(OK) public synchronized void updateAttribute(@PathVariable("entityName") String entityName, @PathVariable("attributeName") String attributeName, @PathVariable("id") String untypedId, @RequestBody Object paramValue) { EntityType entityType = dataService.getEntityType(entityName); if (entityType == null) { throw new UnknownEntityException("Entity of type " + entityName + " not found"); } Object id = getTypedValue(untypedId, entityType.getIdAttribute()); Entity entity = dataService.findOneById(entityName, id); if (entity == null) { throw new UnknownEntityException("Entity of type " + entityName + " with id " + id + " not found"); } Attribute attr = entityType.getAttribute(attributeName); if (attr == null) { throw new UnknownAttributeException( "Attribute '" + attributeName + "' of entity '" + entityName + "' does not exist"); } if (attr.isReadOnly()) { throw new MolgenisDataAccessException( "Attribute '" + attributeName + "' of entity '" + entityName + "' is readonly"); } Object value = this.restService.toEntityValue(attr, paramValue); entity.set(attributeName, value); dataService.update(entityName, entity); } /** * Updates an entity from a html form post. * <p> * Tunnels PUT through POST * <p> * Example url: /api/v1/person/99?_method=PUT * * @param entityName * @param untypedId * @param request * @throws UnknownEntityException */ @RequestMapping(value = "/{entityName}/{id}", method = POST, params = "_method=PUT", headers = "Content-Type=multipart/form-data") @ResponseStatus(NO_CONTENT) public void updateFromFormPostMultiPart(@PathVariable("entityName") String entityName, @PathVariable("id") String untypedId, MultipartHttpServletRequest request) { Map<String, Object> paramMap = new HashMap<String, Object>(); 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<String, List<MultipartFile>> entry : request.getMultiFileMap().entrySet()) { String param = entry.getKey(); List<MultipartFile> 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(entityName, untypedId, paramMap); } /** * Updates an entity from a html form post. * <p> * Tunnels PUT through POST * <p> * Example url: /api/v1/person/99?_method=PUT * * @param entityName * @param untypedId * @param request * @throws UnknownEntityException */ @RequestMapping(value = "/{entityName}/{id}", method = POST, params = "_method=PUT", headers = "Content-Type=application/x-www-form-urlencoded") @ResponseStatus(NO_CONTENT) public void updateFromFormPost(@PathVariable("entityName") String entityName, @PathVariable("id") String untypedId, HttpServletRequest request) { Map<String, Object> paramMap = new HashMap<String, Object>(); for (String param : request.getParameterMap().keySet()) { String[] values = request.getParameterValues(param); String value = values != null ? StringUtils.join(values, ',') : null; paramMap.put(param, value); } updateInternal(entityName, untypedId, paramMap); } /** * Deletes an entity by it's id * * @param entityName * @param untypedId */ @RequestMapping(value = "/{entityName}/{id}", method = DELETE) @ResponseStatus(NO_CONTENT) public void delete(@PathVariable("entityName") String entityName, @PathVariable("id") String untypedId) { EntityType entityType = dataService.getEntityType(entityName); Object id = getTypedValue(untypedId, entityType.getIdAttribute()); if (ATTRIBUTE_META_DATA.equals(entityName)) { dataService.getMeta().deleteAttributeById(id); } else { dataService.deleteById(entityName, id); } } /** * Deletes an entity by it's id but tunnels DELETE through POST * <p> * Example url: /api/v1/person/99?_method=DELETE * * @param entityName * @param untypedId */ @RequestMapping(value = "/{entityName}/{id}", method = POST, params = "_method=DELETE") @ResponseStatus(NO_CONTENT) public void deletePost(@PathVariable("entityName") String entityName, @PathVariable("id") String untypedId) { delete(entityName, untypedId); } /** * Deletes all entities for the given entity name * * @param entityName */ @RequestMapping(value = "/{entityName}", method = DELETE) @ResponseStatus(NO_CONTENT) public void deleteAll(@PathVariable("entityName") String entityName) { dataService.deleteAll(entityName); } /** * Deletes all entities for the given entity name but tunnels DELETE through POST * * @param entityName */ @RequestMapping(value = "/{entityName}", method = POST, params = "_method=DELETE") @ResponseStatus(NO_CONTENT) public void deleteAllPost(@PathVariable("entityName") String entityName) { dataService.deleteAll(entityName); } /** * Deletes all entities and entity meta data for the given entity name * * @param entityName */ @RequestMapping(value = "/{entityName}/meta", method = DELETE) @ResponseStatus(NO_CONTENT) public void deleteMeta(@PathVariable("entityName") String entityName) { deleteMetaInternal(entityName); } /** * Deletes all entities and entity meta data for the given entity name but tunnels DELETE through POST * * @param entityName */ @RequestMapping(value = "/{entityName}/meta", method = POST, params = "_method=DELETE") @ResponseStatus(NO_CONTENT) public void deleteMetaPost(@PathVariable("entityName") String entityName) { deleteMetaInternal(entityName); } private void deleteMetaInternal(String entityName) { dataService.getMeta().deleteEntityType(entityName); } /** * Login to the api. * <p> * Returns a json object with a token on correct login else throws an AuthenticationException. Clients can use this * token when calling the api. * <p> * Example: * <p> * Request: {username:admin,password:xxx} * <p> * Response: {token: b4fd94dc-eae6-4d9a-a1b7-dd4525f2f75d} * * @param login * @param request * @return */ @RequestMapping(value = "/login", method = POST, produces = APPLICATION_JSON_VALUE) @ResponseBody @RunAsSystem public LoginResponse login(@Valid @RequestBody LoginRequest login, HttpServletRequest request) { if (login == null) { throw new HttpMessageNotReadableException("Missing login"); } 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<User>().eq(UserMetaData.USERNAME, authentication.getName()), User.class); // 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()); } @RequestMapping("/logout") @ResponseStatus(OK) public void logout(HttpServletRequest request) { String token = TokenExtractor.getToken(request); if (token == null) { throw new HttpMessageNotReadableException("Missing token in header"); } tokenService.removeToken(token); SecurityContextHolder.getContext().setAuthentication(null); if (request.getSession(false) != null) { request.getSession().invalidate(); } } @ExceptionHandler(HttpMessageNotReadableException.class) @ResponseStatus(BAD_REQUEST) @ResponseBody public ErrorMessageResponse handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { LOG.error("", e); return new ErrorMessageResponse(new ErrorMessage(e.getMessage())); } @ExceptionHandler(UnknownTokenException.class) @ResponseStatus(NOT_FOUND) @ResponseBody public ErrorMessageResponse handleUnknownTokenException(UnknownTokenException e) { LOG.debug("", e); return new ErrorMessageResponse(new ErrorMessage(e.getMessage())); } @ExceptionHandler(UnknownEntityException.class) @ResponseStatus(NOT_FOUND) @ResponseBody public ErrorMessageResponse handleUnknownEntityException(UnknownEntityException e) { LOG.debug("", e); return new ErrorMessageResponse(new ErrorMessage(e.getMessage())); } @ExceptionHandler(UnknownAttributeException.class) @ResponseStatus(NOT_FOUND) @ResponseBody public ErrorMessageResponse handleUnknownAttributeException(UnknownAttributeException e) { LOG.debug("", e); return new ErrorMessageResponse(new ErrorMessage(e.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(BAD_REQUEST) @ResponseBody public ErrorMessageResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { LOG.debug("", e); List<ErrorMessage> 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) @ResponseBody public ErrorMessageResponse handleMolgenisValidationException(MolgenisValidationException e) { LOG.info("", e); List<ErrorMessage> messages = Lists.newArrayList(); for (ConstraintViolation violation : e.getViolations()) { messages.add(new ErrorMessage(violation.getMessage())); } return new ErrorMessageResponse(messages); } @ExceptionHandler(ConversionException.class) @ResponseStatus(BAD_REQUEST) @ResponseBody public ErrorMessageResponse handleConversionException(ConversionException e) { LOG.info("", e); return new ErrorMessageResponse(new ErrorMessage(e.getMessage())); } @ExceptionHandler(MolgenisDataException.class) @ResponseStatus(BAD_REQUEST) @ResponseBody public ErrorMessageResponse handleMolgenisDataException(MolgenisDataException e) { LOG.error("", e); return new ErrorMessageResponse(new ErrorMessage(e.getMessage())); } @ExceptionHandler(AuthenticationException.class) @ResponseStatus(UNAUTHORIZED) @ResponseBody 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) @ResponseBody public ErrorMessageResponse handleMolgenisDataAccessException(MolgenisDataAccessException e) { LOG.info("", e); return new ErrorMessageResponse(new ErrorMessage(e.getMessage())); } @ExceptionHandler(RuntimeException.class) @ResponseStatus(INTERNAL_SERVER_ERROR) @ResponseBody public ErrorMessageResponse handleRuntimeException(RuntimeException e) { LOG.error("", e); return new ErrorMessageResponse(new ErrorMessage(e.getMessage())); } @ExceptionHandler(MolgenisReferencedEntityException.class) @ResponseStatus(INTERNAL_SERVER_ERROR) @ResponseBody public ErrorMessageResponse handleMolgenisReferencingEntityException(MolgenisReferencedEntityException e) { LOG.error("", e); return new ErrorMessageResponse(new ErrorMessage(e.getMessage())); } @Transactional private void updateInternal(String entityName, String untypedId, Map<String, Object> entityMap) { EntityType meta = dataService.getEntityType(entityName); if (meta.getIdAttribute() == null) { throw new IllegalArgumentException(entityName + " does not have an id attribute"); } Object id = getTypedValue(untypedId, meta.getIdAttribute()); Entity existing = dataService.findOneById(entityName, id, new Fetch().field(meta.getIdAttribute().getName())); if (existing == null) { throw new UnknownEntityException("Entity of type " + entityName + " with id " + id + " not found"); } Entity entity = this.restService.toEntity(meta, entityMap); dataService.update(entityName, entity); restService.updateMappedByEntities(entity, existing); } @Transactional private void createInternal(String entityName, Map<String, Object> entityMap, HttpServletResponse response) { EntityType entityType = dataService.getEntityType(entityName); Entity entity = this.restService.toEntity(entityType, entityMap); if (ATTRIBUTE_META_DATA.equals(entityName)) { dataService.getMeta().addAttribute(new Attribute(entity)); } else { dataService.add(entityName, entity); } restService.updateMappedByEntities(entity); Object id = entity.getIdValue(); if (id != null) { response.addHeader("Location", Href.concatEntityHref(RestController.BASE_URI, entityName, id)); } response.setStatus(HttpServletResponse.SC_CREATED); } private AttributeResponse getAttributePostInternal(String entityName, String attributeName, Set<String> attributeSet, Map<String, Set<String>> attributeExpandSet) { EntityType meta = dataService.getEntityType(entityName); Attribute attribute = meta.getAttribute(attributeName); if (attribute != null) { return new AttributeResponse(entityName, meta, attribute, attributeSet, attributeExpandSet, molgenisPermissionService, dataService, languageService); } else { throw new UnknownAttributeException(attributeName); } } private Object retrieveEntityAttributeInternal(String entityName, String untypedId, String refAttributeName, EntityCollectionRequest request, Set<String> attributesSet, Map<String, Set<String>> attributeExpandSet) { EntityType meta = dataService.getEntityType(entityName); 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(entityName + " does not have an attribute named " + refAttributeName); } // Get the entity Entity entity = dataService.findOneById(entityName, id); if (entity == null) { throw new UnknownEntityException(entityName + " " + id + " not found"); } String attrHref = Href .concatAttributeHref(RestController.BASE_URI, meta.getName(), entity.getIdValue(), refAttributeName); switch (attr.getDataType()) { case COMPOUND: Map<String, Object> entityHasAttributeMap = new LinkedHashMap<String, Object>(); entityHasAttributeMap.put("href", attrHref); @SuppressWarnings("unchecked") Iterable<Attribute> attributeParts = (Iterable<Attribute>) 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<Entity> mrefEntities = new ArrayList<Entity>(); 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<Map<String, Object>> refEntityMaps = new ArrayList<Map<String, Object>>(); for (Entity refEntity : mrefEntities) { Map<String, Object> refEntityMap = getEntityAsMap(refEntity, attr.getRefEntity(), attributesSet, attributeExpandSet); refEntityMaps.add(refEntityMap); } EntityPager pager = new EntityPager(request.getStart(), request.getNum(), (long) count, mrefEntities); return new EntityCollectionResponse(pager, refEntityMaps, attrHref, null, molgenisPermissionService, dataService, languageService); case CATEGORICAL: case XREF: Map<String, Object> entityXrefAttributeMap = getEntityAsMap((Entity) entity.get(refAttributeName), attr.getRefEntity(), attributesSet, attributeExpandSet); entityXrefAttributeMap.put("href", attrHref); return entityXrefAttributeMap; default: Map<String, Object> entityAttributeMap = new LinkedHashMap<String, Object>(); entityAttributeMap.put("href", attrHref); entityAttributeMap.put(refAttributeName, entity.get(refAttributeName)); return entityAttributeMap; } } // Handles a Query private EntityCollectionResponse retrieveEntityCollectionInternal(String entityName, EntityCollectionRequest request, Set<String> attributesSet, Map<String, Set<String>> attributeExpandsSet) { EntityType meta = dataService.getEntityType(entityName); Repository<Entity> repository = dataService.getRepository(entityName); // convert sort Sort sort; SortV1 sortV1 = request.getSort(); if (sortV1 != null) { sort = new Sort(); for (SortV1.OrderV1 orderV1 : sortV1) { sort.on(orderV1.getProperty(), orderV1.getDirection() == SortV1.DirectionV1.ASC ? Sort.Direction.ASC : Sort.Direction.DESC); } } else { sort = null; } List<QueryRule> queryRules = request.getQ() == null ? Collections.<QueryRule>emptyList() : request.getQ(); Query<Entity> q = new QueryImpl<>(queryRules).pageSize(request.getNum()).offset(request.getStart()).sort(sort); Iterable<Entity> it = () -> dataService.findAll(entityName, q).iterator(); Long count = repository.count(q); EntityPager pager = new EntityPager(request.getStart(), request.getNum(), count, it); List<Map<String, Object>> entities = new ArrayList<>(); for (Entity entity : it) { entities.add(getEntityAsMap(entity, meta, attributesSet, attributeExpandsSet)); } return new EntityCollectionResponse(pager, entities, BASE_URI + "/" + entityName, meta, molgenisPermissionService, dataService, languageService); } // Transforms an entity to a Map so it can be transformed to json private Map<String, Object> getEntityAsMap(Entity entity, EntityType meta, Set<String> attributesSet, Map<String, Set<String>> attributeExpandsSet) { if (null == entity) throw new IllegalArgumentException("entity is null"); if (null == meta) throw new IllegalArgumentException("meta is null"); Map<String, Object> entityMap = new LinkedHashMap<String, Object>(); entityMap.put("href", Href.concatEntityHref(RestController.BASE_URI, meta.getName(), entity.getIdValue())); for (Attribute attr : meta.getAtomicAttributes()) { // filter fields if (attributesSet != null && !attributesSet.contains(attr.getName().toLowerCase())) continue; String attrName = attr.getName(); AttributeType attrType = attr.getDataType(); if (attrType == COMPOUND) { if (attributeExpandsSet != null && attributeExpandsSet.containsKey(attrName.toLowerCase())) { Set<String> subAttributesSet = attributeExpandsSet.get(attrName.toLowerCase()); entityMap.put(attrName, new AttributeResponse(meta.getName(), meta, attr, subAttributesSet, null, molgenisPermissionService, dataService, languageService)); } else { entityMap.put(attrName, Collections.singletonMap("href", Href.concatAttributeHref(RestController.BASE_URI, meta.getName(), entity.getIdValue(), attrName))); } } else if (attrType == DATE) { Date date = entity.getDate(attrName); entityMap.put(attrName, date != null ? MolgenisDateFormat.getDateFormat().format(date) : null); } else if (attrType == DATE_TIME) { Date date = entity.getDate(attrName); entityMap.put(attrName, date != null ? MolgenisDateFormat.getDateTimeFormat().format(date) : null); } else if (attrType != XREF && attrType != CATEGORICAL && attrType != MREF && attrType != CATEGORICAL_MREF && attrType != ONE_TO_MANY && attrType != FILE) { entityMap.put(attrName, entity.get(attr.getName())); } else if ((attrType == XREF || attrType == CATEGORICAL || attrType == FILE) && attributeExpandsSet != null && attributeExpandsSet.containsKey(attrName.toLowerCase())) { Entity refEntity = entity.getEntity(attr.getName()); if (refEntity != null) { Set<String> subAttributesSet = attributeExpandsSet.get(attrName.toLowerCase()); EntityType refEntityType = dataService.getEntityType(attr.getRefEntity().getName()); Map<String, Object> refEntityMap = getEntityAsMap(refEntity, refEntityType, subAttributesSet, null); entityMap.put(attrName, refEntityMap); } } else if ((attrType == MREF || attrType == CATEGORICAL_MREF || attrType == ONE_TO_MANY) && attributeExpandsSet != null && attributeExpandsSet.containsKey(attrName.toLowerCase())) { EntityType refEntityType = dataService.getEntityType(attr.getRefEntity().getName()); Iterable<Entity> mrefEntities = entity.getEntities(attr.getName()); Set<String> subAttributesSet = attributeExpandsSet.get(attrName.toLowerCase()); List<Map<String, Object>> refEntityMaps = new ArrayList<Map<String, Object>>(); for (Entity refEntity : mrefEntities) { Map<String, Object> refEntityMap = getEntityAsMap(refEntity, refEntityType, subAttributesSet, null); refEntityMaps.add(refEntityMap); } EntityPager pager = new EntityPager(0, new EntityCollectionRequest().getNum(), (long) refEntityMaps.size(), mrefEntities); EntityCollectionResponse ecr = new EntityCollectionResponse(pager, refEntityMaps, Href.concatAttributeHref(RestController.BASE_URI, meta.getName(), entity.getIdValue(), attrName), null, molgenisPermissionService, dataService, languageService); entityMap.put(attrName, ecr); } else if ((attrType == XREF && entity.get(attr.getName()) != null) || (attrType == CATEGORICAL && entity.get(attr.getName()) != null) || (attrType == FILE && entity.get(attr.getName()) != null) || attrType == MREF || attrType == CATEGORICAL_MREF || attrType == ONE_TO_MANY) { // Add href to ref field Map<String, String> ref = new LinkedHashMap<String, String>(); ref.put("href", Href.concatAttributeHref(RestController.BASE_URI, meta.getName(), entity.getIdValue(), attrName)); entityMap.put(attrName, ref); } } return entityMap; } /** * @param attributes * @return set of lower case attribute names */ private Set<String> toAttributeSet(String[] attributes) { return attributes != null && attributes.length > 0 ? Sets.newHashSet( Iterables.transform(Arrays.asList(attributes), new com.google.common.base.Function<String, String>() { @Override public String apply(String attribute) { return attribute.toLowerCase(); } })) : null; } /** * expand is of form 'attr1', 'entity1[attr1]', 'entity1[attr1;attr2]' * * @param expands * @return map from lower case expand names to a attribute set */ private Map<String, Set<String>> toExpandMap(String[] expands) { if (expands != null) { Map<String, Set<String>> expandMap = new HashMap<String, Set<String>>(); for (String expand : expands) { // validate Matcher matcher = PATTERN_EXPANDS.matcher(expand); if (!matcher.matches()) throw new MolgenisDataException("invalid expand value: " + expand); // for partial expands, create set expand = matcher.group(1); String attrsStr = matcher.group(2); Set<String> attrSet; if (attrsStr != null && !attrsStr.isEmpty()) { attrSet = new HashSet<String>(); for (String attr : attrsStr.split(";")) { attrSet.add(attr.toLowerCase()); } } else attrSet = null; expandMap.put(expand.toLowerCase(), attrSet); } return expandMap; } return null; } }