View Javadoc

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 }