Java tutorial
/******************************************************************************* * Copyright (c) 2015, 2018 Stichting Yona Foundation This Source Code Form is subject to the terms of the Mozilla Public License, * v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. *******************************************************************************/ package nu.yona.server.subscriptions.entities; import java.time.LocalDateTime; import java.util.HashSet; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.function.Function; import java.util.function.Supplier; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Convert; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.OneToMany; import javax.persistence.Table; import org.hibernate.annotations.BatchSize; import org.hibernate.annotations.Type; import org.springframework.core.annotation.Order; import nu.yona.server.Translator; import nu.yona.server.crypto.seckey.DateTimeFieldEncryptor; import nu.yona.server.crypto.seckey.UUIDFieldEncryptor; import nu.yona.server.device.entities.BuddyDevice; import nu.yona.server.entities.RepositoryProvider; import nu.yona.server.subscriptions.entities.BuddyAnonymized.Status; import nu.yona.server.subscriptions.service.migration.EncryptFirstAndLastName; import nu.yona.server.util.TimeUtil; @Entity @Table(name = "BUDDIES") public class Buddy extends PrivateUserProperties { private static final int VERSION_OF_NAME_MIGRATION = EncryptFirstAndLastName.class.getAnnotation(Order.class) .value() + 1; @Type(type = "uuid-char") @Column(name = "owning_user_private_id") private UUID owningUserPrivateId; @Convert(converter = UUIDFieldEncryptor.class) private UUID userId; @Convert(converter = UUIDFieldEncryptor.class) private UUID buddyAnonymizedId; @Convert(converter = DateTimeFieldEncryptor.class) private LocalDateTime lastStatusChangeTime; /** * The BuddyDevice entities owned by this user */ @BatchSize(size = 20) @OneToMany(mappedBy = "owningBuddy", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) private Set<BuddyDevice> devices; // Default constructor is required for JPA public Buddy() { super(); } private Buddy(UUID id, UUID userId, String firstName, String lastName, String nickname, UUID buddyAnonymizedId) { super(id, firstName, lastName, Objects.requireNonNull(nickname)); this.userId = Objects.requireNonNull(userId); this.buddyAnonymizedId = Objects.requireNonNull(buddyAnonymizedId); this.devices = new HashSet<>(); setLastStatusChangeTimeToNow(); } public static BuddyRepository getRepository() { return (BuddyRepository) RepositoryProvider.getRepository(Buddy.class, UUID.class); } public static Buddy createInstance(UUID buddyUserId, String firstName, String lastName, String nickname, Status sendingStatus, Status receivingStatus) { BuddyAnonymized buddyAnonymized = BuddyAnonymized.createInstance(sendingStatus, receivingStatus); buddyAnonymized = BuddyAnonymized.getRepository().save(buddyAnonymized); return new Buddy(UUID.randomUUID(), buddyUserId, firstName, lastName, nickname, buddyAnonymized.getId()); } public UUID getBuddyAnonymizedId() { return buddyAnonymizedId; } public BuddyAnonymized getBuddyAnonymized() { BuddyAnonymized buddyAnonymized = BuddyAnonymized.getRepository().findOne(buddyAnonymizedId); assert buddyAnonymized != null : "Buddy with ID " + getId() + " cannot find buddy anonymized with ID " + buddyAnonymizedId; return buddyAnonymized; } public UUID getUserId() { return userId; } public User getUser() { return User.getRepository().findOne(userId); } public Optional<UUID> getUserAnonymizedId() { return getBuddyAnonymized().getUserAnonymizedId(); } public Status getReceivingStatus() { return getBuddyAnonymized().getReceivingStatus(); } public void setReceivingStatus(Status status) { BuddyAnonymized buddyAnonymized = getBuddyAnonymized(); buddyAnonymized.setReceivingStatus(status); BuddyAnonymized.getRepository().save(buddyAnonymized); setLastStatusChangeTimeToNow(); } public void setSendingStatus(Status status) { BuddyAnonymized buddyAnonymized = getBuddyAnonymized(); buddyAnonymized.setSendingStatus(status); BuddyAnonymized.getRepository().save(buddyAnonymized); setLastStatusChangeTimeToNow(); } public Status getSendingStatus() { return getBuddyAnonymized().getSendingStatus(); } public LocalDateTime getLastStatusChangeTime() { return lastStatusChangeTime; } private void setLastStatusChangeTimeToNow() { lastStatusChangeTime = TimeUtil.utcNow(); } public void setUserAnonymizedId(UUID userAnonymizedId) { BuddyAnonymized buddyAnonymized = getBuddyAnonymized(); buddyAnonymized.setUserAnonymizedId(userAnonymizedId); BuddyAnonymized.getRepository().save(buddyAnonymized); } @Override public Buddy touch() { super.touch(); return this; } /** * @deprecated only for use by migration step. */ @Deprecated public void setLastStatusChangeTime(LocalDateTime lastStatusChangeTime) { this.lastStatusChangeTime = lastStatusChangeTime; } public void addDevice(BuddyDevice device) { devices.add(device); device.setOwningBuddy(this); } public void removeDevice(BuddyDevice device) { boolean removed = devices.remove(device); assert removed; device.clearOwningBuddy(); } public Set<BuddyDevice> getDevices() { return devices; } /** * Determines the first name (see {@link #determineName(Supplier, Optional, Function, String, String)}). * * @param user Optional user * @return The first name or a substitute for it (never null) */ public String determineFirstName(Optional<User> user) { return determineName(this::getFirstName, user, User::getFirstName, "message.alternative.first.name", getNickname()); } /** * Determines the last name (see {@link #determineName(Supplier, Optional, Function, String, String)}). * * @param user Optional user * @return The last name or a substitute for it (never null) */ public String determineLastName(Optional<User> user) { return determineName(this::getLastName, user, User::getLastName, "message.alternative.last.name", getNickname()); } /** * Determines the name of the user (first or last) through an algorithm that ensures the best possible value is returned, but * never null. It first tries calling the getter that is supposed to take it from the buddy entity or message. If that returns * null and a user is given and that user is not yet migrated (i.e. the name is not removed from it), it tries to get that. If * that doesn't return anything either, it builds a string based on the given nickname. * * @param buddyUserNameGetter Getter to fetch the name from the buddy entity or a message * @param user Optional user entity * @param userNameGetter Getter to fetch the name (first or last) from the user entity * @param fallbackMessageId The ID of the translatable message to build the fallback string * @param nickname The nickname to include in the fallback string * @return The name or a substitute for it (never null) */ public static String determineName(Supplier<String> buddyUserNameGetter, Optional<User> user, Function<User, String> userNameGetter, String fallbackMessageId, String nickname) { String name = buddyUserNameGetter.get(); if (name != null) { return name; } if ((user.isPresent()) && (user.get().getPrivateDataMigrationVersion() < VERSION_OF_NAME_MIGRATION)) { // User is not deleted yet and not yet migrated, so get the name from the user entity name = userNameGetter.apply(user.get()); } if (name != null) { return name; } // We're apparently in a migration process to move first and last name to the private data // The app will fetch the message, causing processing of all unprocessed messages. That'll fill in the first and last // name in the buddy entity, so from then onward, the user will see the right data return Translator.getInstance().getLocalizedMessage(fallbackMessageId, nickname); } }