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.attribute;
35  
36  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeChangeStatus;
37  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeStatus;
38  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.IdentityDto;
39  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.IdentityChangeRequest;
40  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.merge.IdentityMergeRequest;
41  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.IdentitySearchRequest;
42  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.SearchAttribute;
43  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.SearchDto;
44  import fr.paris.lutece.plugins.identitystore.v3.web.rs.util.Constants;
45  import org.apache.commons.lang3.StringUtils;
46  
47  import java.util.*;
48  import java.util.stream.Collectors;
49  
50  /**
51   * Service class used to format attribute values in requests
52   */
53  public class IdentityAttributeFormatterService
54  {
55  
56      private static IdentityAttributeFormatterService _instance;
57      private static final List<String> PHONE_ATTR_KEYS = Arrays.asList( "mobile_phone", "fixed_phone" );
58      private static final List<String> DATE_ATTR_KEYS = Collections.singletonList( "birthdate" );
59      private static final List<String> FIRSTNAME_ATTR_KEYS = Collections.singletonList( "first_name" );
60      private static final List<String> UPPERCASE_ATTR_KEYS = Arrays.asList( "birthcountry", "family_name", "preferred_username" );
61      private static final List<String> LOWERCASE_ATTR_KEYS = Arrays.asList( "login", "email" );
62  
63      public static IdentityAttributeFormatterService instance( )
64      {
65          if ( _instance == null )
66          {
67              _instance = new IdentityAttributeFormatterService( );
68          }
69          return _instance;
70      }
71  
72      /**
73       * Formats attribute values in the Identity contained in the provided request.
74       * 
75       * @see IdentityAttributeFormatterService#formatIdentityAttributeValues(IdentityDto)
76       * @param request
77       *            the identity change request
78       */
79      public List<AttributeStatus> formatIdentityChangeRequestAttributeValues( final IdentityChangeRequest request )
80      {
81          final IdentityDto identity = request.getIdentity( );
82          final List<AttributeStatus> statuses = this.formatIdentityAttributeValues( identity );
83          request.setIdentity( identity );
84          return statuses;
85      }
86  
87      /**
88       * Formats attribute values in the Identity contained in the provided request.
89       * 
90       * @see IdentityAttributeFormatterService#formatIdentityAttributeValues(IdentityDto)
91       * @param request
92       *            the identity merge request
93       */
94      public List<AttributeStatus> formatIdentityMergeRequestAttributeValues( final IdentityMergeRequest request )
95      {
96          final List<AttributeStatus> statuses = new ArrayList<>( );
97          final IdentityDto identity = request.getIdentity( );
98          if ( identity != null )
99          {
100             statuses.addAll( this.formatIdentityAttributeValues( identity ) );
101             request.setIdentity( identity );
102         }
103         return statuses;
104     }
105 
106     /**
107      * Formats attribute values in the provided search request.
108      *
109      * @see IdentityAttributeFormatterService#formatAttribute(String, String, List)
110      * @param request
111      *            the identity change request
112      */
113     public List<AttributeStatus> formatIdentitySearchRequestAttributeValues( final IdentitySearchRequest request )
114     {
115         final List<AttributeStatus> statuses = new ArrayList<>( );
116         final SearchDto search = request.getSearch( );
117         if ( search != null )
118         {
119             search.getAttributes( ).stream( ).filter( attributeDto -> StringUtils.isNotBlank( attributeDto.getValue( ) ) )
120                     .forEach(attribute -> attribute.setValue( this.formatAttribute( attribute.getKey(), attribute.getValue(), statuses ) ));
121         }
122         return statuses;
123     }
124 
125     /**
126      * Formats attribute values in the provided search request.
127      *
128      * @see IdentityAttributeFormatterService#formatAttribute(String, String, List)
129      * @param attributes
130      *            the searched attributes
131      */
132     public List<AttributeStatus> formatDuplicateSearchRequestAttributeValues( final Map<String, String> attributes )
133     {
134         final List<AttributeStatus> statuses = new ArrayList<>( );
135         if ( attributes != null )
136         {
137             attributes.entrySet().stream( ).filter( attributeDto -> StringUtils.isNotBlank( attributeDto.getValue( ) ) )
138                     .forEach(attribute -> attribute.setValue( this.formatAttribute( attribute.getKey(), attribute.getValue(), statuses ) ));
139         }
140         return statuses;
141     }
142 
143     /**
144      * Formats all attributes stored in the provided identity :
145      *
146      * @param identity
147      *            identity containing attributes to format
148      * @see IdentityAttributeFormatterService#formatAttribute(String, String, List)
149      * @return FORMATTED_VALUE statuses for attributes whose value has changed after the formatting.
150      */
151     private List<AttributeStatus> formatIdentityAttributeValues( final IdentityDto identity )
152     {
153         final List<AttributeStatus> statuses = new ArrayList<>( );
154         identity.getAttributes( ).stream( ).filter( attributeDto -> StringUtils.isNotBlank( attributeDto.getValue( ) ) ).forEach( attribute -> {
155             attribute.setValue( this.formatAttribute( attribute.getKey(), attribute.getValue(), statuses ) );
156         } );
157         return statuses;
158     }
159 
160     /**
161      * Formats the provided list of attributes :
162      * <ul>
163      * <li>Remove leading and trailing spaces</li>
164      * <li>Replace all blank characters by an actual space</li>
165      * <li>Replace space successions with a single space</li>
166      * <li>For phone number attributes :
167      * <ul>
168      * <li>Remove all spaces, dots, dashes and parenthesis</li>
169      * <li>Replace leading indicative part (0033 or +33) by a single zero</li>
170      * </ul>
171      * </li>
172      * <li>For date attributes :
173      * <ul>
174      * <li>Put a leading zero in day and month parts if they contain only one character</li>
175      * </ul>
176      * </li>
177      * <li>For first name attributes :
178      * <ul>
179      * <li>Replace comas (,) by a single whitespace</li>
180      * <li>Force the first character of each group (space-separated) to be uppercase, the rest is forced to lowercase</li>
181      * </ul>
182      * </li>
183      * <li>For country label, family name and prefered name attributes :
184      * <ul>
185      * <li>force to uppercase</li>
186      * </ul>
187      * </li>
188      * <li>For login and email attributes :
189      * <ul>
190      * <li>force to lowercase</li>
191      * </ul>
192      * </li>
193      * </ul>
194      *
195      * @param key the attribute key
196      * @param value the attribute value
197      * @param statuses the status list that must be filled with formatting results
198      * @return FORMATTED_VALUE statuses for attributes whose value has changed after the formatting.
199      */
200     private String formatAttribute( final String key, final String value, final List<AttributeStatus> statuses )
201     {
202         // Suppression espaces avant et après, et uniformisation des espacements (tab, space, nbsp, successions d'espaces, ...) en les remplaçant tous par
203         // un espace
204         String formattedValue = value.trim( ).replaceAll( "\\s+", " " );
205 
206         if ( PHONE_ATTR_KEYS.contains( key ) )
207         {
208             formattedValue = formatPhoneValue( formattedValue );
209         }
210         if ( DATE_ATTR_KEYS.contains( key ) )
211         {
212             formattedValue = formatDateValue( formattedValue );
213         }
214         if ( FIRSTNAME_ATTR_KEYS.contains( key ) )
215         {
216             formattedValue = formatFirstnameValue( formattedValue );
217         }
218         if ( UPPERCASE_ATTR_KEYS.contains( key ) )
219         {
220             formattedValue = StringUtils.upperCase( formattedValue );
221         }
222         if ( LOWERCASE_ATTR_KEYS.contains( key ) )
223         {
224             formattedValue = StringUtils.lowerCase( formattedValue );
225         }
226 
227         // Si la valeur a été modifiée, on renvoie un status
228         if ( !formattedValue.equals( value ) )
229         {
230             statuses.add( buildAttributeValueFormattedStatus( key, value, formattedValue ) );
231         }
232 
233         return formattedValue;
234     }
235 
236     /**
237      * <ul>
238      * <li>Remove all spaces, dots, dashes and parenthesis</li>
239      * <li>Replace leading indicative part (0033 or +33) by a single zero</li>
240      * </ul>
241      * 
242      * @param value
243      *            the value to format
244      * @return the formatted value
245      */
246     private String formatPhoneValue( final String value )
247     {
248         // Suppression des espaces, points, tirets, et parenthèses
249         String formattedValue = value.replaceAll( "\\s", "" ).replace( ".", "" ).replace( "-", "" ).replace( "(", "" ).replace( ")", "" );
250         // Remplacement de l'indicatif (0033 ou +33) par un 0
251         formattedValue = formattedValue.replaceAll( "^(0{2}|\\+)3{2}", "0" );
252 
253         return formattedValue;
254     }
255 
256     /**
257      * Put a leading zero in day and month parts if they contain only one character
258      * 
259      * @param value
260      *            the value to format
261      * @return the formatted value
262      */
263     public String formatDateValue( final String value )
264     {
265         final StringBuilder sb = new StringBuilder( );
266         final String [ ] splittedDate = value.split( "/" );
267         if ( splittedDate.length == 3 )
268         {
269             final String day = splittedDate [0];
270             if ( day.length( ) == 1 )
271             {
272                 sb.append( "0" );
273             }
274             sb.append( day ).append( "/" );
275 
276             final String month = splittedDate [1];
277             if ( month.length( ) == 1 )
278             {
279                 sb.append( "0" );
280             }
281             sb.append( month ).append( "/" ).append( splittedDate [2] );
282 
283             return sb.toString( );
284         }
285         else
286         {
287             return value;
288         }
289     }
290 
291     /**
292      * <ul>
293      * <li>Replace comas (,) by a single whitespace</li>
294      * <li>Force the first character of each group (space-separated) to be uppercase, the rest is forced to lowercase</li>
295      * </ul>
296      * 
297      * @param value
298      *            the value to format
299      * @return the formatted value
300      */
301     private String formatFirstnameValue( final String value )
302     {
303         if ( StringUtils.isBlank( value ) )
304         {
305             return value;
306         }
307         return Arrays.stream( value.replace( ",", " " ).trim( ).split( " " ) ).filter( StringUtils::isNotBlank ).map( String::trim )
308                 .map( firstname -> {
309                     if( firstname.contains("-") )
310                     {
311                         return Arrays.stream(firstname.split("-")).map( this::toFirstLetterUpperCased ).map( String::trim ).collect(Collectors.joining("-"));
312                     }
313                     else
314                     {
315                         return this.toFirstLetterUpperCased( firstname );
316                     }
317                 } ).collect( Collectors.joining( " " ) );
318     }
319 
320     private String toFirstLetterUpperCased ( final String value )
321     {
322         return !value.isEmpty() ? value.substring( 0, 1 ).toUpperCase( ) + value.substring( 1 ).toLowerCase( ) : value;
323     }
324 
325     /**
326      * Build attribute value formatted status
327      * 
328      * @param attrStrKey
329      *            the attribute key
330      * @return the status
331      */
332     public AttributeStatus buildAttributeValueFormattedStatus( final String attrStrKey, final String oldValue, final String newValue )
333     {
334         final AttributeStatus status = new AttributeStatus( );
335         status.setKey( attrStrKey );
336         status.setStatus( AttributeChangeStatus.FORMATTED_VALUE );
337         status.setMessage( "[" + oldValue + "] -> [" + newValue + "]" );
338         status.setMessageKey( Constants.PROPERTY_ATTRIBUTE_STATUS_FORMATTED_VALUE );
339         return status;
340     }
341 
342 }