PreferencesController.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.core.preferences;

import jakarta.validation.constraints.NotBlank;
import org.ameba.exception.ResourceExistsException;
import org.ameba.http.MeasuredRestController;
import org.ameba.i18n.Translator;
import org.ameba.mapping.BeanMapper;
import org.openwms.core.http.AbstractWebController;
import org.openwms.core.http.Index;
import org.openwms.core.preferences.api.ApplicationPreferenceVO;
import org.openwms.core.preferences.api.ModulePreferenceVO;
import org.openwms.core.preferences.api.PreferenceVO;
import org.openwms.core.preferences.api.RolePreferenceVO;
import org.openwms.core.preferences.api.UserPreferenceVO;
import org.openwms.core.preferences.impl.PreferenceVOConverter;
import org.openwms.core.preferences.impl.jpa.PreferenceEO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
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.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.ArrayList;
import java.util.List;

import static org.openwms.core.preferences.api.PreferenceVO.MEDIA_TYPE;
import static org.openwms.core.preferences.api.PreferencesApi.API_PREFERENCES;
import static org.openwms.core.preferences.api.PreferencesConstants.ALREADY_EXISTS_WITH_OWNER_AND_SCOPE_AND_KEY;
import static org.openwms.core.preferences.api.PreferencesConstants.NOT_ALLOWED_PKEY;
import static org.openwms.core.preferences.api.PreferencesConstants.PROPERTY_SCOPE_NOT_DEFINED;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

/**
 * A PreferencesController.
 *
 * @author Heiko Scherrer
 */
@Validated
@MeasuredRestController
public class PreferencesController extends AbstractWebController {

    private static final Logger LOGGER = LoggerFactory.getLogger(PreferencesController.class);
    private final PreferencesService preferencesService;
    private final Translator translator;
    private final BeanMapper mapper;

    public PreferencesController(PreferencesService preferencesService, Translator translator, BeanMapper mapper) {
        this.preferencesService = preferencesService;
        this.translator = translator;
        this.mapper = mapper;
    }

    @GetMapping(API_PREFERENCES + "/index")
    public ResponseEntity<Index> index() {
        return ResponseEntity.ok(
                new Index(
                        linkTo(methodOn(PreferencesController.class).findAll()).withRel("preferences-findall"),
                        linkTo(methodOn(PreferencesController.class).findByPKey("pKey")).withRel("preferences-findbypkey"),
                        linkTo(methodOn(PreferencesController.class).findAllOfScope("{scope}")).withRel("preferences-findallofscope"),
                        linkTo(methodOn(PreferencesController.class).findPreferencesForGroupName("user", "USER", "group1")).withRel("preferences-findbyownerscopekey"),
                        linkTo(methodOn(PreferencesController.class).create(new PreferenceVO(), false)).withRel("preferences-create"),
                        linkTo(methodOn(PreferencesController.class).update("pKey", new PreferenceVO())).withRel("preferences-update"),
                        linkTo(methodOn(PreferencesController.class).delete("pKey")).withRel("preferences-delete"),
                        linkTo(methodOn(UserPreferencesController.class).findByUser("user")).withRel("user-preferences-findbyuser"),
                        linkTo(methodOn(UserPreferencesController.class).findByUserAndKey("user", "key")).withRel("user-preferences-findbyuserandkey")
                )
        );
    }

    @GetMapping(value = API_PREFERENCES, produces = MEDIA_TYPE)
    public ResponseEntity<List<PreferenceVO>> findAll() {
        return ResponseEntity.ok(
                mapper.map(new ArrayList<>(preferencesService.findAll()), PreferenceVO.class)
        );
    }

    @GetMapping(value = API_PREFERENCES + "/{pKey}")
    public ResponseEntity<PreferenceVO> findByPKey(
            @PathVariable("pKey") String pKey
    ) {
        var result = mapper.map(preferencesService.findByPKey(pKey), PreferenceVO.class);
        return ResponseEntity.ok().header(HttpHeaders.CONTENT_TYPE, result.getContentType()).body(result);
    }

    @GetMapping(value = API_PREFERENCES, params = {"scope", "key"})
    public ResponseEntity<PreferenceVO> findForOwnerAndScopeAndKey(
            @RequestParam(value = "owner", required = false) String owner,
            @RequestParam("scope") @NotBlank String scope,
            @RequestParam("key") @NotBlank String key
    ) {
        var propertyScope = convert(scope);
        var eoOpt = preferencesService.findForOwnerAndScopeAndKey(owner, propertyScope, key);
        if (eoOpt.isPresent()) {
            var result = mapper.map(eoOpt.get(), PreferenceVO.class);
            return ResponseEntity.ok().header(HttpHeaders.CONTENT_TYPE, result.getContentType()).body(result);
        }
        return ResponseEntity.noContent().build();
    }

    @GetMapping(value = API_PREFERENCES, params = "scope")
    public ResponseEntity<List<PreferenceVO>> findAllOfScope(
            @RequestParam("scope") String scope
    ) {
        var propertyScope = convert(scope);
        var result = mapper.map(
                new ArrayList<>(preferencesService.findForOwnerAndScope(null, propertyScope)),
                PreferenceVO.class
        );
        return ResponseEntity.ok().body(result);
    }

    @GetMapping(value = "/preferences/groups",params = {"scope", "name"}, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<List<PreferenceVO>> findPreferencesForGroupName(
            @RequestParam(value = "owner", required = false) String owner,
            @RequestParam("scope") @NotBlank String scope,
            @RequestParam("name") String groupName) {
        var propertyScope = convert(scope);
        var groups = preferencesService.findForScopeOwnerGroupName(owner, propertyScope, groupName);
        return groups.isEmpty()
                ? ResponseEntity.noContent().build()
                :ResponseEntity.ok(mapper.map(groups, PreferenceVO.class));
    }

    private PropertyScope convert(String scope) {
        PropertyScope propertyScope;
        try {
            propertyScope = PropertyScope.valueOf(scope);
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException(translator.translate(PROPERTY_SCOPE_NOT_DEFINED, new String[]{scope}, scope));
        }
        return propertyScope;
    }

    @Transactional
    @PostMapping(value = API_PREFERENCES)
    public ResponseEntity<PreferenceVO> create(
            @RequestBody PreferenceVO preference,
            @RequestParam(value = "strict", required = false) Boolean strict
    ) {
        PreferenceEO result;
        if (strict == null || strict == Boolean.FALSE) {
            var existingPrefOpt = preferencesService.findForOwnerAndScopeAndKey(
                    preference.getOwner(), PreferenceVOConverter.resolveScope(preference), preference.getKey()
            );
            if (existingPrefOpt.isPresent() && preference.getpKey() != null) {
                if (!existingPrefOpt.get().getPersistentKey().equals(preference.getpKey())) {
                    LOGGER.warn("The preference to create already exists, strict-mode allows updates but the persistent keys are not the same");
                    throw new IllegalArgumentException(translator.translate(NOT_ALLOWED_PKEY, preference.getpKey()));
                } else {
                    var eo = mapper.map(preference, PreferenceEO.class);
                    eo.setPersistentKey(existingPrefOpt.get().getPersistentKey());
                    result = preferencesService.update(
                            existingPrefOpt.get().getPersistentKey(),
                            eo
                    );
                    var vo = mapper.map(result, PreferenceVO.class);
                    return ResponseEntity
                            .created(linkTo(methodOn(PreferencesController.class).findByPKey(result.getPersistentKey())).toUri())
                            .header(HttpHeaders.CONTENT_TYPE, "application/json")
                            .body(vo);
                }
            }
        }
        if (preference.hasPKey()) {
            throw new IllegalArgumentException(translator.translate(NOT_ALLOWED_PKEY, preference.getpKey()));
        }
        ensurePreferenceNotExists(preference);
        result = preferencesService.create(mapper.map(preference, PreferenceEO.class));
        var vo = mapper.map(result, PreferenceVO.class);
        return ResponseEntity
                .created(linkTo(methodOn(PreferencesController.class).findByPKey(result.getPersistentKey())).toUri())
                .header(HttpHeaders.CONTENT_TYPE, "application/json")
                .body(vo);
    }

    private void ensurePreferenceNotExists(PreferenceVO preference) {
        var scope = switch (preference) {
            case UserPreferenceVO ignored -> PropertyScope.USER;
            case RolePreferenceVO ignored -> PropertyScope.ROLE;
            case ModulePreferenceVO ignored -> PropertyScope.MODULE;
            case ApplicationPreferenceVO ignored -> PropertyScope.APPLICATION;
            case null, default -> throw new IllegalArgumentException("Not implemented Preference type");
        };
        if (preferencesService.existsForOwnerAndScopeAndKey(preference.getOwner(), scope, preference.getKey())) {
            throw new ResourceExistsException(
                    translator,
                    ALREADY_EXISTS_WITH_OWNER_AND_SCOPE_AND_KEY,
                    new Serializable[]{preference.getKey(), preference.getOwner(), scope},
                    preference.getKey(), preference.getOwner(), scope
            );
        }
    }

    @PutMapping(API_PREFERENCES + "/{pKey}")
    public ResponseEntity<PreferenceVO> update(
            @PathVariable("pKey") String pKey,
            @RequestBody PreferenceVO preference
    ) {
        var vo = mapper.map(preferencesService.update(pKey, mapper.map(preference, PreferenceEO.class)), PreferenceVO.class);
        return ResponseEntity
                .ok()
                .header(HttpHeaders.CONTENT_TYPE, vo.getContentType())
                .body(vo);
    }

    @DeleteMapping(API_PREFERENCES + "/{pKey}")
    ResponseEntity<Void> delete(
            @PathVariable("pKey") String pKey
    ) {
        preferencesService.delete(pKey);
        return ResponseEntity.noContent().build();
    }
}