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