blob: dd6ff61918fb8d7078f864358df045c6a12babdc [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2017 Cloudsmith Inc. and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Cloudsmith Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.equinox.internal.p2.metadata;
import java.io.Serializable;
import java.util.*;
import org.eclipse.equinox.internal.p2.metadata.EnumDefinition.EnumSegment;
import org.eclipse.equinox.internal.p2.metadata.VersionFormat.TreeInfo;
import org.eclipse.equinox.p2.metadata.VersionFormatException;
import org.eclipse.osgi.util.NLS;
/**
* This is the Omni Version Format parser. It will parse a version format in string form
* into a group of {@link VersionFormatParser.Fragment} elements. That group, wrapped in a
* {@link VersionFormat}, becomes the parser for versions corresponding to the format.
*
* The class is not intended to included in a public API. Instead VersionFormats should
* be created using {@link VersionFormat#parse(String)}
*
*/
class VersionFormatParser {
private static class EnumInstruction {
private final EnumDefinition definition;
private final boolean caseSensitive;
private final boolean optional;
private final boolean begins;
EnumInstruction(EnumDefinition definition, boolean caseSensitive, boolean optional, boolean begins) {
this.definition = definition;
this.caseSensitive = caseSensitive;
this.optional = optional;
this.begins = begins;
}
EnumSegment getEnumSegment(RangeFragment fragment, String version, int[] posHolder, int maxPos) {
int pos = posHolder[0];
int len = maxPos - pos;
int minLen = definition.getShortestLength();
if (minLen > len)
return null;
int maxLen = definition.getLongestLength();
if (maxLen < len)
len = maxLen;
++len;
while (--len >= minLen) {
int last = pos + len;
if (!begins && last < maxPos) {
char c = version.charAt(last);
if (VersionParser.isLetter(c) && fragment.isAllowed(c)) {
// We are not allowed to truncate at this point
continue;
}
}
String identifier = version.substring(pos, last);
if (!caseSensitive)
identifier = identifier.toLowerCase();
int ordinal = definition.getOrdinal(identifier);
if (ordinal >= 0) {
posHolder[0] = pos + len;
return definition.getSegment(ordinal);
}
}
return null;
}
void toString(StringBuffer bld) {
bld.append('=');
definition.toString(bld);
if (begins)
bld.append('b');
if (!caseSensitive)
bld.append('i');
if (optional)
bld.append('?');
bld.append(';');
}
public boolean isOptional() {
return optional;
}
}
static class Instructions {
char[] characters = null;
Comparable<?> defaultValue = null;
char oppositeTranslationChar = 0;
int oppositeTranslationRepeat = 0;
boolean ignore = false;
boolean inverted = false;
Comparable<?> padValue = null;
int rangeMax = Integer.MAX_VALUE;
int rangeMin = 0;
EnumInstruction enumInstruction = null;
}
static final Qualifier EXACT_ONE_QUALIFIER = new Qualifier(1, 1);
static final Qualifier ONE_OR_MANY_QUALIFIER = new Qualifier(1, Integer.MAX_VALUE);
static final Qualifier ZERO_OR_MANY_QUALIFIER = new Qualifier(0, Integer.MAX_VALUE);
static final Qualifier ZERO_OR_ONE_QUALIFIER = new Qualifier(0, 1);
/**
* Represents one fragment of a format (i.e. auto, number, string, delimiter, etc.)
*/
static abstract class Fragment implements Serializable {
private static final long serialVersionUID = 4109185333058622681L;
private final Qualifier qualifier;
Fragment(Qualifier qualifier) {
this.qualifier = qualifier;
}
@Override
public final boolean equals(Object f) {
return f == this || getClass().equals(f.getClass()) && qualifier.equals(((Fragment) f).qualifier);
}
@Override
public final int hashCode() {
return 11 * qualifier.hashCode();
}
public boolean isGroup() {
return false;
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
toString(sb);
return sb.toString();
}
Comparable<?> getDefaultValue() {
return null;
}
Fragment getFirstLeaf() {
return this;
}
Comparable<?> getPadValue() {
return null;
}
Qualifier getQualifier() {
return qualifier;
}
boolean parse(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
return qualifier.parse(new Fragment[] {this}, 0, segments, version, maxPos, info);
}
abstract boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info);
void setDefaults(List<Comparable<?>> segments) {
// No-op at this level
}
void toString(StringBuffer sb) {
if (!(qualifier == VersionFormatParser.EXACT_ONE_QUALIFIER || (qualifier == VersionFormatParser.ZERO_OR_ONE_QUALIFIER && this.isGroup())))
qualifier.toString(sb);
}
}
/**
* Specifies the min and max occurrences of a fragment
*/
static class Qualifier implements Serializable {
private static final long serialVersionUID = 7494021832824671685L;
private final int max;
private final int min;
Qualifier(int min, int max) {
this.min = min;
this.max = max;
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Qualifier))
return false;
Qualifier oq = (Qualifier) o;
return min == oq.min && max == oq.max;
}
@Override
public int hashCode() {
return 31 * min + 67 * max;
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
toString(sb);
return sb.toString();
}
int getMax() {
return max;
}
int getMin() {
return min;
}
boolean parse(Fragment[] fragments, int fragIdx, List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
Fragment fragment = fragments[fragIdx++];
int idx = 0;
// Do the required parsing. I.e. iterate this fragment
// min number of times.
//
for (; idx < min; ++idx)
if (!fragment.parseOne(segments, version, maxPos, info))
return false;
for (; idx < max; ++idx) {
// We are greedy. Continue parsing until we get an exception
// and remember the state before each parse is performed.
//
info.pushState(segments.size(), fragment);
if (!fragment.parseOne(segments, version, maxPos, info)) {
info.popState(segments, fragment);
break;
}
}
int maxParsed = idx;
for (;;) {
// Pad with default values unless the max is unbounded
//
if (idx < max) {
if (max != Integer.MAX_VALUE) {
for (; idx < max; ++idx)
fragment.setDefaults(segments);
}
} else {
if (fragment instanceof StringFragment) {
// Check for translations if we default to for MINS or MAXS
StringFragment stringFrag = (StringFragment) fragment;
Comparable<?> opposite = stringFrag.getOppositeDefaultValue();
if (opposite != null) {
idx = segments.size() - 1;
if (stringFrag.isOppositeTranslation(segments.get(idx)))
segments.set(idx, opposite);
}
}
}
if (fragIdx == fragments.length)
// We are the last segment
//
return true;
// Try to parse the next segment. If it fails, pop the state of
// this segment (or a child thereof) and try again
//
if (fragments[fragIdx].getQualifier().parse(fragments, fragIdx, segments, version, maxPos, info))
return true;
// Be less greedy, step back one position and try again.
//
if (maxParsed <= min)
// We have no more states to pop. Tell previous that we failed.
//
return false;
info.popState(segments, fragment);
idx = --maxParsed; // segments now have room for one more default value
}
}
void toString(StringBuffer sb) {
if (min == 0) {
switch (max) {
case 1:
sb.append('?');
break;
case Integer.MAX_VALUE:
sb.append('*');
break;
default:
sb.append('{');
sb.append(min);
sb.append(',');
sb.append(max);
sb.append('}');
break;
}
} else if (max == Integer.MAX_VALUE) {
if (min == 1)
sb.append('+');
else {
sb.append('{');
sb.append(min);
sb.append(",}"); //$NON-NLS-1$
}
} else {
sb.append('{');
sb.append(min);
if (min != max) {
sb.append(',');
sb.append(max);
}
sb.append('}');
}
}
// Preserve singleton when deserialized
private Object readResolve() {
Qualifier q = this;
if (min == 0) {
if (max == 1)
q = VersionFormatParser.ZERO_OR_ONE_QUALIFIER;
else if (max == Integer.MAX_VALUE)
q = VersionFormatParser.ZERO_OR_MANY_QUALIFIER;
} else if (min == 1) {
if (max == 1)
q = VersionFormatParser.EXACT_ONE_QUALIFIER;
else if (max == Integer.MAX_VALUE)
q = VersionFormatParser.ONE_OR_MANY_QUALIFIER;
}
return q;
}
}
private static class AutoFragment extends RangeFragment {
private static final long serialVersionUID = -1016534328164247755L;
AutoFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
super(instr, qualifier);
}
@Override
boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
int pos = info.getPosition();
maxPos = checkRange(pos, maxPos);
if (maxPos < 0)
return false;
char c = version.charAt(pos);
if (VersionParser.isDigit(c) && isAllowed(c) && (enumInstruction == null || enumInstruction.isOptional())) {
// Parse to next non-digit
//
int start = pos;
int value = c - '0';
while (++pos < maxPos) {
c = version.charAt(pos);
if (!(VersionParser.isDigit(c) && isAllowed(c)))
break;
value *= 10;
value += (c - '0');
}
int len = pos - start;
if (rangeMin > len || len > rangeMax)
return false;
if (!isIgnored())
segments.add(VersionParser.valueOf(value));
info.setPosition(pos);
return true;
}
int start = pos;
if (enumInstruction != null) {
int[] posHolder = new int[] {pos};
EnumSegment es = enumInstruction.getEnumSegment(this, version, posHolder, maxPos);
if (es != null) {
pos = posHolder[0];
int len = pos - start;
if (rangeMin > len || len > rangeMax)
return false;
if (!isIgnored())
segments.add(es);
info.setPosition(pos);
return true;
}
if (!enumInstruction.isOptional())
return false;
}
if (!(VersionParser.isLetter(c) && isAllowed(c)))
return false;
// Parse to next non-letter or next delimiter
//
for (++pos; pos < maxPos; ++pos) {
c = version.charAt(pos);
if (!(VersionParser.isLetter(c) && isAllowed(c)))
break;
}
int len = pos - start;
if (rangeMin > len || len > rangeMax)
return false;
if (!isIgnored())
segments.add(version.substring(start, pos));
info.setPosition(pos);
return true;
}
@Override
void toString(StringBuffer sb) {
sb.append('a');
super.toString(sb);
}
}
private static class DelimiterFragment extends Fragment {
private static final long serialVersionUID = 8173654376143370605L;
private final char[] delimChars;
private final boolean inverted;
DelimiterFragment(VersionFormatParser.Instructions ep, Qualifier qualifier) {
super(qualifier);
if (ep == null) {
delimChars = null;
inverted = false;
} else {
inverted = ep.inverted;
delimChars = ep.characters;
}
}
boolean isMatch(String version, int pos) {
char c = version.charAt(pos);
if (delimChars != null) {
for (char delimChar : delimChars)
if (c == delimChar)
return !inverted;
return inverted;
} else if (VersionParser.isLetterOrDigit(c))
return false;
return true;
}
@Override
boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
int pos = info.getPosition();
if (pos < maxPos && isMatch(version, pos)) {
// Just swallow, a delimiter does not contribute to the vector.
//
info.setPosition(pos + 1);
return true;
}
return false;
}
@Override
void toString(StringBuffer sb) {
sb.append('d');
if (delimChars != null)
appendCharacterRange(sb, delimChars, inverted);
super.toString(sb);
}
}
static void appendCharacterRange(StringBuffer sb, char[] range, boolean inverted) {
sb.append('=');
sb.append('[');
if (inverted)
sb.append('^');
int top = range.length;
for (int idx = 0; idx < top; ++idx) {
char b = range[idx];
if (b == '\\' || b == ']' || (b == '-' && idx + 1 < top))
sb.append('\\');
sb.append(b);
int ndx = idx + 1;
if (ndx + 2 < top) {
char c = b;
for (; ndx < top; ++ndx) {
char n = range[ndx];
if (c + 1 != n)
break;
c = n;
}
if (ndx <= idx + 3)
continue;
sb.append('-');
if (c == '\\' || c == ']' || (c == '-' && idx + 1 < top))
sb.append('\\');
sb.append(c);
idx = ndx - 1;
}
}
sb.append(']');
sb.append(';');
}
static Fragment createAutoFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
return new AutoFragment(instr, qualifier);
}
static Fragment createDelimiterFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
return new DelimiterFragment(instr, qualifier);
}
static Fragment createGroupFragment(VersionFormatParser.Instructions instr, Qualifier qualifier, Fragment[] fragments, boolean array) {
return new GroupFragment(instr, qualifier, fragments, array);
}
static Fragment createLiteralFragment(Qualifier qualifier, String literal) {
return new LiteralFragment(qualifier, literal);
}
static Fragment createNumberFragment(VersionFormatParser.Instructions instr, Qualifier qualifier, boolean signed) {
return new NumberFragment(instr, qualifier, signed);
}
static Fragment createPadFragment(Qualifier qualifier) {
return new PadFragment(qualifier);
}
static Fragment createQuotedFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
return new QuotedFragment(instr, qualifier);
}
static Fragment createRawFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
return new RawFragment(instr, qualifier);
}
static Fragment createStringFragment(VersionFormatParser.Instructions instr, Qualifier qualifier, boolean unbound) {
return new StringFragment(instr, qualifier, unbound);
}
static boolean equalsAllowNull(Object a, Object b) {
return (a == null) ? (b == null) : (b != null && a.equals(b));
}
private static abstract class ElementFragment extends Fragment {
private static final long serialVersionUID = -6834591415456539713L;
private final Comparable<?> defaultValue;
private final boolean ignored;
private final Comparable<?> padValue;
ElementFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
super(qualifier);
if (instr != null) {
ignored = instr.ignore;
defaultValue = instr.defaultValue;
padValue = instr.padValue;
} else {
ignored = false;
defaultValue = null;
padValue = null;
}
}
@Override
Comparable<?> getDefaultValue() {
return defaultValue;
}
@Override
Comparable<?> getPadValue() {
return padValue;
}
boolean isIgnored() {
return ignored;
}
@Override
void setDefaults(List<Comparable<?>> segments) {
Comparable<?> defaultVal = getDefaultValue();
if (defaultVal != null)
segments.add(defaultVal);
}
@Override
void toString(StringBuffer sb) {
if (ignored) {
sb.append('=');
sb.append('!');
sb.append(';');
}
if (defaultValue != null) {
sb.append('=');
VersionFormat.rawToString(sb, false, defaultValue);
sb.append(';');
}
if (padValue != null) {
sb.append('=');
sb.append('p');
VersionFormat.rawToString(sb, false, padValue);
sb.append(';');
}
super.toString(sb);
}
}
private static class GroupFragment extends ElementFragment {
private static final long serialVersionUID = 9219978678087669699L;
private final boolean array;
private final Fragment[] fragments;
GroupFragment(VersionFormatParser.Instructions instr, Qualifier qualifier, Fragment[] fragments, boolean array) {
super(instr, qualifier);
this.fragments = fragments;
this.array = array;
}
@Override
public boolean isGroup() {
return !array;
}
@Override
Fragment getFirstLeaf() {
return fragments[0].getFirstLeaf();
}
@Override
boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
if (array) {
ArrayList<Comparable<?>> subSegs = new ArrayList<>();
boolean success = fragments[0].getQualifier().parse(fragments, 0, subSegs, version, maxPos, info);
if (!success || subSegs.isEmpty())
return false;
Comparable<?> padValue = info.getPadValue();
if (padValue != null)
info.setPadValue(null); // Prevent outer group from getting this.
else
padValue = getPadValue();
padValue = VersionParser.removeRedundantTrail(segments, padValue);
segments.add(new VersionVector(subSegs.toArray(new Comparable[subSegs.size()]), padValue));
return true;
}
if (fragments[0].getQualifier().parse(fragments, 0, segments, version, maxPos, info)) {
Comparable<?> padValue = getPadValue();
if (padValue != null)
info.setPadValue(padValue);
return true;
}
return false;
}
@Override
void setDefaults(List<Comparable<?>> segments) {
Comparable<?> dflt = getDefaultValue();
if (dflt != null) {
// A group default overrides any defaults within the
// group fragments
super.setDefaults(segments);
} else {
// Assign defaults for all fragments
for (Fragment fragment : fragments)
fragment.setDefaults(segments);
}
}
@Override
void toString(StringBuffer sb) {
if (array) {
sb.append('<');
for (Fragment fragment : fragments)
fragment.toString(sb);
sb.append('>');
} else {
if (getQualifier() == VersionFormatParser.ZERO_OR_ONE_QUALIFIER) {
sb.append('[');
for (Fragment fragment : fragments)
fragment.toString(sb);
sb.append(']');
} else {
sb.append('(');
for (Fragment fragment : fragments)
fragment.toString(sb);
sb.append(')');
}
}
super.toString(sb);
}
}
private static class LiteralFragment extends Fragment {
private static final long serialVersionUID = 6210696245839471802L;
private final String string;
LiteralFragment(Qualifier qualifier, String string) {
super(qualifier);
this.string = string;
}
@Override
boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
int pos = info.getPosition();
int litLen = string.length();
if (pos + litLen > maxPos)
return false;
for (int idx = 0; idx < litLen; ++idx, ++pos) {
if (string.charAt(idx) != version.charAt(pos))
return false;
}
info.setPosition(pos);
return true;
}
@Override
void toString(StringBuffer sb) {
String str = string;
if (str.length() != 1) {
sb.append('\'');
VersionFormatParser.toStringEscaped(sb, str, "\'"); //$NON-NLS-1$
sb.append('\'');
} else {
char c = str.charAt(0);
switch (c) {
case '\'' :
case '\\' :
case '<' :
case '[' :
case '(' :
case '{' :
case '?' :
case '*' :
case '+' :
case '=' :
sb.append('\\');
sb.append(c);
break;
default :
if (VersionParser.isLetterOrDigit(c)) {
sb.append('\\');
sb.append(c);
} else
sb.append(c);
}
}
super.toString(sb);
}
}
private static class NumberFragment extends RangeFragment {
private static final long serialVersionUID = -8552754381106711507L;
private final boolean signed;
NumberFragment(VersionFormatParser.Instructions instr, Qualifier qualifier, boolean signed) {
super(instr, qualifier);
this.signed = signed;
}
@Override
boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
int pos = info.getPosition();
maxPos = checkRange(pos, maxPos);
if (maxPos < 0)
return false;
// Parse to next non-digit
//
int start = pos;
int value;
char c = version.charAt(pos);
if (signed || characters != null) {
boolean negate = false;
if (signed && c == '-' && pos + 1 < maxPos) {
negate = true;
c = version.charAt(++pos);
}
if (!(c >= '0' && c <= '9' && isAllowed(c)))
return false;
// Parse to next non-digit
//
value = c - '0';
while (++pos < maxPos) {
c = version.charAt(pos);
if (!(c >= '0' && c <= '9' && isAllowed(c)))
break;
value *= 10;
value += (c - '0');
}
if (negate)
value = -value;
} else {
if (c < '0' || c > '9')
return false;
// Parse to next non-digit
//
value = c - '0';
while (++pos < maxPos) {
c = version.charAt(pos);
if (c < '0' || c > '9')
break;
value *= 10;
value += (c - '0');
}
}
int len = pos - start;
if (rangeMin > len || len > rangeMax)
return false;
if (!isIgnored())
segments.add(VersionParser.valueOf(value));
info.setPosition(pos);
return true;
}
@Override
void toString(StringBuffer sb) {
sb.append(signed ? 'N' : 'n');
super.toString(sb);
}
}
private static class PadFragment extends ElementFragment {
private static final long serialVersionUID = 5052010199974380170L;
PadFragment(Qualifier qualifier) {
super(null, qualifier);
}
@Override
boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
int pos = info.getPosition();
if (pos >= maxPos || version.charAt(pos) != 'p')
return false;
int[] position = new int[] {++pos};
Comparable<?> v = VersionParser.parseRawElement(version, position, maxPos);
if (v == null)
return false;
if (!isIgnored())
info.setPadValue(v);
info.setPosition(position[0]);
return true;
}
@Override
void toString(StringBuffer sb) {
sb.append('p');
super.toString(sb);
}
}
private static class QuotedFragment extends RangeFragment {
private static final long serialVersionUID = 6057751133533608969L;
QuotedFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
super(instr, qualifier);
}
@Override
boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
int pos = info.getPosition();
if (pos >= maxPos)
return false;
char endQuote;
char quote = version.charAt(pos);
switch (quote) {
case '<' :
endQuote = '>';
break;
case '{' :
endQuote = '}';
break;
case '(' :
endQuote = ')';
break;
case '[' :
endQuote = ']';
break;
case '>' :
endQuote = '<';
break;
case '}' :
endQuote = '{';
break;
case ')' :
endQuote = '(';
break;
case ']' :
endQuote = '[';
break;
default :
if (VersionParser.isLetterOrDigit(quote))
return false;
endQuote = quote;
}
int start = ++pos;
char c = version.charAt(pos);
while (c != endQuote && isAllowed(c) && ++pos < maxPos)
c = version.charAt(pos);
if (c != endQuote || rangeMin > pos - start)
// End quote not found
return false;
int len = pos - start;
if (rangeMin > len || len > rangeMax)
return false;
if (!isIgnored())
segments.add(version.substring(start, pos));
info.setPosition(++pos); // Skip quote
return true;
}
@Override
void toString(StringBuffer sb) {
sb.append('q');
super.toString(sb);
}
}
private static abstract class RangeFragment extends ElementFragment {
private static final long serialVersionUID = -6680402803630334708L;
final char[] characters;
final boolean inverted;
final int rangeMax;
final int rangeMin;
final EnumInstruction enumInstruction;
RangeFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
super(instr, qualifier);
if (instr == null) {
characters = null;
inverted = false;
rangeMin = 0;
rangeMax = Integer.MAX_VALUE;
enumInstruction = null;
} else {
characters = instr.characters;
inverted = instr.inverted;
rangeMin = instr.rangeMin;
rangeMax = instr.rangeMax;
enumInstruction = instr.enumInstruction;
}
}
/**
* Checks that pos is at a valid character position, that we
* have at least the required minimum characters left, and
* if a maximum number of characters is set, limits the
* returned value to a maxPos that reflects that maximum.
* @param pos the current position
* @param maxPos the current maxPos
* @return maxPos, possibly limited by rangeMax
*/
int checkRange(int pos, int maxPos) {
int check = pos;
if (rangeMin == 0)
check++; // Verify one character
else
check += rangeMin;
if (check > maxPos)
// Less then min characters left
maxPos = -1;
else {
if (rangeMax != Integer.MAX_VALUE) {
check = pos + rangeMax;
if (check < maxPos)
maxPos = check;
}
}
return maxPos;
}
boolean isAllowed(char c) {
char[] crs = characters;
if (crs != null) {
int idx = crs.length;
while (--idx >= 0)
if (c == crs[idx])
return !inverted;
return inverted;
}
return true;
}
@Override
void toString(StringBuffer sb) {
if (characters != null)
appendCharacterRange(sb, characters, inverted);
if (rangeMin != 0 || rangeMax != Integer.MAX_VALUE) {
sb.append('=');
sb.append('{');
sb.append(rangeMin);
if (rangeMin != rangeMax) {
sb.append(',');
if (rangeMax != Integer.MAX_VALUE)
sb.append(rangeMax);
}
sb.append('}');
sb.append(';');
}
if (enumInstruction != null)
enumInstruction.toString(sb);
super.toString(sb);
}
}
private static class RawFragment extends ElementFragment {
private static final long serialVersionUID = 4107448125256042602L;
RawFragment(VersionFormatParser.Instructions processing, Qualifier qualifier) {
super(processing, qualifier);
}
@Override
boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
int[] position = new int[] {info.getPosition()};
Comparable<?> v = VersionParser.parseRawElement(version, position, maxPos);
if (v == null)
return false;
if (!isIgnored())
segments.add(v);
info.setPosition(position[0]);
return true;
}
@Override
void toString(StringBuffer sb) {
sb.append('r');
super.toString(sb);
}
}
private static class StringFragment extends RangeFragment {
private static final long serialVersionUID = -2265924553606430164L;
final boolean anyChar;
private final char oppositeTranslationChar;
private final int oppositeTranslationRepeat;
StringFragment(VersionFormatParser.Instructions instr, Qualifier qualifier, boolean noLimit) {
super(instr, qualifier);
anyChar = noLimit;
char otc = 0;
int otr = 0;
if (instr != null) {
otc = instr.oppositeTranslationChar;
otr = instr.oppositeTranslationRepeat;
if (instr.defaultValue == VersionVector.MINS_VALUE) {
if (otc == 0)
otc = 'z';
if (otr == 0)
otr = 3;
} else if (instr.defaultValue == VersionVector.MAXS_VALUE) {
if (otc == 0)
otc = '-';
otr = 1;
}
}
oppositeTranslationChar = otc;
oppositeTranslationRepeat = otr;
}
Comparable<?> getOppositeDefaultValue() {
Comparable<?> dflt = getDefaultValue();
return dflt == VersionVector.MAXS_VALUE ? VersionVector.MINS_VALUE : (dflt == VersionVector.MINS_VALUE ? VersionVector.MAXS_VALUE : null);
}
public boolean isOppositeTranslation(Object val) {
if (val instanceof String) {
String str = (String) val;
int idx = oppositeTranslationRepeat;
if (str.length() == idx) {
while (--idx >= 0)
if (str.charAt(idx) != oppositeTranslationChar)
break;
return idx < 0;
}
}
return false;
}
@Override
boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
int pos = info.getPosition();
maxPos = checkRange(pos, maxPos);
if (maxPos < 0)
return false;
int start = pos;
if (enumInstruction != null) {
int[] posHolder = new int[] {pos};
EnumSegment es = enumInstruction.getEnumSegment(this, version, posHolder, maxPos);
if (es != null) {
pos = posHolder[0];
int len = pos - start;
if (rangeMin > len || len > rangeMax)
return false;
if (!isIgnored())
segments.add(es);
info.setPosition(pos);
return true;
}
if (!enumInstruction.isOptional())
return false;
}
// Parse to next delimiter or end of string
//
if (characters != null) {
if (anyChar) {
// Swallow everything that matches the allowed characters
for (; pos < maxPos; ++pos) {
if (!isAllowed(version.charAt(pos)))
break;
}
} else {
// Swallow letters that matches the allowed characters
for (; pos < maxPos; ++pos) {
char c = version.charAt(pos);
if (!(VersionParser.isLetter(c) && isAllowed(c)))
break;
}
}
} else {
if (anyChar)
// Swallow all characters
pos = maxPos;
else {
// Swallow all letters
for (; pos < maxPos; ++pos) {
if (!VersionParser.isLetter(version.charAt(pos)))
break;
}
}
}
int len = pos - start;
if (len == 0 || rangeMin > len || len > rangeMax)
return false;
if (!isIgnored())
segments.add(version.substring(start, pos));
info.setPosition(pos);
return true;
}
@Override
void toString(StringBuffer sb) {
sb.append(anyChar ? 'S' : 's');
super.toString(sb);
}
}
private int current;
private List<Fragment> currentList;
private int eos;
private String format;
private int start;
Fragment compile(String fmt, int pos, int maxPos) throws VersionFormatException {
format = fmt;
if (start >= maxPos)
throw new VersionFormatException(Messages.format_is_empty);
start = pos;
current = pos;
eos = maxPos;
currentList = new ArrayList<>();
while (current < eos)
parseFragment();
Fragment topFrag;
switch (currentList.size()) {
case 0 :
throw new VersionFormatException(Messages.format_is_empty);
case 1 :
Fragment frag = currentList.get(0);
if (frag.isGroup()) {
topFrag = frag;
break;
}
// Fall through to default
default :
topFrag = createGroupFragment(null, EXACT_ONE_QUALIFIER, currentList.toArray(new Fragment[currentList.size()]), false);
}
currentList = null;
return topFrag;
}
private void assertChar(char expected) throws VersionFormatException {
if (current >= eos)
throw formatException(NLS.bind(Messages.premature_end_of_format_expected_0, new String(new char[] {expected})));
char c = format.charAt(current);
if (c != expected)
throw formatException(c, new String(new char[] {expected}));
++current;
}
private VersionFormatException formatException(char found, String expected) {
return formatException(new String(new char[] {found}), expected);
}
private VersionFormatException formatException(String message) {
return new VersionFormatException(NLS.bind(Messages.syntax_error_in_version_format_0_1_2, new Object[] {format.substring(start, eos), Integer.valueOf(current), message}));
}
private VersionFormatException formatException(String found, String expected) {
return new VersionFormatException(NLS.bind(Messages.syntax_error_in_version_format_0_1_found_2_expected_3, new Object[] {format.substring(start, eos), Integer.valueOf(current), found, expected}));
}
private VersionFormatException illegalControlCharacter(char c) {
return formatException(NLS.bind(Messages.illegal_character_encountered_ascii_0, VersionParser.valueOf(c)));
}
private String parseAndConsiderEscapeUntil(char endChar) throws VersionFormatException {
StringBuilder sb = new StringBuilder();
while (current < eos) {
char c = format.charAt(current++);
if (c == endChar)
break;
if (c < 32)
throw illegalControlCharacter(c);
if (c == '\\') {
if (current == eos)
throw formatException(Messages.EOS_after_escape);
c = format.charAt(current++);
if (c < 32)
throw illegalControlCharacter(c);
}
sb.append(c);
}
return sb.toString();
}
private void parseAuto() throws VersionFormatException {
VersionFormatParser.Instructions ep = parseProcessing();
if (ep != null) {
if (ep.padValue != null)
throw formatException(Messages.auto_can_not_have_pad_value);
}
currentList.add(createAutoFragment(ep, parseQualifier()));
}
private void parseBracketGroup() throws VersionFormatException {
List<Fragment> saveList = currentList;
currentList = new ArrayList<>();
while (current < eos && format.charAt(current) != ']')
parseFragment();
if (current == eos)
throw formatException(NLS.bind(Messages.premature_end_of_format_expected_0, "]")); //$NON-NLS-1$
++current;
VersionFormatParser.Instructions ep = parseProcessing();
saveList.add(createGroupFragment(ep, ZERO_OR_ONE_QUALIFIER, currentList.toArray(new Fragment[currentList.size()]), false));
currentList = saveList;
}
private void parseCharacterGroup(VersionFormatParser.Instructions ep) throws VersionFormatException {
assertChar('[');
StringBuilder sb = new StringBuilder();
outer: for (; current < eos; ++current) {
char c = format.charAt(current);
switch (c) {
case '\\' :
if (current + 1 < eos) {
sb.append(format.charAt(++current));
continue;
}
throw formatException(Messages.premature_end_of_format);
case '^' :
if (sb.length() == 0)
ep.inverted = true;
else
sb.append(c);
continue;
case ']' :
break outer;
case '-' :
if (sb.length() > 0 && current + 1 < eos) {
char rangeEnd = format.charAt(++current);
if (rangeEnd == ']') {
// Use dash verbatim when last in range
sb.append(c);
break outer;
}
char rangeStart = sb.charAt(sb.length() - 1);
if (rangeEnd < rangeStart)
throw formatException(Messages.negative_character_range);
while (++rangeStart <= rangeEnd)
sb.append(rangeStart);
continue;
}
// Fall through to default
default :
if (c < 32)
throw illegalControlCharacter(c);
sb.append(c);
}
}
assertChar(']');
int top = sb.length();
char[] chars = new char[top];
sb.getChars(0, top, chars, 0);
ep.characters = chars;
}
private void parseDelimiter() throws VersionFormatException {
VersionFormatParser.Instructions ep = parseProcessing();
if (ep != null) {
if (ep.rangeMin != 0 || ep.rangeMax != Integer.MAX_VALUE)
throw formatException(Messages.delimiter_can_not_have_range);
if (ep.ignore)
throw formatException(Messages.delimiter_can_not_be_ignored);
if (ep.defaultValue != null)
throw formatException(Messages.delimiter_can_not_have_default_value);
if (ep.padValue != null)
throw formatException(Messages.delimiter_can_not_have_pad_value);
}
currentList.add(createDelimiterFragment(ep, parseQualifier()));
}
private void parseEnum(Instructions processing) throws VersionFormatException {
++current;
ArrayList<List<String>> identifiers = new ArrayList<>();
ArrayList<String> idents = new ArrayList<>();
StringBuilder sb = new StringBuilder();
for (;;) {
if (current >= eos)
throw formatException(Messages.bad_enum_definition);
char c = format.charAt(current++);
while (c != '}' && c != ',' && c != '=') {
if (current >= eos || c <= ' ')
throw formatException(Messages.bad_enum_definition);
if (c == '\\') {
c = format.charAt(current++);
if (current >= eos)
throw formatException(Messages.bad_enum_definition);
}
sb.append(c);
c = format.charAt(current++);
}
idents.add(sb.toString());
sb.setLength(0);
if (c == '=')
continue;
identifiers.add(idents);
if (c == '}')
break;
// c must be ',' at this point
idents = new ArrayList<>();
}
boolean enumCaseSensitive = true;
boolean enumOptional = false;
boolean enumBegins = false;
OUTER:
while (current < eos) {
char c = format.charAt(current);
switch (c) {
case 'i':
enumCaseSensitive = false;
current++;
break;
case 'b':
enumBegins = true;
current++;
break;
case '?':
enumOptional = true;
current++;
break;
default:
break OUTER;
}
}
// Ensure that all identifiers are unique and make them
// lower case if necessary
HashSet<String> unique = new HashSet<>();
int ordinal = identifiers.size();
while (--ordinal >= 0) {
List<String> ids = identifiers.get(ordinal);
int idx = ids.size();
while (--idx >= 0) {
String id = ids.get(idx);
if (!enumCaseSensitive)
id = id.toLowerCase();
if (!unique.add(id))
throw formatException(Messages.bad_enum_definition);
ids.set(idx, id);
}
}
EnumDefinition enumDefinition = EnumDefinition.getEnumDefinition(identifiers);
processing.enumInstruction = new EnumInstruction(enumDefinition, enumCaseSensitive, enumOptional, enumBegins);
}
private void parseFragment() throws VersionFormatException {
if (current == eos)
throw formatException(Messages.premature_end_of_format);
char c = format.charAt(current++);
switch (c) {
case '(' :
parseGroup(false);
break;
case '<' :
parseGroup(true);
break;
case '[' :
parseBracketGroup();
break;
case 'a' :
parseAuto();
break;
case 'r' :
parseRaw();
break;
case 'n' :
parseNumber(false);
break;
case 'N' :
parseNumber(true);
break;
case 's' :
parseString(false);
break;
case 'S' :
parseString(true);
break;
case 'd' :
parseDelimiter();
break;
case 'q' :
parseQuotedString();
break;
case 'p' :
parsePad();
break;
default :
parseLiteral(c);
}
}
private void parseGroup(boolean array) throws VersionFormatException {
List<Fragment> saveList = currentList;
currentList = new ArrayList<>();
char expectedEnd = array ? '>' : ')';
while (current < eos && format.charAt(current) != expectedEnd)
parseFragment();
assertChar(expectedEnd);
VersionFormatParser.Instructions ep = parseProcessing();
if (ep != null) {
if (ep.characters != null)
throw formatException(Messages.array_can_not_have_character_group);
if (ep.rangeMax != Integer.MAX_VALUE && ep.padValue != null)
throw formatException(Messages.cannot_combine_range_upper_bound_with_pad_value);
if (ep.enumInstruction != null)
throw formatException(Messages.array_can_not_have_enum);
}
if (currentList.isEmpty())
throw formatException(array ? Messages.array_can_not_be_empty : Messages.group_can_not_be_empty);
saveList.add(createGroupFragment(ep, parseQualifier(), currentList.toArray(new Fragment[currentList.size()]), array));
currentList = saveList;
}
private int parseIntegerLiteral() throws VersionFormatException {
if (current == eos)
throw formatException(NLS.bind(Messages.premature_end_of_format_expected_0, "<integer>")); //$NON-NLS-1$
char c = format.charAt(current);
if (!VersionParser.isDigit(c))
throw formatException(c, "<integer>"); //$NON-NLS-1$
int value = c - '0';
while (++current < eos) {
c = format.charAt(current);
if (!VersionParser.isDigit(c))
break;
value *= 10;
value += (c - '0');
}
return value;
}
private void parseLiteral(char c) throws VersionFormatException {
String value;
switch (c) {
case '\'' :
value = parseAndConsiderEscapeUntil(c);
break;
case ')' :
case ']' :
case '{' :
case '}' :
case '?' :
case '*' :
throw formatException(c, "<literal>"); //$NON-NLS-1$
default :
if (VersionParser.isLetterOrDigit(c))
throw formatException(c, "<literal>"); //$NON-NLS-1$
if (c < 32)
throw illegalControlCharacter(c);
if (c == '\\') {
if (current == eos)
throw formatException(Messages.EOS_after_escape);
c = format.charAt(current++);
if (c < 32)
throw illegalControlCharacter(c);
}
value = new String(new char[] {c});
}
currentList.add(createLiteralFragment(parseQualifier(), value));
}
private int[] parseMinMax() throws VersionFormatException {
int max = Integer.MAX_VALUE;
++current;
int min = parseIntegerLiteral();
char c = format.charAt(current);
if (c == '}') {
max = min;
if (max == 0)
throw formatException(Messages.range_max_cannot_be_zero);
++current;
} else if (c == ',' && current + 1 < eos) {
if (format.charAt(++current) != '}') {
max = parseIntegerLiteral();
if (max == 0)
throw formatException(Messages.range_max_cannot_be_zero);
if (max < min)
throw formatException(Messages.range_max_cannot_be_less_then_range_min);
}
assertChar('}');
} else
throw formatException(c, "},"); //$NON-NLS-1$
return new int[] {min, max};
}
private void parseNumber(boolean signed) throws VersionFormatException {
VersionFormatParser.Instructions ep = parseProcessing();
if (ep != null) {
if (ep.padValue != null)
throw formatException(Messages.number_can_not_have_pad_value);
}
currentList.add(createNumberFragment(ep, parseQualifier(), signed));
}
private void parsePad() throws VersionFormatException {
currentList.add(createPadFragment(parseQualifier()));
}
private VersionFormatParser.Instructions parseProcessing() throws VersionFormatException {
if (current >= eos)
return null;
char c = format.charAt(current);
if (c != '=')
return null;
VersionFormatParser.Instructions ep = new VersionFormatParser.Instructions();
do {
current++;
parseProcessingInstruction(ep);
} while (current < eos && format.charAt(current) == '=');
return ep;
}
private void parseProcessingInstruction(VersionFormatParser.Instructions processing) throws VersionFormatException {
if (current == eos)
throw formatException(Messages.premature_end_of_format);
char c = format.charAt(current);
switch (c) {
case 'p':
// =pad(<raw-element>);
//
if (processing.padValue != null)
throw formatException(Messages.pad_defined_more_then_once);
if (processing.ignore)
throw formatException(Messages.cannot_combine_ignore_with_other_instruction);
++current;
processing.padValue = parseRawElement();
break;
case '!':
// =ignore;
//
if (processing.ignore)
throw formatException(Messages.ignore_defined_more_then_once);
if (processing.padValue != null || processing.characters != null || processing.rangeMin != 0 || processing.rangeMax != Integer.MAX_VALUE || processing.defaultValue != null)
throw formatException(Messages.cannot_combine_ignore_with_other_instruction);
++current;
processing.ignore = true;
break;
case '[':
// =[<character group];
//
if (processing.characters != null)
throw formatException(Messages.character_group_defined_more_then_once);
if (processing.ignore)
throw formatException(Messages.cannot_combine_ignore_with_other_instruction);
parseCharacterGroup(processing);
break;
case '{':
if (current + 1 == eos)
throw formatException(Messages.premature_end_of_format);
if (VersionParser.isDigit(format.charAt(current + 1))) {
// ={min,max};
//
if (processing.rangeMin != 0 || processing.rangeMax != Integer.MAX_VALUE)
throw formatException(Messages.range_defined_more_then_once);
if (processing.ignore)
throw formatException(Messages.cannot_combine_ignore_with_other_instruction);
int[] minMax = parseMinMax();
processing.rangeMin = minMax[0];
processing.rangeMax = minMax[1];
} else {
// ={enum1,enum2,...};
//
if (processing.enumInstruction != null)
throw formatException(Messages.enum_defined_more_then_once);
parseEnum(processing);
}
break;
default:
// =<raw-element>;
if (processing.defaultValue != null)
throw formatException(Messages.default_defined_more_then_once);
if (processing.ignore)
throw formatException(Messages.cannot_combine_ignore_with_other_instruction);
Comparable<?> dflt = parseRawElement();
processing.defaultValue = dflt;
if (current < eos && format.charAt(current) == '{') {
// =m{<translated min char>}
// =''{<translated max char>,<max char repeat>}
if (++current == eos)
throw formatException(Messages.premature_end_of_format);
processing.oppositeTranslationChar = format.charAt(current++);
if (current == eos)
throw formatException(Messages.premature_end_of_format);
if (dflt == VersionVector.MINS_VALUE) {
processing.oppositeTranslationRepeat = 3;
if (format.charAt(current) == ',') {
++current;
processing.oppositeTranslationRepeat = parseIntegerLiteral();
}
} else if (dflt != VersionVector.MAXS_VALUE) {
current -= 2;
throw formatException(Messages.only_max_and_empty_string_defaults_can_have_translations);
}
assertChar('}');
}
break;
}
assertChar(';');
}
private Qualifier parseQualifier() throws VersionFormatException {
if (current >= eos)
return EXACT_ONE_QUALIFIER;
char c = format.charAt(current);
if (c == '?') {
++current;
return ZERO_OR_ONE_QUALIFIER;
}
if (c == '*') {
++current;
return ZERO_OR_MANY_QUALIFIER;
}
if (c == '+') {
++current;
return ONE_OR_MANY_QUALIFIER;
}
if (c != '{')
return EXACT_ONE_QUALIFIER;
int[] minMax = parseMinMax();
int min = minMax[0];
int max = minMax[1];
// Use singletons for commonly used ranges
//
if (min == 0) {
if (max == 1)
return ZERO_OR_ONE_QUALIFIER;
if (max == Integer.MAX_VALUE)
return ZERO_OR_MANY_QUALIFIER;
} else if (min == 1) {
if (max == 1)
return EXACT_ONE_QUALIFIER;
if (max == Integer.MAX_VALUE)
return ONE_OR_MANY_QUALIFIER;
}
return new Qualifier(min, max);
}
private void parseQuotedString() throws VersionFormatException {
VersionFormatParser.Instructions ep = parseProcessing();
if (ep != null) {
if (ep.padValue != null)
throw formatException(Messages.string_can_not_have_pad_value);
}
currentList.add(createQuotedFragment(ep, parseQualifier()));
}
private void parseRaw() throws VersionFormatException {
VersionFormatParser.Instructions ep = parseProcessing();
if (ep != null) {
if (ep.padValue != null)
throw formatException(Messages.raw_element_can_not_have_pad_value);
}
currentList.add(createRawFragment(ep, parseQualifier()));
}
private Comparable<?> parseRawElement() throws VersionFormatException {
int[] position = new int[] {current};
Comparable<?> v = VersionParser.parseRawElement(format, position, eos);
if (v == null)
throw new VersionFormatException(NLS.bind(Messages.raw_element_expected_0, format));
current = position[0];
return v;
}
private void parseString(boolean unlimited) throws VersionFormatException {
VersionFormatParser.Instructions ep = parseProcessing();
if (ep != null) {
if (ep.padValue != null)
throw formatException(Messages.string_can_not_have_pad_value);
}
currentList.add(createStringFragment(ep, parseQualifier(), unlimited));
}
static void toStringEscaped(StringBuffer sb, String value, String escapes) {
for (int idx = 0; idx < value.length(); ++idx) {
char c = value.charAt(idx);
if (c == '\\' || escapes.indexOf(c) >= 0)
sb.append('\\');
sb.append(c);
}
}
}