TransportUnitServiceImpl.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.transport.impl;
import jakarta.validation.Valid;
import jakarta.validation.Validator;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import org.ameba.annotation.Measured;
import org.ameba.annotation.TxService;
import org.ameba.exception.NotFoundException;
import org.ameba.exception.ResourceExistsException;
import org.ameba.exception.ServiceLayerException;
import org.ameba.i18n.Translator;
import org.openwms.common.CommonMessageCodes;
import org.openwms.common.StateChangeException;
import org.openwms.common.location.Location;
import org.openwms.common.location.LocationPK;
import org.openwms.common.location.LocationService;
import org.openwms.common.transport.TransportUnit;
import org.openwms.common.transport.TransportUnitMapper;
import org.openwms.common.transport.TransportUnitService;
import org.openwms.common.transport.TransportUnitType;
import org.openwms.common.transport.UnitError;
import org.openwms.common.transport.api.ValidationGroups;
import org.openwms.common.transport.api.commands.TUCommand;
import org.openwms.common.transport.api.messages.TransportUnitMO;
import org.openwms.common.transport.barcode.Barcode;
import org.openwms.common.transport.barcode.BarcodeGenerator;
import org.openwms.common.transport.events.TransportUnitEvent;
import org.openwms.common.transport.spi.NotApprovedException;
import org.openwms.common.transport.spi.TransportUnitMoveApproval;
import org.openwms.common.transport.spi.TransportUnitStateChangeApproval;
import org.openwms.core.exception.IllegalConfigurationValueException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.validation.annotation.Validated;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import static java.lang.String.format;
import static org.ameba.system.ValidationUtil.validate;
import static org.openwms.common.transport.api.commands.TUCommand.Type.REMOVING;
/**
* A TransportUnitServiceImpl is a Spring managed bean that deals with TransportUnits.
*
* @author Heiko Scherrer
*/
@Validated
@TxService
class TransportUnitServiceImpl implements TransportUnitService {
private static final Logger LOGGER = LoggerFactory.getLogger(TransportUnitServiceImpl.class);
private static final String NO_LOCATION_SET = "The actualLocation must be given in order to create a TransportUnit";
private static final String NO_BARCODE = "The barcode must be given in order to create a TransportUnit";
private static final String NO_TRANSPORT_UNIT_TYPE = "The transportUnitType must be given in order to create a TransportUnit";
private final ApplicationEventPublisher publisher;
private final Validator validator;
private final Translator translator;
private final BarcodeGenerator barcodeGenerator;
private final TransportUnitMapper mapper;
private final TransportUnitRepository repository;
private final TransportUnitTypeRepository transportUnitTypeRepository;
private final TransportUnitStateChangeApproval stateChangeApproval;
private final TransportUnitMoveApproval moveApproval;
private final LocationService locationService;
private final String deleteTransportUnitMode;
@SuppressWarnings("squid:S107")
TransportUnitServiceImpl(ApplicationEventPublisher publisher, Validator validator, Translator translator,
BarcodeGenerator barcodeGenerator, TransportUnitMapper mapper, TransportUnitRepository repository,
TransportUnitTypeRepository transportUnitTypeRepository,
@Autowired(required = false) TransportUnitStateChangeApproval stateChangeApproval,
@Autowired(required = false) TransportUnitMoveApproval moveApproval,
LocationService locationService, @Value("${owms.common.delete-transport-unit-mode}") String deleteTransportUnitMode) {
this.publisher = publisher;
this.validator = validator;
this.translator = translator;
this.barcodeGenerator = barcodeGenerator;
this.mapper = mapper;
this.repository = repository;
this.transportUnitTypeRepository = transportUnitTypeRepository;
this.stateChangeApproval = stateChangeApproval;
this.moveApproval = moveApproval;
this.locationService = locationService;
this.deleteTransportUnitMode = deleteTransportUnitMode;
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public @NotNull TransportUnit create(
@NotBlank String transportUnitBK, @NotNull TransportUnitType transportUnitType,
@NotNull LocationPK actualLocation, Boolean strict) {
Assert.notNull(transportUnitBK, NO_BARCODE);
Assert.notNull(transportUnitType, NO_TRANSPORT_UNIT_TYPE);
Assert.notNull(actualLocation, NO_LOCATION_SET);
return createInternal(
barcodeGenerator.convert(transportUnitBK),
transportUnitType.getType(),
strict,
() -> locationService.findByLocationPk(actualLocation)
.orElseThrow(() -> new NotFoundException(format("No Location with locationPk [%s] found", actualLocation)))
);
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public @NotNull TransportUnit create(
@NotBlank String transportUnitBK, @NotBlank String transportUnitType,
@NotBlank String actualLocation, Boolean strict) {
Assert.notNull(actualLocation, NO_LOCATION_SET);
return createInternal(
barcodeGenerator.convert(transportUnitBK),
transportUnitType,
strict,
() -> locationService.findByLocationId(actualLocation)
.orElseThrow(() -> new NotFoundException(format("No Location with actual location [%s] found", actualLocation)))
);
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public @NotNull TransportUnit createNew(@NotBlank String transportUnitType, @NotBlank String actualLocation) {
var nextBarcode = barcodeGenerator.generate(transportUnitType, actualLocation);
return createInternal(
nextBarcode,
transportUnitType,
false,
() -> locationService.findByLocationId(actualLocation)
.orElseThrow(() -> new NotFoundException(format("No Location with actual location [%s] found", actualLocation)))
);
}
private TransportUnit createInternal(Barcode barcode, String transportUnitType, Boolean strict, Supplier<Location> locationResolver) {
Assert.notNull(barcode, NO_BARCODE);
Assert.hasText(transportUnitType, NO_TRANSPORT_UNIT_TYPE);
var optTransportUnit = repository.findByBarcode(barcode);
if (strict == null || Boolean.FALSE.equals(strict)) {
if (optTransportUnit.isPresent()) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("TransportUnit with Barcode [{}] already exists, silently returning the existing one", barcode);
}
return optTransportUnit.get();
}
} else {
optTransportUnit.ifPresent(tu -> {
throw new ResourceExistsException(format("TransportUnit with id [%s] already exists", barcode));
});
}
var actualLocation = locationResolver.get();
var type = transportUnitTypeRepository.findByType(transportUnitType).orElseThrow(() -> new NotFoundException(format("TransportUnitType [%s] not found", transportUnitType)));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Creating a TransportUnit with Barcode [{}] of Type [{}] on Location [{}]", barcode, transportUnitType, actualLocation);
}
var transportUnit = repository.save(new TransportUnit(barcode, type, actualLocation));
publisher.publishEvent(
TransportUnitEvent.newBuilder()
.tu(transportUnit)
.type(TransportUnitEvent.TransportUnitEventType.CREATED)
.build()
);
return transportUnit;
}
/**
* {@inheritDoc}
*/
@Override
@Validated(ValidationGroups.TransportUnit.Update.class)
@Measured
public @NotNull TransportUnit update(@NotNull Barcode barcode, final @Valid @NotNull TransportUnit tu) {
if (!barcode.equals(tu.getBarcode())) {
throw new ServiceLayerException("Mismatch between Barcode and tu.Barcode in API");
}
var existing = findByBarcodeInternal(barcode);
var updated = new TransportUnit(barcode);
updated.setTransportUnitType(existing.getTransportUnitType());
mapper.copy(existing, updated);
if (tu.getActualLocation() != null && tu.getActualLocation().isNew()) {
moveInternal(existing, this.locationService.findByLocationPk(tu.getActualLocation().getLocationId())
.orElseThrow(() -> new NotFoundException(format("Location [%s] not found", tu.getActualLocation()))));
}
var saved = repository.save(existing);
publisher.publishEvent(TransportUnitEvent.newBuilder()
.tu(saved)
.type(TransportUnitEvent.TransportUnitEventType.CHANGED)
.build()
);
return saved;
}
private void approveMove(TransportUnit transportUnit, Location newLocation) {
if (moveApproval == null) {
return;
}
try {
moveApproval.approve(transportUnit, newLocation);
} catch (NotApprovedException nae) {
LOGGER.error(nae.getMessage(), nae);
throw new StateChangeException("Not allowed to move the TransportUnit [%s] to [%s]".formatted(transportUnit.getBarcode(), newLocation));
}
}
private TransportUnit moveInternal(TransportUnit transportUnit, Location target) {
if (transportUnit.getActualLocation().getLocationId().equals(target.getLocationId())) {
LOGGER.debug("TransportUnit [{}] is already booked on Location [{}]", transportUnit.getBarcode(), target.getLocationId());
return transportUnit;
}
approveMove(transportUnit, target);
transportUnit.setActualLocation(target);
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Moving TransportUnit with barcode [{}] from Location [{}] to Location [{}]", transportUnit.getBarcode(),
transportUnit.getActualLocation(), target.getLocationId());
}
var saved = repository.save(transportUnit);
publisher.publishEvent(
TransportUnitEvent.newBuilder()
.tu(saved)
.type(TransportUnitEvent.TransportUnitEventType.MOVED)
.previousLocation(transportUnit.getActualLocation())
.actualLocation(transportUnit.getActualLocation())
.build()
);
return saved;
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("javasecurity:S5145")
@Override
@Measured
public @NotNull TransportUnit moveTransportUnit(@NotNull Barcode barcode, @NotBlank String targetLocation) {
var transportUnit = findByBarcodeInternal(barcode);
var target = LocationPK.isValid(targetLocation)
? locationService.findByLocationPk(LocationPK.fromString(targetLocation))
.orElseThrow(() -> new NotFoundException(format("No Location with LocationPk [%s] found", LocationPK.fromString(targetLocation))))
: locationService.findByErpCode(targetLocation).orElseGet(() -> locationService.findByPlcCode(targetLocation)
.orElseThrow(() -> new NotFoundException(format("No Location with LocationPk [%s] found", LocationPK.fromString(targetLocation)))));
return moveInternal(transportUnit, target);
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public void deleteTransportUnits(@NotNull List<TransportUnit> transportUnits) {
if (!transportUnits.isEmpty()) {
var tus = new ArrayList<>(transportUnits);
tus.sort((o1, o2) -> {
if (o1.hasChildren() && o2.hasChildren() ||
!o1.hasChildren() && !o2.hasChildren()) {
return 0;
} else if (!o1.hasChildren() && o2.hasChildren()) {
return -1;
} else {
return 1;
}
});
tus.forEach(this::delete);
}
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public void delete(String pKey) {
var existing = this.findByPKeyInternal(pKey);
this.delete(existing);
}
/* we expect that the calling service spans the TX here, because the EL advice may come differently in the chain. */
@Transactional(propagation = Propagation.MANDATORY)
@EventListener
public void onEvent(TUCommand command) {
if (command.getType() == TUCommand.Type.REMOVE) {
validate(validator, command, ValidationGroups.TransportUnit.Remove.class);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Got command to REMOVE TransportUnit with pKey [{}]", command.getTransportUnit().getpKey());
}
deleteDefinitely(command.getTransportUnit().getpKey());
}
}
private void deleteDefinitely(String pKey) {
repository.findByPKey(pKey).ifPresent(tu -> {
repository.delete(tu);
publisher.publishEvent(
TransportUnitEvent.newBuilder()
.tu(tu)
.type(TransportUnitEvent.TransportUnitEventType.DELETED)
.build());
});
}
private void delete(TransportUnit transportUnit) {
if ("strict".equalsIgnoreCase(deleteTransportUnitMode)) {
deleteDefinitely(transportUnit.getPersistentKey());
} else if ("on-accept".equalsIgnoreCase(deleteTransportUnitMode)) {
var mo = TransportUnitMO.newBuilder()
.withPKey(transportUnit.getPersistentKey())
.withBarcode(transportUnit.getBarcode().getValue())
.build();
publisher.publishEvent(TUCommand.newBuilder(REMOVING)
.withTransportUnit(mo)
.build()
);
} else {
throw new IllegalConfigurationValueException(format("Configuration value [owms.common.delete-transport-unit-mode] is configured with invalid value [%s]", deleteTransportUnitMode));
}
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public @NotNull TransportUnit findByBarcode(@NotBlank String transportUnitBK) {
return findByBarcodeInternal(barcodeGenerator.convert(transportUnitBK));
}
private TransportUnit findByBarcodeInternal(Barcode barcode) {
return repository.findByBarcode(barcode)
.orElseThrow(() -> new NotFoundException(translator, CommonMessageCodes.TU_BARCODE_NOT_FOUND, new Serializable[]{barcode}, barcode));
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public @NotNull List<TransportUnit> findByBarcodes(@NotEmpty List<Barcode> barcodes) {
var tus = repository.findByBarcodeIn(barcodes);
return tus == null ? new ArrayList<>(0) : tus;
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public @NotNull List<TransportUnit> findOnLocation(@NotBlank String actualLocation) {
Assert.hasText(actualLocation, NO_LOCATION_SET);
var location = locationService.findByLocationIdOrThrow(actualLocation);
return repository.findByActualLocationOrderByActualLocationDate(location);
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public @NotNull TransportUnit findByPKey(@NotBlank String pKey) {
return this.findByPKeyInternal(pKey);
}
private TransportUnit findByPKeyInternal(String pKey) {
return repository.findByPKey(pKey).orElseThrow(() -> new NotFoundException(format("No TransportUnit with pKey [%s] found", pKey)));
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public void addError(@NotBlank String transportUnitBK, @NotNull UnitError unitError) {
var tu = this.findByBarcodeInternal(barcodeGenerator.convert(transportUnitBK));
tu.addError(unitError);
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public @NotNull List<TransportUnit> findAll() {
return repository.findAll();
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public TransportUnit changeTarget(@NotNull Barcode barcode, @NotBlank String targetLocation) {
var transportUnit = findByBarcodeInternal(barcode);
var location = locationService.findByLocationIdOrThrow(targetLocation);
transportUnit.setTargetLocation(location);
var saved = repository.save(transportUnit);
publisher.publishEvent(TransportUnitEvent.newBuilder()
.tu(saved)
.type(TransportUnitEvent.TransportUnitEventType.CHANGED).build()
);
return saved;
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public void setState(@NotBlank String transportUnitBK, @NotBlank String newState) {
var transportUnit = findByBarcodeInternal(barcodeGenerator.convert(transportUnitBK));
approveStateChange(transportUnit, newState);
LOGGER.debug("Setting state of TransportUnit [{}] to [{}]", transportUnitBK, newState);
transportUnit.setState(newState);
publisher.publishEvent(TransportUnitEvent.newBuilder()
.tu(transportUnit)
.type(TransportUnitEvent.TransportUnitEventType.STATE_CHANGE).build()
);
repository.save(transportUnit);
}
private void approveStateChange(TransportUnit transportUnit, String newState) {
if (stateChangeApproval == null) {
return;
}
try {
stateChangeApproval.approve(transportUnit, newState);
} catch (NotApprovedException nae) {
LOGGER.error(nae.getMessage(), nae);
throw new StateChangeException("Not allowed to change the state of TransportUnit [%s] to [%s]".formatted(transportUnit.getBarcode(), newState));
}
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public void synchronizeTransportUnits() {
var all = repository.findAll();
all.forEach(tu -> publisher.publishEvent(
TransportUnitEvent.newBuilder()
.tu(tu)
.type(TransportUnitEvent.TransportUnitEventType.CREATED)
.build()
));
}
}