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