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

  • How Spring and Hibernate Simplify Web and Database Management
  • Graceful Shutdown: Spring Framework vs Golang Web Services
  • Enabling Behavior-Driven Service Discovery: A Lightweight Approach to Augment Java Factory Design Pattern
  • Choosing the Right Caching Strategy

Trending

  • Article Moderation: Your Questions, Answered
  • Building Resilient Identity Systems: Lessons from Securing Billions of Authentication Requests
  • Unlocking the Potential of Apache Iceberg: A Comprehensive Analysis
  • How to Perform Custom Error Handling With ANTLR
  1. DZone
  2. Coding
  3. Frameworks
  4. Tutorial: Generating Java Files with Spring and Mustache

Tutorial: Generating Java Files with Spring and Mustache

If you need to generate complex model from a large source data, you should try this

By 
Farith Sanmiguel user avatar
Farith Sanmiguel
·
Jul. 06, 20 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
8.7K Views

Join the DZone community and get the full member experience.

Join For Free

In this tutorial, we are going to use Spring Boot and Mustache to generate a model using a given set of data.

The data was extracted from a rules and validation table. Here's a quick explanation of how it works — we receive a request with SOAP, and we validate its structure by reading this data, for example, what type of request it is and we transform each field if necessary. The problem appears when there are many requests that we have to validate, so we need to generate and adapt a specific object for each type of request.

In this tutorial, we will create our model from a source file called data.txt, generating java files with properties and annotations in a Builder pattern structure

1. Create a Spring Boot Project in https://start.spring.io

In this tutorial, we don't need any additional dependencies except for the defaults dependencies.

Spring Initializr


2. Add the Mustache Java Library in the pom.xml File

This library works with Java 8:

XML
 




x


 
1
<dependency>
2
            <groupId>com.github.spullara.mustache.java</groupId>
3
            <artifactId>compiler</artifactId>
4
            <version>0.9.6</version>
5
</dependency>



 3. Create a Template for the Class (Builder Pattern)

Here the Mustache manual https://mustache.github.io/mustache.5.html 

Java
xxxxxxxxxx
1
58
 
1
package {{packages}}
2
3
import java.io.Serializable;
4
import javax.validation.constraints.*;
5
6
/**
7
 *
8
 * @author Fsanmiguel
9
 */
10
public class {{classname}} implements Serializable{
11
12
    private static final long serialVersionUID = {{serialVersionUID}};
13
14
    {{#properties}}
15
    {{#annotations}}
16
    {{&.}}
17
    {{/annotations}}
18
    private String {{property}};
19
20
    {{/properties}}
21
22
//getters
23
    {{#properties}}
24
     public String {{getter}}() {
25
        return {{property}};
26
     }
27
    {{/properties}}
28
29
    private {{classname}}(Builder builder){
30
        {{#properties}}
31
        this.{{property}} = builder.{{property}};
32
        {{/properties}}
33
    }
34
35
    public static Builder new{{classname}}() {
36
            return new Builder();
37
    }
38
39
      public static class Builder {
40
            {{#properties}}
41
                private String {{property}};
42
            {{/properties}}
43
44
            private Builder() {}
45
46
            public {{classname}} build() {
47
                return new {{classname}}(this);
48
            }
49
            {{#properties}}
50
            public Builder with{{camelcase}}(String {{property}}) {
51
                this.{{property}} = {{property}};
52
                return this;
53
            }
54
            {{/properties}}
55
      }
56
57
58
}


4. Create a Context Class to Adapt the Given Data to the Template

In this tutorial, we need a simple class that matches the template property names

Java
xxxxxxxxxx
1
97
 
1
package com.modelgenerator.modelgenerator.model;
2
3
import org.springframework.util.StringUtils;
4
5
import java.util.List;
6
7
public class Context {
8
9
    List<Property> properties;
10
    List<String> getters;
11
    List<String> setters;
12
    List<String> constants;
13
    String classname;
14
    String serialVersionUID;
15
    String packages;
16
17
18
    public List<Property> getProperties() {
19
        return properties;
20
    }
21
22
    public void setProperties(List<Property> properties) {
23
        this.properties = properties;
24
    }
25
26
    public String getClassname() {
27
        return classname;
28
    }
29
30
    public void setClassname(String classname) {
31
        this.classname = classname;
32
    }
33
34
    public String getSerialVersionUID() {
35
        return serialVersionUID;
36
    }
37
38
    public void setSerialVersionUID(String serialVersionUID) {
39
        this.serialVersionUID = serialVersionUID;
40
    }
41
42
    public String getPackages() {
43
        return packages;
44
    }
45
46
    public void setPackages(String packages) {
47
        this.packages = packages;
48
    }
49
50
    public List<String> getGetters() {
51
        return getters;
52
    }
53
54
    public List<String> getSetters() {
55
        return setters;
56
    }
57
58
    public void setGetters(List<String> getters) {
59
        this.getters = getters;
60
    }
61
62
    public void setSetters(List<String> setters) {
63
        this.setters = setters;
64
    }
65
66
    public List<String> getConstants() {
67
        return constants;
68
    }
69
70
    public void setConstants(List<String> constants) {
71
        this.constants = constants;
72
    }
73
74
    public static class Property {
75
        String property;
76
        String getter;
77
        String camelcase;
78
        List<String> annotations;
79
        String type = "String";
80
81
        public Property(String property, List<String> annotations) {
82
            this.annotations = annotations;
83
            this.property = property;
84
            this.getter = "get"+StringUtils.capitalize(property);
85
            this.camelcase = StringUtils.capitalize(property);
86
        }
87
88
        public Property(String property, List<String> annotations, String type) {
89
            this.annotations = annotations;
90
            this.property = property;
91
            this.getter = "get"+StringUtils.capitalize(property);
92
            this.camelcase = StringUtils.capitalize(property);
93
            this.type = type;
94
        }
95
    }
96
}
97


5. Read and Transform the Data to the Context Class

Java
xxxxxxxxxx
1
122
 
1
package com.modelgenerator.modelgenerator;
2
3
import com.modelgenerator.modelgenerator.model.Context;
4
import com.modelgenerator.modelgenerator.model.MessField;
5
import com.github.mustachejava.DefaultMustacheFactory;
6
import com.github.mustachejava.Mustache;
7
import com.github.mustachejava.MustacheFactory;
8
import org.slf4j.Logger;
9
import org.slf4j.LoggerFactory;
10
11
import java.io.IOException;
12
import java.io.StringWriter;
13
import java.nio.file.Files;
14
import java.nio.file.Path;
15
import java.nio.file.Paths;
16
import java.nio.file.StandardOpenOption;
17
import java.text.MessageFormat;
18
import java.util.ArrayList;
19
import java.util.Arrays;
20
import java.util.List;
21
import java.util.Map;
22
import java.util.stream.Collectors;
23
24
public class ModelGeneratorApp {
25
26
    private static Logger LOG = LoggerFactory
27
            .getLogger(ModelGeneratorApp.class);
28
29
    public static void main(String[] args) throws IOException {
30
        Path data = Paths.get("./data1.txt");
31
        List<String> content = Files.readAllLines(data);
32
      
33
        //TODO fill this list with the given data
34
        List<MessField> messFieldList = new ArrayList<>();
35
36
        String networks []  = {"ACCL","ACCL-MLD",};
37
        String messages [] = {"1A","2A","3A","4A"};
38
39
        String formatClassName = "{0}{1}";
40
        Arrays.asList(networks).stream()
41
                .forEach(n -> {
42
                    Arrays.asList(messages).stream()
43
                    .forEach(m -> {
44
                        String className = MessageFormat.format(formatClassName,n,m);
45
                        className = className.replace("-","");
46
                        generateClasses(className,messFieldList,n,m );
47
                    });
48
                });
49
    }
50
51
52
    public static void generateClasses(String className,
53
                                List<MessField> messFieldList,
54
                                String network,
55
                                String message) {
56
57
        //filter the data and group by a field
58
        Map<String, List<MessField>> collect = messFieldList
59
                .stream()
60
                .distinct()
61
                .filter(s -> network.equals(s.getVi_network()))
62
                .filter(s -> message.equals(s.getVi_mess_name())).
63
                collect(Collectors.groupingBy(MessField::getVi_variablename));
64
65
66
        Context context = new Context();
67
        context.setClassname(className);
68
        context.setPackages("com.modelgenerator.modelgenerator;");
69
        context.setSerialVersionUID("312312412312312312L");
70
        List<Context.Property> properties = collect.entrySet()
71
                .stream()
72
                .map(f -> new Context.Property(camelCase(f.getKey()),
73
                        Arrays.asList("@NotNull")))
74
                .collect(Collectors.toList());
75
76
        context.setProperties(properties);
77
        String content = mustacheContent(context, "dtotemplate.mustache");
78
        generateFile(MessageFormat.format("./classes/{0}.java", className), content);
79
80
    }
81
82
    public static String mustacheContent(Object context, String template) {
83
        try {
84
            StringWriter writer = new StringWriter();
85
            MustacheFactory mf = new DefaultMustacheFactory();
86
            Mustache mustache = mf.compile(template);
87
            mustache.execute(writer, context).flush();
88
            return writer.toString();
89
        } catch (Exception ex) {
90
            LOG.error("mustache error", ex);
91
        }
92
        return "";
93
    }
94
95
    public static void generateFile(String className, String content) {
96
        LOG.info("GENERATING "+ className);
97
        try {
98
            Files.deleteIfExists(Paths.get(className));
99
            Files.write(Paths.get(className), content.getBytes(), StandardOpenOption.CREATE_NEW);
100
        } catch (IOException e) {
101
            LOG.error("Error generating file", e);
102
        }
103
104
    }
105
106
    public static String camelCase(String word) {
107
        String[] chars = word.split("");
108
        Boolean upper = false;
109
        String results = "";
110
        for (String c : chars) {
111
            if (!"_".equals(c)) {
112
                results += upper ? c.toUpperCase() : c;
113
                upper = false;
114
            } else {
115
                upper = true;
116
            }
117
        }
118
        return results;
119
    }
120
121
}
122


6. Examine the Generated Objects

You can run several times until you get the Java files correctly

Here's the output:

Models generated

Here's a generated Java file:

Java
x
393
 
1
package com.modelgenerator.modelgenerator;
2
3
import java.io.Serializable;
4
import javax.validation.constraints.*;
5
6
/**
7
 *
8
 * @author Fsanmiguel
9
 */
10
public class ACCL2A implements Serializable{
11
12
    private static final long serialVersionUID = 312312412312312312L;
13
14
    @NotNull
15
    private String destinoFondos;
16
17
    @NotNull
18
    private String codProcedimiento;
19
20
    @NotNull
21
    private String cuentaOrigen;
22
23
    @NotNull
24
    private String numOrdenAch;
25
26
    @NotNull
27
    private String cuentaDestino;
28
29
    @NotNull
30
    private String origenFondos;
31
32
    @NotNull
33
    private String titularOriginante;
34
35
    @NotNull
36
    private String codSucursalOriginante;
37
38
    @NotNull
39
    private String etc;
40
41
    @NotNull
42
    private String codSubDestinatario;
43
44
    @NotNull
45
    private String codSubOriginante;
46
47
    @NotNull
48
    private String codServicio;
49
50
    @NotNull
51
    private String canal;
52
53
    @NotNull
54
    private String tipOrden;
55
56
    @NotNull
57
    private String codOriginante;
58
59
    @NotNull
60
    private String glosa;
61
62
    @NotNull
63
    private String codPaisOriginante;
64
65
    @NotNull
66
    private String titularDestinatario;
67
68
    @NotNull
69
    private String tipCuentaDestino;
70
71
    @NotNull
72
    private String importe;
73
74
    @NotNull
75
    private String numOrdenOriginante;
76
77
    @NotNull
78
    private String ciNitOriginante;
79
80
    @NotNull
81
    private String tipoDocumento;
82
83
    @NotNull
84
    private String codDestinatario;
85
86
    @NotNull
87
    private String codMoneda;
88
89
    @NotNull
90
    private String codPaisDestinatario;
91
92
    @NotNull
93
    private String tipCuentaOrigen;
94
95
    @NotNull
96
    private String codCamara;
97
98
    @NotNull
99
    private String ciNitDestinatario;
100
101
    @NotNull
102
    private String fecCamara;
103
104
105
//getters
106
     public String getDestinoFondos() {
107
        return destinoFondos;
108
     }
109
     public String getCodProcedimiento() {
110
        return codProcedimiento;
111
     }
112
     public String getCuentaOrigen() {
113
        return cuentaOrigen;
114
     }
115
     public String getNumOrdenAch() {
116
        return numOrdenAch;
117
     }
118
     public String getCuentaDestino() {
119
        return cuentaDestino;
120
     }
121
     public String getOrigenFondos() {
122
        return origenFondos;
123
     }
124
     public String getTitularOriginante() {
125
        return titularOriginante;
126
     }
127
     public String getCodSucursalOriginante() {
128
        return codSucursalOriginante;
129
     }
130
     public String getEtc() {
131
        return etc;
132
     }
133
     public String getCodSubDestinatario() {
134
        return codSubDestinatario;
135
     }
136
     public String getCodSubOriginante() {
137
        return codSubOriginante;
138
     }
139
     public String getCodServicio() {
140
        return codServicio;
141
     }
142
     public String getCanal() {
143
        return canal;
144
     }
145
     public String getTipOrden() {
146
        return tipOrden;
147
     }
148
     public String getCodOriginante() {
149
        return codOriginante;
150
     }
151
     public String getGlosa() {
152
        return glosa;
153
     }
154
     public String getCodPaisOriginante() {
155
        return codPaisOriginante;
156
     }
157
     public String getTitularDestinatario() {
158
        return titularDestinatario;
159
     }
160
     public String getTipCuentaDestino() {
161
        return tipCuentaDestino;
162
     }
163
     public String getImporte() {
164
        return importe;
165
     }
166
     public String getNumOrdenOriginante() {
167
        return numOrdenOriginante;
168
     }
169
     public String getCiNitOriginante() {
170
        return ciNitOriginante;
171
     }
172
     public String getTipoDocumento() {
173
        return tipoDocumento;
174
     }
175
     public String getCodDestinatario() {
176
        return codDestinatario;
177
     }
178
     public String getCodMoneda() {
179
        return codMoneda;
180
     }
181
     public String getCodPaisDestinatario() {
182
        return codPaisDestinatario;
183
     }
184
     public String getTipCuentaOrigen() {
185
        return tipCuentaOrigen;
186
     }
187
     public String getCodCamara() {
188
        return codCamara;
189
     }
190
     public String getCiNitDestinatario() {
191
        return ciNitDestinatario;
192
     }
193
     public String getFecCamara() {
194
        return fecCamara;
195
     }
196
197
    private ACCL2A(Builder builder){
198
        this.destinoFondos = builder.destinoFondos;
199
        this.codProcedimiento = builder.codProcedimiento;
200
        this.cuentaOrigen = builder.cuentaOrigen;
201
        this.numOrdenAch = builder.numOrdenAch;
202
        this.cuentaDestino = builder.cuentaDestino;
203
        this.origenFondos = builder.origenFondos;
204
        this.titularOriginante = builder.titularOriginante;
205
        this.codSucursalOriginante = builder.codSucursalOriginante;
206
        this.etc = builder.etc;
207
        this.codSubDestinatario = builder.codSubDestinatario;
208
        this.codSubOriginante = builder.codSubOriginante;
209
        this.codServicio = builder.codServicio;
210
        this.canal = builder.canal;
211
        this.tipOrden = builder.tipOrden;
212
        this.codOriginante = builder.codOriginante;
213
        this.glosa = builder.glosa;
214
        this.codPaisOriginante = builder.codPaisOriginante;
215
        this.titularDestinatario = builder.titularDestinatario;
216
        this.tipCuentaDestino = builder.tipCuentaDestino;
217
        this.importe = builder.importe;
218
        this.numOrdenOriginante = builder.numOrdenOriginante;
219
        this.ciNitOriginante = builder.ciNitOriginante;
220
        this.tipoDocumento = builder.tipoDocumento;
221
        this.codDestinatario = builder.codDestinatario;
222
        this.codMoneda = builder.codMoneda;
223
        this.codPaisDestinatario = builder.codPaisDestinatario;
224
        this.tipCuentaOrigen = builder.tipCuentaOrigen;
225
        this.codCamara = builder.codCamara;
226
        this.ciNitDestinatario = builder.ciNitDestinatario;
227
        this.fecCamara = builder.fecCamara;
228
    }
229
230
    public static Builder newACCL2A() {
231
            return new Builder();
232
    }
233
234
      public static class Builder {
235
                private String destinoFondos;
236
                private String codProcedimiento;
237
                private String cuentaOrigen;
238
                private String numOrdenAch;
239
                private String cuentaDestino;
240
                private String origenFondos;
241
                private String titularOriginante;
242
                private String codSucursalOriginante;
243
                private String etc;
244
                private String codSubDestinatario;
245
                private String codSubOriginante;
246
                private String codServicio;
247
                private String canal;
248
                private String tipOrden;
249
                private String codOriginante;
250
                private String glosa;
251
                private String codPaisOriginante;
252
                private String titularDestinatario;
253
                private String tipCuentaDestino;
254
                private String importe;
255
                private String numOrdenOriginante;
256
                private String ciNitOriginante;
257
                private String tipoDocumento;
258
                private String codDestinatario;
259
                private String codMoneda;
260
                private String codPaisDestinatario;
261
                private String tipCuentaOrigen;
262
                private String codCamara;
263
                private String ciNitDestinatario;
264
                private String fecCamara;
265
266
            private Builder() {}
267
268
            public ACCL2A build() {
269
                return new ACCL2A(this);
270
            }
271
            public Builder withDestinoFondos(String destinoFondos) {
272
                this.destinoFondos = destinoFondos;
273
                return this;
274
            }
275
            public Builder withCodProcedimiento(String codProcedimiento) {
276
                this.codProcedimiento = codProcedimiento;
277
                return this;
278
            }
279
            public Builder withCuentaOrigen(String cuentaOrigen) {
280
                this.cuentaOrigen = cuentaOrigen;
281
                return this;
282
            }
283
            public Builder withNumOrdenAch(String numOrdenAch) {
284
                this.numOrdenAch = numOrdenAch;
285
                return this;
286
            }
287
            public Builder withCuentaDestino(String cuentaDestino) {
288
                this.cuentaDestino = cuentaDestino;
289
                return this;
290
            }
291
            public Builder withOrigenFondos(String origenFondos) {
292
                this.origenFondos = origenFondos;
293
                return this;
294
            }
295
            public Builder withTitularOriginante(String titularOriginante) {
296
                this.titularOriginante = titularOriginante;
297
                return this;
298
            }
299
            public Builder withCodSucursalOriginante(String codSucursalOriginante) {
300
                this.codSucursalOriginante = codSucursalOriginante;
301
                return this;
302
            }
303
            public Builder withEtc(String etc) {
304
                this.etc = etc;
305
                return this;
306
            }
307
            public Builder withCodSubDestinatario(String codSubDestinatario) {
308
                this.codSubDestinatario = codSubDestinatario;
309
                return this;
310
            }
311
            public Builder withCodSubOriginante(String codSubOriginante) {
312
                this.codSubOriginante = codSubOriginante;
313
                return this;
314
            }
315
            public Builder withCodServicio(String codServicio) {
316
                this.codServicio = codServicio;
317
                return this;
318
            }
319
            public Builder withCanal(String canal) {
320
                this.canal = canal;
321
                return this;
322
            }
323
            public Builder withTipOrden(String tipOrden) {
324
                this.tipOrden = tipOrden;
325
                return this;
326
            }
327
            public Builder withCodOriginante(String codOriginante) {
328
                this.codOriginante = codOriginante;
329
                return this;
330
            }
331
            public Builder withGlosa(String glosa) {
332
                this.glosa = glosa;
333
                return this;
334
            }
335
            public Builder withCodPaisOriginante(String codPaisOriginante) {
336
                this.codPaisOriginante = codPaisOriginante;
337
                return this;
338
            }
339
            public Builder withTitularDestinatario(String titularDestinatario) {
340
                this.titularDestinatario = titularDestinatario;
341
                return this;
342
            }
343
            public Builder withTipCuentaDestino(String tipCuentaDestino) {
344
                this.tipCuentaDestino = tipCuentaDestino;
345
                return this;
346
            }
347
            public Builder withImporte(String importe) {
348
                this.importe = importe;
349
                return this;
350
            }
351
            public Builder withNumOrdenOriginante(String numOrdenOriginante) {
352
                this.numOrdenOriginante = numOrdenOriginante;
353
                return this;
354
            }
355
            public Builder withCiNitOriginante(String ciNitOriginante) {
356
                this.ciNitOriginante = ciNitOriginante;
357
                return this;
358
            }
359
            public Builder withTipoDocumento(String tipoDocumento) {
360
                this.tipoDocumento = tipoDocumento;
361
                return this;
362
            }
363
            public Builder withCodDestinatario(String codDestinatario) {
364
                this.codDestinatario = codDestinatario;
365
                return this;
366
            }
367
            public Builder withCodMoneda(String codMoneda) {
368
                this.codMoneda = codMoneda;
369
                return this;
370
            }
371
            public Builder withCodPaisDestinatario(String codPaisDestinatario) {
372
                this.codPaisDestinatario = codPaisDestinatario;
373
                return this;
374
            }
375
            public Builder withTipCuentaOrigen(String tipCuentaOrigen) {
376
                this.tipCuentaOrigen = tipCuentaOrigen;
377
                return this;
378
            }
379
            public Builder withCodCamara(String codCamara) {
380
                this.codCamara = codCamara;
381
                return this;
382
            }
383
            public Builder withCiNitDestinatario(String ciNitDestinatario) {
384
                this.ciNitDestinatario = ciNitDestinatario;
385
                return this;
386
            }
387
            public Builder withFecCamara(String fecCamara) {
388
                this.fecCamara = fecCamara;
389
                return this;
390
            }
391
      }
392
393
}


Conclusion

There are situations when it becomes very repetitive to write a complete and complex model in a migration or a new implementation, and maybe  the definition of each file can be structure in a source data. This is very useful and can save a lot of time.

Here the source code: https://github.com/farvher/modelgenerator 

Spring Framework Java (programming language) Mustache (template system)

Published at DZone with permission of Farith Sanmiguel. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • How Spring and Hibernate Simplify Web and Database Management
  • Graceful Shutdown: Spring Framework vs Golang Web Services
  • Enabling Behavior-Driven Service Discovery: A Lightweight Approach to Augment Java Factory Design Pattern
  • Choosing the Right Caching Strategy

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!