2 * Sone - Profile.java - Copyright © 2010–2013 David Roden
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.
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.
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/>.
18 package net.pterodactylus.sone.data;
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;
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.List;
28 import java.util.UUID;
30 import com.google.common.base.Optional;
31 import com.google.common.hash.Hasher;
32 import com.google.common.hash.Hashing;
35 * A profile stores personal information about a {@link Sone}. All information
36 * is optional and can be {@code null}.
38 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
40 public class Profile implements Fingerprintable {
42 /** The Sone this profile belongs to. */
43 private final Sone sone;
45 private volatile Name name;
47 /** The day of the birth date. */
48 private volatile Integer birthDay;
50 /** The month of the birth date. */
51 private volatile Integer birthMonth;
53 /** The year of the birth date. */
54 private volatile Integer birthYear;
56 /** The ID of the avatar image. */
57 private volatile String avatar;
59 /** Additional fields in the profile. */
60 private final List<Field> fields = Collections.synchronizedList(new ArrayList<Field>());
63 * Creates a new empty profile.
66 * The Sone this profile belongs to
68 public Profile(Sone sone) {
73 * Creates a copy of a profile.
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);
93 * Returns the Sone this profile belongs to.
95 * @return The Sone this profile belongs to
97 public Sone getSone() {
102 * Returns the first name.
104 * @return The first name
106 public String getFirstName() {
107 return name.getFirst().orNull();
111 * Returns the middle name(s).
113 * @return The middle name
115 public String getMiddleName() {
116 return name.getMiddle().orNull();
120 * Returns the last name.
122 * @return The last name
124 public String getLastName() {
125 return name.getLast().orNull();
129 * Returns the day of the birth date.
131 * @return The day of the birth date (from 1 to 31)
133 public Integer getBirthDay() {
138 * Sets the day of the birth date.
141 * The day of the birth date (from 1 to 31)
142 * @return This profile (for method chaining)
144 public Profile setBirthDay(Integer birthDay) {
145 this.birthDay = birthDay;
150 * Returns the month of the birth date.
152 * @return The month of the birth date (from 1 to 12)
154 public Integer getBirthMonth() {
159 * Sets the month of the birth date.
162 * The month of the birth date (from 1 to 12)
163 * @return This profile (for method chaining)
165 public Profile setBirthMonth(Integer birthMonth) {
166 this.birthMonth = birthMonth;
171 * Returns the year of the birth date.
173 * @return The year of the birth date
175 public Integer getBirthYear() {
180 * Returns the ID of the currently selected avatar image.
182 * @return The ID of the currently selected avatar image, or {@code null} if
183 * no avatar is selected.
185 public String getAvatar() {
190 * Sets the avatar image.
193 * The new avatar image, or {@code null} to not select an avatar
197 public Profile setAvatar(Image avatar) {
198 if (avatar == null) {
202 checkArgument(avatar.getSone().equals(sone), "avatar must belong to Sone");
203 this.avatar = avatar.getId();
208 * Sets the year of the birth date.
211 * The year of the birth date
212 * @return This profile (for method chaining)
214 public Profile setBirthYear(Integer birthYear) {
215 this.birthYear = birthYear;
220 * Returns the fields of this profile.
222 * @return The fields of this profile
224 public List<Field> getFields() {
225 return new ArrayList<Field>(fields);
229 * Returns whether this profile contains the given field.
232 * The field to check for
233 * @return {@code true} if this profile contains the field, false otherwise
235 public boolean hasField(Field field) {
236 return fields.contains(field);
240 * Returns the field with the given ID.
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
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)) {
258 * Returns the field with the given name.
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
265 public Field getFieldByName(String fieldName) {
266 for (Field field : fields) {
267 if (field.getName().equals(fieldName)) {
275 * Appends a new field to the list of fields.
278 * The name of the new field
279 * @return The new field
280 * @throws IllegalArgumentException
281 * if the name is not valid
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);
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).
299 * The field to move up
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);
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).
316 * The field to move down
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);
328 * Removes the given field.
331 * The field to remove
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);
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();
346 public Modifier setFirstName(String firstName) {
347 this.firstName = fromNullable(firstName);
352 public Modifier setMiddleName(String middleName) {
353 this.middleName = fromNullable(middleName);
358 public Modifier setLastName(String lastName) {
359 this.lastName = fromNullable(lastName);
364 public Profile update() {
365 Profile.this.name = new Name(firstName, middleName, lastName);
371 public interface Modifier {
373 Modifier setFirstName(String firstName);
374 Modifier setMiddleName(String middleName);
375 Modifier setLastName(String lastName);
385 * Returns the index of the field with the given name.
388 * The name of the field
389 * @return The index of the field, or {@code -1} if there is no field with
392 private int getFieldIndex(Field field) {
393 return fields.indexOf(field);
397 // INTERFACE Fingerprintable
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(")");
411 if (birthMonth != null) {
412 hash.putString("BirthMonth(").putInt(birthMonth).putString(")");
414 if (birthYear != null) {
415 hash.putString("BirthYear(").putInt(birthYear).putString(")");
417 if (avatar != null) {
418 hash.putString("Avatar(").putString(avatar).putString(")");
420 hash.putString("ContactInformation(");
421 for (Field field : fields) {
422 hash.putString(field.getName()).putString("(").putString(field.getValue()).putString(")");
427 return hash.hash().toString();
431 * Container for a profile field.
433 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
437 /** The ID of the field. */
438 private final String id;
440 /** The name of the field. */
443 /** The value of the field. */
444 private String value;
447 * Creates a new field with a random ID.
450 this(UUID.randomUUID().toString());
454 * Creates a new field with the given ID.
457 * The ID of the field
459 private Field(String id) {
460 this.id = checkNotNull(id, "id must not be null");
464 * Returns the ID of this field.
466 * @return The ID of this field
468 public String getId() {
473 * Returns the name of this field.
475 * @return The name of this field
477 public String getName() {
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
487 * The new name of this field
490 public Field setName(String name) {
491 checkNotNull(name, "name must not be null");
492 checkArgument(getFieldByName(name) == null, "name must be unique");
498 * Returns the value of this field.
500 * @return The value of this field
502 public String getValue() {
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!
512 * The new value of this field
515 public Field setValue(String value) {
528 public boolean equals(Object object) {
529 if (!(object instanceof Field)) {
532 Field field = (Field) object;
533 return id.equals(field.id);
540 public int hashCode() {
541 return id.hashCode();
546 public static class Name implements Fingerprintable {
548 private final Optional<String> first;
549 private final Optional<String> middle;
550 private final Optional<String> last;
552 public Name(Optional<String> first, Optional<String> middle, Optional<String> last) {
554 this.middle = middle;
558 public Optional<String> getFirst() {
562 public Optional<String> getMiddle() {
566 public Optional<String> getLast() {
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(")");
577 if (middle.isPresent()) {
578 hash.putString("Middle(").putString(middle.get()).putString(")");
580 if (last.isPresent()) {
581 hash.putString("Last(").putString(last.get()).putString(")");
584 return hash.hash().toString();