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 * <major>/<minor>/<micro/incremental>/<qualifier> 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 * "<major>.<minor>.<micro>.<qualifier>" 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}