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 }