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