IdentityService.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.identity;

import fr.paris.lutece.plugins.identitystore.business.application.ClientApplication;
import fr.paris.lutece.plugins.identitystore.business.attribute.AttributeKey;
import fr.paris.lutece.plugins.identitystore.business.attribute.AttributeKeyHome;
import fr.paris.lutece.plugins.identitystore.business.contract.ServiceContract;
import fr.paris.lutece.plugins.identitystore.business.duplicates.suspicions.ExcludedIdentities;
import fr.paris.lutece.plugins.identitystore.business.duplicates.suspicions.SuspiciousIdentity;
import fr.paris.lutece.plugins.identitystore.business.duplicates.suspicions.SuspiciousIdentityHome;
import fr.paris.lutece.plugins.identitystore.business.identity.Identity;
import fr.paris.lutece.plugins.identitystore.business.identity.IdentityAttribute;
import fr.paris.lutece.plugins.identitystore.business.identity.IdentityAttributeHome;
import fr.paris.lutece.plugins.identitystore.business.identity.IdentityHome;
import fr.paris.lutece.plugins.identitystore.business.referentiel.RefAttributeCertificationLevel;
import fr.paris.lutece.plugins.identitystore.business.referentiel.RefAttributeCertificationLevelHome;
import fr.paris.lutece.plugins.identitystore.business.rules.duplicate.DuplicateRule;
import fr.paris.lutece.plugins.identitystore.business.rules.duplicate.DuplicateRuleHome;
import fr.paris.lutece.plugins.identitystore.business.rules.search.IdentitySearchRule;
import fr.paris.lutece.plugins.identitystore.business.rules.search.IdentitySearchRuleHome;
import fr.paris.lutece.plugins.identitystore.business.rules.search.SearchRuleType;
import fr.paris.lutece.plugins.identitystore.cache.IdentityDtoCache;
import fr.paris.lutece.plugins.identitystore.service.attribute.IdentityAttributeService;
import fr.paris.lutece.plugins.identitystore.service.contract.ServiceContractNotFoundException;
import fr.paris.lutece.plugins.identitystore.service.contract.ServiceContractService;
import fr.paris.lutece.plugins.identitystore.service.duplicate.IDuplicateService;
import fr.paris.lutece.plugins.identitystore.service.geocodes.GeocodesService;
import fr.paris.lutece.plugins.identitystore.service.listeners.IdentityStoreNotifyListenerService;
import fr.paris.lutece.plugins.identitystore.service.search.ISearchIdentityService;
import fr.paris.lutece.plugins.identitystore.service.user.InternalUserService;
import fr.paris.lutece.plugins.identitystore.utils.Batch;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.DtoConverter;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeChangeStatus;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeChangeStatusType;
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.common.AuthorType;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.ChangeResponse;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.IdentityDto;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.QualityDefinition;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.RequestAuthor;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.ResponseStatus;
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.history.AttributeChangeType;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.history.IdentityChangeType;
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.DuplicateSearchResponse;
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.QualifiedIdentitySearchResult;
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.security.AccessLogService;
import fr.paris.lutece.portal.service.security.AccessLoggerConstants;
import fr.paris.lutece.portal.service.spring.SpringContextService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.util.http.SecurityUtil;
import fr.paris.lutece.util.sql.TransactionManager;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

public class IdentityService
{
    // Conf
    private static final String PIVOT_CERTIF_LEVEL_THRESHOLD = "identitystore.identity.attribute.update.pivot.certif.level.threshold";

    // EVENTS FOR ACCESS LOGGING
    public static final String CREATE_IDENTITY_EVENT_CODE = "CREATE_IDENTITY";
    public static final String UPDATE_IDENTITY_EVENT_CODE = "UPDATE_IDENTITY";
    public static final String DECERTIFY_IDENTITY_EVENT_CODE = "DECERTIFY_IDENTITY";
    public static final String GET_IDENTITY_EVENT_CODE = "GET_IDENTITY";
    public static final String SEARCH_IDENTITY_EVENT_CODE = "SEARCH_IDENTITY";
    public static final String DELETE_IDENTITY_EVENT_CODE = "DELETE_IDENTITY";
    public static final String CONSOLIDATE_IDENTITY_EVENT_CODE = "CONSOLIDATE_IDENTITY";
    public static final String MERGE_IDENTITY_EVENT_CODE = "MERGE_IDENTITY";
    public static final String CANCEL_MERGE_IDENTITY_EVENT_CODE = "CANCEL_MERGE_IDENTITY";
    public static final String CANCEL_CONSOLIDATE_IDENTITY_EVENT_CODE = "CANCEL_CONSOLIDATE_IDENTITY";
    public static final String SPECIFIC_ORIGIN = "BO";

    // PROPERTIES
    private static final String PROPERTY_DUPLICATES_IMPORT_RULES_SUSPICION = "identitystore.identity.duplicates.import.rules.suspicion";
    private static final String PROPERTY_DUPLICATES_IMPORT_RULES_STRICT = "identitystore.identity.duplicates.import.rules.strict";
    private static final String PROPERTY_DUPLICATES_CREATION_RULES = "identitystore.identity.duplicates.creation.rules";
    private static final String PROPERTY_DUPLICATES_UPDATE_RULES = "identitystore.identity.duplicates.update.rules";
    private static final String PROPERTY_DUPLICATES_CHECK_DATABASE_ACTIVATED = "identitystore.identity.duplicates.check.database";

    // SERVICES
    private final IdentityStoreNotifyListenerService _identityStoreNotifyListenerService = IdentityStoreNotifyListenerService.instance( );
    private final ServiceContractService _serviceContractService = ServiceContractService.instance( );
    private final IdentityAttributeService _identityAttributeService = IdentityAttributeService.instance( );
    private final InternalUserService _internalUserService = InternalUserService.getInstance( );
    private final IDuplicateService _duplicateServiceDatabase = SpringContextService.getBean( "identitystore.duplicateService.database" );
    private final IDuplicateService _duplicateServiceElasticSearch = SpringContextService.getBean( "identitystore.duplicateService.elasticsearch" );
    private final ISearchIdentityService _elasticSearchIdentityService = SpringContextService.getBean( "identitystore.searchIdentityService.elasticsearch" );

    // CACHE
    private final IdentityDtoCache _identityDtoCache = SpringContextService.getBean( "identitystore.identityDtoCache" );

    private static IdentityService _instance;

    public static IdentityService instance( )
    {
        if ( _instance == null )
        {
            _instance = new IdentityService( );
        }
        return _instance;
    }

    /**
     * Creates a new {@link Identity} according to the given {@link IdentityChangeRequest}
     *
     * @param request
     *            the {@link IdentityChangeRequest} holding the parameters of the identity change request
     * @param author
     *            the author of the request
     * @param clientCode
     *            code of the {@link ClientApplication} requesting the change
     * @param response
     *            the {@link IdentityChangeResponse} holding the status of the execution of the request
     * @return the created {@link Identity}
     * @throws IdentityStoreException
     *             in case of error
     */
    public Identity create( final IdentityChangeRequest request, final RequestAuthor author, final String clientCode, final IdentityChangeResponse response )
            throws IdentityStoreException
    {
        if ( !_serviceContractService.canCreateIdentity( clientCode ) )
        {
            response.setStatus( ResponseStatusFactory.failure( ).setMessage( "The client application is not authorized to create an identity." )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_CREATE_UNAUTHORIZED ) );
            return null;
        }

        if ( StringUtils.isNotEmpty( request.getIdentity( ).getCustomerId( ) ) )
        {
            throw new IdentityStoreException( "You cannot specify a CUID when requesting for a creation" );
        }

        if ( StringUtils.isNotEmpty( request.getIdentity( ).getConnectionId( ) ) && !_serviceContractService.canModifyConnectedIdentity( clientCode ) )
        {
            throw new IdentityStoreException( "You cannot specify a GUID when requesting for a creation" );
        }

        // check if all mandatory attributes are present
        final List<String> mandatoryAttributes = _serviceContractService.getMandatoryAttributes( clientCode,
                AttributeKeyHome.getMandatoryForCreationAttributeKeyList( ) );
        if ( CollectionUtils.isNotEmpty( mandatoryAttributes ) )
        {
            final Set<String> providedKeySet = request.getIdentity( ).getAttributes( ).stream( ).filter( a -> StringUtils.isNotBlank( a.getValue( ) ) )
                    .map( AttributeDto::getKey ).collect( Collectors.toSet( ) );
            if ( !providedKeySet.containsAll( mandatoryAttributes ) )
            {
                response.setStatus( ResponseStatusFactory.failure( ).setMessage( "All mandatory attributes must be provided : " + mandatoryAttributes )
                        .setMessageKey( Constants.PROPERTY_REST_ERROR_MISSING_MANDATORY_ATTRIBUTES ) );
                return null;
            }
        }

        // check if can set "mon_paris_active" flag to true
        if ( request.getIdentity( ).getMonParisActive( ) != null && !_serviceContractService.canModifyConnectedIdentity( clientCode ) )
        {
            throw new IdentityStoreException( "You cannot set the 'mon_paris_active' flag when requesting for a creation" );
        }

        // check if GUID is already in use
        if ( StringUtils.isNotEmpty( request.getIdentity( ).getConnectionId( ) )
                && IdentityHome.findByConnectionId( request.getIdentity( ).getConnectionId( ) ) != null )
        {
            throw new IdentityStoreException( "GUID is already in use." );
        }

        final Map<String, String> attributes = request.getIdentity( ).getAttributes( ).stream( ).filter( a -> StringUtils.isNotBlank( a.getValue( ) ) )
                .collect( Collectors.toMap( AttributeDto::getKey, AttributeDto::getValue, ( a, b ) -> a ) );
        final DuplicateSearchResponse duplicateSearchResponse = this.checkDuplicates( attributes, PROPERTY_DUPLICATES_CREATION_RULES, "" );
        if ( duplicateSearchResponse != null && CollectionUtils.isNotEmpty( duplicateSearchResponse.getIdentities( ) ) )
        {
            response.setStatus( ResponseStatusFactory.conflict( ).setMessage( duplicateSearchResponse.getStatus( ).getMessage( ) )
                    .setMessageKey( duplicateSearchResponse.getStatus( ).getMessageKey( ) ) );
            return null;
        }

        final Identity identity = new Identity( );
        TransactionManager.beginTransaction( null );
        try
        {
            identity.setMonParisActive( request.getIdentity( ).isMonParisActive( ) );
            if ( StringUtils.isNotEmpty( request.getIdentity( ).getConnectionId( ) ) )
            {
                identity.setConnectionId( request.getIdentity( ).getConnectionId( ) );
            }
            IdentityHome.create( identity, _serviceContractService.getDataRetentionPeriodInMonths( clientCode ) );

            final List<AttributeDto> attributesToCreate = request.getIdentity( ).getAttributes( );
            final List<AttributeStatus> attrStatusList = GeocodesService.processCountryAndCityForCreate( identity, attributesToCreate, clientCode );
            for ( final AttributeDto attributeDto : attributesToCreate )
            {
                // TODO vérifier que la clef d'attribut existe dans le référentiel
                final AttributeStatus attributeStatus = _identityAttributeService.createAttribute( attributeDto, identity, clientCode );
                attrStatusList.add( attributeStatus );
            }

            response.setCustomerId( identity.getCustomerId( ) );
            response.setCreationDate( identity.getCreationDate( ) );
            final boolean incompleteCreation = attrStatusList.stream( ).anyMatch( s -> s.getStatus( ).equals( AttributeChangeStatus.NOT_CREATED ) );
            final ResponseStatus status = incompleteCreation ? ResponseStatusFactory.incompleteSuccess( ) : ResponseStatusFactory.success( );
            response.setStatus( status.setAttributeStatuses( attrStatusList ).setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
            TransactionManager.commitTransaction( null );

            /* Historique des modifications */
            final List<AttributeStatus> createdAttributes = attrStatusList.stream( ).filter( s -> s.getStatus( ).equals( AttributeChangeStatus.CREATED ) )
                    .collect( Collectors.toList( ) );
            for ( AttributeStatus attributeStatus : createdAttributes )
            {
                _identityStoreNotifyListenerService.notifyListenersAttributeChange( AttributeChangeType.CREATE, identity, attributeStatus, author, clientCode );
            }

            /* Indexation et historique */
            _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.CREATE, identity, response.getStatus( ).getType( ).name( ),
                    response.getStatus( ).getMessage( ), author, clientCode, new HashMap<>( ) );
            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_CREATE, CREATE_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( identity.getCustomerId( ) ), SPECIFIC_ORIGIN );
        }
        catch( Exception e )
        {
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( e.getMessage( ) ).setMessageKey( Constants.PROPERTY_REST_ERROR_DURING_TREATMENT ) );
            TransactionManager.rollBack( null );
        }

        return identity;
    }

    /**
     * Updates an existing {@link Identity} according to the given {@link IdentityChangeRequest} and following the given rules: <br>
     * <ul>
     * <li>The {@link Identity} must exist in te database. If not, NOT_FOUND status is returned in the execution response</li>
     * <li>The {@link Identity} must not be merged or deleted. In case of merged/deleted identity, the update is not performed and the customer ID of the
     * primary identity is returned in the execution response with a CONFLICT status</li>
     * <li>If the {@link Identity} can be updated, its {@link IdentityAttribute} list is updated following the given rule:
     * <ul>
     * <li>If the {@link IdentityAttribute} exists, it is updated if the value is different, and if the process level given in the request is higher than the
     * existing one. If the value cannot be updated, the NOT_UPDATED status, associated with the attribute key, is returned in the execution response.</li>
     * <li>If the {@link IdentityAttribute} does not exist, it is created. The CREATED status, associated with the attribute key, is returned in the execution
     * response.</li>
     * <li>CUID and GUID attributes cannot be modified.</li>
     * </ul>
     * </li>
     * </ul>
     *
     * @param customerId
     *            the id of the updated {@link Identity}
     * @param request
     *            the {@link IdentityChangeRequest} holding the parameters of the identity change request
     * @param author
     *            the author of the request
     * @param clientCode
     *            code of the {@link ClientApplication} requesting the change
     * @param response
     *            the {@link IdentityChangeResponse} holding the status of the execution of the request
     * @return the updated {@link Identity}
     * @throws IdentityStoreException
     *             in case of error
     */
    public Identity update( final String customerId, final IdentityChangeRequest request, final RequestAuthor author, final String clientCode,
            final IdentityChangeResponse response ) throws IdentityStoreException
    {
        if ( !_serviceContractService.canUpdateIdentity( clientCode ) )
        {
            response.setStatus( ResponseStatusFactory.failure( ).setMessage( "The client application is not authorized to update an identity." )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_UPDATE_UNAUTHORIZED ) );
            response.setCustomerId( customerId );
            return null;
        }

        final Identity identity = IdentityHome.findByCustomerId( customerId );

        // check if identity exists
        if ( identity == null )
        {
            response.setStatus( ResponseStatusFactory.notFound( ).setMessage( "No matching identity could be found" )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_NO_MATCHING_IDENTITY ) );
            return null;
        }

        // check if identity hasn't been updated between when the user retrieved the identity, and this request
        if ( !Objects.equals( identity.getLastUpdateDate( ), request.getIdentity( ).getLastUpdateDate( ) ) )
        {
            response.setStatus(
                    ResponseStatusFactory.conflict( ).setMessage( "This identity has been updated recently, please load the latest data before updating." )
                            .setMessageKey( Constants.PROPERTY_REST_ERROR_UPDATE_CONFLICT ) );
            response.setCustomerId( identity.getCustomerId( ) );
            return identity;
        }

        // check if identity is not merged
        if ( identity.isMerged( ) )
        {
            final Identity masterIdentity = IdentityHome.findMasterIdentityByCustomerId( request.getIdentity( ).getCustomerId( ) );
            response.setStatus(
                    ResponseStatusFactory.conflict( ).setMessage( "Cannot update a merged Identity. Master identity customerId is provided in the response." )
                            .setMessageKey( Constants.PROPERTY_REST_ERROR_FORBIDDEN_UPDATE_ON_MERGED_IDENTITY ) );
            response.setCustomerId( masterIdentity.getCustomerId( ) );
            return identity;
        }

        // check if identity is active
        if ( identity.isDeleted( ) )
        {
            response.setStatus( ResponseStatusFactory.conflict( ).setMessage( "Cannot update a deleted Identity." )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_FORBIDDEN_UPDATE_ON_DELETED_IDENTITY ) );
            response.setCustomerId( identity.getCustomerId( ) );
            return identity;
        }

        // check if the service contract allows the update of "mon_paris_active" flag
        if ( request.getIdentity( ).getMonParisActive( ) != null && !_serviceContractService.canModifyConnectedIdentity( clientCode ) )
        {
            response.setStatus(
                    ResponseStatusFactory.conflict( ).setMessage( "The client application is not authorized to update the 'mon_paris_active' flag." )
                            .setMessageKey( Constants.PROPERTY_REST_ERROR_FORBIDDEN_MON_PARIS_ACTIVE_UPDATE ) );
            response.setCustomerId( identity.getCustomerId( ) );
            return null;
        }

        // check if update would create duplicates
        if ( doesRequestContainsAttributeValueChangesImpactingRules( request, identity, PROPERTY_DUPLICATES_UPDATE_RULES ) )
        {
            // collect all non blank attributes from request
            final Map<String, String> attributes = request.getIdentity( ).getAttributes( ).stream( ).filter( a -> StringUtils.isNotBlank( a.getValue( ) ) )
                    .collect( Collectors.toMap( AttributeDto::getKey, AttributeDto::getValue, ( a, b ) -> a ) );
            // add other existing identity attributes
            identity.getAttributes( ).forEach( ( key, attr ) -> attributes.putIfAbsent( key, attr.getValue( ) ) );
            // remove attributes that have blank values in the request
            request.getIdentity( ).getAttributes( ).stream( ).filter( a -> StringUtils.isBlank( a.getValue( ) ) )
                    .forEach( a -> attributes.remove( a.getKey( ) ) );

            // search for potential duplicates with those attributes
            final DuplicateSearchResponse duplicateSearchResponse = this.checkDuplicates( attributes, PROPERTY_DUPLICATES_UPDATE_RULES, customerId );
            if ( duplicateSearchResponse != null && !duplicateSearchResponse.getIdentities( ).isEmpty( ) )
            {
                response.setStatus( ResponseStatusFactory.conflict( ).setMessage( duplicateSearchResponse.getStatus( ).getMessage( ) )
                        .setMessageKey( duplicateSearchResponse.getStatus( ).getMessageKey( ) ) );
                return null;
            }
        }

        // If GUID is updated, check if the new GUID does not exist in database
        TransactionManager.beginTransaction( null );
        try
        {
            if ( _serviceContractService.canModifyConnectedIdentity( clientCode )
                    && !StringUtils.equalsIgnoreCase( identity.getConnectionId( ), request.getIdentity( ).getConnectionId( ) )
                    && request.getIdentity( ).getConnectionId( ) != null )
            {
                final Identity byConnectionId = IdentityHome.findByConnectionId( request.getIdentity( ).getConnectionId( ) );
                if ( byConnectionId != null )
                {
                    response.setStatus( ResponseStatusFactory.conflict( )
                            .setMessage(
                                    "An identity already exists with the given connection ID. The customer ID of that identity is provided in the response." )
                            .setMessageKey( Constants.PROPERTY_REST_ERROR_CONFLICT_CONNECTION_ID ) );
                    response.setCustomerId( byConnectionId.getCustomerId( ) );
                    TransactionManager.rollBack( null );
                    return null;
                }
                else
                {
                    identity.setConnectionId( request.getIdentity( ).getConnectionId( ) );
                    IdentityHome.update( identity );
                }
            }

            // => process update :

            final List<AttributeStatus> attrStatusList = this.updateIdentity( request.getIdentity( ), clientCode, response, identity );
            if ( ResponseStatusFactory.unauthorized( ).equals( response.getStatus( ) ) )
            {
                response.setCustomerId( identity.getCustomerId( ) );
                TransactionManager.rollBack( null );
                return null;
            }

            response.setCustomerId( identity.getCustomerId( ) );
            response.setConnectionId( identity.getConnectionId( ) );
            response.setCreationDate( identity.getCreationDate( ) );
            response.setLastUpdateDate( identity.getLastUpdateDate( ) );

            final boolean allAttributesCreatedOrUpdated = attrStatusList.stream( ).map( AttributeStatus::getStatus )
                    .allMatch( status -> status.getType( ) == AttributeChangeStatusType.SUCCESS );
            final ResponseStatus status = allAttributesCreatedOrUpdated ? ResponseStatusFactory.success( ) : ResponseStatusFactory.incompleteSuccess( );

            final String msgKey;
            if ( Collections.disjoint( AttributeChangeStatus.getSuccessStatuses( ),
                    attrStatusList.stream( ).map( AttributeStatus::getStatus ).collect( Collectors.toList( ) ) ) )
            {
                // If there was no attribute change, send back a specific message key
                msgKey = Constants.PROPERTY_REST_INFO_NO_ATTRIBUTE_CHANGE;
            }
            else
            {
                msgKey = Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION;
            }
            response.setStatus( status.setAttributeStatuses( attrStatusList ).setMessageKey( msgKey ) );
            TransactionManager.commitTransaction( null );

            /* Historique des modifications */
            for ( final AttributeStatus attributeStatus : attrStatusList )
            {
                _identityStoreNotifyListenerService.notifyListenersAttributeChange( AttributeChangeType.UPDATE, identity, attributeStatus, author, clientCode );
            }

            /* Indexation et historique */
            _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.UPDATE, identity, response.getStatus( ).getType( ).name( ),
                    response.getStatus( ).getMessage( ), author, clientCode, new HashMap<>( ) );
            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, UPDATE_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( identity.getCustomerId( ) ), SPECIFIC_ORIGIN );
        }
        catch( Exception e )
        {
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( e.getMessage( ) ).setMessageKey( Constants.PROPERTY_REST_ERROR_DURING_TREATMENT ) );
            TransactionManager.rollBack( null );
        }

        return identity;
    }

    /**
     * Returns <code>true</code> if the request aims to add new attributes, remove existing attributes, or modify existing attribute's value, of attributes
     * checked by the duplicate rules in parameter.<br/>
     * Returns <code>false</code> otherwise.
     * 
     * @param request
     *            the request
     * @param identity
     *            the identity
     */
    private boolean doesRequestContainsAttributeValueChangesImpactingRules( final IdentityChangeRequest request, final Identity identity,
            final String duplicateRulesProperty )
    {
        final Set<String> checkedAttributeKeys = Arrays.stream( AppPropertiesService.getProperty( duplicateRulesProperty ).split( "," ) )
                .map( DuplicateRuleHome::findByCode ).flatMap( rule -> rule.getCheckedAttributes( ).stream( ) ).map( AttributeKey::getKeyName )
                .collect( Collectors.toSet( ) );
        return request.getIdentity( ).getAttributes( ).stream( ).filter( a -> checkedAttributeKeys.contains( a.getKey( ) ) ).anyMatch( a -> {
            if ( StringUtils.isNotBlank( a.getValue( ) ) )
            {
                return !identity.getAttributes( ).containsKey( a.getKey( ) )
                        || !Objects.equals( identity.getAttributes( ).get( a.getKey( ) ).getValue( ), a.getValue( ) );
            }
            else
            {
                return identity.getAttributes( ).containsKey( a.getKey( ) );
            }
        } );
    }

    /**
     * Merges two existing {@link Identity} specified in the given {@link IdentityMergeRequest} and following the given rules: <br>
     * <ul>
     * <li>Both {@link Identity} must exist and not be merged or deleted in te database. If not, FAILURE status is returned in the execution response</li>
     * <li>The {@link IdentityAttribute}(s) of the secondary {@link Identity} are processed, according to the list of keys specified in the
     * {@link IdentityMergeRequest}.
     * <ul>
     * <li>If the {@link IdentityAttribute} is not present in the primary {@link Identity}, it is created.</li>
     * <li>If the {@link IdentityAttribute} is present in the primary {@link Identity}, it is updated if the certification process is higher and the value is
     * different.</li>
     * </ul>
     * </li>
     * </ul>
     *
     * @param request
     *            the {@link IdentityMergeRequest} holding the parameters of the request
     * @param author
     *            the author of the request
     * @param clientCode
     *            code of the {@link ClientApplication} requesting the change
     * @param response
     *            the {@link IdentityMergeResponse} holding the status of the execution of the request
     * @return the merged {@link Identity}
     * @throws IdentityStoreException
     *             in case of error
     */
    // TODO: récupérer la plus haute date d'expiration des deux identités
    public Identity merge( final IdentityMergeRequest request, final RequestAuthor author, final String clientCode, final IdentityMergeResponse response )
    {
        final Identity primaryIdentity = IdentityHome.findByCustomerId( request.getPrimaryCuid( ) );
        if ( primaryIdentity == null )
        {
            response.setStatus( ResponseStatusFactory.failure( ).setMessage( "Could not find primary identity with customer_id " + request.getPrimaryCuid( ) )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_PRIMARY_IDENTITY_NOT_FOUND ) );
            return null;
        }

        if ( primaryIdentity.isDeleted( ) )
        {
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( "Primary identity found with customer_id " + request.getPrimaryCuid( ) + " is deleted" )
                            .setMessageKey( Constants.PROPERTY_REST_ERROR_PRIMARY_IDENTITY_DELETED ) );
            return null;
        }

        if ( primaryIdentity.isMerged( ) )
        {
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( "Primary identity found with customer_id " + request.getPrimaryCuid( ) + " is merged" )
                            .setMessageKey( Constants.PROPERTY_REST_ERROR_PRIMARY_IDENTITY_MERGED ) );
            return null;
        }

        if ( !Objects.equals( primaryIdentity.getLastUpdateDate( ), request.getPrimaryLastUpdateDate( ) ) )
        {
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( "The primary identity has been updated recently, please load the latest data before merging." )
                            .setMessageKey( Constants.PROPERTY_REST_ERROR_PRIMARY_IDENTITY_UPDATE_CONFLICT ) );
            return null;
        }

        final Identity secondaryIdentity = IdentityHome.findByCustomerId( request.getSecondaryCuid( ) );
        if ( secondaryIdentity == null )
        {
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( "Could not find secondary identity with customer_id " + request.getSecondaryCuid( ) )
                            .setMessageKey( Constants.PROPERTY_REST_ERROR_SECONDARY_IDENTITY_NOT_FOUND ) );
            return null;
        }

        if ( secondaryIdentity.isDeleted( ) )
        {
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( "Secondary identity found with customer_id " + request.getSecondaryCuid( ) + " is deleted" )
                            .setMessageKey( Constants.PROPERTY_REST_ERROR_SECONDARY_IDENTITY_DELETED ) );
            return null;
        }

        if ( secondaryIdentity.isMerged( ) )
        {
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( "Secondary identity found with customer_id " + request.getSecondaryCuid( ) + " is merged" )
                            .setMessageKey( Constants.PROPERTY_REST_ERROR_SECONDARY_IDENTITY_MERGED ) );
            return null;
        }

        if ( !Objects.equals( secondaryIdentity.getLastUpdateDate( ), request.getSecondaryLastUpdateDate( ) ) )
        {
            response.setStatus( ResponseStatusFactory.failure( )
                    .setMessage( "The secondary identity has been updated recently, please load the latest data before merging." )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_SECONDARY_IDENTITY_UPDATE_CONFLICT ) );
            return null;
        }

        TransactionManager.beginTransaction( null );
        try
        {
            final List<AttributeStatus> attrStatusList = new ArrayList<>( );
            if ( request.getIdentity( ) != null )
            {
                attrStatusList.addAll( this.updateIdentity( request.getIdentity( ), clientCode, response, primaryIdentity ) );
                if ( ResponseStatusFactory.unauthorized( ).equals( response.getStatus( ) ) )
                {
                    response.setCustomerId( primaryIdentity.getCustomerId( ) );
                    TransactionManager.rollBack( null );
                    return null;
                }
            }

            /* Tag de l'identité secondaire */
            secondaryIdentity.setMerged( true );
            secondaryIdentity.setMasterIdentityId( primaryIdentity.getId( ) );
            IdentityHome.merge( secondaryIdentity );
            IdentityAttributeHome.removeAllAttributes( secondaryIdentity.getId( ) );

            response.setCustomerId( primaryIdentity.getCustomerId( ) );
            response.setConnectionId( primaryIdentity.getConnectionId( ) );
            response.setLastUpdateDate( primaryIdentity.getLastUpdateDate( ) );

            final boolean allAttributesCreatedOrUpdated = attrStatusList.stream( ).map( AttributeStatus::getStatus )
                    .allMatch( status -> status.getType( ) == AttributeChangeStatusType.SUCCESS );
            final ResponseStatus status = allAttributesCreatedOrUpdated ? ResponseStatusFactory.success( ) : ResponseStatusFactory.incompleteSuccess( );

            final String msgKey;
            if ( Collections.disjoint( AttributeChangeStatus.getSuccessStatuses( ),
                    attrStatusList.stream( ).map( AttributeStatus::getStatus ).collect( Collectors.toList( ) ) ) )
            {
                // If there was no attribute change, send back a specific message key
                msgKey = Constants.PROPERTY_REST_INFO_NO_ATTRIBUTE_CHANGE;
            }
            else
            {
                msgKey = Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION;
            }

            response.setStatus( status.setAttributeStatuses( attrStatusList ).setMessageKey( msgKey ) );
            TransactionManager.commitTransaction( null );

            /* Historique des modifications */
            for ( AttributeStatus attributeStatus : attrStatusList )
            {
                _identityStoreNotifyListenerService.notifyListenersAttributeChange( AttributeChangeType.MERGE, primaryIdentity, attributeStatus, author,
                        clientCode );
            }

            /* Indexation */
            final Map<String, String> primaryMetadata = new HashMap<>( );
            primaryMetadata.put( Constants.METADATA_MERGED_MASTER_IDENTITY_CUID, primaryIdentity.getCustomerId( ) );
            primaryMetadata.put( Constants.METADATA_DUPLICATE_RULE_CODE, request.getDuplicateRuleCode( ) );
            _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.MERGED, secondaryIdentity,
                    response.getStatus( ).getType( ).name( ), response.getStatus( ).getType( ).name( ), author, clientCode, primaryMetadata );

            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, CONSOLIDATE_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( primaryIdentity.getCustomerId( ) ),
                    SPECIFIC_ORIGIN );

            final Map<String, String> secondaryMetadata = new HashMap<>( );
            secondaryMetadata.put( Constants.METADATA_MERGED_CHILD_IDENTITY_CUID, secondaryIdentity.getCustomerId( ) );
            secondaryMetadata.put( Constants.METADATA_DUPLICATE_RULE_CODE, request.getDuplicateRuleCode( ) );
            _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.CONSOLIDATED, primaryIdentity,
                    response.getStatus( ).getType( ).name( ), response.getStatus( ).getType( ).name( ), author, clientCode, secondaryMetadata );

            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, MERGE_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( secondaryIdentity.getCustomerId( ) ),
                    SPECIFIC_ORIGIN );
        }
        catch( Exception e )
        {
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( e.getMessage( ) ).setMessageKey( Constants.PROPERTY_REST_ERROR_DURING_TREATMENT ) );
            TransactionManager.rollBack( null );
        }

        return primaryIdentity;
    }

    /**
     * Detach a merged {@link Identity} from its master {@link Identity}
     * 
     * @param request
     *            the unmerge request
     * @param author
     *            the author of the request
     * @param clientCode
     *            the client code of the calling application
     * @param response
     *            the status of the execution
     */
    public void cancelMerge( final IdentityMergeRequest request, final RequestAuthor author, final String clientCode, final IdentityMergeResponse response )
    {
        final Identity primaryIdentity = IdentityHome.findByCustomerId( request.getPrimaryCuid( ) );
        if ( primaryIdentity == null )
        {
            response.setStatus( ResponseStatusFactory.failure( ).setMessage( "Could not find primary identity with customer_id " + request.getPrimaryCuid( ) )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_PRIMARY_IDENTITY_NOT_FOUND ) );
            return;
        }

        if ( !Objects.equals( primaryIdentity.getLastUpdateDate( ), request.getPrimaryLastUpdateDate( ) ) )
        {
            response.setStatus( ResponseStatusFactory.failure( )
                    .setMessage( "The primary identity has been updated recently, please load the latest data before canceling merge." )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_PRIMARY_IDENTITY_UPDATE_CONFLICT ) );
            return;
        }

        final Identity secondaryIdentity = IdentityHome.findByCustomerId( request.getSecondaryCuid( ) );
        if ( secondaryIdentity == null )
        {
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( "Could not find secondary identity with customer_id " + request.getSecondaryCuid( ) )
                            .setMessageKey( Constants.PROPERTY_REST_ERROR_SECONDARY_IDENTITY_NOT_FOUND ) );
            return;
        }

        if ( !secondaryIdentity.isMerged( ) )
        {
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( "Secondary identity found with customer_id " + request.getSecondaryCuid( ) + " is not merged" )
                            .setMessageKey( Constants.PROPERTY_REST_ERROR_SECONDARY_IDENTITY_NOT_MERGED ) );
            return;
        }

        if ( !Objects.equals( secondaryIdentity.getMasterIdentityId( ), primaryIdentity.getId( ) ) )
        {
            response.setStatus( ResponseStatusFactory.failure( )
                    .setMessage( "Secondary identity found with customer_id " + request.getSecondaryCuid( )
                            + " is not merged to Primary identity found with customer ID " + request.getPrimaryCuid( ) )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITIES_NOT_MERGED_TOGETHER ) );
            return;
        }

        // TODO il n'y a pas d'API permettant de récupérer une identité merged, on renvoie systématiquement le master => impossible d'unmerge en passant ce test
        // if ( !Objects.equals( secondaryIdentity.getLastUpdateDate( ), request.getSecondaryLastUpdateDate( ) ) )
        // {
        // response.setStatus( ResponseStatusFactory.failure( )
        // .setMessage( "The secondary identity has been updated recently, please load the latest data before canceling merge." )
        // .setMessageKey( Constants.PROPERTY_REST_ERROR_SECONDARY_IDENTITY_UPDATE_CONFLICT ) );
        // return;
        // }

        TransactionManager.beginTransaction( null );
        try
        {
            /* Tag de l'identité secondaire */
            IdentityHome.cancelMerge( secondaryIdentity );
            response.setStatus( ResponseStatusFactory.success( ).setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
            TransactionManager.commitTransaction( null );

            /* Indexation */
            final Map<String, String> secondaryMetadata = new HashMap<>( );
            secondaryMetadata.put( Constants.METADATA_UNMERGED_MASTER_CUID, primaryIdentity.getCustomerId( ) );
            _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.MERGE_CANCELLED, secondaryIdentity,
                    response.getStatus( ).getType( ).name( ), response.getStatus( ).getType( ).name( ), author, clientCode, secondaryMetadata );
            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, CANCEL_MERGE_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( secondaryIdentity.getCustomerId( ) ),
                    SPECIFIC_ORIGIN );

            final Map<String, String> primaryMetadata = new HashMap<>( );
            primaryMetadata.put( Constants.METADATA_UNMERGED_CHILD_CUID, secondaryIdentity.getCustomerId( ) );
            _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.CONSOLIDATION_CANCELLED, primaryIdentity,
                    response.getStatus( ).getType( ).name( ), response.getStatus( ).getType( ).name( ), author, clientCode, primaryMetadata );
            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, CANCEL_CONSOLIDATE_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( primaryIdentity.getCustomerId( ) ),
                    SPECIFIC_ORIGIN );
        }
        catch( Exception e )
        {
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( e.getMessage( ) ).setMessageKey( Constants.PROPERTY_REST_ERROR_DURING_TREATMENT ) );
            TransactionManager.rollBack( null );
        }
    }

    /**
     * Imports an {@link Identity} according to the given {@link IdentityChangeRequest}
     *
     * @param identityChangeRequest
     *            the {@link IdentityChangeRequest} holding the parameters of the identity change request
     * @param author
     *            the author of the request
     * @param clientCode
     *            code of the {@link ClientApplication} requesting the change
     * @param response
     *            the {@link IdentityChangeResponse} holding the status of the execution of the request
     * @return the imported {@link Identity}
     * @throws IdentityStoreException
     *             in case of error
     */
    public Identity importIdentity( final IdentityChangeRequest identityChangeRequest, final RequestAuthor author, final String clientCode,
            final IdentityChangeResponse response ) throws IdentityStoreException
    {
        final Map<String, String> attributes = identityChangeRequest.getIdentity( ).getAttributes( ).stream( )
                .collect( Collectors.toMap( AttributeDto::getKey, AttributeDto::getValue ) );

        final DuplicateSearchResponse certitudeDuplicates = this.checkDuplicates( attributes, PROPERTY_DUPLICATES_IMPORT_RULES_STRICT, "" );
        if ( certitudeDuplicates != null && CollectionUtils.isNotEmpty( certitudeDuplicates.getIdentities( ) ) )
        {
            if ( certitudeDuplicates.getIdentities( ).size( ) == 1 )
            {
                final IdentityDto strictDuplicate = certitudeDuplicates.getIdentities( ).get( 0 );
                identityChangeRequest.getIdentity( ).setLastUpdateDate( strictDuplicate.getLastUpdateDate( ) );
                return this.update( strictDuplicate.getCustomerId( ), identityChangeRequest, author, clientCode, response );
            }
            else
            {
                response.setStatus( ResponseStatusFactory.conflict( ).setMessage( certitudeDuplicates.getStatus( ).getMessage( ) )
                        .setMessageKey( certitudeDuplicates.getStatus( ).getMessageKey( ) ) );
            }
        }
        else
        {
            final DuplicateSearchResponse suspicionDuplicates = this.checkDuplicates( attributes, PROPERTY_DUPLICATES_IMPORT_RULES_SUSPICION, "" );
            if ( suspicionDuplicates != null && CollectionUtils.isNotEmpty( suspicionDuplicates.getIdentities( ) ) )
            {
                response.setStatus( ResponseStatusFactory.conflict( ).setMessage( suspicionDuplicates.getStatus( ).getMessage( ) )
                        .setMessageKey( suspicionDuplicates.getStatus( ).getMessageKey( ) ) );
            }
            else
            {
                return this.create( identityChangeRequest, author, clientCode, response );
            }
        }

        return null;
    }

    public IdentityDto getQualifiedIdentity( final String customerId ) throws IdentityStoreException
    {
        final Identity identity = IdentityHome.findByCustomerId( customerId );
        final IdentityDto qualifiedIdentity = DtoConverter.convertIdentityToDto( identity );
        IdentityQualityService.instance( ).computeQuality( qualifiedIdentity );
        return qualifiedIdentity;
    }

    /**
     * Perform an identity research over a list of attributes (key and values) specified in the {@link IdentitySearchRequest}
     *
     * @param request
     *            the {@link IdentitySearchRequest} holding the parameters of the research
     * @param author
     *            the author of the request
     * @param response
     *            the {@link IdentitySearchResponse} holding the status of the execution status and the results of the request
     * @param clientCode
     *            code of the {@link ClientApplication} requesting the change
     * @throws ServiceContractNotFoundException
     *             in case of {@link ServiceContract} management error
     * @throws IdentityAttributeNotFoundException
     *             in case of {@link AttributeKey} management error
     */
    public void search( final IdentitySearchRequest request, final RequestAuthor author, final IdentitySearchResponse response, final String clientCode )
            throws IdentityStoreException
    {
        AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, SEARCH_IDENTITY_EVENT_CODE,
                _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( request.toString( ) ), SPECIFIC_ORIGIN );
        final List<SearchAttribute> providedAttributes = request.getSearch( ).getAttributes( );
        final Set<String> providedKeys = commonKeytoKey( providedAttributes.stream( ).map( SearchAttribute::getKey ).collect( Collectors.toSet( ) ) );

        boolean hasRequirements = false;
        final List<IdentitySearchRule> searchRules = IdentitySearchRuleHome.findAll( );
        final Iterator<IdentitySearchRule> iterator = searchRules.iterator( );
        while ( !hasRequirements && iterator.hasNext( ) )
        {
            final IdentitySearchRule searchRule = iterator.next( );
            final Set<String> requiredKeys = searchRule.getAttributes( ).stream( ).map( AttributeKey::getKeyName ).collect( Collectors.toSet( ) );
            if ( searchRule.getType( ) == SearchRuleType.AND )
            {
                if ( providedKeys.containsAll( requiredKeys ) )
                {
                    hasRequirements = true;
                }
            }
            else
                if ( searchRule.getType( ) == SearchRuleType.OR )
                {
                    if ( providedKeys.stream( ).anyMatch( requiredKeys::contains ) )
                    {
                        hasRequirements = true;
                    }
                }
        }

        if ( !hasRequirements )
        {
            final StringBuilder sb = new StringBuilder( );
            final Iterator<IdentitySearchRule> ruleIt = searchRules.iterator( );
            while ( ruleIt.hasNext( ) )
            {
                final IdentitySearchRule rule = ruleIt.next( );
                sb.append( "( " );
                final Iterator<AttributeKey> attrIt = rule.getAttributes( ).iterator( );
                while ( attrIt.hasNext( ) )
                {
                    final AttributeKey attr = attrIt.next( );
                    sb.append( attr.getKeyName( ) ).append( " " );
                    if ( attrIt.hasNext( ) )
                    {
                        sb.append( rule.getType( ).name( ) ).append( " " );
                    }
                }
                sb.append( ")" );
                if ( ruleIt.hasNext( ) )
                {
                    sb.append( " OR " );
                }
            }
            final IdentitySearchMessage alert = new IdentitySearchMessage( );
            alert.setAttributeName( sb.toString( ) );
            alert.setMessage( "Please provide those required attributes to be able to search identities." );
            response.getAlerts( ).add( alert );
            response.setStatus( ResponseStatusFactory.failure( ).setMessageKey( Constants.PROPERTY_REST_ERROR_MISSING_MANDATORY_ATTRIBUTES ) );
            return;
        }

        final QualifiedIdentitySearchResult result = _elasticSearchIdentityService.getQualifiedIdentities( providedAttributes, request.getMax( ),
                request.isConnected( ), Collections.emptyList( ) );
        if ( CollectionUtils.isNotEmpty( result.getQualifiedIdentities( ) ) )
        {
            final List<IdentityDto> filteredIdentities = this.getEnrichedIdentities( request.getSearch( ).getAttributes( ), clientCode,
                    result.getQualifiedIdentities( ) );
            response.setIdentities( filteredIdentities );
            if ( CollectionUtils.isNotEmpty( response.getIdentities( ) ) )
            {
                response.setStatus( ResponseStatusFactory.ok( ).setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
                for ( final IdentityDto identity : response.getIdentities( ) )
                {
                    AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, SEARCH_IDENTITY_EVENT_CODE,
                            _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( identity.getCustomerId( ) ),
                            SPECIFIC_ORIGIN );
                    if ( author.getType( ).equals( AuthorType.agent ) )
                    {
                        /* Indexation et historique */
                        _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.READ,
                                DtoConverter.convertDtoToIdentity( identity ), response.getStatus( ).getType( ).name( ), response.getStatus( ).getMessage( ),
                                author, clientCode, new HashMap<>( ) );
                    }
                }
            }
            else
            {
                response.setStatus( ResponseStatusFactory.noResult( ).setMessageKey( Constants.PROPERTY_REST_ERROR_NO_IDENTITY_FOUND ) );
            }
        }
        else
        {
            response.setStatus( ResponseStatusFactory.noResult( ).setMessageKey( Constants.PROPERTY_REST_ERROR_NO_IDENTITY_FOUND ) );
        }
    }

    /***
     * Check if the attributes are commonKeys, in that case it change them to the attributeKeys it refer
     *
     * @param providedAttributes
     * @return the list of keys
     */
    private Set<String> commonKeytoKey( Set<String> providedAttributes )
    {
        Set<String> returnKeys = new HashSet<>( );

        for ( String attribute : providedAttributes )
        {
            List<AttributeKey> keys = IdentityAttributeService.instance( ).getCommonAttributeKeys( attribute );
            if ( keys != null && !keys.isEmpty( ) )
            {
                for ( AttributeKey key : keys )
                {
                    returnKeys.add( key.getKeyName( ) );
                }
            }
            else
            {
                returnKeys.add( attribute );
            }
        }
        return returnKeys;
    }

    /**
     * Perform an identity research by customer or connection ID.
     *
     * @param customerId
     * @param connectionId
     * @param response
     * @param clientCode
     * @param author
     *            the author of the request
     * @throws IdentityAttributeNotFoundException
     * @throws ServiceContractNotFoundException
     */
    public void search( final String customerId, final String connectionId, final IdentitySearchResponse response, final String clientCode,
            final RequestAuthor author ) throws IdentityStoreException
    {
        AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, GET_IDENTITY_EVENT_CODE, _internalUserService.getApiUser( clientCode ),
                SecurityUtil.logForgingProtect( StringUtils.isNotBlank( customerId ) ? customerId : connectionId ), SPECIFIC_ORIGIN );

        final ServiceContract serviceContract = _serviceContractService.getActiveServiceContract( clientCode );
        if ( serviceContract == null )
        {
            throw new ServiceContractNotFoundException( "No active service contract could be found for clientCode = " + clientCode );
        }
        final IdentityDto identityDto = StringUtils.isNotBlank( customerId ) ? _identityDtoCache.getByCustomerId( customerId, serviceContract )
                : _identityDtoCache.getByConnectionId( connectionId, serviceContract );
        if ( identityDto == null )
        {
            // #345 : If the identity doesn't exist, make an extra search in the history (only for CUID search).
            // If there is a record, it means the identity has been deleted => send back a specific message
            if ( StringUtils.isNotBlank( customerId ) && !IdentityHome.findHistoryByCustomerId( customerId ).isEmpty( ) )
            {
                response.setStatus( ResponseStatusFactory.notFound( ).setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITY_DELETED ) );
            }
            else
            {
                response.setStatus( ResponseStatusFactory.notFound( ).setMessageKey( Constants.PROPERTY_REST_ERROR_NO_IDENTITY_FOUND ) );
            }
        }
        else
        {
            response.setIdentities( Collections.singletonList( identityDto ) );
            response.setStatus( ResponseStatusFactory.ok( ).setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
            // #27998 : Dans le cas d'une interrogation sur un CUID/GUID rapproché, ajouter une ligne dans le bloc "Alerte" dans la réponse de l'identité consolidée
            if ((StringUtils.isNotBlank(customerId) && !identityDto.getCustomerId().equals(customerId)) ||
                (StringUtils.isNotBlank(connectionId) && !identityDto.getConnectionId().equals(connectionId))) {
                final IdentitySearchMessage alert = new IdentitySearchMessage();
                alert.setMessage("Le CUID ou GUID demandé correspond à une identité rapprochée. Cette réponse contient l'identité consilidée.");
                response.getAlerts().add(alert);
            }
            if ( author != null )
            {
                AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, SEARCH_IDENTITY_EVENT_CODE,
                        _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( identityDto.getCustomerId( ) ),
                        SPECIFIC_ORIGIN );
            }
            if ( author != null && author.getType( ).equals( AuthorType.agent ) )
            {
                /* Indexation et historique */
                _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.READ, DtoConverter.convertDtoToIdentity( identityDto ),
                        response.getStatus( ).getType( ).name( ), response.getStatus( ).getMessage( ), author, clientCode, new HashMap<>( ) );
            }
        }
    }

    /**
     * Performs a search of a list of {@link IdentityDto}, providing a list of customer ids
     * 
     * @param customerIds
     *            the customer ids to search for
     * @return a list of {@link IdentityDto}
     */
    public List<IdentityDto> search( final List<String> customerIds, final List<String> attributes )
    {
        try
        {
            final QualifiedIdentitySearchResult result = _elasticSearchIdentityService.getQualifiedIdentities( customerIds, attributes );
            if ( result != null && CollectionUtils.isNotEmpty( result.getQualifiedIdentities( ) ) )
            {
                result.getQualifiedIdentities( ).forEach( identityDto -> {
                    IdentityQualityService.instance( ).computeQuality( identityDto );
                    identityDto.getQuality( ).setScoring( 1D );
                    identityDto.getQuality( ).setCoverage( 1 );
                } );
                return result.getQualifiedIdentities( );
            }
        }
        catch( final IdentityStoreException e )
        {
            // ignore this identity
        }
        return new ArrayList<>( );
    }

    /**
     * Performs a search of an {@link IdentityDto}, providing its customer id
     * 
     * @param customerId
     *            the customer id to search for
     * @return an {@link IdentityDto}
     */
    public IdentityDto search( final String customerId )
    {
        try
        {
            final QualifiedIdentitySearchResult result = _elasticSearchIdentityService.getQualifiedIdentities( customerId, Collections.emptyList( ) );
            if ( result != null && CollectionUtils.isNotEmpty( result.getQualifiedIdentities( ) ) )
            {
                final IdentityDto identityDto = result.getQualifiedIdentities( ).get( 0 );
                IdentityQualityService.instance( ).computeQuality( identityDto );
                identityDto.getQuality( ).setScoring( 1D );
                identityDto.getQuality( ).setCoverage( 1 );
                return identityDto;
            }
        }
        catch( final IdentityStoreException e )
        {
            // ignore this identity
        }
        return null;
    }

    /**
     * Filter a list of search results over {@link ServiceContract} defined for the given clientCode. Also complete identities with additional information
     * (quality, duplicates, ...).
     * 
     * @param searchAttributes
     *            la requête de recherche si existante
     * @param clientCode
     *            le code client du demandeur
     * @param identities
     *            la liste de résultats à traiter
     * @return the list of filtered and completed {@link IdentityDto}
     * @throws ServiceContractNotFoundException
     *             in case of error
     */
    private List<IdentityDto> getEnrichedIdentities( final List<SearchAttribute> searchAttributes, final String clientCode, final List<IdentityDto> identities )
            throws ServiceContractNotFoundException
    {
        final ServiceContract serviceContract = _serviceContractService.getActiveServiceContract( clientCode );
        final Comparator<QualityDefinition> qualityComparator = Comparator.comparing( QualityDefinition::getScoring )
                .thenComparingDouble( QualityDefinition::getQuality ).reversed( );
        final Comparator<IdentityDto> identityComparator = Comparator.comparing( IdentityDto::getQuality, qualityComparator );
        return identities.stream( ).filter( IdentityDto::isNotMerged )
                .peek( identity -> IdentityQualityService.instance( ).enrich( searchAttributes, identity, serviceContract, null ) ).sorted( identityComparator )
                .collect( Collectors.toList( ) );
    }

    private List<AttributeStatus> updateIdentity( final IdentityDto requestIdentity, final String clientCode, final ChangeResponse response,
            final Identity identity ) throws IdentityStoreException
    {
        final List<AttributeStatus> attrStatusList = new ArrayList<>( );

        /* Récupération des attributs déja existants ou non */
        final Map<Boolean, List<AttributeDto>> sortedAttributes = requestIdentity.getAttributes( ).stream( )
                .collect( Collectors.partitioningBy( a -> identity.getAttributes( ).containsKey( a.getKey( ) ) ) );
        final List<AttributeDto> existingWritableAttributes = CollectionUtils.isNotEmpty( sortedAttributes.get( true ) ) ? sortedAttributes.get( true )
                : new ArrayList<>( );
        final List<AttributeDto> newWritableAttributes = CollectionUtils.isNotEmpty( sortedAttributes.get( false ) ) ? sortedAttributes.get( false )
                : new ArrayList<>( );

        // If identity is connected and service contract doesn't allow unrestricted update, do a bunch of checks
        if ( identity.isConnected( ) && !_serviceContractService.canModifyConnectedIdentity( clientCode ) )
        {
            this.connectedIdentityUpdateCheck( requestIdentity, identity, existingWritableAttributes, newWritableAttributes, response );
            if ( ResponseStatusFactory.unauthorized( ).equals( response.getStatus( ) ) )
            {
                return attrStatusList;
            }
        }

        /* Create or Update birth country and city */
        attrStatusList.addAll( GeocodesService.processCountryAndCityForUpdate( identity, newWritableAttributes, existingWritableAttributes, clientCode ) );

        /* Create new attributes */
        for ( final AttributeDto attributeToWrite : newWritableAttributes )
        {
            final AttributeStatus attributeStatus = _identityAttributeService.createAttribute( attributeToWrite, identity, clientCode );
            attrStatusList.add( attributeStatus );
        }

        /* Update existing attributes */
        for ( final AttributeDto attributeToUpdate : existingWritableAttributes )
        {
            final AttributeStatus attributeStatus = _identityAttributeService.updateAttribute( attributeToUpdate, identity, clientCode );
            attrStatusList.add( attributeStatus );
        }

        boolean monParisUpdated = false;
        if ( requestIdentity.getMonParisActive( ) != null && requestIdentity.getMonParisActive( ) != identity.isMonParisActive( ) )
        {
            monParisUpdated = true;
            identity.setMonParisActive( requestIdentity.isMonParisActive( ) );
        }

        if ( monParisUpdated || !Collections.disjoint( AttributeChangeStatus.getSuccessStatuses( ),
                attrStatusList.stream( ).map( AttributeStatus::getStatus ).collect( Collectors.toList( ) ) ) )
        {
            // If there was an update on the monParis flag or in the attributes, we update the identity
            IdentityHome.update( identity );
        }

        return attrStatusList;
    }

    /**
     * Makes a bunch of checks regarding the validity of this update request on this connected identity.
     * <ul>
     * <li>Authorise update on "PIVOT" attributes only</li>
     * <li>For new attributes, certification level must be > 100 (better than self-declare)</li>
     * <li>For existing attributes, certification level must be >= than the existing level</li>
     * <li>If one "PIVOT" attribute is certified at a certain level N (conf) :
     * <ul>
     * <li>All "PIVOT" attributes must be set</li>
     * <li>All "PIVOT" attributes must be certified with level greater or equal to N</li>
     * </ul>
     * </li>
     * </ul>
     * 
     * @param requestIdentity
     *            the request
     * @param identity
     *            the identity
     * @param existingWritableAttributes
     *            existing attributes in identity from request
     * @param newWritableAttributes
     *            new attributes from request
     */
    private void connectedIdentityUpdateCheck( final IdentityDto requestIdentity, final Identity identity, final List<AttributeDto> existingWritableAttributes,
            final List<AttributeDto> newWritableAttributes, final ChangeResponse response )
    {
        // TODO refactor to use cache ?
        final Map<String, AttributeKey> allAttributesByKey = AttributeKeyHome.getAttributeKeysList( false ).stream( )
                .collect( Collectors.toMap( AttributeKey::getKeyName, a -> a ) );

        // - Authorise update on "PIVOT" attributes only
        final boolean requestOnNonPivot = requestIdentity.getAttributes( ).stream( ).map( a -> allAttributesByKey.get( a.getKey( ) ) )
                .anyMatch( a -> !a.getPivot( ) );
        if ( requestOnNonPivot )
        {
            response.setStatus( ResponseStatusFactory.unauthorized( ).setMessage( "Identity is connected, updating non 'pivot' attributes is forbidden." )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_CONNECTED_IDENTITY_FORBIDDEN_UPDATE_NON_PIVOT ) );
            return;
        }

        // - For new attributes, certification level must be > 100 (better than self-declare)
        final boolean newAttrSelfDeclare = newWritableAttributes.stream( )
                .map( a -> RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( a.getCertifier( ), a.getKey( ) ) )
                .anyMatch( c -> Integer.parseInt( c.getRefCertificationLevel( ).getLevel( ) ) <= 100 );
        if ( newAttrSelfDeclare )
        {
            response.setStatus( ResponseStatusFactory.unauthorized( )
                    .setMessage( "Identity is connected, adding 'pivot' attributes with self-declarative certification level is forbidden." )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_CONNECTED_IDENTITY_FORBIDDEN_PIVOT_SELF_DECLARE ) );
            return;
        }

        // - For existing attributes, certification level must be >= than the existing level
        final boolean lesserWantedLvl = existingWritableAttributes.stream( )
                .map( a -> RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( a.getCertifier( ), a.getKey( ) ) ).anyMatch( wantedCertif -> {
                    final int wantedLvl = Integer.parseInt( wantedCertif.getRefCertificationLevel( ).getLevel( ) );

                    final IdentityAttribute existingAttr = identity.getAttributes( ).get( wantedCertif.getAttributeKey( ).getKeyName( ) );
                    final RefAttributeCertificationLevel existingCertif = RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName(
                            existingAttr.getCertificate( ).getCertifierCode( ), existingAttr.getAttributeKey( ).getKeyName( ) );
                    final int existingLvl = Integer.parseInt( existingCertif.getRefCertificationLevel( ).getLevel( ) );

                    return wantedLvl < existingLvl;
                } );
        if ( lesserWantedLvl )
        {
            response.setStatus( ResponseStatusFactory.unauthorized( )
                    .setMessage( "Identity is connected, updating existing 'pivot' attributes with lesser certification level is forbidden." )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_CONNECTED_IDENTITY_FORBIDDEN_UPDATE_PIVOT_LESSER_CERTIFICATION ) );
            return;
        }

        // - If one "PIVOT" attribute is certified at a certain level N (conf), all "PIVOT" attributes must be set and certified with level >= N.
        final int threshold = AppPropertiesService.getPropertyInt( PIVOT_CERTIF_LEVEL_THRESHOLD, 400 );
        final boolean breakingThreshold = identity.getAttributes( ).values( ).stream( ).filter( a -> a.getAttributeKey( ).getPivot( ) )
                .map( a -> RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( a.getCertificate( ).getCertifierCode( ),
                        a.getAttributeKey( ).getKeyName( ) ) )
                .anyMatch( c -> Integer.parseInt( c.getRefCertificationLevel( ).getLevel( ) ) >= threshold )
                || requestIdentity.getAttributes( ).stream( )
                        .map( a -> RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( a.getCertifier( ), a.getKey( ) ) )
                        .anyMatch( c -> Integer.parseInt( c.getRefCertificationLevel( ).getLevel( ) ) >= threshold );
        if ( breakingThreshold )
        {
            // get all pivot attributes from database
            final List<String> pivotAttributeKeys = allAttributesByKey.values( ).stream( ).filter( AttributeKey::getPivot ).map( AttributeKey::getKeyName )
                    .collect( Collectors.toList( ) );

            // if any pivot is missing from request + existing -> unauthorized
            @SuppressWarnings( "unchecked" )
            final Collection<String> unionOfExistingAndRequestedPivotKeys = CollectionUtils.union(
                    requestIdentity.getAttributes( ).stream( ).map( AttributeDto::getKey ).collect( Collectors.toSet( ) ),
                    identity.getAttributes( ).values( ).stream( ).map( IdentityAttribute::getAttributeKey ).filter( AttributeKey::getPivot )
                            .map( AttributeKey::getKeyName ).collect( Collectors.toSet( ) ) );
            if ( !CollectionUtils.isEqualCollection( pivotAttributeKeys, unionOfExistingAndRequestedPivotKeys ) )
            {
                response.setStatus( ResponseStatusFactory.unauthorized( )
                        .setMessage( "Identity is connected, and at least one 'pivot' attribute is, or has been requested to be, certified above level "
                                + threshold + ". In that case, all 'pivot' attributes must be set, and certified with level greater or equal to " + threshold
                                + "." )
                        .setMessageKey( Constants.PROPERTY_REST_ERROR_CONNECTED_IDENTITY_FORBIDDEN_PIVOT_CERTIFICATION_UNDER_THRESHOLD ) );
                return;
            }

            // if any has level lesser than threshold -> unauthorized
            final boolean lesserThanThreshold = pivotAttributeKeys.stream( ).map( key -> {
                final AttributeDto requested = requestIdentity.getAttributes( ).stream( ).filter( a -> a.getKey( ).equals( key ) ).findFirst( ).orElse( null );
                final IdentityAttribute existing = identity.getAttributes( ).get( key );
                int requestedLvl = 0;
                int existingLvl = 0;
                if ( requested != null )
                {
                    requestedLvl = Integer.parseInt( RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( requested.getCertifier( ), key )
                            .getRefCertificationLevel( ).getLevel( ) );
                }
                if ( existing != null )
                {
                    existingLvl = Integer.parseInt(
                            RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( existing.getCertificate( ).getCertifierCode( ), key )
                                    .getRefCertificationLevel( ).getLevel( ) );
                }
                return Math.max( requestedLvl, existingLvl );
            } ).anyMatch( lvl -> lvl < threshold );

            if ( lesserThanThreshold )
            {
                response.setStatus( ResponseStatusFactory.unauthorized( )
                        .setMessage( "Identity is connected, and at least one 'pivot' attribute is, or has been requested to be, certified above level "
                                + threshold + ". In that case, all 'pivot' attributes must be set, and certified with level greater or equal to " + threshold
                                + "." )
                        .setMessageKey( Constants.PROPERTY_REST_ERROR_CONNECTED_IDENTITY_FORBIDDEN_PIVOT_CERTIFICATION_UNDER_THRESHOLD ) );
            }
        }
    }

    /**
     * Gets a list of qualified identities on which to search potential duplicates.<br/>
     * Returned identities must have all attributes checked by the provided rule, and must also not be already merged nor be tagged as suspicious.<br/>
     * The list is sorted by quality (higher quality identities first).
     *
     * @param rule
     *            the rule used to get matching identities
     * @param batchSize the size of the batches
     * @param includeSuspicions filter CUIDs, to include or not, the CUIDs that are identified as suspicions
     * @return the list of identities
     */
    public Batch<String> getCUIDsBatchForPotentialDuplicate(final DuplicateRule rule, final int batchSize, final boolean includeSuspicions )
    {
        final List<Integer> attributes = rule.getCheckedAttributes( ).stream( ).map( AttributeKey::getId ).collect( Collectors.toList( ) );
        final List<String> customerIdsList = IdentityHome.findByAttributeExisting( attributes, rule.getNbFilledAttributes( ), true, !includeSuspicions, rule.getPriority() );
        if ( customerIdsList.isEmpty( ) )
        {
            return Batch.ofSize( Collections.emptyList( ), 0 );
        }
        return Batch.ofSize( customerIdsList, batchSize );
    }

    /**
     * request a deletion of identity .
     *
     * @param customerId
     *            the customer ID
     * @param clientCode
     *            the client code
     * @param author
     *            the author of the request
     */
    public void deleteRequest( final String customerId, final String clientCode, final RequestAuthor author, final IdentityChangeResponse response )
            throws IdentityStoreException
    {
        if ( !_serviceContractService.canDeleteIdentity( clientCode ) )
        {
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( "The client application is not authorized to request the deletion of an identity." )
                            .setMessageKey( Constants.PROPERTY_REST_ERROR_DELETE_UNAUTHORIZED ) );
            response.setCustomerId( customerId );
            return;
        }

        // check identity
        Identity identity = IdentityHome.findByCustomerId( customerId );
        if ( identity == null )
        {
            response.setStatus(
                    ResponseStatusFactory.notFound( ).setMessage( "Identity not found." ).setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITY_NOT_FOUND ) );
            response.setCustomerId( customerId );
            return;
        }
        if ( identity.isDeleted( ) )
        {
            response.setStatus( ResponseStatusFactory.failure( ).setMessage( "Identity allready in deleted state." )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITY_ALREADY_DELETED ) );
            response.setCustomerId( customerId );

            return;
        }
        if ( identity.isMerged( ) )
        {
            response.setStatus( ResponseStatusFactory.failure( ).setMessage( "Identity in merged state can not be deleted." )
                    .setMessageKey( Constants.PROPERTY_REST_ERROR_FORBIDDEN_DELETE_ON_MERGED_IDENTITY ) );
            response.setCustomerId( customerId );
            return;
        }

        TransactionManager.beginTransaction( null );
        try
        {
            // expire identity (the deletion is managed by the dedicated Daemon)
            IdentityHome.softRemove( customerId );
            response.setStatus( ResponseStatusFactory.success( ).setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
            TransactionManager.commitTransaction( null );

            /* Notify listeners for indexation, history, ... */
            _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.DELETE, identity, response.getStatus( ).getType( ).name( ),
                    response.getStatus( ).getMessage( ), author, clientCode, new HashMap<>( ) );

            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_DELETE, DELETE_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( customerId ), SPECIFIC_ORIGIN );
        }
        catch( Exception e )
        {
            TransactionManager.rollBack( null );
        }

    }

    /**
     * Check if duplicates exist for a set of attributes
     * 
     * @param attributes
     *            the set of attributes
     * @param ruleCodeProperty
     *            the properties that defines the list of rules to check
     * @return a {@link DuplicateSearchResponse} that holds the execution result
     * @throws IdentityStoreException
     *             in case of error
     */
    private DuplicateSearchResponse checkDuplicates( final Map<String, String> attributes, final String ruleCodeProperty, final String customerId )
            throws IdentityStoreException
    {
        final List<String> ruleCodes = Arrays.stream(AppPropertiesService.getProperty( ruleCodeProperty, "" ).split( "," )).filter(StringUtils::isNotEmpty).collect(Collectors.toList());
        if( !ruleCodes.isEmpty( ) )
        {
            final DuplicateSearchResponse esDuplicates = _duplicateServiceElasticSearch.findDuplicates( attributes, customerId, ruleCodes,
                    Collections.emptyList( ) );
            if ( esDuplicates != null )
            {
                return esDuplicates;
            }
            final boolean checkDatabase = AppPropertiesService.getPropertyBoolean( PROPERTY_DUPLICATES_CHECK_DATABASE_ACTIVATED, false );
            if ( checkDatabase )
            {
                return _duplicateServiceDatabase.findDuplicates( attributes, "", ruleCodes, Collections.emptyList( ) );
            }
        }
        return null;
    }

    /**
     * Dé-certification d'une identité.
     *
     * @param strCustomerId
     *            customer ID
     * @return the response
     * @see IdentityAttributeService#uncertifyAttribute
     */
    public IdentityChangeResponse uncertifyIdentity( final String strCustomerId, final String strClientCode, final RequestAuthor author )
    {
        final IdentityChangeResponse response = new IdentityChangeResponse( );

        final Identity identity = IdentityHome.findByCustomerId( strCustomerId );
        if ( identity == null )
        {
            response.setStatus(
                    ResponseStatusFactory.notFound( ).setMessage( "No identity found" ).setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITY_NOT_FOUND ) );
            return response;
        }

        TransactionManager.beginTransaction( null );
        try
        {
            final List<AttributeStatus> attrStatusList = new ArrayList<>( );
            for ( final IdentityAttribute attribute : identity.getAttributes( ).values( ) )
            {
                final AttributeStatus status = _identityAttributeService.uncertifyAttribute( attribute );
                attrStatusList.add( status );
            }

            // update identity to set lastupdate_date
            IdentityHome.update( identity );

            response.setLastUpdateDate( identity.getLastUpdateDate( ) );
            response.setStatus( ResponseStatusFactory.success( ).setAttributeStatuses( attrStatusList )
                    .setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
            TransactionManager.commitTransaction( null );

            /* Historique des modifications */
            for ( AttributeStatus attributeStatus : attrStatusList )
            {
                _identityStoreNotifyListenerService.notifyListenersAttributeChange( AttributeChangeType.UPDATE, identity, attributeStatus, author,
                        strClientCode );
            }

            /* Indexation et historique */
            _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.UPDATE, identity, response.getStatus( ).getType( ).name( ),
                    response.getStatus( ).getMessage( ), author, strClientCode, new HashMap<>( ) );

            AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, DECERTIFY_IDENTITY_EVENT_CODE,
                    _internalUserService.getApiUser( author, strClientCode ), SecurityUtil.logForgingProtect( strCustomerId ), SPECIFIC_ORIGIN );
        }
        catch( final Exception e )
        {
            response.setStatus(
                    ResponseStatusFactory.failure( ).setMessage( e.getMessage( ) ).setMessageKey( Constants.PROPERTY_REST_ERROR_DURING_TREATMENT ) );
            TransactionManager.rollBack( null );
        }

        return response;
    }

    /**
     * Delete the identity and all his children, including potential merged identities, EXCEPT identity history.<br/>
     * The purge consist of deleting, for the identity and all of its potential merged identities :
     * <ul>
     * <li>the {@link Identity} object</li>
     * <li>the {@link IdentityAttribute} objetcs</li>
     * <li>the IdentityAttributeHistory objects</li>
     * <li>the {@link SuspiciousIdentity} objects</li>
     * <li>the {@link ExcludedIdentities} objects</li>
     * </ul>
     * The identity's history is kept.
     * 
     * @param customerId
     *            the customerId of the identity to delete
     */
    public void delete( final String customerId )
    {
        final int identityId = IdentityHome.findIdByCustomerId( customerId );
        if ( identityId != -1 )
        {
            final List<Identity> mergedIdentities = IdentityHome.findMergedIdentities( identityId );
            TransactionManager.beginTransaction( null );
            try
            {
                // Delete eventual merged identities first
                for ( final Identity mergedIdentity : mergedIdentities )
                {
                    SuspiciousIdentityHome.remove( mergedIdentity.getCustomerId( ) );
                    SuspiciousIdentityHome.removeExcludedIdentities( mergedIdentity.getCustomerId( ) );
                    IdentityHome.deleteAttributeHistory( mergedIdentity.getId( ) );
                    IdentityHome.hardRemove( mergedIdentity.getId( ) );
                }
                // Delete the actual identity
                SuspiciousIdentityHome.remove( customerId );
                SuspiciousIdentityHome.removeExcludedIdentities( customerId );
                IdentityHome.deleteAttributeHistory( identityId );
                IdentityHome.hardRemove( identityId );

                TransactionManager.commitTransaction( null );
            }
            catch( final Exception e )
            {
                TransactionManager.rollBack( null );
                throw e;
            }
        }
    }

}