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;
35  
36  import static fr.paris.lutece.plugins.identitystore.service.identity.IdentityService.UPDATE_IDENTITY_EVENT_CODE;
37  
38  import java.sql.Date;
39  import java.sql.Timestamp;
40  import java.time.Instant;
41  import java.time.ZoneId;
42  import java.time.ZonedDateTime;
43  import java.util.ArrayList;
44  import java.util.Collections;
45  import java.util.List;
46  
47  import org.apache.commons.lang3.StringUtils;
48  
49  import fr.paris.lutece.plugins.grubusiness.business.demand.DemandType;
50  import fr.paris.lutece.plugins.grubusiness.business.web.rs.DemandDisplay;
51  import fr.paris.lutece.plugins.grubusiness.business.web.rs.DemandResult;
52  import fr.paris.lutece.plugins.grubusiness.business.web.rs.EnumGenericStatus;
53  import fr.paris.lutece.plugins.identitystore.business.contract.ServiceContract;
54  import fr.paris.lutece.plugins.identitystore.business.identity.Identity;
55  import fr.paris.lutece.plugins.identitystore.business.identity.IdentityHome;
56  import fr.paris.lutece.plugins.identitystore.service.contract.ServiceContractService;
57  import fr.paris.lutece.plugins.identitystore.service.identity.IdentityService;
58  import fr.paris.lutece.plugins.identitystore.service.listeners.IdentityStoreNotifyListenerService;
59  import fr.paris.lutece.plugins.identitystore.service.user.InternalUserService;
60  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.RequestAuthor;
61  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.history.IdentityChangeType;
62  import fr.paris.lutece.plugins.notificationstore.v1.web.service.NotificationStoreService;
63  import fr.paris.lutece.portal.service.security.AccessLogService;
64  import fr.paris.lutece.portal.service.security.AccessLoggerConstants;
65  import fr.paris.lutece.portal.service.spring.SpringContextService;
66  import fr.paris.lutece.portal.service.util.AppLogService;
67  import fr.paris.lutece.portal.service.util.AppPropertiesService;
68  
69  public final class PurgeIdentityService
70  {
71      private static PurgeIdentityService _instance;
72  
73      private final NotificationStoreService _notificationStoreService = SpringContextService.getBean( "notificationStore.notificationStoreService" );
74     
75      public static PurgeIdentityService getInstance( )
76      {
77          if ( _instance == null )
78          {
79              _instance = new PurgeIdentityService( );
80          }
81          return _instance;
82      }
83  
84      /**
85       * purge identities
86       *
87       * @return log {@link StringBuilder}
88       */
89      public String purge( final RequestAuthor daemonAuthor, final String daemonClientCode, final List<String> excludedAppCodes, final int batchLimit )
90      {
91          final StringBuilder msg = new StringBuilder( );
92  
93          // search identities with a passed peremption date, not merged to a primary identity, and not associated to a MonParis account
94          final List<Identity> expiredIdentities = IdentityHome.findExpiredNotMergedAndNotConnectedIdentities( 
95          	batchLimit, AppPropertiesService.getPropertyBoolean ( "daemon.purgeIdentityDaemon.withGuidOnly", true) );
96          final Timestamp now = Timestamp.from( Instant.now( ) );
97  
98          msg.append( expiredIdentities.size( ) ).append( " expired identities found" ).append( "\n" );
99  
100         for ( final Identity expiredIdentity : expiredIdentities )
101         {
102             try
103             {
104                 final List<Identity> mergedIdentities = IdentityHome.findMergedIdentities( expiredIdentity.getId( ) );
105                 
106                 // Get Demands notifications associated to each identity or its merged ones
107                 DemandResult demandResult = _notificationStoreService.getListDemand( expiredIdentity.getCustomerId( ), null, null, null, null );
108                 final List<DemandDisplay> demandDisplayList = demandResult.getListDemandDisplay() == null ? new ArrayList<>() : new ArrayList<>(demandResult.getListDemandDisplay());
109                 for ( final Identity mergedIdentity : mergedIdentities )
110                 {
111                     demandDisplayList
112                             .addAll( _notificationStoreService.getListDemand( mergedIdentity.getCustomerId( ), null, null, null, null ).getListDemandDisplay( ) );
113                 }
114 
115                 boolean isAtLeastOneServiceContractFound = false;
116                 boolean isInfosAreMissing = false;
117                 
118                 // Calculate the new expiration date (date of demand last update + CGUs term)
119                 Timestamp demandExpirationDateMAX = expiredIdentity.getExpirationDate( );
120                 for ( final DemandDisplay demand : demandDisplayList )
121                 {
122                     // ignore canceled demands
123                     if (demand.getDemand().getStatusId() == EnumGenericStatus.CANCELED.getStatusId()) {
124                         continue;
125                     }
126                     
127                     final String appCode = getAppCodeFromDemandTypeId( demand.getDemand( ).getTypeId( ) );
128                     if ( appCode == null )
129                     {
130                 	msg.append( "Unknown AppCode for demand_type_id : ")
131                 		.append ( (demand.getDemand( ).getTypeId( )!=null?demand.getDemand( ).getTypeId( ):"") )
132                 		.append ( "\n" );
133                 	AppLogService.info( "IdentityPurgDaemon / Unknown AppCode for demand_type_id : "  
134                 		+ (demand.getDemand( ).getTypeId( )!=null?demand.getDemand( ).getTypeId( ):"") );
135 
136                 	isInfosAreMissing = true;
137                     }
138                     
139                     if ( !excludedAppCodes.contains( appCode ) )
140                     {
141                         final List<String> clientCodeList = ServiceContractService.instance( ).getClientCodesFromAppCode( appCode );
142 
143                         int nbMonthsCGUsMAX = 0;
144                         for ( final String clientCode : clientCodeList )
145                         {
146                             // if there is more than one client code for the app_code, keep the max value of cgus
147                             Timestamp tscreate = new Timestamp( demand.getDemand( ).getCreationDate( ) );
148                             Date demandCreationDate = Date.valueOf( tscreate.toLocalDateTime( ).toLocalDate( ) );
149                             
150                             ServiceContract sc = ServiceContractService.instance( ).getActiveServiceContractAtSpecificDate( clientCode, demandCreationDate );
151                             if ( sc == null )
152                             {
153                         	sc = ServiceContractService.instance( ).getActiveServiceContract( clientCode );
154                             }
155                             
156                             if ( sc != null )
157                             {
158                         	isAtLeastOneServiceContractFound = true;
159                                 if ( sc.getDataRetentionPeriodInMonths( ) > nbMonthsCGUsMAX )
160                                 {
161                                     nbMonthsCGUsMAX = sc.getDataRetentionPeriodInMonths( );
162                                 }
163                             }
164                             else
165                             {
166                         	isInfosAreMissing = true;
167                             }
168                         }
169 
170                         final ZonedDateTime demandDate = ZonedDateTime.ofInstant( Instant.ofEpochMilli( demand.getDemand( ).getModifyDate( ) ),
171                                 ZoneId.systemDefault( ) );
172                         final Timestamp expirationDateFromDemand = Timestamp.from( demandDate.plusMonths( nbMonthsCGUsMAX ).toInstant( ) );
173 
174                         // keep the max expiration date
175                         if ( demandExpirationDateMAX.before( expirationDateFromDemand ) )
176                         {
177                             demandExpirationDateMAX = expirationDateFromDemand;
178                         }
179                     }
180                 }
181 
182                 if ( !isAtLeastOneServiceContractFound )
183                 {
184                     msg.append ( "No service contact found for identity [").append( expiredIdentity.getCustomerId( ) ).append ( "]\n");
185                     // do not delete whithout infos
186                     continue;
187                 }
188                 
189                 // check if expiredIdentity should be preserved or can be deleted
190                 if ( demandExpirationDateMAX.after( now ) )
191                 {
192                     // expiration date calculated from most recent demand is later than today
193                     // => update the expiration date of expiredIdentity : it will be deleted later
194                     expiredIdentity.setExpirationDate( demandExpirationDateMAX );
195                     IdentityHome.update( expiredIdentity );
196 
197                     // re-index and add history
198                     IdentityStoreNotifyListenerService.instance( ).notifyListenersIdentityChange( IdentityChangeType.UPDATE, expiredIdentity,
199                             "EXPIRATION_POSTPONED", StringUtils.EMPTY, daemonAuthor, daemonClientCode, Collections.emptyMap( ) );
200                     AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, UPDATE_IDENTITY_EVENT_CODE,
201                             InternalUserService.getInstance( ).getApiUser( daemonAuthor, daemonClientCode ), null, "PURGE_DAEMON" );
202 
203                     msg.append( "Identity expiration date updated : [" ).append( expiredIdentity.getCustomerId( ) ).append( "]" ).append( "\n" );
204                 }
205                 else if ( isInfosAreMissing )
206                 {
207                     // not enough infos to delete identity
208                     msg.append( "Unsufficient infos to delete [" ).append( expiredIdentity.getCustomerId( ) ).append( "]" ).append( "\n" );
209                 }
210                 else
211                 {
212                     // if the updated peremption date still passed, delete the identity (and children as merged identities,
213                     // suspicious, attributes and attributes history, etc ...) EXCEPT the identity history
214                     IdentityService.instance( ).delete( expiredIdentity.getCustomerId( ) );
215                     msg.append( "Detete Identity [" ).append( expiredIdentity.getCustomerId( ) ).append( "]" ).append( "\n" );
216 
217                     // delete notifications
218                     _notificationStoreService.deleteNotificationByCuid( expiredIdentity.getCustomerId( ) );
219                     msg.append( "Notifications deleted for main identity [" ).append( expiredIdentity.getCustomerId( ) ).append( "]" ).append( "\n" );
220                     for ( final Identity mergedIdentity : mergedIdentities )
221                     {
222                         _notificationStoreService.deleteNotificationByCuid( mergedIdentity.getCustomerId( ) );
223                         msg.append( "Notifications deleted for merged identity [" ).append( mergedIdentity.getCustomerId( ) ).append( "]" ).append( "\n" );
224                     }
225                 }
226             }
227             catch( final Exception e )
228             {
229                 msg.append( "Daemon execution error for identity : " ).append( expiredIdentity.getCustomerId( ) ).append( " :: " ).append( e.getMessage( ) )
230                         .append( "\n" );
231                 return msg.toString( );
232             }
233         }
234 
235         // return message for daemons
236         return msg.toString( );
237     }
238 
239     /**
240      * get app code
241      *
242      * @param strTypeId
243      *            the type id
244      * @return the app code
245      */
246     private String getAppCodeFromDemandTypeId( final String strTypeId )
247     {
248         DemandType demandType = _notificationStoreService.getDemandType( strTypeId );
249 
250         if ( demandType != null )
251         {
252             return demandType.getAppCode( );
253         }
254         
255         return null;
256     }
257 
258 }