Job.java
package com.wilzwert.myjobs.core.domain.model.job;
import com.wilzwert.myjobs.core.domain.model.activity.exception.ActivityNotFoundException;
import com.wilzwert.myjobs.core.domain.shared.exception.ValidationException;
import com.wilzwert.myjobs.core.domain.model.activity.Activity;
import com.wilzwert.myjobs.core.domain.model.activity.ActivityType;
import com.wilzwert.myjobs.core.domain.model.attachment.Attachment;
import com.wilzwert.myjobs.core.domain.model.DomainEntity;
import com.wilzwert.myjobs.core.domain.model.user.UserId;
import com.wilzwert.myjobs.core.domain.shared.validation.ValidationErrors;
import com.wilzwert.myjobs.core.domain.shared.validation.Validator;
import java.time.Instant;
import java.util.*;
/**
* @author Wilhelm Zwertvaegher
*/
public class Job extends DomainEntity<JobId> {
private final JobId id;
private final String url;
private final JobStatus status;
private final String title;
private final String company;
private final String description;
private final String profile;
private final String comment;
private final String salary;
private final JobRating rating;
private final Instant createdAt;
private final Instant updatedAt;
private final Instant statusUpdatedAt;
private final Instant followUpReminderSentAt;
private final UserId userId;
private final List<Activity> activities;
private final List<Attachment> attachments;
private static final Map<ActivityType, JobStatus> activityToStatus = Map.of(
ActivityType.APPLICATION, JobStatus.PENDING,
ActivityType.APPLICANT_REFUSAL, JobStatus.APPLICANT_REFUSED,
ActivityType.COMPANY_REFUSAL, JobStatus.COMPANY_REFUSED,
ActivityType.RELAUNCH, JobStatus.RELAUNCHED
);
public static Builder builder() {
return new Builder();
}
public static Builder from(Job job) {
return new Builder(job, true);
}
/**
* Warning : this is used to get a Builder allowing an incomplete aggregate
* Use with great caution, as some methods on the aggregate won't work if it is not complete !
* @param job the job we want to get a Builder from
* @return the Builder
*/
public static Builder fromMinimal(Job job) {
return new Job.Builder(job, false);
}
public static class Builder {
private JobId id;
private String url;
private JobStatus status;
private String title;
private String company;
private String description;
private String profile;
private String comment;
private String salary;
private JobRating rating;
private Instant createdAt;
private Instant updatedAt;
private Instant statusUpdatedAt;
private Instant followUpReminderSentAt;
private UserId userId;
private List<Activity> activities;
private List<Attachment> attachments;
public Builder() {
}
public Builder(Job job, boolean full) {
this.id = job.getId();
this.url = job.getUrl();
this.status = job.getStatus();
this.title = job.getTitle();
this.company = job.getCompany();
this.description = job.getDescription();
this.profile = job.getProfile();
this.comment = job.getComment();
this.salary = job.getSalary();
this.rating = job.getRating();
this.createdAt = job.getCreatedAt();
this.updatedAt = job.getUpdatedAt();
this.statusUpdatedAt = job.getStatusUpdatedAt();
this.followUpReminderSentAt = job.getFollowUpReminderSentAt();
this.userId = job.getUserId();
this.activities = full || job.activities != null ? job.getActivities() : null;
this.attachments = full || job.attachments != null ? job.getAttachments() : null;
}
public Builder id(JobId id) {
this.id = id;
return this;
}
public Builder url(String url) {
this.url = url;
return this;
}
public Builder status(JobStatus status) {
this.status = status;
return this;
}
public Builder title(String title) {
this.title = title;
return this;
}
public Builder company(String company) {
this.company = company;
return this;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Builder profile(String profile) {
this.profile = profile;
return this;
}
public Builder comment(String comment) {
this.comment = comment;
return this;
}
public Builder salary(String salary) {
this.salary = salary;
return this;
}
public Builder rating(JobRating rating) {
this.rating = rating;
return this;
}
public Builder createdAt(Instant createdAt) {
this.createdAt = createdAt;
return this;
}
public Builder updatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
return this;
}
public Builder statusUpdatedAt(Instant updatedAt) {
this.statusUpdatedAt = updatedAt;
return this;
}
public Builder followUpReminderSentAt(Instant followUpReminderSentAt) {
this.followUpReminderSentAt = followUpReminderSentAt;
return this;
}
public Builder userId(UserId userId) {
this.userId = userId;
return this;
}
public Builder activities(List<Activity> activities) {
this.activities = activities;
return this;
}
public Builder attachments(List<Attachment> attachments) {
this.attachments = attachments;
return this;
}
public Job build() {
return new Job(this);
}
}
private ValidationErrors validate() {
return new Validator()
.requireNotEmpty("id", id)
.requireNotEmpty("userId", userId)
.requireNotEmpty("title", title)
.requireNotEmpty("description", description)
.requireNotEmpty("company", company)
.requireValidUrl("url", url)
.getErrors();
}
private Job(Builder builder) {
this.id = builder.id != null ? builder.id : JobId.generate();
this.url = builder.url;
this.status = builder.status != null ? builder.status : JobStatus.CREATED;
this.title = builder.title;
this.company = builder.company;
this.description = builder.description;
this.profile = builder.profile;
this.comment = builder.comment;
this.salary = builder.salary;
this.rating = builder.rating != null ? builder.rating : JobRating.of(0);
this.createdAt = builder.createdAt != null ? builder.createdAt : Instant.now();
this.updatedAt = builder.updatedAt != null ? builder.updatedAt : Instant.now();
this.statusUpdatedAt = builder.statusUpdatedAt != null ? builder.statusUpdatedAt : Instant.now();
this.followUpReminderSentAt = builder.followUpReminderSentAt;
this.userId = builder.userId;
// ensure immutability
// null is accepted because in some cases we allow working on aggregates that could be "incomplete"
this.activities = builder.activities != null ? List.copyOf(builder.activities) : null;
// ensure immutability
// null is accepted because in some cases we allow working on aggregates that could be "incomplete"
this.attachments = builder.attachments != null ? List.copyOf(builder.attachments) : null;
ValidationErrors validationErrors = validate();
if(validationErrors.hasErrors()) {
throw new ValidationException(validationErrors);
}
}
public static Job create(Job.Builder builder) {
builder.attachments(Collections.emptyList()).activities(Collections.emptyList());
return new Job(builder);
}
private void requireFull() {
requireLoadedProperty(attachments);
requireLoadedProperty(activities);
}
private Job copy(List<Attachment> attachments, List<Activity> activities, JobStatus status, Instant updatedAt) {
Instant newStatusUpdatedAt = getStatusUpdatedAt();
if( status != null && !status.equals(getStatus())) {
// set statusUpdatedAt
newStatusUpdatedAt = Instant.now();
}
return new Job(
from(this)
.status(status != null ? status : getStatus())
.updatedAt(updatedAt != null ? updatedAt : getUpdatedAt())
.statusUpdatedAt(newStatusUpdatedAt)
.activities(activities != null ? activities : getActivities())
.attachments(attachments != null ? attachments : getAttachments())
);
}
public Job addActivity(Activity activity) {
requireFull();
JobStatus newJobStatus = activityToStatus.get(activity.getType());
var updatedActivities = new ArrayList<>(getActivities());
updatedActivities.add(activity);
updatedActivities.sort(Comparator.comparing(Activity::getCreatedAt).reversed());
return copy(null, updatedActivities, newJobStatus, Instant.now());
}
public Job updateActivity(Activity activity) {
requireFull();
Activity oldActivity = activities.stream().filter(a -> a.equals(activity)).findFirst().orElseThrow(ActivityNotFoundException::new);
JobStatus newJobStatus = activityToStatus.get(activity.getType());
// create new activity based on the old one and the activity passed
Activity updateActivity = Activity.from(oldActivity)
.updatedAt(Instant.now())
.type(activity.getType())
.comment(activity.getComment())
.build();
// update activities list
var updatedActivities = new ArrayList<>(getActivities());
updatedActivities.remove(oldActivity);
updatedActivities.add(updateActivity);
updatedActivities.sort(Comparator.comparing(Activity::getCreatedAt).thenComparing(Activity::getUpdatedAt).reversed());
// return a copy of this job with updated data
return copy(null, updatedActivities, newJobStatus, Instant.now());
}
public Job addAttachment(Attachment attachment) {
requireFull();
var updatedAttachments = new ArrayList<>(getAttachments());
updatedAttachments.add(attachment);
updatedAttachments.sort(Comparator.comparing(Attachment::getCreatedAt).reversed());
return copy(updatedAttachments, null, null, Instant.now());
}
public Job removeAttachment(Attachment attachment) {
requireFull();
var updatedAttachments = new ArrayList<>(getAttachments());
if(!updatedAttachments.contains(attachment)) {
throw new IllegalArgumentException("Attachment not in list");
}
updatedAttachments.remove(attachment);
return copy(updatedAttachments, null, null, Instant.now());
}
public Job updateStatus(JobStatus newStatus) {
if(this.status == newStatus) return this;
requireFull();
Job result;
ActivityType activityType = activityToStatus.entrySet().stream().filter(entry -> newStatus.equals(entry.getValue())).map(Map.Entry::getKey).findFirst().orElse(null);
Activity activity = !activities.isEmpty() ? activities.getLast() : null;
if(activityType != null && (activity == null || !activity.getType().equals(activityType))) {
// create activity
Activity newActivity = Activity.builder().type(activityType).build();
result = addActivity(newActivity);
}
else {
result = this;
}
return result.copy(null, result.getActivities(), newStatus, Instant.now());
}
public Job updateRating(JobRating newJobRating) {
if(newJobRating.equals(getRating())) return this;
requireFull();
Job result = new Job(
from(this)
.rating(newJobRating)
.updatedAt(Instant.now())
);
Activity newActivity = Activity.builder().type(ActivityType.RATING).comment(""+newJobRating.getValue()).build();
return result.addActivity(newActivity);
}
public Job saveFollowUpReminderSentAt() {
return new Job(
from(this)
.followUpReminderSentAt(Instant.now())
);
}
public JobId getId() {
return id;
}
public String getUrl() {
return url;
}
public JobStatus getStatus() {
return status;
}
public String getTitle() {
return title;
}
public String getCompany() {
return company;
}
public String getDescription() {
return description;
}
public String getProfile() {
return profile;
}
public String getComment() { return comment; }
public String getSalary() { return salary; }
public JobRating getRating() {
return rating;
}
public Instant getCreatedAt() {return createdAt;}
public Instant getUpdatedAt() {
return updatedAt;
}
public Instant getStatusUpdatedAt() {
return statusUpdatedAt;
}
public Instant getFollowUpReminderSentAt() { return followUpReminderSentAt; }
public UserId getUserId() {
return userId;
}
public List<Activity> getActivities() {
return activities;
}
public List<Attachment> getAttachments() {
return attachments;
}
@Override
public String toString() {
return getId().toString()+ " [userId="+getUserId().toString()+",title="+getTitle() + "]";
}
}