Descriptor.java

/*
 * Copyright 2018 Global Crop Diversity Trust
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.genesys.server.model.traits;

import com.fasterxml.jackson.annotation.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.genesys.blocks.annotations.NotCopyable;
import org.genesys.blocks.auditlog.annotations.Audited;
import org.genesys.blocks.model.*;
import org.genesys.blocks.security.model.AclAwareModel;
import org.genesys.filerepository.model.RepositoryImage;
import org.genesys.server.model.Partner;
import org.genesys.server.model.PublishState;
import org.genesys.server.model.dataset.Dataset;
import org.genesys.server.model.impl.Crop;
import org.genesys.server.model.impl.TranslatedUuidModel;
import org.genesys.server.model.vocab.ControlledVocabulary;
import org.genesys.server.model.vocab.VocabularyTerm;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * A single descriptor definition belonging to a single "owner".
 *
 * @author Matija Obreza
 */
@Entity
@Cacheable
@Table(name = "descriptor")
@Audited
@Document(indexName = "descriptor")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "uuid", scope = Descriptor.class)
@Getter
@Setter
@NoArgsConstructor
public class Descriptor extends TranslatedUuidModel<DescriptorLang, Descriptor> implements SelfCleaning, Publishable, Copyable<Descriptor>, AclAwareModel {

	/** The Constant serialVersionUID. */
	private static final long serialVersionUID = 7307818236681549484L;

	/**
	 * Descriptor data types.
	 */
	public static enum DataType {
		/** The numeric. */
		NUMERIC,
		/** Free text. */
		TEXT,

		/** Scale (0-9 scale) type. */
		SCALE,
		/** Coded descriptors using controlled vocabularies. */
		CODED,
		/** Yes/No type. */
		BOOLEAN,

		/** Dates. */
		DATE
	}

	/**
	 * Descriptor classification.
	 */
	public static enum Category {

		/** Passport descriptor. */
		PASSPORT,

		/** Management descriptor. */
		MANAGEMENT,

		/** Environment and Site descriptor. */
		ENVIRONMENT,

		/** Characterization descriptor. */
		CHARACTERIZATION,

		/** Evaluation descriptor. */
		EVALUATION,

		/** Abiotic stress descriptor. */
		ABIOTICSTRESS,

		/** Biotic stress descriptor. */
		BIOTICSTRESS,

		/** Molecular marker descriptor. */
		MOLECULAR
	}

	/**
	 * User-specified version tag. E.g. "1.0", "1.1"
	 */
	@NotNull
	@Column(nullable = false, updatable = false)
	private String versionTag;

	/**
	 * Trait title in English.
	 */
	@NotNull
	@Column(nullable = false)
	private String title;

	/**
	 * Trait description in English.
	 */
	@Lob
	private String description;

	/** Descriptor data type. */
	@NotNull
	@Enumerated(EnumType.STRING)
	@Column(nullable = false)
	private DataType dataType;

	/** The key. */
	@NotNull
	@Column(name = "keyDescriptor", nullable = false)
	private boolean key;

	/** The publisher. */
	@Size(max = 200)
	@Column(length = 200)
	private String publisher;

	/** Not published by default. */
	@Enumerated(EnumType.ORDINAL)
	private PublishState state = PublishState.DRAFT;

	/** Allow only integers, no decimal numbers. */
	private Boolean integerOnly;

	/** Data constraints: minimum allowed numeric values. */
	@Column(name = "min_value")
	private Double minValue;

	/** Data constraints: maximum allowed numeric values. */
	@Column(name = "max_value")
	private Double maxValue;

	/** The uom. */
	@Size(max = 20)
	@Column(name = "uom", length = 20)
	private String uom;

	/** The preferred column name in databases and spreadsheets. */
	@Size(max = 50)
	@Column(name = "columnName", length = 50)
	private String columnName;

	/** The bibliographic citation. */
	@Column(name = "bibliographicCitation")
	@Lob
	private String bibliographicCitation;

	/** The shared controlled vocabulary used by the descriptor */
	@ManyToOne(cascade = {}, optional = true)
	@JoinColumn(name = "vocabularyId")
	@JsonIdentityReference(alwaysAsId = false)
	@JsonIgnoreProperties(value = { "terms" })
	@JsonView({ JsonViews.Public.class })
	@Field(type = FieldType.Nested)
	private ControlledVocabulary vocabulary;

	/**
	 * Vocabulary terms specific to this descriptor (99% of the cases).
	 */
	@OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.REFRESH }, orphanRemoval = true)
	@JoinTable(name = "descriptor_term", joinColumns = @JoinColumn(name = "descriptorId"),
			// other side
			inverseJoinColumns = @JoinColumn(name = "termId"),
			// Index
			indexes = { @Index(columnList = "descriptorId, idx") },
			// unique constraints
			uniqueConstraints = { })
	@OrderColumn(name = "idx")
	@JsonView({ JsonViews.Public.class })
	@Field(type = FieldType.Nested)
	private List<VocabularyTerm> terms;

	/** The owner. */
	@NotNull
	@JsonIdentityReference(alwaysAsId = false)
	@ManyToOne(cascade = {}, optional = false)
	@JoinColumn(name = "partnerId", updatable = false)
	@JsonView({ JsonViews.Public.class })
	@Field(type = FieldType.Object)
	private Partner owner;

	/** The descriptor lists. */
	@JsonIdentityReference(alwaysAsId = false)
	@ManyToMany(fetch = FetchType.LAZY, mappedBy = "descriptors", cascade = { CascadeType.REFRESH })
	@JsonIgnore
	private List<DescriptorList> descriptorLists;

	/** The datasets. */
	@ManyToMany(fetch = FetchType.LAZY, cascade = {}, mappedBy = "descriptors")
	@JsonIgnore
	private List<Dataset> datasets;

	/** The crop. */
	@Size(max = Crop.CROP_SHORTNAME_LENGTH)
	@Column(name = "crop", length = Crop.CROP_SHORTNAME_LENGTH, nullable = true)
	private String crop;

	/** Descriptor classification. */
	@NotNull
	@Enumerated(EnumType.STRING)
	@Column(length = 20, nullable = false)
	private Category category;

	@OneToOne(fetch = FetchType.LAZY, cascade = { }, optional = true, orphanRemoval = false)
	@JoinColumn(name = "imageId", unique = true)
	private RepositoryImage image;

	@Column(name = "firstPublishedDate")
	@NotCopyable
	private Instant firstPublishedDate;

	/**
	 * Pre-persist, pre-update
	 */
	@PrePersist
	@PreUpdate
	private void preupdate() {
		if (Objects.isNull(firstPublishedDate) && Objects.equals(state, PublishState.PUBLISHED)) {
			firstPublishedDate = Instant.now();
		}
		trimStringsToNull();
	}
	
	@PreRemove
	private void preventRemoveWithImage() {
		if (image != null) {
			throw new DataIntegrityViolationException("Refusing to delete Descriptor with image");
		}
	}

	/**
	 * Owner is the ACL parent object for the descriptor
	 */
	@Override
	public AclAwareModel aclParentObject() {
		return null;
	}

	/**
	 * Instantiates a new descriptor.
	 *
	 * @param descriptor the descriptor
	 */
	public Descriptor(final Descriptor descriptor) {
		this(descriptor.getVersionTag(), descriptor.getTitle(), descriptor.description, descriptor.getDataType(), descriptor.getState(), descriptor.isKey(), descriptor
			.getIntegerOnly(), descriptor.getMinValue(), descriptor.getMaxValue(), descriptor.getColumnName(), descriptor.getUom(), descriptor.getVocabulary(), descriptor
				.getOwner(), descriptor.getDescriptorLists(), descriptor.getImage());
	}

	/**
	 * Instantiates a new descriptor.
	 *
	 * @param versionTag the version tag
	 * @param title the title
	 * @param description the description
	 * @param dataType the data type
	 * @param state the publish state
	 * @param key the key
	 * @param integerOnly the integer only
	 * @param minValue the min value
	 * @param maxValue the max value
	 * @param columnName the column name
	 * @param uom the uom
	 * @param vocabulary the vocabulary
	 * @param owner the owner
	 * @param descriptorLists the descriptor lists
	 * @param image the image
	 */
	public Descriptor(final String versionTag, final String title, final String description, final DataType dataType, final PublishState state, final boolean key,
			final Boolean integerOnly, final Double minValue, final Double maxValue, final String columnName, final String uom, final ControlledVocabulary vocabulary,
			final Partner owner, final List<DescriptorList> descriptorLists, final RepositoryImage image) {
		this.versionTag = versionTag;
		this.title = title;
		this.description = description;
		this.dataType = dataType;
		this.state = state;
		this.key = key;
		this.integerOnly = integerOnly;
		this.minValue = minValue;
		this.maxValue = maxValue;
		this.columnName = columnName;
		this.uom = uom;
		this.vocabulary = vocabulary;
		this.owner = owner;
		this.descriptorLists = descriptorLists;
		this.image = image;
	}

	/**
	 * Checks if is published.
	 *
	 * @return the published
	 */
	@Override
	public boolean isPublished() {
		return this.state == PublishState.PUBLISHED;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.genesys.blocks.model.Copyable#copy()
	 */
	@Override
	public Descriptor copy() {
		return null;
	}

	/*
	 * (non-Javadoc)
	 * @see org.genesys.blocks.model.Copyable#apply(java.lang.Object)
	 */
	@Override
	public Descriptor apply(final Descriptor source) {
		Copyable.super.apply(source);

		if (source.getTerms() != null) {
			if (this.terms == null) {
				this.terms = new ArrayList<>(source.getTerms());
			} else {
				this.terms.clear();
				this.terms.addAll(source.getTerms());
			}
		}

		// Check that CODED has terms or vocabulary
		if (this.dataType == DataType.CODED && (this.terms == null || this.terms.size() == 0) && this.vocabulary == null) {
			throw new DataIntegrityViolationException("Coded descriptor " + this.title + " requires terms or a vocabulary");
		}

		// Check that SCALE has terms
		if (this.dataType == DataType.SCALE && (this.terms == null || this.terms.size() == 0)) {
			throw new DataIntegrityViolationException("Scale descriptor " + this.title + " requires terms");
		}

		// Check that SCALE has min and max values
		if (this.dataType == DataType.SCALE && (this.minValue == null || this.maxValue == null)) {
			throw new DataIntegrityViolationException("Scale descriptor " + this.title + " requires min and max values.");
		}

		switch (this.dataType) {
		case BOOLEAN:
			this.setVocabulary(null);
			this.setMinValue(null);
			this.setMaxValue(null);
			break;
		case CODED:
			this.setMinValue(null);
			this.setMaxValue(null);
			break;
		case DATE:
		case TEXT:
		case NUMERIC:
		case SCALE:
			this.setVocabulary(null);
			break;
		default:
			break;
		}

		// When using a specified vocabulary
		if (this.vocabulary != null && this.terms != null) {
			// We don't use our terms
			this.terms.clear();
		}

		return this;
	}

	// @Override // TODO Introduce LazyLoadable
	public void lazyLoad() {
		if (this.getOwner() != null) {
			this.getOwner().getId();
			// System.err.println("Owner is " + this.getOwner());
		} else {
			// System.err.println("Owner is null");
		}

		if (this.getDescriptorLists() != null) {
			this.getDescriptorLists().size();
		}

		if (this.getDataType() == DataType.CODED || this.getDataType() == DataType.SCALE) {
			if (this.getVocabulary() != null) {
				this.getVocabulary().getId();
				if (this.getVocabulary().getTerms() != null) {
					this.getVocabulary().getTerms().size();
				}
				this.setTerms(null);
			}
			if (this.getTerms() != null) {
				this.getTerms().size();
			}
		} else {
			// clear terms list
			if (this.getTerms() != null) {
				this.getTerms().clear();
			}
		}

		if (this.getImage() != null) {
			this.getImage().getId();
		}
	}

	@Override
	public boolean canEqual(Object other) {
		return other instanceof Descriptor;
	}
}