View Javadoc
1   /*
2    * Copyright (c) 2002-2024, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.plugins.identitystore.service.identity;
35  
36  import fr.paris.lutece.plugins.identitystore.business.application.ClientApplication;
37  import fr.paris.lutece.plugins.identitystore.business.attribute.AttributeKey;
38  import fr.paris.lutece.plugins.identitystore.business.attribute.AttributeKeyHome;
39  import fr.paris.lutece.plugins.identitystore.business.contract.ServiceContract;
40  import fr.paris.lutece.plugins.identitystore.business.duplicates.suspicions.ExcludedIdentities;
41  import fr.paris.lutece.plugins.identitystore.business.duplicates.suspicions.SuspiciousIdentity;
42  import fr.paris.lutece.plugins.identitystore.business.duplicates.suspicions.SuspiciousIdentityHome;
43  import fr.paris.lutece.plugins.identitystore.business.identity.Identity;
44  import fr.paris.lutece.plugins.identitystore.business.identity.IdentityAttribute;
45  import fr.paris.lutece.plugins.identitystore.business.identity.IdentityAttributeHome;
46  import fr.paris.lutece.plugins.identitystore.business.identity.IdentityHome;
47  import fr.paris.lutece.plugins.identitystore.business.referentiel.RefAttributeCertificationLevel;
48  import fr.paris.lutece.plugins.identitystore.business.referentiel.RefAttributeCertificationLevelHome;
49  import fr.paris.lutece.plugins.identitystore.business.rules.duplicate.DuplicateRule;
50  import fr.paris.lutece.plugins.identitystore.business.rules.duplicate.DuplicateRuleHome;
51  import fr.paris.lutece.plugins.identitystore.business.rules.search.IdentitySearchRule;
52  import fr.paris.lutece.plugins.identitystore.business.rules.search.IdentitySearchRuleHome;
53  import fr.paris.lutece.plugins.identitystore.business.rules.search.SearchRuleType;
54  import fr.paris.lutece.plugins.identitystore.cache.IdentityDtoCache;
55  import fr.paris.lutece.plugins.identitystore.service.attribute.IdentityAttributeService;
56  import fr.paris.lutece.plugins.identitystore.service.contract.ServiceContractNotFoundException;
57  import fr.paris.lutece.plugins.identitystore.service.contract.ServiceContractService;
58  import fr.paris.lutece.plugins.identitystore.service.duplicate.IDuplicateService;
59  import fr.paris.lutece.plugins.identitystore.service.geocodes.GeocodesService;
60  import fr.paris.lutece.plugins.identitystore.service.listeners.IdentityStoreNotifyListenerService;
61  import fr.paris.lutece.plugins.identitystore.service.search.ISearchIdentityService;
62  import fr.paris.lutece.plugins.identitystore.service.user.InternalUserService;
63  import fr.paris.lutece.plugins.identitystore.utils.Batch;
64  import fr.paris.lutece.plugins.identitystore.v3.web.rs.DtoConverter;
65  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeChangeStatus;
66  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeChangeStatusType;
67  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeDto;
68  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeStatus;
69  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AuthorType;
70  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.ChangeResponse;
71  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.IdentityDto;
72  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.QualityDefinition;
73  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.RequestAuthor;
74  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.ResponseStatus;
75  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.IdentityChangeRequest;
76  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.IdentityChangeResponse;
77  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.history.AttributeChangeType;
78  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.history.IdentityChangeType;
79  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.merge.IdentityMergeRequest;
80  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.merge.IdentityMergeResponse;
81  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.DuplicateSearchResponse;
82  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.IdentitySearchMessage;
83  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.IdentitySearchRequest;
84  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.IdentitySearchResponse;
85  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.QualifiedIdentitySearchResult;
86  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.SearchAttribute;
87  import fr.paris.lutece.plugins.identitystore.v3.web.rs.util.Constants;
88  import fr.paris.lutece.plugins.identitystore.v3.web.rs.util.ResponseStatusFactory;
89  import fr.paris.lutece.plugins.identitystore.web.exception.IdentityStoreException;
90  import fr.paris.lutece.portal.service.security.AccessLogService;
91  import fr.paris.lutece.portal.service.security.AccessLoggerConstants;
92  import fr.paris.lutece.portal.service.spring.SpringContextService;
93  import fr.paris.lutece.portal.service.util.AppPropertiesService;
94  import fr.paris.lutece.util.http.SecurityUtil;
95  import fr.paris.lutece.util.sql.TransactionManager;
96  import org.apache.commons.collections4.CollectionUtils;
97  import org.apache.commons.lang3.StringUtils;
98  
99  import java.util.ArrayList;
100 import java.util.Arrays;
101 import java.util.Collection;
102 import java.util.Collections;
103 import java.util.Comparator;
104 import java.util.HashMap;
105 import java.util.HashSet;
106 import java.util.Iterator;
107 import java.util.List;
108 import java.util.Map;
109 import java.util.Objects;
110 import java.util.Set;
111 import java.util.stream.Collectors;
112 
113 public class IdentityService
114 {
115     // Conf
116     private static final String PIVOT_CERTIF_LEVEL_THRESHOLD = "identitystore.identity.attribute.update.pivot.certif.level.threshold";
117 
118     // EVENTS FOR ACCESS LOGGING
119     public static final String CREATE_IDENTITY_EVENT_CODE = "CREATE_IDENTITY";
120     public static final String UPDATE_IDENTITY_EVENT_CODE = "UPDATE_IDENTITY";
121     public static final String DECERTIFY_IDENTITY_EVENT_CODE = "DECERTIFY_IDENTITY";
122     public static final String GET_IDENTITY_EVENT_CODE = "GET_IDENTITY";
123     public static final String SEARCH_IDENTITY_EVENT_CODE = "SEARCH_IDENTITY";
124     public static final String DELETE_IDENTITY_EVENT_CODE = "DELETE_IDENTITY";
125     public static final String CONSOLIDATE_IDENTITY_EVENT_CODE = "CONSOLIDATE_IDENTITY";
126     public static final String MERGE_IDENTITY_EVENT_CODE = "MERGE_IDENTITY";
127     public static final String CANCEL_MERGE_IDENTITY_EVENT_CODE = "CANCEL_MERGE_IDENTITY";
128     public static final String CANCEL_CONSOLIDATE_IDENTITY_EVENT_CODE = "CANCEL_CONSOLIDATE_IDENTITY";
129     public static final String SPECIFIC_ORIGIN = "BO";
130 
131     // PROPERTIES
132     private static final String PROPERTY_DUPLICATES_IMPORT_RULES_SUSPICION = "identitystore.identity.duplicates.import.rules.suspicion";
133     private static final String PROPERTY_DUPLICATES_IMPORT_RULES_STRICT = "identitystore.identity.duplicates.import.rules.strict";
134     private static final String PROPERTY_DUPLICATES_CREATION_RULES = "identitystore.identity.duplicates.creation.rules";
135     private static final String PROPERTY_DUPLICATES_UPDATE_RULES = "identitystore.identity.duplicates.update.rules";
136     private static final String PROPERTY_DUPLICATES_CHECK_DATABASE_ACTIVATED = "identitystore.identity.duplicates.check.database";
137 
138     // SERVICES
139     private final IdentityStoreNotifyListenerService _identityStoreNotifyListenerService = IdentityStoreNotifyListenerService.instance( );
140     private final ServiceContractService _serviceContractService = ServiceContractService.instance( );
141     private final IdentityAttributeService _identityAttributeService = IdentityAttributeService.instance( );
142     private final InternalUserService _internalUserService = InternalUserService.getInstance( );
143     private final IDuplicateService _duplicateServiceDatabase = SpringContextService.getBean( "identitystore.duplicateService.database" );
144     private final IDuplicateService _duplicateServiceElasticSearch = SpringContextService.getBean( "identitystore.duplicateService.elasticsearch" );
145     private final ISearchIdentityService _elasticSearchIdentityService = SpringContextService.getBean( "identitystore.searchIdentityService.elasticsearch" );
146 
147     // CACHE
148     private final IdentityDtoCache _identityDtoCache = SpringContextService.getBean( "identitystore.identityDtoCache" );
149 
150     private static IdentityService _instance;
151 
152     public static IdentityService instance( )
153     {
154         if ( _instance == null )
155         {
156             _instance = new IdentityService( );
157         }
158         return _instance;
159     }
160 
161     /**
162      * Creates a new {@link Identity} according to the given {@link IdentityChangeRequest}
163      *
164      * @param request
165      *            the {@link IdentityChangeRequest} holding the parameters of the identity change request
166      * @param author
167      *            the author of the request
168      * @param clientCode
169      *            code of the {@link ClientApplication} requesting the change
170      * @param response
171      *            the {@link IdentityChangeResponse} holding the status of the execution of the request
172      * @return the created {@link Identity}
173      * @throws IdentityStoreException
174      *             in case of error
175      */
176     public Identity create( final IdentityChangeRequest request, final RequestAuthor author, final String clientCode, final IdentityChangeResponse response )
177             throws IdentityStoreException
178     {
179         if ( !_serviceContractService.canCreateIdentity( clientCode ) )
180         {
181             response.setStatus( ResponseStatusFactory.failure( ).setMessage( "The client application is not authorized to create an identity." )
182                     .setMessageKey( Constants.PROPERTY_REST_ERROR_CREATE_UNAUTHORIZED ) );
183             return null;
184         }
185 
186         if ( StringUtils.isNotEmpty( request.getIdentity( ).getCustomerId( ) ) )
187         {
188             throw new IdentityStoreException( "You cannot specify a CUID when requesting for a creation" );
189         }
190 
191         if ( StringUtils.isNotEmpty( request.getIdentity( ).getConnectionId( ) ) && !_serviceContractService.canModifyConnectedIdentity( clientCode ) )
192         {
193             throw new IdentityStoreException( "You cannot specify a GUID when requesting for a creation" );
194         }
195 
196         // check if all mandatory attributes are present
197         final List<String> mandatoryAttributes = _serviceContractService.getMandatoryAttributes( clientCode,
198                 AttributeKeyHome.getMandatoryForCreationAttributeKeyList( ) );
199         if ( CollectionUtils.isNotEmpty( mandatoryAttributes ) )
200         {
201             final Set<String> providedKeySet = request.getIdentity( ).getAttributes( ).stream( ).filter( a -> StringUtils.isNotBlank( a.getValue( ) ) )
202                     .map( AttributeDto::getKey ).collect( Collectors.toSet( ) );
203             if ( !providedKeySet.containsAll( mandatoryAttributes ) )
204             {
205                 response.setStatus( ResponseStatusFactory.failure( ).setMessage( "All mandatory attributes must be provided : " + mandatoryAttributes )
206                         .setMessageKey( Constants.PROPERTY_REST_ERROR_MISSING_MANDATORY_ATTRIBUTES ) );
207                 return null;
208             }
209         }
210 
211         // check if can set "mon_paris_active" flag to true
212         if ( request.getIdentity( ).getMonParisActive( ) != null && !_serviceContractService.canModifyConnectedIdentity( clientCode ) )
213         {
214             throw new IdentityStoreException( "You cannot set the 'mon_paris_active' flag when requesting for a creation" );
215         }
216 
217         // check if GUID is already in use
218         if ( StringUtils.isNotEmpty( request.getIdentity( ).getConnectionId( ) )
219                 && IdentityHome.findByConnectionId( request.getIdentity( ).getConnectionId( ) ) != null )
220         {
221             throw new IdentityStoreException( "GUID is already in use." );
222         }
223 
224         final Map<String, String> attributes = request.getIdentity( ).getAttributes( ).stream( ).filter( a -> StringUtils.isNotBlank( a.getValue( ) ) )
225                 .collect( Collectors.toMap( AttributeDto::getKey, AttributeDto::getValue, ( a, b ) -> a ) );
226         final DuplicateSearchResponse duplicateSearchResponse = this.checkDuplicates( attributes, PROPERTY_DUPLICATES_CREATION_RULES, "" );
227         if ( duplicateSearchResponse != null && CollectionUtils.isNotEmpty( duplicateSearchResponse.getIdentities( ) ) )
228         {
229             response.setStatus( ResponseStatusFactory.conflict( ).setMessage( duplicateSearchResponse.getStatus( ).getMessage( ) )
230                     .setMessageKey( duplicateSearchResponse.getStatus( ).getMessageKey( ) ) );
231             return null;
232         }
233 
234         final Identity/identitystore/business/identity/Identity.html#Identity">Identity identity = new Identity( );
235         TransactionManager.beginTransaction( null );
236         try
237         {
238             identity.setMonParisActive( request.getIdentity( ).isMonParisActive( ) );
239             if ( StringUtils.isNotEmpty( request.getIdentity( ).getConnectionId( ) ) )
240             {
241                 identity.setConnectionId( request.getIdentity( ).getConnectionId( ) );
242             }
243             IdentityHome.create( identity, _serviceContractService.getDataRetentionPeriodInMonths( clientCode ) );
244 
245             final List<AttributeDto> attributesToCreate = request.getIdentity( ).getAttributes( );
246             final List<AttributeStatus> attrStatusList = GeocodesService.processCountryAndCityForCreate( identity, attributesToCreate, clientCode );
247             for ( final AttributeDto attributeDto : attributesToCreate )
248             {
249                 // TODO vérifier que la clef d'attribut existe dans le référentiel
250                 final AttributeStatus attributeStatus = _identityAttributeService.createAttribute( attributeDto, identity, clientCode );
251                 attrStatusList.add( attributeStatus );
252             }
253 
254             response.setCustomerId( identity.getCustomerId( ) );
255             response.setCreationDate( identity.getCreationDate( ) );
256             final boolean incompleteCreation = attrStatusList.stream( ).anyMatch( s -> s.getStatus( ).equals( AttributeChangeStatus.NOT_CREATED ) );
257             final ResponseStatus status = incompleteCreation ? ResponseStatusFactory.incompleteSuccess( ) : ResponseStatusFactory.success( );
258             response.setStatus( status.setAttributeStatuses( attrStatusList ).setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
259             TransactionManager.commitTransaction( null );
260 
261             /* Historique des modifications */
262             final List<AttributeStatus> createdAttributes = attrStatusList.stream( ).filter( s -> s.getStatus( ).equals( AttributeChangeStatus.CREATED ) )
263                     .collect( Collectors.toList( ) );
264             for ( AttributeStatus attributeStatus : createdAttributes )
265             {
266                 _identityStoreNotifyListenerService.notifyListenersAttributeChange( AttributeChangeType.CREATE, identity, attributeStatus, author, clientCode );
267             }
268 
269             /* Indexation et historique */
270             _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.CREATE, identity, response.getStatus( ).getType( ).name( ),
271                     response.getStatus( ).getMessage( ), author, clientCode, new HashMap<>( ) );
272             AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_CREATE, CREATE_IDENTITY_EVENT_CODE,
273                     _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( identity.getCustomerId( ) ), SPECIFIC_ORIGIN );
274         }
275         catch( Exception e )
276         {
277             response.setStatus(
278                     ResponseStatusFactory.failure( ).setMessage( e.getMessage( ) ).setMessageKey( Constants.PROPERTY_REST_ERROR_DURING_TREATMENT ) );
279             TransactionManager.rollBack( null );
280         }
281 
282         return identity;
283     }
284 
285     /**
286      * Updates an existing {@link Identity} according to the given {@link IdentityChangeRequest} and following the given rules: <br>
287      * <ul>
288      * <li>The {@link Identity} must exist in te database. If not, NOT_FOUND status is returned in the execution response</li>
289      * <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
290      * primary identity is returned in the execution response with a CONFLICT status</li>
291      * <li>If the {@link Identity} can be updated, its {@link IdentityAttribute} list is updated following the given rule:
292      * <ul>
293      * <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
294      * existing one. If the value cannot be updated, the NOT_UPDATED status, associated with the attribute key, is returned in the execution response.</li>
295      * <li>If the {@link IdentityAttribute} does not exist, it is created. The CREATED status, associated with the attribute key, is returned in the execution
296      * response.</li>
297      * <li>CUID and GUID attributes cannot be modified.</li>
298      * </ul>
299      * </li>
300      * </ul>
301      *
302      * @param customerId
303      *            the id of the updated {@link Identity}
304      * @param request
305      *            the {@link IdentityChangeRequest} holding the parameters of the identity change request
306      * @param author
307      *            the author of the request
308      * @param clientCode
309      *            code of the {@link ClientApplication} requesting the change
310      * @param response
311      *            the {@link IdentityChangeResponse} holding the status of the execution of the request
312      * @return the updated {@link Identity}
313      * @throws IdentityStoreException
314      *             in case of error
315      */
316     public Identity update( final String customerId, final IdentityChangeRequest request, final RequestAuthor author, final String clientCode,
317             final IdentityChangeResponse response ) throws IdentityStoreException
318     {
319         if ( !_serviceContractService.canUpdateIdentity( clientCode ) )
320         {
321             response.setStatus( ResponseStatusFactory.failure( ).setMessage( "The client application is not authorized to update an identity." )
322                     .setMessageKey( Constants.PROPERTY_REST_ERROR_UPDATE_UNAUTHORIZED ) );
323             response.setCustomerId( customerId );
324             return null;
325         }
326 
327         final Identity identity = IdentityHome.findByCustomerId( customerId );
328 
329         // check if identity exists
330         if ( identity == null )
331         {
332             response.setStatus( ResponseStatusFactory.notFound( ).setMessage( "No matching identity could be found" )
333                     .setMessageKey( Constants.PROPERTY_REST_ERROR_NO_MATCHING_IDENTITY ) );
334             return null;
335         }
336 
337         // check if identity hasn't been updated between when the user retrieved the identity, and this request
338         if ( !Objects.equals( identity.getLastUpdateDate( ), request.getIdentity( ).getLastUpdateDate( ) ) )
339         {
340             response.setStatus(
341                     ResponseStatusFactory.conflict( ).setMessage( "This identity has been updated recently, please load the latest data before updating." )
342                             .setMessageKey( Constants.PROPERTY_REST_ERROR_UPDATE_CONFLICT ) );
343             response.setCustomerId( identity.getCustomerId( ) );
344             return identity;
345         }
346 
347         // check if identity is not merged
348         if ( identity.isMerged( ) )
349         {
350             final Identity masterIdentity = IdentityHome.findMasterIdentityByCustomerId( request.getIdentity( ).getCustomerId( ) );
351             response.setStatus(
352                     ResponseStatusFactory.conflict( ).setMessage( "Cannot update a merged Identity. Master identity customerId is provided in the response." )
353                             .setMessageKey( Constants.PROPERTY_REST_ERROR_FORBIDDEN_UPDATE_ON_MERGED_IDENTITY ) );
354             response.setCustomerId( masterIdentity.getCustomerId( ) );
355             return identity;
356         }
357 
358         // check if identity is active
359         if ( identity.isDeleted( ) )
360         {
361             response.setStatus( ResponseStatusFactory.conflict( ).setMessage( "Cannot update a deleted Identity." )
362                     .setMessageKey( Constants.PROPERTY_REST_ERROR_FORBIDDEN_UPDATE_ON_DELETED_IDENTITY ) );
363             response.setCustomerId( identity.getCustomerId( ) );
364             return identity;
365         }
366 
367         // check if the service contract allows the update of "mon_paris_active" flag
368         if ( request.getIdentity( ).getMonParisActive( ) != null && !_serviceContractService.canModifyConnectedIdentity( clientCode ) )
369         {
370             response.setStatus(
371                     ResponseStatusFactory.conflict( ).setMessage( "The client application is not authorized to update the 'mon_paris_active' flag." )
372                             .setMessageKey( Constants.PROPERTY_REST_ERROR_FORBIDDEN_MON_PARIS_ACTIVE_UPDATE ) );
373             response.setCustomerId( identity.getCustomerId( ) );
374             return null;
375         }
376 
377         // check if update would create duplicates
378         if ( doesRequestContainsAttributeValueChangesImpactingRules( request, identity, PROPERTY_DUPLICATES_UPDATE_RULES ) )
379         {
380             // collect all non blank attributes from request
381             final Map<String, String> attributes = request.getIdentity( ).getAttributes( ).stream( ).filter( a -> StringUtils.isNotBlank( a.getValue( ) ) )
382                     .collect( Collectors.toMap( AttributeDto::getKey, AttributeDto::getValue, ( a, b ) -> a ) );
383             // add other existing identity attributes
384             identity.getAttributes( ).forEach( ( key, attr ) -> attributes.putIfAbsent( key, attr.getValue( ) ) );
385             // remove attributes that have blank values in the request
386             request.getIdentity( ).getAttributes( ).stream( ).filter( a -> StringUtils.isBlank( a.getValue( ) ) )
387                     .forEach( a -> attributes.remove( a.getKey( ) ) );
388 
389             // search for potential duplicates with those attributes
390             final DuplicateSearchResponse duplicateSearchResponse = this.checkDuplicates( attributes, PROPERTY_DUPLICATES_UPDATE_RULES, customerId );
391             if ( duplicateSearchResponse != null && !duplicateSearchResponse.getIdentities( ).isEmpty( ) )
392             {
393                 response.setStatus( ResponseStatusFactory.conflict( ).setMessage( duplicateSearchResponse.getStatus( ).getMessage( ) )
394                         .setMessageKey( duplicateSearchResponse.getStatus( ).getMessageKey( ) ) );
395                 return null;
396             }
397         }
398 
399         // If GUID is updated, check if the new GUID does not exist in database
400         TransactionManager.beginTransaction( null );
401         try
402         {
403             if ( _serviceContractService.canModifyConnectedIdentity( clientCode )
404                     && !StringUtils.equalsIgnoreCase( identity.getConnectionId( ), request.getIdentity( ).getConnectionId( ) )
405                     && request.getIdentity( ).getConnectionId( ) != null )
406             {
407                 final Identity byConnectionId = IdentityHome.findByConnectionId( request.getIdentity( ).getConnectionId( ) );
408                 if ( byConnectionId != null )
409                 {
410                     response.setStatus( ResponseStatusFactory.conflict( )
411                             .setMessage(
412                                     "An identity already exists with the given connection ID. The customer ID of that identity is provided in the response." )
413                             .setMessageKey( Constants.PROPERTY_REST_ERROR_CONFLICT_CONNECTION_ID ) );
414                     response.setCustomerId( byConnectionId.getCustomerId( ) );
415                     TransactionManager.rollBack( null );
416                     return null;
417                 }
418                 else
419                 {
420                     identity.setConnectionId( request.getIdentity( ).getConnectionId( ) );
421                     IdentityHome.update( identity );
422                 }
423             }
424 
425             // => process update :
426 
427             final List<AttributeStatus> attrStatusList = this.updateIdentity( request.getIdentity( ), clientCode, response, identity );
428             if ( ResponseStatusFactory.unauthorized( ).equals( response.getStatus( ) ) )
429             {
430                 response.setCustomerId( identity.getCustomerId( ) );
431                 TransactionManager.rollBack( null );
432                 return null;
433             }
434 
435             response.setCustomerId( identity.getCustomerId( ) );
436             response.setConnectionId( identity.getConnectionId( ) );
437             response.setCreationDate( identity.getCreationDate( ) );
438             response.setLastUpdateDate( identity.getLastUpdateDate( ) );
439 
440             final boolean allAttributesCreatedOrUpdated = attrStatusList.stream( ).map( AttributeStatus::getStatus )
441                     .allMatch( status -> status.getType( ) == AttributeChangeStatusType.SUCCESS );
442             final ResponseStatus status = allAttributesCreatedOrUpdated ? ResponseStatusFactory.success( ) : ResponseStatusFactory.incompleteSuccess( );
443 
444             final String msgKey;
445             if ( Collections.disjoint( AttributeChangeStatus.getSuccessStatuses( ),
446                     attrStatusList.stream( ).map( AttributeStatus::getStatus ).collect( Collectors.toList( ) ) ) )
447             {
448                 // If there was no attribute change, send back a specific message key
449                 msgKey = Constants.PROPERTY_REST_INFO_NO_ATTRIBUTE_CHANGE;
450             }
451             else
452             {
453                 msgKey = Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION;
454             }
455             response.setStatus( status.setAttributeStatuses( attrStatusList ).setMessageKey( msgKey ) );
456             TransactionManager.commitTransaction( null );
457 
458             /* Historique des modifications */
459             for ( final AttributeStatus attributeStatus : attrStatusList )
460             {
461                 _identityStoreNotifyListenerService.notifyListenersAttributeChange( AttributeChangeType.UPDATE, identity, attributeStatus, author, clientCode );
462             }
463 
464             /* Indexation et historique */
465             _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.UPDATE, identity, response.getStatus( ).getType( ).name( ),
466                     response.getStatus( ).getMessage( ), author, clientCode, new HashMap<>( ) );
467             AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, UPDATE_IDENTITY_EVENT_CODE,
468                     _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( identity.getCustomerId( ) ), SPECIFIC_ORIGIN );
469         }
470         catch( Exception e )
471         {
472             response.setStatus(
473                     ResponseStatusFactory.failure( ).setMessage( e.getMessage( ) ).setMessageKey( Constants.PROPERTY_REST_ERROR_DURING_TREATMENT ) );
474             TransactionManager.rollBack( null );
475         }
476 
477         return identity;
478     }
479 
480     /**
481      * Returns <code>true</code> if the request aims to add new attributes, remove existing attributes, or modify existing attribute's value, of attributes
482      * checked by the duplicate rules in parameter.<br/>
483      * Returns <code>false</code> otherwise.
484      * 
485      * @param request
486      *            the request
487      * @param identity
488      *            the identity
489      */
490     private boolean doesRequestContainsAttributeValueChangesImpactingRules( final IdentityChangeRequest request, final Identity identity,
491             final String duplicateRulesProperty )
492     {
493         final Set<String> checkedAttributeKeys = Arrays.stream( AppPropertiesService.getProperty( duplicateRulesProperty ).split( "," ) )
494                 .map( DuplicateRuleHome::findByCode ).flatMap( rule -> rule.getCheckedAttributes( ).stream( ) ).map( AttributeKey::getKeyName )
495                 .collect( Collectors.toSet( ) );
496         return request.getIdentity( ).getAttributes( ).stream( ).filter( a -> checkedAttributeKeys.contains( a.getKey( ) ) ).anyMatch( a -> {
497             if ( StringUtils.isNotBlank( a.getValue( ) ) )
498             {
499                 return !identity.getAttributes( ).containsKey( a.getKey( ) )
500                         || !Objects.equals( identity.getAttributes( ).get( a.getKey( ) ).getValue( ), a.getValue( ) );
501             }
502             else
503             {
504                 return identity.getAttributes( ).containsKey( a.getKey( ) );
505             }
506         } );
507     }
508 
509     /**
510      * Merges two existing {@link Identity} specified in the given {@link IdentityMergeRequest} and following the given rules: <br>
511      * <ul>
512      * <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>
513      * <li>The {@link IdentityAttribute}(s) of the secondary {@link Identity} are processed, according to the list of keys specified in the
514      * {@link IdentityMergeRequest}.
515      * <ul>
516      * <li>If the {@link IdentityAttribute} is not present in the primary {@link Identity}, it is created.</li>
517      * <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
518      * different.</li>
519      * </ul>
520      * </li>
521      * </ul>
522      *
523      * @param request
524      *            the {@link IdentityMergeRequest} holding the parameters of the request
525      * @param author
526      *            the author of the request
527      * @param clientCode
528      *            code of the {@link ClientApplication} requesting the change
529      * @param response
530      *            the {@link IdentityMergeResponse} holding the status of the execution of the request
531      * @return the merged {@link Identity}
532      * @throws IdentityStoreException
533      *             in case of error
534      */
535     // TODO: récupérer la plus haute date d'expiration des deux identités
536     public Identity merge( final IdentityMergeRequest request, final RequestAuthor author, final String clientCode, final IdentityMergeResponse response )
537     {
538         final Identity primaryIdentity = IdentityHome.findByCustomerId( request.getPrimaryCuid( ) );
539         if ( primaryIdentity == null )
540         {
541             response.setStatus( ResponseStatusFactory.failure( ).setMessage( "Could not find primary identity with customer_id " + request.getPrimaryCuid( ) )
542                     .setMessageKey( Constants.PROPERTY_REST_ERROR_PRIMARY_IDENTITY_NOT_FOUND ) );
543             return null;
544         }
545 
546         if ( primaryIdentity.isDeleted( ) )
547         {
548             response.setStatus(
549                     ResponseStatusFactory.failure( ).setMessage( "Primary identity found with customer_id " + request.getPrimaryCuid( ) + " is deleted" )
550                             .setMessageKey( Constants.PROPERTY_REST_ERROR_PRIMARY_IDENTITY_DELETED ) );
551             return null;
552         }
553 
554         if ( primaryIdentity.isMerged( ) )
555         {
556             response.setStatus(
557                     ResponseStatusFactory.failure( ).setMessage( "Primary identity found with customer_id " + request.getPrimaryCuid( ) + " is merged" )
558                             .setMessageKey( Constants.PROPERTY_REST_ERROR_PRIMARY_IDENTITY_MERGED ) );
559             return null;
560         }
561 
562         if ( !Objects.equals( primaryIdentity.getLastUpdateDate( ), request.getPrimaryLastUpdateDate( ) ) )
563         {
564             response.setStatus(
565                     ResponseStatusFactory.failure( ).setMessage( "The primary identity has been updated recently, please load the latest data before merging." )
566                             .setMessageKey( Constants.PROPERTY_REST_ERROR_PRIMARY_IDENTITY_UPDATE_CONFLICT ) );
567             return null;
568         }
569 
570         final Identity secondaryIdentity = IdentityHome.findByCustomerId( request.getSecondaryCuid( ) );
571         if ( secondaryIdentity == null )
572         {
573             response.setStatus(
574                     ResponseStatusFactory.failure( ).setMessage( "Could not find secondary identity with customer_id " + request.getSecondaryCuid( ) )
575                             .setMessageKey( Constants.PROPERTY_REST_ERROR_SECONDARY_IDENTITY_NOT_FOUND ) );
576             return null;
577         }
578 
579         if ( secondaryIdentity.isDeleted( ) )
580         {
581             response.setStatus(
582                     ResponseStatusFactory.failure( ).setMessage( "Secondary identity found with customer_id " + request.getSecondaryCuid( ) + " is deleted" )
583                             .setMessageKey( Constants.PROPERTY_REST_ERROR_SECONDARY_IDENTITY_DELETED ) );
584             return null;
585         }
586 
587         if ( secondaryIdentity.isMerged( ) )
588         {
589             response.setStatus(
590                     ResponseStatusFactory.failure( ).setMessage( "Secondary identity found with customer_id " + request.getSecondaryCuid( ) + " is merged" )
591                             .setMessageKey( Constants.PROPERTY_REST_ERROR_SECONDARY_IDENTITY_MERGED ) );
592             return null;
593         }
594 
595         if ( !Objects.equals( secondaryIdentity.getLastUpdateDate( ), request.getSecondaryLastUpdateDate( ) ) )
596         {
597             response.setStatus( ResponseStatusFactory.failure( )
598                     .setMessage( "The secondary identity has been updated recently, please load the latest data before merging." )
599                     .setMessageKey( Constants.PROPERTY_REST_ERROR_SECONDARY_IDENTITY_UPDATE_CONFLICT ) );
600             return null;
601         }
602 
603         TransactionManager.beginTransaction( null );
604         try
605         {
606             final List<AttributeStatus> attrStatusList = new ArrayList<>( );
607             if ( request.getIdentity( ) != null )
608             {
609                 attrStatusList.addAll( this.updateIdentity( request.getIdentity( ), clientCode, response, primaryIdentity ) );
610                 if ( ResponseStatusFactory.unauthorized( ).equals( response.getStatus( ) ) )
611                 {
612                     response.setCustomerId( primaryIdentity.getCustomerId( ) );
613                     TransactionManager.rollBack( null );
614                     return null;
615                 }
616             }
617 
618             /* Tag de l'identité secondaire */
619             secondaryIdentity.setMerged( true );
620             secondaryIdentity.setMasterIdentityId( primaryIdentity.getId( ) );
621             IdentityHome.merge( secondaryIdentity );
622             IdentityAttributeHome.removeAllAttributes( secondaryIdentity.getId( ) );
623 
624             response.setCustomerId( primaryIdentity.getCustomerId( ) );
625             response.setConnectionId( primaryIdentity.getConnectionId( ) );
626             response.setLastUpdateDate( primaryIdentity.getLastUpdateDate( ) );
627 
628             final boolean allAttributesCreatedOrUpdated = attrStatusList.stream( ).map( AttributeStatus::getStatus )
629                     .allMatch( status -> status.getType( ) == AttributeChangeStatusType.SUCCESS );
630             final ResponseStatus status = allAttributesCreatedOrUpdated ? ResponseStatusFactory.success( ) : ResponseStatusFactory.incompleteSuccess( );
631 
632             final String msgKey;
633             if ( Collections.disjoint( AttributeChangeStatus.getSuccessStatuses( ),
634                     attrStatusList.stream( ).map( AttributeStatus::getStatus ).collect( Collectors.toList( ) ) ) )
635             {
636                 // If there was no attribute change, send back a specific message key
637                 msgKey = Constants.PROPERTY_REST_INFO_NO_ATTRIBUTE_CHANGE;
638             }
639             else
640             {
641                 msgKey = Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION;
642             }
643 
644             response.setStatus( status.setAttributeStatuses( attrStatusList ).setMessageKey( msgKey ) );
645             TransactionManager.commitTransaction( null );
646 
647             /* Historique des modifications */
648             for ( AttributeStatus attributeStatus : attrStatusList )
649             {
650                 _identityStoreNotifyListenerService.notifyListenersAttributeChange( AttributeChangeType.MERGE, primaryIdentity, attributeStatus, author,
651                         clientCode );
652             }
653 
654             /* Indexation */
655             final Map<String, String> primaryMetadata = new HashMap<>( );
656             primaryMetadata.put( Constants.METADATA_MERGED_MASTER_IDENTITY_CUID, primaryIdentity.getCustomerId( ) );
657             primaryMetadata.put( Constants.METADATA_DUPLICATE_RULE_CODE, request.getDuplicateRuleCode( ) );
658             _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.MERGED, secondaryIdentity,
659                     response.getStatus( ).getType( ).name( ), response.getStatus( ).getType( ).name( ), author, clientCode, primaryMetadata );
660 
661             AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, CONSOLIDATE_IDENTITY_EVENT_CODE,
662                     _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( primaryIdentity.getCustomerId( ) ),
663                     SPECIFIC_ORIGIN );
664 
665             final Map<String, String> secondaryMetadata = new HashMap<>( );
666             secondaryMetadata.put( Constants.METADATA_MERGED_CHILD_IDENTITY_CUID, secondaryIdentity.getCustomerId( ) );
667             secondaryMetadata.put( Constants.METADATA_DUPLICATE_RULE_CODE, request.getDuplicateRuleCode( ) );
668             _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.CONSOLIDATED, primaryIdentity,
669                     response.getStatus( ).getType( ).name( ), response.getStatus( ).getType( ).name( ), author, clientCode, secondaryMetadata );
670 
671             AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, MERGE_IDENTITY_EVENT_CODE,
672                     _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( secondaryIdentity.getCustomerId( ) ),
673                     SPECIFIC_ORIGIN );
674         }
675         catch( Exception e )
676         {
677             response.setStatus(
678                     ResponseStatusFactory.failure( ).setMessage( e.getMessage( ) ).setMessageKey( Constants.PROPERTY_REST_ERROR_DURING_TREATMENT ) );
679             TransactionManager.rollBack( null );
680         }
681 
682         return primaryIdentity;
683     }
684 
685     /**
686      * Detach a merged {@link Identity} from its master {@link Identity}
687      * 
688      * @param request
689      *            the unmerge request
690      * @param author
691      *            the author of the request
692      * @param clientCode
693      *            the client code of the calling application
694      * @param response
695      *            the status of the execution
696      */
697     public void cancelMerge( final IdentityMergeRequest request, final RequestAuthor author, final String clientCode, final IdentityMergeResponse response )
698     {
699         final Identity primaryIdentity = IdentityHome.findByCustomerId( request.getPrimaryCuid( ) );
700         if ( primaryIdentity == null )
701         {
702             response.setStatus( ResponseStatusFactory.failure( ).setMessage( "Could not find primary identity with customer_id " + request.getPrimaryCuid( ) )
703                     .setMessageKey( Constants.PROPERTY_REST_ERROR_PRIMARY_IDENTITY_NOT_FOUND ) );
704             return;
705         }
706 
707         if ( !Objects.equals( primaryIdentity.getLastUpdateDate( ), request.getPrimaryLastUpdateDate( ) ) )
708         {
709             response.setStatus( ResponseStatusFactory.failure( )
710                     .setMessage( "The primary identity has been updated recently, please load the latest data before canceling merge." )
711                     .setMessageKey( Constants.PROPERTY_REST_ERROR_PRIMARY_IDENTITY_UPDATE_CONFLICT ) );
712             return;
713         }
714 
715         final Identity secondaryIdentity = IdentityHome.findByCustomerId( request.getSecondaryCuid( ) );
716         if ( secondaryIdentity == null )
717         {
718             response.setStatus(
719                     ResponseStatusFactory.failure( ).setMessage( "Could not find secondary identity with customer_id " + request.getSecondaryCuid( ) )
720                             .setMessageKey( Constants.PROPERTY_REST_ERROR_SECONDARY_IDENTITY_NOT_FOUND ) );
721             return;
722         }
723 
724         if ( !secondaryIdentity.isMerged( ) )
725         {
726             response.setStatus(
727                     ResponseStatusFactory.failure( ).setMessage( "Secondary identity found with customer_id " + request.getSecondaryCuid( ) + " is not merged" )
728                             .setMessageKey( Constants.PROPERTY_REST_ERROR_SECONDARY_IDENTITY_NOT_MERGED ) );
729             return;
730         }
731 
732         if ( !Objects.equals( secondaryIdentity.getMasterIdentityId( ), primaryIdentity.getId( ) ) )
733         {
734             response.setStatus( ResponseStatusFactory.failure( )
735                     .setMessage( "Secondary identity found with customer_id " + request.getSecondaryCuid( )
736                             + " is not merged to Primary identity found with customer ID " + request.getPrimaryCuid( ) )
737                     .setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITIES_NOT_MERGED_TOGETHER ) );
738             return;
739         }
740 
741         // 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
742         // if ( !Objects.equals( secondaryIdentity.getLastUpdateDate( ), request.getSecondaryLastUpdateDate( ) ) )
743         // {
744         // response.setStatus( ResponseStatusFactory.failure( )
745         // .setMessage( "The secondary identity has been updated recently, please load the latest data before canceling merge." )
746         // .setMessageKey( Constants.PROPERTY_REST_ERROR_SECONDARY_IDENTITY_UPDATE_CONFLICT ) );
747         // return;
748         // }
749 
750         TransactionManager.beginTransaction( null );
751         try
752         {
753             /* Tag de l'identité secondaire */
754             IdentityHome.cancelMerge( secondaryIdentity );
755             response.setStatus( ResponseStatusFactory.success( ).setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
756             TransactionManager.commitTransaction( null );
757 
758             /* Indexation */
759             final Map<String, String> secondaryMetadata = new HashMap<>( );
760             secondaryMetadata.put( Constants.METADATA_UNMERGED_MASTER_CUID, primaryIdentity.getCustomerId( ) );
761             _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.MERGE_CANCELLED, secondaryIdentity,
762                     response.getStatus( ).getType( ).name( ), response.getStatus( ).getType( ).name( ), author, clientCode, secondaryMetadata );
763             AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, CANCEL_MERGE_IDENTITY_EVENT_CODE,
764                     _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( secondaryIdentity.getCustomerId( ) ),
765                     SPECIFIC_ORIGIN );
766 
767             final Map<String, String> primaryMetadata = new HashMap<>( );
768             primaryMetadata.put( Constants.METADATA_UNMERGED_CHILD_CUID, secondaryIdentity.getCustomerId( ) );
769             _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.CONSOLIDATION_CANCELLED, primaryIdentity,
770                     response.getStatus( ).getType( ).name( ), response.getStatus( ).getType( ).name( ), author, clientCode, primaryMetadata );
771             AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, CANCEL_CONSOLIDATE_IDENTITY_EVENT_CODE,
772                     _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( primaryIdentity.getCustomerId( ) ),
773                     SPECIFIC_ORIGIN );
774         }
775         catch( Exception e )
776         {
777             response.setStatus(
778                     ResponseStatusFactory.failure( ).setMessage( e.getMessage( ) ).setMessageKey( Constants.PROPERTY_REST_ERROR_DURING_TREATMENT ) );
779             TransactionManager.rollBack( null );
780         }
781     }
782 
783     /**
784      * Imports an {@link Identity} according to the given {@link IdentityChangeRequest}
785      *
786      * @param identityChangeRequest
787      *            the {@link IdentityChangeRequest} holding the parameters of the identity change request
788      * @param author
789      *            the author of the request
790      * @param clientCode
791      *            code of the {@link ClientApplication} requesting the change
792      * @param response
793      *            the {@link IdentityChangeResponse} holding the status of the execution of the request
794      * @return the imported {@link Identity}
795      * @throws IdentityStoreException
796      *             in case of error
797      */
798     public Identity importIdentity( final IdentityChangeRequest identityChangeRequest, final RequestAuthor author, final String clientCode,
799             final IdentityChangeResponse response ) throws IdentityStoreException
800     {
801         final Map<String, String> attributes = identityChangeRequest.getIdentity( ).getAttributes( ).stream( )
802                 .collect( Collectors.toMap( AttributeDto::getKey, AttributeDto::getValue ) );
803 
804         final DuplicateSearchResponse certitudeDuplicates = this.checkDuplicates( attributes, PROPERTY_DUPLICATES_IMPORT_RULES_STRICT, "" );
805         if ( certitudeDuplicates != null && CollectionUtils.isNotEmpty( certitudeDuplicates.getIdentities( ) ) )
806         {
807             if ( certitudeDuplicates.getIdentities( ).size( ) == 1 )
808             {
809                 final IdentityDto strictDuplicate = certitudeDuplicates.getIdentities( ).get( 0 );
810                 identityChangeRequest.getIdentity( ).setLastUpdateDate( strictDuplicate.getLastUpdateDate( ) );
811                 return this.update( strictDuplicate.getCustomerId( ), identityChangeRequest, author, clientCode, response );
812             }
813             else
814             {
815                 response.setStatus( ResponseStatusFactory.conflict( ).setMessage( certitudeDuplicates.getStatus( ).getMessage( ) )
816                         .setMessageKey( certitudeDuplicates.getStatus( ).getMessageKey( ) ) );
817             }
818         }
819         else
820         {
821             final DuplicateSearchResponse suspicionDuplicates = this.checkDuplicates( attributes, PROPERTY_DUPLICATES_IMPORT_RULES_SUSPICION, "" );
822             if ( suspicionDuplicates != null && CollectionUtils.isNotEmpty( suspicionDuplicates.getIdentities( ) ) )
823             {
824                 response.setStatus( ResponseStatusFactory.conflict( ).setMessage( suspicionDuplicates.getStatus( ).getMessage( ) )
825                         .setMessageKey( suspicionDuplicates.getStatus( ).getMessageKey( ) ) );
826             }
827             else
828             {
829                 return this.create( identityChangeRequest, author, clientCode, response );
830             }
831         }
832 
833         return null;
834     }
835 
836     public IdentityDto getQualifiedIdentity( final String customerId ) throws IdentityStoreException
837     {
838         final Identity identity = IdentityHome.findByCustomerId( customerId );
839         final IdentityDto qualifiedIdentity = DtoConverter.convertIdentityToDto( identity );
840         IdentityQualityService.instance( ).computeQuality( qualifiedIdentity );
841         return qualifiedIdentity;
842     }
843 
844     /**
845      * Perform an identity research over a list of attributes (key and values) specified in the {@link IdentitySearchRequest}
846      *
847      * @param request
848      *            the {@link IdentitySearchRequest} holding the parameters of the research
849      * @param author
850      *            the author of the request
851      * @param response
852      *            the {@link IdentitySearchResponse} holding the status of the execution status and the results of the request
853      * @param clientCode
854      *            code of the {@link ClientApplication} requesting the change
855      * @throws ServiceContractNotFoundException
856      *             in case of {@link ServiceContract} management error
857      * @throws IdentityAttributeNotFoundException
858      *             in case of {@link AttributeKey} management error
859      */
860     public void search( final IdentitySearchRequest request, final RequestAuthor author, final IdentitySearchResponse response, final String clientCode )
861             throws IdentityStoreException
862     {
863         AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, SEARCH_IDENTITY_EVENT_CODE,
864                 _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( request.toString( ) ), SPECIFIC_ORIGIN );
865         final List<SearchAttribute> providedAttributes = request.getSearch( ).getAttributes( );
866         final Set<String> providedKeys = commonKeytoKey( providedAttributes.stream( ).map( SearchAttribute::getKey ).collect( Collectors.toSet( ) ) );
867 
868         boolean hasRequirements = false;
869         final List<IdentitySearchRule> searchRules = IdentitySearchRuleHome.findAll( );
870         final Iterator<IdentitySearchRule> iterator = searchRules.iterator( );
871         while ( !hasRequirements && iterator.hasNext( ) )
872         {
873             final IdentitySearchRule searchRule = iterator.next( );
874             final Set<String> requiredKeys = searchRule.getAttributes( ).stream( ).map( AttributeKey::getKeyName ).collect( Collectors.toSet( ) );
875             if ( searchRule.getType( ) == SearchRuleType.AND )
876             {
877                 if ( providedKeys.containsAll( requiredKeys ) )
878                 {
879                     hasRequirements = true;
880                 }
881             }
882             else
883                 if ( searchRule.getType( ) == SearchRuleType.OR )
884                 {
885                     if ( providedKeys.stream( ).anyMatch( requiredKeys::contains ) )
886                     {
887                         hasRequirements = true;
888                     }
889                 }
890         }
891 
892         if ( !hasRequirements )
893         {
894             final StringBuilder sb = new StringBuilder( );
895             final Iterator<IdentitySearchRule> ruleIt = searchRules.iterator( );
896             while ( ruleIt.hasNext( ) )
897             {
898                 final IdentitySearchRule rule = ruleIt.next( );
899                 sb.append( "( " );
900                 final Iterator<AttributeKey> attrIt = rule.getAttributes( ).iterator( );
901                 while ( attrIt.hasNext( ) )
902                 {
903                     final AttributeKey attr = attrIt.next( );
904                     sb.append( attr.getKeyName( ) ).append( " " );
905                     if ( attrIt.hasNext( ) )
906                     {
907                         sb.append( rule.getType( ).name( ) ).append( " " );
908                     }
909                 }
910                 sb.append( ")" );
911                 if ( ruleIt.hasNext( ) )
912                 {
913                     sb.append( " OR " );
914                 }
915             }
916             final IdentitySearchMessage alert = new IdentitySearchMessage( );
917             alert.setAttributeName( sb.toString( ) );
918             alert.setMessage( "Please provide those required attributes to be able to search identities." );
919             response.getAlerts( ).add( alert );
920             response.setStatus( ResponseStatusFactory.failure( ).setMessageKey( Constants.PROPERTY_REST_ERROR_MISSING_MANDATORY_ATTRIBUTES ) );
921             return;
922         }
923 
924         final QualifiedIdentitySearchResult result = _elasticSearchIdentityService.getQualifiedIdentities( providedAttributes, request.getMax( ),
925                 request.isConnected( ), Collections.emptyList( ) );
926         if ( CollectionUtils.isNotEmpty( result.getQualifiedIdentities( ) ) )
927         {
928             final List<IdentityDto> filteredIdentities = this.getEnrichedIdentities( request.getSearch( ).getAttributes( ), clientCode,
929                     result.getQualifiedIdentities( ) );
930             response.setIdentities( filteredIdentities );
931             if ( CollectionUtils.isNotEmpty( response.getIdentities( ) ) )
932             {
933                 response.setStatus( ResponseStatusFactory.ok( ).setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
934                 for ( final IdentityDto identity : response.getIdentities( ) )
935                 {
936                     AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, SEARCH_IDENTITY_EVENT_CODE,
937                             _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( identity.getCustomerId( ) ),
938                             SPECIFIC_ORIGIN );
939                     if ( author.getType( ).equals( AuthorType.agent ) )
940                     {
941                         /* Indexation et historique */
942                         _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.READ,
943                                 DtoConverter.convertDtoToIdentity( identity ), response.getStatus( ).getType( ).name( ), response.getStatus( ).getMessage( ),
944                                 author, clientCode, new HashMap<>( ) );
945                     }
946                 }
947             }
948             else
949             {
950                 response.setStatus( ResponseStatusFactory.noResult( ).setMessageKey( Constants.PROPERTY_REST_ERROR_NO_IDENTITY_FOUND ) );
951             }
952         }
953         else
954         {
955             response.setStatus( ResponseStatusFactory.noResult( ).setMessageKey( Constants.PROPERTY_REST_ERROR_NO_IDENTITY_FOUND ) );
956         }
957     }
958 
959     /***
960      * Check if the attributes are commonKeys, in that case it change them to the attributeKeys it refer
961      *
962      * @param providedAttributes
963      * @return the list of keys
964      */
965     private Set<String> commonKeytoKey( Set<String> providedAttributes )
966     {
967         Set<String> returnKeys = new HashSet<>( );
968 
969         for ( String attribute : providedAttributes )
970         {
971             List<AttributeKey> keys = IdentityAttributeService.instance( ).getCommonAttributeKeys( attribute );
972             if ( keys != null && !keys.isEmpty( ) )
973             {
974                 for ( AttributeKey key : keys )
975                 {
976                     returnKeys.add( key.getKeyName( ) );
977                 }
978             }
979             else
980             {
981                 returnKeys.add( attribute );
982             }
983         }
984         return returnKeys;
985     }
986 
987     /**
988      * Perform an identity research by customer or connection ID.
989      *
990      * @param customerId
991      * @param connectionId
992      * @param response
993      * @param clientCode
994      * @param author
995      *            the author of the request
996      * @throws IdentityAttributeNotFoundException
997      * @throws ServiceContractNotFoundException
998      */
999     public void search( final String customerId, final String connectionId, final IdentitySearchResponse response, final String clientCode,
1000             final RequestAuthor author ) throws IdentityStoreException
1001     {
1002         AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, GET_IDENTITY_EVENT_CODE, _internalUserService.getApiUser( clientCode ),
1003                 SecurityUtil.logForgingProtect( StringUtils.isNotBlank( customerId ) ? customerId : connectionId ), SPECIFIC_ORIGIN );
1004 
1005         final ServiceContract serviceContract = _serviceContractService.getActiveServiceContract( clientCode );
1006         if ( serviceContract == null )
1007         {
1008             throw new ServiceContractNotFoundException( "No active service contract could be found for clientCode = " + clientCode );
1009         }
1010         final IdentityDto identityDto = StringUtils.isNotBlank( customerId ) ? _identityDtoCache.getByCustomerId( customerId, serviceContract )
1011                 : _identityDtoCache.getByConnectionId( connectionId, serviceContract );
1012         if ( identityDto == null )
1013         {
1014             // #345 : If the identity doesn't exist, make an extra search in the history (only for CUID search).
1015             // If there is a record, it means the identity has been deleted => send back a specific message
1016             if ( StringUtils.isNotBlank( customerId ) && !IdentityHome.findHistoryByCustomerId( customerId ).isEmpty( ) )
1017             {
1018                 response.setStatus( ResponseStatusFactory.notFound( ).setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITY_DELETED ) );
1019             }
1020             else
1021             {
1022                 response.setStatus( ResponseStatusFactory.notFound( ).setMessageKey( Constants.PROPERTY_REST_ERROR_NO_IDENTITY_FOUND ) );
1023             }
1024         }
1025         else
1026         {
1027             response.setIdentities( Collections.singletonList( identityDto ) );
1028             response.setStatus( ResponseStatusFactory.ok( ).setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
1029             // #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
1030             if ((StringUtils.isNotBlank(customerId) && !identityDto.getCustomerId().equals(customerId)) ||
1031                 (StringUtils.isNotBlank(connectionId) && !identityDto.getConnectionId().equals(connectionId))) {
1032                 final IdentitySearchMessage alert = new IdentitySearchMessage();
1033                 alert.setMessage("Le CUID ou GUID demandé correspond à une identité rapprochée. Cette réponse contient l'identité consilidée.");
1034                 response.getAlerts().add(alert);
1035             }
1036             if ( author != null )
1037             {
1038                 AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, SEARCH_IDENTITY_EVENT_CODE,
1039                         _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( identityDto.getCustomerId( ) ),
1040                         SPECIFIC_ORIGIN );
1041             }
1042             if ( author != null && author.getType( ).equals( AuthorType.agent ) )
1043             {
1044                 /* Indexation et historique */
1045                 _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.READ, DtoConverter.convertDtoToIdentity( identityDto ),
1046                         response.getStatus( ).getType( ).name( ), response.getStatus( ).getMessage( ), author, clientCode, new HashMap<>( ) );
1047             }
1048         }
1049     }
1050 
1051     /**
1052      * Performs a search of a list of {@link IdentityDto}, providing a list of customer ids
1053      * 
1054      * @param customerIds
1055      *            the customer ids to search for
1056      * @return a list of {@link IdentityDto}
1057      */
1058     public List<IdentityDto> search( final List<String> customerIds, final List<String> attributes )
1059     {
1060         try
1061         {
1062             final QualifiedIdentitySearchResult result = _elasticSearchIdentityService.getQualifiedIdentities( customerIds, attributes );
1063             if ( result != null && CollectionUtils.isNotEmpty( result.getQualifiedIdentities( ) ) )
1064             {
1065                 result.getQualifiedIdentities( ).forEach( identityDto -> {
1066                     IdentityQualityService.instance( ).computeQuality( identityDto );
1067                     identityDto.getQuality( ).setScoring( 1D );
1068                     identityDto.getQuality( ).setCoverage( 1 );
1069                 } );
1070                 return result.getQualifiedIdentities( );
1071             }
1072         }
1073         catch( final IdentityStoreException e )
1074         {
1075             // ignore this identity
1076         }
1077         return new ArrayList<>( );
1078     }
1079 
1080     /**
1081      * Performs a search of an {@link IdentityDto}, providing its customer id
1082      * 
1083      * @param customerId
1084      *            the customer id to search for
1085      * @return an {@link IdentityDto}
1086      */
1087     public IdentityDto search( final String customerId )
1088     {
1089         try
1090         {
1091             final QualifiedIdentitySearchResult result = _elasticSearchIdentityService.getQualifiedIdentities( customerId, Collections.emptyList( ) );
1092             if ( result != null && CollectionUtils.isNotEmpty( result.getQualifiedIdentities( ) ) )
1093             {
1094                 final IdentityDto identityDto = result.getQualifiedIdentities( ).get( 0 );
1095                 IdentityQualityService.instance( ).computeQuality( identityDto );
1096                 identityDto.getQuality( ).setScoring( 1D );
1097                 identityDto.getQuality( ).setCoverage( 1 );
1098                 return identityDto;
1099             }
1100         }
1101         catch( final IdentityStoreException e )
1102         {
1103             // ignore this identity
1104         }
1105         return null;
1106     }
1107 
1108     /**
1109      * Filter a list of search results over {@link ServiceContract} defined for the given clientCode. Also complete identities with additional information
1110      * (quality, duplicates, ...).
1111      * 
1112      * @param searchAttributes
1113      *            la requête de recherche si existante
1114      * @param clientCode
1115      *            le code client du demandeur
1116      * @param identities
1117      *            la liste de résultats à traiter
1118      * @return the list of filtered and completed {@link IdentityDto}
1119      * @throws ServiceContractNotFoundException
1120      *             in case of error
1121      */
1122     private List<IdentityDto> getEnrichedIdentities( final List<SearchAttribute> searchAttributes, final String clientCode, final List<IdentityDto> identities )
1123             throws ServiceContractNotFoundException
1124     {
1125         final ServiceContract serviceContract = _serviceContractService.getActiveServiceContract( clientCode );
1126         final Comparator<QualityDefinition> qualityComparator = Comparator.comparing( QualityDefinition::getScoring )
1127                 .thenComparingDouble( QualityDefinition::getQuality ).reversed( );
1128         final Comparator<IdentityDto> identityComparator = Comparator.comparing( IdentityDto::getQuality, qualityComparator );
1129         return identities.stream( ).filter( IdentityDto::isNotMerged )
1130                 .peek( identity -> IdentityQualityService.instance( ).enrich( searchAttributes, identity, serviceContract, null ) ).sorted( identityComparator )
1131                 .collect( Collectors.toList( ) );
1132     }
1133 
1134     private List<AttributeStatus> updateIdentity( final IdentityDto requestIdentity, final String clientCode, final ChangeResponse response,
1135             final Identity identity ) throws IdentityStoreException
1136     {
1137         final List<AttributeStatus> attrStatusList = new ArrayList<>( );
1138 
1139         /* Récupération des attributs déja existants ou non */
1140         final Map<Boolean, List<AttributeDto>> sortedAttributes = requestIdentity.getAttributes( ).stream( )
1141                 .collect( Collectors.partitioningBy( a -> identity.getAttributes( ).containsKey( a.getKey( ) ) ) );
1142         final List<AttributeDto> existingWritableAttributes = CollectionUtils.isNotEmpty( sortedAttributes.get( true ) ) ? sortedAttributes.get( true )
1143                 : new ArrayList<>( );
1144         final List<AttributeDto> newWritableAttributes = CollectionUtils.isNotEmpty( sortedAttributes.get( false ) ) ? sortedAttributes.get( false )
1145                 : new ArrayList<>( );
1146 
1147         // If identity is connected and service contract doesn't allow unrestricted update, do a bunch of checks
1148         if ( identity.isConnected( ) && !_serviceContractService.canModifyConnectedIdentity( clientCode ) )
1149         {
1150             this.connectedIdentityUpdateCheck( requestIdentity, identity, existingWritableAttributes, newWritableAttributes, response );
1151             if ( ResponseStatusFactory.unauthorized( ).equals( response.getStatus( ) ) )
1152             {
1153                 return attrStatusList;
1154             }
1155         }
1156 
1157         /* Create or Update birth country and city */
1158         attrStatusList.addAll( GeocodesService.processCountryAndCityForUpdate( identity, newWritableAttributes, existingWritableAttributes, clientCode ) );
1159 
1160         /* Create new attributes */
1161         for ( final AttributeDto attributeToWrite : newWritableAttributes )
1162         {
1163             final AttributeStatus attributeStatus = _identityAttributeService.createAttribute( attributeToWrite, identity, clientCode );
1164             attrStatusList.add( attributeStatus );
1165         }
1166 
1167         /* Update existing attributes */
1168         for ( final AttributeDto attributeToUpdate : existingWritableAttributes )
1169         {
1170             final AttributeStatus attributeStatus = _identityAttributeService.updateAttribute( attributeToUpdate, identity, clientCode );
1171             attrStatusList.add( attributeStatus );
1172         }
1173 
1174         boolean monParisUpdated = false;
1175         if ( requestIdentity.getMonParisActive( ) != null && requestIdentity.getMonParisActive( ) != identity.isMonParisActive( ) )
1176         {
1177             monParisUpdated = true;
1178             identity.setMonParisActive( requestIdentity.isMonParisActive( ) );
1179         }
1180 
1181         if ( monParisUpdated || !Collections.disjoint( AttributeChangeStatus.getSuccessStatuses( ),
1182                 attrStatusList.stream( ).map( AttributeStatus::getStatus ).collect( Collectors.toList( ) ) ) )
1183         {
1184             // If there was an update on the monParis flag or in the attributes, we update the identity
1185             IdentityHome.update( identity );
1186         }
1187 
1188         return attrStatusList;
1189     }
1190 
1191     /**
1192      * Makes a bunch of checks regarding the validity of this update request on this connected identity.
1193      * <ul>
1194      * <li>Authorise update on "PIVOT" attributes only</li>
1195      * <li>For new attributes, certification level must be > 100 (better than self-declare)</li>
1196      * <li>For existing attributes, certification level must be >= than the existing level</li>
1197      * <li>If one "PIVOT" attribute is certified at a certain level N (conf) :
1198      * <ul>
1199      * <li>All "PIVOT" attributes must be set</li>
1200      * <li>All "PIVOT" attributes must be certified with level greater or equal to N</li>
1201      * </ul>
1202      * </li>
1203      * </ul>
1204      * 
1205      * @param requestIdentity
1206      *            the request
1207      * @param identity
1208      *            the identity
1209      * @param existingWritableAttributes
1210      *            existing attributes in identity from request
1211      * @param newWritableAttributes
1212      *            new attributes from request
1213      */
1214     private void connectedIdentityUpdateCheck( final IdentityDto requestIdentity, final Identity identity, final List<AttributeDto> existingWritableAttributes,
1215             final List<AttributeDto> newWritableAttributes, final ChangeResponse response )
1216     {
1217         // TODO refactor to use cache ?
1218         final Map<String, AttributeKey> allAttributesByKey = AttributeKeyHome.getAttributeKeysList( false ).stream( )
1219                 .collect( Collectors.toMap( AttributeKey::getKeyName, a -> a ) );
1220 
1221         // - Authorise update on "PIVOT" attributes only
1222         final boolean requestOnNonPivot = requestIdentity.getAttributes( ).stream( ).map( a -> allAttributesByKey.get( a.getKey( ) ) )
1223                 .anyMatch( a -> !a.getPivot( ) );
1224         if ( requestOnNonPivot )
1225         {
1226             response.setStatus( ResponseStatusFactory.unauthorized( ).setMessage( "Identity is connected, updating non 'pivot' attributes is forbidden." )
1227                     .setMessageKey( Constants.PROPERTY_REST_ERROR_CONNECTED_IDENTITY_FORBIDDEN_UPDATE_NON_PIVOT ) );
1228             return;
1229         }
1230 
1231         // - For new attributes, certification level must be > 100 (better than self-declare)
1232         final boolean newAttrSelfDeclare = newWritableAttributes.stream( )
1233                 .map( a -> RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( a.getCertifier( ), a.getKey( ) ) )
1234                 .anyMatch( c -> Integer.parseInt( c.getRefCertificationLevel( ).getLevel( ) ) <= 100 );
1235         if ( newAttrSelfDeclare )
1236         {
1237             response.setStatus( ResponseStatusFactory.unauthorized( )
1238                     .setMessage( "Identity is connected, adding 'pivot' attributes with self-declarative certification level is forbidden." )
1239                     .setMessageKey( Constants.PROPERTY_REST_ERROR_CONNECTED_IDENTITY_FORBIDDEN_PIVOT_SELF_DECLARE ) );
1240             return;
1241         }
1242 
1243         // - For existing attributes, certification level must be >= than the existing level
1244         final boolean lesserWantedLvl = existingWritableAttributes.stream( )
1245                 .map( a -> RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( a.getCertifier( ), a.getKey( ) ) ).anyMatch( wantedCertif -> {
1246                     final int wantedLvl = Integer.parseInt( wantedCertif.getRefCertificationLevel( ).getLevel( ) );
1247 
1248                     final IdentityAttribute existingAttr = identity.getAttributes( ).get( wantedCertif.getAttributeKey( ).getKeyName( ) );
1249                     final RefAttributeCertificationLevel existingCertif = RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName(
1250                             existingAttr.getCertificate( ).getCertifierCode( ), existingAttr.getAttributeKey( ).getKeyName( ) );
1251                     final int existingLvl = Integer.parseInt( existingCertif.getRefCertificationLevel( ).getLevel( ) );
1252 
1253                     return wantedLvl < existingLvl;
1254                 } );
1255         if ( lesserWantedLvl )
1256         {
1257             response.setStatus( ResponseStatusFactory.unauthorized( )
1258                     .setMessage( "Identity is connected, updating existing 'pivot' attributes with lesser certification level is forbidden." )
1259                     .setMessageKey( Constants.PROPERTY_REST_ERROR_CONNECTED_IDENTITY_FORBIDDEN_UPDATE_PIVOT_LESSER_CERTIFICATION ) );
1260             return;
1261         }
1262 
1263         // - If one "PIVOT" attribute is certified at a certain level N (conf), all "PIVOT" attributes must be set and certified with level >= N.
1264         final int threshold = AppPropertiesService.getPropertyInt( PIVOT_CERTIF_LEVEL_THRESHOLD, 400 );
1265         final boolean breakingThreshold = identity.getAttributes( ).values( ).stream( ).filter( a -> a.getAttributeKey( ).getPivot( ) )
1266                 .map( a -> RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( a.getCertificate( ).getCertifierCode( ),
1267                         a.getAttributeKey( ).getKeyName( ) ) )
1268                 .anyMatch( c -> Integer.parseInt( c.getRefCertificationLevel( ).getLevel( ) ) >= threshold )
1269                 || requestIdentity.getAttributes( ).stream( )
1270                         .map( a -> RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( a.getCertifier( ), a.getKey( ) ) )
1271                         .anyMatch( c -> Integer.parseInt( c.getRefCertificationLevel( ).getLevel( ) ) >= threshold );
1272         if ( breakingThreshold )
1273         {
1274             // get all pivot attributes from database
1275             final List<String> pivotAttributeKeys = allAttributesByKey.values( ).stream( ).filter( AttributeKey::getPivot ).map( AttributeKey::getKeyName )
1276                     .collect( Collectors.toList( ) );
1277 
1278             // if any pivot is missing from request + existing -> unauthorized
1279             @SuppressWarnings( "unchecked" )
1280             final Collection<String> unionOfExistingAndRequestedPivotKeys = CollectionUtils.union(
1281                     requestIdentity.getAttributes( ).stream( ).map( AttributeDto::getKey ).collect( Collectors.toSet( ) ),
1282                     identity.getAttributes( ).values( ).stream( ).map( IdentityAttribute::getAttributeKey ).filter( AttributeKey::getPivot )
1283                             .map( AttributeKey::getKeyName ).collect( Collectors.toSet( ) ) );
1284             if ( !CollectionUtils.isEqualCollection( pivotAttributeKeys, unionOfExistingAndRequestedPivotKeys ) )
1285             {
1286                 response.setStatus( ResponseStatusFactory.unauthorized( )
1287                         .setMessage( "Identity is connected, and at least one 'pivot' attribute is, or has been requested to be, certified above level "
1288                                 + threshold + ". In that case, all 'pivot' attributes must be set, and certified with level greater or equal to " + threshold
1289                                 + "." )
1290                         .setMessageKey( Constants.PROPERTY_REST_ERROR_CONNECTED_IDENTITY_FORBIDDEN_PIVOT_CERTIFICATION_UNDER_THRESHOLD ) );
1291                 return;
1292             }
1293 
1294             // if any has level lesser than threshold -> unauthorized
1295             final boolean lesserThanThreshold = pivotAttributeKeys.stream( ).map( key -> {
1296                 final AttributeDto requested = requestIdentity.getAttributes( ).stream( ).filter( a -> a.getKey( ).equals( key ) ).findFirst( ).orElse( null );
1297                 final IdentityAttribute existing = identity.getAttributes( ).get( key );
1298                 int requestedLvl = 0;
1299                 int existingLvl = 0;
1300                 if ( requested != null )
1301                 {
1302                     requestedLvl = Integer.parseInt( RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( requested.getCertifier( ), key )
1303                             .getRefCertificationLevel( ).getLevel( ) );
1304                 }
1305                 if ( existing != null )
1306                 {
1307                     existingLvl = Integer.parseInt(
1308                             RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( existing.getCertificate( ).getCertifierCode( ), key )
1309                                     .getRefCertificationLevel( ).getLevel( ) );
1310                 }
1311                 return Math.max( requestedLvl, existingLvl );
1312             } ).anyMatch( lvl -> lvl < threshold );
1313 
1314             if ( lesserThanThreshold )
1315             {
1316                 response.setStatus( ResponseStatusFactory.unauthorized( )
1317                         .setMessage( "Identity is connected, and at least one 'pivot' attribute is, or has been requested to be, certified above level "
1318                                 + threshold + ". In that case, all 'pivot' attributes must be set, and certified with level greater or equal to " + threshold
1319                                 + "." )
1320                         .setMessageKey( Constants.PROPERTY_REST_ERROR_CONNECTED_IDENTITY_FORBIDDEN_PIVOT_CERTIFICATION_UNDER_THRESHOLD ) );
1321             }
1322         }
1323     }
1324 
1325     /**
1326      * Gets a list of qualified identities on which to search potential duplicates.<br/>
1327      * Returned identities must have all attributes checked by the provided rule, and must also not be already merged nor be tagged as suspicious.<br/>
1328      * The list is sorted by quality (higher quality identities first).
1329      *
1330      * @param rule
1331      *            the rule used to get matching identities
1332      * @param batchSize the size of the batches
1333      * @param includeSuspicions filter CUIDs, to include or not, the CUIDs that are identified as suspicions
1334      * @return the list of identities
1335      */
1336     public Batch<String> getCUIDsBatchForPotentialDuplicate(final DuplicateRule rule, final int batchSize, final boolean includeSuspicions )
1337     {
1338         final List<Integer> attributes = rule.getCheckedAttributes( ).stream( ).map( AttributeKey::getId ).collect( Collectors.toList( ) );
1339         final List<String> customerIdsList = IdentityHome.findByAttributeExisting( attributes, rule.getNbFilledAttributes( ), true, !includeSuspicions, rule.getPriority() );
1340         if ( customerIdsList.isEmpty( ) )
1341         {
1342             return Batch.ofSize( Collections.emptyList( ), 0 );
1343         }
1344         return Batch.ofSize( customerIdsList, batchSize );
1345     }
1346 
1347     /**
1348      * request a deletion of identity .
1349      *
1350      * @param customerId
1351      *            the customer ID
1352      * @param clientCode
1353      *            the client code
1354      * @param author
1355      *            the author of the request
1356      */
1357     public void deleteRequest( final String customerId, final String clientCode, final RequestAuthor author, final IdentityChangeResponse response )
1358             throws IdentityStoreException
1359     {
1360         if ( !_serviceContractService.canDeleteIdentity( clientCode ) )
1361         {
1362             response.setStatus(
1363                     ResponseStatusFactory.failure( ).setMessage( "The client application is not authorized to request the deletion of an identity." )
1364                             .setMessageKey( Constants.PROPERTY_REST_ERROR_DELETE_UNAUTHORIZED ) );
1365             response.setCustomerId( customerId );
1366             return;
1367         }
1368 
1369         // check identity
1370         Identity identity = IdentityHome.findByCustomerId( customerId );
1371         if ( identity == null )
1372         {
1373             response.setStatus(
1374                     ResponseStatusFactory.notFound( ).setMessage( "Identity not found." ).setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITY_NOT_FOUND ) );
1375             response.setCustomerId( customerId );
1376             return;
1377         }
1378         if ( identity.isDeleted( ) )
1379         {
1380             response.setStatus( ResponseStatusFactory.failure( ).setMessage( "Identity allready in deleted state." )
1381                     .setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITY_ALREADY_DELETED ) );
1382             response.setCustomerId( customerId );
1383 
1384             return;
1385         }
1386         if ( identity.isMerged( ) )
1387         {
1388             response.setStatus( ResponseStatusFactory.failure( ).setMessage( "Identity in merged state can not be deleted." )
1389                     .setMessageKey( Constants.PROPERTY_REST_ERROR_FORBIDDEN_DELETE_ON_MERGED_IDENTITY ) );
1390             response.setCustomerId( customerId );
1391             return;
1392         }
1393 
1394         TransactionManager.beginTransaction( null );
1395         try
1396         {
1397             // expire identity (the deletion is managed by the dedicated Daemon)
1398             IdentityHome.softRemove( customerId );
1399             response.setStatus( ResponseStatusFactory.success( ).setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
1400             TransactionManager.commitTransaction( null );
1401 
1402             /* Notify listeners for indexation, history, ... */
1403             _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.DELETE, identity, response.getStatus( ).getType( ).name( ),
1404                     response.getStatus( ).getMessage( ), author, clientCode, new HashMap<>( ) );
1405 
1406             AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_DELETE, DELETE_IDENTITY_EVENT_CODE,
1407                     _internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( customerId ), SPECIFIC_ORIGIN );
1408         }
1409         catch( Exception e )
1410         {
1411             TransactionManager.rollBack( null );
1412         }
1413 
1414     }
1415 
1416     /**
1417      * Check if duplicates exist for a set of attributes
1418      * 
1419      * @param attributes
1420      *            the set of attributes
1421      * @param ruleCodeProperty
1422      *            the properties that defines the list of rules to check
1423      * @return a {@link DuplicateSearchResponse} that holds the execution result
1424      * @throws IdentityStoreException
1425      *             in case of error
1426      */
1427     private DuplicateSearchResponse checkDuplicates( final Map<String, String> attributes, final String ruleCodeProperty, final String customerId )
1428             throws IdentityStoreException
1429     {
1430         final List<String> ruleCodes = Arrays.stream(AppPropertiesService.getProperty( ruleCodeProperty, "" ).split( "," )).filter(StringUtils::isNotEmpty).collect(Collectors.toList());
1431         if( !ruleCodes.isEmpty( ) )
1432         {
1433             final DuplicateSearchResponse esDuplicates = _duplicateServiceElasticSearch.findDuplicates( attributes, customerId, ruleCodes,
1434                     Collections.emptyList( ) );
1435             if ( esDuplicates != null )
1436             {
1437                 return esDuplicates;
1438             }
1439             final boolean checkDatabase = AppPropertiesService.getPropertyBoolean( PROPERTY_DUPLICATES_CHECK_DATABASE_ACTIVATED, false );
1440             if ( checkDatabase )
1441             {
1442                 return _duplicateServiceDatabase.findDuplicates( attributes, "", ruleCodes, Collections.emptyList( ) );
1443             }
1444         }
1445         return null;
1446     }
1447 
1448     /**
1449      * Dé-certification d'une identité.
1450      *
1451      * @param strCustomerId
1452      *            customer ID
1453      * @return the response
1454      * @see IdentityAttributeService#uncertifyAttribute
1455      */
1456     public IdentityChangeResponse uncertifyIdentity( final String strCustomerId, final String strClientCode, final RequestAuthor author )
1457     {
1458         final IdentityChangeResponse response = new IdentityChangeResponse( );
1459 
1460         final Identity identity = IdentityHome.findByCustomerId( strCustomerId );
1461         if ( identity == null )
1462         {
1463             response.setStatus(
1464                     ResponseStatusFactory.notFound( ).setMessage( "No identity found" ).setMessageKey( Constants.PROPERTY_REST_ERROR_IDENTITY_NOT_FOUND ) );
1465             return response;
1466         }
1467 
1468         TransactionManager.beginTransaction( null );
1469         try
1470         {
1471             final List<AttributeStatus> attrStatusList = new ArrayList<>( );
1472             for ( final IdentityAttribute attribute : identity.getAttributes( ).values( ) )
1473             {
1474                 final AttributeStatus status = _identityAttributeService.uncertifyAttribute( attribute );
1475                 attrStatusList.add( status );
1476             }
1477 
1478             // update identity to set lastupdate_date
1479             IdentityHome.update( identity );
1480 
1481             response.setLastUpdateDate( identity.getLastUpdateDate( ) );
1482             response.setStatus( ResponseStatusFactory.success( ).setAttributeStatuses( attrStatusList )
1483                     .setMessageKey( Constants.PROPERTY_REST_INFO_SUCCESSFUL_OPERATION ) );
1484             TransactionManager.commitTransaction( null );
1485 
1486             /* Historique des modifications */
1487             for ( AttributeStatus attributeStatus : attrStatusList )
1488             {
1489                 _identityStoreNotifyListenerService.notifyListenersAttributeChange( AttributeChangeType.UPDATE, identity, attributeStatus, author,
1490                         strClientCode );
1491             }
1492 
1493             /* Indexation et historique */
1494             _identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.UPDATE, identity, response.getStatus( ).getType( ).name( ),
1495                     response.getStatus( ).getMessage( ), author, strClientCode, new HashMap<>( ) );
1496 
1497             AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, DECERTIFY_IDENTITY_EVENT_CODE,
1498                     _internalUserService.getApiUser( author, strClientCode ), SecurityUtil.logForgingProtect( strCustomerId ), SPECIFIC_ORIGIN );
1499         }
1500         catch( final Exception e )
1501         {
1502             response.setStatus(
1503                     ResponseStatusFactory.failure( ).setMessage( e.getMessage( ) ).setMessageKey( Constants.PROPERTY_REST_ERROR_DURING_TREATMENT ) );
1504             TransactionManager.rollBack( null );
1505         }
1506 
1507         return response;
1508     }
1509 
1510     /**
1511      * Delete the identity and all his children, including potential merged identities, EXCEPT identity history.<br/>
1512      * The purge consist of deleting, for the identity and all of its potential merged identities :
1513      * <ul>
1514      * <li>the {@link Identity} object</li>
1515      * <li>the {@link IdentityAttribute} objetcs</li>
1516      * <li>the IdentityAttributeHistory objects</li>
1517      * <li>the {@link SuspiciousIdentity} objects</li>
1518      * <li>the {@link ExcludedIdentities} objects</li>
1519      * </ul>
1520      * The identity's history is kept.
1521      * 
1522      * @param customerId
1523      *            the customerId of the identity to delete
1524      */
1525     public void delete( final String customerId )
1526     {
1527         final int identityId = IdentityHome.findIdByCustomerId( customerId );
1528         if ( identityId != -1 )
1529         {
1530             final List<Identity> mergedIdentities = IdentityHome.findMergedIdentities( identityId );
1531             TransactionManager.beginTransaction( null );
1532             try
1533             {
1534                 // Delete eventual merged identities first
1535                 for ( final Identity mergedIdentity : mergedIdentities )
1536                 {
1537                     SuspiciousIdentityHome.remove( mergedIdentity.getCustomerId( ) );
1538                     SuspiciousIdentityHome.removeExcludedIdentities( mergedIdentity.getCustomerId( ) );
1539                     IdentityHome.deleteAttributeHistory( mergedIdentity.getId( ) );
1540                     IdentityHome.hardRemove( mergedIdentity.getId( ) );
1541                 }
1542                 // Delete the actual identity
1543                 SuspiciousIdentityHome.remove( customerId );
1544                 SuspiciousIdentityHome.removeExcludedIdentities( customerId );
1545                 IdentityHome.deleteAttributeHistory( identityId );
1546                 IdentityHome.hardRemove( identityId );
1547 
1548                 TransactionManager.commitTransaction( null );
1549             }
1550             catch( final Exception e )
1551             {
1552                 TransactionManager.rollBack( null );
1553                 throw e;
1554             }
1555         }
1556     }
1557 
1558 }