TransportUnitController.java

  1. /*
  2.  * Copyright 2005-2025 the original author or authors.
  3.  *
  4.  * Licensed under the Apache License, Version 2.0 (the "License");
  5.  * you may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  *
  8.  * http://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  */
  16. package org.openwms.common.transport;

  17. import jakarta.servlet.http.HttpServletRequest;
  18. import jakarta.validation.Valid;
  19. import jakarta.validation.constraints.NotBlank;
  20. import jakarta.validation.constraints.NotEmpty;
  21. import org.ameba.LoggingCategories;
  22. import org.ameba.exception.NotFoundException;
  23. import org.ameba.exception.ResourceExistsException;
  24. import org.ameba.http.MeasuredRestController;
  25. import org.ameba.http.Response;
  26. import org.ameba.i18n.Translator;
  27. import org.openwms.common.SimpleLink;
  28. import org.openwms.common.StateChangeException;
  29. import org.openwms.common.location.LocationController;
  30. import org.openwms.common.transport.api.TransportApiConstants;
  31. import org.openwms.common.transport.api.TransportUnitVO;
  32. import org.openwms.common.transport.api.ValidationGroups;
  33. import org.openwms.common.transport.barcode.BarcodeGenerator;
  34. import org.openwms.core.SpringProfiles;
  35. import org.openwms.core.http.AbstractWebController;
  36. import org.openwms.core.http.Index;
  37. import org.slf4j.Logger;
  38. import org.slf4j.LoggerFactory;
  39. import org.springframework.context.annotation.Profile;
  40. import org.springframework.http.HttpStatus;
  41. import org.springframework.http.ResponseEntity;
  42. import org.springframework.validation.annotation.Validated;
  43. import org.springframework.web.bind.annotation.DeleteMapping;
  44. import org.springframework.web.bind.annotation.ExceptionHandler;
  45. import org.springframework.web.bind.annotation.GetMapping;
  46. import org.springframework.web.bind.annotation.PatchMapping;
  47. import org.springframework.web.bind.annotation.PathVariable;
  48. import org.springframework.web.bind.annotation.PostMapping;
  49. import org.springframework.web.bind.annotation.PutMapping;
  50. import org.springframework.web.bind.annotation.RequestBody;
  51. import org.springframework.web.bind.annotation.RequestParam;

  52. import java.util.List;

  53. import static java.util.Arrays.asList;
  54. import static org.openwms.common.CommonMessageCodes.TU_BARCODE_MISSING;
  55. import static org.openwms.common.CommonMessageCodes.TU_EXISTS;
  56. import static org.openwms.common.transport.api.TransportApiConstants.API_TRANSPORT_UNIT;
  57. import static org.openwms.common.transport.api.TransportApiConstants.API_TRANSPORT_UNITS;
  58. import static org.openwms.common.transport.api.TransportUnitVO.MEDIA_TYPE;
  59. import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
  60. import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

  61. /**
  62.  * A TransportUnitController.
  63.  *
  64.  * @author Heiko Scherrer
  65.  */
  66. @Profile("!" + SpringProfiles.IN_MEMORY)
  67. @Validated
  68. @MeasuredRestController
  69. public class TransportUnitController extends AbstractWebController {

  70.     private static final Logger EXC_LOGGER = LoggerFactory.getLogger(LoggingCategories.PRESENTATION_LAYER_EXCEPTION);
  71.     private final TransportUnitMapper mapper;
  72.     private final Translator translator;
  73.     private final BarcodeGenerator barcodeGenerator;
  74.     private final TransportUnitService service;

  75.     TransportUnitController(TransportUnitMapper mapper, Translator translator, BarcodeGenerator barcodeGenerator, TransportUnitService service) {
  76.         this.mapper = mapper;
  77.         this.translator = translator;
  78.         this.barcodeGenerator = barcodeGenerator;
  79.         this.service = service;
  80.     }

  81.     @ExceptionHandler({ StateChangeException.class })
  82.     private ResponseEntity<?> handleStateChangeException(StateChangeException e) {
  83.         EXC_LOGGER.error("[P] Presentation Layer Exception: {}", e.getLocalizedMessage(), e);
  84.         return new ResponseEntity<>(Response.newBuilder().withMessage(e.getMessage()).withMessageKey(e.getMessageKey())
  85.                 .withHttpStatus(HttpStatus.CONFLICT.toString()).build(), HttpStatus.CONFLICT);
  86.     }

  87.     @GetMapping(value = API_TRANSPORT_UNITS + "/{pKey}", produces = MEDIA_TYPE)
  88.     public ResponseEntity<TransportUnitVO> findTransportUnitByPKey(
  89.             @PathVariable("pKey") String pKey
  90.     ) {
  91.         return ResponseEntity.ok(
  92.                 convertAndLinks(service.findByPKey(pKey))
  93.         );
  94.     }

  95.     @GetMapping(value = API_TRANSPORT_UNITS, params = {"bk"}, produces = MEDIA_TYPE)
  96.     public ResponseEntity<TransportUnitVO> findTransportUnit(
  97.             @RequestParam("bk") String transportUnitBK
  98.     ) {
  99.         return ResponseEntity.ok(
  100.                 convertAndLinks(service.findByBarcode(transportUnitBK))
  101.         );
  102.     }

  103.     @GetMapping(value = API_TRANSPORT_UNITS, produces = MEDIA_TYPE)
  104.     public ResponseEntity<List<TransportUnitVO>> findAll() {
  105.         return ResponseEntity.ok(
  106.                 convertAndLinks(service.findAll())
  107.         );
  108.     }

  109.     @GetMapping(value = API_TRANSPORT_UNITS, params = {"bks"}, produces = MEDIA_TYPE)
  110.     public ResponseEntity<List<TransportUnitVO>> findTransportUnits(
  111.             @RequestParam("bks") @NotEmpty List<String> barcodes
  112.     ) {
  113.         return ResponseEntity.ok(
  114.                 convertAndLinks(service.findByBarcodes(barcodes.stream().map(barcodeGenerator::convert).toList()))
  115.         );
  116.     }

  117.     @GetMapping(value = API_TRANSPORT_UNITS, params = {"actualLocation"}, produces = MEDIA_TYPE)
  118.     public ResponseEntity<List<TransportUnitVO>> findTransportUnitsOn(
  119.             @RequestParam("actualLocation") String actualLocation
  120.     ) {
  121.         return ResponseEntity.ok(
  122.                 convertAndLinks(service.findOnLocation(actualLocation))
  123.         );
  124.     }

  125.     @PostMapping(value = API_TRANSPORT_UNITS, params = {"bk"})
  126.     public ResponseEntity<Void> createTU(
  127.             @RequestParam("bk") String transportUnitBK,
  128.             @Validated(ValidationGroups.TransportUnit.Create.class) @RequestBody TransportUnitVO tu,
  129.             @RequestParam(value = "strict", required = false) Boolean strict,
  130.             HttpServletRequest req
  131.     ) {
  132.         if (Boolean.TRUE.equals(strict)) {
  133.             // check if already exists ...
  134.             try {
  135.                 service.findByBarcode(transportUnitBK);
  136.                 throw new ResourceExistsException(translator.translate(TU_EXISTS, transportUnitBK), TU_EXISTS, transportUnitBK);
  137.             } catch (NotFoundException nfe) {
  138.                 // that's fine we just cast the exception thrown by the service
  139.             }
  140.         }
  141.         var created = service.create(transportUnitBK, tu.getTransportUnitType().getType(), tu.getActualLocation().getLocationId(), strict);
  142.         return ResponseEntity.created(getLocationURIForCreatedResource(req, created.getPersistentKey())).build();
  143.     }

  144.     @PostMapping(API_TRANSPORT_UNITS + "/synchronize")
  145.     public void synchronizeTU() {
  146.         service.synchronizeTransportUnits();
  147.     }

  148.     @DeleteMapping(value = API_TRANSPORT_UNITS + "/{pKey}")
  149.     public ResponseEntity<Void> deleteTU(@PathVariable("pKey") String pKey) {
  150.         service.delete(pKey);
  151.         return ResponseEntity.noContent().build();
  152.     }

  153.     @PostMapping(value = API_TRANSPORT_UNITS, params = {"actualLocation", "tut"}, produces = MEDIA_TYPE)
  154.     public ResponseEntity<TransportUnitVO> createTU(
  155.             @RequestParam(value = "bk", required = false) String transportUnitBK,
  156.             @RequestParam("actualLocation") String actualLocation,
  157.             @RequestParam("tut") String tut,
  158.             @RequestParam(value = "strict", required = false) Boolean strict,
  159.             HttpServletRequest req
  160.     ) {
  161.         if (Boolean.TRUE.equals(strict)) {

  162.             if (transportUnitBK == null || transportUnitBK.isEmpty()) {
  163.                 throw new IllegalArgumentException(translator.translate(TU_BARCODE_MISSING));
  164.             }

  165.             // check if already exists ...
  166.             try {
  167.                 service.findByBarcode(transportUnitBK);
  168.                 throw new ResourceExistsException(translator.translate(TU_EXISTS, transportUnitBK), TU_EXISTS, transportUnitBK);
  169.             } catch (NotFoundException nfe) {
  170.                 // that's fine we just cast the exception thrown by the service
  171.             }
  172.         }
  173.         var created = transportUnitBK == null
  174.                 ? service.createNew(tut, actualLocation)
  175.                 : service.create(transportUnitBK, tut, actualLocation, strict);
  176.         return ResponseEntity
  177.                 .created(getLocationURIForCreatedResource(req, created.getPersistentKey()))
  178.                 .body(convertAndLinks(created))
  179.                 ;
  180.     }

  181.     @Validated(ValidationGroups.TransportUnit.Update.class)
  182.     @PutMapping(value = API_TRANSPORT_UNITS, params = {"bk"}, produces = MEDIA_TYPE)
  183.     public ResponseEntity<TransportUnitVO> updateTU(
  184.             @RequestParam("bk") String transportUnitBK,
  185.             @Valid @RequestBody TransportUnitVO tu
  186.     ) {
  187.         return ResponseEntity.ok(
  188.                 convertAndLinks(service.update(barcodeGenerator.convert(transportUnitBK), mapper.convert(tu)))
  189.         );
  190.     }

  191.     @PatchMapping(value = API_TRANSPORT_UNITS, params = {"bk", "newLocation"}, produces = MEDIA_TYPE)
  192.     public ResponseEntity<TransportUnitVO> moveTU(
  193.             @RequestParam("bk") String transportUnitBK,
  194.             @RequestParam("newLocation") String newLocation
  195.     ) {
  196.         return ResponseEntity.ok(
  197.                 convertAndLinks(service.moveTransportUnit(barcodeGenerator.convert(transportUnitBK), newLocation))
  198.         );
  199.     }

  200.     @PostMapping(value = API_TRANSPORT_UNIT + "/error", params = {"bk", "errorCode"}, produces = MEDIA_TYPE)
  201.     public ResponseEntity<Void> addErrorToTransportUnit(
  202.             @RequestParam("bk") String transportUnitBK,
  203.             @RequestParam(value = "errorCode") String errorCode
  204.     ) {
  205.         service.addError(transportUnitBK, UnitError.newBuilder()
  206.                 .errorNo(errorCode)
  207.                 .build()
  208.         );
  209.         return ResponseEntity.noContent().build();
  210.     }

  211.     @GetMapping(API_TRANSPORT_UNITS + "/index")
  212.     public ResponseEntity<Index> index() {
  213.         return ResponseEntity.ok(
  214.                 new Index(
  215.                         linkTo(methodOn(TransportUnitController.class).createTU("{transportUnitBK}", null, true, null)).withRel("transport-unit-createtuwithbody"),
  216.                         linkTo(methodOn(TransportUnitController.class).createTU("{transportUnitBK}", "{actualLocation}", "{transportUnitType}", true, null)).withRel("transport-unit-createtuwithparams"),
  217.                         linkTo(methodOn(TransportUnitController.class).deleteTU("{pKey}")).withRel("transport-unit-deletebypkey"),
  218.                         linkTo(methodOn(TransportUnitController.class).findTransportUnitByPKey("1")).withRel("transport-unit-findbypkey"),
  219.                         linkTo(methodOn(TransportUnitController.class).findTransportUnit("{transportUnitBK}")).withRel("transport-unit-findbybarcode"),
  220.                         linkTo(methodOn(TransportUnitController.class).findTransportUnits(asList("{transportUnitBK-1}", "{transportUnitBK-n}"))).withRel("transport-unit-findbybarcodes"),
  221.                         linkTo(methodOn(TransportUnitController.class).findTransportUnitsOn("{actualLocation.locationId}")).withRel("transport-unit-findonlocation"),
  222.                         linkTo(methodOn(TransportUnitController.class).blockTransportUnit("{transportUnitBK}")).withRel("transport-unit-block"),
  223.                         linkTo(methodOn(TransportUnitController.class).unblockTransportUnit("{transportUnitBK}")).withRel("transport-unit-unblock"),
  224.                         linkTo(methodOn(TransportUnitController.class).qcTransportUnit("{transportUnitBK}")).withRel("transport-unit-qc")
  225.                 )
  226.         );
  227.     }

  228.     @PostMapping(value = TransportApiConstants.API_TRANSPORT_UNITS + "/block", params = {"bk"})
  229.     public ResponseEntity<Void> blockTransportUnit(@NotBlank @RequestParam("bk") String transportUnitBK) {
  230.         service.setState(transportUnitBK, TransportUnitState.BLOCKED.name());
  231.         return ResponseEntity.noContent().build();
  232.     }

  233.     @PostMapping(value = TransportApiConstants.API_TRANSPORT_UNITS + "/available", params = {"bk"})
  234.     public ResponseEntity<Void> unblockTransportUnit(@NotBlank @RequestParam("bk") String transportUnitBK) {
  235.         service.setState(transportUnitBK, TransportUnitState.AVAILABLE.name());
  236.         return ResponseEntity.noContent().build();
  237.     }

  238.     @PostMapping(value = TransportApiConstants.API_TRANSPORT_UNITS + "/quality-check", params = {"bk"})
  239.     public ResponseEntity<Void> qcTransportUnit(@NotBlank @RequestParam("bk") String transportUnitBK) {
  240.         service.setState(transportUnitBK, TransportUnitState.QUALITY_CHECK.name());
  241.         return ResponseEntity.noContent().build();
  242.     }

  243.     @PostMapping(value = TransportApiConstants.API_TRANSPORT_UNITS, params = {"bk", "state"})
  244.     public ResponseEntity<Void> changeState(
  245.             @NotBlank @RequestParam("bk") String transportUnitBK,
  246.             @NotBlank @RequestParam("state") String newState) {
  247.         service.setState(transportUnitBK, newState);
  248.         return ResponseEntity.noContent().build();
  249.     }

  250.     private TransportUnitVO addLinks(TransportUnitVO result) {
  251.         result.add(
  252.                 new SimpleLink(linkTo(methodOn(TransportUnitController.class).findTransportUnitByPKey(result.getpKey())).withSelfRel()),
  253.                 new SimpleLink(linkTo(methodOn(TransportUnitTypeController.class).findTransportUnitType(result.getTransportUnitType().getType())).withRel("transport-unit-type"))
  254.         );
  255.         if (result.getActualLocation() != null) {
  256.             result.add(
  257.                     new SimpleLink(linkTo(methodOn(LocationController.class).findByCoordinate(result.getActualLocation().getLocationId())).withRel("actual-location"))
  258.             );
  259.         }
  260.         return result;
  261.     }

  262.     private TransportUnitVO convertAndLinks(TransportUnit entity) {
  263.         return addLinks(
  264.                 mapper.convertToVO(entity)
  265.         );
  266.     }

  267.     private List<TransportUnitVO> convertAndLinks(List<TransportUnit> entities) {
  268.         return entities.stream()
  269.                 .map(mapper::convertToVO)
  270.                 .map(this::addLinks)
  271.                 .toList();
  272.     }
  273. }