UserController.java
/*
* Copyright 2005-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openwms.core.uaa;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import org.ameba.exception.NotFoundException;
import org.ameba.http.MeasuredRestController;
import org.ameba.i18n.Translator;
import org.openwms.core.http.AbstractWebController;
import org.openwms.core.http.Index;
import org.openwms.core.uaa.api.AuthenticatedUserVO;
import org.openwms.core.uaa.api.PasswordString;
import org.openwms.core.uaa.api.RoleVO;
import org.openwms.core.uaa.api.SecurityObjectVO;
import org.openwms.core.uaa.api.UserVO;
import org.openwms.core.uaa.api.ValidationGroups;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
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.RequestParam;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import static org.openwms.core.uaa.MessageCodes.USER_WITH_NAME_NOT_EXIST;
import static org.openwms.core.uaa.api.UAAConstants.API_USERS;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
/**
* An UserController represents a RESTful access to {@code User}s. It is transactional by the means it is the outer application service
* facade that returns validated and completed {@code User} objects to its clients.
*
* @author Heiko Scherrer
*/
@Validated
@Transactional
@MeasuredRestController
public class UserController extends AbstractWebController {
private final Translator translator;
private final UserService userService;
private final UserMapper userMapper;
private final RoleMapper roleMapper;
private final SecurityObjectMapper securityObjectMapper;
public UserController(Translator translator, UserService userService, UserMapper userMapper, RoleMapper roleMapper,
SecurityObjectMapper securityObjectMapper) {
this.translator = translator;
this.userService = userService;
this.userMapper = userMapper;
this.roleMapper = roleMapper;
this.securityObjectMapper = securityObjectMapper;
}
@GetMapping(API_USERS + "/index")
@Transactional(propagation = Propagation.NEVER)
public ResponseEntity<Index> index() {
return ResponseEntity.ok(
new Index(
linkTo(methodOn(UserController.class).findByPKey("{pKey}")).withRel("users-findbypkey"),
linkTo(methodOn(UserController.class).findAllUsers()).withRel("users-findall"),
linkTo(methodOn(UserController.class).findGrantsForUser("{pKey}")).withRel("users-findgrants"),
linkTo(methodOn(UserController.class).create(new UserVO(), null)).withRel("users-create"),
linkTo(methodOn(UserController.class).save(new UserVO())).withRel("users-save"),
linkTo(methodOn(UserController.class).saveImage("", "{pKey}")).withRel("users-saveimage"),
linkTo(methodOn(UserController.class).updatePassword("{pKey}", new PasswordString("newPassword"))).withRel("users-change-password"),
linkTo(methodOn(UserController.class).delete("{pKey}")).withRel("users-delete")
)
);
}
@GetMapping(API_USERS + "/{pKey}")
public ResponseEntity<UserVO> findByPKey(@PathVariable("pKey") String pKey) {
var eo = userService.findByPKey(pKey);
var result = userMapper.convertToVO(eo);
addSelfLink(result);
return ResponseEntity
.status(HttpStatus.OK)
.header(HttpHeaders.CONTENT_TYPE, UserVO.MEDIA_TYPE)
.body(result);
}
@GetMapping(value = API_USERS, params = "username")
public ResponseEntity<AuthenticatedUserVO> findByUsername(@RequestParam("username") String username) {
var eoOpt = userService.findByUsername(username);
if (eoOpt.isEmpty()) {
throw new NotFoundException(
translator.translate(USER_WITH_NAME_NOT_EXIST, username),
USER_WITH_NAME_NOT_EXIST,
username);
}
var result = userMapper.convertToAuthenticatedUserVO(eoOpt.get());
addSelfLink(result);
return ResponseEntity
.status(HttpStatus.OK)
.header(HttpHeaders.CONTENT_TYPE, AuthenticatedUserVO.MEDIA_TYPE)
.body(result);
}
@GetMapping(API_USERS)
public ResponseEntity<List<UserVO>> findAllUsers() {
var eos = userService.findAll();
var result = userMapper.convertToVO(new ArrayList<>(eos));
result.forEach(this::addSelfLink);
return ResponseEntity
.status(HttpStatus.OK)
.header(HttpHeaders.CONTENT_TYPE, UserVO.MEDIA_TYPE)
.body(result);
}
@GetMapping(API_USERS + "/{pKey}/grants")
public ResponseEntity<List<SecurityObjectVO>> findGrantsForUser(@PathVariable("pKey") String pKey) {
var user = userService.findByPKey(pKey);
var result = securityObjectMapper.convertToVOs(user.getGrants());
return ResponseEntity
.status(HttpStatus.OK)
.header(HttpHeaders.CONTENT_TYPE, SecurityObjectVO.MEDIA_TYPE)
.body(result);
}
@GetMapping(API_USERS + "/{pKey}/roles")
public ResponseEntity<List<RoleVO>> findRolesForUser(@PathVariable("pKey") @NotEmpty String pKey) {
var eo = userService.findByPKey(pKey);
var result = roleMapper.convertToVO(eo.getRoles());
return ResponseEntity
.status(HttpStatus.OK)
.header(HttpHeaders.CONTENT_TYPE, RoleVO.MEDIA_TYPE)
.body(result);
}
@PostMapping(API_USERS)
@Validated(ValidationGroups.Create.class)
public ResponseEntity<UserVO> create(@RequestBody @Valid @NotNull UserVO vo, HttpServletRequest req) {
var eo = userMapper.convertFrom(vo);
var created = userService.create(eo, vo.getRoleNames());
var result = userMapper.convertToVO(created);
return ResponseEntity
.created(getLocationURIForCreatedResource(req, result.getpKey()))
.header(HttpHeaders.CONTENT_TYPE, UserVO.MEDIA_TYPE)
.body(result);
}
@PutMapping(API_USERS)
@Validated(ValidationGroups.Modify.class)
public ResponseEntity<UserVO> save(@RequestBody @Valid @NotNull UserVO vo) {
var user = userMapper.convertFrom(vo);
var saved = userService.save(user, vo.getRoleNames());
var result = userMapper.convertToVO(saved);
return ResponseEntity
.status(HttpStatus.OK)
.header(HttpHeaders.CONTENT_TYPE, UserVO.MEDIA_TYPE)
.body(result);
}
@PostMapping(API_USERS + "/{pKey}/details/image")
public ResponseEntity<Void> saveImage(@RequestBody @NotNull String image, @PathVariable("pKey") String pKey) {
userService.uploadImageFile(pKey, image.getBytes(StandardCharsets.UTF_8));
return ResponseEntity.ok().build();
}
@PostMapping(API_USERS + "/{pKey}/password")
public ResponseEntity<UserVO> updatePassword(@PathVariable("pKey") String pKey, @RequestBody @NotNull PasswordString password) {
try {
var result = userService.updatePassword(pKey, password.asValue());
return ResponseEntity
.status(HttpStatus.OK)
.header(HttpHeaders.CONTENT_TYPE, UserVO.MEDIA_TYPE)
.body(result);
} catch (InvalidPasswordException e) {
return ResponseEntity.status(HttpStatus.CONFLICT).build();
}
}
@DeleteMapping(API_USERS + "/{pKey}")
public ResponseEntity<Void> delete(@PathVariable("pKey") String pKey) {
userService.delete(pKey);
return ResponseEntity.noContent().build();
}
private void addSelfLink(UserVO result) {
result.add(linkTo(methodOn(UserController.class).findByPKey(result.getpKey())).withRel("user-findbypkey"));
}
}