ServiceContractService.java

/*
 * Copyright (c) 2002-2024, City of Paris
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *
 *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * License 1.0
 */
package fr.paris.lutece.plugins.identitystore.service.contract;

import fr.paris.lutece.plugins.identitystore.business.application.ClientApplication;
import fr.paris.lutece.plugins.identitystore.business.application.ClientApplicationHome;
import fr.paris.lutece.plugins.identitystore.business.attribute.AttributeKey;
import fr.paris.lutece.plugins.identitystore.business.contract.AttributeCertification;
import fr.paris.lutece.plugins.identitystore.business.contract.AttributeRequirement;
import fr.paris.lutece.plugins.identitystore.business.contract.AttributeRight;
import fr.paris.lutece.plugins.identitystore.business.contract.ServiceContract;
import fr.paris.lutece.plugins.identitystore.business.contract.ServiceContractHome;
import fr.paris.lutece.plugins.identitystore.business.referentiel.RefAttributeCertificationProcessus;
import fr.paris.lutece.plugins.identitystore.cache.ActiveServiceContractCache;
import fr.paris.lutece.plugins.identitystore.service.attribute.IdentityAttributeService;
import fr.paris.lutece.plugins.identitystore.service.identity.IdentityAttributeNotFoundException;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeChangeStatus;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeDto;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeStatus;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.IdentityChangeRequest;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.IdentityChangeResponse;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.exporting.IdentityExportRequest;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.exporting.IdentityExportResponse;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.merge.IdentityMergeRequest;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.merge.IdentityMergeResponse;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.IdentitySearchMessage;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.IdentitySearchRequest;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.IdentitySearchResponse;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.SearchAttribute;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.util.Constants;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.util.ResponseStatusFactory;
import fr.paris.lutece.plugins.identitystore.web.exception.IdentityStoreException;
import fr.paris.lutece.portal.service.i18n.I18nService;
import fr.paris.lutece.portal.service.spring.SpringContextService;
import fr.paris.lutece.util.sql.TransactionManager;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ServiceContractService
{

    private final ActiveServiceContractCache _cache = SpringContextService.getBean( "identitystore.activeServiceContractCache" );
    private static ServiceContractService _instance;

    public static ServiceContractService instance( )
    {
        if ( _instance == null )
        {
            _instance = new ServiceContractService( );
            _instance._cache.refresh( );
        }
        return _instance;
    }

    private ServiceContractService( )
    {
    }

    /**
     * Get the active {@link ServiceContract} associated to the given {@link ClientApplication}
     *
     * @param clientCode
     *            code of the {@link ClientApplication} requesting the change
     * @return the active {@link ServiceContract}
     * @throws ServiceContractNotFoundException
     */
    public ServiceContract getActiveServiceContract( final String clientCode ) throws ServiceContractNotFoundException
    {
        return _cache.get( clientCode );
    }

    /**
     * Validates the {@link IdentityChangeRequest} against the {@link ServiceContract} associated to the {@link ClientApplication} requesting the change. Each
     * violation is listed in the {@link IdentityChangeResponse} with a status by attribute key. The following rules are verified: <br>
     * <ul>
     * <li>The attribute must be writable</li>
     * <li>The certification processus, if given in the request, must be in the list of authorized processus</li>
     * </ul>
     *
     * @param identityChangeRequest
     *            {@link IdentityChangeRequest} with list of attributes
     * @param clientCode
     *            code of the {@link ClientApplication} requesting the change
     * @return {@link IdentityChangeResponse} containing the execution status
     * @throws ServiceContractNotFoundException
     * @throws IdentityAttributeNotFoundException
     */
    public IdentityChangeResponse validateIdentityChange( final IdentityChangeRequest identityChangeRequest, final String clientCode )
            throws ServiceContractNotFoundException, IdentityAttributeNotFoundException
    {
        final IdentityChangeResponse response = new IdentityChangeResponse( );
        final List<AttributeStatus> attrStatusList = new ArrayList<>( );
        final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
        for ( final AttributeDto attributeDto : identityChangeRequest.getIdentity( ).getAttributes( ) )
        {
            boolean canWriteAttribute = IdentityAttributeService.instance( ).getAttributeKey( attributeDto.getKey( ) ) != null;
            if ( !canWriteAttribute )
            {
                attrStatusList.add( this.buildAttributeStatus( attributeDto, AttributeChangeStatus.NOT_FOUND ) );
                continue;
            }

            canWriteAttribute = serviceContract.getAttributeRights( ).stream( )
                    .anyMatch( attributeRight -> StringUtils.equals( attributeRight.getAttributeKey( ).getKeyName( ), attributeDto.getKey( ) )
                            && attributeRight.isWritable( ) );
            if ( !canWriteAttribute )
            {
                attrStatusList.add( this.buildAttributeStatus( attributeDto, AttributeChangeStatus.UNAUTHORIZED ) );
                continue;
            }

            if ( attributeDto.getCertifier( ) != null )
            {
                canWriteAttribute = serviceContract.getAttributeCertifications( ).stream( ).anyMatch(
                        attributeCertification -> StringUtils.equals( attributeCertification.getAttributeKey( ).getKeyName( ), attributeDto.getKey( ) )
                                && attributeCertification.getRefAttributeCertificationProcessus( ).stream( )
                                        .anyMatch( processus -> StringUtils.equals( processus.getCode( ), attributeDto.getCertifier( ) ) ) );
                if ( !canWriteAttribute )
                {
                    attrStatusList.add( this.buildAttributeStatus( attributeDto, AttributeChangeStatus.INSUFFICIENT_RIGHTS ) );
                }
            }
        }

        if ( !attrStatusList.isEmpty( ) )
        {
            response.setStatus(
                    ResponseStatusFactory.failure( ).setAttributeStatuses( attrStatusList ).setMessage( "The request violates service contract definition" )
                            .setMessageKey( Constants.PROPERTY_REST_ERROR_SERVICE_CONTRACT_VIOLATION ) );
        }

        return response;
    }

    /**
     * Validates the {@link IdentityMergeRequest} against the {@link ServiceContract} associated to the {@link ClientApplication} requesting the change. Each
     * violation is listed in the {@link IdentityMergeResponse} with a status by attribute key. The following rules are verified: <br>
     * <ul>
     * <li>The {@link ClientApplication} must be authorized to perform the merge</li>
     * </ul>
     *
     * @param identityMergeRequest
     *            {@link IdentityMergeRequest} with list of attributes
     * @param clientCode
     *            code of the {@link ClientApplication} requesting the change
     * @return {@link IdentityMergeResponse} containing the execution status
     * @throws ServiceContractNotFoundException
     */
    public IdentityMergeResponse validateIdentityMerge( final IdentityMergeRequest identityMergeRequest, final String clientCode )
            throws ServiceContractNotFoundException
    {
        final IdentityMergeResponse response = new IdentityMergeResponse( );
        final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
        if ( !serviceContract.getAuthorizedMerge( ) )
        {
            response.setStatus( ResponseStatusFactory.failure( ).setMessage( "The client application is not authorized to merge identities" )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_MERGE_UNAUTHORIZED ) );
        }

        return response;
    }

    /**
     * Validates the {@link IdentityChangeRequest} against the {@link ServiceContract} associated to the {@link ClientApplication} requesting the change. Each
     * violation is listed in the {@link IdentityChangeResponse} with a status by attribute key. The following rules are verified: <br>
     * <ul>
     * <li>The {@link ClientApplication} must be authorized to perform the import</li>
     * </ul>
     *
     * @param identityChangeRequest
     *            {@link IdentityMergeRequest} with list of attributes
     * @param clientCode
     *            code of the {@link ClientApplication} requesting the change
     * @return {@link IdentityMergeResponse} containing the execution status
     * @throws ServiceContractNotFoundException
     */
    public IdentityChangeResponse validateIdentityImport( final IdentityChangeRequest identityChangeRequest, final String clientCode )
            throws ServiceContractNotFoundException, IdentityAttributeNotFoundException
    {
        final IdentityChangeResponse response = new IdentityChangeResponse( );
        final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
        if ( !serviceContract.getAuthorizedImport( ) )
        {
            response.setStatus( ResponseStatusFactory.failure( ).setMessage( "The client application is not authorized to import identities " )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_IMPORT_UNAUTHORIZED ) );
        }
        else
        {
            return this.validateIdentityChange( identityChangeRequest, clientCode );
        }

        return response;
    }

    /**
     * Validates the {@link IdentityExportRequest} against the {@link ServiceContract} associated to the {@link ClientApplication} requesting the export. Each
     * violation is listed in the response with a status by attribute key. The following rules are verified: <br>
     * <ul>
     * <li>The attribute must be readable</li>
     * </ul>
     *
     * @param identityExportRequest
     *            {@link IdentityExportRequest} with list of attributes
     * @param clientCode
     *            code of the {@link ClientApplication} requesting the search
     * @throws ServiceContractNotFoundException
     */
    public IdentityExportResponse validateIdentityExport(final IdentityExportRequest identityExportRequest, final String clientCode ) throws ServiceContractNotFoundException
    {
        final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
        final IdentityExportResponse response = new IdentityExportResponse( );
        if ( !serviceContract.getAuthorizedExport( ) )
        {
            response.setStatus( ResponseStatusFactory.failure( ).setMessage( "The client application is not authorized to export identities." )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_EXPORT_UNAUTHORIZED ) );
            return response;
        }

        if ( identityExportRequest.getAttributeKeyList( ) != null && !identityExportRequest.getAttributeKeyList( ).isEmpty( ) )
        {

            for ( final String searchAttributeKey : identityExportRequest.getAttributeKeyList( ) )
            {
                final Optional<AttributeRight> attributeRight = serviceContract.getAttributeRights( ).stream( )
                        .filter( a -> StringUtils.equals( a.getAttributeKey( ).getKeyName( ), searchAttributeKey ) ).findFirst( );
                if ( attributeRight.isPresent( ) )
                {
                    boolean canReadAttribute = attributeRight.get( ).isReadable( );

                    if ( !canReadAttribute )
                    {
                        response.setStatus( ResponseStatusFactory.failure( ).setMessageKey( Constants.PROPERTY_REST_ERROR_SERVICE_CONTRACT_VIOLATION ).setMessage( searchAttributeKey + " key is not readable in service contract definition." ) );
                    }
                }
                else
                {
                    response.setStatus( ResponseStatusFactory.failure( ).setMessageKey( Constants.PROPERTY_REST_ERROR_SERVICE_CONTRACT_VIOLATION ).setMessage( searchAttributeKey + " key does not exist in service contract definition." ) );
                }
            }
        }

        return response;
    }

    /**
     * Validates the {@link IdentitySearchRequest} against the {@link ServiceContract} associated to the {@link ClientApplication} requesting the search. Each
     * violation is listed in the response with a status by attribute key. The following rules are verified: <br>
     * <ul>
     * <li>The attribute must be searchable</li>
     * </ul>
     *
     * @param identitySearchRequest
     *            {@link IdentitySearchRequest} with list of attributes
     * @param clientCode
     *            code of the {@link ClientApplication} requesting the search
     * @throws ServiceContractNotFoundException
     */
    public IdentitySearchResponse validateIdentitySearch( final IdentitySearchRequest identitySearchRequest, final String clientCode,
            final boolean checkContract ) throws ServiceContractNotFoundException
    {
        final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
        final IdentitySearchResponse response = new IdentitySearchResponse( );
        if ( checkContract && !serviceContract.getAuthorizedSearch( ) )
        {
            response.setStatus( ResponseStatusFactory.failure( ).setMessage( "The client application is not authorized to search an identity." )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_SEARCH_UNAUTHORIZED ) );
            final IdentitySearchMessage message = new IdentitySearchMessage( );
            message.setMessage( "The client application is not authorized to search an identity." ); // TODO améliorer le modèle
            response.getAlerts( ).add( message );
            return response;
        }

        if ( identitySearchRequest.getSearch( ) != null )
        {

            for ( final SearchAttribute searchAttribute : identitySearchRequest.getSearch( ).getAttributes( ) )
            {
                final Optional<AttributeRight> attributeRight = serviceContract.getAttributeRights( ).stream( )
                        .filter( a -> StringUtils.equals( a.getAttributeKey( ).getKeyName( ), searchAttribute.getKey( ) ) ).findFirst( );
                if ( attributeRight.isPresent( ) )
                {
                    boolean canSearchAttribute = attributeRight.get( ).isSearchable( );

                    if ( !canSearchAttribute )
                    {
                        final IdentitySearchMessage alert = new IdentitySearchMessage( );
                        alert.setAttributeName( searchAttribute.getKey( ) );
                        alert.setMessage( "This attribute is not searchable in service contract definition." );
                        response.getAlerts( ).add( alert );
                        response.setStatus( ResponseStatusFactory.failure( ).setMessageKey( Constants.PROPERTY_REST_ERROR_SERVICE_CONTRACT_VIOLATION ) );
                    }
                }
                else
                { // if key does not exist, it can be a common key for search
                    final List<AttributeRight> commonAttributes = serviceContract.getAttributeRights( ).stream( )
                            .filter( a -> StringUtils.equals( a.getAttributeKey( ).getCommonSearchKeyName( ), searchAttribute.getKey( ) ) )
                            .collect( Collectors.toList( ) );
                    if ( CollectionUtils.isNotEmpty( commonAttributes ) )
                    {
                        boolean canSearchAttribute = commonAttributes.stream( ).allMatch( a -> a.isSearchable( ) );
                        if ( !canSearchAttribute )
                        {
                            final IdentitySearchMessage alert = new IdentitySearchMessage( );
                            alert.setAttributeName( searchAttribute.getKey( ) );
                            alert.setMessage( "This attribute group is not searchable in service contract definition." );
                            response.getAlerts( ).add( alert );
                            response.setStatus( ResponseStatusFactory.failure( ).setMessageKey( Constants.PROPERTY_REST_ERROR_SERVICE_CONTRACT_VIOLATION ) );
                        }
                    }
                    else
                    {
                        final IdentitySearchMessage alert = new IdentitySearchMessage( );
                        alert.setAttributeName( searchAttribute.getKey( ) );
                        alert.setMessage( "This attribute does not exist in service contract definition." );
                        response.getAlerts( ).add( alert );
                        response.setStatus( ResponseStatusFactory.failure( ).setMessageKey( Constants.PROPERTY_REST_ERROR_SERVICE_CONTRACT_VIOLATION ) );
                    }
                }
            }
        }

        return response;
    }

    public List<String> getMandatoryAttributes( final String clientCode, final List<AttributeKey> sharedMandatoryAttributeList )
            throws ServiceContractNotFoundException
    {
        final List<AttributeRight> rights = this.getActiveServiceContract( clientCode ).getAttributeRights( );
        return Stream
                .concat( sharedMandatoryAttributeList.stream( ).map( AttributeKey::getKeyName ),
                        rights.stream( ).filter( AttributeRight::isMandatory ).map( ar -> ar.getAttributeKey( ).getKeyName( ) ) )
                .distinct( ).collect( Collectors.toList( ) );

    }

    public boolean canModifyConnectedIdentity( final String clientCode ) throws ServiceContractNotFoundException
    {
        final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
        return serviceContract.getAuthorizedAccountUpdate( );
    }

    public boolean canCreateIdentity( final String clientCode ) throws ServiceContractNotFoundException
    {
        final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
        return serviceContract.getAuthorizedCreation( );
    }

    public boolean canUpdateIdentity( final String clientCode ) throws ServiceContractNotFoundException
    {
        final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
        return serviceContract.getAuthorizedUpdate( );
    }

    public boolean canUncertifyIdentity( final String clientCode ) throws ServiceContractNotFoundException
    {
        final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
        return serviceContract.getAuthorizedDecertification( );
    }

    public boolean canDeleteIdentity( String clientCode ) throws ServiceContractNotFoundException
    {
        final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
        return serviceContract.getAuthorizedDeletion( );
    }

    public int getDataRetentionPeriodInMonths( final String clientCode ) throws ServiceContractNotFoundException
    {
        final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
        return serviceContract.getDataRetentionPeriodInMonths( );
    }

    private AttributeStatus buildAttributeStatus( final AttributeDto attributeDto, final AttributeChangeStatus status )
    {
        final AttributeStatus attributeStatus = new AttributeStatus( );
        attributeStatus.setKey( attributeDto.getKey( ) );
        attributeStatus.setStatus( status );
        return attributeStatus;
    }

    /**
     * Creates a new {@link ServiceContract} (if possible) and adds it to cache if active. The contract can be created if:
     * <ul>
     * <li>The start date of the contract is not in the range [start date; end date] of an existing contract</li>
     * <li>The end date of the contract is not in the range [start date; end date] of an existing contract</li>
     * </ul>
     *
     * @param serviceContract
     * @param applicationId
     * @return
     */
    public ServiceContract create( final ServiceContract serviceContract, final Integer applicationId ) throws IdentityStoreException
    {
        TransactionManager.beginTransaction( null );
        try
        {
            final ClientApplication clientApplication = ClientApplicationHome.findByPrimaryKey( applicationId );
            this.validateContractDefinition( serviceContract, clientApplication );
            ServiceContractHome.create( serviceContract, applicationId );
            if ( CollectionUtils.isNotEmpty( serviceContract.getAttributeRights( ) ) )
            {
                ServiceContractHome.addAttributeRights( serviceContract.getAttributeRights( ), serviceContract );
            }
            if ( CollectionUtils.isNotEmpty( serviceContract.getAttributeRequirements( ) ) )
            {
                ServiceContractHome.addAttributeRequirements( serviceContract.getAttributeRequirements( ), serviceContract );
            }
            if ( CollectionUtils.isNotEmpty( serviceContract.getAttributeCertifications( ) ) )
            {
                ServiceContractHome.addAttributeCertifications( serviceContract.getAttributeCertifications( ), serviceContract );
            }
            serviceContract.setClientCode( clientApplication.getClientCode( ) );

            if ( serviceContract.isActive( ) )
            {
                this._cache.put( clientApplication.getClientCode( ), serviceContract );
            }
            TransactionManager.commitTransaction( null );
        }
        catch( Exception e )
        {
            TransactionManager.rollBack( null );
            throw new IdentityStoreException( e.getMessage( ), e );
        }

        return serviceContract;
    }

    /**
     * Update an existing {@link ServiceContract} (if possible) and adds it to cache if active. The contract can be updated if:
     * <ul>
     * <li>The start date of the contract is not in the range [start date; end date] of an existing contract</li>
     * <li>The end date of the contract is not in the range [start date; end date] of an existing contract</li>
     * </ul>
     *
     * @param serviceContract
     * @param applicationId
     * @return
     */
    public ServiceContract update( final ServiceContract serviceContract, final Integer applicationId ) throws IdentityStoreException
    {

        TransactionManager.beginTransaction( null );
        try
        {
            final ClientApplication clientApplication = ClientApplicationHome.findByPrimaryKey( applicationId );
            this.validateContractDefinition( serviceContract, clientApplication );
            ServiceContractHome.update( serviceContract, applicationId );
            ServiceContractHome.removeAttributeRights( serviceContract );
            ServiceContractHome.addAttributeRights( serviceContract.getAttributeRights( ), serviceContract );
            ServiceContractHome.removeAttributeRequirements( serviceContract );
            ServiceContractHome.addAttributeRequirements( serviceContract.getAttributeRequirements( ), serviceContract );
            ServiceContractHome.removeAttributeCertifications( serviceContract );
            ServiceContractHome.addAttributeCertifications( serviceContract.getAttributeCertifications( ), serviceContract );
            serviceContract.setClientCode( clientApplication.getClientCode( ) );

            if ( serviceContract.isActive( ) )
            {
                this._cache.deleteById( serviceContract.getId( ) );
                this._cache.put( clientApplication.getClientCode( ), serviceContract );
            }
            TransactionManager.commitTransaction( null );
        }
        catch( Exception e )
        {
            TransactionManager.rollBack( null );
            throw new IdentityStoreException( e.getMessage( ), e );
        }
        return serviceContract;
    }

    /**
     * Closes an existing {@link ServiceContract} (if possible) and adds it to cache if active.
     *
     * @param serviceContract
     * @return
     */
    public ServiceContract close( final ServiceContract serviceContract ) throws IdentityStoreException
    {

        TransactionManager.beginTransaction( null );
        try
        {
            ServiceContractHome.close( serviceContract );
            TransactionManager.commitTransaction( null );
        }
        catch( Exception e )
        {
            TransactionManager.rollBack( null );
            throw new IdentityStoreException( e.getMessage( ), e );
        }
        return serviceContract;
    }

    /**
     * Deletes a {@link ServiceContract} by its id in the database
     *
     * @param id
     */
    public void delete( final Integer id )
    {
        ServiceContractHome.remove( id );
        _cache.deleteById( id );
    }

    /**
     * Deletes a {@link ClientApplication} and all the related {@link ServiceContract}
     *
     * @param clientApplication
     */
    public void deleteApplication( final ClientApplication clientApplication ) throws IdentityStoreException
    {
        TransactionManager.beginTransaction( null );
        try
        {
            ClientApplicationHome.removeContracts( clientApplication );
            ClientApplicationHome.remove( clientApplication );
            _cache.removeKey( clientApplication.getClientCode( ) );
            TransactionManager.commitTransaction( null );
        }
        catch( Exception e )
        {
            TransactionManager.rollBack( null );
            throw new IdentityStoreException( e.getMessage( ), e );
        }
    }

    /**
     * Checks if the definition of a given {@link ServiceContract} is Valid. Start date and End date of the given contract shall not be in the range of Start
     * date and End date of an existing contract.
     *
     * @param serviceContract
     * @param clientApplication
     */
    private void validateContractDefinition( final ServiceContract serviceContract, final ClientApplication clientApplication )
            throws ServiceContractDefinitionException
    {
        final List<ServiceContract> serviceContracts = ClientApplicationHome.selectServiceContracts( clientApplication );
        if ( serviceContracts == null || serviceContracts.isEmpty( ) )
        {
            return;
        }
        if ( serviceContract.getStartingDate( ) == null )
        {
            throw new ServiceContractDefinitionException( "The start date of the contract shall be set" );
        }
        // filter current contract in case of update
        final List<ServiceContract> filteredServiceContracts = serviceContracts.stream( ).filter( c -> !Objects.equals( c.getId( ), serviceContract.getId( ) ) )
                .collect( Collectors.toList( ) );
        if ( filteredServiceContracts.stream( )
                .anyMatch( contract -> isInRange( serviceContract.getStartingDate( ), contract.getStartingDate( ), contract.getEndingDate( ) ) ) )
        {
            throw new ServiceContractDefinitionException( "The start date of the contract is in the range of an existing service contract" );
        }
        if ( filteredServiceContracts.stream( )
                .anyMatch( contract -> isInRange( serviceContract.getEndingDate( ), contract.getStartingDate( ), contract.getEndingDate( ) ) ) )
        {
            throw new ServiceContractDefinitionException( "The end date of the contract is in the range of an existing service contract" );
        }
        // TODO traiter le cas où il existe un contrat sans date de fin => soit on interdit soit on ferme le contrat automatiquement
        if ( filteredServiceContracts.stream( )
                .anyMatch( contract -> contract.getEndingDate( ) == null && contract.getStartingDate( ).before( serviceContract.getStartingDate( ) ) ) )
        {
            throw new ServiceContractDefinitionException( "A contract exists with an infinite end date" );
        }

        // https://dev.lutece.paris.fr/gitlab/bild/gestion-identite/identity-management/-/issues/224
        // Les processus sélectionnés pour l'écriture d'un attribut doivent avoir un level >= Niveau de certification minimum exigé s'il est présent
        final List<AttributeRequirement> filledRequirements = serviceContract.getAttributeRequirements( ).stream( )
                .filter( requirement -> requirement.getRefCertificationLevel( ) != null && requirement.getRefCertificationLevel( ).getLevel( ) != null )
                .collect( Collectors.toList( ) );
        final StringBuilder message = new StringBuilder( );
        boolean hasLevelError = false;
        for ( final AttributeRequirement requirement : filledRequirements )
        {
            final Optional<AttributeCertification> result = serviceContract.getAttributeCertifications( ).stream( )
                    .filter( certification -> certification.getAttributeKey( ).getId( ) == requirement.getAttributeKey( ).getId( ) ).findFirst( );
            if ( result.isPresent( ) )
            {
                final AttributeCertification attributeCertification = result.get( );
                final int minLevel = Integer.parseInt( requirement.getRefCertificationLevel( ).getLevel( ) );
                for ( final RefAttributeCertificationProcessus processus : attributeCertification.getRefAttributeCertificationProcessus( ) )
                {
                    final int processLevel = AttributeCertificationDefinitionService.instance( ).getLevelAsInteger( processus.getCode( ),
                            attributeCertification.getAttributeKey( ).getKeyName( ) );
                    if ( processLevel < minLevel )
                    {
                        hasLevelError = true;
                        final String [ ] params = {
                                processus.getLabel( ), requirement.getAttributeKey( ).getName( ), String.valueOf( processLevel ), String.valueOf( minLevel )
                        };
                        message.append(
                                I18nService.getLocalizedString( "identitystore.message.error.servicecontract.processus.level", params, Locale.getDefault( ) ) );
                        message.append( "<br>" );
                    }
                }
            }
        }
        if ( hasLevelError )
        {
            throw new ServiceContractDefinitionException( message.toString( ) );
        }
    }

    public boolean isInRange( final Date testedDate, final Date min, final Date max )
    {
        if ( testedDate != null && min != null && max != null )
        {
            return testedDate.getTime( ) >= min.getTime( ) && testedDate.getTime( ) <= max.getTime( );
        }
        return false;
    }

    /**
     * get Client Code list From AppCode
     * 
     * @param appCode
     *            the app code
     * @return list of corresponding client code
     */
    public List<String> getClientCodesFromAppCode( String appCode )
    {
        final List<ClientApplication> clientApplicationList = ClientApplicationHome.findByApplicationCode( appCode );
        return clientApplicationList.stream( ).map( ClientApplication::getClientCode ).collect( Collectors.toList( ) );
    }

}