1 /*
2 * ModeShape (http://www.modeshape.org)
3 * See the COPYRIGHT.txt file distributed with this work for information
4 * regarding copyright ownership. Some portions may be licensed
5 * to Red Hat, Inc. under one or more contributor license agreements.
6 * See the AUTHORS.txt file in the distribution for a full listing of
7 * individual contributors.
8 *
9 * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10 * is licensed to you under the terms of the GNU Lesser General Public License as
11 * published by the Free Software Foundation; either version 2.1 of
12 * the License, or (at your option) any later version.
13 *
14 * ModeShape is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 * Lesser General Public License for more details.
18 *
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with this software; if not, write to the Free
21 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23 */
24 package org.modeshape.maven;
25
26 import java.util.LinkedHashSet;
27 import java.util.Set;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30 import org.modeshape.common.text.TextEncoder;
31 import org.modeshape.common.text.NoOpEncoder;
32 import org.modeshape.common.util.CheckArg;
33 import org.modeshape.common.util.HashCode;
34 import org.modeshape.common.util.StringUtil;
35
36 /**
37 * Identifier of a Maven 2 artifact.
38 */
39 public class MavenId implements Comparable<MavenId>, Cloneable {
40
41 /**
42 * Build a classpath of {@link MavenId}s by parsing the supplied string containing comma-separated Maven artifact
43 * coordinates. Any duplicates in the classpath are excluded.
44 * @param commaSeparatedCoordinates the string of Maven artifact coordinates
45 * @return the array of {@link MavenId} instances representing the classpath
46 */
47 public static MavenId[] createClasspath( String commaSeparatedCoordinates ) {
48 if (commaSeparatedCoordinates == null) return new MavenId[] {};
49 String[] coordinates = commaSeparatedCoordinates.split(",");
50 return createClasspath(coordinates);
51 }
52
53 /**
54 * Build a classpath of {@link MavenId}s by parsing the supplied Maven artifact coordinates. Any duplicates in the classpath
55 * are excluded.
56 * @param mavenCoordinates the array of Maven artifact coordinates
57 * @return the array of {@link MavenId} instances representing the classpath
58 */
59 public static MavenId[] createClasspath( String... mavenCoordinates ) {
60 if (mavenCoordinates == null) return new MavenId[] {};
61 // Use a linked set that maintains order and adds no duplicates ...
62 Set<MavenId> result = new LinkedHashSet<MavenId>();
63 for (int i = 0; i < mavenCoordinates.length; i++) {
64 String coordinateStr = mavenCoordinates[i];
65 if (coordinateStr == null) continue;
66 coordinateStr = coordinateStr.trim();
67 if (coordinateStr.length() != 0) {
68 result.add(new MavenId(coordinateStr));
69 }
70 }
71 return result.toArray(new MavenId[result.size()]);
72 }
73
74 /**
75 * Create a classpath of {@link MavenId}s by examining the supplied IDs and removing any duplicates.
76 * @param mavenIds the Maven IDs
77 * @return the array of {@link MavenId} instances representing the classpath
78 */
79 public static MavenId[] createClasspath( MavenId... mavenIds ) {
80 // Use a linked set that maintains order and adds no duplicates ...
81 Set<MavenId> result = new LinkedHashSet<MavenId>();
82 for (MavenId mavenId : mavenIds) {
83 if (mavenId != null) result.add(mavenId);
84 }
85 return result.toArray(new MavenId[result.size()]);
86 }
87
88 private final String groupId;
89 private final String artifactId;
90 private final Version version;
91 private final String classifier;
92
93 /**
94 * Create an Maven ID from the supplied string containing the coordinates for a Maven artifact. Coordinates are of the form:
95 *
96 * <pre>
97 * groupId:artifactId[:version[:classifier]]
98 * </pre>
99 *
100 * where
101 * <dl>
102 * <dt>groupId</dt>
103 * <dd> is the group identifier (e.g., <code>org.modeshape</code>), which may not be empty
104 * <dt>artifactId</dt>
105 * <dd> is the artifact identifier (e.g., <code>modeshape-maven</code>), which may not be empty
106 * <dt>version</dt>
107 * <dd> is the optional version (e.g., <code>org.modeshape</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 }