001/*
002 * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the
010 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
011 * express or implied. See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014
015package net.openid.appauth.browser;
016
017import androidx.annotation.NonNull;
018
019/**
020 * Represents a delimited version number for an application. This can parse common version number
021 * formats, treating any sequence of non-numeric characters as a delimiter, and discards these
022 * to retain just the numeric content for comparison. Trailing zeroes in a version number
023 * are discarded to produce a compact, canonical representation. Empty versions are equivalent to
024 * "0". Each numeric part is expected to fit within a 64-bit integer.
025 */
026public class DelimitedVersion implements Comparable<DelimitedVersion> {
027
028    // See: http://stackoverflow.com/a/2816747
029    private static final int PRIME_HASH_FACTOR = 92821;
030
031    private static final long BIT_MASK_32 = 0xFFFFFFFF;
032
033    private final long[] mNumericParts;
034
035    /**
036     * Creates a version with the specified parts, ordered from major to minor.
037     */
038    public DelimitedVersion(long[] numericParts) {
039        mNumericParts = numericParts;
040    }
041
042    @Override
043    public String toString() {
044        if (mNumericParts.length == 0) {
045            return "0";
046        }
047
048        StringBuilder builder = new StringBuilder();
049        builder.append(mNumericParts[0]);
050
051        int index = 1;
052        while (index < mNumericParts.length) {
053            builder.append('.');
054            builder.append(mNumericParts[index]);
055            index++;
056        }
057
058        return builder.toString();
059    }
060
061    @Override
062    public boolean equals(Object obj) {
063        if (this == obj) {
064            return true;
065        }
066
067        if (obj == null || !(obj instanceof DelimitedVersion)) {
068            return false;
069        }
070
071        return this.compareTo((DelimitedVersion) obj) == 0;
072    }
073
074    @Override
075    public int hashCode() {
076        int result = 0;
077
078        for (long numericPart : mNumericParts) {
079            result = result * PRIME_HASH_FACTOR + (int)(numericPart & BIT_MASK_32);
080        }
081
082        return result;
083    }
084
085    @Override
086    public int compareTo(@NonNull DelimitedVersion other) {
087        int index = 0;
088
089        while (index < this.mNumericParts.length && index < other.mNumericParts.length) {
090            int currentPartOrder =
091                    compareLongs(this.mNumericParts[index], other.mNumericParts[index]);
092            if (currentPartOrder != 0) {
093                return currentPartOrder;
094            }
095            index++;
096        }
097
098        return compareLongs(this.mNumericParts.length, other.mNumericParts.length);
099    }
100
101    private int compareLongs(long l1, long l2) {
102        if (l1 < l2) {
103            return -1;
104        } else if (l1 > l2) {
105            return 1;
106        }
107        return 0;
108    }
109
110    /**
111     * Parses a delimited version number from the provided string.
112     */
113    public static DelimitedVersion parse(String versionString) {
114
115        if (versionString == null) {
116            return new DelimitedVersion(new long[0]);
117        }
118
119        String[] stringParts = versionString.split("[^0-9]+");
120
121        long[] parsedParts = new long[stringParts.length];
122        int index = 0;
123        for (String numericPart : stringParts) {
124            if (numericPart.isEmpty()) {
125                continue;
126            }
127
128            parsedParts[index] = Long.parseLong(numericPart);
129            index++;
130        }
131
132        // discard all trailing zeroes
133        index--;
134        while (index >= 0) {
135            if (parsedParts[index] > 0) {
136                break;
137            }
138            index--;
139        }
140
141        int length = index + 1;
142        long[] onlyParsedParts = new long[length];
143        System.arraycopy(parsedParts, 0, onlyParsedParts, 0, length);
144        return new DelimitedVersion(onlyParsedParts);
145    }
146}