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    }