re base code
[sdc.git] / catalog-model / src / main / java / org / openecomp / sdc / be / model / tosca / version / ComparableVersion.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * SDC
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  * 
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  * 
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.openecomp.sdc.be.model.tosca.version;
22
23 /*
24  * Licensed to the Apache Software Foundation (ASF) under one
25  * or more contributor license agreements.  See the NOTICE file
26  * distributed with this work for additional information
27  * regarding copyright ownership.  The ASF licenses this file
28  * to you under the Apache License, Version 2.0 (the
29  * "License"); you may not use this file except in compliance
30  * with the License.  You may obtain a copy of the License at
31  *
32  *  http://www.apache.org/licenses/LICENSE-2.0
33  *
34  * Unless required by applicable law or agreed to in writing,
35  * software distributed under the License is distributed on an
36  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
37  * KIND, either express or implied.  See the License for the
38  * specific language governing permissions and limitations
39  * under the License.
40  */
41
42 import java.math.BigInteger;
43 import java.util.*;
44
45 /**
46  * Generic implementation of version comparison.
47  * 
48  * <p>
49  * Features:
50  * <ul>
51  * <li>mixing of '<code>-</code>' (dash) and '<code>.</code>' (dot)
52  * separators,</li>
53  * <li>transition between characters and digits also constitutes a separator:
54  * <code>1.0alpha1 =&gt; [1, 0, alpha, 1]</code></li>
55  * <li>unlimited number of version components,</li>
56  * <li>version components in the text can be digits or strings,</li>
57  * <li>strings are checked for well-known qualifiers and the qualifier ordering
58  * is used for version ordering. Well-known qualifiers (case insensitive) are:
59  * <ul>
60  * <li><code>alpha</code> or <code>a</code></li>
61  * <li><code>beta</code> or <code>b</code></li>
62  * <li><code>milestone</code> or <code>m</code></li>
63  * <li><code>rc</code> or <code>cr</code></li>
64  * <li><code>snapshot</code></li>
65  * <li><code>(the empty string)</code> or <code>ga</code> or
66  * <code>final</code></li>
67  * <li><code>sp</code></li>
68  * </ul>
69  * Unknown qualifiers are considered after known qualifiers, with lexical order
70  * (always case insensitive),</li>
71  * <li>a dash usually precedes a qualifier, and is always less important than
72  * something preceded with a dot.</li>
73  * </ul>
74  * </p>
75  * 
76  * @see <a href=
77  *      "https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning">"Versioning"
78  *      on Maven Wiki</a>
79  * @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a>
80  * @author <a href="mailto:hboutemy@apache.org">HervĂ© Boutemy</a>
81  */
82 public class ComparableVersion implements Comparable<ComparableVersion> {
83     private String value;
84
85     private String canonical;
86
87     private ListItem items;
88
89     private interface Item {
90         int INTEGER_ITEM = 0;
91         int STRING_ITEM = 1;
92         int LIST_ITEM = 2;
93
94         int compareTo(Item item);
95
96         int getType();
97
98         boolean isNull();
99     }
100
101     /**
102      * Represents a numeric item in the version item list.
103      */
104     private static class IntegerItem implements Item {
105         private static final String INVALID_ITEM = "invalid item: ";
106                 private static final BigInteger BIG_INTEGER_ZERO = new BigInteger("0");
107         private final BigInteger value;
108         public static final IntegerItem ZERO = new IntegerItem();
109
110         private IntegerItem() {
111             this.value = BIG_INTEGER_ZERO;
112         }
113
114         public IntegerItem(String str) {
115             this.value = new BigInteger(str);
116         }
117
118         @Override
119         public int getType() {
120             return INTEGER_ITEM;
121         }
122
123         @Override
124         public boolean isNull() {
125             return BIG_INTEGER_ZERO.equals(value);
126         }
127
128         @Override
129         public int compareTo(Item item) {
130             if (item == null) {
131                 return BIG_INTEGER_ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1
132                                                                 // > 1
133             }
134
135             switch (item.getType()) {
136             case INTEGER_ITEM:
137                 return value.compareTo(((IntegerItem) item).value);
138
139             case STRING_ITEM:
140                 return 1; // 1.1 > 1-sp
141
142             case LIST_ITEM:
143                 return 1; // 1.1 > 1-1
144
145             default:
146                 throw new RuntimeException(INVALID_ITEM + item.getClass());
147             }
148         }
149
150         @Override
151         public String toString() {
152             return value.toString();
153         }
154     }
155
156     /**
157      * Represents a string in the version item list, usually a qualifier.
158      */
159     private static class StringItem implements Item {
160         private static final String[] QUALIFIERS = { "alpha", "beta", "milestone", "rc", "snapshot", "", "sp" };
161
162         private static final List<String> _QUALIFIERS = Arrays.asList(QUALIFIERS);
163
164         private static final Properties ALIASES = new Properties();
165         static {
166             ALIASES.put("ga", "");
167             ALIASES.put("final", "");
168             ALIASES.put("cr", "rc");
169         }
170
171         /**
172          * A comparable value for the empty-string qualifier. This one is used
173          * to determine if a given qualifier makes the version older than one
174          * without a qualifier, or more recent.
175          */
176         private static final String RELEASE_VERSION_INDEX = String.valueOf(_QUALIFIERS.indexOf(""));
177
178         private String value;
179
180         public StringItem(String value, boolean followedByDigit) {
181             if (followedByDigit && value.length() == 1) {
182                 // a1 = alpha-1, b1 = beta-1, m1 = milestone-1
183                 switch (value.charAt(0)) {
184                 case 'a':
185                     value = "alpha";
186                     break;
187                 case 'b':
188                     value = "beta";
189                     break;
190                 case 'm':
191                     value = "milestone";
192                     break;
193                 }
194             }
195             this.value = ALIASES.getProperty(value, value);
196         }
197
198         @Override
199         public int getType() {
200             return STRING_ITEM;
201         }
202
203         @Override
204         public boolean isNull() {
205             return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0);
206         }
207
208         /**
209          * Returns a comparable value for a qualifier.
210          *
211          * This method takes into account the ordering of known qualifiers then
212          * unknown qualifiers with lexical ordering.
213          *
214          * just returning an Integer with the index here is faster, but requires
215          * a lot of if/then/else to check for -1 or QUALIFIERS.size and then
216          * resort to lexical ordering. Most comparisons are decided by the first
217          * character, so this is still fast. If more characters are needed then
218          * it requires a lexical sort anyway.
219          *
220          * @param qualifier
221          * @return an equivalent value that can be used with lexical comparison
222          */
223         public static String comparableQualifier(String qualifier) {
224             int i = _QUALIFIERS.indexOf(qualifier);
225
226             return i == -1 ? (_QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i);
227         }
228
229         // @Override
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         @Override
251         public String toString() {
252             return value;
253         }
254     }
255
256     /**
257      * Represents a version list item. This class is used both for the global
258      * item list and for sub-lists (which start with '-(number)' in the version
259      * specification).
260      */
261     private static class ListItem extends ArrayList<Item> implements Item {
262
263         @Override
264         public int getType() {
265             return LIST_ITEM;
266         }
267
268         @Override
269         public boolean isNull() {
270             return (size() == 0);
271         }
272
273         void normalize() {
274             for (ListIterator<Item> iterator = listIterator(size()); iterator.hasPrevious();) {
275                 Item item = iterator.previous();
276                 if (item.isNull()) {
277                     iterator.remove(); // remove null trailing items: 0, "",
278                                         // empty list
279                 } else {
280                     break;
281                 }
282             }
283         }
284
285         @Override
286         public int compareTo(Item item) {
287             if (item == null) {
288                 if (size() == 0) {
289                     return 0; // 1-0 = 1- (normalize) = 1
290                 }
291                 Item first = get(0);
292                 return first.compareTo(null);
293             }
294             switch (item.getType()) {
295             case INTEGER_ITEM:
296                 return -1; // 1-1 < 1.0.x
297
298             case STRING_ITEM:
299                 return 1; // 1-1 > 1-sp
300
301             case LIST_ITEM:
302                 Iterator<Item> left = iterator();
303                 Iterator<Item> right = ((ListItem) item).iterator();
304
305                 while (left.hasNext() || right.hasNext()) {
306                     Item l = left.hasNext() ? left.next() : null;
307                     Item r = right.hasNext() ? right.next() : null;
308
309                     int result = 0;
310                     if (r != null && l != null) {
311                         result = l.compareTo(r);
312                     } else if (r == null && l == null) {
313                         result = 0;
314                     } else if (l == null) {
315                         result = -1;
316                     } else {
317                         result = 1;
318                     }
319
320                     // if this is shorter, then invert the compare and mul with
321                     // -1
322                     // int result = (l == null ? (r == null ? 0 : -1 *
323                     // r.compareTo(l)) : l.compareTo(r));
324
325                     if (result != 0) {
326                         return result;
327                     }
328                 }
329
330                 return 0;
331
332             default:
333                 throw new RuntimeException("invalid item: " + item.getClass());
334             }
335         }
336
337         @Override
338         public String toString() {
339             StringBuilder buffer = new StringBuilder("(");
340             for (Iterator<Item> iter = iterator(); iter.hasNext();) {
341                 buffer.append(iter.next());
342                 if (iter.hasNext()) {
343                     buffer.append(',');
344                 }
345             }
346             buffer.append(')');
347             return buffer.toString();
348         }
349     }
350
351     public ComparableVersion(String version) {
352         parseVersion(version);
353     }
354
355     public final void parseVersion(String version) {
356         this.value = version;
357
358         items = new ListItem();
359
360         version = version.toLowerCase(Locale.ENGLISH);
361
362         ListItem list = items;
363
364         Stack<Item> stack = new Stack<>();
365         stack.push(list);
366
367         boolean isDigit = false;
368
369         int startIndex = 0;
370
371         for (int i = 0; i < version.length(); i++) {
372             char c = version.charAt(i);
373
374             if (c == '.') {
375                 if (i == startIndex) {
376                     list.add(IntegerItem.ZERO);
377                 } else {
378                     list.add(parseItem(isDigit, version.substring(startIndex, i)));
379                 }
380                 startIndex = i + 1;
381             } else if (c == '-') {
382                 if (i == startIndex) {
383                     list.add(IntegerItem.ZERO);
384                 } else {
385                     list.add(parseItem(isDigit, version.substring(startIndex, i)));
386                 }
387                 startIndex = i + 1;
388
389                 if (isDigit) {
390                     list.normalize(); // 1.0-* = 1-*
391
392                     if ((i + 1 < version.length()) && Character.isDigit(version.charAt(i + 1))) {
393                         // new ListItem only if previous were digits and new
394                         // char is a digit,
395                         // ie need to differentiate only 1.1 from 1-1
396                         list.add(list = new ListItem());
397
398                         stack.push(list);
399                     }
400                 }
401             } else if (Character.isDigit(c)) {
402                 if (!isDigit && i > startIndex) {
403                     list.add(new StringItem(version.substring(startIndex, i), true));
404                     startIndex = i;
405                 }
406
407                 isDigit = true;
408             } else {
409                 if (isDigit && i > startIndex) {
410                     list.add(parseItem(true, version.substring(startIndex, i)));
411                     startIndex = i;
412                 }
413
414                 isDigit = false;
415             }
416         }
417
418         if (version.length() > startIndex) {
419             list.add(parseItem(isDigit, version.substring(startIndex)));
420         }
421
422         while (!stack.isEmpty()) {
423             list = (ListItem) stack.pop();
424             list.normalize();
425         }
426
427         canonical = items.toString();
428     }
429
430     private static Item parseItem(boolean isDigit, String buf) {
431         return isDigit ? new IntegerItem(buf) : new StringItem(buf, false);
432     }
433
434     @Override
435     public int compareTo(ComparableVersion o) {
436         return items.compareTo(o.items);
437     }
438
439     @Override
440     public String toString() {
441         return value;
442     }
443
444     @Override
445     public boolean equals(Object o) {
446         return (o instanceof ComparableVersion) && canonical.equals(((ComparableVersion) o).canonical);
447     }
448
449     @Override
450     public int hashCode() {
451         return canonical.hashCode();
452     }
453 }