Don’t throw exception when removing a field that is not in the profile.
[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         public void removeField(Field field) {
262                 checkNotNull(field, "field must not be null");
263                 fields.remove(field);
264         }
265
266         public Modifier modify() {
267                 return new Modifier() {
268                         private Optional<String> firstName = name.getFirst();
269                         private Optional<String> middleName = name.getMiddle();
270                         private Optional<String> lastName = name.getLast();
271                         private Optional<Integer> birthYear = birthDate.getYear();
272                         private Optional<Integer> birthMonth = birthDate.getMonth();
273                         private Optional<Integer> birthDay = birthDate.getDay();
274
275                         @Override
276                         public Modifier setFirstName(String firstName) {
277                                 this.firstName = fromNullable(firstName);
278                                 return this;
279                         }
280
281                         @Override
282                         public Modifier setMiddleName(String middleName) {
283                                 this.middleName = fromNullable(middleName);
284                                 return this;
285                         }
286
287                         @Override
288                         public Modifier setLastName(String lastName) {
289                                 this.lastName = fromNullable(lastName);
290                                 return this;
291                         }
292
293                         @Override
294                         public Modifier setBirthYear(Integer birthYear) {
295                                 this.birthYear = fromNullable(birthYear);
296                                 return this;
297                         }
298
299                         @Override
300                         public Modifier setBirthMonth(Integer birthMonth) {
301                                 this.birthMonth = fromNullable(birthMonth);
302                                 return this;
303                         }
304
305                         @Override
306                         public Modifier setBirthDay(Integer birthDay) {
307                                 this.birthDay = fromNullable(birthDay);
308                                 return this;
309                         }
310
311                         @Override
312                         public Profile update() {
313                                 Profile.this.name = new Name(firstName, middleName, lastName);
314                                 Profile.this.birthDate = new BirthDate(birthYear, birthMonth, birthDay);
315                                 return Profile.this;
316                         }
317                 };
318         }
319
320         public interface Modifier {
321
322                 Modifier setFirstName(String firstName);
323                 Modifier setMiddleName(String middleName);
324                 Modifier setLastName(String lastName);
325                 Modifier setBirthYear(Integer birthYear);
326                 Modifier setBirthMonth(Integer birthMonth);
327                 Modifier setBirthDay(Integer birthDay);
328                 Profile update();
329
330         }
331
332         //
333         // PRIVATE METHODS
334         //
335
336         /**
337          * Returns the index of the field with the given name.
338          *
339          * @param field
340          *            The name of the field
341          * @return The index of the field, or {@code -1} if there is no field with
342          *         the given name
343          */
344         private int getFieldIndex(Field field) {
345                 return fields.indexOf(field);
346         }
347
348         //
349         // INTERFACE Fingerprintable
350         //
351
352         /**
353          * {@inheritDoc}
354          */
355         @Override
356         public String getFingerprint() {
357                 Hasher hash = Hashing.sha256().newHasher();
358                 hash.putString("Profile(");
359                 hash.putString(name.getFingerprint());
360                 hash.putString(birthDate.getFingerprint());
361                 if (avatar != null) {
362                         hash.putString("Avatar(").putString(avatar).putString(")");
363                 }
364                 hash.putString("ContactInformation(");
365                 for (Field field : fields) {
366                         hash.putString(field.getName()).putString("(").putString(field.getValue()).putString(")");
367                 }
368                 hash.putString(")");
369                 hash.putString(")");
370
371                 return hash.hash().toString();
372         }
373
374         /**
375          * Container for a profile field.
376          *
377          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
378          */
379         public static class Field {
380
381                 private final String id;
382                 private final String name;
383                 private final String value;
384
385                 public Field(String name) {
386                         this(name, null);
387                 }
388
389                 public Field(String name, String value) {
390                         this(randomUUID().toString(), name, value);
391                 }
392
393                 public Field(String id, String name, String value) {
394                         this.id = checkNotNull(id, "id must not be null");
395                         this.name = name;
396                         this.value = value;
397                 }
398
399                 public String getId() {
400                         return id;
401                 }
402
403                 public String getName() {
404                         return name;
405                 }
406
407                 public String getValue() {
408                         return value;
409                 }
410
411                 @Override
412                 public boolean equals(Object object) {
413                         if (!(object instanceof Field)) {
414                                 return false;
415                         }
416                         Field field = (Field) object;
417                         return id.equals(field.id);
418                 }
419
420                 @Override
421                 public int hashCode() {
422                         return id.hashCode();
423                 }
424
425         }
426
427         public static class Name implements Fingerprintable {
428
429                 private final Optional<String> first;
430                 private final Optional<String> middle;
431                 private final Optional<String> last;
432
433                 public Name() {
434                         this(Optional.<String>absent(), Optional.<String>absent(), Optional.<String>absent());
435                 }
436
437                 public Name(Optional<String> first, Optional<String> middle, Optional<String> last) {
438                         this.first = first;
439                         this.middle = middle;
440                         this.last = last;
441                 }
442
443                 public Optional<String> getFirst() {
444                         return first;
445                 }
446
447                 public Optional<String> getMiddle() {
448                         return middle;
449                 }
450
451                 public Optional<String> getLast() {
452                         return last;
453                 }
454
455                 @Override
456                 public String getFingerprint() {
457                         Hasher hash = Hashing.sha256().newHasher();
458                         hash.putString("Name(");
459                         if (first.isPresent()) {
460                                 hash.putString("First(").putString(first.get()).putString(")");
461                         }
462                         if (middle.isPresent()) {
463                                 hash.putString("Middle(").putString(middle.get()).putString(")");
464                         }
465                         if (last.isPresent()) {
466                                 hash.putString("Last(").putString(last.get()).putString(")");
467                         }
468                         hash.putString(")");
469                         return hash.hash().toString();
470                 }
471
472         }
473
474         public static class BirthDate implements Fingerprintable {
475
476                 private final Optional<Integer> year;
477                 private final Optional<Integer> month;
478                 private final Optional<Integer> day;
479
480                 public BirthDate() {
481                         this(Optional.<Integer>absent(), Optional.<Integer>absent(), Optional.<Integer>absent());
482                 }
483
484                 public BirthDate(Optional<Integer> year, Optional<Integer> month, Optional<Integer> day) {
485                         this.year = year;
486                         this.month = month;
487                         this.day = day;
488                 }
489
490                 public Optional<Integer> getYear() {
491                         return year;
492                 }
493
494                 public Optional<Integer> getMonth() {
495                         return month;
496                 }
497
498                 public Optional<Integer> getDay() {
499                         return day;
500                 }
501
502                 @Override
503                 public String getFingerprint() {
504                         Hasher hash = Hashing.sha256().newHasher();
505                         hash.putString("Birthdate(");
506                         if (year.isPresent()) {
507                                 hash.putString("Year(").putInt(year.get()).putString(")");
508                         }
509                         if (month.isPresent()) {
510                                 hash.putString("Month(").putInt(month.get()).putString(")");
511                         }
512                         if (day.isPresent()) {
513                                 hash.putString("Day(").putInt(day.get()).putString(")");
514                         }
515                         hash.putString(")");
516                         return hash.hash().toString();
517                 }
518
519         }
520
521 }