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.web;
35  
36  import com.fasterxml.jackson.databind.DeserializationFeature;
37  import com.fasterxml.jackson.databind.ObjectMapper;
38  import com.fasterxml.jackson.databind.SerializationFeature;
39  
40  import fr.paris.lutece.plugins.grubusiness.business.notification.Notification;
41  import fr.paris.lutece.plugins.grubusiness.business.web.rs.DemandDisplay;
42  import fr.paris.lutece.plugins.grubusiness.business.web.rs.DemandResult;
43  import fr.paris.lutece.plugins.grubusiness.business.web.rs.EnumGenericStatus;
44  import fr.paris.lutece.plugins.grubusiness.service.notification.NotificationException;
45  import fr.paris.lutece.plugins.identitystore.business.duplicates.suspicions.SuspiciousIdentityHome;
46  import fr.paris.lutece.plugins.identitystore.business.identity.Identity;
47  import fr.paris.lutece.plugins.identitystore.business.identity.IdentityAttribute;
48  import fr.paris.lutece.plugins.identitystore.business.identity.IdentityAttributeHome;
49  import fr.paris.lutece.plugins.identitystore.business.identity.IdentityConstants;
50  import fr.paris.lutece.plugins.identitystore.business.identity.IdentityHome;
51  import fr.paris.lutece.plugins.identitystore.service.IdentityManagementResourceIdService;
52  import fr.paris.lutece.plugins.identitystore.service.PurgeIdentityService;
53  import fr.paris.lutece.plugins.identitystore.service.identity.IdentityService;
54  import fr.paris.lutece.plugins.identitystore.service.search.ISearchIdentityService;
55  import fr.paris.lutece.plugins.identitystore.utils.Batch;
56  import fr.paris.lutece.plugins.identitystore.v3.csv.CsvIdentityService;
57  import fr.paris.lutece.plugins.identitystore.v3.web.rs.DtoConverter;
58  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeTreatmentType;
59  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.BatchDto;
60  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.IdentityDto;
61  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.history.AttributeChange;
62  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.history.IdentityChange;
63  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.importing.BatchImportRequest;
64  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.SearchAttribute;
65  import fr.paris.lutece.plugins.identitystore.v3.web.rs.util.Constants;
66  import fr.paris.lutece.plugins.identitystore.web.exception.ClientAuthorizationException;
67  import fr.paris.lutece.plugins.identitystore.web.exception.IdentityStoreException;
68  import fr.paris.lutece.plugins.notificationstore.v1.web.service.NotificationStoreService;
69  import fr.paris.lutece.portal.service.admin.AccessDeniedException;
70  import fr.paris.lutece.portal.service.security.AccessLogService;
71  import fr.paris.lutece.portal.service.security.AccessLoggerConstants;
72  import fr.paris.lutece.portal.service.spring.SpringContextService;
73  import fr.paris.lutece.portal.service.util.AppLogService;
74  import fr.paris.lutece.portal.service.util.AppPropertiesService;
75  import fr.paris.lutece.portal.util.mvc.admin.annotations.Controller;
76  import fr.paris.lutece.portal.util.mvc.commons.annotations.Action;
77  import fr.paris.lutece.portal.util.mvc.commons.annotations.View;
78  import fr.paris.lutece.util.http.SecurityUtil;
79  import org.apache.commons.collections.CollectionUtils;
80  import org.apache.commons.collections.MapUtils;
81  import org.apache.commons.lang3.StringUtils;
82  
83  import javax.servlet.http.HttpServletRequest;
84  import java.io.ByteArrayOutputStream;
85  import java.sql.Date;
86  import java.sql.Timestamp;
87  import java.time.Instant;
88  import java.time.LocalDate;
89  import java.util.ArrayList;
90  import java.util.Arrays;
91  import java.util.Collections;
92  import java.util.HashMap;
93  import java.util.List;
94  import java.util.Map;
95  import java.util.Objects;
96  import java.util.Optional;
97  import java.util.UUID;
98  import java.util.stream.Collectors;
99  import java.util.zip.ZipEntry;
100 import java.util.zip.ZipOutputStream;
101 
102 /**
103  * This class provides the user interface to manage Identity features ( manage, create, modify, remove )
104  */
105 @Controller( controllerJsp = "ManageIdentities.jsp", controllerPath = "jsp/admin/plugins/identitystore/", right = "IDENTITYSTORE_MANAGEMENT" )
106 public class IdentityJspBean extends ManageIdentitiesJspBean
107 {
108     /**
109      * 
110      */
111     private static final long serialVersionUID = 6053504380426222888L;
112 
113     // Templates
114     private static final String TEMPLATE_SEARCH_IDENTITIES = "/admin/plugins/identitystore/search_identities.html";
115     private static final String TEMPLATE_VIEW_IDENTITY = "/admin/plugins/identitystore/view_identity.html";
116     private static final String TEMPLATE_VIEW_IDENTITY_HISTORY = "/admin/plugins/identitystore/view_identity_change_history.html";
117     private static final String TEMPLATE_VIEW_IDENTITY_NOTIFICATIONS = "/admin/plugins/identitystore/view_identity_notifications.html";
118 
119     // jsp
120     private static final String JSP_MANAGE_IDENTITIES = "jsp/admin/plugins/identitystore/ManageIdentities.jsp";
121 
122     // Parameters
123     private static final String PARAMETER_ID_IDENTITY = "id";
124 
125     // Properties for page titles
126     private static final String PROPERTY_PAGE_TITLE_MANAGE_IDENTITIES = "identitystore.manage_identities.pageTitle";
127     private static final String PROPERTY_PAGE_TITLE_VIEW_IDENTITY = "identitystore.create_identity.pageTitle";
128     private static final String PROPERTY_PAGE_TITLE_VIEW_CHANGE_HISTORY = "identitystore.view_change_history.pageTitle";
129     private static final String PROPERTY_PAGE_TITLE_VIEW_NOTIFICATIONS = "identitystore.view_notifications.pageTitle";
130 
131     // Markers
132     private static final String MARK_IDENTITY_LIST = "identity_list";
133     private static final String MARK_IDENTITY = "identity";
134     private static final String MARK_ATTRIBUTES = "attributes";
135     private static final String MARK_MERGED_IDENTITIES = "merged_identities";
136     private static final String MARK_IDENTITY_IS_SUSPICIOUS = "identity_is_suspicious";
137     private static final String MARK_IDENTITY_CHANGE_LIST = "identity_change_list";
138     private static final String MARK_ATTRIBUTES_CHANGE_LIST = "attributes_change_list";
139     private static final String MARK_HAS_CREATE_ROLE = "createIdentityRole";
140     private static final String MARK_HAS_MODIFY_ROLE = "modifyIdentityRole";
141     private static final String MARK_HAS_DELETE_ROLE = "deleteIdentityRole";
142     private static final String MARK_HAS_VIEW_ROLE = "viewIdentityRole";
143     private static final String MARK_HAS_ATTRIBUTS_HISTO_ROLE = "histoAttributsRole";
144     private static final String MARK_IDENTITY_NOTIFICATIONS_LIST = "demand_list";
145     private static final String MARK_GENERIC_STATUS = "generic_status_list";
146     private static final String MARK_INFOS_ARE_MISSING = "infos_are_missing";
147     private static final String MARK_AT_LEAST_ONE_SC_FOUND = "at_least_one_service_contract_found";
148 
149     // Views
150     private static final String VIEW_MANAGE_IDENTITIES = "manageIdentitys";
151     private static final String VIEW_IDENTITY = "viewIdentity";
152     private static final String VIEW_IDENTITY_HISTORY = "viewIdentityHistory";
153     private static final String VIEW_IDENTITY_NOTIFICATIONS = "viewIdentityNotifications";
154 
155     // Actions
156     private static final String ACTION_EXPORT_IDENTITIES = "exportIdentities";
157     private static final String ACTION_BATCH_GENERATE_REQUESTS = "exportRequestIdentities";
158 
159     // Events
160     private static final String DISPLAY_IDENTITY_EVENT_CODE = "DISPLAY_IDENTITY";
161     private static final String DISPLAY_IDENTITY_HISTORY_EVENT_CODE = "DISPLAY_HISTORY_IDENTITY";
162 
163     // Datasource
164     private static final String DATASOURCE_DB = "db";
165     private static final String DATASOURCE_ES = "es";
166     private static final int BATCH_PARTITION_SIZE = AppPropertiesService.getPropertyInt( "identitystore.export.batch.size", 100 );
167     private static final int PROPERTY_MAX_NB_IDENTITY_RETURNED = AppPropertiesService.getPropertyInt("identitystore.search.maxNbIdentityReturned", 0);
168     private static final String RESOURCE_SEARCH_LINK = AppPropertiesService.getProperty("identitystore.search.resource.link", "");
169 
170     // Session variable to store working values
171     private Identity _identity;
172 
173     private final List<IdentityDto> _identities = new ArrayList<>( );
174 
175     private final ISearchIdentityService _searchIdentityServiceDB = SpringContextService.getBean( "identitystore.searchIdentityService.database" );
176     private final ISearchIdentityService _searchIdentityServiceES = SpringContextService.getBean( "identitystore.searchIdentityService.elasticsearch" );
177     private final NotificationStoreService _notificationStoreService = SpringContextService.getBean( "notificationStore.notificationStoreService" );
178 
179     private final List<String> excludedAppCodes = Arrays
180 	    .asList( AppPropertiesService.getProperty( "daemon.purgeIdentityDaemon.excluded.app.codes", "" ).split( "," ) );
181 
182     @View( value = VIEW_MANAGE_IDENTITIES, defaultView = true )
183     public String getManageIdentitys( HttpServletRequest request )
184     {
185 	_identity = null;
186 	_identities.clear( );
187 	final Map<String, String> queryParameters = this.getQueryParameters( request );
188 
189 	final List<SearchAttribute> atttributes = new ArrayList<>( );
190 	final String cuid = queryParameters.get( QUERY_PARAM_CUID );
191 	final String guid = queryParameters.get( QUERY_PARAM_GUID );
192 	final String insee_city = queryParameters.get( QUERY_PARAM_INSEE_CITY );
193 	final String insee_country = queryParameters.get( QUERY_PARAM_INSEE_COUNTRY );
194 	final String email = queryParameters.get( QUERY_PARAM_EMAIL );
195 	final String gender = queryParameters.get( QUERY_PARAM_GENDER );
196 	final String common_name = queryParameters.get(QUERY_PARAM_COMMON_LASTNAME);
197 	final String first_name = queryParameters.get( QUERY_PARAM_FIRST_NAME );
198 	final String birthdate = queryParameters.get( QUERY_PARAM_BIRTHDATE );
199 	final String birthplace = queryParameters.get( QUERY_PARAM_INSEE_BIRTHPLACE_LABEL );
200 	final String birthcountry = queryParameters.get( QUERY_PARAM_INSEE_BIRTHCOUNTRY_LABEL );
201 	final String phone = queryParameters.get( QUERY_PARAM_PHONE );
202 	final String datasource = Optional.ofNullable( queryParameters.get( QUERY_PARAM_DATASOURCE ) ).orElse( DATASOURCE_DB );
203 
204 	try
205 	{
206 	    if ( StringUtils.isNotEmpty( cuid ) )
207 	    {
208 		if ( datasource.equals( DATASOURCE_DB ) )
209 		{
210 		    final Identity identity = IdentityHome.findMasterIdentityByCustomerId( cuid );
211 		    if ( identity != null )
212 		    {
213 			final IdentityDto qualifiedIdentity = DtoConverter.convertIdentityToDto( identity );
214 			_identities.add( qualifiedIdentity );
215 		    }
216 		}
217 		else
218 		    if ( datasource.equals( DATASOURCE_ES ) )
219 		    {
220 			_identities.addAll( _searchIdentityServiceES.getQualifiedIdentities( cuid, Collections.emptyList() )
221 				.getQualifiedIdentities( ) );
222 		    }
223 	    }
224 	    else
225 	    {
226 		if ( StringUtils.isNotEmpty( guid ) )
227 		{
228 		    if ( datasource.equals( DATASOURCE_DB ) )
229 		    {
230 			final Identity identity = IdentityHome.findMasterIdentityByConnectionId( guid );
231 			if ( identity != null )
232 			{
233 			    final IdentityDto qualifiedIdentity = DtoConverter.convertIdentityToDto( identity );
234 			    _identities.add( qualifiedIdentity );
235 			}
236 		    }
237 		    else
238 			if ( datasource.equals( DATASOURCE_ES ) )
239 			{
240 			    _identities.addAll( _searchIdentityServiceES.getQualifiedIdentitiesByConnectionId( guid, Collections.emptyList() )
241 				    .getQualifiedIdentities( ) );
242 			}
243 		}
244 		else
245 		{
246 		    if ( StringUtils.isNotEmpty( insee_city ) )
247 		    {
248 			atttributes.add( new SearchAttribute( Constants.PARAM_BIRTH_PLACE_CODE, insee_city, AttributeTreatmentType.STRICT ) );
249 		    }
250 		    if ( StringUtils.isNotEmpty( insee_country ) )
251 		    {
252 			atttributes.add( new SearchAttribute( Constants.PARAM_BIRTH_COUNTRY_CODE, insee_country, AttributeTreatmentType.STRICT ) );
253 		    }
254 		    if ( StringUtils.isNotEmpty( email ) )
255 		    {
256 			atttributes.add( new SearchAttribute( Constants.PARAM_COMMON_EMAIL, email, AttributeTreatmentType.STRICT ) );
257 		    }
258 		    if ( StringUtils.isNotEmpty( gender ) )
259 		    {
260 			atttributes.add( new SearchAttribute( Constants.PARAM_GENDER, gender, AttributeTreatmentType.STRICT ) );
261 		    }
262 		    if ( StringUtils.isNotEmpty( common_name ) )
263 		    {
264 			atttributes.add( new SearchAttribute( Constants.PARAM_COMMON_LASTNAME, common_name, AttributeTreatmentType.APPROXIMATED ) );
265 		    }
266 		    if ( StringUtils.isNotEmpty( first_name ) )
267 		    {
268 			atttributes.add( new SearchAttribute( Constants.PARAM_FIRST_NAME, first_name, AttributeTreatmentType.APPROXIMATED ) );
269 		    }
270 		    if ( StringUtils.isNotEmpty( birthdate ) )
271 		    {
272 			atttributes.add( new SearchAttribute( Constants.PARAM_BIRTH_DATE, birthdate, AttributeTreatmentType.STRICT ) );
273 		    }
274 		    if ( StringUtils.isNotEmpty( birthplace ) )
275 		    {
276 			atttributes.add( new SearchAttribute( Constants.PARAM_BIRTH_PLACE, birthplace, AttributeTreatmentType.APPROXIMATED ) );
277 		    }
278 		    if ( StringUtils.isNotEmpty( birthcountry ) )
279 		    {
280 			atttributes.add( new SearchAttribute( Constants.PARAM_BIRTH_COUNTRY, birthcountry, AttributeTreatmentType.APPROXIMATED ) );
281 		    }
282 		    if ( StringUtils.isNotEmpty( phone ) )
283 		    {
284 			atttributes.add( new SearchAttribute( Constants.PARAM_COMMON_PHONE, phone, AttributeTreatmentType.STRICT ) );
285 		    }
286 		    if ( CollectionUtils.isNotEmpty( atttributes ) )
287 		    {
288 			if ( datasource.equals( DATASOURCE_DB ) )
289 			{
290 			    _identities.addAll( _searchIdentityServiceDB.getQualifiedIdentities( atttributes, PROPERTY_MAX_NB_IDENTITY_RETURNED, false, Collections.emptyList( ) )
291 				    .getQualifiedIdentities( ) );
292 			}
293 			else
294 			    if ( datasource.equals( DATASOURCE_ES ) )
295 			    {
296 				_identities.addAll( _searchIdentityServiceES.getQualifiedIdentities( atttributes, PROPERTY_MAX_NB_IDENTITY_RETURNED, false, Collections.emptyList( ) )
297 					.getQualifiedIdentities( ) );
298 			    }
299 		    }
300 		}
301 	    }
302 	}
303 	catch( Exception e )
304 	{
305 	    addError( e.getMessage( ) );
306 	    this.clearParameters( request );
307 	    return redirectView( request, VIEW_MANAGE_IDENTITIES );
308 	}
309 
310 	_identities.forEach(
311 		qualifiedIdentity -> AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, IdentityService.SEARCH_IDENTITY_EVENT_CODE,
312 			getUser( ), SecurityUtil.logForgingProtect( qualifiedIdentity.getCustomerId( ) ), IdentityService.SPECIFIC_ORIGIN ) );
313 
314 	final Map<String, Object> model = getPaginatedListModel( request, MARK_IDENTITY_LIST, _identities, JSP_MANAGE_IDENTITIES );
315 	model.put( MARK_HAS_CREATE_ROLE,
316 		IdentityManagementResourceIdService.isAuthorized( IdentityManagementResourceIdService.PERMISSION_CREATE_IDENTITY, getUser( ) ) );
317 	model.put( MARK_HAS_MODIFY_ROLE,
318 		IdentityManagementResourceIdService.isAuthorized( IdentityManagementResourceIdService.PERMISSION_MODIFY_IDENTITY, getUser( ) ) );
319 	model.put( MARK_HAS_DELETE_ROLE,
320 		IdentityManagementResourceIdService.isAuthorized( IdentityManagementResourceIdService.PERMISSION_DELETE_IDENTITY, getUser( ) ) );
321 	model.put( MARK_HAS_VIEW_ROLE,
322 		IdentityManagementResourceIdService.isAuthorized( IdentityManagementResourceIdService.PERMISSION_VIEW_IDENTITY, getUser( ) ) );
323 
324 	model.put( QUERY_PARAM_CUID, cuid );
325 	model.put( QUERY_PARAM_GUID, guid );
326 	model.put( QUERY_PARAM_INSEE_CITY, insee_city );
327 	model.put( QUERY_PARAM_INSEE_COUNTRY, insee_country );
328 	model.put( QUERY_PARAM_COMMON_LASTNAME, common_name );
329 	model.put( QUERY_PARAM_FIRST_NAME, first_name );
330 	model.put( QUERY_PARAM_EMAIL, email );
331 	model.put( QUERY_PARAM_INSEE_BIRTHPLACE_LABEL, birthplace );
332 	model.put( QUERY_PARAM_INSEE_BIRTHCOUNTRY_LABEL, birthcountry );
333 	model.put( QUERY_PARAM_PHONE, phone );
334 	model.put( QUERY_PARAM_BIRTHDATE, birthdate );
335 	model.put( QUERY_PARAM_GENDER, gender );
336 	model.put( QUERY_PARAM_DATASOURCE, datasource );
337 
338 	return getPage( PROPERTY_PAGE_TITLE_MANAGE_IDENTITIES, TEMPLATE_SEARCH_IDENTITIES, model );
339     }
340 
341     /**
342      * view identity
343      *
344      * @param request
345      *            http request
346      * @return The HTML form to view info
347      */
348      @View( VIEW_IDENTITY )
349     public String getViewIdentity( HttpServletRequest request )
350      {
351 	 final String nId = request.getParameter( PARAMETER_ID_IDENTITY );
352 
353 	 _identity = IdentityHome.findByCustomerId( nId );
354 
355 	 List<Identity> mergedIdentities = IdentityHome.findMergedIdentities(_identity.getId());
356 	 _identity.setMerged(mergedIdentities != null && !mergedIdentities.isEmpty());
357 
358 	 final String filteredCustomerId = SecurityUtil.logForgingProtect( _identity.getCustomerId( ) );
359 	 AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, DISPLAY_IDENTITY_EVENT_CODE, getUser( ), filteredCustomerId,
360 		 IdentityService.SPECIFIC_ORIGIN );
361 
362 	 List<IdentityAttribute> attributes = sortIdentityttributes( );
363 
364 	 final Map<String, Object> model = getModel( );
365 	 model.put( MARK_IDENTITY, _identity );
366 	 model.put( MARK_ATTRIBUTES, attributes );
367 	 model.put( MARK_MERGED_IDENTITIES, mergedIdentities );
368 	 model.put( MARK_IDENTITY_IS_SUSPICIOUS, SuspiciousIdentityHome.hasSuspicious( Collections.singletonList( _identity.getCustomerId( ) ) ) );
369 	 model.put( MARK_HAS_ATTRIBUTS_HISTO_ROLE,
370 		 IdentityManagementResourceIdService.isAuthorized( IdentityManagementResourceIdService.PERMISSION_ATTRIBUTS_HISTO, getUser( ) ) );
371 	 model.put( QUERY_PARAM_CUID_LINK, RESOURCE_SEARCH_LINK );
372 
373 	 return getPage( PROPERTY_PAGE_TITLE_VIEW_IDENTITY, TEMPLATE_VIEW_IDENTITY, model );
374      }
375 
376      /**
377       * Build the attribute history View
378       *
379       * @param request
380       *            The HTTP request
381       * @return The page
382       */
383      @View( value = VIEW_IDENTITY_HISTORY )
384      public String getIdentityHistoryView( HttpServletRequest request )
385      {
386 	 final String CUID = request.getParameter( PARAMETER_ID_IDENTITY );
387 
388 	 if ( CUID != null && ( _identity == null || !_identity.getCustomerId( ).equals( CUID ) ) )
389 	 {
390 	     _identity = IdentityHome.findByCustomerId( CUID );
391 	 }
392 
393 	 // here we use a LinkedHashMap to have same attributs order as in viewIdentity
394 	 final List<AttributeChange> attributeChangeList = new ArrayList<>( );
395 	 final List<IdentityChange> identityChangeList = new ArrayList<>( );
396 
397 	 if ( _identity != null && MapUtils.isNotEmpty( _identity.getAttributes( ) ) )
398 	 {
399 	     try
400 	     {
401 		 attributeChangeList.addAll( IdentityAttributeHome.getAttributeChangeHistory( _identity.getId( ) ) );
402 		 identityChangeList.addAll( IdentityHome.findHistoryByCustomerId( _identity.getCustomerId( ) ) );
403 	     }
404 	     catch( IdentityStoreException e )
405 	     {
406 		 addError( e.getMessage( ) );
407 		 return getViewIdentity( request );
408 	     }
409 	 }
410 	 if ( _identity != null )
411 	 {
412 	     final String filteredCustomerId = SecurityUtil.logForgingProtect( _identity.getCustomerId( ) );
413 	     AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, DISPLAY_IDENTITY_HISTORY_EVENT_CODE, getUser( ), filteredCustomerId,
414 		     IdentityService.SPECIFIC_ORIGIN );
415 	 }
416 
417 	 final Map<String, Object> model = getModel( );
418 	 model.put( MARK_IDENTITY_CHANGE_LIST, identityChangeList );
419 	 model.put( MARK_ATTRIBUTES_CHANGE_LIST, attributeChangeList );
420 
421 	 return getPage( PROPERTY_PAGE_TITLE_VIEW_CHANGE_HISTORY, TEMPLATE_VIEW_IDENTITY_HISTORY, model );
422      }
423 
424      /**
425       * Build the attribute history View
426       *
427       * @param request
428       *            The HTTP request
429       * @return The page
430       * @throws NotificationException 
431       */
432      @View( value = VIEW_IDENTITY_NOTIFICATIONS )
433      public String getIdentityNotificationsView( HttpServletRequest request )
434      {
435 	 final String CUID = request.getParameter( PARAMETER_ID_IDENTITY );
436 
437 	 if ( CUID != null && ( _identity == null || !_identity.getCustomerId( ).equals( CUID ) ) )
438 	 {
439 	     _identity = IdentityHome.findByCustomerId( CUID );
440 	 }
441 
442 	 List<DemandDisplay> demandDisplayList = new ArrayList<>( );
443 	 Map<String,Boolean> indicators = new HashMap<>( );
444 	 indicators.put( PurgeIdentityService.KEY_AT_LEAST_ONE_CS_FOUND, false);
445 	 indicators.put( PurgeIdentityService.KEY_INFOS_ARE_MISSING, false);
446 	 StringBuilder msg = new StringBuilder( );
447 
448 	 if ( _identity != null )
449 	 {
450 	     final List<Identity> mergedIdentities = IdentityHome.findMergedIdentities( _identity.getId( ) );
451 
452 	     // Get Demands notifications associated to each identity or its merged ones
453 	     DemandResult demandResult;
454 	     try
455 	     {
456 		 demandResult = _notificationStoreService.getListDemand( _identity.getCustomerId( ), null, null, null, null );
457 
458 		 demandDisplayList = demandResult.getListDemandDisplay() == null ? new ArrayList<>() : new ArrayList<>(demandResult.getListDemandDisplay());
459 		 for ( final Identity mergedIdentity : mergedIdentities )
460 		 {
461 		     List<DemandDisplay> mergedIdsDemands = _notificationStoreService.getListDemand( mergedIdentity.getCustomerId( ), null, null, null, null ).getListDemandDisplay( );
462 		     if ( mergedIdsDemands != null )
463 		     {
464 			 demandDisplayList.addAll( mergedIdsDemands );                  
465 		     }
466 		 }
467 
468 		 for ( final DemandDisplay demand : demandDisplayList )
469 		 { 
470 		     final Timestamp expirationDateFromDemand = PurgeIdentityService.checkExpirationDateByDemand( demand, 
471 			     indicators, excludedAppCodes, msg, _notificationStoreService);
472 
473 		     demand.setRegulatoryDateForIdentityRetention( expirationDateFromDemand );
474 		     demand.setAppCode( PurgeIdentityService.getAppCodeFromDemandTypeId( 
475 			     demand.getDemand( ).getTypeId( ), _notificationStoreService ) );   
476 		 }
477 
478 	     } 
479 	     catch ( NotificationException | ClientAuthorizationException e )
480 	     {
481 		 AppLogService.error( "Error on Notification request", e );
482 		 addError( "Error on Notification request : " + e.getMessage( ) );
483 	     }
484 	 }
485 
486 	 if ( msg.length( ) > 0 )
487 	 {
488 	     addWarning( msg.toString( ) );
489 	 }
490 
491 	 final Map<String, Object> model = getModel( );
492 	 model.put( MARK_IDENTITY_NOTIFICATIONS_LIST, demandDisplayList );
493 	 model.put( MARK_GENERIC_STATUS, EnumGenericStatus.getGenericStatusAsMap( ) );
494 	 model.put( MARK_AT_LEAST_ONE_SC_FOUND, indicators.get( PurgeIdentityService.KEY_AT_LEAST_ONE_CS_FOUND ) );
495 	 model.put( MARK_INFOS_ARE_MISSING, indicators.get( PurgeIdentityService.KEY_INFOS_ARE_MISSING ) );
496 
497 	 return getPage( PROPERTY_PAGE_TITLE_VIEW_NOTIFICATIONS, TEMPLATE_VIEW_IDENTITY_NOTIFICATIONS, model );
498      }
499 
500      /**
501       * Process the data capture form of a new suspiciousidentity
502       *
503       * @param request
504       *            The Http Request
505       * @return The Jsp URL of the process result
506       * @throws AccessDeniedException
507       */
508      @Action( ACTION_EXPORT_IDENTITIES )
509      public void doExportIdentities( HttpServletRequest request )
510      {
511 	 try
512 	 {
513 	     final List<IdentityDto> identitiesToProcess = _identities.stream( ).filter( this::validateMinimumAttributes )
514 		     .peek( identityDto -> identityDto.setExternalCustomerId( UUID.randomUUID( ).toString( ) ) ).collect( Collectors.toList( ) );
515 	     final Batch<IdentityDto> batches = Batch.ofSize( identitiesToProcess, BATCH_PARTITION_SIZE );
516 
517 	     final ByteArrayOutputStream outputStream = new ByteArrayOutputStream( );
518 	     final ZipOutputStream zipOut = new ZipOutputStream( outputStream );
519 
520 	     int i = 0;
521 	     for ( final List<IdentityDto> batch : batches )
522 	     {
523 		 final byte [ ] bytes = CsvIdentityService.instance( ).write( batch );
524 		 final ZipEntry zipEntry = new ZipEntry( "identities-" + ++i + ".csv" );
525 		 zipEntry.setSize( bytes.length );
526 		 zipOut.putNextEntry( zipEntry );
527 		 zipOut.write( bytes );
528 	     }
529 	     zipOut.closeEntry( );
530 	     zipOut.close( );
531 	     this.download( outputStream.toByteArray( ), "identities.zip", "application/zip" );
532 	 }
533 	 catch( Exception e )
534 	 {
535 	     addError( e.getMessage( ) );
536 	     redirectView( request, VIEW_MANAGE_IDENTITIES );
537 	 }
538      }
539 
540      private boolean validateMinimumAttributes( final IdentityDto identity )
541      {
542 	 return this.checkAttributeExists( identity, Constants.PARAM_FAMILY_NAME ) && this.checkAttributeExists( identity, Constants.PARAM_FIRST_NAME )
543 		 && this.checkAttributeExists( identity, Constants.PARAM_BIRTH_DATE );
544      }
545 
546      private boolean checkAttributeExists( final IdentityDto identity, final String attributeKey )
547      {
548 	 return identity.getAttributes( ).stream( )
549 		 .anyMatch( attributeDto -> Objects.equals( attributeDto.getKey( ), attributeKey ) && StringUtils.isNotBlank( attributeDto.getValue( ) ) );
550      }
551 
552      /**
553       * Process the data capture form of a new suspiciousidentity
554       *
555       * @param request
556       *            The Http Request
557       * @return The Jsp URL of the process result
558       * @throws AccessDeniedException
559       */
560      @Action( ACTION_BATCH_GENERATE_REQUESTS )
561      public void doGenerateBatchIdentities( HttpServletRequest request )
562      {
563 	 try
564 	 {
565 	     // Prepare identities for import (clear not used params)
566 	     _identities.forEach( identityDto -> {
567 		 identityDto.setExternalCustomerId( UUID.randomUUID( ).toString( ) );
568 		 identityDto.setCustomerId( null );
569 		 identityDto.setQuality( null );
570 		 identityDto.setExpiration( null );
571 		 identityDto.setMerge( null );
572 		 identityDto.setLastUpdateDate( null );
573 		 identityDto.setConnectionId( null );
574 		 identityDto.setMonParisActive( null );
575 		 identityDto.setCreationDate( null );
576 		 identityDto.setDuplicateDefinition( null );
577 		 identityDto.setSuspicious( null );
578 	     } );
579 	     final Batch<IdentityDto> batches = Batch.ofSize( _identities, BATCH_PARTITION_SIZE );
580 
581 	     final ByteArrayOutputStream outputStream = new ByteArrayOutputStream( );
582 	     final ZipOutputStream zipOut = new ZipOutputStream( outputStream );
583 
584 	     int i = 0;
585 	     final String reference = UUID.randomUUID( ).toString( );
586 
587 	     final ObjectMapper mapper = new ObjectMapper( );
588 	     mapper.enable( SerializationFeature.INDENT_OUTPUT );
589 	     mapper.disable( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES );
590 
591 	     for ( final List<IdentityDto> batch : batches )
592 	     {
593 		 final BatchImportRequest batchImportRequest = new BatchImportRequest( );
594 		 batchImportRequest.setBatch( new BatchDto( ) );
595 		 batchImportRequest.getBatch( ).setReference( reference );
596 		 batchImportRequest.getBatch( ).setComment( "Batch exporté depuis identity store" );
597 		 batchImportRequest.getBatch( ).setCreationDate( Timestamp.from( Instant.now( ) ) );
598 		 batchImportRequest.getBatch( ).setUser( getUser( ).getEmail( ) );
599 		 batchImportRequest.getBatch( ).setAppCode( "TEST" );
600 		 batchImportRequest.getBatch( ).setIdentities( batch );
601 		 final ZipEntry zipEntry = new ZipEntry( "identities-" + ++i + ".json" );
602 		 final byte [ ] bytes = mapper.writeValueAsBytes( batchImportRequest );
603 		 zipEntry.setSize( bytes.length );
604 		 zipOut.putNextEntry( zipEntry );
605 		 zipOut.write( bytes );
606 	     }
607 	     zipOut.closeEntry( );
608 	     zipOut.close( );
609 	     this.download( outputStream.toByteArray( ), "identity_requests.zip", "application/zip" );
610 	 }
611 	 catch( Exception e )
612 	 {
613 	     addError( e.getMessage( ) );
614 	     redirectView( request, VIEW_MANAGE_IDENTITIES );
615 	 }
616      }
617 
618      private List<IdentityAttribute> sortIdentityttributes( )
619      {
620 	 if ( _identity != null )
621 	 {
622 	     final List<String> _sortedAttributeKeyList = Arrays.asList( AppPropertiesService.getProperty(IdentityConstants.PROPERTY_IDENTITY_ATTRIBUTE_ORDER, "" ).split( "," ) );
623 	     List<IdentityAttribute> valueList = new ArrayList<>(_identity.getAttributes( ).values());
624 	     valueList.sort( ( a1, a2 ) -> {
625 		 final int index1 = _sortedAttributeKeyList.indexOf( a1.getAttributeKey( ).getKeyName() );
626 		 final int index2 = _sortedAttributeKeyList.indexOf( a2.getAttributeKey( ).getKeyName() );
627 		 final int i1 = index1 == -1 ? 999 : index1;
628 		 final int i2 = index2 == -1 ? 999 : index2;
629 		 if ( i1 == i2 )
630 		 {
631 		     return a1.getAttributeKey( ).getKeyName().compareTo( a2.getAttributeKey( ).getKeyName() );
632 		 }
633 		 return Integer.compare( i1, i2 );
634 	     } );
635 	     return valueList;
636 	 }
637 	 return null;
638      }
639 }