001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.util;
018
019import java.math.BigInteger;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Locale;
025import java.util.Properties;
026import java.util.Stack;
027
028/**
029 * A simple util to test Camel versions.
030 */
031public final class CamelVersionHelper {
032
033    private CamelVersionHelper() {
034        // utility class, never constructed
035    }
036
037    /**
038     * Checks whether other >= base
039     *
040     * @param base the base version
041     * @param other the other version
042     * @return <tt>true</tt> if GE, <tt>false</tt> otherwise
043     */
044    public static boolean isGE(String base, String other) {
045        ComparableVersion v1 = new ComparableVersion(base);
046        ComparableVersion v2 = new ComparableVersion(other);
047        return v2.compareTo(v1) >= 0;
048    }
049
050    /**
051     * Generic implementation of version comparison.
052     * https://github.com/apache/maven/blob/master/maven-artifact/src/main/java/
053     * org/apache/maven/artifact/versioning/ComparableVersion.java
054     * <p>
055     * Features:
056     * <ul>
057     * <li>mixing of '<code>-</code>' (hyphen) and '<code>.</code>' (dot)
058     * separators,</li>
059     * <li>transition between characters and digits also constitutes a
060     * separator: <code>1.0alpha1 =&gt; [1, 0, alpha, 1]</code></li>
061     * <li>unlimited number of version components,</li>
062     * <li>version components in the text can be digits or strings,</li>
063     * <li>strings are checked for well-known qualifiers and the qualifier
064     * ordering is used for version ordering. Well-known qualifiers (case
065     * insensitive) are:
066     * <ul>
067     * <li><code>alpha</code> or <code>a</code></li>
068     * <li><code>beta</code> or <code>b</code></li>
069     * <li><code>milestone</code> or <code>m</code></li>
070     * <li><code>rc</code> or <code>cr</code></li>
071     * <li><code>snapshot</code></li>
072     * <li><code>(the empty string)</code> or <code>ga</code> or
073     * <code>final</code></li>
074     * <li><code>sp</code></li>
075     * </ul>
076     * Unknown qualifiers are considered after known qualifiers, with lexical
077     * order (always case insensitive),</li>
078     * <li>a hyphen usually precedes a qualifier, and is always less important
079     * than something preceded with a dot.</li>
080     * </ul>
081     * </p>
082     *
083     * @see <a href=
084     *      "https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning">
085     *      "Versioning" on Maven Wiki</a>
086     * @author <a href="mailto:[email protected]">Kenney Westerhof</a>
087     * @author <a href="mailto:[email protected]">HervĂ© Boutemy</a>
088     */
089    private static final class ComparableVersion implements Comparable<ComparableVersion> {
090
091        private String value;
092        private String canonical;
093        private ListItem items;
094
095        private interface Item {
096            int INTEGER_ITEM = 0;
097            int STRING_ITEM = 1;
098            int LIST_ITEM = 2;
099
100            int compareTo(Item item);
101
102            int getType();
103
104            boolean isNull();
105        }
106
107        /**
108         * Represents a numeric item in the version item list.
109         */
110        private static class IntegerItem implements Item {
111
112            private static final BigInteger BIG_INTEGER_ZERO = new BigInteger("0");
113            private static final IntegerItem ZERO = new IntegerItem();
114            private final BigInteger value;
115
116            private IntegerItem() {
117                this.value = BIG_INTEGER_ZERO;
118            }
119
120            IntegerItem(String str) {
121                this.value = new BigInteger(str);
122            }
123
124            public int getType() {
125                return INTEGER_ITEM;
126            }
127
128            public boolean isNull() {
129                return BIG_INTEGER_ZERO.equals(value);
130            }
131
132            public int compareTo(Item item) {
133                if (item == null) {
134                    return BIG_INTEGER_ZERO.equals(value) ? 0 : 1; // 1.0 == 1,
135                                                                   // 1.1 > 1
136                }
137
138                switch (item.getType()) {
139                case INTEGER_ITEM:
140                    return value.compareTo(((IntegerItem)item).value);
141
142                case STRING_ITEM:
143                    return 1; // 1.1 > 1-sp
144
145                case LIST_ITEM:
146                    return 1; // 1.1 > 1-1
147
148                default:
149                    throw new RuntimeException("invalid item: " + item.getClass());
150                }
151            }
152
153            public String toString() {
154                return value.toString();
155            }
156        }
157
158        /**
159         * Represents a string in the version item list, usually a qualifier.
160         */
161        private static class StringItem implements Item {
162            private static final String[] QUALIFIERS = {"alpha", "beta", "milestone", "rc", "snapshot", "", "sp"};
163
164            private static final List<String> QUALIFIERS_LIST = Arrays.asList(QUALIFIERS);
165
166            private static final Properties ALIASES = new Properties();
167
168            static {
169                ALIASES.put("ga", "");
170                ALIASES.put("final", "");
171                ALIASES.put("cr", "rc");
172            }
173
174            /**
175             * A comparable value for the empty-string qualifier. This one is
176             * used to determine if a given qualifier makes the version older
177             * than one without a qualifier, or more recent.
178             */
179            private static final String RELEASE_VERSION_INDEX = String.valueOf(QUALIFIERS_LIST.indexOf(""));
180
181            private String value;
182
183            StringItem(String value, boolean followedByDigit) {
184                if (followedByDigit && value.length() == 1) {
185                    // a1 = alpha-1, b1 = beta-1, m1 = milestone-1
186                    switch (value.charAt(0)) {
187                    case 'a':
188                        value = "alpha";
189                        break;
190                    case 'b':
191                        value = "beta";
192                        break;
193                    case 'm':
194                        value = "milestone";
195                        break;
196                    default:
197                    }
198                }
199                this.value = ALIASES.getProperty(value, value);
200            }
201
202            public int getType() {
203                return STRING_ITEM;
204            }
205
206            public boolean isNull() {
207                return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0;
208            }
209
210            /**
211             * Returns a comparable value for a qualifier. This method takes
212             * into account the ordering of known qualifiers then unknown
213             * qualifiers with lexical ordering. just returning an Integer with
214             * the index here is faster, but requires a lot of if/then/else to
215             * check for -1 or QUALIFIERS.size and then resort to lexical
216             * ordering. Most comparisons are decided by the first character, so
217             * this is still fast. If more characters are needed then it
218             * requires a lexical sort anyway.
219             *
220             * @param qualifier
221             * @return an equivalent value that can be used with lexical
222             *         comparison
223             */
224            public static String comparableQualifier(String qualifier) {
225                int i = QUALIFIERS_LIST.indexOf(qualifier);
226
227                return i == -1 ? (QUALIFIERS_LIST.size() + "-" + qualifier) : String.valueOf(i);
228            }
229
230            public int compareTo(Item item) {
231                if (item == null) {
232                    // 1-rc < 1, 1-ga > 1
233                    return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX);
234                }
235                switch (item.getType()) {
236                case INTEGER_ITEM:
237                    return -1; // 1.any < 1.1 ?
238
239                case STRING_ITEM:
240                    return comparableQualifier(value).compareTo(comparableQualifier(((StringItem)item).value));
241
242                case LIST_ITEM:
243                    return -1; // 1.any < 1-1
244
245                default:
246                    throw new RuntimeException("invalid item: " + item.getClass());
247                }
248            }
249
250            public String toString() {
251                return value;
252            }
253        }
254
255        /**
256         * Represents a version list item. This class is used both for the
257         * global item list and for sub-lists (which start with '-(number)' in
258         * the version specification).
259         */
260        @SuppressWarnings("serial")
261        private static class ListItem extends ArrayList<Item> implements Item {
262            public int getType() {
263                return LIST_ITEM;
264            }
265
266            public boolean isNull() {
267                return size() == 0;
268            }
269
270            void normalize() {
271                for (int i = size() - 1; i >= 0; i--) {
272                    Item lastItem = get(i);
273
274                    if (lastItem.isNull()) {
275                        // remove null trailing items: 0, "", empty list
276                        remove(i);
277                    } else if (!(lastItem instanceof ListItem)) {
278                        break;
279                    }
280                }
281            }
282
283            public int compareTo(Item item) {
284                if (item == null) {
285                    if (size() == 0) {
286                        return 0; // 1-0 = 1- (normalize) = 1
287                    }
288                    Item first = get(0);
289                    return first.compareTo(null);
290                }
291                switch (item.getType()) {
292                case INTEGER_ITEM:
293                    return -1; // 1-1 < 1.0.x
294
295                case STRING_ITEM:
296                    return 1; // 1-1 > 1-sp
297
298                case LIST_ITEM:
299                    Iterator<Item> left = iterator();
300                    Iterator<Item> right = ((ListItem)item).iterator();
301
302                    while (left.hasNext() || right.hasNext()) {
303                        Item l = left.hasNext() ? left.next() : null;
304                        Item r = right.hasNext() ? right.next() : null;
305
306                        // if this is shorter, then invert the compare and mul
307                        // with -1
308                        int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r);
309
310                        if (result != 0) {
311                            return result;
312                        }
313                    }
314
315                    return 0;
316
317                default:
318                    throw new RuntimeException("invalid item: " + item.getClass());
319                }
320            }
321
322            public String toString() {
323                StringBuilder buffer = new StringBuilder();
324                for (Item item : this) {
325                    if (buffer.length() > 0) {
326                        buffer.append((item instanceof ListItem) ? '-' : '.');
327                    }
328                    buffer.append(item);
329                }
330                return buffer.toString();
331            }
332        }
333
334        private ComparableVersion(String version) {
335            parseVersion(version);
336        }
337
338        private void parseVersion(String version) {
339            this.value = version;
340
341            items = new ListItem();
342
343            version = version.toLowerCase(Locale.ENGLISH);
344
345            ListItem list = items;
346
347            Stack<Item> stack = new Stack<>();
348            stack.push(list);
349
350            boolean isDigit = false;
351
352            int startIndex = 0;
353
354            for (int i = 0; i < version.length(); i++) {
355                char c = version.charAt(i);
356
357                if (c == '.') {
358                    if (i == startIndex) {
359                        list.add(IntegerItem.ZERO);
360                    } else {
361                        list.add(parseItem(isDigit, version.substring(startIndex, i)));
362                    }
363                    startIndex = i + 1;
364                } else if (c == '-') {
365                    if (i == startIndex) {
366                        list.add(IntegerItem.ZERO);
367                    } else {
368                        list.add(parseItem(isDigit, version.substring(startIndex, i)));
369                    }
370                    startIndex = i + 1;
371
372                    list.add(list = new ListItem());
373                    stack.push(list);
374                } else if (Character.isDigit(c)) {
375                    if (!isDigit && i > startIndex) {
376                        list.add(new StringItem(version.substring(startIndex, i), true));
377                        startIndex = i;
378
379                        list.add(list = new ListItem());
380                        stack.push(list);
381                    }
382
383                    isDigit = true;
384                } else {
385                    if (isDigit && i > startIndex) {
386                        list.add(parseItem(true, version.substring(startIndex, i)));
387                        startIndex = i;
388
389                        list.add(list = new ListItem());
390                        stack.push(list);
391                    }
392
393                    isDigit = false;
394                }
395            }
396
397            if (version.length() > startIndex) {
398                list.add(parseItem(isDigit, version.substring(startIndex)));
399            }
400
401            while (!stack.isEmpty()) {
402                list = (ListItem)stack.pop();
403                list.normalize();
404            }
405
406            canonical = items.toString();
407        }
408
409        private static Item parseItem(boolean isDigit, String buf) {
410            return isDigit ? new IntegerItem(buf) : new StringItem(buf, false);
411        }
412
413        public int compareTo(ComparableVersion o) {
414            return items.compareTo(o.items);
415        }
416
417        public String toString() {
418            return value;
419        }
420
421        public boolean equals(Object o) {
422            return (o instanceof ComparableVersion) && canonical.equals(((ComparableVersion)o).canonical);
423        }
424
425        public int hashCode() {
426            return canonical.hashCode();
427        }
428    }
429}