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:kenney@apache.org">Kenney Westerhof</a>
087     * @author <a href="mailto:hboutemy@apache.org">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            @Override
125            public int getType() {
126                return INTEGER_ITEM;
127            }
128
129            @Override
130            public boolean isNull() {
131                return BIG_INTEGER_ZERO.equals(value);
132            }
133
134            @Override
135            public int compareTo(Item item) {
136                if (item == null) {
137                    return BIG_INTEGER_ZERO.equals(value) ? 0 : 1; // 1.0 == 1,
138                                                                   // 1.1 > 1
139                }
140
141                switch (item.getType()) {
142                case INTEGER_ITEM:
143                    return value.compareTo(((IntegerItem)item).value);
144
145                case STRING_ITEM:
146                    return 1; // 1.1 > 1-sp
147
148                case LIST_ITEM:
149                    return 1; // 1.1 > 1-1
150
151                default:
152                    throw new RuntimeException("invalid item: " + item.getClass());
153                }
154            }
155
156            @Override
157            public String toString() {
158                return value.toString();
159            }
160        }
161
162        /**
163         * Represents a string in the version item list, usually a qualifier.
164         */
165        private static class StringItem implements Item {
166            private static final String[] QUALIFIERS = {"alpha", "beta", "milestone", "rc", "snapshot", "", "sp"};
167
168            private static final List<String> QUALIFIERS_LIST = Arrays.asList(QUALIFIERS);
169
170            private static final Properties ALIASES = new Properties();
171
172            static {
173                ALIASES.put("ga", "");
174                ALIASES.put("final", "");
175                ALIASES.put("cr", "rc");
176            }
177
178            /**
179             * A comparable value for the empty-string qualifier. This one is
180             * used to determine if a given qualifier makes the version older
181             * than one without a qualifier, or more recent.
182             */
183            private static final String RELEASE_VERSION_INDEX = String.valueOf(QUALIFIERS_LIST.indexOf(""));
184
185            private String value;
186
187            StringItem(String value, boolean followedByDigit) {
188                if (followedByDigit && value.length() == 1) {
189                    // a1 = alpha-1, b1 = beta-1, m1 = milestone-1
190                    switch (value.charAt(0)) {
191                    case 'a':
192                        value = "alpha";
193                        break;
194                    case 'b':
195                        value = "beta";
196                        break;
197                    case 'm':
198                        value = "milestone";
199                        break;
200                    default:
201                    }
202                }
203                this.value = ALIASES.getProperty(value, value);
204            }
205
206            @Override
207            public int getType() {
208                return STRING_ITEM;
209            }
210
211            @Override
212            public boolean isNull() {
213                return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0;
214            }
215
216            /**
217             * Returns a comparable value for a qualifier. This method takes
218             * into account the ordering of known qualifiers then unknown
219             * qualifiers with lexical ordering. just returning an Integer with
220             * the index here is faster, but requires a lot of if/then/else to
221             * check for -1 or QUALIFIERS.size and then resort to lexical
222             * ordering. Most comparisons are decided by the first character, so
223             * this is still fast. If more characters are needed then it
224             * requires a lexical sort anyway.
225             *
226             * @param qualifier
227             * @return an equivalent value that can be used with lexical
228             *         comparison
229             */
230            public static String comparableQualifier(String qualifier) {
231                int i = QUALIFIERS_LIST.indexOf(qualifier);
232
233                return i == -1 ? (QUALIFIERS_LIST.size() + "-" + qualifier) : String.valueOf(i);
234            }
235
236            @Override
237            public int compareTo(Item item) {
238                if (item == null) {
239                    // 1-rc < 1, 1-ga > 1
240                    return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX);
241                }
242                switch (item.getType()) {
243                case INTEGER_ITEM:
244                    return -1; // 1.any < 1.1 ?
245
246                case STRING_ITEM:
247                    return comparableQualifier(value).compareTo(comparableQualifier(((StringItem)item).value));
248
249                case LIST_ITEM:
250                    return -1; // 1.any < 1-1
251
252                default:
253                    throw new RuntimeException("invalid item: " + item.getClass());
254                }
255            }
256
257            @Override
258            public String toString() {
259                return value;
260            }
261        }
262
263        /**
264         * Represents a version list item. This class is used both for the
265         * global item list and for sub-lists (which start with '-(number)' in
266         * the version specification).
267         */
268        @SuppressWarnings("serial")
269        private static class ListItem extends ArrayList<Item> implements Item {
270            @Override
271            public int getType() {
272                return LIST_ITEM;
273            }
274
275            @Override
276            public boolean isNull() {
277                return size() == 0;
278            }
279
280            void normalize() {
281                for (int i = size() - 1; i >= 0; i--) {
282                    Item lastItem = get(i);
283
284                    if (lastItem.isNull()) {
285                        // remove null trailing items: 0, "", empty list
286                        remove(i);
287                    } else if (!(lastItem instanceof ListItem)) {
288                        break;
289                    }
290                }
291            }
292
293            @Override
294            public int compareTo(Item item) {
295                if (item == null) {
296                    if (size() == 0) {
297                        return 0; // 1-0 = 1- (normalize) = 1
298                    }
299                    Item first = get(0);
300                    return first.compareTo(null);
301                }
302                switch (item.getType()) {
303                case INTEGER_ITEM:
304                    return -1; // 1-1 < 1.0.x
305
306                case STRING_ITEM:
307                    return 1; // 1-1 > 1-sp
308
309                case LIST_ITEM:
310                    Iterator<Item> left = iterator();
311                    Iterator<Item> right = ((ListItem)item).iterator();
312
313                    while (left.hasNext() || right.hasNext()) {
314                        Item l = left.hasNext() ? left.next() : null;
315                        Item r = right.hasNext() ? right.next() : null;
316
317                        // if this is shorter, then invert the compare and mul
318                        // with -1
319                        int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r);
320
321                        if (result != 0) {
322                            return result;
323                        }
324                    }
325
326                    return 0;
327
328                default:
329                    throw new RuntimeException("invalid item: " + item.getClass());
330                }
331            }
332
333            @Override
334            public String toString() {
335                StringBuilder buffer = new StringBuilder();
336                for (Item item : this) {
337                    if (buffer.length() > 0) {
338                        buffer.append((item instanceof ListItem) ? '-' : '.');
339                    }
340                    buffer.append(item);
341                }
342                return buffer.toString();
343            }
344        }
345
346        private ComparableVersion(String version) {
347            parseVersion(version);
348        }
349
350        private void parseVersion(String version) {
351            this.value = version;
352
353            items = new ListItem();
354
355            version = version.toLowerCase(Locale.ENGLISH);
356
357            ListItem list = items;
358
359            Stack<Item> stack = new Stack<>();
360            stack.push(list);
361
362            boolean isDigit = false;
363
364            int startIndex = 0;
365
366            for (int i = 0; i < version.length(); i++) {
367                char c = version.charAt(i);
368
369                if (c == '.') {
370                    if (i == startIndex) {
371                        list.add(IntegerItem.ZERO);
372                    } else {
373                        list.add(parseItem(isDigit, version.substring(startIndex, i)));
374                    }
375                    startIndex = i + 1;
376                } else if (c == '-') {
377                    if (i == startIndex) {
378                        list.add(IntegerItem.ZERO);
379                    } else {
380                        list.add(parseItem(isDigit, version.substring(startIndex, i)));
381                    }
382                    startIndex = i + 1;
383
384                    list.add(list = new ListItem());
385                    stack.push(list);
386                } else if (Character.isDigit(c)) {
387                    if (!isDigit && i > startIndex) {
388                        list.add(new StringItem(version.substring(startIndex, i), true));
389                        startIndex = i;
390
391                        list.add(list = new ListItem());
392                        stack.push(list);
393                    }
394
395                    isDigit = true;
396                } else {
397                    if (isDigit && i > startIndex) {
398                        list.add(parseItem(true, version.substring(startIndex, i)));
399                        startIndex = i;
400
401                        list.add(list = new ListItem());
402                        stack.push(list);
403                    }
404
405                    isDigit = false;
406                }
407            }
408
409            if (version.length() > startIndex) {
410                list.add(parseItem(isDigit, version.substring(startIndex)));
411            }
412
413            while (!stack.isEmpty()) {
414                list = (ListItem)stack.pop();
415                list.normalize();
416            }
417
418            canonical = items.toString();
419        }
420
421        private static Item parseItem(boolean isDigit, String buf) {
422            return isDigit ? new IntegerItem(buf) : new StringItem(buf, false);
423        }
424
425        @Override
426        public int compareTo(ComparableVersion o) {
427            return items.compareTo(o.items);
428        }
429
430        @Override
431        public String toString() {
432            return value;
433        }
434
435        @Override
436        public boolean equals(Object o) {
437            return (o instanceof ComparableVersion) && canonical.equals(((ComparableVersion)o).canonical);
438        }
439
440        @Override
441        public int hashCode() {
442            return canonical.hashCode();
443        }
444    }
445}