HsBookingItem.java

package net.hostsharing.hsadminng.hs.booking.item;

import io.hypersistence.utils.hibernate.type.json.JsonType;
import io.hypersistence.utils.hibernate.type.range.PostgreSQLRangeType;
import io.hypersistence.utils.hibernate.type.range.Range;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.Type;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.OneToMany;
import jakarta.persistence.PostLoad;
import jakarta.persistence.Transient;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import static java.util.Collections.emptyMap;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange;
import static net.hostsharing.hsadminng.repr.Stringify.stringify;

@MappedSuperclass
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@SuperBuilder(builderMethodName = "baseBuilder", toBuilder = true)
public abstract class HsBookingItem implements Stringifyable, BaseEntity<HsBookingItem>, PropertiesProvider {

    private static Stringify<HsBookingItem> stringify = stringify(HsBookingItem.class)
            .withProp(HsBookingItem::getType)
            .withProp(HsBookingItem::getCaption)
            .withProp(HsBookingItem::getProject)
            .withProp(e -> e.getValidity().asString())
            .withProp(HsBookingItem::getResources)
            .quotedValues(false);

    @Id
    @GeneratedValue
    private UUID uuid;

    @Version
    private int version;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "projectuuid")
    private HsBookingProjectRealEntity project;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parentitemuuid")
    private HsBookingItemRealEntity parentItem;

    @NotNull
    @Column(name = "type")
    @Enumerated(EnumType.STRING)
    private HsBookingItemType type;

    @Builder.Default
    @Type(PostgreSQLRangeType.class)
    @Column(name = "validity", columnDefinition = "daterange")
    private Range<LocalDate> validity = Range.closedInfinite(LocalDate.now());

    @Column(name = "caption")
    private String caption;

    @Builder.Default
    @Setter(AccessLevel.NONE)
    @Type(JsonType.class)
    @Column(columnDefinition = "resources")
    private Map<String, Object> resources = new HashMap<>();

    @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true)
    @JoinColumn(name = "parentitemuuid", referencedColumnName = "uuid")
    private List<HsBookingItemRealEntity> subBookingItems;

    @Transient
    private PatchableMapWrapper<Object> resourcesWrapper;

    @Transient
    private boolean isLoaded;

    @PostLoad
    public void markAsLoaded() {
        this.isLoaded = true;
    }

    public PatchableMapWrapper<Object> getResources() {
        return PatchableMapWrapper.of(resourcesWrapper, (newWrapper) -> {resourcesWrapper = newWrapper;}, resources);
    }

    public void putResources(Map<String, Object> newResources) {
        getResources().assign(newResources);
    }

    public void setValidFrom(final LocalDate validFrom) {
        setValidity(toPostgresDateRange(validFrom, getValidTo()));
    }

    public void setValidTo(final LocalDate validTo) {
        setValidity(toPostgresDateRange(getValidFrom(), validTo));
    }

    public LocalDate getValidFrom() {
        return lowerInclusiveFromPostgresDateRange(getValidity());
    }

    public LocalDate getValidTo() {
        return upperInclusiveFromPostgresDateRange(getValidity());
    }

    @Override
    public PatchableMapWrapper<Object> directProps() {
        return getResources();
    }

    @Override
    public Object getContextValue(final String propName) {
        final var v = resources.get(propName);
        if (v != null) {
            return v;
        }
        if (parentItem != null) {
            return parentItem.getResources().get(propName);
        }
        return emptyMap();
    }

    @Override
    public String toString() {
        return stringify.apply(this);
    }

    @Override
    public String toShortString() {
        return ofNullable(getRelatedProject()).map(HsBookingProject::toShortString).orElse("D-???????-?") +
                ":" + caption;
    }

    public HsBookingProject getRelatedProject() {
        return project != null ? project
                : parentItem != null ? parentItem.getRelatedProject()
                : null; // can be the case for technical assets like IP-numbers
    }
}