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