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