Add custom exception for adding duplicate fields
[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.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                 checkArgument(fieldName.length() > 0, "fieldName must not be empty");
329                 if (getFieldByName(fieldName) != null) {
330                         throw new DuplicateField();
331                 }
332                 @SuppressWarnings("synthetic-access")
333                 Field field = new Field().setName(fieldName);
334                 fields.add(field);
335                 return field;
336         }
337
338         /**
339          * Moves the given field up one position in the field list. The index of the
340          * field to move must be greater than {@code 0} (because you obviously can
341          * not move the first field further up).
342          *
343          * @param field
344          *            The field to move up
345          */
346         public void moveFieldUp(Field field) {
347                 checkNotNull(field, "field must not be null");
348                 checkArgument(hasField(field), "field must belong to this profile");
349                 checkArgument(getFieldIndex(field) > 0, "field index must be > 0");
350                 int fieldIndex = getFieldIndex(field);
351                 fields.remove(field);
352                 fields.add(fieldIndex - 1, field);
353         }
354
355         /**
356          * Moves the given field down one position in the field list. The index of
357          * the field to move must be less than the index of the last field (because
358          * you obviously can not move the last field further down).
359          *
360          * @param field
361          *            The field to move down
362          */
363         public void moveFieldDown(Field field) {
364                 checkNotNull(field, "field must not be null");
365                 checkArgument(hasField(field), "field must belong to this profile");
366                 checkArgument(getFieldIndex(field) < fields.size() - 1, "field index must be < " + (fields.size() - 1));
367                 int fieldIndex = getFieldIndex(field);
368                 fields.remove(field);
369                 fields.add(fieldIndex + 1, field);
370         }
371
372         /**
373          * Removes the given field.
374          *
375          * @param field
376          *            The field to remove
377          */
378         public void removeField(Field field) {
379                 checkNotNull(field, "field must not be null");
380                 checkArgument(hasField(field), "field must belong to this profile");
381                 fields.remove(field);
382         }
383
384         //
385         // PRIVATE METHODS
386         //
387
388         /**
389          * Returns the index of the field with the given name.
390          *
391          * @param field
392          *            The name of the field
393          * @return The index of the field, or {@code -1} if there is no field with
394          *         the given name
395          */
396         private int getFieldIndex(Field field) {
397                 return fields.indexOf(field);
398         }
399
400         //
401         // INTERFACE Fingerprintable
402         //
403
404         /**
405          * {@inheritDoc}
406          */
407         @Override
408         public String getFingerprint() {
409                 Hasher hash = Hashing.sha256().newHasher();
410                 hash.putString("Profile(");
411                 if (firstName != null) {
412                         hash.putString("FirstName(").putString(firstName).putString(")");
413                 }
414                 if (middleName != null) {
415                         hash.putString("MiddleName(").putString(middleName).putString(")");
416                 }
417                 if (lastName != null) {
418                         hash.putString("LastName(").putString(lastName).putString(")");
419                 }
420                 if (birthDay != null) {
421                         hash.putString("BirthDay(").putInt(birthDay).putString(")");
422                 }
423                 if (birthMonth != null) {
424                         hash.putString("BirthMonth(").putInt(birthMonth).putString(")");
425                 }
426                 if (birthYear != null) {
427                         hash.putString("BirthYear(").putInt(birthYear).putString(")");
428                 }
429                 if (avatar != null) {
430                         hash.putString("Avatar(").putString(avatar).putString(")");
431                 }
432                 hash.putString("ContactInformation(");
433                 for (Field field : fields) {
434                         hash.putString(field.getName()).putString("(").putString(field.getValue()).putString(")");
435                 }
436                 hash.putString(")");
437                 hash.putString(")");
438
439                 return hash.hash().toString();
440         }
441
442         /**
443          * Container for a profile field.
444          *
445          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
446          */
447         public class Field {
448
449                 /** The ID of the field. */
450                 private final String id;
451
452                 /** The name of the field. */
453                 private String name;
454
455                 /** The value of the field. */
456                 private String value;
457
458                 /**
459                  * Creates a new field with a random ID.
460                  */
461                 private Field() {
462                         this(UUID.randomUUID().toString());
463                 }
464
465                 /**
466                  * Creates a new field with the given ID.
467                  *
468                  * @param id
469                  *            The ID of the field
470                  */
471                 private Field(String id) {
472                         this.id = checkNotNull(id, "id must not be null");
473                 }
474
475                 /**
476                  * Returns the ID of this field.
477                  *
478                  * @return The ID of this field
479                  */
480                 public String getId() {
481                         return id;
482                 }
483
484                 /**
485                  * Returns the name of this field.
486                  *
487                  * @return The name of this field
488                  */
489                 public String getName() {
490                         return name;
491                 }
492
493                 /**
494                  * Sets the name of this field. The name must not be {@code null} and
495                  * must not match any other fields in this profile but my match the name
496                  * of this field.
497                  *
498                  * @param name
499                  *            The new name of this field
500                  * @return This field
501                  */
502                 public Field setName(String name) {
503                         checkNotNull(name, "name must not be null");
504                         checkArgument(getFieldByName(name) == null, "name must be unique");
505                         this.name = name;
506                         return this;
507                 }
508
509                 /**
510                  * Returns the value of this field.
511                  *
512                  * @return The value of this field
513                  */
514                 public String getValue() {
515                         return value;
516                 }
517
518                 /**
519                  * Sets the value of this field. While {@code null} is allowed, no
520                  * guarantees are made that {@code null} values are correctly persisted
521                  * across restarts of the plugin!
522                  *
523                  * @param value
524                  *            The new value of this field
525                  * @return This field
526                  */
527                 public Field setValue(String value) {
528                         this.value = value;
529                         return this;
530                 }
531
532                 //
533                 // OBJECT METHODS
534                 //
535
536                 /**
537                  * {@inheritDoc}
538                  */
539                 @Override
540                 public boolean equals(Object object) {
541                         if (!(object instanceof Field)) {
542                                 return false;
543                         }
544                         Field field = (Field) object;
545                         return id.equals(field.id);
546                 }
547
548                 /**
549                  * {@inheritDoc}
550                  */
551                 @Override
552                 public int hashCode() {
553                         return id.hashCode();
554                 }
555
556         }
557
558         /**
559          * Exception that signals the addition of a field that already exists.
560          *
561          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
562          */
563         public static class DuplicateField extends IllegalArgumentException { }
564
565 }