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