LocationController.java
/*
* Copyright 2005-2024 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.common.location;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.ameba.exception.BusinessRuntimeException;
import org.ameba.exception.NotFoundException;
import org.ameba.http.MeasuredRestController;
import org.ameba.i18n.Translator;
import org.openwms.common.location.api.ErrorCodeVO;
import org.openwms.common.location.api.LocationVO;
import org.openwms.common.location.api.LockMode;
import org.openwms.common.location.api.LockType;
import org.openwms.common.location.api.ValidationGroups;
import org.openwms.core.SpringProfiles;
import org.openwms.core.http.AbstractWebController;
import org.openwms.core.http.Index;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.PatchMapping;
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.io.Serializable;
import java.util.List;
import java.util.function.BiConsumer;
import static java.util.Arrays.asList;
import static org.openwms.common.CommonMessageCodes.LOCATION_ID_INVALID;
import static org.openwms.common.CommonMessageCodes.LOCATION_NOT_FOUND_BY_ERP_CODE;
import static org.openwms.common.CommonMessageCodes.LOCATION_NOT_FOUND_BY_ID;
import static org.openwms.common.CommonMessageCodes.LOCATION_NOT_FOUND_BY_PLC_CODE;
import static org.openwms.common.CommonMessageCodes.LOCK_MODE_UNSUPPORTED;
import static org.openwms.common.CommonMessageCodes.LOCK_TYPE_UNSUPPORTED;
import static org.openwms.common.location.api.LocationApiConstants.API_LOCATION;
import static org.openwms.common.location.api.LocationApiConstants.API_LOCATIONS;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
/**
* A LocationController.
*
* @author Heiko Scherrer
*/
@Profile("!" + SpringProfiles.IN_MEMORY)
@Validated
@MeasuredRestController
public class LocationController extends AbstractWebController {
private final LocationMapper mapper;
private final Translator translator;
private final LocationService locationService;
private final LocationRemovalManager locationRemovalManager;
LocationController(LocationService locationService, LocationMapper mapper, Translator translator, LocationRemovalManager locationRemovalManager) {
this.locationService = locationService;
this.mapper = mapper;
this.translator = translator;
this.locationRemovalManager = locationRemovalManager;
}
@PostMapping(value = API_LOCATIONS)
@Validated(ValidationGroups.Create.class)
public ResponseEntity<LocationVO> createLocation(@Valid @RequestBody LocationVO location, HttpServletRequest req) {
var created = locationService.create(mapper.convertVO(location));
var result = mapper.convertToVO(created);
addSelfLink(result);
return ResponseEntity
.created(super.getLocationURIForCreatedResource(req, created.getPersistentKey()))
.header(HttpHeaders.CONTENT_TYPE, LocationVO.MEDIA_TYPE)
.body(result);
}
@PutMapping(value = API_LOCATIONS)
@Validated(ValidationGroups.Update.class)
public ResponseEntity<LocationVO> updateLocation(@Valid @RequestBody LocationVO location) {
var updated = locationService.save(mapper.convertVO(location));
var result = mapper.convertToVO(updated);
addSelfLink(result);
return ResponseEntity.status(HttpStatus.OK).header(HttpHeaders.CONTENT_TYPE, LocationVO.MEDIA_TYPE).body(result);
}
@DeleteMapping(value = API_LOCATIONS + "/{pKey}")
public ResponseEntity<Void> deleteLocation(@PathVariable("pKey") String pKey) {
locationRemovalManager.tryDelete(pKey);
return ResponseEntity.noContent().build();
}
@GetMapping(value = API_LOCATIONS + "/{pKey}")
public ResponseEntity<LocationVO> findByPKey(@PathVariable("pKey") String pKey) {
var location = locationService.findByPKey(pKey);
var result = mapper.convertToVO(location);
addSelfLink(result);
return ResponseEntity.status(HttpStatus.OK).header(HttpHeaders.CONTENT_TYPE, LocationVO.MEDIA_TYPE).body(result);
}
private void addSelfLink(LocationVO result) {
result.add(linkTo(methodOn(LocationController.class).findByPKey(result.getpKey())).withRel("location-findbypkey"));
}
@GetMapping(value = API_LOCATIONS, params = {"locationId"})
public ResponseEntity<LocationVO> findByCoordinate(@RequestParam("locationId") String locationId) {
if (!LocationPK.isValid(locationId)) {
// here we need to throw an NFE because Feign needs to cast it into an Optional. IAE won't work!
throw new NotFoundException(translator, LOCATION_ID_INVALID, new String[]{locationId}, locationId);
}
var location = locationService.findByLocationPk(LocationPK.fromString(locationId))
.orElseThrow(() -> new NotFoundException(
translator,
LOCATION_NOT_FOUND_BY_ID,
new String[]{locationId},
locationId
));
var result = mapper.convertToVO(location);
addSelfLink(result);
return ResponseEntity.status(HttpStatus.OK).header(HttpHeaders.CONTENT_TYPE, LocationVO.MEDIA_TYPE).body(result);
}
@GetMapping(value = API_LOCATIONS, params = {"erpCode"})
public ResponseEntity<LocationVO> findByErpCode(@RequestParam("erpCode") String erpCode) {
var location = locationService.findByErpCode(erpCode).orElseThrow(() -> locationNotFound(erpCode));
var result = mapper.convertToVO(location);
addSelfLink(result);
return ResponseEntity.status(HttpStatus.OK).header(HttpHeaders.CONTENT_TYPE, LocationVO.MEDIA_TYPE).body(result);
}
@GetMapping(value = API_LOCATIONS, params = {"plcCode"})
public ResponseEntity<LocationVO> findByPlcCode(@RequestParam("plcCode") String plcCode) {
var location = locationService.findByPlcCode(plcCode)
.orElseThrow(() -> new NotFoundException(
translator,
LOCATION_NOT_FOUND_BY_PLC_CODE,
new String[]{plcCode},
plcCode
));
var result = mapper.convertToVO(location);
addSelfLink(result);
return ResponseEntity.status(HttpStatus.OK).header(HttpHeaders.CONTENT_TYPE, LocationVO.MEDIA_TYPE).body(result);
}
@GetMapping(value = API_LOCATIONS, params = {"locationGroupNames"})
public ResponseEntity<List<LocationVO>> findForLocationGroups(
@RequestParam("locationGroupNames") List<String> locationGroupNames) {
var locations = locationService.findAllOf(locationGroupNames);
var result = mapper.convertToVO(locations);
result.forEach(this::addSelfLink);
return ResponseEntity.status(HttpStatus.OK).header(HttpHeaders.CONTENT_TYPE, LocationVO.MEDIA_TYPE).body(result);
}
@PatchMapping(value = API_LOCATION + "/{pKey}", params = "op=change-state")
public ResponseEntity<Void> changeState(
@PathVariable(name = "pKey") String pKey,
@RequestParam(name = "op") String op,
@RequestBody ErrorCodeVO errorCode
) {
locationService.changeState(pKey, errorCode);
return ResponseEntity.noContent().build();
}
@PatchMapping(value = API_LOCATION, params = {"locationId", "op=change-state"})
public ResponseEntity<Void> changeState(
@RequestParam(name = "locationId") String locationId,
@RequestBody ErrorCodeVO errorCode
) {
locationService.changeState(LocationPK.fromString(locationId), errorCode);
return ResponseEntity.noContent().build();
}
/**
* Change the current {@code mode} a {@code Location}, identified by {@code erpCode}.
*
* @param erpCode The ERP code of the Location
* @param type The type of lock to apply to the Location
* @param mode The mode to apply to the Locations lock
*/
@PostMapping(path = API_LOCATIONS , params = {"erpCode", "type!=PERMANENT_LOCK", "mode"})
public ResponseEntity<Void> changeState(
@RequestParam("erpCode") String erpCode,
@RequestParam("type") LockType type,
@RequestParam("mode") LockMode mode,
@RequestParam(value = "plcState", required = false) Integer plcState
) {
var location = locationService.findByErpCode(erpCode).orElseThrow(() -> locationNotFound(erpCode));
if (type == LockType.ALLOCATION_LOCK) {
changeLocation(
mode,
location,
plcState,
(l, code) -> locationService.changeState(l.getPersistentKey(), code)
);
} else {
unsupportedOperation(type);
}
return ResponseEntity.noContent().build();
}
@GetMapping(API_LOCATIONS)
public ResponseEntity<List<LocationVO>> findByCoordinate(
@RequestParam(value = "area", required = false, defaultValue = "%") String area,
@RequestParam(value = "aisle", required = false, defaultValue = "%") String aisle,
@RequestParam(value = "x", required = false, defaultValue = "%") String x,
@RequestParam(value = "y", required = false, defaultValue = "%") String y,
@RequestParam(value = "z", required = false, defaultValue = "%") String z
) {
var pk = LocationPK.of(area, aisle, x, y, z);
var result = mapper.convertToVO(locationService.findLocations(pk));
result.forEach(this::addSelfLink);
return result.isEmpty()
? ResponseEntity.notFound().build()
: ResponseEntity.status(HttpStatus.OK).header(HttpHeaders.CONTENT_TYPE, LocationVO.MEDIA_TYPE).body(result);
}
@GetMapping(API_LOCATIONS + "/index")
public ResponseEntity<Index> index() {
return ResponseEntity.ok(
new Index(
linkTo(methodOn(LocationController.class).createLocation(new LocationVO("locationId"), null)).withRel("location-create"),
linkTo(methodOn(LocationController.class).updateLocation(new LocationVO("locationId"))).withRel("location-updatelocation"),
linkTo(methodOn(LocationController.class).deleteLocation("pKey")).withRel("location-deletelocation"),
linkTo(methodOn(LocationController.class).findByPKey("pKey")).withRel("location-findbypkey"),
linkTo(methodOn(LocationController.class).findByCoordinate("AREA/AISLE/X/Y/Z")).withRel("location-findbycoordinate"),
linkTo(methodOn(LocationController.class).findByCoordinate("area", "aisle", "x", "y", "z")).withRel("location-findbycoordinate-wc"),
linkTo(methodOn(LocationController.class).findByErpCode("ERP_CODE")).withRel("location-findbyerpcode"),
linkTo(methodOn(LocationController.class).findByPlcCode("PLC_CODE")).withRel("location-findbyplccode"),
linkTo(methodOn(LocationController.class).findForLocationGroups(asList("LG1", "LG2"))).withRel("location-forlocationgroup"),
linkTo(methodOn(LocationController.class).changeState("pKey", "change-state", ErrorCodeVO.LOCK_STATE_IN_AND_OUT)).withRel("location-changestate")
)
);
}
private void changeLocation(LockMode mode, Target target, Integer plcState, BiConsumer<Target, ErrorCodeVO> fnc) {
ErrorCodeVO state;
switch (mode) {
case IN -> {
state = ErrorCodeVO.LOCK_STATE_IN;
state.setPlcState(plcState);
fnc.accept(target, state);
}
case OUT -> {
state = ErrorCodeVO.LOCK_STATE_OUT;
state.setPlcState(plcState);
fnc.accept(target, state);
}
case IN_AND_OUT -> {
state = ErrorCodeVO.LOCK_STATE_IN_AND_OUT;
state.setPlcState(plcState);
fnc.accept(target, state);
}
case NONE -> {
state = ErrorCodeVO.UNLOCK_STATE_IN_AND_OUT;
state.setPlcState(plcState);
fnc.accept(target, state);
}
default -> unsupportedOperation(mode);
}
}
private NotFoundException locationNotFound(String erpCode) {
return new NotFoundException(translator, LOCATION_NOT_FOUND_BY_ERP_CODE, new String[]{erpCode}, erpCode);
}
private void unsupportedOperation(LockMode mode) {
throw new BusinessRuntimeException(translator, LOCK_MODE_UNSUPPORTED, new Serializable[]{mode}, mode);
}
private void unsupportedOperation(LockType type) {
throw new BusinessRuntimeException(translator, LOCK_TYPE_UNSUPPORTED, new Serializable[]{type}, type);
}
}