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    }