/** * 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.state.repository; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.StringWriter; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import javax.xml.XMLConstants; import javax.xml.bind.JAXBContext; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlTransient; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamReader; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import org.apache.ambari.server.state.ComponentInfo; import org.apache.ambari.server.state.RepositoryType; import org.apache.ambari.server.state.ServiceInfo; import org.apache.ambari.server.state.StackId; import org.apache.ambari.server.state.StackInfo; import org.apache.ambari.server.state.repository.AvailableVersion.Component; import org.apache.ambari.server.state.stack.RepositoryXml; import org.apache.ambari.server.state.stack.RepositoryXml.Os; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; /** * Class that wraps a repository definition file. */ @XmlRootElement(name="repository-version") @XmlAccessorType(XmlAccessType.FIELD) public class VersionDefinitionXml { public static String SCHEMA_LOCATION = "version_definition.xsd"; /** * Release details. */ @XmlElement(name = "release") public Release release; /** * The manifest of ALL services available in this repository. */ @XmlElementWrapper(name="manifest") @XmlElement(name="service") List<ManifestService> manifestServices = new ArrayList<>(); /** * For PATCH and SERVICE repositories, this dictates what is available for upgrade * from the manifest. */ @XmlElementWrapper(name="available-services") @XmlElement(name="service") List<AvailableServiceReference> availableServices = new ArrayList<>(); /** * Represents the repository details. This is reused from stack repo info. */ @XmlElement(name="repository-info") public RepositoryXml repositoryInfo; /** * The xsd location. Never {@code null}. */ @XmlTransient public String xsdLocation; @XmlTransient private Map<String, AvailableService> m_availableMap; @XmlTransient private List<ManifestServiceInfo> m_manifest = null; @XmlTransient private boolean m_stackDefault = false; @XmlTransient private Map<String, String> m_packageVersions = null; /** * @param stack the stack info needed to lookup service and component display names * @return a collection of AvailableServices used for web service consumption. This * collection is either the subset of the manifest, or the manifest itself if no services * are specified as "available". */ public synchronized Collection<AvailableService> getAvailableServices(StackInfo stack) { if (null == m_availableMap) { Map<String, ManifestService> manifests = buildManifest(); m_availableMap = new HashMap<>(); if (availableServices.isEmpty()) { for (ManifestService ms : manifests.values()) { addToAvailable(ms, stack, Collections.<String>emptySet()); } } else { for (AvailableServiceReference ref : availableServices) { ManifestService ms = manifests.get(ref.serviceIdReference); addToAvailable(ms, stack, ref.components); } } } return m_availableMap.values(); } /** * Gets the set of services that are included in this XML * @return an empty set for STANDARD repositories, or a non-empty set for PATCH type. */ public Set<String> getAvailableServiceNames() { if (availableServices.isEmpty()) { return Collections.emptySet(); } else { Set<String> serviceNames = new HashSet<>(); Map<String, ManifestService> manifest = buildManifest(); for (AvailableServiceReference ref : availableServices) { ManifestService ms = manifest.get(ref.serviceIdReference); serviceNames.add(ms.serviceName); } return serviceNames; } } /** * Gets if the version definition was built as the default for a stack * @return {@code true} if default for a stack */ public boolean isStackDefault() { return m_stackDefault; } /** * Gets the list of stack services, applying information from the version definition. * @param stack the stack for which to get the information * @return the list of {@code ManifestServiceInfo} instances for each service in the stack */ public synchronized List<ManifestServiceInfo> getStackServices(StackInfo stack) { if (null != m_manifest) { return m_manifest; } Map<String, Set<String>> manifestVersions = new HashMap<>(); for (ManifestService manifest : manifestServices) { String name = manifest.serviceName; if (!manifestVersions.containsKey(name)) { manifestVersions.put(manifest.serviceName, new TreeSet<String>()); } manifestVersions.get(manifest.serviceName).add(manifest.version); } m_manifest = new ArrayList<>(); for (ServiceInfo si : stack.getServices()) { Set<String> versions = manifestVersions.containsKey(si.getName()) ? manifestVersions.get(si.getName()) : Collections.singleton( null == si.getVersion() ? "" : si.getVersion()); m_manifest.add(new ManifestServiceInfo(si.getName(), si.getDisplayName(), si.getComment(), versions)); } return m_manifest; } /** * Gets the package version for an OS family * @param osFamily the os family * @return the package version, or {@code null} if not found */ public String getPackageVersion(String osFamily) { if (null == m_packageVersions) { m_packageVersions = new HashMap<>(); for (Os os : repositoryInfo.getOses()) { m_packageVersions.put(os.getFamily(), os.getPackageVersion()); } } return m_packageVersions.get(osFamily); } /** * Helper method to use a {@link ManifestService} to generate the available services structure * @param ms the ManifestService instance * @param stack the stack object * @param components the set of components for the service */ private void addToAvailable(ManifestService ms, StackInfo stack, Set<String> components) { ServiceInfo service = stack.getService(ms.serviceName); if (!m_availableMap.containsKey(ms.serviceName)) { String display = (null == service) ? ms.serviceName: service.getDisplayName(); m_availableMap.put(ms.serviceName, new AvailableService(ms.serviceName, display)); } AvailableService as = m_availableMap.get(ms.serviceName); as.getVersions().add(new AvailableVersion(ms.version, ms.versionId, buildComponents(service, components))); } /** * @return the list of manifest services to a map for easier access. */ private Map<String, ManifestService> buildManifest() { Map<String, ManifestService> normalized = new HashMap<>(); for (ManifestService ms : manifestServices) { normalized.put(ms.serviceId, ms); } return normalized; } /** * @param service the service containing components * @param components the set of components in the service * @return the set of components name/display name pairs */ private Set<Component> buildComponents(ServiceInfo service, Set<String> components) { Set<Component> set = new HashSet<>(); for (String component : components) { ComponentInfo ci = service.getComponentByName(component); String display = (null == ci) ? component : ci.getDisplayName(); set.add(new Component(component, display)); } return set; } /** * Returns the XML representation of this instance. */ public String toXml() throws Exception { JAXBContext ctx = JAXBContext.newInstance(VersionDefinitionXml.class); Marshaller marshaller = ctx.createMarshaller(); SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); InputStream xsdStream = VersionDefinitionXml.class.getClassLoader().getResourceAsStream(xsdLocation); if (null == xsdStream) { throw new Exception(String.format("Could not load XSD identified by '%s'", xsdLocation)); } try { Schema schema = factory.newSchema(new StreamSource(xsdStream)); marshaller.setSchema(schema); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); marshaller.setProperty("jaxb.noNamespaceSchemaLocation", xsdLocation); StringWriter w = new StringWriter(); marshaller.marshal(this, w); return w.toString(); } finally { IOUtils.closeQuietly(xsdStream); } } /** * Parses a URL for a definition XML file into the object graph. * @param url the URL to load. Can be a file URL reference also. * @return the definition */ public static VersionDefinitionXml load(URL url) throws Exception { InputStream stream = null; try { stream = url.openStream(); return load(stream); } finally { IOUtils.closeQuietly(stream); } } /** * Parses an xml string. Used when the xml is in the database. * @param xml the xml string * @return the definition */ public static VersionDefinitionXml load(String xml) throws Exception { return load(new ByteArrayInputStream(xml.getBytes("UTF-8"))); } /** * Parses a stream into an object graph * @param stream the stream * @return the definition * @throws Exception */ private static VersionDefinitionXml load(InputStream stream) throws Exception { XMLInputFactory xmlFactory = XMLInputFactory.newInstance(); XMLStreamReader xmlReader = xmlFactory.createXMLStreamReader(stream); xmlReader.nextTag(); String xsdName = xmlReader.getAttributeValue(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "noNamespaceSchemaLocation"); if (null == xsdName) { throw new IllegalArgumentException("Provided XML does not have a Schema defined using 'noNamespaceSchemaLocation'"); } InputStream xsdStream = VersionDefinitionXml.class.getClassLoader().getResourceAsStream(xsdName); if (null == xsdStream) { throw new Exception(String.format("Could not load XSD identified by '%s'", xsdName)); } JAXBContext ctx = JAXBContext.newInstance(VersionDefinitionXml.class); Unmarshaller unmarshaller = ctx.createUnmarshaller(); SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); Schema schema = factory.newSchema(new StreamSource(xsdStream)); unmarshaller.setSchema(schema); try { VersionDefinitionXml xml = (VersionDefinitionXml) unmarshaller.unmarshal(xmlReader); xml.xsdLocation = xsdName; return xml; } finally { IOUtils.closeQuietly(xsdStream); } } /** * Builds a Version Definition that is the default for the stack * @return the version definition */ public static VersionDefinitionXml build(StackInfo stackInfo) { VersionDefinitionXml xml = new VersionDefinitionXml(); xml.m_stackDefault = true; xml.release = new Release(); xml.repositoryInfo = new RepositoryXml(); xml.xsdLocation = SCHEMA_LOCATION; StackId stackId = new StackId(stackInfo.getName(), stackInfo.getVersion()); xml.release.repositoryType = RepositoryType.STANDARD; xml.release.stackId = stackId.toString(); xml.release.version = stackInfo.getVersion(); xml.release.releaseNotes = "NONE"; xml.release.display = stackId.toString(); for (ServiceInfo si : stackInfo.getServices()) { ManifestService ms = new ManifestService(); ms.serviceName = si.getName(); ms.version = StringUtils.trimToEmpty(si.getVersion()); ms.serviceId = ms.serviceName + "-" + ms.version.replace(".", ""); xml.manifestServices.add(ms); } if (null != stackInfo.getRepositoryXml()) { xml.repositoryInfo.getOses().addAll(stackInfo.getRepositoryXml().getOses()); } try { xml.toXml(); } catch (Exception e) { throw new IllegalArgumentException(e); } return xml; } /** * Used to facilitate merging when multiple version definitions are provided. Ambari * represents them as a unified entity. Since there is no knowledge of which one is * "correct" - the first one is used for the release meta-info. */ public static class Merger { private VersionDefinitionXml m_xml = new VersionDefinitionXml(); private boolean m_seeded = false; public Merger() { m_xml.release = new Release(); m_xml.repositoryInfo = new RepositoryXml(); } /** * Adds definition to this one. * @param version the version the definition represents * @param xml the definition object */ public void add(String version, VersionDefinitionXml xml) { if (!m_seeded) { m_xml.xsdLocation = xml.xsdLocation; StackId stackId = new StackId(xml.release.stackId); m_xml.release.build = null; // could be combining builds, so this is invalid m_xml.release.compatibleWith = xml.release.compatibleWith; m_xml.release.display = stackId.getStackName() + "-" + xml.release.version; m_xml.release.repositoryType = RepositoryType.STANDARD; // assumption since merging only done for new installs m_xml.release.releaseNotes = xml.release.releaseNotes; m_xml.release.stackId = xml.release.stackId; m_xml.release.version = version; m_xml.manifestServices.addAll(xml.manifestServices); m_seeded = true; } m_xml.repositoryInfo.getOses().addAll(xml.repositoryInfo.getOses()); } /** * @return the merged definition file */ public VersionDefinitionXml merge() { return m_seeded ? m_xml : null; } } }