LocationRemovalManagerImpl.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.impl;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.ameba.annotation.Measured;
import org.ameba.annotation.TxService;
import org.ameba.exception.NotFoundException;
import org.ameba.i18n.Translator;
import org.openwms.common.location.Location;
import org.openwms.common.location.LocationRemovalManager;
import org.openwms.common.location.api.events.LocationEvent;
import org.openwms.common.location.events.DeletionFailedEvent;
import org.openwms.common.location.impl.registration.RegistrationService;
import org.openwms.common.location.impl.registration.ReplicaRegistry;
import org.openwms.core.listener.RemovalNotAllowedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static java.lang.String.format;
import static org.openwms.common.CommonMessageCodes.LOCATION_NOT_FOUND_BY_PKEY;
/**
* A LocationRemovalManagerImpl.
*
* @author Heiko Scherrer
*/
@TxService
class LocationRemovalManagerImpl implements LocationRemovalManager {
private static final Logger LOGGER = LoggerFactory.getLogger(LocationRemovalManagerImpl.class);
private final ApplicationEventPublisher eventPublisher;
private final Translator translator;
private final RestTemplate aLoadBalanced;
private final LocationRepository repository;
private final RegistrationService registrationService;
LocationRemovalManagerImpl(ApplicationEventPublisher eventPublisher, Translator translator, RestTemplate aLoadBalanced,
LocationRepository repository, RegistrationService registrationService) {
this.eventPublisher = eventPublisher;
this.translator = translator;
this.aLoadBalanced = aLoadBalanced;
this.repository = repository;
this.registrationService = registrationService;
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public void tryDelete(@NotBlank String pKey) {
var locationOpt = repository.findBypKey(pKey);
if (locationOpt.isEmpty()) {
LOGGER.warn("Location with pKey [{}] shall be deleted but it does not exist", pKey);
throw new NotFoundException(translator, LOCATION_NOT_FOUND_BY_PKEY, new String[]{pKey}, pKey);
}
try {
deleteInternal(locationOpt.get());
} catch (Exception e) {
// if any failure occurs, send a persistent async message to release the deletion for everyone
eventPublisher.publishEvent(new DeletionFailedEvent(locationOpt.get().getPersistentKey()));
throw new RemovalNotAllowedException(e.getMessage());
}
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public void deleteAll(@NotNull Collection<Location> locations) {
repository.deleteAll(locations);
}
private void deleteInternal(Location location) {
verifyNoTransportUnitsOn(location);
var registeredServices = registrationService.getAllRegistered();
if (registeredServices.isEmpty()) {
LOGGER.info("No services registered that are requested for removal");
}
// first ask all services and call the requestRemovalEndpoint
// might throw RemovalNotAllowedException and exit
for (var srv : registeredServices) {
askForRemoval(srv, List.of(location.getPersistentKey()));
}
// if all are fine then call the removeEndpoint to mark the Location as deleted and not be visible in the foreign service
for (var srv : registeredServices) {
removeLocation(srv, List.of(location.getPersistentKey()));
}
deleteLocation(location);
}
private void verifyNoTransportUnitsOn(Location location) {
if (repository.doesTUonLocationExists(location.getPk())) {
throw new RemovalNotAllowedException("TransportUnit exist on Location [%s]".formatted(location.getLocationId()));
}
LOGGER.info("No TransportUnits exist on Location [{}]", location.getLocationId());
}
private void verifyNoTransportUnitsOn(Collection<String> pKeys) {
if (repository.doesTUonLocationExists(pKeys)) {
throw new RemovalNotAllowedException("TransportUnit exist on one or more Locations");
}
}
private void deleteLocation(Location location) {
verifyNoTransportUnitsOn(location);
// if ALL still agree, then send a persistent async message to commit the deletion after the transaction
eventPublisher.publishEvent(LocationEvent.of(location, LocationEvent.LocationEventType.DELETED));
// and finally delete the Location
repository.delete(location);
}
/**
* {@inheritDoc}
*
* At first the implementation checks if any {@code TransportUnit} is booked onto one of the {@link Location}s and as second all parties
* of interest are asked if deletion in the current moment is okay for them, too.
*/
@Override
@Measured
public boolean allowedToDelete(@NotNull Collection<String> pKeys) {
verifyNoTransportUnitsOn(pKeys);
var registeredServices = registrationService.getAllRegistered();
try {
// first ask all services and call the requestRemovalEndpoint
// might throw RemovalNotAllowedException and exit
for (var srv : registeredServices) {
askForRemoval(srv, pKeys);
}
LOGGER.debug("It's allowed to delete the given Locations");
return true;
} catch (Exception e) {
LOGGER.warn(e.getMessage(), e);
LOGGER.debug("It's NOT allowed to delete the given Locations");
return false;
}
}
/**
* {@inheritDoc}
*
* Call interesing parties to mark the {@link Location}s for deletion.
*/
@Override
@Measured
public void markForDeletion(@NotNull Collection<String> pKeys) {
var registeredServices = registrationService.getAllRegistered();
try {
for (var srv : registeredServices) {
removeLocation(srv, pKeys);
}
LOGGER.debug("All Locations marked for deletion");
} catch (Exception e) {
// if any failure occurs, send a persistent async message to release the deletion for everyone
LOGGER.debug("Location cannot be marked for deletion, sending events to rollback");
for (var pKey : pKeys) {
eventPublisher.publishEvent(new DeletionFailedEvent(pKey));
}
throw e;
}
}
private void askForRemoval(ReplicaRegistry srv, Collection<String> pKeys) {
var headers = new HttpHeaders();
boolean result;
var endpoint = "http://" + srv.getApplicationName() + srv.getRequestRemovalEndpoint();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Request for removal to the service with API [{}]", endpoint);
}
try {
var response = aLoadBalanced.exchange(
endpoint,
HttpMethod.POST,
new HttpEntity<List<String>>(new ArrayList<>(pKeys), headers),
Boolean.class
);
result = response.getBody() != null && response.getBody();
if (result) {
LOGGER.info("Service [{}] allows to remove all Locations", srv.getApplicationName());
} else {
LOGGER.info("Service [{}] does not allow to remove Locations", srv.getApplicationName());
}
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
throw new RemovalNotAllowedException("Exception. Removal of Locations is declined by service [%s]".formatted(srv.getApplicationName()));
}
if (!result) {
throw new RemovalNotAllowedException("Removal of Locations has been declined by service [%s]".formatted(srv.getApplicationName()));
}
}
private void removeLocation(ReplicaRegistry srv, Collection<String> pKeys) {
var headers = new HttpHeaders();
var endpoint = "http://" + srv.getApplicationName() + srv.getRemovalEndpoint();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Ask for removal the service with API [{}]", endpoint);
}
try {
aLoadBalanced.exchange(
endpoint,
HttpMethod.DELETE,
new HttpEntity<List<String>>(new ArrayList<>(pKeys), headers),
Void.class
);
LOGGER.info("Service [{}] removed Locations", srv.getApplicationName());
} catch (Exception e) {
throw new RemovalNotAllowedException(format("Exception. Removal of Locations is declined by service [%s]", srv.getApplicationName()));
}
}
}