1313package  org .assertj .core .api .recursive .comparison ;
1414
1515import  static  java .lang .String .format ;
16+ import  static  java .util .Arrays .stream ;
1617import  static  java .util .stream .Collectors .toList ;
18+ import  static  java .util .stream .Collectors .toSet ;
1719import  static  org .assertj .core .configuration .ConfigurationProvider .CONFIGURATION_PROVIDER ;
1820import  static  org .assertj .core .internal .TypeComparators .defaultTypeComparators ;
1921import  static  org .assertj .core .util .Lists .list ;
22+ import  static  org .assertj .core .util .Lists .newArrayList ;
2023import  static  org .assertj .core .util .Strings .join ;
24+ import  static  org .assertj .core .util .introspection .PropertyOrFieldSupport .COMPARISON ;
2125
2226import  java .util .ArrayList ;
2327import  java .util .Comparator ;
2428import  java .util .LinkedHashSet ;
2529import  java .util .List ;
2630import  java .util .Map .Entry ;
2731import  java .util .Set ;
28- import  java .util .function .Predicate ;
2932import  java .util .regex .Pattern ;
3033import  java .util .stream .Stream ;
3134
3235import  org .assertj .core .api .RecursiveComparisonAssert ;
36+ import  org .assertj .core .internal .Objects ;
3337import  org .assertj .core .internal .TypeComparators ;
3438import  org .assertj .core .presentation .Representation ;
3539import  org .assertj .core .util .VisibleForTesting ;
@@ -43,6 +47,7 @@ public class RecursiveComparisonConfiguration {
4347 private  boolean  ignoreAllActualNullFields  = false ;
4448 private  Set <FieldLocation > ignoredFields  = new  LinkedHashSet <>();
4549 private  List <Pattern > ignoredFieldsRegexes  = new  ArrayList <>();
50+  private  Set <Class <?>> ignoredTypes  = new  LinkedHashSet <>();
4651
4752 // overridden equals method to ignore section 
4853 private  List <Class <?>> ignoredOverriddenEqualsForTypes  = new  ArrayList <>();
@@ -132,6 +137,33 @@ public void ignoreFieldsMatchingRegexes(String... regexes) {
132137 .collect (toList ()));
133138 }
134139
140+  /** 
141+  * Adds the given types to the list of the object under test fields types to ignore in the recursive comparison. 
142+  * The fields are ignored if their types exactly match one of the ignored types, if a field is a subtype of an ignored type it won't be ignored. 
143+  * <p> 
144+  * Note that if some object under test fields are null, they are not ignored by this method as their type can't be evaluated. 
145+  * <p> 
146+  * See {@link RecursiveComparisonAssert#ignoringFields(String...) RecursiveComparisonAssert#ignoringFields(String...)} for examples. 
147+  * 
148+  * @param types the types of the object under test to ignore in the comparison. 
149+  */ 
150+  public  void  ignoreFieldsOfTypes (Class <?>... types ) {
151+  stream (types ).map (type  -> asWrapperIfPrimitiveType (type )).forEach (ignoredTypes ::add );
152+  }
153+ 
154+  private  static  Class <?> asWrapperIfPrimitiveType (Class <?> type ) {
155+  if  (!type .isPrimitive ()) return  type ;
156+  if  (type .equals (boolean .class )) return  Boolean .class ;
157+  if  (type .equals (byte .class )) return  Byte .class ;
158+  if  (type .equals (int .class )) return  Integer .class ;
159+  if  (type .equals (short .class )) return  Short .class ;
160+  if  (type .equals (char .class )) return  Character .class ;
161+  if  (type .equals (float .class )) return  Float .class ;
162+  if  (type .equals (double .class )) return  Double .class ;
163+  // should not arrive here since we have tested primitive types first 
164+  return  type ;
165+  }
166+ 
135167 /** 
136168 * Returns the list of the object under test fields to ignore in the recursive comparison. 
137169 * 
@@ -141,6 +173,15 @@ public Set<FieldLocation> getIgnoredFields() {
141173 return  ignoredFields ;
142174 }
143175
176+  /** 
177+  * Returns the set of the object under test fields types to ignore in the recursive comparison. 
178+  * 
179+  * @return the set of the object under test fields types to ignore in the recursive comparison. 
180+  */ 
181+  public  Set <Class <?>> getIgnoredTypes () {
182+  return  ignoredTypes ;
183+  }
184+ 
144185 /** 
145186 * Force a recursive comparison on all fields (except java types). 
146187 * <p> 
@@ -324,6 +365,7 @@ public String multiLineDescription(Representation representation) {
324365 describeIgnoreAllActualNullFields (description );
325366 describeIgnoredFields (description );
326367 describeIgnoredFieldsRegexes (description );
368+  describeIgnoredFieldsForTypes (description );
327369 describeOverriddenEqualsMethodsUsage (description , representation );
328370 describeIgnoreCollectionOrder (description );
329371 describeIgnoredCollectionOrderInFields (description );
@@ -334,25 +376,58 @@ public String multiLineDescription(Representation representation) {
334376 return  description .toString ();
335377 }
336378
337-  // non public stuff 
338- 
339-  boolean  shouldIgnore (DualValue  dualKey ) {
340-  return  matchesAnIgnoredNullField (dualKey )
341-  || matchesAnIgnoredField (dualKey )
342-  || matchesAnIgnoredFieldRegex (dualKey );
379+  boolean  shouldIgnore (DualValue  dualValue ) {
380+  String  concatenatedPath  = dualValue .concatenatedPath ;
381+  return  matchesAnIgnoredField (concatenatedPath )
382+  || matchesAnIgnoredFieldRegex (concatenatedPath )
383+  || shouldIgnoreNotEvalutingFieldName (dualValue );
343384 }
344385
345-  Predicate <String > shouldKeepField (String  parentConcatenatedPath ) {
346-  return  fieldName  -> shouldKeepField (parentConcatenatedPath , fieldName );
386+  Set <String > getNonIgnoredActualFieldNames (DualValue  dualValue ) {
387+  Set <String > actualFieldsNames  = Objects .getFieldsNames (dualValue .actual .getClass ());
388+  // we are doing the same as shouldIgnore(DualValue dualKey) but in two steps for performance reasons: 
389+  // - we filter first ignored field by names that don't need building DualValues 
390+  // - then we filter field DualValues with the remaining criteria (shouldIgnoreNotEvalutingFieldName) 
391+  // DualValuea are built introspecting fields which is expensive. 
392+  return  actualFieldsNames .stream ()
393+  // evaluate field name ignoring criteria 
394+  .filter (fieldName  -> !shouldIgnore (dualValue .path , fieldName ))
395+  .map (fieldName  -> dualValueForField (dualValue , fieldName ))
396+  // evaluate field value ignoring criteria 
397+  .filter (fieldDualValue  -> !shouldIgnoreNotEvalutingFieldName (fieldDualValue ))
398+  // back to field name 
399+  .map (DualValue ::getFieldName )
400+  .filter (fieldName  -> !fieldName .isEmpty ())
401+  .collect (toSet ());
347402 }
348403
349-  private  boolean  shouldKeepField (String  parentPath , String  fieldName ) {
350-  String  fieldConcatenatedPath  = concatenatedPath (parentPath , fieldName );
351-  return  !matchesAnIgnoredField (fieldConcatenatedPath ) && !matchesAnIgnoredFieldRegex (fieldConcatenatedPath );
352-  }
404+  // non public stuff 
353405
354-  private  static  String  concatenatedPath (String  parentPath , String  name ) {
355-  return  parentPath .isEmpty () ? name  : format ("%s.%s" , parentPath , name );
406+  private  boolean  shouldIgnoreNotEvalutingFieldName (DualValue  dualKey ) {
407+  return  matchesAnIgnoredNullField (dualKey ) || matchesAnIgnoredFieldType (dualKey );
408+  }
409+ 
410+  private  boolean  shouldIgnore (List <String > parentConcatenatedPath , String  fieldName ) {
411+  List <String > fieldConcatenatedPathList  = newArrayList (parentConcatenatedPath );
412+  fieldConcatenatedPathList .add (fieldName );
413+  String  fieldConcatenatedPath  = join (fieldConcatenatedPathList ).with ("." );
414+  return  matchesAnIgnoredField (fieldConcatenatedPath ) || matchesAnIgnoredFieldRegex (fieldConcatenatedPath );
415+  }
416+ 
417+  private  static  DualValue  dualValueForField (DualValue  parentDualValue , String  fieldName ) {
418+  List <String > path  = newArrayList (parentDualValue .path );
419+  path .add (fieldName );
420+  Object  actualFieldValue  = COMPARISON .getSimpleValue (fieldName , parentDualValue .actual );
421+  // no guarantees we have a field in expected named as fieldName 
422+  Object  expectedFieldValue ;
423+  try  {
424+  expectedFieldValue  = COMPARISON .getSimpleValue (fieldName , parentDualValue .expected );
425+  } catch  (@ SuppressWarnings ("unused" ) Exception  e ) {
426+  // set the field to null to express it is absent, this not 100% accurate as the value could be null 
427+  // but it works to evaluate if dualValue should be ignored with matchesAnIgnoredFieldType 
428+  expectedFieldValue  = null ;
429+  }
430+  return  new  DualValue (path , actualFieldValue , expectedFieldValue );
356431 }
357432
358433 boolean  hasCustomComparator (DualValue  dualValue ) {
@@ -393,6 +468,11 @@ private void describeIgnoredFields(StringBuilder description) {
393468 description .append (format ("- the following fields were ignored in the comparison: %s%n" , describeIgnoredFields ()));
394469 }
395470
471+  private  void  describeIgnoredFieldsForTypes (StringBuilder  description ) {
472+  if  (!ignoredTypes .isEmpty ())
473+  description .append (format ("- the following types were ignored in the comparison: %s%n" , describeIgnoredTypes ()));
474+  }
475+ 
396476 private  void  describeIgnoreAllActualNullFields (StringBuilder  description ) {
397477 if  (ignoreAllActualNullFields ) description .append (format ("- all actual null fields were ignored in the comparison%n" ));
398478 }
@@ -469,21 +549,24 @@ private boolean matchesAnIgnoredOverriddenEqualsField(DualValue dualKey) {
469549 .anyMatch (fieldLocation  -> fieldLocation .matches (dualKey .concatenatedPath ));
470550 }
471551
472-  private  boolean  matchesAnIgnoredNullField (DualValue  dualKey ) {
473-  return  ignoreAllActualNullFields  && dualKey .actual  == null ;
552+  private  boolean  matchesAnIgnoredNullField (DualValue  dualValue ) {
553+  return  ignoreAllActualNullFields  && dualValue .actual  == null ;
474554 }
475555
476556 private  boolean  matchesAnIgnoredFieldRegex (String  fieldConcatenatedPath ) {
477557 return  ignoredFieldsRegexes .stream ()
478558 .anyMatch (regex  -> regex .matcher (fieldConcatenatedPath ).matches ());
479559 }
480560
481-  private  boolean  matchesAnIgnoredFieldRegex (DualValue  dualKey ) {
482-  return  matchesAnIgnoredFieldRegex (dualKey .concatenatedPath );
483-  }
484- 
485-  private  boolean  matchesAnIgnoredField (DualValue  dualKey ) {
486-  return  matchesAnIgnoredField (dualKey .concatenatedPath );
561+  private  boolean  matchesAnIgnoredFieldType (DualValue  dualKey ) {
562+  Object  actual  = dualKey .actual ;
563+  if  (actual  != null ) return  ignoredTypes .contains (actual .getClass ());
564+  Object  expected  = dualKey .expected ;
565+  // actual is null => we can't evaluate its type, we can only reliably check dualKey.expected's type if 
566+  // strictTypeChecking is enabled which guarantees expected is of the same type. 
567+  if  (strictTypeChecking  && expected  != null ) return  ignoredTypes .contains (expected .getClass ());
568+  // if strictTypeChecking is disabled, we can't safely ignore the field (if we did, we would ignore all null fields!). 
569+  return  false ;
487570 }
488571
489572 private  boolean  matchesAnIgnoredField (String  fieldConcatenatedPath ) {
@@ -508,6 +591,13 @@ private String describeIgnoredFields() {
508591 return  join (fieldsDescription ).with (", " );
509592 }
510593
594+  private  String  describeIgnoredTypes () {
595+  List <String > typesDescription  = ignoredTypes .stream ()
596+  .map (Class ::getName )
597+  .collect (toList ());
598+  return  join (typesDescription ).with (", " );
599+  }
600+ 
511601 private  String  describeIgnoredCollectionOrderInFields () {
512602 List <String > fieldsDescription  = ignoredCollectionOrderInFields .stream ()
513603 .map (FieldLocation ::getFieldPath )
0 commit comments