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 com.google.common.util.concurrent.AtomicDouble;
37  import fr.paris.lutece.plugins.identitystore.business.attribute.AttributeKey;
38  import fr.paris.lutece.plugins.identitystore.business.contract.AttributeRequirement;
39  import fr.paris.lutece.plugins.identitystore.business.contract.AttributeRight;
40  import fr.paris.lutece.plugins.identitystore.business.contract.ServiceContract;
41  import fr.paris.lutece.plugins.identitystore.business.duplicates.suspicions.ExcludedIdentities;
42  import fr.paris.lutece.plugins.identitystore.business.duplicates.suspicions.SuspiciousIdentity;
43  import fr.paris.lutece.plugins.identitystore.business.duplicates.suspicions.SuspiciousIdentityHome;
44  import fr.paris.lutece.plugins.identitystore.business.identity.Identity;
45  import fr.paris.lutece.plugins.identitystore.business.identity.IdentityHome;
46  import fr.paris.lutece.plugins.identitystore.cache.QualityBaseCache;
47  import fr.paris.lutece.plugins.identitystore.service.attribute.IdentityAttributeService;
48  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeDto;
49  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.ConsolidateDefinition;
50  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.IdentityDto;
51  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.QualityDefinition;
52  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.duplicate.IdentityDuplicateDefinition;
53  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.duplicate.IdentityDuplicateExclusion;
54  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.duplicate.IdentityDuplicateSuspicion;
55  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.SearchAttribute;
56  import fr.paris.lutece.portal.service.spring.SpringContextService;
57  import fr.paris.lutece.portal.service.util.AppPropertiesService;
58  import org.apache.commons.collections4.CollectionUtils;
59  import org.apache.commons.lang3.StringUtils;
60  
61  import java.util.Collections;
62  import java.util.HashMap;
63  import java.util.List;
64  import java.util.Map;
65  import java.util.Objects;
66  import java.util.Set;
67  import java.util.concurrent.atomic.AtomicInteger;
68  import java.util.stream.Collectors;
69  
70  public class IdentityQualityService
71  {
72      private static final QualityBaseCache _qualityBaseCache = SpringContextService.getBean( "identitystore.qualityBaseCache" );
73  
74      private static IdentityQualityService _instance;
75  
76      public static IdentityQualityService instance( )
77      {
78          if ( _instance == null )
79          {
80              _instance = new IdentityQualityService( );
81              _qualityBaseCache.refresh( );
82          }
83          return _instance;
84      }
85  
86      private IdentityQualityService( )
87      {
88      }
89  
90      public void enrich( final List<SearchAttribute> searchAttributes, final IdentityDto identity, final ServiceContract serviceContract, final Identity bean )
91      {
92          this.enrich( searchAttributes, identity, serviceContract, bean, true );
93      }
94  
95      public void enrich( final List<SearchAttribute> searchAttributes, final IdentityDto identity, final ServiceContract serviceContract, final Identity bean,
96              final boolean computeDuplicateDefinition )
97      {
98          /* Compute Quality Definition */
99          IdentityQualityService.instance( ).computeCoverage( identity, serviceContract );
100         IdentityQualityService.instance( ).computeQuality( identity );
101         IdentityQualityService.instance( ).computeMatchScore( identity, searchAttributes );
102 
103         /* Filter client readable attributes */
104         final List<AttributeDto> filteredAttributeValues = identity.getAttributes( ).stream( )
105                 .filter( certifiedAttribute -> serviceContract.getAttributeRights( ).stream( )
106                         .anyMatch( attributeRight -> StringUtils.equals( attributeRight.getAttributeKey( ).getKeyName( ), certifiedAttribute.getKey( ) )
107                                 && attributeRight.isReadable( ) ) )
108                 .collect( Collectors.toList( ) );
109         identity.getAttributes( ).clear( );
110         identity.getAttributes( ).addAll( filteredAttributeValues );
111 
112         if ( computeDuplicateDefinition )
113         {
114             /* Compute Duplicate Definition */
115             final SuspiciousIdentity suspiciousIdentity = SuspiciousIdentityHome.selectByCustomerID( identity.getCustomerId( ) );
116             if ( suspiciousIdentity != null )
117             {
118                 identity.setDuplicateDefinition( new IdentityDuplicateDefinition( ) );
119                 final IdentityDuplicateSuspicion duplicateSuspicion = new IdentityDuplicateSuspicion( );
120                 identity.getDuplicateDefinition( ).setDuplicateSuspicion( duplicateSuspicion );
121                 duplicateSuspicion.setDuplicateRuleCode( suspiciousIdentity.getDuplicateRuleCode( ) );
122                 duplicateSuspicion.setCreationDate( suspiciousIdentity.getCreationDate( ) );
123             }
124 
125             final List<ExcludedIdentities> excludedIdentitiesList = SuspiciousIdentityHome.getExcludedIdentitiesList( identity.getCustomerId( ) );
126             if ( CollectionUtils.isNotEmpty( excludedIdentitiesList ) )
127             {
128                 if ( identity.getDuplicateDefinition( ) == null )
129                 {
130                     identity.setDuplicateDefinition( new IdentityDuplicateDefinition( ) );
131                 }
132                 identity.getDuplicateDefinition( ).getDuplicateExclusions( ).addAll( excludedIdentitiesList.stream( ).map( excludedIdentities -> {
133                     final IdentityDuplicateExclusion exclusion = new IdentityDuplicateExclusion( );
134                     exclusion.setExclusionDate( excludedIdentities.getExclusionDate( ) );
135                     exclusion.setAuthorName( excludedIdentities.getAuthorName( ) );
136                     exclusion.setAuthorType( excludedIdentities.getAuthorType( ) );
137                     final String excludedCustomerId = Objects.equals( excludedIdentities.getFirstCustomerId( ), identity.getCustomerId( ) )
138                             ? excludedIdentities.getSecondCustomerId( )
139                             : excludedIdentities.getFirstCustomerId( );
140                     exclusion.setExcludedCustomerId( excludedCustomerId );
141                     return exclusion;
142                 } ).collect( Collectors.toList( ) ) );
143             }
144         }
145 
146         if ( bean != null )
147         {
148             final List<Identity> mergedIdentities = IdentityHome.findMergedIdentities( bean.getId( ) );
149             if ( !mergedIdentities.isEmpty( ) )
150             {
151                 final ConsolidateDefinition consolidateDefinition = new ConsolidateDefinition( );
152                 for ( final Identity mergedIdentity : mergedIdentities )
153                 {
154                     final IdentityDto mergedDto = new IdentityDto( );
155                     mergedDto.setCustomerId( mergedIdentity.getCustomerId( ) );
156                     mergedDto.setConnectionId( mergedIdentity.getConnectionId( ) );
157                     consolidateDefinition.getMergedIdentities( ).add( mergedDto );
158                 }
159                 identity.setConsolidate( consolidateDefinition );
160             }
161         }
162     }
163 
164     /**
165      * Compute the {@link IdentityDto} coverage of the {@link ServiceContract} requirements. <br>
166      * Rule:<br>
167      * The coverage is set to 1 when
168      * <ul>
169      * <li>All mandatory keys defined in CS must be present in identity and match the defined minimum level</li>
170      * <li>Optional keys (not mandatory but with a minimum level defined in CS) can be absent in identity, but if present must match the defined minimum
171      * level</li>
172      * </ul>
173      * Otherwise, the coverage is set to 0
174      * 
175      * @param identity
176      *            the identity to qualify
177      * @param serviceContract
178      *            the base service contract
179      */
180     private void computeCoverage( final IdentityDto identity, final ServiceContract serviceContract )
181     {
182         final Set<String> mandatoryKeys = serviceContract.getAttributeRights( ).stream( ).filter( AttributeRight::isMandatory )
183                 .map( AttributeRight::getAttributeKey ).map( AttributeKey::getKeyName ).collect( Collectors.toSet( ) );
184         final Set<String> identityKeys = identity.getAttributes( ).stream( ).map( AttributeDto::getKey ).collect( Collectors.toSet( ) );
185 
186         if ( identity.getQuality( ) == null )
187         {
188             identity.setQuality( new QualityDefinition( ) );
189         }
190 
191         if ( !identityKeys.containsAll( mandatoryKeys ) )
192         {
193             // Some mandatory attributes are missing
194             identity.getQuality( ).setCoverage( 0 );
195         }
196         else
197         {
198             // All mandatory attributes are present, check all present attributes match the minimum certification level if defined in CS
199             boolean coverageMatches = identity.getAttributes( ).stream( ).noneMatch( certifiedAttribute -> {
200                 final AttributeRequirement requirement = serviceContract.getAttributeRequirements( ).stream( )
201                         .filter( req -> Objects.equals( req.getAttributeKey( ).getKeyName( ), certifiedAttribute.getKey( ) ) ).findFirst( ).orElse( null );
202                 final int attributeLevel = certifiedAttribute.getCertificationLevel( ) != null ? certifiedAttribute.getCertificationLevel( ) : 0;
203                 final int minLevel = ( requirement != null && requirement.getRefCertificationLevel( ) != null
204                         && requirement.getRefCertificationLevel( ).getLevel( ) != null )
205                                 ? Integer.parseInt( requirement.getRefCertificationLevel( ).getLevel( ) )
206                                 : 0;
207                 return minLevel > attributeLevel;
208             } );
209             identity.getQuality( ).setCoverage( coverageMatches ? 1 : 0 );
210         }
211     }
212 
213     public void computeQuality( final IdentityDto identity )
214     {
215         if ( identity.getQuality( ) == null )
216         {
217             identity.setQuality( new QualityDefinition( ) );
218         }
219         final AtomicInteger levels = new AtomicInteger( );
220         for ( final AttributeDto attribute : identity.getAttributes( ) )
221         {
222             if ( attribute.getCertificationLevel( ) == null || attribute.getCertificationLevel( ) == 0 || StringUtils.isBlank( attribute.getValue( ) ) )
223             {
224                 continue;
225             }
226             final AttributeKey attributeKey = IdentityAttributeService.instance( ).getAttributeKeySafe( attribute.getKey( ) );
227             if ( attributeKey != null && attributeKey.getKeyWeight( ) > 0 )
228             {
229                 levels.addAndGet( attributeKey.getKeyWeight( ) * attribute.getCertificationLevel( ) );
230             }
231         }
232         identity.getQuality( ).setQuality( levels.doubleValue( ) / _qualityBaseCache.get( ) );
233     }
234 
235     private void computeMatchScore( final IdentityDto identity, final List<SearchAttribute> searchAttributes )
236     {
237         if ( identity.getQuality( ) == null )
238         {
239             identity.setQuality( new QualityDefinition( ) );
240         }
241 
242         if ( CollectionUtils.isEmpty( searchAttributes ) )
243         {
244             identity.getQuality( ).setScoring( 1.0 );
245         }
246         else
247         {
248             final AtomicDouble levels = new AtomicDouble( );
249             final AtomicDouble base = new AtomicDouble( );
250             final Map<SearchAttribute, List<AttributeKey>> attributesToProcess = new HashMap<>( );
251             for ( final SearchAttribute searchAttribute : searchAttributes )
252             {
253                 AttributeKey refKey = null;
254                 try
255                 {
256                     refKey = IdentityAttributeService.instance( ).getAttributeKey( searchAttribute.getKey( ) );
257                 }
258                 catch( IdentityAttributeNotFoundException e )
259                 {
260                     // do nothing, we check if attribute exists
261                 }
262                 if ( refKey != null )
263                 {
264                     attributesToProcess.put( searchAttribute, Collections.singletonList( refKey ) );
265                 }
266                 else
267                 {
268                     // In this case we have a common search key in the request, so retrieve the attribute
269                     final List<AttributeKey> commonAttributes = IdentityAttributeService.instance( ).getCommonAttributeKeys( searchAttribute.getKey( ) );
270                     attributesToProcess.put( searchAttribute, commonAttributes );
271                 }
272             }
273 
274             for ( final Map.Entry<SearchAttribute, List<AttributeKey>> entry : attributesToProcess.entrySet( ) )
275             {
276                 for ( final AttributeKey attributeKey : entry.getValue( ) )
277                 {
278                     final AttributeDto attributeDto = identity.getAttributes( ).stream( )
279                             .filter( attribute -> Objects.equals( attribute.getKey( ), attributeKey.getKeyName( ) ) ).findFirst( ).orElse( null );
280                     base.addAndGet( attributeKey.getKeyWeight( ) );
281                     if ( attributeDto != null && attributeDto.getValue( ).equalsIgnoreCase( entry.getKey( ).getValue( ) ) )
282                     {
283                         levels.addAndGet( attributeKey.getKeyWeight( ) );
284                     }
285                     else
286                     {
287                         final double penalty = Double.parseDouble( AppPropertiesService.getProperty( "identitystore.identity.scoring.penalty", "0.3" ) );
288                         levels.addAndGet( attributeKey.getKeyWeight( ) - ( attributeKey.getKeyWeight( ) * penalty ) );
289                     }
290                 }
291             }
292 
293             identity.getQuality( ).setScoring( levels.doubleValue( ) / base.doubleValue( ) );
294         }
295 
296     }
297 }