001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.util.version;
020
021import java.math.BigInteger;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.List;
025import java.util.Locale;
026
027import org.eclipse.aether.version.Version;
028
029import static java.util.Objects.requireNonNull;
030
031/**
032 * A generic version, that is a version that accepts any input string and tries to apply common sense sorting. See
033 * {@link GenericVersionScheme} for details.
034 */
035public final class GenericVersion implements Version {
036
037    private final String version;
038
039    private final List<Item> items;
040
041    private final int hash;
042
043    /**
044     * Creates a generic version from the specified string.
045     *
046     * @param version the version string, must not be {@code null}
047     */
048    GenericVersion(String version) {
049        this.version = requireNonNull(version, "version cannot be null");
050        items = parse(version);
051        hash = items.hashCode();
052    }
053
054    /**
055     * Returns this instance backing string representation.
056     *
057     * @since 1.9.5
058     */
059    public String asString() {
060        return version;
061    }
062
063    /**
064     * Returns this instance tokenized representation as unmodifiable list.
065     *
066     * @since 1.9.5
067     */
068    public List<Item> asItems() {
069        return items;
070    }
071
072    private static List<Item> parse(String version) {
073        List<Item> items = new ArrayList<>();
074
075        for (Tokenizer tokenizer = new Tokenizer(version); tokenizer.next(); ) {
076            Item item = tokenizer.toItem();
077            items.add(item);
078        }
079
080        trimPadding(items);
081
082        return Collections.unmodifiableList(items);
083    }
084
085    /**
086     * Visible for testing.
087     */
088    static void trimPadding(List<Item> items) {
089        Boolean number = null;
090        int end = items.size() - 1;
091        for (int i = end; i > 0; i--) {
092            Item item = items.get(i);
093            if (!Boolean.valueOf(item.isNumber()).equals(number)) {
094                end = i;
095                number = item.isNumber();
096            }
097            if (end == i
098                    && (i == items.size() - 1 || items.get(i - 1).isNumber() == item.isNumber())
099                    && item.compareTo(null) == 0) {
100                items.remove(i);
101                end--;
102            }
103        }
104    }
105
106    @Override
107    public int compareTo(Version obj) {
108        final List<Item> these = items;
109        final List<Item> those = ((GenericVersion) obj).items;
110
111        boolean number = true;
112
113        for (int index = 0; ; index++) {
114            if (index >= these.size() && index >= those.size()) {
115                return 0;
116            } else if (index >= these.size()) {
117                return -comparePadding(those, index, null);
118            } else if (index >= those.size()) {
119                return comparePadding(these, index, null);
120            }
121
122            Item thisItem = these.get(index);
123            Item thatItem = those.get(index);
124
125            if (thisItem.isNumber() != thatItem.isNumber()) {
126                if (index == 0) {
127                    return thisItem.compareTo(thatItem);
128                }
129                if (number == thisItem.isNumber()) {
130                    return comparePadding(these, index, number);
131                } else {
132                    return -comparePadding(those, index, number);
133                }
134            } else {
135                int rel = thisItem.compareTo(thatItem);
136                if (rel != 0) {
137                    return rel;
138                }
139                number = thisItem.isNumber();
140            }
141        }
142    }
143
144    private static int comparePadding(List<Item> items, int index, Boolean number) {
145        int rel = 0;
146        for (int i = index; i < items.size(); i++) {
147            Item item = items.get(i);
148            if (number != null && number != item.isNumber()) {
149                // do not stop here, but continue, skipping non-number members
150                continue;
151            }
152            rel = item.compareTo(null);
153            if (rel != 0) {
154                break;
155            }
156        }
157        return rel;
158    }
159
160    @Override
161    public boolean equals(Object obj) {
162        return (obj instanceof GenericVersion) && compareTo((GenericVersion) obj) == 0;
163    }
164
165    @Override
166    public int hashCode() {
167        return hash;
168    }
169
170    @Override
171    public String toString() {
172        return version;
173    }
174
175    static final class Tokenizer {
176        private final String version;
177
178        private final int versionLength;
179
180        private int index;
181
182        private String token;
183
184        private boolean number;
185
186        private boolean terminatedByNumber;
187
188        Tokenizer(String version) {
189            this.version = (!version.isEmpty()) ? version : "0";
190            this.versionLength = this.version.length();
191        }
192
193        public boolean next() {
194            if (index >= versionLength) {
195                return false;
196            }
197
198            int state = -2;
199
200            int start = index;
201            int end = versionLength;
202            terminatedByNumber = false;
203
204            for (; index < versionLength; index++) {
205                char c = version.charAt(index);
206
207                if (c == '.' || c == '-' || c == '_') {
208                    end = index;
209                    index++;
210                    break;
211                } else {
212                    if (c >= '0' && c <= '9') { // only ASCII digits
213                        int digit = c - '0';
214                        if (state == -1) {
215                            end = index;
216                            terminatedByNumber = true;
217                            break;
218                        }
219                        if (state == 0) {
220                            // normalize numbers and strip leading zeros (prereq for Integer/BigInteger handling)
221                            start++;
222                        }
223                        state = (state > 0 || digit > 0) ? 1 : 0;
224                    } else {
225                        if (state >= 0) {
226                            end = index;
227                            break;
228                        }
229                        state = -1;
230                    }
231                }
232            }
233
234            if (end - start > 0) {
235                token = version.substring(start, end);
236                number = state >= 0;
237            } else {
238                token = "0";
239                number = true;
240            }
241
242            return true;
243        }
244
245        @Override
246        public String toString() {
247            return String.valueOf(token);
248        }
249
250        public Item toItem() {
251            if (number) {
252                try {
253                    if (token.length() < 10) {
254                        return new Item(Item.KIND_INT, Integer.parseInt(token));
255                    } else {
256                        return new Item(Item.KIND_BIGINT, new BigInteger(token));
257                    }
258                } catch (NumberFormatException e) {
259                    throw new IllegalStateException(e);
260                }
261            } else {
262                if (index >= version.length()) {
263                    if ("min".equalsIgnoreCase(token)) {
264                        return Item.MIN;
265                    } else if ("max".equalsIgnoreCase(token)) {
266                        return Item.MAX;
267                    }
268                }
269                if (terminatedByNumber && token.length() == 1) {
270                    switch (token.charAt(0)) {
271                        case 'a':
272                        case 'A':
273                            token = GenericQualifiers.LABEL_ALPHA;
274                            break;
275                        case 'b':
276                        case 'B':
277                            token = GenericQualifiers.LABEL_BETA;
278                            break;
279                        case 'm':
280                        case 'M':
281                            token = GenericQualifiers.LABEL_MILESTONE;
282                            break;
283                        default:
284                    }
285                }
286                token = token.toLowerCase(Locale.ENGLISH);
287                return GenericQualifiers.tokenQualifier(token)
288                        .map(integer -> new Item(Item.KIND_QUALIFIER, integer))
289                        .orElseGet(() -> new Item(Item.KIND_STRING, token));
290            }
291        }
292    }
293
294    static final class Item {
295
296        static final int KIND_MAX = 8;
297
298        static final int KIND_BIGINT = 5;
299
300        static final int KIND_INT = 4;
301
302        static final int KIND_STRING = 3;
303
304        static final int KIND_QUALIFIER = 2;
305
306        static final int KIND_MIN = 0;
307
308        static final Item MAX = new Item(KIND_MAX, "max");
309
310        static final Item MIN = new Item(KIND_MIN, "min");
311
312        private final int kind;
313
314        private final Object value;
315
316        Item(int kind, Object value) {
317            this.kind = kind;
318            this.value = value;
319        }
320
321        public boolean isNumber() {
322            return (kind & KIND_QUALIFIER) == 0; // i.e. kind != string/qualifier
323        }
324
325        public int compareTo(Item that) {
326            int rel;
327            if (that == null) {
328                // null in this context denotes the pad item (0 or "ga")
329                switch (kind) {
330                    case KIND_MIN:
331                        rel = -1;
332                        break;
333                    case KIND_MAX:
334                    case KIND_BIGINT:
335                    case KIND_STRING:
336                        rel = 1;
337                        break;
338                    case KIND_INT:
339                    case KIND_QUALIFIER:
340                        rel = (Integer) value;
341                        break;
342                    default:
343                        throw new IllegalStateException("unknown version item kind " + kind);
344                }
345            } else {
346                rel = kind - that.kind;
347                if (rel == 0) {
348                    switch (kind) {
349                        case KIND_MAX:
350                        case KIND_MIN:
351                            break;
352                        case KIND_BIGINT:
353                            rel = ((BigInteger) value).compareTo((BigInteger) that.value);
354                            break;
355                        case KIND_INT:
356                        case KIND_QUALIFIER:
357                            rel = ((Integer) value).compareTo((Integer) that.value);
358                            break;
359                        case KIND_STRING:
360                            rel = ((String) value).compareToIgnoreCase((String) that.value);
361                            break;
362                        default:
363                            throw new IllegalStateException("unknown version item kind " + kind);
364                    }
365                }
366            }
367            return rel;
368        }
369
370        @Override
371        public boolean equals(Object obj) {
372            return (obj instanceof Item) && compareTo((Item) obj) == 0;
373        }
374
375        @Override
376        public int hashCode() {
377            return value.hashCode() + kind * 31;
378        }
379
380        @Override
381        public String toString() {
382            return String.valueOf(value);
383        }
384    }
385}