TargetController.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 org.ameba.exception.BusinessRuntimeException;
import org.ameba.http.MeasuredRestController;
import org.ameba.i18n.Translator;
import org.openwms.common.location.api.ErrorCodeVO;
import org.openwms.common.location.api.LocationGroupMode;
import org.openwms.common.location.api.LocationGroupState;
import org.openwms.common.location.api.LockMode;
import org.openwms.common.location.api.LockType;
import org.openwms.common.location.api.events.TargetEvent;
import org.openwms.core.SpringProfiles;
import org.openwms.core.http.AbstractWebController;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.io.Serializable;
import java.util.function.BiConsumer;

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_TARGETS;
import static org.openwms.common.location.api.LocationGroupState.AVAILABLE;
import static org.openwms.common.location.api.LocationGroupState.NOT_AVAILABLE;

/**
 * A TargetController represents the REST API to handle the state and availability of {@code Target}s.
 *
 * @author Heiko Scherrer
 */
@Profile("!" + SpringProfiles.IN_MEMORY)
@Validated
@MeasuredRestController
class TargetController extends AbstractWebController {

    private final Translator translator;
    private final LocationService locationService;
    private final LocationGroupService locationGroupService;
    private final ApplicationContext ctx;

    TargetController(Translator translator, LocationService locationService, LocationGroupService locationGroupService,
                     ApplicationContext ctx) {
        this.translator = translator;
        this.locationService = locationService;
        this.locationGroupService = locationGroupService;
        this.ctx = ctx;
    }

    /**
     * Change the current {@code mode} a {@code Target}, identified by {@code targetBK} operates in.
     *
     * @param targetBK The business key of the Target, can be a {@code LocationPK} in String format or a LocationGroup name
     * @param type The type of lock to apply to the Target
     * @param mode The mode to apply to the Targets lock
     */
    @PostMapping(path = API_TARGETS + "/{targetBK}", params = {"type!=PERMANENT_LOCK", "mode"})
    public void changeState(
            @PathVariable("targetBK") String targetBK,
            @RequestParam("type") LockType type,
            @RequestParam("mode") LockMode mode
    ) {

            if (LocationPK.isValid(targetBK)) {

                var location = locationService.findByLocationIdOrThrow(targetBK);
                switch (type) {
                    case ALLOCATION_LOCK -> changeLocation(
                            mode,
                            location,
                            (l, code) -> locationService.changeState(l.getPersistentKey(), code)
                    );
                    case OPERATION_LOCK -> throw new UnsupportedOperationException("Changing the operation mode of Locations is currently not supported in the API");
                    default -> unsupportedOperation(type);
                }
            } else {

                var locationGroup = locationGroupService.findByNameOrThrow(targetBK);
                switch (type) {
                    case ALLOCATION_LOCK -> changeLocationGroupState(
                            mode,
                            locationGroup,
                            (lg, states) -> locationGroupService.changeGroupState(lg.getPersistentKey(), states[0], states[1])
                    );
                    case OPERATION_LOCK -> changeLocationGroupMode(
                            mode,
                            locationGroup,
                            (lg, m) -> locationGroupService.changeOperationMode(lg.getName(), m)
                    );
                    default -> unsupportedOperation(type);
                }
            }
    }

    private void changeLocation(LockMode mode, Target target, BiConsumer<Target, ErrorCodeVO> fnc) {
        switch (mode) {
            case IN -> fnc.accept(target, ErrorCodeVO.LOCK_STATE_IN);
            case OUT -> fnc.accept(target, ErrorCodeVO.LOCK_STATE_OUT);
            case IN_AND_OUT -> fnc.accept(target, ErrorCodeVO.LOCK_STATE_IN_AND_OUT);
            case NONE -> fnc.accept(target, ErrorCodeVO.UNLOCK_STATE_IN_AND_OUT);
            default -> unsupportedOperation(mode);
        }
    }

    private void changeLocationGroupState(LockMode mode, Target target, BiConsumer<Target, LocationGroupState[]> fnc) {
        switch (mode) {
            case IN -> fnc.accept(target, new LocationGroupState[]{NOT_AVAILABLE, AVAILABLE});
            case OUT -> fnc.accept(target, new LocationGroupState[]{AVAILABLE, NOT_AVAILABLE});
            case IN_AND_OUT -> fnc.accept(target, new LocationGroupState[]{NOT_AVAILABLE, NOT_AVAILABLE});
            case NONE -> fnc.accept(target, new LocationGroupState[]{AVAILABLE, AVAILABLE});
            default -> unsupportedOperation(mode);
        }
    }

    private void changeLocationGroupMode(LockMode mode, LocationGroup target, BiConsumer<LocationGroup, String> fnc) {
        switch (mode) {
            case IN -> fnc.accept(target, LocationGroupMode.OUTFEED);
            case OUT -> fnc.accept(target, LocationGroupMode.INFEED);
            case IN_AND_OUT -> fnc.accept(target, LocationGroupMode.NO_OPERATION);
            case NONE -> fnc.accept(target, LocationGroupMode.INFEED_AND_OUTFEED);
            default -> unsupportedOperation(mode);
        }
    }

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

    /**
     * Lock the {@code Target} identified by {@code targetBK}.
     *
     * @param targetBK The business key of the Target, can be a {@code LocationPK} in String format or a LocationGroup name
     * @param reAllocation If {@literal true} open outfeed orders will be re-allocated
     */
    @PostMapping(path = API_TARGETS + "/{targetBK}", params = {"type=PERMANENT_LOCK", "mode=lock"})
    public void lock(
            @PathVariable("targetBK") String targetBK,
            @RequestParam(value = "reallocation", required = false) Boolean reAllocation
    ) {
        if (LocationPK.isValid(targetBK)) {
            var location = locationService.findByLocationIdOrThrow(targetBK);

            // Okay we have a Location as Target
            locationService.changeState(location.getPersistentKey(), ErrorCodeVO.LOCK_STATE_IN_AND_OUT);
            raiseEvent(targetBK, reAllocation, LockMode.NONE);
            return;
        }

        var locationGroup = locationGroupService.findByNameOrThrow(targetBK);
        // The Target is a LocationGroup
        locationGroupService.changeGroupState(locationGroup.getPersistentKey(), NOT_AVAILABLE, NOT_AVAILABLE);
        locationGroupService.changeOperationMode(targetBK, LocationGroupMode.NO_OPERATION);
        raiseEvent(targetBK, reAllocation, LockMode.NONE);
    }

    /**
     * Unlock or release the {@code Target} identified by {@code targetBK}.
     *
     * @param targetBK The business key of the Target, can be a {@code LocationPK} in String format or a LocationGroup name
     */
    @ResponseStatus(HttpStatus.OK)
    @PostMapping(value = API_TARGETS + "/{targetBK}", params = {"type=PERMANENT_LOCK", "mode=unlock"})
    public void release(
            @PathVariable("targetBK") String targetBK
    ) {
        if (LocationPK.isValid(targetBK)) {
            var location = locationService.findByLocationIdOrThrow(targetBK);

            // Okay we have a Location as Target
            locationService.changeState(location.getPersistentKey(), ErrorCodeVO.UNLOCK_STATE_IN_AND_OUT);
            raiseEvent(targetBK, null, LockMode.IN_AND_OUT);
            return;
        }

        var locationGroup = locationGroupService.findByNameOrThrow(targetBK);
        // The Target is a LocationGroup
        locationGroupService.changeGroupState(locationGroup.getPersistentKey(), AVAILABLE, AVAILABLE);
        locationGroupService.changeOperationMode(targetBK, LocationGroupMode.INFEED_AND_OUTFEED);
        raiseEvent(targetBK, null, LockMode.IN_AND_OUT);
    }

    private void raiseEvent(String targetBK, Boolean reAllocation, LockMode mode) {
        ctx.publishEvent(
                TargetEvent
                        .newBuilder()
                        .targetBK(targetBK)
                        .lockType(LockType.PERMANENT_LOCK)
                        .operationMode(mode)
                        .reAllocation(reAllocation)
                        .build()
        );
    }
}