001 /* 002 * JBoss, Home of Professional Open Source. 003 * Copyright 2008, Red Hat Middleware LLC, and individual contributors 004 * as indicated by the @author tags. See the copyright.txt file in the 005 * distribution for a full listing of individual contributors. 006 * 007 * This is free software; you can redistribute it and/or modify it 008 * under the terms of the GNU Lesser General Public License as 009 * published by the Free Software Foundation; either version 2.1 of 010 * the License, or (at your option) any later version. 011 * 012 * This software is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this software; if not, write to the Free 019 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 020 * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 021 */ 022 package org.jboss.dna.maven; 023 024 import java.util.LinkedHashSet; 025 import java.util.Set; 026 import java.util.regex.Matcher; 027 import java.util.regex.Pattern; 028 import org.jboss.dna.common.text.TextEncoder; 029 import org.jboss.dna.common.text.NoOpEncoder; 030 import org.jboss.dna.common.util.CheckArg; 031 import org.jboss.dna.common.util.HashCode; 032 import org.jboss.dna.common.util.StringUtil; 033 034 /** 035 * Identifier of a Maven 2 artifact. 036 */ 037 public class MavenId implements Comparable<MavenId>, Cloneable { 038 039 /** 040 * Build a classpath of {@link MavenId}s by parsing the supplied string containing comma-separated Maven artifact 041 * coordinates. Any duplicates in the classpath are excluded. 042 * @param commaSeparatedCoordinates the string of Maven artifact coordinates 043 * @return the array of {@link MavenId} instances representing the classpath 044 */ 045 public static MavenId[] createClasspath( String commaSeparatedCoordinates ) { 046 if (commaSeparatedCoordinates == null) return new MavenId[] {}; 047 String[] coordinates = commaSeparatedCoordinates.split(","); 048 return createClasspath(coordinates); 049 } 050 051 /** 052 * Build a classpath of {@link MavenId}s by parsing the supplied Maven artifact coordinates. Any duplicates in the classpath 053 * are excluded. 054 * @param mavenCoordinates the array of Maven artifact coordinates 055 * @return the array of {@link MavenId} instances representing the classpath 056 */ 057 public static MavenId[] createClasspath( String... mavenCoordinates ) { 058 if (mavenCoordinates == null) return new MavenId[] {}; 059 // Use a linked set that maintains order and adds no duplicates ... 060 Set<MavenId> result = new LinkedHashSet<MavenId>(); 061 for (int i = 0; i < mavenCoordinates.length; i++) { 062 String coordinateStr = mavenCoordinates[i]; 063 if (coordinateStr == null) continue; 064 coordinateStr = coordinateStr.trim(); 065 if (coordinateStr.length() != 0) { 066 result.add(new MavenId(coordinateStr)); 067 } 068 } 069 return result.toArray(new MavenId[result.size()]); 070 } 071 072 /** 073 * Create a classpath of {@link MavenId}s by examining the supplied IDs and removing any duplicates. 074 * @param mavenIds the Maven IDs 075 * @return the array of {@link MavenId} instances representing the classpath 076 */ 077 public static MavenId[] createClasspath( MavenId... mavenIds ) { 078 // Use a linked set that maintains order and adds no duplicates ... 079 Set<MavenId> result = new LinkedHashSet<MavenId>(); 080 for (MavenId mavenId : mavenIds) { 081 if (mavenId != null) result.add(mavenId); 082 } 083 return result.toArray(new MavenId[result.size()]); 084 } 085 086 private final String groupId; 087 private final String artifactId; 088 private final Version version; 089 private final String classifier; 090 091 /** 092 * Create an Maven ID from the supplied string containing the coordinates for a Maven artifact. Coordinates are of the form: 093 * 094 * <pre> 095 * groupId:artifactId[:version[:classifier]] 096 * </pre> 097 * 098 * where 099 * <dl> 100 * <dt>groupId</dt> 101 * <dd> is the group identifier (e.g., <code>org.jboss.dna</code>), which may not be empty 102 * <dt>artifactId</dt> 103 * <dd> is the artifact identifier (e.g., <code>dna-maven</code>), which may not be empty 104 * <dt>version</dt> 105 * <dd> is the optional version (e.g., <code>org.jboss.dna</code>) 106 * <dt>classifier</dt> 107 * <dd> is the optional classifier (e.g., <code>test</code> or <code>jdk1.4</code>) 108 * </dl> 109 * @param coordinates the string containing the Maven coordinates 110 * @throws IllegalArgumentException if the supplied string is null or if the string does not match the expected format 111 */ 112 public MavenId( String coordinates ) { 113 CheckArg.isNotNull(coordinates, "coordinates"); 114 coordinates = coordinates.trim(); 115 CheckArg.isNotEmpty(coordinates, "coordinates"); 116 117 // This regular expression has the following groups: 118 // 1) groupId 119 // 2) :artifactId 120 // 3) artifactId 121 // 4) :version 122 // 5) version 123 // 6) :classifier 124 // 7) classifier 125 Pattern urlPattern = Pattern.compile("([^:]+)(:([^:]+)(:([^:]*)(:([^:]*))?)?)?"); 126 Matcher matcher = urlPattern.matcher(coordinates); 127 if (!matcher.find()) { 128 throw new IllegalArgumentException(MavenI18n.unsupportedMavenCoordinateFormat.text(coordinates)); 129 } 130 String groupId = matcher.group(1); 131 String artifactId = matcher.group(3); 132 String version = matcher.group(5); 133 String classifier = matcher.group(7); 134 CheckArg.isNotEmpty(groupId, "groupId"); 135 CheckArg.isNotEmpty(artifactId, "artifactId"); 136 this.groupId = groupId.trim(); 137 this.artifactId = artifactId.trim(); 138 this.classifier = classifier != null ? classifier.trim() : ""; 139 this.version = version != null ? new Version(version) : new Version(""); 140 } 141 142 /** 143 * Create a Maven ID from the supplied group and artifact IDs. 144 * @param groupId the group identifier 145 * @param artifactId the artifact identifier 146 * @throws IllegalArgumentException if the group or artifact identifiers are null, empty or blank 147 */ 148 public MavenId( String groupId, String artifactId ) { 149 this(groupId, artifactId, null, null); 150 } 151 152 /** 153 * Create a Maven ID from the supplied group and artifact IDs and the version. 154 * @param groupId the group identifier 155 * @param artifactId the artifact identifier 156 * @param version the version; may be null or empty 157 * @throws IllegalArgumentException if the group or artifact identifiers are null, empty or blank 158 */ 159 public MavenId( String groupId, String artifactId, String version ) { 160 this(groupId, artifactId, version, null); 161 } 162 163 /** 164 * Create a Maven ID from the supplied group ID, artifact ID, version, and classifier. 165 * @param groupId the group identifier 166 * @param artifactId the artifact identifier 167 * @param version the version; may be null or empty 168 * @param classifier the classifier; may be null or empty 169 * @throws IllegalArgumentException if the group or artifact identifiers are null, empty or blank 170 */ 171 public MavenId( String groupId, String artifactId, String version, String classifier ) { 172 CheckArg.isNotEmpty(groupId, "groupId"); 173 CheckArg.isNotEmpty(artifactId, "artifactId"); 174 this.groupId = groupId.trim(); 175 this.artifactId = artifactId.trim(); 176 this.classifier = classifier != null ? classifier.trim() : ""; 177 this.version = version != null ? new Version(version) : new Version(""); 178 } 179 180 /** 181 * A universally unique identifier for a project. It is normal to use a fully-qualified package name to distinguish it from 182 * other projects with a similar name (eg. <code>org.apache.maven</code>). 183 * @return the group identifier 184 */ 185 public String getGroupId() { 186 return this.groupId; 187 } 188 189 /** 190 * The identifier for this artifact that is unique within the group given by the group ID. An artifact is something that is 191 * either produced or used by a project. Examples of artifacts produced by Maven for a project include: JARs, source and 192 * binary distributions, and WARs. 193 * @return the artifact identifier 194 */ 195 public String getArtifactId() { 196 return this.artifactId; 197 } 198 199 /** 200 * @return classifier 201 */ 202 public String getClassifier() { 203 return this.classifier; 204 } 205 206 /** 207 * @return version 208 */ 209 public String getVersion() { 210 return this.version.toString(); 211 } 212 213 /** 214 * Return the relative JCR path for this resource, built from the components of the {@link #getGroupId() group ID}, the 215 * {@link #getArtifactId() artifact ID}, and the {@link #getVersion() version}. 216 * @return the path; never null 217 */ 218 public String getRelativePath() { 219 return getRelativePath(NoOpEncoder.getInstance()); 220 } 221 222 /** 223 * Return the relative JCR path for this resource, built from the components of the {@link #getGroupId() group ID}, the 224 * {@link #getArtifactId() artifact ID}, and the {@link #getVersion() version}. 225 * @param escapingStrategy the strategy to use for escaping characters that are not allowed in JCR names. 226 * @return the path; never null 227 */ 228 public String getRelativePath( TextEncoder escapingStrategy ) { 229 return getRelativePath(NoOpEncoder.getInstance(), true); 230 } 231 232 /** 233 * Return the relative JCR path for this resource, built from the components of the {@link #getGroupId() group ID}, the 234 * {@link #getArtifactId() artifact ID}, and the {@link #getVersion() version}. 235 * @param includeVersion true if the version is to be included in the path 236 * @return the path; never null 237 */ 238 public String getRelativePath( boolean includeVersion ) { 239 return getRelativePath(NoOpEncoder.getInstance(), includeVersion); 240 } 241 242 /** 243 * Return the relative JCR path for this resource, built from the components of the {@link #getGroupId() group ID}, the 244 * {@link #getArtifactId() artifact ID}, and the {@link #getVersion() version}. 245 * @param escapingStrategy the strategy to use for escaping characters that are not allowed in JCR names. 246 * @param includeVersion true if the version is to be included in the path 247 * @return the path; never null 248 */ 249 public String getRelativePath( TextEncoder escapingStrategy, boolean includeVersion ) { 250 StringBuilder sb = new StringBuilder(); 251 String[] groupComponents = this.getGroupId().split("[\\.]"); 252 for (String groupComponent : groupComponents) { 253 if (sb.length() != 0) sb.append("/"); 254 sb.append(escapingStrategy.encode(groupComponent)); 255 } 256 sb.append("/").append(escapingStrategy.encode(this.getArtifactId())); 257 if (includeVersion) { 258 sb.append("/").append(escapingStrategy.encode(this.getVersion())); 259 } 260 return sb.toString(); 261 } 262 263 public String getCoordinates() { 264 return StringUtil.createString("{0}:{1}:{2}:{3}", this.groupId, this.artifactId, this.version, this.classifier); 265 } 266 267 public static MavenId createFromCoordinates( String coordinates ) { 268 String[] parts = coordinates.split("[:]"); 269 String groupId = null; 270 String artifactId = null; 271 String version = null; 272 String classifier = null; 273 if (parts.length > 0) groupId = parts[0]; 274 if (parts.length > 1) artifactId = parts[1]; 275 if (parts.length > 2) version = parts[2]; 276 if (parts.length > 3) classifier = parts[3]; 277 return new MavenId(groupId, artifactId, classifier, version); 278 } 279 280 protected boolean isAnyVersion() { 281 return this.version.isAnyVersion(); 282 } 283 284 /** 285 * {@inheritDoc} 286 */ 287 @Override 288 public int hashCode() { 289 // The version is excluded from the hash code so that the 'any version' will be in the same bucket of a hash table 290 return HashCode.compute(this.groupId, this.artifactId, this.classifier); 291 } 292 293 /** 294 * {@inheritDoc} 295 */ 296 @Override 297 public boolean equals( Object obj ) { 298 if (this == obj) return true; 299 if (obj instanceof MavenId) { 300 MavenId that = (MavenId)obj; 301 if (!this.groupId.equalsIgnoreCase(that.groupId)) return false; 302 if (!this.artifactId.equalsIgnoreCase(that.artifactId)) return false; 303 if (!this.version.equals(that.version)) return false; 304 if (!this.classifier.equalsIgnoreCase(that.classifier)) return false; 305 return true; 306 } 307 return false; 308 } 309 310 /** 311 * {@inheritDoc} 312 */ 313 public int compareTo( MavenId that ) { 314 if (that == null) return 1; 315 if (this == that) return 0; 316 317 // Check the group ID ... 318 int diff = this.groupId.compareTo(that.groupId); 319 if (diff != 0) return diff; 320 321 // then the artifact ID ... 322 diff = this.artifactId.compareTo(that.artifactId); 323 if (diff != 0) return diff; 324 325 // then the version ... 326 diff = this.version.compareTo(that.version); 327 if (diff != 0) return diff; 328 329 // then the classifier ... 330 diff = this.classifier.compareTo(that.classifier); 331 return diff; 332 } 333 334 /** 335 * {@inheritDoc} 336 */ 337 @Override 338 public String toString() { 339 return this.getCoordinates(); 340 } 341 342 public class Version implements Comparable<Version> { 343 344 private final String version; 345 private final Object[] components; 346 347 protected Version( String version ) { 348 this.version = version != null ? version.trim() : ""; 349 this.components = getVersionComponents(this.version); 350 } 351 352 /** 353 * @return components 354 */ 355 public Object[] getComponents() { 356 return this.components; 357 } 358 359 public boolean isAnyVersion() { 360 return this.version.length() == 0; 361 } 362 363 /** 364 * {@inheritDoc} 365 */ 366 @Override 367 public String toString() { 368 return version; 369 } 370 371 /** 372 * {@inheritDoc} 373 */ 374 @Override 375 public int hashCode() { 376 return this.version.hashCode(); 377 } 378 379 /** 380 * {@inheritDoc} 381 */ 382 public int compareTo( Version that ) { 383 if (that == null) return 1; 384 Object[] thisComponents = this.getComponents(); 385 Object[] thatComponents = that.getComponents(); 386 int thisLength = thisComponents.length; 387 int thatLength = thatComponents.length; 388 int minLength = Math.min(thisLength, thatLength); 389 for (int i = 0; i != minLength; ++i) { 390 Object thisComponent = thisComponents[i]; 391 Object thatComponent = thatComponents[i]; 392 int diff = 0; 393 if (thisComponent instanceof Integer && thatComponent instanceof Integer) { 394 diff = ((Integer)thisComponent).compareTo((Integer)thatComponent); 395 } else { 396 String thisString = thisComponent.toString(); 397 String thatString = thatComponent.toString(); 398 diff = thisString.compareToIgnoreCase(thatString); 399 } 400 if (diff != 0) return diff; 401 } 402 return 0; 403 } 404 405 /** 406 * {@inheritDoc} 407 */ 408 @Override 409 public boolean equals( Object obj ) { 410 if (obj == this) return true; 411 if (obj instanceof Version) { 412 Version that = (Version)obj; 413 if (this.isAnyVersion() || that.isAnyVersion()) return true; 414 if (!this.version.equalsIgnoreCase(that.version)) return false; 415 return true; 416 } 417 return false; 418 } 419 } 420 421 /** 422 * Utility to break down the version string into the individual components. This utility splits the supplied version on 423 * periods ('.'), dashes ('-'), forward slashes ('/'), and commas (','). 424 * @param version the version string 425 * @return the array of {@link String} and {@link Integer} components; never null 426 */ 427 protected static Object[] getVersionComponents( String version ) { 428 if (version == null) return new Object[] {}; 429 version = version.trim(); 430 if (version.length() == 0) return new Object[] {}; 431 String[] parts = version.split("[\\.\\-/,]"); 432 if (parts == null) return new Object[] {}; 433 Object[] components = new Object[parts.length]; 434 for (int i = 0, len = parts.length; i < len; i++) { 435 String part = parts[i].trim(); 436 Object component = part; 437 try { 438 component = Integer.parseInt(part); 439 } catch (NumberFormatException e) { 440 // If there are any problems, we don't treat it as an integer 441 } 442 components[i] = component; 443 } 444 return components; 445 } 446 447 /** 448 * {@inheritDoc} 449 */ 450 @Override 451 public MavenId clone() { 452 return new MavenId(this.groupId, this.artifactId, this.version.toString(), this.classifier); 453 } 454 }