org.artifactory.maven.MavenModelUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.artifactory.maven.MavenModelUtils.java

Source

/*
 * Artifactory is a binaries repository manager.
 * Copyright (C) 2012 JFrog Ltd.
 *
 * Artifactory is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Artifactory 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Artifactory.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.artifactory.maven;

import com.google.common.collect.Lists;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.maven.artifact.repository.metadata.Metadata;
import org.apache.maven.artifact.repository.metadata.Snapshot;
import org.apache.maven.artifact.repository.metadata.SnapshotVersion;
import org.apache.maven.artifact.repository.metadata.Versioning;
import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader;
import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Writer;
import org.apache.maven.model.Model;
import org.apache.maven.model.Parent;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
import org.artifactory.api.context.ContextHelper;
import org.artifactory.api.maven.MavenArtifactInfo;
import org.artifactory.api.module.ModuleInfo;
import org.artifactory.common.ConstantValues;
import org.artifactory.ivy.IvyNaming;
import org.artifactory.ivy.IvyService;
import org.artifactory.mime.MavenNaming;
import org.artifactory.mime.NamingUtils;
import org.artifactory.sapi.common.RepositoryRuntimeException;
import org.artifactory.util.PathUtils;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Date;
import java.util.SortedSet;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Yoav Landman
 */
public abstract class MavenModelUtils {
    public static final String UTF8 = "utf-8";
    private static final Logger log = LoggerFactory.getLogger(MavenModelUtils.class);
    private static final DateTimeFormatter UNIQUE_SNAPSHOT_FORMATTER = DateTimeFormat.forPattern("yyyyMMdd.HHmmss")
            .withZoneUTC();
    //Uses lazy evaluation of the version (+?)
    //see: http://www.regular-expressions.info/reference.html
    //For testing, see: http://www.cis.upenn.edu/~matuszek/General/RegexTester/regex-tester.html
    //Should be used with standard artifact names:
    //artifactId-version.ext
    //artifactId-version-classifier.ext
    //groups: 1-artifactId; 2-version; 5 or 7-classifier; 8-packaging (file extension).
    //note that you may have a multi-part version value, e.g. 1.0-rc3, in this case check that there
    //is a '-' with a value and that the classifier is not empty and simply append it.
    private static final Pattern ARTIFACT_NAME_PATTERN = Pattern
            .compile("(.+?)-(\\d.+?(-SNAPSHOT)?)(-(.+?))?(-(.+?))?\\.(\\w{1,}?)");

    private MavenModelUtils() {
        // utility class
    }

    /**
     * @param time Time to format
     * @return Maven unique snapshot version timestamp for the input date. For example: 20130603.113821
     */
    public static String dateToUniqueSnapshotTimestamp(long time) {
        return UNIQUE_SNAPSHOT_FORMATTER.print(time);
    }

    /**
     * @param dateTime Time to format
     * @return Maven unique snapshot timestamp for the input date. For example: 20130603.113821
     */
    public static Date uniqueSnapshotTimestampToDate(String dateTime) {
        return UNIQUE_SNAPSHOT_FORMATTER.parseDateTime(dateTime).toDate();
    }

    /**
     * Creates a maven <code>Metadata</code> out of a string.
     *
     * @param metadataAsString String representing content of maven-metadata.xml
     * @return Metadata object created from the input string
     * @throws java.io.IOException If the input string is not a valid maven metadata
     */
    public static Metadata toMavenMetadata(String metadataAsString) throws IOException {
        return toMavenMetadata(new StringReader(metadataAsString));
    }

    /**
     * Creates a maven <code>Metadata</code> out of an input stream and will close the stream.
     *
     * @param metadataStream An input stream representing content of maven-metadata.xml
     * @return Metadata object created from the input stream
     * @throws java.io.IOException If the input stream is not a valid maven metadata
     */
    public static Metadata toMavenMetadata(InputStream metadataStream) throws IOException {
        return toMavenMetadata(new InputStreamReader(metadataStream, UTF8));
    }

    /**
     * Creates a maven <code>Metadata</code> out of a <code>java.io.Reader</code> and will close the reader.
     *
     * @param reader Reader representing content of maven-metadata.xml
     * @return Metadata object created from the reader
     * @throws java.io.IOException If the input reader doesn't holds a valid maven metadata
     */
    public static Metadata toMavenMetadata(Reader reader) throws IOException {
        MetadataXpp3Reader metadataReader = new MetadataXpp3Reader();
        try {
            return metadataReader.read(reader, false);
        } catch (XmlPullParserException e) {
            throw new IOException("Failed to parse metadata: " + e.getMessage());
        } finally {
            IOUtils.closeQuietly(reader);
        }
    }

    /**
     * Build custom maven-metadata.xml according to a specific version.
     *
     * @param moduleInfo The original {@code ModuleInfo} to assemble the maven metadata according to the same
     *                   gid,aid,and version, {@link org.apache.maven.artifact.repository.metadata.Versioning#setLastUpdatedTimestamp(java.util.Date)} is updated to now. and
     *                   the build number and timestamp in the {@link org.apache.maven.artifact.repository.metadata.Snapshot} is set according to the name.
     * @param fileName   The file name
     * @return The custom maven-metadata.xml
     */
    public static Metadata buildSnapshotMavenMetadata(ModuleInfo moduleInfo, String fileName) {
        Metadata metadata = new Metadata();
        metadata.setGroupId(moduleInfo.getOrganization());
        metadata.setArtifactId(moduleInfo.getModule());
        metadata.setVersion(moduleInfo.getBaseRevision() + "-" + moduleInfo.getFolderIntegrationRevision());
        Versioning versioning = new Versioning();
        metadata.setVersioning(versioning);
        versioning.setLastUpdatedTimestamp(new Date());
        Snapshot snapshot = new Snapshot();
        versioning.setSnapshot(snapshot);
        snapshot.setBuildNumber(MavenNaming.getUniqueSnapshotVersionBuildNumber(fileName));
        snapshot.setTimestamp(MavenNaming.getUniqueSnapshotVersionTimestamp(fileName));
        if (ConstantValues.mvnMetadataVersion3Enabled.getBoolean()) {
            SnapshotVersion snapshotVersion = new SnapshotVersion();
            snapshotVersion.setUpdated(StringUtils.remove(snapshot.getTimestamp(), '.'));
            snapshotVersion
                    .setVersion(moduleInfo.getBaseRevision() + "-" + moduleInfo.getFileIntegrationRevision());
            //Should always be a pom, since this method is called only by PropertiesAddonImpl.assembleDynamicMetadata
            snapshotVersion.setExtension(moduleInfo.getExt());
            versioning.setSnapshotVersions(Lists.newArrayList(snapshotVersion));
        }
        return metadata;
    }

    /**
     * Create xml string from the input <code>Metadata</code>.
     *
     * @param metadata Maven metadata object
     * @return Xml string for the input metadata
     */
    public static String mavenMetadataToString(Metadata metadata) throws IOException {
        MetadataXpp3Writer writer = new MetadataXpp3Writer();
        StringWriter stringWriter = new StringWriter();
        writer.write(stringWriter, metadata);
        return stringWriter.toString();
    }

    /**
     * @param artifactInfo Maven artifact info to build the model from
     * @return A maven {@link Model} matching the values of the maven artifact info.
     */
    public static Model toMavenModel(MavenArtifactInfo artifactInfo) {
        return new MavenPomBuilder().groupId(artifactInfo.getGroupId()).artifactId(artifactInfo.getArtifactId())
                .version(artifactInfo.getVersion()).packaging(artifactInfo.getType()).build();
    }

    public static String mavenModelToString(Model model) {
        MavenXpp3Writer writer = new MavenXpp3Writer();
        StringWriter stringWriter = new StringWriter();
        try {
            writer.write(stringWriter, model);
        } catch (IOException e) {
            throw new RepositoryRuntimeException("Failed to convert maven model to string", e);
        }
        return stringWriter.toString();
    }

    public static Model stringToMavenModel(String pomAsString) {
        MavenXpp3Reader reader = new MavenXpp3Reader();
        StringReader pomStream = new StringReader(pomAsString);
        try {
            return reader.read(pomStream);
        } catch (Exception e) {
            throw new RepositoryRuntimeException("Failed to convert string to maven model", e);
        }
    }

    /**
     * @param pomInputStream Input stream of the pom content.
     * @return Maven artifact info built from the pom data.
     */
    public static MavenArtifactInfo mavenModelToArtifactInfo(InputStream pomInputStream)
            throws IOException, XmlPullParserException {
        MavenXpp3Reader reader = new MavenXpp3Reader();
        InputStreamReader pomStream = new InputStreamReader(pomInputStream, UTF8);
        Model model = reader.read(pomStream);
        return mavenModelToArtifactInfo(model);
    }

    public static MavenArtifactInfo mavenModelToArtifactInfo(Model model) {
        Parent parent = model.getParent();
        String groupId = model.getGroupId();
        if (groupId == null && parent != null) {
            groupId = parent.getGroupId();
        }
        MavenArtifactInfo artifactInfo = new MavenArtifactInfo();
        artifactInfo.setGroupId(groupId);
        artifactInfo.setArtifactId(model.getArtifactId());
        String version = model.getVersion();
        if (version == null && parent != null) {
            version = parent.getVersion();
        }
        artifactInfo.setVersion(version);
        return artifactInfo;
    }

    /**
     * Returns a maven artifact info after gathering information from the given file
     *
     * @param file File to gather information from
     * @return MavenArtifactInfo object containing gathered information
     */
    public static MavenArtifactInfo artifactInfoFromFile(File file) {
        MavenArtifactInfo result;
        result = attemptToBuildInfoFromModel(file);
        if (result != null) {
            // built from model - most accurate, we're done
            return result;
        }

        // no info from a model, try to guess as good as possible based on the file name and path
        result = getInfoByMatching(file.getName());
        fillMissingRequiredFields(file, result);
        return result;
    }

    /**
     * Attempt to gather maven artifact information from the given file based on a model (pom, ivy etc.).
     *
     * @param file Uploaded file to gather info from
     * @return Maven artifact info based on the model, null if model not found or couldn't be parsed
     */
    private static MavenArtifactInfo attemptToBuildInfoFromModel(File file) {
        MavenArtifactInfo result = null;
        String fileName = file.getName();
        if (NamingUtils.isJarVariant(fileName)) {
            //File is a jar variant
            result = gatherInfoFromJarFile(file);
        } else if (MavenNaming.isClientOrServerPom(fileName)) {
            result = gatherInfoFromPomFile(file);
        } else if (IvyNaming.isIvyFileName(fileName)) {
            result = gatherInfoFromIvyFile(file);
        }
        return result;
    }

    /**
     * Gathers maven artifact information which was (or was not) managed to gather from the given Jar file
     *
     * @param file Jar file to gather info from
     */

    private static MavenArtifactInfo gatherInfoFromJarFile(File file) {
        MavenArtifactInfo artifactInfo = null;
        JarInputStream jis = null;
        JarEntry entry;
        try {
            //Create a stream and try to find the pom file within the jar
            jis = new JarInputStream(new FileInputStream(file));
            entry = getPomFile(jis);

            //If a valid pom file was found
            if (entry != null) {
                try {
                    //Read the uncompressed content
                    artifactInfo = mavenModelToArtifactInfo(jis);
                    artifactInfo.setType(PathUtils.getExtension(file.getPath()));
                } catch (Exception e) {
                    log.warn("Failed to read maven model from '" + entry.getName() + "'. Cause: " + e.getMessage()
                            + ".", e);
                    artifactInfo = null;
                }
            }
        } catch (IOException e) {
            log.warn("Failed to read maven model from '" + file + "'. Cause: " + e.getMessage() + ".", e);
        } finally {
            IOUtils.closeQuietly(jis);
        }
        return artifactInfo;
    }

    /**
     * Returns a JarEntry object if a valid pom file is found in the given jar input stream
     *
     * @param jis Input stream of given jar
     * @return JarEntry object if a pom file is found. Null if not
     * @throws IOException Any exceptions that might occur while using the given stream
     */
    private static JarEntry getPomFile(JarInputStream jis) throws IOException {
        if (jis != null) {
            JarEntry entry;
            while (((entry = jis.getNextJarEntry()) != null)) {
                String name = entry.getName();
                //Look for pom.xml in META-INF/maven/
                if (name.startsWith("META-INF/maven/") && name.endsWith("pom.xml")) {
                    return entry;
                }
            }
        }
        return null;
    }

    /**
     * @param file The file from which to try to extract the POM entry from.
     * @return The POM from the JAR in its String representation.
     */
    public static String getPomFileAsStringFromJar(File file) {
        JarEntry pomEntry;
        JarInputStream inputStream = null;
        try {
            inputStream = new JarInputStream(new FileInputStream(file));
            pomEntry = getPomFile(inputStream);
            if (pomEntry != null) {
                return IOUtils.toString(inputStream);
            }
        } catch (IOException e) {
            log.warn("Unable to read JAR to extract the POM from it.");
            // If the file has a corrupt file, the following error will be thrown.
            // See java.util.zip.ZipInputStream.getUTF8String()
        } catch (IllegalArgumentException iae) {
            log.warn("Unable to read JAR to extract the POM from it.");
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
        return null;
    }

    /**
     * Gathers maven artifact information which was (or was not) managed to gather from the given pom file
     *
     * @param file Jar file to gather info from
     * @return MavenArtifactInfo object to append info to, null if pom parsing failed
     */
    private static MavenArtifactInfo gatherInfoFromPomFile(File file) {
        MavenArtifactInfo result = null;
        FileInputStream in = null;
        try {
            in = new FileInputStream(file);
            result = mavenModelToArtifactInfo(in);
            result.setType(MavenArtifactInfo.POM);
        } catch (Exception e) {
            log.debug("Failed to read maven model from '{}'. Cause: {}.", file.getName(), e.getMessage());
        } finally {
            IOUtils.closeQuietly(in);
        }
        return result;
    }

    private static MavenArtifactInfo gatherInfoFromIvyFile(File file) {
        MavenArtifactInfo result = null;
        try {
            IvyService ivyService = ContextHelper.get().beanForType(IvyService.class);
            ModuleDescriptor ivyDescriptor = ivyService.parseIvyFile(file);
            if (ivyDescriptor != null) {
                ModuleRevisionId ivyModule = ivyDescriptor.getModuleRevisionId();
                result = new MavenArtifactInfo();
                result.setGroupId(ivyModule.getOrganisation());
                result.setArtifactId(ivyModule.getName());
                result.setVersion(ivyModule.getRevision());
                result.setClassifier("ivy");
                result.setType(MavenArtifactInfo.XML);
            } else {
                log.debug("Failed to read ivy model from '{}'", file.getName());
            }
        } catch (Exception e) {
            log.debug("Failed to read ivy model from '{}'. Cause: {}.", file.getName(), e.getMessage());
        }
        return result;
    }

    /**
     * Provides a fallback in order to fill in essential but missing information by using the given file's name
     *
     * @param file   Uploaded file to gather info from
     * @param result MavenArtifactInfo object to append info to
     */
    private static void fillMissingRequiredFields(File file, MavenArtifactInfo result) {
        String fileName = file.getName();
        String baseFileName = FilenameUtils.getBaseName(fileName);

        //Complete values by falling back to dumb defaults
        if (MavenArtifactInfo.NA.equals(result.getArtifactId())) {
            result.setArtifactId(baseFileName);
        }
        if (MavenArtifactInfo.NA.equals(result.getGroupId())) {
            //If we have no group, set it to be the same as the artifact name
            result.setGroupId(result.getArtifactId());
        }
        if (MavenArtifactInfo.NA.equals(result.getVersion())) {
            result.setVersion(baseFileName);
        }

        // fill the type if the extension is not null and the result holds the default (jar) or is NA
        String extension = PathUtils.getExtension(fileName);
        if (extension != null && (MavenArtifactInfo.NA.equals(result.getType())
                || MavenArtifactInfo.JAR.equals(result.getType()))) {
            result.setType(extension);
        }
    }

    /**
     * Returns a MavenArtifactInfo based on info that was managed to gather from the file name matcher
     *
     * @param fileName The file name (with no preceding path) to match against
     * @return MavenArtifactInfo object with gathered info
     */
    public static MavenArtifactInfo getInfoByMatching(String fileName) {
        MavenArtifactInfo mavenArtifactInfo = new MavenArtifactInfo();
        Matcher matcher = ARTIFACT_NAME_PATTERN.matcher(fileName);
        if (matcher.matches()) {
            mavenArtifactInfo.setArtifactId(matcher.group(1));
            String version = matcher.group(2);
            if (StringUtils.isNotBlank(matcher.group(5)) && StringUtils.isNotBlank((matcher.group(7)))) {
                version += "-" + matcher.group(5);
            }
            mavenArtifactInfo.setVersion(version);
            String classifier;
            if (StringUtils.isNotBlank(matcher.group(5)) && StringUtils.isBlank(matcher.group(7))) {
                classifier = matcher.group(5);
            } else {
                classifier = matcher.group(7);
            }
            mavenArtifactInfo.setClassifier(classifier);
            mavenArtifactInfo.setType(matcher.group(8));
        }
        return mavenArtifactInfo;
    }

    /**
     * Returns a MavenArtifactInfo based on the supplied maven GAV coordinates, e.g. "group.id:artifactId:version"
     *
     * @param gav GAV, GAVC or GACV string
     * @return MavenMetadataInfo object with gathered info
     */
    @Nonnull
    public static MavenArtifactInfo getInfoFromGavString(String gav) {
        MavenArtifactInfo result = new MavenArtifactInfo();
        String[] splitId = gav.split(":");
        if (splitId.length == 4) {
            result.setGroupId(splitId[0]);
            result.setArtifactId(splitId[1]);
            result.setClassifier(splitId[2]);
            result.setVersion(splitId[3]);
        } else if (splitId.length == 3) {
            result.setGroupId(splitId[0]);
            result.setArtifactId(splitId[1]);
            result.setVersion(splitId[2]);
        }
        return result;
    }

    /**
     * Constructs GAV or GACV string representation of the given MavenArtifactInfo.
     *
     * @param info Maven info to use
     * @param gacv if true, creates "GroupId:ArtifactId:Classifier:Version" string, else create
     *             "GroupId:ArtifactId:Version"
     * @return GAV or GACV string
     */
    public static String getGavStringFromMavenInfo(MavenArtifactInfo info, boolean gacv) {
        StringBuilder builder = new StringBuilder(info.getGroupId());
        builder.append(":").append(info.getArtifactId());
        if (gacv) {
            builder.append(":").append(info.getClassifier());
        }
        return builder.append(":").append(info.getVersion()).toString();
    }

    public static Metadata buildReleasesMavenMetadata(String organization, String module,
            SortedSet<String> sortedVersions) {
        Metadata metadata = new Metadata();
        metadata.setGroupId(organization);
        metadata.setArtifactId(module);
        if (!sortedVersions.isEmpty()) {
            metadata.setVersion(sortedVersions.first());
            Versioning versioning = new Versioning();
            metadata.setVersioning(versioning);
            versioning.setVersions(Lists.newArrayList(sortedVersions));
            versioning.setLastUpdatedTimestamp(new Date());
            versioning.setLatest(sortedVersions.last());
            versioning.setRelease(sortedVersions.last());
        }
        return metadata;
    }
}