ProductMapper.java

/*
 * Copyright 2005-2025 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.wms.inventory;

import org.mapstruct.Context;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.openwms.wms.CycleAvoidingMappingContext;
import org.openwms.wms.inventory.api.ProductVO;
import org.openwms.wms.inventory.api.UomRelationVO;
import org.openwms.wms.inventory.events.ProductMO;
import org.openwms.wms.location.impl.LocationMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * A ProductMapper.
 *
 * @author Heiko Scherrer
 */
@Mapper(uses = {DimensionMapper.class, LocationMapper.class})
public abstract class ProductMapper {

    protected ProductRepository productRepository;
    protected UomRelationService uomRelationService;
    protected DimensionMapper dimensionMapper;
    protected LocationMapper locationMapper;

    @Autowired
    public void setProductRepository(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Autowired
    public void setUomRelationService(UomRelationService uomRelationService) {
        this.uomRelationService = uomRelationService;
    }

    @Autowired
    public void setDimensionMapper(DimensionMapper dimensionMapper) {
        this.dimensionMapper = dimensionMapper;
    }

    @Autowired
    public void setLocationMapper(LocationMapper locationMapper) {
        this.locationMapper = locationMapper;
    }

    @Mapping(target = "pKey", source = "persistentKey")
    @Mapping(target = "sku", source = "sku")
    @Mapping(target = "label", source = "label")
    @Mapping(target = "accountId", source = "accountId")
    @Mapping(target = "baseUnit", source = "baseUnit")
    @Mapping(target = "overbookingAllowed", source = "overbookingAllowed")
    @Mapping(target = "description", source = "description")
    @Mapping(target = "classification", source = "classification")
    @Mapping(target = "group", source = "group")
    @Mapping(target = "stockZone", source = "stockZone")
    @Mapping(target = "units", source = "units")
    @Mapping(target = "details", source = "details")
    public abstract ProductMO convertToMO(Product entity);

    public String convertTo(UomRelation entity) {
        if (entity == null) {
            return null;
        }
        return entity.getLabel();
    }

    @Mapping(target = "pKey", source = "persistentKey")
    @Mapping(target = "quantity", source = "unit")
    public abstract UomRelationVO convert(UomRelation eo, @Context CycleAvoidingMappingContext ctx);

    public UomRelation convertVO(UomRelationVO vo) {
        if (vo == null) {
            return null;
        }

        String pKey = vo.getpKey();
        UomRelation uomRelation = pKey == null ? new UomRelation() : uomRelationService.findBypKey(pKey).orElse(new UomRelation());

        if (vo.getProduct() != null) {
            uomRelation.setProduct(this.convertVO(vo.getProduct()));
        }

        if (vo.getLabel() != null) {
            uomRelation.setLabel(vo.getLabel());
        }

        if (vo.getCode() != null) {
            uomRelation.setCode(vo.getCode());
        }

        if (vo.getDimension() != null) {
            uomRelation.setDimension(dimensionMapper.convertVO(vo.getDimension()));
        }

        if (vo.getQuantity() != null) {
            uomRelation.setUnit(vo.getQuantity());
        }

        Map<String, String> details = vo.getDetails();
        if (details != null) {
            uomRelation.setDetails(new HashMap<>(details));
        }

        return uomRelation;
    }

    public void copy(Product source, Product target) {
        Assert.hasText(source.getSku(), "sku must not be null");
        Assert.notNull(source.getBaseUnit(), "baseUnit must not be null");
        copyValue(source.getSku(), target.getSku(), target::setSku);
        copyValue(source.getLabel(), target.getLabel(), target::setLabel);
        copyValue(source.getAccountId(), target.getAccountId(), target::setAccountId);
        if (source.getBaseUnit() != target.getBaseUnit()) {
            target.setBaseUnit(source.getBaseUnit());
        }
        target.setOverbookingAllowed(source.isOverbookingAllowed());
        copyValue(source.getDescription(), target.getDescription(), target::setDescription);
        if (source.getAvailabilityState() != target.getAvailabilityState()) {
            target.setAvailabilityState(source.getAvailabilityState());
        }
        copyValue(source.getClassification(), target.getClassification(), target::setClassification);
        copyValue(source.getGroup(), target.getGroup(), target::setGroup);
        copyValue(source.getStockZone(), target.getStockZone(), target::setStockZone);
        if (source.getUnits() == null && target.getUnits() != null) {
            target.setUnits(null);
        } else if (source.getUnits() != null) {
            target.setUnits(source.getUnits());
        }
        if (source.getDetails() != null) {
            target.setDetails(source.getDetails());
        }
        target.setDimension(source.getDimension());
        target.setNetWeight(source.getNetWeight());
        target.setPreferableStorageLocation(source.getPreferableStorageLocation());
        target.setDetails(source.getDetails());
    }

    private void copyValue(String source, String target, Consumer<String> func) {
        if (source != null && !source.equals(target)) {
            func.accept(source);
        }
    }

    public Product convertVO(ProductVO vo) {
        if (vo == null) {
            return null;
        }
        var pKey = vo.getpKey();
        var product = pKey == null ? new Product() : productRepository.findBypKey(pKey).orElse(new Product());
        if (vo.getAccountId() != null) {
            product.setAccountId(vo.getAccountId());
        }
        if (vo.getAvailabilityState() != null) {
            product.setAvailabilityState(vo.getAvailabilityState());
        }
        if (vo.getClassification() != null) {
            product.setClassification(vo.getClassification());
        }
        if (vo.getGroup() != null) {
            product.setGroup(vo.getGroup());
        }
        if (vo.getLabel() != null) {
            product.setLabel(vo.getLabel());
        }
        if (vo.getDescription() != null) {
            product.setDescription(vo.getDescription());
        }
        if (vo.getSku() != null) {
            product.setSku(vo.getSku());
        }
        if (vo.getStockZone() != null) {
            product.setStockZone(vo.getStockZone());
        }
        if (vo.getBaseUnit() != null) {
            product.setBaseUnit(vo.getBaseUnit());
        }
        if (vo.getOverbookingAllowed() != null) {
            product.setOverbookingAllowed(vo.getOverbookingAllowed());
        }
        if (vo.getDetails() != null) {
            product.setDetails(vo.getDetails());
        }
        if (vo.getDimension() != null) {
            product.setDimension(dimensionMapper.convertVO(vo.getDimension()));
        }
        if (vo.getNetWeight() != null) {
            product.setNetWeight(vo.getNetWeight());
        }
        if (vo.getPreferableStorageLocation() != null) {
            product.setPreferableStorageLocation(locationMapper.convertFromId(vo.getPreferableStorageLocation().getLocationId()));
        }
        if (vo.getUnits() != null) {
            product.setUnits(convert(vo.getUnits(), product));
        }
        return product;
    }

    public abstract List<Product> convertVO(List<ProductVO> vos);

    @Mapping(target = "pKey", source = "persistentKey")
    @Mapping(target = "units", source = "units")
    @Mapping(target = "stackingRules", source = "stackingRules")
    public abstract ProductVO convert(Product entity, @Context CycleAvoidingMappingContext cycleAvoidingMappingContext);

    public abstract List<ProductVO> convert(List<Product> entities, @Context CycleAvoidingMappingContext cycleAvoidingMappingContext);

    private List<UomRelation> convert(List<UomRelationVO> units, Product product) {
        return units.stream().map(vo -> {
            var uomRelation = vo.pKey == null ? new UomRelation() : uomRelationService.findBypKey(vo.pKey).orElse(new UomRelation());
            if (uomRelation.getProduct() == null) {
                uomRelation.setProduct(product);
            }
            if (vo.getQuantity() != null) {
                uomRelation.setUnit(vo.getQuantity());
            }
            if (vo.getLabel() != null) {
                uomRelation.setLabel(vo.getLabel());
            }
            if (vo.getDimension() != null) {
                uomRelation.setDimension(dimensionMapper.convertVO(vo.getDimension()));
            }
            if (vo.getDetails() != null) {
                uomRelation.setDetails(vo.getDetails());
            }
            if (vo.getCode() != null) {
                uomRelation.setCode(vo.getCode());
            }
            return uomRelation;
        }).collect(Collectors.toList()); // *** Keep this and return a mutable list ***
    }
}