DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Be Punctual! Avoiding Kotlin’s lateinit In Spring Boot Testing
  • Spring Boot: How To Use Java Persistence Query Language (JPQL)
  • Distributed Tracing System (Spring Cloud Sleuth + OpenZipkin)
  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements

Trending

  • How to Submit a Post to DZone
  • Using Python Libraries in Java
  • The Smart Way to Talk to Your Database: Why Hybrid API + NL2SQL Wins
  • How To Build Resilient Microservices Using Circuit Breakers and Retries: A Developer’s Guide To Surviving
  1. DZone
  2. Coding
  3. Frameworks
  4. How to Transform Any Type of Java Bean With BULL

How to Transform Any Type of Java Bean With BULL

BULL stands for Bean Utils Light Library, a transformer that recursively copies data from one object to another.

By 
Fabio Borriello user avatar
Fabio Borriello
·
Updated Jun. 29, 21 · Tutorial
Likes (22)
Comment
Save
Tweet
Share
39.1K Views

Join the DZone community and get the full member experience.

Join For Free

Intro

BULL (Bean Utils Light Library) is a Java-bean-to-Java-bean transformer that recursively copies data from one object to another. It is generic, flexible, reusable, configurable, and incredibly fast.
It's the only library able to transform Mutable, Immutable, and Mixed beans without any custom configuration.

This article explains how to use it, with a concrete example for each feature available.

1. Dependencies

<dependency>
    <groupId>com.hotels.beans</groupId>
    <artifactId>bull-bean-transformer</artifactId>
    <version>2.0.1.1</version>
</dependency>


 The project provides two different builds:
one compatible with jdk 8 (or above) one with jdk 11 and since version 2.0.0 supports jdk 15 or above.

The latest version available of the library can be retrieved from the README file or from CHANGELOG (in case you need a jdk 8-compatible version please refer to CHANGELOG-JDK8).

2. Features

The macro features explained in this article are:

  • Bean transformation
  • Bean validation

3. Bean Transformation

The bean transformation is performed by the Transformer object, which can be obtained executing the following instruction:

BeanTransformer transformer = new BeanUtils().getTransformer();


Once we have a BeanTransformer object instance, we can use the method transform to get our object copied into another.

The method to use is: K transform(T sourceObj, Class<K> targetObject); where the first parameter represents the source object and the second one is the destination class.

For example, given the source and destination class:

public class FromBean {                                     public class ToBean {                           
   private final String name;                                  public BigInteger id;                                  
   private final BigInteger id;                                private final String name;                             
   private final List<FromSubBean> subBeanList;                private final List<String> list;                       
   private List<String> list;                                  private final List<ImmutableToSubFoo> nestedObjectList;
   private final FromSubBean subObject;                        private ImmutableToSubFoo nestedObject;                

    // all args constructor                                     // constructors                                         
   // getters and setters...                                    // getters and setters
}                                                           }


The transformation can be obtained with the following line of code:

ToBean toBean = new BeanUtils().getTransformer().transform(fromBean, ToBean.class);


Note that the field order is not relevant.

Different Field Names Copy

Given two classes with the same number of fields but different names:

public class FromBean {                                     public class ToBean {                           

   private final String name;                                  private final String differentName;                   
   private final int id;                                       private final int id;                      
   private final List<FromSubBean> subBeanList;                private final List<ToSubBean> subBeanList;                 
   private final List<String> list;                            private final List<String> list;                    
   private final FromSubBean subObject;                        private final ToSubBean subObject;                    

   // all constructors                                         // all args constructor
   // getters...                                               // getters... 
}                                                            }


We need to define proper field mappings and pass them to theTransformer object:

// the first parameter is the field name in the source object
// the second one is the the field name in the destination one 
FieldMapping fieldMapping = new FieldMapping("name", "differentName");
Tansformer transformer = new BeanUtils().getTransformer().withFieldMapping(fieldMapping);


Then, we can perform the transformation:

ToBean toBean = transformer.transform(fromBean, ToBean.class);                                                               


Map Fields Between the Source and Destination Object

Case 1: A destination field value has to be retrieved from a nested class in the source object

Assuming that the object FromSubBean is declared as follows:

public class FromSubBean {                         

   private String serialNumber;                 
   private Date creationDate;                    

   // getters and setters... 

}

and our source class and destination class are described as follow:

public class FromBean {                                     public class ToBean {                           
   private final int id;                                       private final int id;                      
   private final String name;                                  private final String name;                   
   private final FromSubBean subObject;                        private final String serialNumber;                 
                                                               private final Date creationDate;                    

   // all args constructor                                     // all args constructor
   // getters...                                               // getters... 
}                                                           }


...and that the values for fields serialNumber and creationDate into the ToBean object needs to be retrieved from subObject, this can be done by defining the whole path to the property dot-separated:

FieldMapping serialNumberMapping = new FieldMapping("subObject.serialNumber", "serialNumber");                                                             
FieldMapping creationDateMapping = new FieldMapping("subObject.creationDate", "creationDate");

ToBean toBean = new BeanUtils().getTransformer()
                   .withFieldMapping(serialNumberMapping, creationDateMapping)
                   .transform(fromBean, ToBean.class);                                                               


Case 2: A destination field value (in a nested class) has to be retrieved from the source class root

The previous example highlighted how to get a value from a source object; this one instead explains how to put a value in a nested object.

Given:

public class FromBean {                                     public class ToBean {                           
   private final String name;                                  private final String name;                   
   private final FromSubBean nestedObject;                     private final ToSubBean nestedObject;                    
   private final int x;
   // all args constructor                                     // all args constructor
   // getters...                                               // getters...
}                                                           }


And:

public class ToSubBean {                           
   private final int x;

   // all args constructor
}  // getters...          


Assuming that the value x should be mapped into the field: With x contained in the ToSubBean object, the field mapping has to be defined as follow:

FieldMapping fieldMapping = new FieldMapping("x", "nestedObject.x");


Then, we just need to pass it to theTransformer and execute the transformation:

ToBean toBean = new BeanUtils().getTransformer()
                     .withFieldMapping(fieldMapping)
   .transform(fromBean, ToBean.class);


Different Field Names Defining Constructor Args

The mapping between different fields can also be defined by adding @ConstructorArg annotation next to constructor arguments.

The @ConstructorArg takes as input the name of the correspondent field in the source object.

public class FromBean {                                     public class ToBean {                           
   private final String name;                                  private final String differentName;                   
   private final int id;                                       private final int id;                      
   private final List<FromSubBean> subBeanList;                private final List<ToSubBean> subBeanList;                 
   private final List<String> list;                            private final List<String> list;                    
   private final FromSubBean subObject;                        private final ToSubBean subObject;                    

   // all args constructor
   // getters...
                                                               public ToBean(@ConstructorArg("name") final String differentName, 
                                                                        @ConstructorArg("id") final int id,
}                                                                       @ConstructorArg("subBeanList") final List<ToSubBean> subBeanList,
                                                                        @ConstructorArg(fieldName ="list") final List<String> list,
                                                                        @ConstructorArg("subObject") final ToSubBean subObject) {
                                                                        this.differentName = differentName;
                                                                        this.id = id;
                                                                        this.subBeanList = subBeanList;
                                                                        this.list = list;
                                                                        this.subObject = subObject; 
                                                                    }

                                                                    // getters...           

                                                            }


Then:

ToBean toBean = beanUtils.getTransformer().transform(fromBean, ToBean.class);


Apply a Custom Transformation on a Specific Field Lambda Function

We know that, in real life, it’s rare that we just need to copy information between two Java Beans almost identical, often occurs that:

  • The destination object has a totally different structure than the source object
  • We need to perform some operation on a specific field value before copying it
  • The destination object’s fields have to be validated
  • The destination object has an additional field than the source object that needs to be filled with something coming from a different source

BULL gives the possibility to perform any kind of operation on a specific field, actually taking advantage of lambda expressions, the developer can define its own method that will be applied to the a value before copying it.

Let’s explain it better with an example:

Given the following Source class:

public class FromFoo {
  private final String id;
  private final String val;
  private final List<FromSubFoo> nestedObjectList;

  // all args constructor   
  // getters
}


And the following Destination class:

public class MixedToFoo {
  public String id;

  @NotNull
  private final Double val;

  // constructors
  // getters and setters
}


And assuming that theval field needs to be multiplied by a random value in our transformer, we have two problems:

  1. Theval field has a different type than the Source object, indeed one is String and one is Double
  2. We need to instruct the library on how we would apply out math operation

Well, this is pretty simple, you just need to define your own lambda expression to do that:

FieldTransformer<String, Double> valTransformer =
     new FieldTransformer<>("val",
                      n -> Double.valueOf(n) * Math.random());


The expression will be applied to the field with the name val in the destination object.

The last step is to pass the function the Transformer instance:

MixedToFoo mixedToFoo = new BeanUtils().getTransformer()
      .withFieldTransformer(valTransformer)
      .transform(fromFoo, MixedToFoo.class);


Assign a Default Value in Case of Missing Field in the Source Object

Sometimes, this happens where the destination object has more fields than the source object; in this case, the BeanUtils library will raise an exception informing it that they cannot perform the mapping as they do not know from where the value has to be retrieved.

A typical scenario is the following:

public class FromBean {                                     public class ToBean {                           
   private final String name;                                  @NotNull                   
   private final BigInteger id;                                public BigInteger id;                      
                                                               private final String name;                 
                                                               private String notExistingField; // this will be null and no exceptions will be raised

   // constructors...                                          // constructors...
   // getters...                                               // getters and setters...

}                                                           }


However, we can configure the library in order to assign the default value for the field type (e.g.0 for int type,null for String, etc.)

ToBean toBean = new BeanUtils().getTransformer()
                      .setDefaultValueForMissingField(true)
       .transform(fromBean, ToBean.class);


Applying a Transformation Function in Case of Missing Fields in the Source Object

The example below shows how to assign a default value (or a result of lambda function) on a not existing field in the source object:

public class FromBean {                                     public class ToBean {                           
   private final String name;                                  @NotNull                   
   private final BigInteger id;                                public BigInteger id;                      
                                                               private final String name;                 
                                                               private String notExistingField; // this will have value: sampleVal

   // all args constructor                                     // constructors...
   // getters...                                               // getters and setters...
}                                                           }


What we need to do is to assign a FieldTransformer function to a specific field:

FieldTransformer<String, String> notExistingFieldTransformer =
                    new FieldTransformer<>("notExistingField", () -> "sampleVal");


The above functions will assign a fixed value to the field notExistingField, but we can return whatever, for example, we can call an external method that returns a value obtained after a set of operation, something like:

FieldTransformer<String, String> notExistingFieldTransformer =
                    new FieldTransformer<>("notExistingField", () -> calculateValue());


However, in the end, we just need to pass it to the Transformer.

ToBean toBean = new BeanUtils().getTransformer()
   .withFieldTransformer(notExistingFieldTransformer)
   .transform(fromBean, ToBean.class);


Apply a Transformation Function to a Specific Field in a Nested Object

Case 1: Lambda transformation function applied to a specific field in a nested class

Given:

public class FromBean {                                     public class ToBean {                           
   private final String name;                                  private final String name;                   
   private final FromSubBean nestedObject;                     private final ToSubBean nestedObject;                    

   // all args constructor                                     // all args constructor
   // getters...                                               // getters...
}                                                           }


And:

public class FromSubBean {                                  public class ToSubBean {                           
   private final String name;                                  private final String name;                   
   private final long index;                                   private final long index;                    

   // all args constructor                                     // all args constructor
   // getters...                                               // getters...
}                                                           }


Assuming that the lambda transformation function should be applied only to the field name contained in the ToSubBeanobject, the transformation function has to be defined as follow:

FieldTransformer<String, String> nameTransformer = new FieldTransformer<>("nestedObject.name", StringUtils::capitalize);


Then, pass the function to the Transformer object:

ToBean toBean = new BeanUtils().getTransformer()
                      .withFieldTransformer(nameTransformer)
                      .transform(fromBean, ToBean.class);


Case 2: Lambda transformation function applied to a specific field independently from its location

Imagine that in our Destination class, there are more occurrences of a field with the same name, located in different classes, and that we want to apply the same transformation function to all of them; there is a setting that allows this.

Taking, as an example, the above objects and assuming that we want to capitalize on all values contained in thename field independently from their location, we can do the following:

FieldTransformer<String, String> nameTransformer = new FieldTransformer<>("name", StringUtils::capitalize);


Then:

ToBean toBean = beanUtils.getTransformer()
      .setFlatFieldTransformation(true)
                    .withFieldTransformer(nameTransformer)
                    .transform(fromBean, ToBean.class);


Static Transformer Function:

BeanUtils offers a "static" version of the transformer method that can be an added value when needs to be applied in a composite lambda expression.

For example:

List<FromFooSimple> fromFooSimpleList = Arrays.asList(fromFooSimple, fromFooSimple);


The transformation should have been done by the following:

BeanTransformer transformer = new BeanUtils().getTransformer();
List<ImmutableToFooSimple> actual = fromFooSimpleList.stream()
                .map(fromFoo -> transformer.transform(fromFoo, ImmutableToFooSimple.class))
                .collect(Collectors.toList());


Thanks to this feature, it's possible to create a transformer function specific for a given object class:

Function<FromFooSimple, ImmutableToFooSimple> transformerFunction = BeanUtils.getTransformer(ImmutableToFooSimple.class);


Then, the list can be transformed as follows:

List<ImmutableToFooSimple> actual = fromFooSimpleList.stream()
                .map(transformerFunction)
                .collect(Collectors.toList());


However, it can happen that we have configured a BeanTransformer instance with several fields, mapping and transformation functions, and we want to use it also for this transformation, so what we need to do is to create the transformer function from our transformer:

BeanTransformer transformer = new BeanUtils().getTransformer()
  .withFieldMapping(new FieldMapping("a", "b"))
  .withFieldMapping(new FieldMapping("c", "d"))
  .withTransformerFunction(new FieldTransformer<>("locale", Locale::forLanguageTag));

Function<FromFooSimple, ImmutableToFooSimple> transformerFunction = BeanUtils.getTransformer(transformer, ImmutableToFooSimple.class);
List<ImmutableToFooSimple> actual = fromFooSimpleList.stream()
                .map(transformerFunction)
                .collect(Collectors.toList());


Enable Java Bean Validation

One of the features offered by the library is bean validation. It consists of checking that the transformed object meets the constraints defined on it. The validation works with both the default javax.constraints and the custom one.

Assuming that the field id in the FromBean instance is null.

public class FromBean {                                     public class ToBean {                           
   private final String name;                                  @NotNull                   
   private final BigInteger id;                                public BigInteger id;                      
                                                               private final String name;

   // all args constructor                                     // all args constructor
   // getters...                                               // getters and setters...
}                                                            }


Adding the following configuration, the validation will be performed at the end of the transformation process, and in our example, an exception will be thrown informing that the object is invalid:

ToBean toBean = new BeanUtils().getTransformer()
                       .setValidationEnabled(true)
                       .transform(fromBean, ToBean.class);


Copy on an Existing Instance

Even if the library is able to create a new instance of the given class and fill it with the values in the given object, there could be cases in which it's needed to inject the values on an already existing instance, so given the following Java Beans:

public class FromBean {                                     public class ToBean {                           
   private final String name;                                  private String name;                   
   private final FromSubBean nestedObject;                     private ToSubBean nestedObject;                    

   // all args constructor                                     // constructor
   // getters...                                               // getters and setters...
}                                                           }


If we need to perform the copy on an already existing object, we just need to pass the class instance to the transform function:

ToBean toBean = new ToBean();
new BeanUtils().getTransformer().transform(fromBean, toBean);


Skip Transformation on a Given Set of Fields

In case we are copying source object values into an already existing instance (with some values already set), we may need to avoid that the transformation operation overrides the existing values. The below example explains how to do it, given:

public class FromBean {                                     public class ToBean {                           
   private final String name;                                  private String name;                   
   private final FromSubBean nestedObject;                     private ToSubBean nestedObject;                    

   // all args constructor                                     // constructor
   // getters...                                               // getters and setters...
}                                                           }

public class FromBean2 {                   
   private final int index;             
   private final FromSubBean nestedObject;

   // all args constructor                
   // getters...                          
}                                         


If we need to skip the transformation for a set of fields, we just need to pass their name to the skipTransformationForField method. For example, if we want to skip the transformation on the field nestedObject, this is what we need to do:

ToBean toBean = new ToBean();
new BeanUtils().getTransformer()
      .skipTransformationForField("nestedObject")
      .transform(fromBean, toBean);


This feature allows transforming an object keeping the data from different sources.

To better explain this function, let's assume that the ToBean (defined above) should be transformed as follows:

  • name field value has been taken from the FromBean object
  • nestedObject field value has been taken from the FromBean2 object

The objective can be reached by doing:

// create the destination object
ToBean toBean = new ToBean();

// execute the first transformation skipping the copy of: 'nestedObject' field that should come from the other source object
new BeanUtils().getTransformer()
      .skipTransformationForField("nestedObject")
      .transform(fromBean, toBean);

// then execute the transformation skipping the copy of: 'name' field that should come from the other source object
new BeanUtils().getTransformer()
      .skipTransformationForField("name")
      .transform(fromBean2, toBean);


Field Type Conversion

In the case where a field type is different from the source class and the destination, we have this example:

public class FromBean {                            public class ToBean {                           
   private final String index;                        private int index;                   

   // all args constructor                            // constructor
   // getters...                                      // getters and setters...
}                                                  }


It can be transformed using a specific transformation function:

FieldTransformer<String, Integer> indexTransformer = new FieldTransformer<>("index", Integer::parseInt);
ToBean toBean = new BeanUtils()
  .withFieldTransformer(indexTransformer)
  .transform(fromBean, ToBean.class);


Transformation of Java Bean using the Builder pattern

The library supports the transformation of Java Bean using different types of Builder patterns: the standard one (supported by default) and a custom one. Let's see them in details and how to enable the custom Builder type transformation.

Let's start from the standard one supported by default:

Java
 




x


 
1
public class ToBean {
2
    private final Class<?> objectClass;
3
    private final Class<?> genericClass;
4
 
          
5
    ToBean(final Class<?> objectClass, final Class<?> genericClass) {
6
        this.objectClass = objectClass;
7
        this.genericClass = genericClass;
8
    }
9
 
          
10
    public static ToBeanBuilder builder() {
11
        return new ToBean.ToBeanBuilder();
12
    }
13
 
          
14
    // getter methods
15
 
          
16
    public static class ToBeanBuilder {
17
        private Class<?> objectClass;
18
        private Class<?> genericClass;
19
 
          
20
        ToBeanBuilder() {
21
        }
22
 
          
23
        public ToBeanBuilder objectClass(final Class<?> objectClass) {
24
            this.objectClass = objectClass;
25
            return this;
26
        }
27
 
          
28
        public ToBeanBuilder genericClass(final Class<?> genericClass) {
29
            this.genericClass = genericClass;
30
            return this;
31
        }
32
 
          
33
        public com.hotels.transformer.model.ToBean build() {
34
            return new ToBean(this.objectClass, this.genericClass);
35
        }
36
    }
37
}



As said, this requires no extra settings, so the transformation can be performed by doing:

ToBean toBean = new BeanTransformer()
                         .transform(sourceObject, ToBean.class);


Custom Builder Pattern:

Java
 




xxxxxxxxxx
1
37


 
1
public class ToBean {
2
    private final Class<?> objectClass;
3
    private final Class<?> genericClass;
4
 
          
5
    ToBean(final ToBeanBuilder builder) {
6
        this.objectClass = builder.objectClass;
7
        this.genericClass = builder.genericClass;
8
    }
9
 
          
10
    public static ToBeanBuilder builder() {
11
        return new ToBean.ToBeanBuilder();
12
    }
13
 
          
14
    // getter methods
15
 
          
16
    public static class ToBeanBuilder {
17
        private Class<?> objectClass;
18
        private Class<?> genericClass;
19
 
          
20
        ToBeanBuilder() {
21
        }
22
 
          
23
        public ToBeanBuilder objectClass(final Class<?> objectClass) {
24
            this.objectClass = objectClass;
25
            return this;
26
        }
27
 
          
28
        public ToBeanBuilder genericClass(final Class<?> genericClass) {
29
            this.genericClass = genericClass;
30
            return this;
31
        }
32
 
          
33
        public com.hotels.transformer.model.ToBean build() {
34
            return new ToBean(this);
35
        }
36
    }
37
}



To transform the above Bean the instruction to use is:

ToBean toBean = new BeanTransformer()
                         .setCustomBuilderTransformationEnabled(true)
                         .transform(sourceObject, ToBean.class);


Transformation of Java Records

As of JDK 14 a new type of objects has been introduced: Java Records. Records are immutable data classes that require only the type and name of fields. The equals, hashCode, and toString methods, as well as the private, final fields, and public constructor, are generated by the Java compiler.

A Java Record defined as following:

public record FromFooRecord(BigInteger id, String name) {
}

can be easily transformed into this:

public record ToFooRecord(BigInteger id, String name) {
}

with a simple instruction:

ToFooRecord toRecord = new BeanTransformer().transform(sourceRecord, ToFooRecord.class);

the library is also able to transform from a Record to a Java Bean and vice versa. 


4. Bean Validation

The class validation against a set of rules can be precious, especially when we need to be sure that the object data is compliant with our expectations.

The “field validation” aspect is one of the features offered by BULL and it's totally automatic — you only need to annotate your field with one of the existing javax.validation.constraints (or defining a custom one) and then execute the validation on this.

Given the following bean:

public class SampleBean {                           
   @NotNull                   
   private BigInteger id;                      
   private String name;                 

   // constructor
   // getters and setters... 
}                                                               


An instance of the above object:

SampleBean sampleBean = new SampleBean();


And one line of code, such as:

new BeanUtils().getValidator().validate(sampleBean);


This will throw an InvalidBeanException, as the field id isnull.

Conclusion

I have tried to explain — with examples — how to use the main features offered by the BULL project. However, looking at the complete source code might even be more helpful.

More examples can be found looking at the test cases implemented on the BULL project, available here.

GitHub also contains a sample Spring Boot project that uses the library for transforming the request/response objects among the different layers, which can be found here.

Spring Framework Spring Boot Java (programming language) Object (computer science)

Opinions expressed by DZone contributors are their own.

Related

  • Be Punctual! Avoiding Kotlin’s lateinit In Spring Boot Testing
  • Spring Boot: How To Use Java Persistence Query Language (JPQL)
  • Distributed Tracing System (Spring Cloud Sleuth + OpenZipkin)
  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!