com.github.jknack.semver.SemverParser.java Source code

Java tutorial

Introduction

Here is the source code for com.github.jknack.semver.SemverParser.java

Source

/**
 * Copyright (c) 2013 Edgar Espina
 *
 * This file is part of amd4j (https://github.com/jknack/amd4j)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.jknack.semver;

import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.commons.lang3.Validate.notNull;

import org.apache.commons.lang3.StringUtils;

public class SemverParser {

    private static abstract class Matcher {
        public abstract boolean match(char ch);
    }

    private static final Matcher PRE_RELEASE_OR_BUILD_NUMBER = new Matcher() {
        @Override
        public boolean match(final char ch) {
            return firstOf(eq('-'), eq('.'), range('0', '9'), range('a', 'z'), range('A', 'Z')).match(ch);
        }
    };

    /**
     * The End-of-Input non-character.
     */
    public static final char EOF = '\uFFFF';

    private String input;

    private int position;

    public SemverParser(final String input) {
        this.input = notNull(input, "The input is required.").trim();
    }

    public Semver parse() {
        if (input.length() == 0) {
            return Semver.ANY;
        }
        return expression();
    }

    private PrefixOperator operator() {
        if (match('=')) {
            return RelationalOp.eq();
        } else if (match("<=")) {
            return RelationalOp.ltEq();
        } else if (match(">=")) {
            return RelationalOp.gtEq();
        } else if (match("<")) {
            return RelationalOp.lt();
        } else if (match(">")) {
            return RelationalOp.gt();
        } else if (match('~')) {
            return Range.tilde();
        }
        return null;
    }

    private Semver expression() {
        // any version
        if (match('*')) {
            return Semver.ANY;
        }
        // uri version
        String[] protocols = { "https", "http", "git+shh", "git+https", "git+http", "git" };
        for (String protocol : protocols) {
            String prefix = protocol + "://";
            if (match(prefix)) {
                return new UrlExpression(prefix + input.substring(position));
            }
        }
        Semver result = rangeOrVersion();
        // check for '||' expression
        while (!eof()) {
            // drop whites
            ws();
            if (match("||")) {
                Semver right = rangeOrVersion();
                result = new OrExpression(result, right);
            }
        }
        // normal version
        return result;
    }

    private Semver rangeOrVersion() {
        // drop whites
        ws();
        Semver result = version();
        String ws = ws();
        if (!extract(2).equals("||") && (match('-') || ws.length() > 0)) {
            ws();
            Semver right = version();
            if (result instanceof Version && right instanceof Version) {
                result = Range.range((Version) result, (Version) right);
            } else {
                result = new AndExpression(result, right);
            }
        }
        // drop whites
        ws();
        return result;
    }

    private boolean eof() {
        return ch() == EOF;
    }

    private Semver version() {
        // operator matching
        final PrefixOperator operator = operator();

        // drop whites
        ws();

        int start = position;
        // drop 'v'
        match('v');

        // major
        int major = versionNumber("0-9+, =, <, <=, >, >= or ~", false);

        // minor
        int minor = 0;
        if (match('.')) {
            minor = versionNumber("0-9+ or x", true);
        }

        // patch
        int patch = 0;
        String[] prerelease = {};
        String[] buildNumber = {};
        if (match('.')) {
            patch = versionNumber("0-9+ or x", true);
            // pre-release number
            prerelease = preReleaseOrBuildNumber('-');
            // build number
            buildNumber = preReleaseOrBuildNumber('+');
        }
        int end = position;
        String text = input.substring(start, end).trim();
        Version version = new Version(text, major, minor, patch);
        version.setPreRelease(prerelease);
        version.setBuildNumber(buildNumber);
        Semver result = text.contains("x") ? Range.x(version) : version;
        return operator == null ? result : operator.setExpression(result);
    }

    private String[] preReleaseOrBuildNumber(final char separator) {
        match(separator);
        String preRelease = match(PRE_RELEASE_OR_BUILD_NUMBER);
        return isEmpty(preRelease) ? new String[0] : StringUtils.split(preRelease, ".");
    }

    private int versionNumber(final String label, final boolean allowX) {
        String digits = digits(label);
        if (!allowX && digits.equalsIgnoreCase("x")) {
            throw error(label, digits.length());
        }
        return toNumber(digits);
    }

    private int toNumber(final String digits) {
        return digits.equalsIgnoreCase("x") ? 0 : Integer.parseInt(digits);
    }

    private RuntimeException error(final String expected) {
        return error(expected, 0);
    }

    private RuntimeException error(final String expected, final int offset) {
        char ch = ch(position - offset);
        return new IllegalArgumentException(
                "found: '" + (ch == EOF ? "eof" : ch) + "', expected: '" + expected + "'");
    }

    private String digits(final String expected) {
        String match = match(new Matcher() {
            @Override
            public boolean match(final char ch) {
                return Character.isDigit(ch) || ch == 'x' || ch == 'X';
            }
        });

        if (isEmpty(match)) {
            throw error(expected);
        }
        return match;
    }

    private String match(final Matcher matcher) {
        char ch = ch();
        int start = position;
        while (ch != EOF && matcher.match(ch)) {
            consume();
            ch = ch();
        }
        int end = position;
        if (start < end) {
            return input.substring(start, end);
        }
        return "";
    }

    private boolean match(final char ch) {
        char la = ch();
        if (la == ch) {
            consume();
            return true;
        }
        return false;
    }

    private String extract(final int len) {
        return input.substring(position, Math.min(len + position, input.length()));
    }

    private boolean match(final String token) {
        String chunk = extract(token.length());
        if (token.equals(chunk)) {
            consume(token.length());
            return true;
        }
        return false;
    }

    private String ws() {
        return match(new Matcher() {
            @Override
            public boolean match(final char ch) {
                return Character.isWhitespace(ch);
            }
        });
    }

    private char ch() {
        return ch(position);
    }

    private char ch(final int position) {
        if (position < 0 || position >= input.length()) {
            return EOF;
        }
        return input.charAt(position);
    }

    private void consume() {
        consume(1);
    }

    private void consume(final int count) {
        for (int i = 0; i < count; i++) {
            position++;
        }
    }

    private static Matcher range(final char lower, final char upper) {
        return new Matcher() {
            @Override
            public boolean match(final char ch) {
                return ch >= lower && ch <= upper;
            }
        };
    }

    private static Matcher eq(final char ch) {
        return new Matcher() {
            @Override
            public boolean match(final char candidate) {
                return ch == candidate;
            }
        };
    }

    private static Matcher firstOf(final Matcher... matchers) {
        return new Matcher() {
            @Override
            public boolean match(final char ch) {
                for (Matcher matcher : matchers) {
                    if (matcher.match(ch)) {
                        return true;
                    }
                }
                return false;
            }
        };
    }
}