ProductController.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.wms.inventory.rest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import jakarta.validation.Validator;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.ameba.exception.NotFoundException;
import org.ameba.http.MeasuredRestController;
import org.openwms.core.http.AbstractWebController;
import org.openwms.core.http.Index;
import org.openwms.wms.CycleAvoidingMappingContext;
import org.openwms.wms.inventory.ProductMapper;
import org.openwms.wms.inventory.ProductService;
import org.openwms.wms.inventory.api.ProductVO;
import org.openwms.wms.inventory.api.ValidationGroups;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.util.List;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static org.ameba.system.ValidationUtil.validate;
import static org.openwms.wms.inventory.api.InventoryConstants.API_PRODUCTS;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
/**
* A ProductController.
*
* @author Heiko Scherrer
*/
@Validated
@MeasuredRestController
public class ProductController extends AbstractWebController {
private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class);
private final ProductService productService;
private final ProductMapper productMapper;
private final Validator validator;
ProductController(ProductService productService, ProductMapper productMapper, Validator validator) {
this.productService = productService;
this.productMapper = productMapper;
this.validator = validator;
}
public static Link getProductSelfLink(ProductVO o) {
return linkTo(methodOn(ProductController.class).findByPKey(o.getpKey())).withRel("self");
}
@GetMapping(API_PRODUCTS + "/index")
public ResponseEntity<Index> index() {
return ResponseEntity.ok(
new Index(
linkTo(methodOn(ProductController.class).findAll()).withRel("products-findall"),
linkTo(methodOn(ProductController.class).findBySKU("ZS0002029")).withRel("products-findbysku"),
linkTo(methodOn(ProductController.class).findByPKey("pKey")).withRel("products-findbyspkey"),
linkTo(methodOn(ProductController.class).findBySKUandUOM("ZS0002029", "PC")).withRel("products-findbyskuanduom"),
linkTo(methodOn(ProductController.class).create(asList(new ProductVO()), null)).withRel("products-create"),
linkTo(methodOn(ProductController.class).update(new ProductVO())).withRel("products-update"),
linkTo(methodOn(ProductController.class).delete("pKey")).withRel("products-delete")
)
);
}
@Transactional(readOnly = true)
@GetMapping(API_PRODUCTS)
public ResponseEntity<List<ProductVO>> findAll() {
return ResponseEntity.ok(productMapper.convert(productService.findAll(), new CycleAvoidingMappingContext()));
}
@Transactional(readOnly = true)
@GetMapping(value = API_PRODUCTS, params = "sku")
public ResponseEntity<ProductVO> findBySKU(@RequestParam("sku") String sku) {
var product = productService.findBySKU(sku).orElseThrow(() -> new NotFoundException(format("Product with SKU [%s] does not exist", sku)));
return ResponseEntity.ok(productMapper.convert(product, new CycleAvoidingMappingContext()));
}
@Transactional(readOnly = true)
@GetMapping(value = API_PRODUCTS + "/{pKey}")
public ResponseEntity<ProductVO> findByPKey(@PathVariable("pKey") String pKey) {
var vo = productMapper.convert(productService.findBy(pKey), new CycleAvoidingMappingContext());
vo.add(getProductSelfLink(vo));
return ResponseEntity.ok(vo);
}
@Transactional(readOnly = true)
@GetMapping(value = API_PRODUCTS, params = {"sku", "uom"})
public ResponseEntity<ProductVO> findBySKUandUOM(@RequestParam("sku") String sku, @RequestParam("uom") String uom) {
var result = productService.findBySKUandUOM(sku, uom);
return result.map(product -> ResponseEntity.ok(productMapper.convert(product, new CycleAvoidingMappingContext()))).orElseGet(() -> ResponseEntity.notFound().build());
}
@Transactional(readOnly = true)
@GetMapping(value = API_PRODUCTS, params = {"bk"})
public ResponseEntity<ProductVO> findByLabelOrSKU(@RequestParam("bk") String bk) {
return ResponseEntity.ok(productMapper.convert(productService.findByLabelOrSKUOrThrow(bk), new CycleAvoidingMappingContext()));
}
@Transactional
@Validated
@PostMapping(API_PRODUCTS)
public ResponseEntity<Void> create(@NotNull @Size(min = 1) @RequestBody List<ProductVO> products, HttpServletRequest req) {
products.forEach(p -> validate(validator, p, ValidationGroups.CreateProduct.class));
if (products.size() == 1) {
var product = productService.create(productMapper.convertVO(products.getFirst()));
return ResponseEntity.created(super.getLocationURIForCreatedResource(req, product.getPersistentKey())).build();
} else {
productService.createAll(productMapper.convertVO(products));
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
@Transactional
@Validated(ValidationGroups.UpdateProduct.class)
@PutMapping(API_PRODUCTS)
public ResponseEntity<ProductVO> update(@Valid @NotNull @RequestBody ProductVO product) {
validate(validator, product, ValidationGroups.UpdateProduct.class);
LOGGER.debug("Requested to modify Product [{}]", product);
var updated = productService.update(productMapper.convertVO(product));
return ResponseEntity.ok(productMapper.convert(updated, new CycleAvoidingMappingContext()));
}
@DeleteMapping(API_PRODUCTS + "/{pKey}")
public ResponseEntity<Void> delete(@PathVariable("pKey") String pKey) {
LOGGER.info("Request to delete Product with pKey [{}]", pKey);
productService.delete(pKey);
return ResponseEntity.noContent().build();
}
}