/*
 * 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.controller.internal;

import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.ambari.annotations.Experimental;
import org.apache.ambari.annotations.ExperimentalFeature;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.StaticallyInject;
import org.apache.ambari.server.api.resources.OperatingSystemResourceDefinition;
import org.apache.ambari.server.api.resources.RepositoryResourceDefinition;
import org.apache.ambari.server.api.services.AmbariMetaInfo;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.controller.spi.NoSuchParentResourceException;
import org.apache.ambari.server.controller.spi.NoSuchResourceException;
import org.apache.ambari.server.controller.spi.Predicate;
import org.apache.ambari.server.controller.spi.Request;
import org.apache.ambari.server.controller.spi.RequestStatus;
import org.apache.ambari.server.controller.spi.Resource;
import org.apache.ambari.server.controller.spi.ResourceAlreadyExistsException;
import org.apache.ambari.server.controller.spi.SystemException;
import org.apache.ambari.server.controller.spi.UnsupportedPropertyException;
import org.apache.ambari.server.controller.utilities.PropertyHelper;
import org.apache.ambari.server.orm.dao.RepositoryVersionDAO;
import org.apache.ambari.server.orm.dao.StackDAO;
import org.apache.ambari.server.orm.entities.RepoDefinitionEntity;
import org.apache.ambari.server.orm.entities.RepoOsEntity;
import org.apache.ambari.server.orm.entities.RepositoryVersionEntity;
import org.apache.ambari.server.orm.entities.StackEntity;
import org.apache.ambari.server.security.authorization.ResourceType;
import org.apache.ambari.server.security.authorization.RoleAuthorization;
import org.apache.ambari.server.stack.RepoUtil;
import org.apache.ambari.server.stack.upgrade.RepositoryVersionHelper;
import org.apache.ambari.server.state.RepositoryInfo;
import org.apache.ambari.server.state.StackId;
import org.apache.ambari.server.state.StackInfo;
import org.apache.ambari.server.state.repository.VersionDefinitionXml;
import org.apache.ambari.server.state.stack.RepoTag;
import org.apache.ambari.spi.RepositoryType;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Provider;

/**
 * The {@link VersionDefinitionResourceProvider} class deals with managing Version Definition
 * files.
 */
@StaticallyInject
public class VersionDefinitionResourceProvider extends AbstractAuthorizedResourceProvider {

  private static final Logger LOG = LoggerFactory.getLogger(VersionDefinitionResourceProvider.class);

  public static final String VERSION_DEF                             = "VersionDefinition";
  public static final String VERSION_DEF_BASE64_PROPERTY             = "version_base64";
  public static final String VERSION_DEF_STACK_NAME                  = "VersionDefinition/stack_name";
  public static final String VERSION_DEF_STACK_VERSION               = "VersionDefinition/stack_version";

  public static final String VERSION_DEF_ID                       = "VersionDefinition/id";
  protected static final String VERSION_DEF_TYPE_PROPERTY_ID         = "VersionDefinition/type";
  protected static final String VERSION_DEF_DEFINITION_URL           = "VersionDefinition/version_url";
  public static final String VERSION_DEF_AVAILABLE_DEFINITION     = "VersionDefinition/available";
  protected static final String VERSION_DEF_DEFINITION_BASE64        = PropertyHelper.getPropertyId(VERSION_DEF, VERSION_DEF_BASE64_PROPERTY);

  protected static final String VERSION_DEF_FULL_VERSION             = "VersionDefinition/repository_version";
  protected static final String VERSION_DEF_RELEASE_VERSION          = "VersionDefinition/release/version";
  protected static final String VERSION_DEF_RELEASE_BUILD            = "VersionDefinition/release/build";
  protected static final String VERSION_DEF_RELEASE_NOTES            = "VersionDefinition/release/notes";
  protected static final String VERSION_DEF_RELEASE_COMPATIBLE_WITH  = "VersionDefinition/release/compatible_with";
  protected static final String VERSION_DEF_AVAILABLE_SERVICES       = "VersionDefinition/services";
  protected static final String VERSION_DEF_STACK_SERVICES           = "VersionDefinition/stack_services";
  protected static final String VERSION_DEF_STACK_DEFAULT            = "VersionDefinition/stack_default";
  protected static final String VERSION_DEF_STACK_REPO_UPDATE_LINK_EXISTS = "VersionDefinition/stack_repo_update_link_exists";
  protected static final String VERSION_DEF_DISPLAY_NAME             = "VersionDefinition/display_name";
  protected static final String VERSION_DEF_VALIDATION               = "VersionDefinition/validation";
  protected static final String SHOW_AVAILABLE                       = "VersionDefinition/show_available";

  // !!! for convenience, bring over jdk compatibility information from the stack
  protected static final String VERSION_DEF_MIN_JDK                  = "VersionDefinition/min_jdk";
  protected static final String VERSION_DEF_MAX_JDK                  = "VersionDefinition/max_jdk";

  public static final String DIRECTIVE_SKIP_URL_CHECK = "skip_url_check";

  public static final String SUBRESOURCE_OPERATING_SYSTEMS_PROPERTY_ID  = new OperatingSystemResourceDefinition().getPluralName();

  @Inject
  private static RepositoryVersionDAO s_repoVersionDAO;

  @Inject
  private static Provider<AmbariMetaInfo> s_metaInfo;

  @Inject
  private static Provider<RepositoryVersionHelper> s_repoVersionHelper;

  @Inject
  private static StackDAO s_stackDAO;

  @Inject
  private static Configuration s_configuration;

  /**
   * Key property ids
   */
  private static final Set<String> PK_PROPERTY_IDS = Sets.newHashSet(
      VERSION_DEF_ID,
      VERSION_DEF_STACK_NAME,
      VERSION_DEF_STACK_VERSION,
      VERSION_DEF_FULL_VERSION);

  /**
   * The property ids for an version definition resource.
   */
  private static final Set<String> PROPERTY_IDS = Sets.newHashSet(
      VERSION_DEF_ID,
      VERSION_DEF_TYPE_PROPERTY_ID,
      VERSION_DEF_DEFINITION_URL,
      VERSION_DEF_DEFINITION_BASE64,
      VERSION_DEF_AVAILABLE_DEFINITION,
      VERSION_DEF_STACK_NAME,
      VERSION_DEF_STACK_VERSION,
      VERSION_DEF_FULL_VERSION,
      VERSION_DEF_RELEASE_NOTES,
      VERSION_DEF_RELEASE_COMPATIBLE_WITH,
      VERSION_DEF_RELEASE_VERSION,
      VERSION_DEF_RELEASE_BUILD,
      VERSION_DEF_AVAILABLE_SERVICES,
      VERSION_DEF_STACK_SERVICES,
      VERSION_DEF_STACK_DEFAULT,
      VERSION_DEF_STACK_REPO_UPDATE_LINK_EXISTS,
      VERSION_DEF_DISPLAY_NAME,
      VERSION_DEF_VALIDATION,
      VERSION_DEF_MIN_JDK,
      VERSION_DEF_MAX_JDK,
      SUBRESOURCE_OPERATING_SYSTEMS_PROPERTY_ID,
      SHOW_AVAILABLE);

  /**
   * The key property ids for an version definition resource.
   */
  private static final Map<Resource.Type, String> KEY_PROPERTY_IDS =
      Collections.singletonMap(Resource.Type.VersionDefinition, VERSION_DEF_ID);

  /**
   * Constructor.
   */
  VersionDefinitionResourceProvider() {
    super(Resource.Type.VersionDefinition, PROPERTY_IDS, KEY_PROPERTY_IDS);

    setRequiredCreateAuthorizations(EnumSet.of(RoleAuthorization.AMBARI_MANAGE_STACK_VERSIONS));
    setRequiredGetAuthorizations(EnumSet.of(RoleAuthorization.AMBARI_MANAGE_STACK_VERSIONS));
  }

  @Override
  protected RequestStatus createResourcesAuthorized(final Request request)
      throws SystemException,
      UnsupportedPropertyException, ResourceAlreadyExistsException,
      NoSuchParentResourceException {

    Set<Map<String, Object>> requestProperties = request.getProperties();

    if (requestProperties.size() > 1) {
      throw new IllegalArgumentException("Cannot process more than one file per request");
    }

    final Map<String, Object> properties = requestProperties.iterator().next();

    if (!properties.containsKey(VERSION_DEF_DEFINITION_URL) &&
        !properties.containsKey(VERSION_DEF_DEFINITION_BASE64) &&
        !properties.containsKey(VERSION_DEF_AVAILABLE_DEFINITION)) {
      throw new IllegalArgumentException(String.format("Creation method is not known.  %s or %s is required or upload the file directly",
          VERSION_DEF_DEFINITION_URL, VERSION_DEF_AVAILABLE_DEFINITION));
    }

    if (properties.containsKey(VERSION_DEF_DEFINITION_URL) && properties.containsKey(VERSION_DEF_DEFINITION_BASE64)) {
      throw new IllegalArgumentException(String.format("Specify ONLY the url with %s or upload the file directly",
          VERSION_DEF_DEFINITION_URL));
    }

    final String definitionUrl = (String) properties.get(VERSION_DEF_DEFINITION_URL);
    final String definitionBase64 = (String) properties.get(VERSION_DEF_DEFINITION_BASE64);
    final String definitionName = (String) properties.get(VERSION_DEF_AVAILABLE_DEFINITION);
    final Set<String> validations = new HashSet<>();
    final boolean dryRun = request.isDryRunRequest();

    final boolean skipUrlCheck;
    if (null != request.getRequestInfoProperties()) {
      skipUrlCheck = BooleanUtils.toBoolean(request.getRequestInfoProperties().get(DIRECTIVE_SKIP_URL_CHECK));
    } else {
      skipUrlCheck = false;
    }

    XmlHolder xmlHolder = createResources(new Command<XmlHolder>() {
      @Override
      public XmlHolder invoke() throws AmbariException {

        XmlHolder holder = null;
        if (null != definitionUrl) {
          holder = loadXml(definitionUrl);
        } else if (null != definitionBase64) {
          holder = loadXml(Base64.decodeBase64(definitionBase64));
        } else if (null != definitionName) {
          VersionDefinitionXml xml = s_metaInfo.get().getVersionDefinition(definitionName);

          if (null == xml) {
            throw new AmbariException(String.format("Version %s not found", definitionName));
          }

          holder = new XmlHolder();
          holder.xml = xml;
          try {
            holder.xmlString = xml.toXml();
          } catch (Exception e) {
            throw new AmbariException(String.format("The available repository %s does not serialize", definitionName), e);
          }

        } else {
          throw new AmbariException("Cannot determine creation method");
        }

        // !!! must be in this anonymous method because things throw AmbariException

        toRepositoryVersionEntity(holder);

        try {
          RepositoryVersionResourceProvider.validateRepositoryVersion(s_repoVersionDAO,
              s_metaInfo.get(), holder.entity, skipUrlCheck);
        } catch (AmbariException e) {
          if (dryRun) {
            validations.add(e.getMessage());
          } else {
            throw e;
          }
        }

        checkForParent(holder);

        return holder;
      }
    });

    if (StringUtils.isNotBlank(ObjectUtils.toString(properties.get(VERSION_DEF_DISPLAY_NAME)))) {
      xmlHolder.xml.release.display = properties.get(VERSION_DEF_DISPLAY_NAME).toString();
      xmlHolder.entity.setDisplayName(properties.get(VERSION_DEF_DISPLAY_NAME).toString());
      // !!! also set the version string to the name for uniqueness reasons.  this is
      // getting replaced during install packages anyway.  this is just for the entity, the
      // VDF should stay the same.
      xmlHolder.entity.setVersion(properties.get(VERSION_DEF_DISPLAY_NAME).toString());
    }

    if (s_repoVersionDAO.findByDisplayName(xmlHolder.entity.getDisplayName()) != null) {
      String err = String.format("Repository version with name %s already exists.",
          xmlHolder.entity.getDisplayName());

      if (dryRun) {
        validations.add(err);
      } else {
        throw new ResourceAlreadyExistsException(err);
      }
    }

    if (s_repoVersionDAO.findByStackAndVersion(xmlHolder.entity.getStack(), xmlHolder.entity.getVersion()) != null) {
      String err = String.format("Repository version for stack %s and version %s already exists.",
              xmlHolder.entity.getStackId(), xmlHolder.entity.getVersion());

      if (dryRun) {
        validations.add(err);
      } else {
        throw new ResourceAlreadyExistsException(err);
      }
    }

    final Resource res;

    if (dryRun) {
      // !!! dry runs imply that the whole entity should be provided.  this is usually
      // done via sub-resources, but that model breaks down since we don't have a saved
      // entity yet
      Set<String> ids = Sets.newHashSet(
        VERSION_DEF_TYPE_PROPERTY_ID,
        VERSION_DEF_FULL_VERSION,
        VERSION_DEF_RELEASE_BUILD,
        VERSION_DEF_RELEASE_COMPATIBLE_WITH,
        VERSION_DEF_RELEASE_NOTES,
        VERSION_DEF_RELEASE_VERSION,
        VERSION_DEF_DISPLAY_NAME,
        VERSION_DEF_AVAILABLE_SERVICES,
        VERSION_DEF_VALIDATION,
        VERSION_DEF_MIN_JDK,
        VERSION_DEF_MAX_JDK,
        VERSION_DEF_STACK_SERVICES);

      res = toResource(null, xmlHolder.xml, ids, validations);
      // !!! if the definition name is not null, it can only be from available
      if (null != definitionName) {
        res.setProperty(SHOW_AVAILABLE, true);
      }

      // !!! for dry run, make sure the version is the entity display
      setResourceProperty(res, VERSION_DEF_FULL_VERSION, xmlHolder.entity.getVersion(), ids);

      addSubresources(res, xmlHolder.entity);
    } else {

      s_repoVersionDAO.create(xmlHolder.entity);

      res = toResource(xmlHolder.entity, Collections.emptySet());
      notifyCreate(Resource.Type.VersionDefinition, request);
    }

    RequestStatusImpl status = new RequestStatusImpl(null, Collections.singleton(res));

    return status;
  }

  @Override
  public Set<Resource> getResources(Request request, Predicate predicate)
      throws SystemException, UnsupportedPropertyException,
      NoSuchResourceException, NoSuchParentResourceException {

    Set<Resource> results = new HashSet<>();
    Set<String> requestPropertyIds = getRequestPropertyIds(request, predicate);

    Set<Map<String, Object>> propertyMaps = getPropertyMaps(predicate);

    if (propertyMaps.isEmpty()) {
      List<RepositoryVersionEntity> versions = s_repoVersionDAO.findRepositoriesWithVersionDefinitions();

      for (RepositoryVersionEntity entity : versions) {
        results.add(toResource(entity, requestPropertyIds));
      }
    } else {
      for (Map<String, Object> propertyMap : propertyMaps) {

        if (propertyMap.containsKey(SHOW_AVAILABLE) &&
            Boolean.parseBoolean(propertyMap.get(SHOW_AVAILABLE).toString())) {

          for (Entry<String, VersionDefinitionXml> entry : s_metaInfo.get().getVersionDefinitions().entrySet()) {
            Resource res = toResource(entry.getKey(), entry.getValue(), requestPropertyIds, null);
            res.setProperty(SHOW_AVAILABLE, true);
            results.add(res);
          }
        } else {
          String id = (String) propertyMap.get(VERSION_DEF_ID);

          if (null != id) {
            // id is either the repo version id from the db, or it's a phantom id from
            // the stack
            if (NumberUtils.isDigits(id)) {

              RepositoryVersionEntity entity = s_repoVersionDAO.findByPK(Long.parseLong(id));
              if (null != entity) {
                results.add(toResource(entity, requestPropertyIds));
              }
            } else {
              VersionDefinitionXml xml = s_metaInfo.get().getVersionDefinition(id);

              if (null == xml) {
                throw new NoSuchResourceException(String.format("Could not find version %s",
                    id));
              }
              Resource res = toResource(id, xml, requestPropertyIds, null);
              res.setProperty(SHOW_AVAILABLE, true);
              results.add(res);
            }
          } else {
            List<RepositoryVersionEntity> versions = s_repoVersionDAO.findRepositoriesWithVersionDefinitions();

            for (RepositoryVersionEntity entity : versions) {
              results.add(toResource(entity, requestPropertyIds));
            }
          }

        }
      }
    }
    return results;
  }

  @Override
  protected RequestStatus updateResourcesAuthorized(final Request request, Predicate predicate)
    throws SystemException, UnsupportedPropertyException,
      NoSuchResourceException, NoSuchParentResourceException {

    throw new SystemException("Cannot update Version Definitions");
  }

  @Override
  protected RequestStatus deleteResourcesAuthorized(Request request, Predicate predicate)
      throws SystemException, UnsupportedPropertyException,
      NoSuchResourceException, NoSuchParentResourceException {
    throw new SystemException("Cannot delete Version Definitions");
  }

  /**
   * In the case of a patch, check if there is a parent repo.
   */
  private void checkForParent(XmlHolder holder) throws AmbariException {
    RepositoryVersionEntity entity = holder.entity;

    // only STANDARD types don't have a parent
    if (entity.getType() == RepositoryType.STANDARD) {
      return;
    }

    List<RepositoryVersionEntity> entities = s_repoVersionDAO.findByStackAndType(
        entity.getStackId(), RepositoryType.STANDARD);

    if (entities.isEmpty()) {
      throw new IllegalArgumentException(String.format("Patch %s was uploaded, but there are no repositories for %s",
          entity.getVersion(), entity.getStackId().toString()));
    }

    List<RepositoryVersionEntity> matching = new ArrayList<>();

    boolean emptyCompatible = StringUtils.isBlank(holder.xml.release.compatibleWith);

    for (RepositoryVersionEntity version : entities) {
      String baseVersion = version.getVersion();
      if (baseVersion.lastIndexOf('-') > -1) {
        baseVersion = baseVersion.substring(0,  baseVersion.lastIndexOf('-'));
      }

      if (emptyCompatible) {
        if (baseVersion.equals(holder.xml.release.version)) {
          matching.add(version);
        }
      } else {
        if (baseVersion.matches(holder.xml.release.compatibleWith)) {
          matching.add(version);
        }
      }
    }

    RepositoryVersionEntity parent = null;

    if (matching.isEmpty()) {
      String format = "No versions matched pattern %s";

      throw new IllegalArgumentException(String.format(format,
          emptyCompatible ? holder.xml.release.version : holder.xml.release.compatibleWith));
    } else if (matching.size() > 1) {

      Function<RepositoryVersionEntity, String> function = new Function<RepositoryVersionEntity, String>() {
        @Override
        public String apply(RepositoryVersionEntity input) {
          return input.getVersion();
        }
      };

      Collection<String> versions = Collections2.transform(matching, function);

      List<RepositoryVersionEntity> used = s_repoVersionDAO.findByServiceDesiredVersion(matching);

      if (used.isEmpty()) {
        throw new IllegalArgumentException(String.format("Could not determine which version " +
          "to associate patch %s. Remove one of %s and try again.",
          entity.getVersion(), StringUtils.join(versions, ", ")));
      } else if (used.size() > 1) {
        Collection<String> usedVersions = Collections2.transform(used, function);

        throw new IllegalArgumentException(String.format("Patch %s was found to match more " +
            "than one repository in use: %s. Move all services to a common version and try again.",
            entity.getVersion(), StringUtils.join(usedVersions, ", ")));
      } else {
        parent = used.get(0);
        LOG.warn("Patch {} was found to match more than one repository in {}. " +
            "Repository {} is in use and will be the parent.", entity.getVersion(),
               StringUtils.join(versions, ", "), parent.getVersion());
      }
    } else {
      parent = matching.get(0);
    }

    if (null == parent) {
      throw new IllegalArgumentException(String.format("Could not find any parent repository for %s.",
          entity.getVersion()));
    }

    entity.setParent(parent);
  }

  @Override
  protected Set<String> getPKPropertyIds() {
    return PK_PROPERTY_IDS;
  }

  @Override
  protected ResourceType getResourceType(Request request, Predicate predicate) {
    return ResourceType.AMBARI;
  }

  /**
   * Load the xml data from a posted Base64 stream
   * @param decoded the decoded Base64 data
   * @return the XmlHolder instance
   * @throws AmbariException
   */
  private XmlHolder loadXml(byte[] decoded) {
    XmlHolder holder = new XmlHolder();

    try {
      holder.xmlString = new String(decoded, "UTF-8");
    } catch (UnsupportedEncodingException e) {
      holder.xmlString = new String(decoded);
    }

    try {
      holder.xml = VersionDefinitionXml.load(holder.xmlString);
    } catch (Exception e) {
      throw new IllegalArgumentException(e);
    }

    return holder;
  }

  /**
   * Load the xml data from a url
   * @param definitionUrl
   * @return the XmlHolder instance
   * @throws AmbariException
   */
  private XmlHolder loadXml(String definitionUrl) throws AmbariException {
    XmlHolder holder = new XmlHolder();
    holder.url = definitionUrl;

    int connectTimeout = s_configuration.getVersionDefinitionConnectTimeout();
    int readTimeout = s_configuration.getVersionDefinitionReadTimeout();

    try {
      URI uri = new URI(definitionUrl);

      if (uri.getScheme().equalsIgnoreCase("file")) {
        if (!s_configuration.areFileVDFAllowed()) {
          throw new AmbariException("File URL for VDF are not enabled");
        }
        InputStream stream = uri.toURL().openStream();
        holder.xmlString = IOUtils.toString(stream, "UTF-8");
      } else {
        URLRedirectProvider provider = new URLRedirectProvider(connectTimeout, readTimeout, true);
        URLRedirectProvider.RequestResult requestResult = provider.executeGet(definitionUrl);

        if (requestResult.getCode() != HttpStatus.SC_OK) {
          String err = String.format("Could not load url from '%s' with code '%d'.  %s",
                                     definitionUrl, requestResult.getCode(), requestResult.getContent());
          throw new AmbariException(err);
        }

        holder.xmlString = requestResult.getContent();
      }
      holder.xml = VersionDefinitionXml.load(holder.xmlString);
    } catch (Exception e) {
      String err = String.format("Could not load url from %s.  %s",
          definitionUrl, e.getMessage());
      throw new AmbariException(err, e);
    }

    return holder;
  }

  /**
   * Transforms a XML version defintion to an entity
   *
   * @throws AmbariException if some properties are missing or json has incorrect structure
   */
  @Experimental(feature = ExperimentalFeature.CUSTOM_SERVICE_REPOS,
    comment = "Remove logic for handling custom service repos after enabling multi-mpack cluster deployment")
  protected void toRepositoryVersionEntity(XmlHolder holder) throws AmbariException {

    // !!! TODO validate parsed object graph

    RepositoryVersionEntity entity = new RepositoryVersionEntity();

    StackId stackId = new StackId(holder.xml.release.stackId);

    StackEntity stackEntity = s_stackDAO.find(stackId.getStackName(), stackId.getStackVersion());

    entity.setStack(stackEntity);

    String credentials = null;
    if (holder.url != null) {
      try {
        URI uri = new URI(holder.url);
        credentials = uri.getUserInfo();
      } catch (URISyntaxException e) {
        throw new AmbariException(String.format("Could not parse url %s", holder.url), e);
      }
    }

    List<RepositoryInfo> repos = holder.xml.repositoryInfo.getRepositories(credentials);

    StackInfo stack = s_metaInfo.get().getStack(stackId);

    // Add service repositories (these are not contained by the VDF but are there in the stack model)
    ListMultimap<String, RepositoryInfo> stackReposByOs = stack.getRepositoriesByOs();
    repos.addAll(RepoUtil.getServiceRepos(repos, stackReposByOs));

    entity.addRepoOsEntities(s_repoVersionHelper.get().createRepoOsEntities(repos));

    entity.setVersion(holder.xml.release.getFullVersion(stack.getReleaseVersion()));

    if (StringUtils.isNotEmpty(holder.xml.release.display)) {
      entity.setDisplayName(holder.xml.release.display);
    } else {
      entity.setDisplayName(stackId.getStackName() + "-" + entity.getVersion());
    }

    entity.setType(holder.xml.release.repositoryType);
    entity.setVersionUrl(holder.url);
    entity.setVersionXml(holder.xmlString);
    entity.setVersionXsd(holder.xml.xsdLocation);

    holder.entity = entity;
  }

  /**
   * Converts a version definition to a resource
   * @param id            the definition id
   * @param xml           the version definition xml
   * @param requestedIds  the requested ids
   * @return the resource
   * @throws SystemException
   */
  private Resource toResource(String id, VersionDefinitionXml xml, Set<String> requestedIds,
      Set<String> validations) throws SystemException {

    Resource resource = new ResourceImpl(Resource.Type.VersionDefinition);
    resource.setProperty(VERSION_DEF_ID, id);

    StackId stackId = new StackId(xml.release.stackId);

    // !!! these are needed for href
    resource.setProperty(VERSION_DEF_STACK_NAME, stackId.getStackName());
    resource.setProperty(VERSION_DEF_STACK_VERSION, stackId.getStackVersion());

    StackInfo stack = null;
    try {
      stack = s_metaInfo.get().getStack(stackId.getStackName(), stackId.getStackVersion());
    } catch (AmbariException e) {
      throw new SystemException(String.format("Could not load stack %s", stackId));
    }

    setResourceProperty(resource, VERSION_DEF_TYPE_PROPERTY_ID, xml.release.repositoryType, requestedIds);
    setResourceProperty(resource, VERSION_DEF_FULL_VERSION, xml.release.getFullVersion(stack.getReleaseVersion()), requestedIds);
    setResourceProperty(resource, VERSION_DEF_RELEASE_BUILD, xml.release.build, requestedIds);
    setResourceProperty(resource, VERSION_DEF_RELEASE_COMPATIBLE_WITH, xml.release.compatibleWith, requestedIds);
    setResourceProperty(resource, VERSION_DEF_RELEASE_NOTES, xml.release.releaseNotes, requestedIds);
    setResourceProperty(resource, VERSION_DEF_RELEASE_VERSION, xml.release.version, requestedIds);
    setResourceProperty(resource, VERSION_DEF_STACK_DEFAULT, xml.isStackDefault(), requestedIds);
    setResourceProperty(resource, VERSION_DEF_STACK_REPO_UPDATE_LINK_EXISTS, (stack.getRepositoryXml().getLatestURI() != null), requestedIds);
    setResourceProperty(resource, VERSION_DEF_DISPLAY_NAME, xml.release.display, requestedIds);

    if (null != validations) {
      setResourceProperty(resource, VERSION_DEF_VALIDATION, validations, requestedIds);
    }

    setResourceProperty(resource, VERSION_DEF_AVAILABLE_SERVICES, xml.getAvailableServices(stack), requestedIds);
    setResourceProperty(resource, VERSION_DEF_STACK_SERVICES, xml.getStackServices(stack), requestedIds);
    setResourceProperty(resource, VERSION_DEF_MIN_JDK, stack.getMinJdk(), requestedIds);
    setResourceProperty(resource, VERSION_DEF_MAX_JDK, stack.getMaxJdk(), requestedIds);

    return resource;
  }

  /**
   * Convert the given {@link RepositoryVersionEntity} to a {@link Resource}.
   *
   * @param entity
   *          the entity to convert.
   * @param requestedIds
   *          the properties that were requested or {@code null} for all.
   * @return the resource representation of the entity (never {@code null}).
   */
  private Resource toResource(RepositoryVersionEntity entity, Set<String> requestedIds)
      throws SystemException {

    Resource resource = new ResourceImpl(Resource.Type.VersionDefinition);
    resource.setProperty(VERSION_DEF_ID, entity.getId());

    VersionDefinitionXml xml = null;
    try {
      xml = entity.getRepositoryXml();
    } catch (Exception e) {
      String msg = String.format("Could not load version definition %s", entity.getId());
      throw new SystemException(msg, e);
    }

    StackId stackId = new StackId(xml.release.stackId);

    // !!! these are needed for href
    resource.setProperty(VERSION_DEF_STACK_NAME, stackId.getStackName());
    resource.setProperty(VERSION_DEF_STACK_VERSION, stackId.getStackVersion());

    setResourceProperty(resource, VERSION_DEF_TYPE_PROPERTY_ID, entity.getType(), requestedIds);
    setResourceProperty(resource, VERSION_DEF_DEFINITION_URL, entity.getVersionUrl(), requestedIds);
    setResourceProperty(resource, VERSION_DEF_FULL_VERSION, entity.getVersion(), requestedIds);
    setResourceProperty(resource, VERSION_DEF_RELEASE_BUILD, xml.release.build, requestedIds);
    setResourceProperty(resource, VERSION_DEF_RELEASE_COMPATIBLE_WITH, xml.release.compatibleWith, requestedIds);
    setResourceProperty(resource, VERSION_DEF_RELEASE_NOTES, xml.release.releaseNotes, requestedIds);
    setResourceProperty(resource, VERSION_DEF_RELEASE_VERSION, xml.release.version, requestedIds);
    setResourceProperty(resource, VERSION_DEF_STACK_DEFAULT, xml.isStackDefault(), requestedIds);
    setResourceProperty(resource, VERSION_DEF_DISPLAY_NAME, xml.release.display, requestedIds);

    StackInfo stack = null;

    if (isPropertyRequested(VERSION_DEF_AVAILABLE_SERVICES, requestedIds) ||
        isPropertyRequested(VERSION_DEF_STACK_SERVICES, requestedIds)) {
      try {
        stack = s_metaInfo.get().getStack(stackId.getStackName(), stackId.getStackVersion());
      } catch (AmbariException e) {
        throw new SystemException(String.format("Could not load stack %s", stackId));
      }
    }

    if (null != stack) {
      setResourceProperty(resource, VERSION_DEF_AVAILABLE_SERVICES, xml.getAvailableServices(stack), requestedIds);
      setResourceProperty(resource, VERSION_DEF_STACK_SERVICES, xml.getStackServices(stack), requestedIds);
      setResourceProperty(resource, VERSION_DEF_MIN_JDK, stack.getMinJdk(), requestedIds);
      setResourceProperty(resource, VERSION_DEF_MAX_JDK, stack.getMaxJdk(), requestedIds);
      setResourceProperty(resource, VERSION_DEF_STACK_REPO_UPDATE_LINK_EXISTS, (stack.getRepositoryXml().getLatestURI() != null), requestedIds);
    }

    return resource;
  }

  /**
   * Provide the dry-run entity with fake sub-resources.  These are not queryable by normal API.
   */
  private void addSubresources(Resource res, RepositoryVersionEntity entity) {
    JsonNodeFactory factory = JsonNodeFactory.instance;

    ArrayNode subs = factory.arrayNode();

    for (RepoOsEntity os : entity.getRepoOsEntities()) {
      ObjectNode osBase = factory.objectNode();

      ObjectNode osElement = factory.objectNode();
      osElement.put(PropertyHelper.getPropertyName(OperatingSystemResourceProvider.OPERATING_SYSTEM_AMBARI_MANAGED_REPOS),
          os.isAmbariManaged());
      osElement.put(PropertyHelper.getPropertyName(OperatingSystemResourceProvider.OPERATING_SYSTEM_OS_TYPE_PROPERTY_ID),
          os.getFamily());

      osElement.put(PropertyHelper.getPropertyName(OperatingSystemResourceProvider.OPERATING_SYSTEM_STACK_NAME_PROPERTY_ID),
