content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you 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.apache.ambari.server.orm.entities;
  
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Set;
  
  import jakarta.persistence.CascadeType;
  import jakarta.persistence.Column;
  import jakarta.persistence.Entity;
  import jakarta.persistence.EnumType;
  import jakarta.persistence.Enumerated;
  import jakarta.persistence.FetchType;
  import jakarta.persistence.GeneratedValue;
  import jakarta.persistence.GenerationType;
  import jakarta.persistence.Id;
  import jakarta.persistence.JoinColumn;
  import jakarta.persistence.Lob;
  import jakarta.persistence.ManyToOne;
  import jakarta.persistence.NamedQueries;
  import jakarta.persistence.NamedQuery;
  import jakarta.persistence.OneToMany;
  import jakarta.persistence.OneToOne;
  import jakarta.persistence.PrePersist;
  import jakarta.persistence.PreUpdate;
  import jakarta.persistence.Table;
  import jakarta.persistence.TableGenerator;
  import jakarta.persistence.Transient;
  import jakarta.persistence.UniqueConstraint;
  
  import org.apache.ambari.annotations.Experimental;
  import org.apache.ambari.annotations.ExperimentalFeature;
  import org.apache.ambari.server.StaticallyInject;
  import org.apache.ambari.server.state.StackId;
  import org.apache.ambari.server.state.repository.VersionDefinitionXml;
  import org.apache.ambari.spi.RepositoryType;
  import org.apache.ambari.spi.RepositoryVersion;
  import org.apache.commons.lang.StringUtils;
  
  import com.google.common.base.MoreObjects;
  import com.google.common.base.Objects;
  
  @Entity
  @Table(name = "repo_version", uniqueConstraints = {
      @UniqueConstraint(columnNames = {"display_name"}),
      @UniqueConstraint(columnNames = {"stack_id", "version"})
  })
  @TableGenerator(name = "repository_version_id_generator",
      table = "ambari_sequences",
      pkColumnName = "sequence_name",
      valueColumnName = "sequence_value",
      pkColumnValue = "repo_version_id_seq"
      )
  @NamedQueries({
      @NamedQuery(
          name = "repositoryVersionByDisplayName",
          query = "SELECT repoversion FROM RepositoryVersionEntity repoversion WHERE repoversion.displayName=:displayname"),
      @NamedQuery(
          name = "repositoryVersionByStack",
          query = "SELECT repoversion FROM RepositoryVersionEntity repoversion WHERE repoversion.stack.stackName=:stackName AND repoversion.stack.stackVersion=:stackVersion"),
      @NamedQuery(
          name = "repositoryVersionByStackAndType",
          query = "SELECT repoversion FROM RepositoryVersionEntity repoversion WHERE repoversion.stack.stackName=:stackName AND repoversion.stack.stackVersion=:stackVersion AND repoversion.type=:type"),
      @NamedQuery(
          name = "repositoryVersionByStackNameAndVersion",
          query = "SELECT repoversion FROM RepositoryVersionEntity repoversion WHERE repoversion.stack.stackName=:stackName AND repoversion.version=:version"),
      @NamedQuery(
          name = "repositoryVersionsFromDefinition",
          query = "SELECT repoversion FROM RepositoryVersionEntity repoversion WHERE repoversion.versionXsd IS NOT NULL"),
      @NamedQuery(
          name = "findRepositoryByVersion",
          query = "SELECT repositoryVersion FROM RepositoryVersionEntity repositoryVersion WHERE repositoryVersion.version = :version ORDER BY repositoryVersion.id DESC"),
      @NamedQuery(
          name = "findByServiceDesiredVersion",
          query = "SELECT repositoryVersion FROM RepositoryVersionEntity repositoryVersion WHERE repositoryVersion IN (SELECT DISTINCT sd1.desiredRepositoryVersion FROM ServiceDesiredStateEntity sd1 WHERE sd1.desiredRepositoryVersion IN ?1)") })
  @StaticallyInject
  public class RepositoryVersionEntity {
    @Id
    @Column(name = "repo_version_id")
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "repository_version_id_generator")
    private Long id;
  
    /**
     * Unidirectional one-to-one association to {@link StackEntity}
     */
    @OneToOne
    @JoinColumn(name = "stack_id", nullable = false)
    private StackEntity stack;
  
    @Column(name = "version")
    private String version;
  
    @Column(name = "display_name")
    private String displayName;
  
    /**
     * one-to-many association to {@link RepoOsEntity}
     */
    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "repositoryVersionEntity", orphanRemoval = true)
    private List<RepoOsEntity> repoOsEntities = new ArrayList<>();
  
    @OneToMany(cascade = CascadeType.REMOVE, mappedBy = "repositoryVersion")
    private Set<HostVersionEntity> hostVersionEntities;
  
    @Column(name = "repo_type", nullable = false)
    @Enumerated(value = EnumType.STRING)
    private RepositoryType type = RepositoryType.STANDARD;
  
    @Lob
    @Column(name="version_xml")
    private String versionXml;
  
    @Transient
    private VersionDefinitionXml versionDefinition = null;
  
    @Column(name="version_url")
    private String versionUrl;
  
    @Column(name="version_xsd")
    private String versionXsd;
  
    @Column(name = "hidden", nullable = false)
    private short isHidden = 0;
  
    /**
     * Repositories can't be trusted until they have been deployed and we've
     * detected their actual version. Most of the time, things match up, but
     * editing a VDF could causes the version to be misrepresented. Once we have
     * received the correct version of the repository (normally after it's been
     * installed), then we can set this flag to {@code true}.
     */
    @Column(name = "resolved", nullable = false)
    private short resolved = 0;
  
    @Column(name = "legacy", nullable = false)
    private short isLegacy = 0;
  
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private RepositoryVersionEntity parent;
  
    @OneToMany(mappedBy = "parent")
    private List<RepositoryVersionEntity> children;
  
  
    // ----- RepositoryVersionEntity -------------------------------------------------------
  
    public RepositoryVersionEntity() {
  
    }
  
    public RepositoryVersionEntity(StackEntity stack, String version,
                                   String displayName, List<RepoOsEntity> repoOsEntities) {
      this.stack = stack;
      this.version = version;
      this.displayName = displayName;
      this.repoOsEntities = repoOsEntities;
      for (RepoOsEntity repoOsEntity : repoOsEntities) {
        repoOsEntity.setRepositoryVersionEntity(this);
      }
    }
  
    @PreUpdate
    @PrePersist
    public void removePrefixFromVersion() {
      String stackName = stack.getStackName();
      if (version.startsWith(stackName)) {
        version = version.substring(stackName.length() + 1);
      }
    }
  
    /**
     * Update one-to-many relation without rebuilding the whole entity
     * @param entity many-to-one entity
     */
    public void updateHostVersionEntityRelation(HostVersionEntity entity){
      hostVersionEntities.add(entity);
    }
  
    public Long getId() {
      return id;
    }
  
    public void setId(Long id) {
      this.id = id;
    }
  
    /**
     * Gets the repository version's stack.
     *
     * @return the stack.
     */
    public StackEntity getStack() {
      return stack;
    }
  
    /**
     * Sets the repository version's stack.
     *
     * @param stack
     *          the stack to set for the repo version (not {@code null}).
     */
    public void setStack(StackEntity stack) {
      this.stack = stack;
    }
  
    public String getVersion() {
      return version;
    }
  
    /**
     * Sets the version on this repository version entity. If the version is
     * confirmed as correct, then the called should also set
     * {@link #setResolved(boolean)}.
     *
     * @param version
     */
    public void setVersion(String version) {
      this.version = version;
  
      // need to be called to avoid work with wrong value until entity would be persisted
      if (null != version && null != stack && null != stack.getStackName()){
        removePrefixFromVersion();
      }
    }
  
    public String getDisplayName() {
      return displayName;
    }
  
    public void setDisplayName(String displayName) {
      this.displayName = displayName;
    }
  
  
    public String getStackName() {
      return getStackId().getStackName();
    }
  
    public String getStackVersion() {
      return getStackId().getStackVersion();
    }
  
    public StackId getStackId() {
      if (null == stack) {
        return null;
      }
  
      return new StackId(stack.getStackName(), stack.getStackVersion());
    }
  
    /**
     * @return the type
     */
    public RepositoryType getType() {
      return type;
    }
  
    /**
     * @param type the repo type
     */
    public void setType(RepositoryType type) {
      this.type = type;
    }
  
    /**
     * @return the XML that is the basis for the version
     */
    public String getVersionXml() {
      return versionXml;
    }
  
    /**
     * @param xml the XML that is the basis for the version
     */
    public void setVersionXml(String xml) {
      versionXml = xml;
    }
  
    /**
     * @return The url used for the version.  Optional in case the XML was loaded via blob.
     */
    public String getVersionUrl() {
      return versionUrl;
    }
  
    /**
     * @param url the url used to load the XML.
     */
    public void setVersionUrl(String url) {
      versionUrl = url;
    }
  
    /**
     * @return the XSD name extracted from the XML.
     */
    public String getVersionXsd() {
      return versionXsd;
    }
  
    /**
     * @param xsdLocation the XSD name extracted from XML.
     */
    public void setVersionXsd(String xsdLocation) {
      versionXsd = xsdLocation;
    }
  
    /**
     * Parse the version XML into its object representation.  This causes the XML to be lazy-loaded
     * from storage, and will only be parsed once per request.
     * @return {@code null} if the XSD (from the XML) is not available.
     * @throws Exception
     */
    public VersionDefinitionXml getRepositoryXml() throws Exception {
      if (null == versionXsd) {
        return null;
      }
  
      if (null == versionDefinition) {
        versionDefinition = VersionDefinitionXml.load(getVersionXml());
      }
  
      return versionDefinition;
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
      return java.util.Objects.hash(stack, version, displayName, repoOsEntities);
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object object) {
      if (null == object) {
        return false;
      }
  
      if (this == object) {
        return true;
      }
  
      if (object.getClass() != getClass()) {
        return false;
      }
  
      RepositoryVersionEntity that = (RepositoryVersionEntity) object;
      return Objects.equal(stack, that.stack) && Objects.equal(version, that.version)
          && Objects.equal(displayName, that.displayName)
          && Objects.equal(repoOsEntities, that.repoOsEntities);
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this).add("id", id).add("stack", stack).add("version",
          version).add("type", type).add("hidden", isHidden == 1).toString();
    }
  
    /**
     * Determine if the version belongs to the stack.
     * @param stackId Stack, such as HDP-2.3
     * @param version Version number, such as 2.3.0.0, or 2.3.0.0-1234
     * @return Return true if the version starts with the digits of the stack.
     */
    public static boolean isVersionInStack(StackId stackId, String version) {
      if (null != version && !StringUtils.isBlank(version)) {
        String stackName = stackId.getStackName();
        if (version.startsWith(stackName + "-")) {
          version = version.substring(stackName.length() + 1);
        }
  
        String leading = stackId.getStackVersion();  // E.g, 2.3
        // In some cases during unit tests, the leading can contain 3 digits, so only the major number (first two parts) are needed.
        String[] leadingParts = leading.split("\\.");
        if (leadingParts.length > 2) {
          leading = leadingParts[0] + "." + leadingParts[1];
        }
        return version.startsWith(leading);
      }
      return false;
    }
  
    /**
     * @param entity parent entity
     */
    public void setParent(RepositoryVersionEntity entity) {
      parent = entity;
      parent.children.add(this);
    }
  
    /**
     * @return the repositories that are denoted children
     */
    public List<RepositoryVersionEntity> getChildren() {
      return children;
    }
  
    /**
     * @return the parentId, or {@code null} if the entity is already a parent
     */
    public Long getParentId() {
      return null == parent ? null : parent.getId();
    }
  
    /**
     * Gets whether this repository is hidden.
     *
     * @return
     */
    public boolean isHidden() {
      return isHidden != 0;
    }
  
    /**
     * Sets whether this repository is hidden. A repository can be hidden for
     * several reasons, including if it has been removed (but needs to be kept
     * around for foreign key relationships) or if it just is not longer desired
     * to see it.
     *
     * @param isHidden
     */
    public void setHidden(boolean isHidden) {
      this.isHidden = (short) (isHidden ? 1 : 0);
    }
  
    /**
     * Gets whether this repository has been installed and has reported back its
     * actual version.
     *
     * @return {@code true} if the version for this repository can be trusted,
     *         {@code false} otherwise.
     */
    public boolean isResolved() {
      return resolved == 1;
    }
  
    /**
     * Gets whether this repository is legacy
     *
     * @return
     */
    @Deprecated
    @Experimental(feature= ExperimentalFeature.PATCH_UPGRADES)
    public boolean isLegacy(){
      return isLegacy == 1;
    }
  
    /**
     * Sets whether this repository is legacy. Scoped for moving from old-style repository naming to new
     *
     * @param isLegacy
     */
    @Deprecated
    @Experimental(feature= ExperimentalFeature.PATCH_UPGRADES)
    public void setLegacy(boolean isLegacy){
      this.isLegacy = isLegacy ? (short) 1 : (short) 0;
    }
  
    /**
     * Sets whether this repository has been installed and has reported back its
     * actual version.
     *
     * @param resolved
     */
    public void setResolved(boolean resolved) {
      this.resolved = resolved ? (short) 1 : (short) 0;
    }
  
    public List<RepoOsEntity> getRepoOsEntities() {
      return repoOsEntities;
    }
  
    public void addRepoOsEntities(List<RepoOsEntity> repoOsEntities) {
      this.repoOsEntities = repoOsEntities;
      for (RepoOsEntity repoOsEntity : repoOsEntities) {
        repoOsEntity.setRepositoryVersionEntity(this);
      }
    }
  
    /**
     * Builds a {@link RepositoryVersion} instance type from this entity.
     *
     * @return a single POJO to represent this entity.
     */
    public RepositoryVersion getRepositoryVersion() {
      return new RepositoryVersion(getId(), getStackName(), getStackVersion(),
          getStackId().getStackId(), getVersion(), getType());
    }
  }
  