001/*
002 * Extra Bnd Repository Plugins
003 * Copyright (C) 2019  Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package de.mnl.osgi.bnd.maven;
020
021import aQute.bnd.version.Version;
022import aQute.maven.api.Program;
023import aQute.maven.api.Revision;
024import java.text.SimpleDateFormat;
025import java.util.Date;
026import java.util.Locale;
027import java.util.TimeZone;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030import org.apache.maven.artifact.versioning.ArtifactVersion;
031import org.apache.maven.artifact.versioning.ComparableVersion;
032import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
033
034/**
035 * Provides a model of an artifact version which can be used as a maven version.
036 * <P>
037 * The maven <a href="https://maven.apache.org/pom.html">POM reference</a> does
038 * not define a format for versions. This is presumably intentional as it allows
039 * artifacts with arbitrary versioning schemes to be referenced in a POM.
040 * <P>
041 * Maven tooling, on the other hand side, defines a rather <a href=
042 * "http://books.sonatype.com/mvnref-book/reference/pom-relationships-sect-pom-syntax.html#pom-reationships-sect-versions">restrictive
043 * version number pattern</a> for maven projects. Non-compliant version numbers
044 * are parsed as qualifier-only versions.
045 * <P>
046 * The parsing methods of this class make an attempt to interpret a version
047 * number as a
048 * &lt;major&gt;/&lt;minor&gt;/&lt;micro/incremental&gt;/&lt;qualifier&gt;
049 * pattern, even if it does not match the restrictive maven project version
050 * number pattern. The string representation of an instance of this class is
051 * always the original, unparsed (or "literal") representation because due to
052 * the permissive parsing algorithm used, the original representation cannot
053 * faithfully be reconstructed from the parsed components.
054 * <P>
055 * Contrary to bnd's {@link aQute.bnd.version.MavenVersion}, this 
056 * implementation inherits from {@link ArtifactVersion}, i.e. from the
057 * version as implemented by maven.
058 */
059@SuppressWarnings({ "PMD.GodClass" })
060public class MavenVersion extends MavenVersionSpecification
061        implements ArtifactVersion {
062
063    /** The usual format of a verson string. */
064    public static final String VERSION_STRING
065        = "(\\d{1,15})(\\.(\\d{1,9})(\\.(\\d{1,9}))?)?([-\\.]?([-_\\.\\da-zA-Z]+))?";
066
067    /** The usual format for a snapshot timestamp. */
068    public static final SimpleDateFormat SNAPSHOT_TIMESTAMP
069        = new SimpleDateFormat("yyyyMMdd.HHmmss", Locale.ROOT);
070
071    static {
072        synchronized (SNAPSHOT_TIMESTAMP) {
073            SNAPSHOT_TIMESTAMP.setTimeZone(TimeZone.getTimeZone("UTC"));
074        }
075    }
076
077    private static final Pattern VERSION = Pattern.compile(VERSION_STRING);
078
079    /** The snapshot identifier. */
080    public static final String SNAPSHOT = "SNAPSHOT";
081
082    /** The Constant HIGHEST. */
083    public static final MavenVersion HIGHEST
084        = new MavenVersion(Version.HIGHEST);
085
086    /** The Constant LOWEST. */
087    public static final MavenVersion LOWEST = new MavenVersion("0");
088
089    // Used as "container" for the components of the maven version number
090    private final Version version;
091    // Some maven versions are too odd to be restored after parsing, keep
092    // original.
093    private final String literal;
094    // Used for comparison, cached for efficiency.
095    private final ComparableVersion comparable;
096
097    private final boolean snapshot;
098
099    /**
100     * Creates a new instance. The maven version is parsed by an instance
101     * of {@link DefaultArtifactVersion}. The parsing is thus fully
102     * maven compliant.
103     *
104     * @param maven the version
105     */
106    public MavenVersion(String maven) {
107        this.literal = maven;
108        this.comparable = new ComparableVersion(literal);
109        DefaultArtifactVersion artVer = new DefaultArtifactVersion(maven);
110        this.version
111            = new Version(artVer.getMajorVersion(), artVer.getMinorVersion(),
112                artVer.getIncrementalVersion(), artVer.getQualifier());
113        this.snapshot = version.isSnapshot();
114    }
115
116    /**
117     * Creates a new maven version from an osgi version. The version components
118     * are copied, the string representation is built from the components as
119     * "&lt;major&gt;.&lt;minor&gt;.&lt;micro&gt;.&lt;qualifier&gt;"
120     *
121     * @param osgiVersion the osgi version
122     */
123    public MavenVersion(Version osgiVersion) {
124        this.version = osgiVersion;
125        StringBuilder qual = new StringBuilder("");
126        if (this.version.getQualifier() != null) {
127            qual.append('-');
128            qual.append(this.version.getQualifier());
129        }
130        this.literal = osgiVersion.getWithoutQualifier().toString() + qual;
131        this.comparable = new ComparableVersion(literal);
132        this.snapshot = osgiVersion.isSnapshot();
133    }
134
135    /**
136     * Creates a new maven version from an osgi version and an unparsed 
137     * literal. The version components are copied, the literal is used
138     * as string representation, the snapshot property is taken from
139     * the argument.
140     *
141     * @param osgiVersion the osgi version
142     * @param literal the literal
143     * @param isSnapshot whether it is a snapshot version
144     */
145    public MavenVersion(Version osgiVersion, String literal,
146            boolean isSnapshot) {
147        this.literal = literal;
148        this.comparable = new ComparableVersion(literal);
149        this.version = osgiVersion;
150        this.snapshot = isSnapshot;
151    }
152
153    /**
154     * Parses the string as a maven version, but allows a dot as separator
155     * before the qualifier.
156     * <P>
157     * Leading sequences of digits followed by a dot or dash are converted to
158     * the major, minor and incremental version components. A dash or a dot that
159     * is not followed by a digit or the third dot is interpreted as the start
160     * of the qualifier.
161     * <P>
162     * In particular, version numbers such as "1.2.3.4.5" are parsed as major=1,
163     * minor=2, incremental=3 and qualifier="4.5". This is closer to the
164     * (assumed) semantics of such a version number than the parsing implemented
165     * in maven tooling, which interprets the complete version as a qualifier in
166     * such cases.
167     *
168     * @param versionStr the version string
169     * @return the maven version
170     * @throws IllegalArgumentException if the version cannot be parsed
171     */
172    public static final MavenVersion parseString(String versionStr) {
173        if (versionStr == null) {
174            versionStr = "0";
175        } else {
176            versionStr = versionStr.trim();
177            if (versionStr.isEmpty()) {
178                versionStr = "0";
179            }
180        }
181        Matcher matcher = VERSION.matcher(versionStr);
182        if (!matcher.matches()) {
183            throw new IllegalArgumentException(
184                "Invalid syntax for version: " + versionStr);
185        }
186        int major = Integer.parseInt(matcher.group(1));
187        @SuppressWarnings("PMD.ConfusingTernary")
188        int minor = (matcher.group(3) != null)
189            ? Integer.parseInt(matcher.group(3))
190            : 0;
191        @SuppressWarnings("PMD.ConfusingTernary")
192        int micro = (matcher.group(5) != null)
193            ? Integer.parseInt(matcher.group(5))
194            : 0;
195        String qualifier = matcher.group(7);
196        Version version = new Version(major, minor, micro, qualifier);
197        return new MavenVersion(version);
198    }
199
200    /**
201     * Similar to {@link #parseString(String)}, but returns {@code null} if the
202     * version cannot be parsed.
203     * 
204     * @param versionStr the version string
205     * @return the maven version
206     */
207    @SuppressWarnings("PMD.AvoidCatchingGenericException")
208    public static final MavenVersion parseMavenString(String versionStr) {
209        try {
210            return new MavenVersion(versionStr);
211        } catch (Exception e) {
212            return null;
213        }
214    }
215
216    /**
217     * Creates a new {@link MavenVersion} from a 
218     * the given representation, see {@link #MavenVersion(String)}.
219     *
220     * @param maven the maven version string
221     * @return the maven version
222     */
223    public static final MavenVersion from(String maven) {
224        return new MavenVersion(maven);
225    }
226
227    /**
228     * Creates a new {@link MavenVersion} from a 
229     * bnd {@link aQute.bnd.version.MavenVersion}.
230     * Propagates {@code null} values.
231     *
232     * @param bndVer the bnd maven version
233     * @return the maven version
234     */
235    public static final MavenVersion
236            from(aQute.bnd.version.MavenVersion bndVer) {
237        if (bndVer == null) {
238            return null;
239        }
240        return new MavenVersion(bndVer.getOSGiVersion(), bndVer.toString(),
241            bndVer.isSnapshot());
242    }
243
244    /**
245     * Converts this version to a
246     * bnd {@link aQute.bnd.version.MavenVersion}.
247     * Propagates {@code null} pointers.
248     *
249     * @param version the version
250     * @return the bnd maven version
251     */
252    public static aQute.bnd.version.MavenVersion
253            toBndMavenVersion(MavenVersion version) {
254        if (version == null) {
255            return null;
256        }
257        return new aQute.bnd.version.MavenVersion(version.version);
258    }
259
260    /**
261     * Converts this version to a
262     * bnd {@link aQute.bnd.version.MavenVersion}.
263     *
264     * @return the a qute.bnd.version. maven version
265     */
266    public aQute.bnd.version.MavenVersion asBndMavenVersion() {
267        // Create from literal, "version" may have lost information.
268        return new aQute.bnd.version.MavenVersion(literal);
269    }
270
271    /**
272     * This method is required by the {@link ArtifactVersion} interface.
273     * However, because instances of this class are intended to be immutable, it
274     * is not implemented. Use one of the other {@code parse...} methods
275     * instead.
276     *
277     * @param version the version to parse
278     * @throws UnsupportedOperationException in any case
279     */
280    @Override
281    public void parseVersion(String version) {
282        throw new UnsupportedOperationException("Not implemented.");
283    }
284
285    /**
286     * Combines this version with a program to a revision.
287     *
288     * @param program the program
289     * @return the revision
290     */
291    @SuppressWarnings("PMD.ShortMethodName")
292    public Revision of(Program program) {
293        return program.version(asBndMavenVersion());
294    }
295
296    /*
297     * (non-Javadoc)
298     * 
299     * @see
300     * org.apache.maven.artifact.versioning.ArtifactVersion#getMajorVersion()
301     */
302    @Override
303    public int getMajorVersion() {
304        return version.getMajor();
305    }
306
307    /*
308     * (non-Javadoc)
309     * 
310     * @see
311     * org.apache.maven.artifact.versioning.ArtifactVersion#getMinorVersion()
312     */
313    @Override
314    public int getMinorVersion() {
315        return version.getMinor();
316    }
317
318    /*
319     * (non-Javadoc)
320     * 
321     * @see org.apache.maven.artifact.versioning.ArtifactVersion#
322     * getIncrementalVersion()
323     */
324    @Override
325    public int getIncrementalVersion() {
326        return version.getMicro();
327    }
328
329    /*
330     * (non-Javadoc)
331     * 
332     * @see
333     * org.apache.maven.artifact.versioning.ArtifactVersion#getBuildNumber()
334     */
335    @Override
336    public int getBuildNumber() {
337        return new DefaultArtifactVersion(literal).getBuildNumber();
338    }
339
340    /*
341     * (non-Javadoc)
342     * 
343     * @see org.apache.maven.artifact.versioning.ArtifactVersion#getQualifier()
344     */
345    @Override
346    public String getQualifier() {
347        return version.getQualifier();
348    }
349
350    /**
351     * Gets the comparable.
352     *
353     * @return the comparable
354     */
355    public ComparableVersion getComparable() {
356        return comparable;
357    }
358
359    /**
360     * Gets the osgi version.
361     *
362     * @return the osgi version
363     */
364    public Version getOsgiVersion() {
365        return version;
366    }
367
368    /**
369     * If the qualifier ends with -SNAPSHOT or for an OSGI version with a
370     * qualifier that is SNAPSHOT.
371     *
372     * @return true, if is snapshot
373     */
374    public boolean isSnapshot() {
375        return snapshot;
376    }
377
378    /**
379     * Compares maven version numbers according to the rules defined in the
380     * <a href=
381     * "https://maven.apache.org/pom.html#Version_Order_Specification">POM
382     * reference</a>.
383     *
384     * @param other the other
385     * @return the int
386     */
387    @Override
388    public int compareTo(ArtifactVersion other) {
389        if (other instanceof MavenVersion) {
390            return comparable.compareTo(((MavenVersion) other).comparable);
391        }
392        return comparable.compareTo(new ComparableVersion(other.toString()));
393    }
394
395    /*
396     * (non-Javadoc)
397     * 
398     * @see java.lang.Object#toString()
399     */
400    @Override
401    public String toString() {
402        return literal;
403    }
404
405    /*
406     * (non-Javadoc)
407     * 
408     * @see java.lang.Object#hashCode()
409     */
410    @Override
411    @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
412    public int hashCode() {
413        @SuppressWarnings("PMD.AvoidFinalLocalVariable")
414        final int prime = 31;
415        int result = 1;
416        result = prime * result + ((literal == null) ? 0 : literal.hashCode());
417        return result;
418    }
419
420    /*
421     * (non-Javadoc)
422     * 
423     * @see java.lang.Object#equals(java.lang.Object)
424     */
425    @Override
426    public boolean equals(Object obj) {
427        if (this == obj) {
428            return true;
429        }
430        if (obj == null) {
431            return false;
432        }
433        if (getClass() != obj.getClass()) {
434            return false;
435        }
436        MavenVersion other = (MavenVersion) obj;
437        return literal.equals(other.literal);
438    }
439
440    /**
441     * To snapshot.
442     *
443     * @return the maven version
444     */
445    public MavenVersion toSnapshot() {
446        Version newv = new Version(version.getMajor(), version.getMinor(),
447            version.getMicro(), SNAPSHOT);
448        return new MavenVersion(newv);
449    }
450
451    /**
452     * To snapshot.
453     *
454     * @param epoch the epoch
455     * @param build the build
456     * @return the maven version
457     */
458    public MavenVersion toSnapshot(long epoch, String build) {
459        return toSnapshot(toDateStamp(epoch, build));
460    }
461
462    /**
463     * To snapshot.
464     *
465     * @param timestamp the timestamp
466     * @param build the build
467     * @return the maven version
468     */
469    public MavenVersion toSnapshot(String timestamp, String build) {
470        if (build != null) {
471            timestamp += "-" + build;
472        }
473        return toSnapshot(timestamp);
474    }
475
476    /**
477     * To snapshot.
478     *
479     * @param dateStamp the date stamp
480     * @return the maven version
481     */
482    public MavenVersion toSnapshot(String dateStamp) {
483        // -SNAPSHOT == 9 characters
484        String clean = literal.substring(0, literal.length() - 9);
485        String result = clean + "-" + dateStamp;
486
487        return new MavenVersion(result);
488    }
489
490    /**
491     * Validate.
492     *
493     * @param value the value
494     * @return the string
495     */
496    public static String validate(String value) {
497        if (value == null) {
498            return "Version is null";
499        }
500        if (!VERSION.matcher(value).matches()) {
501            return "Not a version";
502        }
503        return null;
504    }
505
506    /**
507     * To date stamp.
508     *
509     * @param epoch the epoch
510     * @return the string
511     */
512    public static String toDateStamp(long epoch) {
513        String datestamp;
514        synchronized (SNAPSHOT_TIMESTAMP) {
515            datestamp = SNAPSHOT_TIMESTAMP.format(new Date(epoch));
516        }
517        return datestamp;
518
519    }
520
521    /**
522     * To date stamp.
523     *
524     * @param epoch the epoch
525     * @param build the build
526     * @return the string
527     */
528    public static String toDateStamp(long epoch, String build) {
529        StringBuilder str = new StringBuilder(toDateStamp(epoch));
530        if (build != null) {
531            str.append('-');
532            str.append(build);
533        }
534        return str.toString();
535    }
536
537    /**
538     * Cleanup version.
539     *
540     * @param version the version
541     * @return the string
542     */
543    public static String cleanupVersion(String version) {
544        return aQute.bnd.version.MavenVersion.cleanupVersion(version);
545    }
546
547}