VersionInfoXmlManager.java

/*
 * #%L
 * xcode-maven-plugin
 * %%
 * Copyright (C) 2012 SAP AG
 * %%
 * 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.
 * #L%
 */
package com.sap.prd.mobile.ios.mios;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Properties;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.io.IOUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

import com.sap.prd.mobile.ios.mios.versioninfo.v_1_2_2.Coordinates;
import com.sap.prd.mobile.ios.mios.versioninfo.v_1_2_2.Dependency;
import com.sap.prd.mobile.ios.mios.versioninfo.v_1_2_2.SCM;
import com.sap.prd.mobile.ios.mios.versioninfo.v_1_2_2.Versions;

/**
 * Generates a <artifact-id>-<version>-version.xml for reproducibility reasons. This versions.xml
 * contains information about the scm location and revision of the built project and all its
 * dependencies. Expects a sync.info file in the root folder of the project as input. Additionally,
 * in case of dependencies, the information from the versions.xml files of the dependent projects is
 * added.
 * 
 */
public class VersionInfoXmlManager
{
  private static final String THREEDOTS = "...";

  private final static String SCHEMA_VERSION = "1.2.2";
  private final static String SCHEMA_GROUP_ID = "com.sap.prd.mobile.ios.mios";
  private final static String SCHEMA_ARTIFACT_ID = "versionschema";
  private final static String SCHEMA_REPOSITORY = "deploy.release.ondevice.hosted";
  private final static String NEXUS_URL = "http://nexus.wdf.sap.corp:8081/nexus";

  void createVersionInfoFile(final String groupId, final String artifactId, final String version,
        final File syncInfoFile, List<Dependency> dependencies, OutputStream versionInfoStream)
        throws MojoExecutionException
  {

    InputStream is = null;

    try {

      is = new FileInputStream(syncInfoFile);

      final Properties versionInfo = new Properties();

      versionInfo.load(is);

      createVersionInfoFile(groupId, artifactId, version, versionInfo, dependencies, versionInfoStream);

    }
    catch (IOException e) {
      throw new MojoExecutionException("Could not load sync info from file '" + syncInfoFile + "'.", e);
    }
    catch (JAXBException e) {
      throw new MojoExecutionException("Could not load sync info from file '" + syncInfoFile + "'.", e);
    } finally {
      IOUtils.closeQuietly(is);
    }
  }

  private void createVersionInfoFile(final String groupId, final String artifactId, final String version,
        Properties versionInfo, List<Dependency> dependencies, OutputStream os)
        throws MojoExecutionException, JAXBException
  {
    try {

      final Versions versions = new Versions();

      for (final Dependency dep : dependencies)
        versions.addDependency(dep);

      final SCM scm = new SCM();
      scm.setConnection(SCMUtil.getConnectionString(versionInfo, false));
      scm.setRevision(SCMUtil.getRevision(versionInfo));

      final Coordinates coordinates = new Coordinates();
      coordinates.setGroupId(groupId);
      coordinates.setArtifactId(artifactId);
      coordinates.setVersion(version);

      versions.setScm(scm);
      versions.setCoordinates(coordinates);

      final JAXBContext context = JAXBContext.newInstance(Versions.class);

      final Marshaller marshaller = context.createMarshaller();
      marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
      marshaller.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, "urn:xml.sap.com:XCodePlugin:VersionInfo" + " "
            + NEXUS_URL + "/content/repositories/" + SCHEMA_REPOSITORY + "/" + SCHEMA_GROUP_ID.replace(".", "/") + "/"
            + SCHEMA_ARTIFACT_ID + "/" + SCHEMA_VERSION + "/" + SCHEMA_ARTIFACT_ID + "-" + SCHEMA_VERSION + ".xsd");

      final ByteArrayOutputStream byteOs = new ByteArrayOutputStream();

      marshaller.marshal(versions, byteOs);

      final byte[] b = byteOs.toByteArray();

      DomUtils.validateDocument(DocumentBuilderFactory.newInstance().newDocumentBuilder()
        .parse(new ByteArrayInputStream(b)));

      IOUtils.write(b, os);
    }
    catch (ParserConfigurationException e) {
      throw new MojoExecutionException("Cannot create versions.xml.", e);
    }
    catch (IOException e) {
      throw new MojoExecutionException("Cannot create versions.xml.", e);
    }
    catch (SAXException e) {
      throw new MojoExecutionException("Cannot create versions.xml.", e);
    }
  }

  private static String getDepotPath(String fullDepotPath)
  {
    if (fullDepotPath.endsWith(THREEDOTS)) {
      fullDepotPath = fullDepotPath.substring(0, fullDepotPath.length() - THREEDOTS.length());
    }
    return fullDepotPath;
  }

  static Dependency parseDependency(final File f) throws JAXBException, SAXException, IOException
  {

    final String schemaVersion = getSchemaVersion(f);

    if (schemaVersion == null) {
      return parseOldDependency(f);
    }
    else if (schemaVersion.equals("1.2.0")) {
      return parseOldVersionsWithoutSchema(f);
    }
    else if ((schemaVersion.equals("1.2.1")) || (schemaVersion.equals("1.2.2"))) {
      return parseDependency_1_2_2(f);
    }
    throw new IllegalStateException("Unknown schemaVersion: '" + schemaVersion + "'.");
  }

  private static Dependency parseOldDependency(File f) throws JAXBException
  {
    final JAXBContext context = JAXBContext
      .newInstance(com.sap.prd.mobile.ios.mios.versioninfo.v_0_0_0.Versions.class);
    final Unmarshaller unmarshaller = context.createUnmarshaller();

    final com.sap.prd.mobile.ios.mios.versioninfo.v_0_0_0.Versions versions = (com.sap.prd.mobile.ios.mios.versioninfo.v_0_0_0.Versions) unmarshaller
      .unmarshal(f);

    final Coordinates coordinates = new Coordinates();
    coordinates.setGroupId(versions.getCoordinates().getGroupId());
    coordinates.setArtifactId(versions.getCoordinates().getArtifactId());
    coordinates.setVersion(versions.getCoordinates().getVersion());

    final SCM scm = new SCM();
    scm.setConnection("scm:perforce:" + versions.getScm().getRepository() + ":"
          + getDepotPath(versions.getScm().getPath()));
    scm.setRevision(versions.getScm().getSnapshotId());

    final Dependency dependency = new Dependency();
    dependency.setCoordinates(coordinates);
    dependency.setScm(scm);

    if (versions.getDependencies() != null) {
      for (com.sap.prd.mobile.ios.mios.versioninfo.v_0_0_0.Dependency d : versions.getDependencies()) {

        marshalOldDependencies(dependency, d);
      }
    }

    return dependency;
  }

  private static void marshalOldDependencies(final Dependency dependency,
        com.sap.prd.mobile.ios.mios.versioninfo.v_0_0_0.Dependency d)
  {
    Dependency _d = new Dependency();
    Coordinates depCoorinates = new Coordinates();
    depCoorinates.setGroupId(d.getCoordinates().getGroupId());
    depCoorinates.setArtifactId(d.getCoordinates().getArtifactId());
    depCoorinates.setVersion(d.getCoordinates().getVersion());
    _d.setCoordinates(depCoorinates);

    SCM depSCM = new SCM();
    depSCM.setConnection("scm:perforce:" + d.getScm().getRepository() + ":" + getDepotPath(d.getScm().getPath()));
    depSCM.setRevision(d.getScm().getSnapshotId());
    _d.setScm(depSCM);
    dependency.addDependency(_d);

    for (com.sap.prd.mobile.ios.mios.versioninfo.v_0_0_0.Dependency __d : d.getDependencies())
      marshalOldDependencies(_d, __d);
  }

  private static Dependency parseOldVersionsWithoutSchema(File f) throws JAXBException
  {
    final JAXBContext context = JAXBContext
      .newInstance(com.sap.prd.mobile.ios.mios.versioninfo.v_1_2_0.Versions.class);
    final Unmarshaller unmarshaller = context.createUnmarshaller();

    final com.sap.prd.mobile.ios.mios.versioninfo.v_1_2_0.Versions versions = (com.sap.prd.mobile.ios.mios.versioninfo.v_1_2_0.Versions) unmarshaller
      .unmarshal(f);

    final Coordinates coordinates = new Coordinates();
    coordinates.setGroupId(versions.getCoordinates().getGroupId());
    coordinates.setArtifactId(versions.getCoordinates().getArtifactId());
    coordinates.setVersion(versions.getCoordinates().getVersion());

    final SCM scm = new SCM();
    scm.setConnection(versions.getScm().getConnection());
    scm.setRevision(versions.getScm().getRevision());

    final Dependency dependency = new Dependency();
    dependency.setCoordinates(coordinates);
    dependency.setScm(scm);

    if (versions.getDependencies() != null) {
      for (com.sap.prd.mobile.ios.mios.versioninfo.v_1_2_0.Dependency d : versions.getDependencies()) {
        marshalDependenciesVersionsWithoutSchema(dependency, d);
      }
    }

    return dependency;
  }

  private static void marshalDependenciesVersionsWithoutSchema(final Dependency dependency,
        com.sap.prd.mobile.ios.mios.versioninfo.v_1_2_0.Dependency d)
  {
    Dependency _d = new Dependency();
    Coordinates depCoorinates = new Coordinates();
    depCoorinates.setGroupId(d.getCoordinates().getGroupId());
    depCoorinates.setArtifactId(d.getCoordinates().getArtifactId());
    depCoorinates.setVersion(d.getCoordinates().getVersion());
    _d.setCoordinates(depCoorinates);

    SCM depSCM = new SCM();
    depSCM.setConnection(d.getScm().getConnection());
    depSCM.setRevision(d.getScm().getRevision());
    _d.setScm(depSCM);
    dependency.addDependency(_d);

    for (com.sap.prd.mobile.ios.mios.versioninfo.v_1_2_0.Dependency __d : d.getDependencies())
      marshalDependenciesVersionsWithoutSchema(_d, __d);
  }

  private static Dependency parseDependency_1_2_2(File f) throws JAXBException
  {
    final JAXBContext context = JAXBContext.newInstance(Versions.class);
    final Unmarshaller unmarshaller = context.createUnmarshaller();

    final Versions versions = (Versions) unmarshaller.unmarshal(f);

    final Dependency dependency = new Dependency();
    dependency.setCoordinates(versions.getCoordinates());
    dependency.setScm(versions.getScm());

    if (versions.getDependencies() != null)
      for (Dependency d : versions.getDependencies())
        dependency.addDependency(d);

    return dependency;
  }

  private static String getSchemaVersion(final File f) throws SAXException, IOException
  {

    class StopParsingException extends SAXException
    {
      private static final long serialVersionUID = -7499102356648608429L;
    };

    XMLReader xmlReader = XMLReaderFactory.createXMLReader();

    final InputStream s = new FileInputStream(f);

    final String[] result = new String[1];

    try {

      xmlReader.setContentHandler(new ContentHandler() {

        boolean repositoryTagFound = false;

        @Override
        public void startPrefixMapping(String prefix, String uri) throws SAXException
        {
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException
        {
          if (localName.equals("versions")) {
            result[0] = atts.getValue("schemaVersion");

            if (result[0] != null)
              throw new StopParsingException();
          }

          if (localName.equals("repository")) {
            repositoryTagFound = true;
          }
        }

        @Override
        public void startDocument() throws SAXException
        {
        }

        @Override
        public void skippedEntity(String name) throws SAXException
        {
        }

        @Override
        public void setDocumentLocator(Locator locator)
        {
        }

        @Override
        public void processingInstruction(String target, String data) throws SAXException
        {
        }

        @Override
        public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException
        {
        }

        @Override
        public void endPrefixMapping(String prefix) throws SAXException
        {
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException
        {
        }

        @Override
        public void endDocument() throws SAXException
        {
          if (!repositoryTagFound && result[0] == null)
            result[0] = "1.2.0";
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException
        {
        }
      });

      try {
        xmlReader.parse(new InputSource(s));
      }
      catch (StopParsingException ex) {
        //OK, StopParsingException is thrown when we know all we need from the document.
      }
    }
    finally {
      IOUtils.closeQuietly(s);
    }
    return result[0];
  }
}