com.confighub.core.repository.Property.java Source code

Java tutorial

Introduction

Here is the source code for com.confighub.core.repository.Property.java

Source

/*
 * This file is part of ConfigHub.
 *
 * ConfigHub is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ConfigHub is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with ConfigHub.If not, see <http://www.gnu.org/licenses/>.
 */

package com.confighub.core.repository;

import com.confighub.core.error.ConfigException;
import com.confighub.core.error.Error;
import com.confighub.core.resolver.Context;
import com.confighub.core.store.diff.PropertyDiffTracker;
import com.confighub.core.utils.Utils;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.Type;
import org.hibernate.envers.AuditTable;
import org.hibernate.envers.Audited;

import javax.persistence.Cacheable;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.PostPersist;
import javax.persistence.PostUpdate;
import javax.persistence.PreRemove;
import javax.persistence.Table;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import java.util.Set;

@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Table(name = "property", indexes = {
        @Index(name = "PROP_repoIndex", columnList = "id, propertyKey_id, repositoryId") })
@NamedQueries({
        @NamedQuery(name = "Property.get", query = "SELECT p FROM Property p WHERE repository=:repository AND id=:id"),

        @NamedQuery(name = "Search.values", query = "SELECT p FROM Property p WHERE "
                + "p.repository=:repository AND UPPER(p.value) LIKE :searchTerm"),

        @NamedQuery(name = "Property.count", query = "SELECT COUNT(c) FROM Property c WHERE c.repository.active=true"), })
@Audited
@AuditTable("property_audit")
@EntityListeners({ PropertyDiffTracker.class })
public class Property extends AContextAwarePersistent {
    private static final Logger log = LogManager.getLogger(Property.class);

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REFRESH })
    @JoinColumn(nullable = false)
    private PropertyKey propertyKey;

    @ManyToOne(fetch = FetchType.LAZY, cascade = { CascadeType.REFRESH, CascadeType.PERSIST })
    @JoinColumn(name = "absoluteFilePath")
    private AbsoluteFilePath absoluteFilePath;

    @Lob
    @Type(type = "org.hibernate.type.TextType")
    @Column(columnDefinition = "TEXT")
    private String value;

    // --------------------------------------------------------------------------------------------
    // Construction
    // --------------------------------------------------------------------------------------------

    protected Property() {
    }

    public Property(final Repository repository) {
        this.repository = repository;
    }

    // --------------------------------------------------------------------------------------------
    // Validation before saving
    // --------------------------------------------------------------------------------------------
    @PreRemove
    public void preRemove() {
        this.propertyKey.removeProperty(this);
    }

    private transient boolean enforced = false;

    /**
     * Rules for a correct property
     * <p>
     * 1. Key cannot be blank;
     * 2. Context cannot contain more than one level with the same depth
     * 3. Property with the same context signature
     *
     * @throws ConfigException
     */
    @PostUpdate
    @PostPersist
    public void enforce() throws ConfigException {
        if (enforced) {
            return;
        }

        // Inactive properties can exist without any restrictions
        if (!this.active) {
            return;
        }

        // 1. Key cannot be blank;
        if (null == propertyKey) {
            throw new ConfigException(Error.Code.KEY_BLANK);
        }

        // 2. Context cannot contain more than one level with the same depth
        validateContext();

        // 3. Property with the same context signature
        for (Property property : propertyKey.getProperties()) {
            if (property.equals(this) || !property.isActive()) {
                continue;
            }

            if (property.getContextWeight() == this.getContextWeight()) {
                if (CollectionUtils.isEqualCollection(this.context, property.getContext())) {
                    throw new ConfigException(Error.Code.PROP_DUPLICATION_CONTEXT);
                }
            }
        }

        // 4. Cross-cluster conflict detection
        if (this.repository.isContextClustersEnabled() && null != this.context
                && this.propertyKey.getProperties().size() > 1) {

            // Check if the key for this property has values with the same context weight, as this is
            // the first indicator of a potential cross-cluster conflict.
            List<AContextAwarePersistent> conflictCandidates = null;
            for (Property property : this.propertyKey.getProperties()) {
                if (property.equals(this)) {
                    continue;
                }

                if (this.contextWeight == property.getContextWeight()) {
                    if (null == conflictCandidates) {
                        conflictCandidates = new ArrayList<>();
                    }
                    conflictCandidates.add(property);
                }
            }

            crossClusterValidation(conflictCandidates);
        }

        if (null != this.absoluteFilePath && this.active
                && PropertyKey.ValueDataType.FileEmbed.equals(this.propertyKey.getValueDataType())) {
            Context context = new Context(null, this.repository, this.getContext(), null);

            HashMap<RepoFile, Property> breadcrumbs = new LinkedHashMap<>();
            checkPropertyCircularReference(context, this, breadcrumbs);
        }

        enforced = true;
    }

    // --------------------------------------------------------------------------------------------
    // PropertyKey management
    // --------------------------------------------------------------------------------------------

    public String getKey() {
        return this.propertyKey.getKey();
    }

    public String getReadme() {
        return this.propertyKey.getReadme();
    }

    public Set<Property> getKeyProperties() {
        return this.propertyKey.getProperties();
    }

    public Long getPropertyKeyId() {
        return this.propertyKey.getId();
    }

    // --------------------------------------------------------------------------------------------
    // POJO Ops
    // --------------------------------------------------------------------------------------------

    @Override
    public String toString() {
        return String.format("[%s] Property[%5d]: %s | key: %s | value: %s | context[%3d]: %s | %s", this.revType,
                this.id, this.isEditable ? "rw" : "ro", this.getKey(), this.getValue(), this.getContextWeight(),
                getContextJson(), null == this.type ? "No type" : this.type.name());
    }

    @Override
    public int hashCode() {
        // Because envers will store deleted item without any data, we have to be ready for this.
        if (Utils.anyNull(repository, propertyKey)) {
            return this.id.intValue();
        }

        return Objects.hash(this.repository.getName(), this.propertyKey.getKey(), this.contextWeight,
                this.getValue());
    }

    @Override
    public boolean equals(Object o) {
        if (null == o || !(o instanceof Property)) {
            return false;
        }
        Property other = (Property) o;

        return Utils.same(this.getContextJson(), other.getContextJson())
                && Utils.same(this.getValue(), other.getValue()) && this.isActive() == other.isActive()
                && Utils.equal(this.id, other.id);
    }

    public JsonObject toJson() {
        JsonObject json = new JsonObject();
        json.addProperty("id", this.getId());
        json.addProperty("vdt", this.getPropertyKey().getValueDataType().name());

        Gson gson = new Gson();
        switch (this.getPropertyKey().getValueDataType()) {
        case Map:
            json.add("value", gson.fromJson(this.getValue(), JsonObject.class));
            break;

        case List:
            json.add("value", gson.fromJson(this.getValue(), JsonArray.class));
            break;

        default:
            json.addProperty("value", this.getValue());
        }

        json.add("levels", gson.fromJson(this.contextJson, JsonArray.class));

        return json;
    }

    // --------------------------------------------------------------------------------------------
    // Setters and getters
    // --------------------------------------------------------------------------------------------

    public Long getId() {
        return id;
    }

    public PropertyKey getPropertyKey() {
        return this.propertyKey;
    }

    public void setReadme(final String readme) {
        this.propertyKey.setReadme(readme);
    }

    /**
     * Set new propertyKey, and disassociate from the old one.
     *
     * @param propertyKey
     */
    public void setPropertyKey(PropertyKey propertyKey) {
        if (null == propertyKey) {
            return;
        }

        if (null != this.propertyKey) {
            if (propertyKey.equals(this.propertyKey)) {
                return;
            }

            this.propertyKey.removeProperty(this);
        }

        this.propertyKey = propertyKey;
        this.propertyKey.addProperty(this);
    }

    public String getValue() {
        if (null != this.absoluteFilePath) {
            return this.absoluteFilePath.getAbsPath();
        }
        return value;
    }

    public void setValue(String value) throws ConfigException {
        setValue(value, null);
    }

    public void decryptValue(final String encryptionSecret) throws ConfigException {
        if (!this.isEncrypted() || decrypted) {
            return;
        }
        if (null == this.absoluteFilePath) {
            this.value = this.getPropertyKey().getSecurityProfile().decrypt(this.getValue(), encryptionSecret);
        }

        this.decrypted = true;
    }

    public void encryptValue(final String encryptionSecret) throws ConfigException {
        if (!this.isEncrypted()) {
            return;
        }

        if (null == this.absoluteFilePath) {
            this.value = this.getPropertyKey().getSecurityProfile().encrypt(this.getValue(), encryptionSecret);
        }
    }

    public AbsoluteFilePath getAbsoluteFilePath() {
        return absoluteFilePath;
    }

    public void setAbsoluteFilePath(AbsoluteFilePath absoluteFilePath) {
        this.absoluteFilePath = absoluteFilePath;
        this.value = null;
    }

    public void setValue(String value, String encryptionSecret) throws ConfigException {
        if (repository.isValueTypeEnabled()) {
            PropertyKey.ValueDataType vdt = this.getPropertyKey().getValueDataType();
            switch (vdt) {
            case Text:
            case Code:
            case FileRef:
            case FileEmbed:
                break; // anything goes.

            case JSON:
                if (null != value) {
                    Utils.isJSONValid(value);
                }
                break;

            case Boolean:
                if (null != value && !"true".equals(value) && !"false".equals(value)) {
                    throw new ConfigException(Error.Code.INVALID_VALUE_FOR_DATA_TYPE);
                }
                break;

            case Integer:
                if (null == value) {
                    break;
                }

                try {
                    Integer.valueOf(value);
                } catch (Exception ignore) {
                    throw new ConfigException(Error.Code.INVALID_VALUE_FOR_DATA_TYPE);
                }
                break;

            case Long:
                if (null == value) {
                    break;
                }
                try {
                    Long.valueOf(value);
                } catch (Exception ignore) {
                    throw new ConfigException(Error.Code.INVALID_VALUE_FOR_DATA_TYPE);
                }
                break;

            case Double:
                if (null == value) {
                    break;
                }
                try {
                    Double.valueOf(value);
                    if (value.startsWith(".")) {
                        value = "0" + value;
                    }
                } catch (Exception ignore) {
                    throw new ConfigException(Error.Code.INVALID_VALUE_FOR_DATA_TYPE);
                }
                break;

            case Float:
                if (null == value) {
                    break;
                }
                try {
                    Float.valueOf(value);
                    if (value.startsWith(".")) {
                        value = "0" + value;
                    }
                } catch (Exception ignore) {
                    throw new ConfigException(Error.Code.INVALID_VALUE_FOR_DATA_TYPE);
                }
                break;

            case Map:
                if (null == value) {
                    break;
                }
                try {
                    if (Utils.isBlank(value)) {
                        value = "{}";
                    } else {
                        new Gson().fromJson(value, JsonObject.class);
                    }
                } catch (Exception ignore) {
                    throw new ConfigException(Error.Code.INVALID_VALUE_FOR_DATA_TYPE);
                }
                break;

            case List:
                if (null == value) {
                    break;
                }
                try {
                    if (Utils.isBlank(value)) {
                        value = "[]";
                    } else {
                        new Gson().fromJson(value, JsonArray.class);
                    }
                } catch (Exception ignore) {
                    throw new ConfigException(Error.Code.INVALID_VALUE_FOR_DATA_TYPE);
                }
                break;
            }
        }

        this.absoluteFilePath = null;

        if (!this.isSecure()) {
            this.value = value;
            return;
        }

        if (this.repository.isSecurityProfilesEnabled()) {
            if (!this.getPropertyKey().getSecurityProfile().isSecretValid(encryptionSecret)) {
                throw new ConfigException(Error.Code.INVALID_PASSWORD);
            }

            if (this.isEncrypted()) {
                this.value = this.getPropertyKey().getSecurityProfile().encrypt(value, encryptionSecret);
            } else {
                this.value = value;
            }
        } else {
            this.value = value;
        }
    }

    private transient boolean decrypted = false;

    public boolean isEncrypted() {
        return this.propertyKey.isEncrypted();
    }

    public boolean isSecure() {
        return this.propertyKey.isSecure();
    }

    @Override
    public ClassName getClassName() {
        return ClassName.Property;
    }
}