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();
    }
}