🔀 Merge “release/v81” into “master”
[Sone.git] / src / main / java / net / pterodactylus / sone / data / Profile.java
1 /*
2  * Sone - Profile.java - Copyright Â© 2010–2020 David Roden
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.sone.data;
19
20 import static com.google.common.base.Preconditions.checkArgument;
21 import static com.google.common.base.Preconditions.checkNotNull;
22 import static com.google.common.base.Preconditions.checkState;
23 import static java.nio.charset.StandardCharsets.UTF_8;
24
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.List;
28 import java.util.UUID;
29
30 import javax.annotation.Nonnull;
31 import javax.annotation.Nullable;
32
33 import com.google.common.hash.Hasher;
34 import com.google.common.hash.Hashing;
35
36 /**
37  * A profile stores personal information about a {@link Sone}. All information
38  * is optional and can be {@code null}.
39  */
40 public class Profile implements Fingerprintable {
41
42         /** The Sone this profile belongs to. */
43         private final Sone sone;
44
45         /** The first name. */
46         private volatile String firstName;
47
48         /** The middle name(s). */
49         private volatile String middleName;
50
51         /** The last name. */
52         private volatile String lastName;
53
54         /** The day of the birth date. */
55         private volatile Integer birthDay;
56
57         /** The month of the birth date. */
58         private volatile Integer birthMonth;
59
60         /** The year of the birth date. */
61         private volatile Integer birthYear;
62
63         /** The ID of the avatar image. */
64         private volatile String avatar;
65
66         /** Additional fields in the profile. */
67         private final List<Field> fields = Collections.synchronizedList(new ArrayList<Field>());
68
69         /**
70          * Creates a new empty profile.
71          *
72          * @param sone
73          *            The Sone this profile belongs to
74          */
75         public Profile(Sone sone) {
76                 this.sone = sone;
77         }
78
79         /**
80          * Creates a copy of a profile.
81          *
82          * @param profile
83          *            The profile to copy
84          */
85         public Profile(@Nonnull Profile profile) {
86                 this.sone = profile.sone;
87                 this.firstName = profile.firstName;
88                 this.middleName = profile.middleName;
89                 this.lastName = profile.lastName;
90                 this.birthDay = profile.birthDay;
91                 this.birthMonth = profile.birthMonth;
92                 this.birthYear = profile.birthYear;
93                 this.avatar = profile.avatar;
94                 this.fields.addAll(profile.fields);
95         }
96
97         //
98         // ACCESSORS
99         //
100
101         /**
102          * Returns the Sone this profile belongs to.
103          *
104          * @return The Sone this profile belongs to
105          */
106         @Nonnull
107         public Sone getSone() {
108                 return sone;
109         }
110
111         /**
112          * Returns the first name.
113          *
114          * @return The first name
115          */
116         @Nullable
117         public String getFirstName() {
118                 return firstName;
119         }
120
121         /**
122          * Sets the first name.
123          *
124          * @param firstName
125          *            The first name to set
126          * @return This profile (for method chaining)
127          */
128         @Nonnull
129         public Profile setFirstName(@Nullable String firstName) {
130                 this.firstName = "".equals(firstName) ? null : firstName;
131                 return this;
132         }
133
134         /**
135          * Returns the middle name(s).
136          *
137          * @return The middle name
138          */
139         @Nullable
140         public String getMiddleName() {
141                 return middleName;
142         }
143
144         /**
145          * Sets the middle name.
146          *
147          * @param middleName
148          *            The middle name to set
149          * @return This profile (for method chaining)
150          */
151         @Nonnull
152         public Profile setMiddleName(@Nullable String middleName) {
153                 this.middleName = "".equals(middleName) ? null : middleName;
154                 return this;
155         }
156
157         /**
158          * Returns the last name.
159          *
160          * @return The last name
161          */
162         @Nullable
163         public String getLastName() {
164                 return lastName;
165         }
166
167         /**
168          * Sets the last name.
169          *
170          * @param lastName
171          *            The last name to set
172          * @return This profile (for method chaining)
173          */
174         @Nonnull
175         public Profile setLastName(@Nullable String lastName) {
176                 this.lastName = "".equals(lastName) ? null : lastName;
177                 return this;
178         }
179
180         /**
181          * Returns the day of the birth date.
182          *
183          * @return The day of the birth date (from 1 to 31)
184          */
185         @Nullable
186         public Integer getBirthDay() {
187                 return birthDay;
188         }
189
190         /**
191          * Sets the day of the birth date.
192          *
193          * @param birthDay
194          *            The day of the birth date (from 1 to 31)
195          * @return This profile (for method chaining)
196          */
197         @Nonnull
198         public Profile setBirthDay(@Nullable Integer birthDay) {
199                 this.birthDay = birthDay;
200                 return this;
201         }
202
203         /**
204          * Returns the month of the birth date.
205          *
206          * @return The month of the birth date (from 1 to 12)
207          */
208         @Nullable
209         public Integer getBirthMonth() {
210                 return birthMonth;
211         }
212
213         /**
214          * Sets the month of the birth date.
215          *
216          * @param birthMonth
217          *            The month of the birth date (from 1 to 12)
218          * @return This profile (for method chaining)
219          */
220         @Nonnull
221         public Profile setBirthMonth(@Nullable Integer birthMonth) {
222                 this.birthMonth = birthMonth;
223                 return this;
224         }
225
226         /**
227          * Returns the year of the birth date.
228          *
229          * @return The year of the birth date
230          */
231         @Nullable
232         public Integer getBirthYear() {
233                 return birthYear;
234         }
235
236         /**
237          * Returns the ID of the currently selected avatar image.
238          *
239          * @return The ID of the currently selected avatar image, or {@code null} if
240          *         no avatar is selected.
241          */
242         @Nullable
243         public String getAvatar() {
244                 return avatar;
245         }
246
247         /**
248          * Sets the avatar image.
249          *
250          * @param avatar
251          *            The new avatar image, or {@code null} to not select an avatar
252          *            image.
253          * @return This Sone
254          */
255         @Nonnull
256         public Profile setAvatar(@Nullable Image avatar) {
257                 if (avatar == null) {
258                         this.avatar = null;
259                         return this;
260                 }
261                 checkArgument(avatar.getSone().equals(sone), "avatar must belong to Sone");
262                 this.avatar = avatar.getId();
263                 return this;
264         }
265
266         /**
267          * Sets the year of the birth date.
268          *
269          * @param birthYear
270          *            The year of the birth date
271          * @return This profile (for method chaining)
272          */
273         @Nonnull
274         public Profile setBirthYear(@Nullable Integer birthYear) {
275                 this.birthYear = birthYear;
276                 return this;
277         }
278
279         /**
280          * Returns the fields of this profile.
281          *
282          * @return The fields of this profile
283          */
284         @Nonnull
285         public List<Field> getFields() {
286                 return new ArrayList<>(fields);
287         }
288
289         /**
290          * Returns whether this profile contains the given field.
291          *
292          * @param field
293          *            The field to check for
294          * @return {@code true} if this profile contains the field, false otherwise
295          */
296         public boolean hasField(@Nonnull Field field) {
297                 return fields.contains(field);
298         }
299
300         /**
301          * Returns the field with the given ID.
302          *
303          * @param fieldId
304          *            The ID of the field to get
305          * @return The field, or {@code null} if this profile does not contain a
306          *         field with the given ID
307          */
308         @Nullable
309         public Field getFieldById(@Nonnull String fieldId) {
310                 checkNotNull(fieldId, "fieldId must not be null");
311                 for (Field field : fields) {
312                         if (field.getId().equals(fieldId)) {
313                                 return field;
314                         }
315                 }
316                 return null;
317         }
318
319         /**
320          * Returns the field with the given name.
321          *
322          * @param fieldName
323          *            The name of the field to get
324          * @return The field, or {@code null} if this profile does not contain a
325          *         field with the given name
326          */
327         @Nullable
328         public Field getFieldByName(@Nonnull String fieldName) {
329                 for (Field field : fields) {
330                         if (field.getName().equals(fieldName)) {
331                                 return field;
332                         }
333                 }
334                 return null;
335         }
336
337         /**
338          * Appends a new field to the list of fields.
339          *
340          * @param fieldName
341          *            The name of the new field
342          * @return The new field
343          * @throws IllegalArgumentException
344          *             if the name is not valid
345          */
346         @Nonnull
347         public Field addField(@Nonnull String fieldName) throws IllegalArgumentException {
348                 checkNotNull(fieldName, "fieldName must not be null");
349                 if (fieldName.length() == 0) {
350                         throw new EmptyFieldName();
351                 }
352                 if (getFieldByName(fieldName) != null) {
353                         throw new DuplicateField();
354                 }
355                 @SuppressWarnings("synthetic-access")
356                 Field field = new Field().setName(fieldName).setValue("");
357                 fields.add(field);
358                 return field;
359         }
360
361         /**
362          * Moves the given field up one position in the field list. The index of the
363          * field to move must be greater than {@code 0} (because you obviously can
364          * not move the first field further up).
365          *
366          * @param field
367          *            The field to move up
368          */
369         public void moveFieldUp(@Nonnull Field field) {
370                 checkNotNull(field, "field must not be null");
371                 checkArgument(hasField(field), "field must belong to this profile");
372                 checkArgument(getFieldIndex(field) > 0, "field index must be > 0");
373                 int fieldIndex = getFieldIndex(field);
374                 fields.remove(field);
375                 fields.add(fieldIndex - 1, field);
376         }
377
378         /**
379          * Moves the given field down one position in the field list. The index of
380          * the field to move must be less than the index of the last field (because
381          * you obviously can not move the last field further down).
382          *
383          * @param field
384          *            The field to move down
385          */
386         public void moveFieldDown(@Nonnull Field field) {
387                 checkNotNull(field, "field must not be null");
388                 checkArgument(hasField(field), "field must belong to this profile");
389                 checkArgument(getFieldIndex(field) < fields.size() - 1, "field index must be < " + (fields.size() - 1));
390                 int fieldIndex = getFieldIndex(field);
391                 fields.remove(field);
392                 fields.add(fieldIndex + 1, field);
393         }
394
395         /**
396          * Removes the given field.
397          *
398          * @param field
399          *            The field to remove
400          */
401         public void removeField(@Nonnull Field field) {
402                 checkNotNull(field, "field must not be null");
403                 checkArgument(hasField(field), "field must belong to this profile");
404                 fields.remove(field);
405         }
406
407         //
408         // PRIVATE METHODS
409         //
410
411         /**
412          * Returns the index of the field with the given name.
413          *
414          * @param field
415          *            The name of the field
416          * @return The index of the field, or {@code -1} if there is no field with
417          *         the given name
418          */
419         private int getFieldIndex(@Nonnull Field field) {
420                 return fields.indexOf(field);
421         }
422
423         //
424         // INTERFACE Fingerprintable
425         //
426
427         /**
428          * {@inheritDoc}
429          */
430         @Override
431         public String getFingerprint() {
432                 Hasher hash = Hashing.sha256().newHasher();
433                 hash.putString("Profile(", UTF_8);
434                 if (firstName != null) {
435                         hash.putString("FirstName(", UTF_8).putString(firstName, UTF_8).putString(")", UTF_8);
436                 }
437                 if (middleName != null) {
438                         hash.putString("MiddleName(", UTF_8).putString(middleName, UTF_8).putString(")", UTF_8);
439                 }
440                 if (lastName != null) {
441                         hash.putString("LastName(", UTF_8).putString(lastName, UTF_8).putString(")", UTF_8);
442                 }
443                 if (birthDay != null) {
444                         hash.putString("BirthDay(", UTF_8).putInt(birthDay).putString(")", UTF_8);
445                 }
446                 if (birthMonth != null) {
447                         hash.putString("BirthMonth(", UTF_8).putInt(birthMonth).putString(")", UTF_8);
448                 }
449                 if (birthYear != null) {
450                         hash.putString("BirthYear(", UTF_8).putInt(birthYear).putString(")", UTF_8);
451                 }
452                 if (avatar != null) {
453                         hash.putString("Avatar(", UTF_8).putString(avatar, UTF_8).putString(")", UTF_8);
454                 }
455                 hash.putString("ContactInformation(", UTF_8);
456                 for (Field field : fields) {
457                         hash.putString(field.getName(), UTF_8).putString("(", UTF_8).putString(field.getValue(), UTF_8).putString(")", UTF_8);
458                 }
459                 hash.putString(")", UTF_8);
460                 hash.putString(")", UTF_8);
461
462                 return hash.hash().toString();
463         }
464
465         /**
466          * Container for a profile field.
467          */
468         public class Field {
469
470                 /** The ID of the field. */
471                 private final String id;
472
473                 /** The name of the field. */
474                 private String name;
475
476                 /** The value of the field. */
477                 private String value;
478
479                 /**
480                  * Creates a new field with a random ID.
481                  */
482                 private Field() {
483                         this(UUID.randomUUID().toString());
484                 }
485
486                 /**
487                  * Creates a new field with the given ID.
488                  *
489                  * @param id
490                  *            The ID of the field
491                  */
492                 private Field(@Nonnull String id) {
493                         this.id = checkNotNull(id, "id must not be null");
494                 }
495
496                 /**
497                  * Returns the ID of this field.
498                  *
499                  * @return The ID of this field
500                  */
501                 @Nonnull
502                 public String getId() {
503                         return id;
504                 }
505
506                 /**
507                  * Returns the name of this field.
508                  *
509                  * @return The name of this field
510                  */
511                 @Nonnull
512                 public String getName() {
513                         return name;
514                 }
515
516                 /**
517                  * Sets the name of this field. The name must not be {@code null} and
518                  * must not match any other fields in this profile but my match the name
519                  * of this field.
520                  *
521                  * @param name
522                  *            The new name of this field
523                  * @return This field
524                  */
525                 @Nonnull
526                 public Field setName(@Nonnull String name) {
527                         checkNotNull(name, "name must not be null");
528                         checkArgument(getFieldByName(name) == null, "name must be unique");
529                         this.name = name;
530                         return this;
531                 }
532
533                 /**
534                  * Returns the value of this field.
535                  *
536                  * @return The value of this field
537                  */
538                 @Nullable
539                 public String getValue() {
540                         return value;
541                 }
542
543                 /**
544                  * Sets the value of this field. While {@code null} is allowed, no
545                  * guarantees are made that {@code null} values are correctly persisted
546                  * across restarts of the plugin!
547                  *
548                  * @param value
549                  *            The new value of this field
550                  * @return This field
551                  */
552                 @Nonnull
553                 public Field setValue(@Nullable String value) {
554                         this.value = value;
555                         return this;
556                 }
557
558                 //
559                 // OBJECT METHODS
560                 //
561
562                 /**
563                  * {@inheritDoc}
564                  */
565                 @Override
566                 public boolean equals(Object object) {
567                         if (!(object instanceof Field)) {
568                                 return false;
569                         }
570                         Field field = (Field) object;
571                         return id.equals(field.id);
572                 }
573
574                 /**
575                  * {@inheritDoc}
576                  */
577                 @Override
578                 public int hashCode() {
579                         return id.hashCode();
580                 }
581
582         }
583
584         /**
585          * Exception that signals the addition of a field with an empty name.
586          */
587         public static class EmptyFieldName extends IllegalArgumentException { }
588
589         /**
590          * Exception that signals the addition of a field that already exists.
591          */
592         public static class DuplicateField extends IllegalArgumentException { }
593
594 }