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.contract;
35  
36  import fr.paris.lutece.plugins.identitystore.business.application.ClientApplication;
37  import fr.paris.lutece.plugins.identitystore.business.application.ClientApplicationHome;
38  import fr.paris.lutece.plugins.identitystore.business.attribute.AttributeKey;
39  import fr.paris.lutece.plugins.identitystore.business.contract.AttributeCertification;
40  import fr.paris.lutece.plugins.identitystore.business.contract.AttributeRequirement;
41  import fr.paris.lutece.plugins.identitystore.business.contract.AttributeRight;
42  import fr.paris.lutece.plugins.identitystore.business.contract.ServiceContract;
43  import fr.paris.lutece.plugins.identitystore.business.contract.ServiceContractHome;
44  import fr.paris.lutece.plugins.identitystore.business.referentiel.RefAttributeCertificationProcessus;
45  import fr.paris.lutece.plugins.identitystore.cache.ActiveServiceContractCache;
46  import fr.paris.lutece.plugins.identitystore.service.attribute.IdentityAttributeService;
47  import fr.paris.lutece.plugins.identitystore.service.identity.IdentityAttributeNotFoundException;
48  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeChangeStatus;
49  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeDto;
50  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeStatus;
51  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.IdentityChangeRequest;
52  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.IdentityChangeResponse;
53  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.exporting.IdentityExportRequest;
54  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.exporting.IdentityExportResponse;
55  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.merge.IdentityMergeRequest;
56  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.merge.IdentityMergeResponse;
57  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.IdentitySearchMessage;
58  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.IdentitySearchRequest;
59  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.IdentitySearchResponse;
60  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.SearchAttribute;
61  import fr.paris.lutece.plugins.identitystore.v3.web.rs.util.Constants;
62  import fr.paris.lutece.plugins.identitystore.v3.web.rs.util.ResponseStatusFactory;
63  import fr.paris.lutece.plugins.identitystore.web.exception.IdentityStoreException;
64  import fr.paris.lutece.portal.service.i18n.I18nService;
65  import fr.paris.lutece.portal.service.spring.SpringContextService;
66  import fr.paris.lutece.util.sql.TransactionManager;
67  import org.apache.commons.collections.CollectionUtils;
68  import org.apache.commons.lang3.StringUtils;
69  
70  import java.util.ArrayList;
71  import java.util.Date;
72  import java.util.List;
73  import java.util.Locale;
74  import java.util.Objects;
75  import java.util.Optional;
76  import java.util.stream.Collectors;
77  import java.util.stream.Stream;
78  
79  public class ServiceContractService
80  {
81  
82      private final ActiveServiceContractCache _cache = SpringContextService.getBean( "identitystore.activeServiceContractCache" );
83      private static ServiceContractService _instance;
84  
85      public static ServiceContractService instance( )
86      {
87          if ( _instance == null )
88          {
89              _instance = new ServiceContractService( );
90              _instance._cache.refresh( );
91          }
92          return _instance;
93      }
94  
95      private ServiceContractService( )
96      {
97      }
98  
99      /**
100      * Get the active {@link ServiceContract} associated to the given {@link ClientApplication}
101      *
102      * @param clientCode
103      *            code of the {@link ClientApplication} requesting the change
104      * @return the active {@link ServiceContract}
105      * @throws ServiceContractNotFoundException
106      */
107     public ServiceContract getActiveServiceContract( final String clientCode ) throws ServiceContractNotFoundException
108     {
109         return _cache.get( clientCode );
110     }
111 
112     /**
113      * Validates the {@link IdentityChangeRequest} against the {@link ServiceContract} associated to the {@link ClientApplication} requesting the change. Each
114      * violation is listed in the {@link IdentityChangeResponse} with a status by attribute key. The following rules are verified: <br>
115      * <ul>
116      * <li>The attribute must be writable</li>
117      * <li>The certification processus, if given in the request, must be in the list of authorized processus</li>
118      * </ul>
119      *
120      * @param identityChangeRequest
121      *            {@link IdentityChangeRequest} with list of attributes
122      * @param clientCode
123      *            code of the {@link ClientApplication} requesting the change
124      * @return {@link IdentityChangeResponse} containing the execution status
125      * @throws ServiceContractNotFoundException
126      * @throws IdentityAttributeNotFoundException
127      */
128     public IdentityChangeResponse validateIdentityChange( final IdentityChangeRequest identityChangeRequest, final String clientCode )
129             throws ServiceContractNotFoundException, IdentityAttributeNotFoundException
130     {
131         final IdentityChangeResponse response = new IdentityChangeResponse( );
132         final List<AttributeStatus> attrStatusList = new ArrayList<>( );
133         final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
134         for ( final AttributeDto attributeDto : identityChangeRequest.getIdentity( ).getAttributes( ) )
135         {
136             boolean canWriteAttribute = IdentityAttributeService.instance( ).getAttributeKey( attributeDto.getKey( ) ) != null;
137             if ( !canWriteAttribute )
138             {
139                 attrStatusList.add( this.buildAttributeStatus( attributeDto, AttributeChangeStatus.NOT_FOUND ) );
140                 continue;
141             }
142 
143             canWriteAttribute = serviceContract.getAttributeRights( ).stream( )
144                     .anyMatch( attributeRight -> StringUtils.equals( attributeRight.getAttributeKey( ).getKeyName( ), attributeDto.getKey( ) )
145                             && attributeRight.isWritable( ) );
146             if ( !canWriteAttribute )
147             {
148                 attrStatusList.add( this.buildAttributeStatus( attributeDto, AttributeChangeStatus.UNAUTHORIZED ) );
149                 continue;
150             }
151 
152             if ( attributeDto.getCertifier( ) != null )
153             {
154                 canWriteAttribute = serviceContract.getAttributeCertifications( ).stream( ).anyMatch(
155                         attributeCertification -> StringUtils.equals( attributeCertification.getAttributeKey( ).getKeyName( ), attributeDto.getKey( ) )
156                                 && attributeCertification.getRefAttributeCertificationProcessus( ).stream( )
157                                         .anyMatch( processus -> StringUtils.equals( processus.getCode( ), attributeDto.getCertifier( ) ) ) );
158                 if ( !canWriteAttribute )
159                 {
160                     attrStatusList.add( this.buildAttributeStatus( attributeDto, AttributeChangeStatus.INSUFFICIENT_RIGHTS ) );
161                 }
162             }
163         }
164 
165         if ( !attrStatusList.isEmpty( ) )
166         {
167             response.setStatus(
168                     ResponseStatusFactory.failure( ).setAttributeStatuses( attrStatusList ).setMessage( "The request violates service contract definition" )
169                             .setMessageKey( Constants.PROPERTY_REST_ERROR_SERVICE_CONTRACT_VIOLATION ) );
170         }
171 
172         return response;
173     }
174 
175     /**
176      * Validates the {@link IdentityMergeRequest} against the {@link ServiceContract} associated to the {@link ClientApplication} requesting the change. Each
177      * violation is listed in the {@link IdentityMergeResponse} with a status by attribute key. The following rules are verified: <br>
178      * <ul>
179      * <li>The {@link ClientApplication} must be authorized to perform the merge</li>
180      * </ul>
181      *
182      * @param identityMergeRequest
183      *            {@link IdentityMergeRequest} with list of attributes
184      * @param clientCode
185      *            code of the {@link ClientApplication} requesting the change
186      * @return {@link IdentityMergeResponse} containing the execution status
187      * @throws ServiceContractNotFoundException
188      */
189     public IdentityMergeResponse validateIdentityMerge( final IdentityMergeRequest identityMergeRequest, final String clientCode )
190             throws ServiceContractNotFoundException
191     {
192         final IdentityMergeResponse response = new IdentityMergeResponse( );
193         final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
194         if ( !serviceContract.getAuthorizedMerge( ) )
195         {
196             response.setStatus( ResponseStatusFactory.failure( ).setMessage( "The client application is not authorized to merge identities" )
197                     .setMessageKey( Constants.PROPERTY_REST_ERROR_MERGE_UNAUTHORIZED ) );
198         }
199 
200         return response;
201     }
202 
203     /**
204      * Validates the {@link IdentityChangeRequest} against the {@link ServiceContract} associated to the {@link ClientApplication} requesting the change. Each
205      * violation is listed in the {@link IdentityChangeResponse} with a status by attribute key. The following rules are verified: <br>
206      * <ul>
207      * <li>The {@link ClientApplication} must be authorized to perform the import</li>
208      * </ul>
209      *
210      * @param identityChangeRequest
211      *            {@link IdentityMergeRequest} with list of attributes
212      * @param clientCode
213      *            code of the {@link ClientApplication} requesting the change
214      * @return {@link IdentityMergeResponse} containing the execution status
215      * @throws ServiceContractNotFoundException
216      */
217     public IdentityChangeResponse validateIdentityImport( final IdentityChangeRequest identityChangeRequest, final String clientCode )
218             throws ServiceContractNotFoundException, IdentityAttributeNotFoundException
219     {
220         final IdentityChangeResponse response = new IdentityChangeResponse( );
221         final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
222         if ( !serviceContract.getAuthorizedImport( ) )
223         {
224             response.setStatus( ResponseStatusFactory.failure( ).setMessage( "The client application is not authorized to import identities " )
225                     .setMessageKey( Constants.PROPERTY_REST_ERROR_IMPORT_UNAUTHORIZED ) );
226         }
227         else
228         {
229             return this.validateIdentityChange( identityChangeRequest, clientCode );
230         }
231 
232         return response;
233     }
234 
235     /**
236      * Validates the {@link IdentityExportRequest} against the {@link ServiceContract} associated to the {@link ClientApplication} requesting the export. Each
237      * violation is listed in the response with a status by attribute key. The following rules are verified: <br>
238      * <ul>
239      * <li>The attribute must be readable</li>
240      * </ul>
241      *
242      * @param identityExportRequest
243      *            {@link IdentityExportRequest} with list of attributes
244      * @param clientCode
245      *            code of the {@link ClientApplication} requesting the search
246      * @throws ServiceContractNotFoundException
247      */
248     public IdentityExportResponse validateIdentityExport(final IdentityExportRequest identityExportRequest, final String clientCode ) throws ServiceContractNotFoundException
249     {
250         final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
251         final IdentityExportResponse response = new IdentityExportResponse( );
252         if ( !serviceContract.getAuthorizedExport( ) )
253         {
254             response.setStatus( ResponseStatusFactory.failure( ).setMessage( "The client application is not authorized to export identities." )
255                     .setMessageKey( Constants.PROPERTY_REST_ERROR_EXPORT_UNAUTHORIZED ) );
256             return response;
257         }
258 
259         if ( identityExportRequest.getAttributeKeyList( ) != null && !identityExportRequest.getAttributeKeyList( ).isEmpty( ) )
260         {
261 
262             for ( final String searchAttributeKey : identityExportRequest.getAttributeKeyList( ) )
263             {
264                 final Optional<AttributeRight> attributeRight = serviceContract.getAttributeRights( ).stream( )
265                         .filter( a -> StringUtils.equals( a.getAttributeKey( ).getKeyName( ), searchAttributeKey ) ).findFirst( );
266                 if ( attributeRight.isPresent( ) )
267                 {
268                     boolean canReadAttribute = attributeRight.get( ).isReadable( );
269 
270                     if ( !canReadAttribute )
271                     {
272                         response.setStatus( ResponseStatusFactory.failure( ).setMessageKey( Constants.PROPERTY_REST_ERROR_SERVICE_CONTRACT_VIOLATION ).setMessage( searchAttributeKey + " key is not readable in service contract definition." ) );
273                     }
274                 }
275                 else
276                 {
277                     response.setStatus( ResponseStatusFactory.failure( ).setMessageKey( Constants.PROPERTY_REST_ERROR_SERVICE_CONTRACT_VIOLATION ).setMessage( searchAttributeKey + " key does not exist in service contract definition." ) );
278                 }
279             }
280         }
281 
282         return response;
283     }
284 
285     /**
286      * Validates the {@link IdentitySearchRequest} against the {@link ServiceContract} associated to the {@link ClientApplication} requesting the search. Each
287      * violation is listed in the response with a status by attribute key. The following rules are verified: <br>
288      * <ul>
289      * <li>The attribute must be searchable</li>
290      * </ul>
291      *
292      * @param identitySearchRequest
293      *            {@link IdentitySearchRequest} with list of attributes
294      * @param clientCode
295      *            code of the {@link ClientApplication} requesting the search
296      * @throws ServiceContractNotFoundException
297      */
298     public IdentitySearchResponse validateIdentitySearch( final IdentitySearchRequest identitySearchRequest, final String clientCode,
299             final boolean checkContract ) throws ServiceContractNotFoundException
300     {
301         final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
302         final IdentitySearchResponse response = new IdentitySearchResponse( );
303         if ( checkContract && !serviceContract.getAuthorizedSearch( ) )
304         {
305             response.setStatus( ResponseStatusFactory.failure( ).setMessage( "The client application is not authorized to search an identity." )
306                     .setMessageKey( Constants.PROPERTY_REST_ERROR_SEARCH_UNAUTHORIZED ) );
307             final IdentitySearchMessage message = new IdentitySearchMessage( );
308             message.setMessage( "The client application is not authorized to search an identity." ); // TODO améliorer le modèle
309             response.getAlerts( ).add( message );
310             return response;
311         }
312 
313         if ( identitySearchRequest.getSearch( ) != null )
314         {
315 
316             for ( final SearchAttribute searchAttribute : identitySearchRequest.getSearch( ).getAttributes( ) )
317             {
318                 final Optional<AttributeRight> attributeRight = serviceContract.getAttributeRights( ).stream( )
319                         .filter( a -> StringUtils.equals( a.getAttributeKey( ).getKeyName( ), searchAttribute.getKey( ) ) ).findFirst( );
320                 if ( attributeRight.isPresent( ) )
321                 {
322                     boolean canSearchAttribute = attributeRight.get( ).isSearchable( );
323 
324                     if ( !canSearchAttribute )
325                     {
326                         final IdentitySearchMessage alert = new IdentitySearchMessage( );
327                         alert.setAttributeName( searchAttribute.getKey( ) );
328                         alert.setMessage( "This attribute is not searchable in service contract definition." );
329                         response.getAlerts( ).add( alert );
330                         response.setStatus( ResponseStatusFactory.failure( ).setMessageKey( Constants.PROPERTY_REST_ERROR_SERVICE_CONTRACT_VIOLATION ) );
331                     }
332                 }
333                 else
334                 { // if key does not exist, it can be a common key for search
335                     final List<AttributeRight> commonAttributes = serviceContract.getAttributeRights( ).stream( )
336                             .filter( a -> StringUtils.equals( a.getAttributeKey( ).getCommonSearchKeyName( ), searchAttribute.getKey( ) ) )
337                             .collect( Collectors.toList( ) );
338                     if ( CollectionUtils.isNotEmpty( commonAttributes ) )
339                     {
340                         boolean canSearchAttribute = commonAttributes.stream( ).allMatch( a -> a.isSearchable( ) );
341                         if ( !canSearchAttribute )
342                         {
343                             final IdentitySearchMessage alert = new IdentitySearchMessage( );
344                             alert.setAttributeName( searchAttribute.getKey( ) );
345                             alert.setMessage( "This attribute group is not searchable in service contract definition." );
346                             response.getAlerts( ).add( alert );
347                             response.setStatus( ResponseStatusFactory.failure( ).setMessageKey( Constants.PROPERTY_REST_ERROR_SERVICE_CONTRACT_VIOLATION ) );
348                         }
349                     }
350                     else
351                     {
352                         final IdentitySearchMessage alert = new IdentitySearchMessage( );
353                         alert.setAttributeName( searchAttribute.getKey( ) );
354                         alert.setMessage( "This attribute does not exist in service contract definition." );
355                         response.getAlerts( ).add( alert );
356                         response.setStatus( ResponseStatusFactory.failure( ).setMessageKey( Constants.PROPERTY_REST_ERROR_SERVICE_CONTRACT_VIOLATION ) );
357                     }
358                 }
359             }
360         }
361 
362         return response;
363     }
364 
365     public List<String> getMandatoryAttributes( final String clientCode, final List<AttributeKey> sharedMandatoryAttributeList )
366             throws ServiceContractNotFoundException
367     {
368         final List<AttributeRight> rights = this.getActiveServiceContract( clientCode ).getAttributeRights( );
369         return Stream
370                 .concat( sharedMandatoryAttributeList.stream( ).map( AttributeKey::getKeyName ),
371                         rights.stream( ).filter( AttributeRight::isMandatory ).map( ar -> ar.getAttributeKey( ).getKeyName( ) ) )
372                 .distinct( ).collect( Collectors.toList( ) );
373 
374     }
375 
376     public boolean canModifyConnectedIdentity( final String clientCode ) throws ServiceContractNotFoundException
377     {
378         final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
379         return serviceContract.getAuthorizedAccountUpdate( );
380     }
381 
382     public boolean canCreateIdentity( final String clientCode ) throws ServiceContractNotFoundException
383     {
384         final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
385         return serviceContract.getAuthorizedCreation( );
386     }
387 
388     public boolean canUpdateIdentity( final String clientCode ) throws ServiceContractNotFoundException
389     {
390         final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
391         return serviceContract.getAuthorizedUpdate( );
392     }
393 
394     public boolean canUncertifyIdentity( final String clientCode ) throws ServiceContractNotFoundException
395     {
396         final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
397         return serviceContract.getAuthorizedDecertification( );
398     }
399 
400     public boolean canDeleteIdentity( String clientCode ) throws ServiceContractNotFoundException
401     {
402         final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
403         return serviceContract.getAuthorizedDeletion( );
404     }
405 
406     public int getDataRetentionPeriodInMonths( final String clientCode ) throws ServiceContractNotFoundException
407     {
408         final ServiceContract serviceContract = this.getActiveServiceContract( clientCode );
409         return serviceContract.getDataRetentionPeriodInMonths( );
410     }
411 
412     private AttributeStatus buildAttributeStatus( final AttributeDto attributeDto, final AttributeChangeStatus status )
413     {
414         final AttributeStatus attributeStatus = new AttributeStatus( );
415         attributeStatus.setKey( attributeDto.getKey( ) );
416         attributeStatus.setStatus( status );
417         return attributeStatus;
418     }
419 
420     /**
421      * Creates a new {@link ServiceContract} (if possible) and adds it to cache if active. The contract can be created if:
422      * <ul>
423      * <li>The start date of the contract is not in the range [start date; end date] of an existing contract</li>
424      * <li>The end date of the contract is not in the range [start date; end date] of an existing contract</li>
425      * </ul>
426      *
427      * @param serviceContract
428      * @param applicationId
429      * @return
430      */
431     public ServiceContractitystore/business/contract/ServiceContract.html#ServiceContract">ServiceContract create( final ServiceContract serviceContract, final Integer applicationId ) throws IdentityStoreException
432     {
433         TransactionManager.beginTransaction( null );
434         try
435         {
436             final ClientApplication clientApplication = ClientApplicationHome.findByPrimaryKey( applicationId );
437             this.validateContractDefinition( serviceContract, clientApplication );
438             ServiceContractHome.create( serviceContract, applicationId );
439             if ( CollectionUtils.isNotEmpty( serviceContract.getAttributeRights( ) ) )
440             {
441                 ServiceContractHome.addAttributeRights( serviceContract.getAttributeRights( ), serviceContract );
442             }
443             if ( CollectionUtils.isNotEmpty( serviceContract.getAttributeRequirements( ) ) )
444             {
445                 ServiceContractHome.addAttributeRequirements( serviceContract.getAttributeRequirements( ), serviceContract );
446             }
447             if ( CollectionUtils.isNotEmpty( serviceContract.getAttributeCertifications( ) ) )
448             {
449                 ServiceContractHome.addAttributeCertifications( serviceContract.getAttributeCertifications( ), serviceContract );
450             }
451             serviceContract.setClientCode( clientApplication.getClientCode( ) );
452 
453             if ( serviceContract.isActive( ) )
454             {
455                 this._cache.put( clientApplication.getClientCode( ), serviceContract );
456             }
457             TransactionManager.commitTransaction( null );
458         }
459         catch( Exception e )
460         {
461             TransactionManager.rollBack( null );
462             throw new IdentityStoreException( e.getMessage( ), e );
463         }
464 
465         return serviceContract;
466     }
467 
468     /**
469      * Update an existing {@link ServiceContract} (if possible) and adds it to cache if active. The contract can be updated if:
470      * <ul>
471      * <li>The start date of the contract is not in the range [start date; end date] of an existing contract</li>
472      * <li>The end date of the contract is not in the range [start date; end date] of an existing contract</li>
473      * </ul>
474      *
475      * @param serviceContract
476      * @param applicationId
477      * @return
478      */
479     public ServiceContractitystore/business/contract/ServiceContract.html#ServiceContract">ServiceContract update( final ServiceContract serviceContract, final Integer applicationId ) throws IdentityStoreException
480     {
481 
482         TransactionManager.beginTransaction( null );
483         try
484         {
485             final ClientApplication clientApplication = ClientApplicationHome.findByPrimaryKey( applicationId );
486             this.validateContractDefinition( serviceContract, clientApplication );
487             ServiceContractHome.update( serviceContract, applicationId );
488             ServiceContractHome.removeAttributeRights( serviceContract );
489             ServiceContractHome.addAttributeRights( serviceContract.getAttributeRights( ), serviceContract );
490             ServiceContractHome.removeAttributeRequirements( serviceContract );
491             ServiceContractHome.addAttributeRequirements( serviceContract.getAttributeRequirements( ), serviceContract );
492             ServiceContractHome.removeAttributeCertifications( serviceContract );
493             ServiceContractHome.addAttributeCertifications( serviceContract.getAttributeCertifications( ), serviceContract );
494             serviceContract.setClientCode( clientApplication.getClientCode( ) );
495 
496             if ( serviceContract.isActive( ) )
497             {
498                 this._cache.deleteById( serviceContract.getId( ) );
499                 this._cache.put( clientApplication.getClientCode( ), serviceContract );
500             }
501             TransactionManager.commitTransaction( null );
502         }
503         catch( Exception e )
504         {
505             TransactionManager.rollBack( null );
506             throw new IdentityStoreException( e.getMessage( ), e );
507         }
508         return serviceContract;
509     }
510 
511     /**
512      * Closes an existing {@link ServiceContract} (if possible) and adds it to cache if active.
513      *
514      * @param serviceContract
515      * @return
516      */
517     public ServiceContracttitystore/business/contract/ServiceContract.html#ServiceContract">ServiceContract close( final ServiceContract serviceContract ) throws IdentityStoreException
518     {
519 
520         TransactionManager.beginTransaction( null );
521         try
522         {
523             ServiceContractHome.close( serviceContract );
524             TransactionManager.commitTransaction( null );
525         }
526         catch( Exception e )
527         {
528             TransactionManager.rollBack( null );
529             throw new IdentityStoreException( e.getMessage( ), e );
530         }
531         return serviceContract;
532     }
533 
534     /**
535      * Deletes a {@link ServiceContract} by its id in the database
536      *
537      * @param id
538      */
539     public void delete( final Integer id )
540     {
541         ServiceContractHome.remove( id );
542         _cache.deleteById( id );
543     }
544 
545     /**
546      * Deletes a {@link ClientApplication} and all the related {@link ServiceContract}
547      *
548      * @param clientApplication
549      */
550     public void deleteApplication( final ClientApplication clientApplication ) throws IdentityStoreException
551     {
552         TransactionManager.beginTransaction( null );
553         try
554         {
555             ClientApplicationHome.removeContracts( clientApplication );
556             ClientApplicationHome.remove( clientApplication );
557             _cache.removeKey( clientApplication.getClientCode( ) );
558             TransactionManager.commitTransaction( null );
559         }
560         catch( Exception e )
561         {
562             TransactionManager.rollBack( null );
563             throw new IdentityStoreException( e.getMessage( ), e );
564         }
565     }
566 
567     /**
568      * Checks if the definition of a given {@link ServiceContract} is Valid. Start date and End date of the given contract shall not be in the range of Start
569      * date and End date of an existing contract.
570      *
571      * @param serviceContract
572      * @param clientApplication
573      */
574     private void validateContractDefinition( final ServiceContract serviceContract, final ClientApplication clientApplication )
575             throws ServiceContractDefinitionException
576     {
577         final List<ServiceContract> serviceContracts = ClientApplicationHome.selectServiceContracts( clientApplication );
578         if ( serviceContracts == null || serviceContracts.isEmpty( ) )
579         {
580             return;
581         }
582         if ( serviceContract.getStartingDate( ) == null )
583         {
584             throw new ServiceContractDefinitionException( "The start date of the contract shall be set" );
585         }
586         // filter current contract in case of update
587         final List<ServiceContract> filteredServiceContracts = serviceContracts.stream( ).filter( c -> !Objects.equals( c.getId( ), serviceContract.getId( ) ) )
588                 .collect( Collectors.toList( ) );
589         if ( filteredServiceContracts.stream( )
590                 .anyMatch( contract -> isInRange( serviceContract.getStartingDate( ), contract.getStartingDate( ), contract.getEndingDate( ) ) ) )
591         {
592             throw new ServiceContractDefinitionException( "The start date of the contract is in the range of an existing service contract" );
593         }
594         if ( filteredServiceContracts.stream( )
595                 .anyMatch( contract -> isInRange( serviceContract.getEndingDate( ), contract.getStartingDate( ), contract.getEndingDate( ) ) ) )
596         {
597             throw new ServiceContractDefinitionException( "The end date of the contract is in the range of an existing service contract" );
598         }
599         // TODO traiter le cas où il existe un contrat sans date de fin => soit on interdit soit on ferme le contrat automatiquement
600         if ( filteredServiceContracts.stream( )
601                 .anyMatch( contract -> contract.getEndingDate( ) == null && contract.getStartingDate( ).before( serviceContract.getStartingDate( ) ) ) )
602         {
603             throw new ServiceContractDefinitionException( "A contract exists with an infinite end date" );
604         }
605 
606         // https://dev.lutece.paris.fr/gitlab/bild/gestion-identite/identity-management/-/issues/224
607         // Les processus sélectionnés pour l'écriture d'un attribut doivent avoir un level >= Niveau de certification minimum exigé s'il est présent
608         final List<AttributeRequirement> filledRequirements = serviceContract.getAttributeRequirements( ).stream( )
609                 .filter( requirement -> requirement.getRefCertificationLevel( ) != null && requirement.getRefCertificationLevel( ).getLevel( ) != null )
610                 .collect( Collectors.toList( ) );
611         final StringBuilder message = new StringBuilder( );
612         boolean hasLevelError = false;
613         for ( final AttributeRequirement requirement : filledRequirements )
614         {
615             final Optional<AttributeCertification> result = serviceContract.getAttributeCertifications( ).stream( )
616                     .filter( certification -> certification.getAttributeKey( ).getId( ) == requirement.getAttributeKey( ).getId( ) ).findFirst( );
617             if ( result.isPresent( ) )
618             {
619                 final AttributeCertification attributeCertification = result.get( );
620                 final int minLevel = Integer.parseInt( requirement.getRefCertificationLevel( ).getLevel( ) );
621                 for ( final RefAttributeCertificationProcessus processus : attributeCertification.getRefAttributeCertificationProcessus( ) )
622                 {
623                     final int processLevel = AttributeCertificationDefinitionService.instance( ).getLevelAsInteger( processus.getCode( ),
624                             attributeCertification.getAttributeKey( ).getKeyName( ) );
625                     if ( processLevel < minLevel )
626                     {
627                         hasLevelError = true;
628                         final String [ ] params = {
629                                 processus.getLabel( ), requirement.getAttributeKey( ).getName( ), String.valueOf( processLevel ), String.valueOf( minLevel )
630                         };
631                         message.append(
632                                 I18nService.getLocalizedString( "identitystore.message.error.servicecontract.processus.level", params, Locale.getDefault( ) ) );
633                         message.append( "<br>" );
634                     }
635                 }
636             }
637         }
638         if ( hasLevelError )
639         {
640             throw new ServiceContractDefinitionException( message.toString( ) );
641         }
642     }
643 
644     public boolean isInRange( final Date testedDate, final Date min, final Date max )
645     {
646         if ( testedDate != null && min != null && max != null )
647         {
648             return testedDate.getTime( ) >= min.getTime( ) && testedDate.getTime( ) <= max.getTime( );
649         }
650         return false;
651     }
652 
653     /**
654      * get Client Code list From AppCode
655      * 
656      * @param appCode
657      *            the app code
658      * @return list of corresponding client code
659      */
660     public List<String> getClientCodesFromAppCode( String appCode )
661     {
662         final List<ClientApplication> clientApplicationList = ClientApplicationHome.findByApplicationCode( appCode );
663         return clientApplicationList.stream( ).map( ClientApplication::getClientCode ).collect( Collectors.toList( ) );
664     }
665 
666 }