com.cedarsoftware.visualizer.RpmVisualizerHelper.groovy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of n-cube Show documentation
Show all versions of n-cube Show documentation
Multi-dimensional Rule Engine
package com.cedarsoftware.visualizer
import com.cedarsoftware.ncube.ApplicationID
import com.cedarsoftware.ncube.Axis
import com.cedarsoftware.ncube.Column
import com.cedarsoftware.ncube.NCube
import com.cedarsoftware.ncube.NCubeRuntimeClient
import com.cedarsoftware.ncube.exception.InvalidCoordinateException
import com.cedarsoftware.util.CaseInsensitiveSet
import com.cedarsoftware.util.CompactCILinkedMap
import groovy.transform.CompileStatic
import java.util.regex.Matcher
import java.util.regex.Pattern
import static com.cedarsoftware.ncube.NCubeAppContext.getNcubeRuntime
import static com.cedarsoftware.util.StringUtilities.isEmpty
/**
* Provides helper methods to load fields and traits for an rpm class.
* The methods are are copied from Dynamis unless otherwise is indicated. Some methods are slightly altered.
* Do find on 'COPIED' to find code copied from Dynamis.
* Do find on 'ORIGINAL' to find code not copied from Dynamis.
*/
@CompileStatic
class RpmVisualizerHelper extends VisualizerHelper
{
/**
* ORIGINAL: Not copied from Dynamis
*/
public static final String FIELD_AXIS = "field"
public static final String TRAIT_AXIS = "trait"
public static final String ENUM_NAME_AXIS = "name"
public static final String RPM_CLASS = "rpm.class"
public static final String RPM_ENUM = "rpm.enum"
public static final String R_EXTENDS = 'r:extends'
public static final String R_EXISTS = 'r:exists'
public static final String R_RPM_TYPE = 'r:rpmType'
public static final String R_SCOPED_NAME = 'r:scopedName'
public static final String R_DECLARED = 'r:declared'
public static final String R_SINCE = 'r:since'
public static final String R_OBSOLETE = 'r:obsolete'
public static final String V_ENUM = 'v:enum'
public static final String V_MIN = 'v:min'
public static final String V_MAX = 'v:max'
private static final String NOT_DEFINED = '#NOT_DEFINED'
public static final String CLASS_TRAITS = 'CLASS_TRAITS'
public static final String SYSTEM_SCOPE_KEY_PREFIX = "_"
public static final String EFFECTIVE_VERSION_SCOPE_KEY = SYSTEM_SCOPE_KEY_PREFIX + "effectiveVersion"
public static final List MINIMAL_TRAITS = [R_RPM_TYPE, R_SCOPED_NAME, R_EXTENDS, R_EXISTS, R_DECLARED, R_SINCE, R_OBSOLETE, V_ENUM, V_MIN, V_MAX]
private static final String EXISTS_TRAIT_CONTAINS_NULL_VALUE = " may not contain a value of null. If there is a value, it must be true or false. "
private static ApplicationID appId
private static boolean loadAllTraits
RpmVisualizerHelper(NCubeRuntimeClient runtimeClient, ApplicationID appId)
{
super(runtimeClient, appId)
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
*/
public static
final Pattern PATTERN_CLASS_NAME = Pattern.compile('^(?:[a-z][a-z0-9_]*)(?:\\.[a-z][a-z0-9_]*)*$', Pattern.CASE_INSENSITIVE)
public static
final Pattern PATTERN_CLASS_EXTENDS_TRAIT = Pattern.compile('[^,\\s][^\\,]*[^,\\s]*', Pattern.CASE_INSENSITIVE)
public static
final Pattern PATTERN_FIELD_EXTENDS_TRAIT = Pattern.compile('^\\s*((?:[a-z][a-z0-9_]*)(?:\\.[a-z][a-z0-9_]*)*)\\s*(?:[\\[]\\s*([a-z0-9_]+?)\\s*[\\]])?\\s*$', Pattern.CASE_INSENSITIVE)
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
*/
boolean isPrimitive(String type){
for (PRIMITIVE_TYPE pt : PRIMITIVE_TYPE.values()) {
if (pt.getClassType().getSimpleName().equalsIgnoreCase(type)) {
return true
}
}
return false
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
* pulls all of the fields and associated traits from nCube that will be used to create the RpmClass/RpmEnum instance
*/
void loadRpmClassFields(ApplicationID applicationId, String cubeType, String cubeName, Map scope, Map> traitMaps, boolean loadAllTraits, Map output)
{
this.appId = applicationId
this.loadAllTraits = loadAllTraits
LinkedList classesToProcess = new LinkedList()
Set visited = new LinkedHashSet()
// loop through class hierarchy until all classes in the r:extends chain have been handled
boolean isOriginalClass = true
classesToProcess.add(cubeName)
while (!classesToProcess.isEmpty())
{
String className = classesToProcess.pop()
// don't allow cycles
if (visited.contains(className))
{
continue
}
visited.add(className)
try
{
loadFieldTraitsForClass(cubeType, className, scope, traitMaps, classesToProcess, output)
if(isOriginalClass)
{
for(Map.Entry> entry : traitMaps.entrySet())
{
if(!CLASS_TRAITS.equals(entry.getKey()))
{
entry.getValue().put(R_DECLARED, true)
}
}
}
isOriginalClass = false
}
catch (Exception e)
{
handleException(cubeType,visited,className,e)
}
} // end class stack
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
* Populates the field traits for the class or enum
*/
private void loadFieldTraitsForClass(String cubeType, String className, Map scope, Map> fieldAndTraits, LinkedList classesToProcess, Map output)
{
String axisName = RPM_ENUM.equals(cubeType) ? ENUM_NAME_AXIS : FIELD_AXIS
NCube classCube = findClassCube(cubeType, scope, className, output)
if (classCube == null)
{
String classType = cubeType==RPM_CLASS ? "RpmClass" : "RpmEnum"
throw new IllegalArgumentException(classType + " definition not found for identifier='" + className + "'")
}
// populate initial fields
populateAllFieldsFromAxis(fieldAndTraits, axisName, classCube)
// wildcard the fieldAxis using r:exists
populateExistsTrait(className, axisName, scope, fieldAndTraits, classCube, output)
// determine traits to fetch, except for r:exists already fetched
List traitNames = getTraitNamesForCube(classCube, (String) scope.get(EFFECTIVE_VERSION_SCOPE_KEY))
traitNames.remove(R_EXISTS)
// pull traits for existing fields
Axis fieldAxis = classCube.getAxis(axisName)
for(String fieldName : fieldAndTraits.keySet()) {
Map fieldTraits = fieldAndTraits.get(fieldName)
// short circuit the fields that don't exist
Boolean exist = (Boolean) fieldTraits.get(R_EXISTS)
if (Boolean.FALSE.equals(exist)) {
continue
}
else if (fieldAxis.findColumn(fieldName)==null) {
continue
}
// gather traits for current field that haven't already been populated
Map coord = new LinkedHashMap<>(scope)
coord.put(axisName,fieldName)
loadTraitsForField(classCube, traitNames, fieldTraits, coord, output)
// eliminate scoped fields
if (!isFieldValidSince(fieldTraits,(String) scope.get(EFFECTIVE_VERSION_SCOPE_KEY))) {
fieldTraits.put(R_EXISTS,false)
}
if (!isFieldValidObsolete(fieldTraits,(String) scope.get(EFFECTIVE_VERSION_SCOPE_KEY))) {
fieldTraits.put(R_EXISTS,false)
}
// check extends value
if (traitNames.contains(R_EXTENDS)) {
coord.put(TRAIT_AXIS, R_EXTENDS)
Object extendsValue = classCube.getCell(coord, output, NOT_DEFINED)
if (extendsValue!=null && hasValue(extendsValue))
{
if (!fieldTraits.containsKey(R_EXTENDS)) {
fieldTraits.put(R_EXTENDS,extendsValue.toString())
}
if (CLASS_TRAITS.equals(fieldName)) {
processClassMixins(className, extendsValue.toString(), classesToProcess)
}
else {
processMasterDefinition(className, fieldName, extendsValue.toString(), cubeType, axisName, scope, fieldTraits, output)
}
}
}
}
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
* applies the master definition specified in r:extends to the current field traits
*/
private void processMasterDefinition(String className, String fieldName, String masterDefinition,
String cubeType, String axisName, Map scope, Map traits, Map output)
{
String classType = cubeType==RPM_CLASS ? "RpmClass" : "RpmEnum"
LinkedList defsToProcess = new LinkedList()
Set visited = new HashSet<>() // list of visited definitions pulled from traits using class_name or class_name[field_name] format
Set fqVisited = new HashSet<>() // list of visited definitions which have been fully qualified, e.g. class_name[field_name]
final String exceptionFormat = "%s for field='%s' of %s='%s': definition(s)='%s'"
defsToProcess.add(masterDefinition)
while (!defsToProcess.empty) {
String fieldToMerge = defsToProcess.pop()
visited.add(fieldToMerge)
Matcher m = PATTERN_FIELD_EXTENDS_TRAIT.matcher(fieldToMerge)
if (!m.matches())
{
throw new IllegalArgumentException(String.format(exceptionFormat, "Invalid master definition format used", fieldName,
classType, className, Arrays.toString(visited.toArray())))
}
// determine the master definition to use
String masterClass = m.group(1)
String masterField = isEmpty(m.group(2)) ? fieldName : m.group(2)
String fqMasterDef = masterClass + "[" + masterField + "]" // fully qualified master definition
// make sure class hasn't already been processed
if (fqVisited.contains(fqMasterDef))
{
continue
}
fqVisited.add(fqMasterDef)
// make sure the class definition exists
NCube masterCube = findClassCube(cubeType, scope, masterClass, output)
if (masterCube == null)
{
throw new IllegalArgumentException(String.format(exceptionFormat, "Class in master definition not found", fieldName,
classType, className, Arrays.toString(visited.toArray())))
}
// validate the field name
boolean validField = masterCube.getAxis(axisName).findColumn(masterField) != null
if (!validField) {
throw new IllegalArgumentException(String.format(exceptionFormat, "Field in master definition not found", fieldName,
classType, className, Arrays.toString(visited.toArray())))
}
Map coord = new CompactCILinkedMap<>(scope)
coord.put(axisName,masterField)
List traitNames = getTraitNamesForCube(masterCube, (String) scope.get(EFFECTIVE_VERSION_SCOPE_KEY))
loadTraitsForField(masterCube, traitNames, traits, coord, output)
if (traits.containsKey(R_EXISTS) && traits.get(R_EXISTS) == null){
throw new IllegalArgumentException(String.format(exceptionFormat, R_EXISTS + EXISTS_TRAIT_CONTAINS_NULL_VALUE, fieldName,
classType, className, Arrays.toString(visited.toArray())))
}
// check for extended definitions
if (traitNames.contains(R_EXTENDS)) {
coord.put(TRAIT_AXIS, R_EXTENDS)
String extension = (String) masterCube.getCell(coord, output, NOT_DEFINED)
if (hasValue(extension) && !isEmpty(extension)) {
defsToProcess.add(extension)
}
}
} // end while
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
* Returns the nCube for the specified class (or enum)
*/
private static NCube findClassCube(String cubeType, Map scope, String className, Map output) {
if (className==null || !PATTERN_CLASS_NAME.matcher(className).matches())
{
throw new IllegalArgumentException("Invalid class identifier [" + className + "] was specified for " + cubeType)
}
NCube ncube = ncubeRuntime.getCube(appId, cubeType + "." + className)
if (ncube==null)
{
return null
}
Set requiredScope = getRequiredScope(ncube, scope, output)
if (RPM_ENUM.equals(cubeType))
{
requiredScope.remove("name")
}
ensureEnoughScopeProvided(cubeType, className, scope, requiredScope)
return ncube
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
* parses the value of the r:extends trait and adds all mixins to the list of classes to process
*/
private static void processClassMixins(String className, String mixins, LinkedList classesToProcess) {
if (classesToProcess == null)
{
return
}
Matcher matcher = PATTERN_CLASS_EXTENDS_TRAIT.matcher(mixins)
if (isEmpty(mixins) || !matcher.find())
{
throw new IllegalArgumentException("Invalid mixin format specified for class='${className}': mixin='${mixins}'")
}
for (; ;) { // infinite for
String mixinName = matcher.group(0)
if (!isEmpty(mixinName))
{
classesToProcess.addFirst(mixinName.trim())
}
if (!matcher.find())
{ //condition to break, opposite to while
break
}
}
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
*/
private enum PRIMITIVE_TYPE {
BOOLEAN (Boolean.class), LONG(Long.class), DOUBLE(Double.class), BIG_DECIMAL(BigDecimal.class), STRING(String.class), DATE(Date.class);
private Class> classType
private PRIMITIVE_TYPE(Class> classType) {
this.classType = classType
}
Class> getClassType() {
return this.classType
}
static PRIMITIVE_TYPE fromName(String typeName) {
for (PRIMITIVE_TYPE type : values()) {
if (type.toString().equalsIgnoreCase(typeName) || type.getClassType().getSimpleName().equalsIgnoreCase(typeName)) {
return type
}
}
throw new IllegalArgumentException("Unknown primitive type specified: " + typeName)
}
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
* Throws RpmException which includes list of classes processed and cause
*/
private static void handleException(String cubeType, Set visited, String className, Exception e) {
// to help with debugging issues related to classes using mixins, dump the list of classes processed thus far
StringBuilder msg = new StringBuilder()
msg.append("Failed to load " + (cubeType==RPM_CLASS ? "RpmClass" : "RpmEnum") + "='")
msg.append(className)
msg.append("'")
if (visited.size()>1)
{
msg.append(", classes processed=")
msg.append(Arrays.toString(visited.toArray()))
}
throw new Exception( msg.toString(), e)
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
* Load trait values for a given field into the fieldTraits map, ignoring traits already loaded
*/
private void loadTraitsForField(NCube classCube, List traitNames, Map fieldTraits, Map coord, Map output) {
for (String traitName : traitNames) {
if (fieldTraits.containsKey(traitName) || R_EXTENDS.equals(traitName)) {
continue
}
coord.put(TRAIT_AXIS,traitName)
Object val = classCube.getCell(coord, output, NOT_DEFINED)
if (hasValue(val)) {
fieldTraits.put(traitName, val)
}
}
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
* Determines initial list of fields by extracting column values from axis
*/
private static void populateAllFieldsFromAxis(Map> fieldAndTraits, String axisName, NCube classCube) {
for (Column c:classCube.getAxis(axisName).getColumns()) {
String fieldName = c.getValueThatMatches().toString()
Map fieldTraits = fieldAndTraits.get(fieldName)
if (fieldTraits==null) {
fieldTraits = new LinkedHashMap<>()
fieldAndTraits.put(fieldName,fieldTraits)
}
}
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
*/
private boolean isFieldValidSince(Map traits, String sourceVersion) {
if (!traits.containsKey(R_SINCE)) {
return true
}
Object sinceVersionString = traits.get(R_SINCE)
ComparableVersion sinceVersion = new ComparableVersion(sinceVersionString.toString())
ComparableVersion version = new ComparableVersion(sourceVersion)
return version.compareTo(sinceVersion) >= 0
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
*/
private boolean isFieldValidObsolete(Map traits, String sourceVersion) {
if (!traits.containsKey(R_OBSOLETE)) {
return true
}
Object obsoleteVersionString = traits.get(R_OBSOLETE)
ComparableVersion obsoleteVersion = new ComparableVersion(obsoleteVersionString.toString())
ComparableVersion version = new ComparableVersion(sourceVersion)
return version.compareTo(obsoleteVersion) < 0
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
* Return List of Strings, containing names of trait columns defined on the NCube specified
*/
private static List getTraitNamesForCube(NCube classCube, String sourceVersion) {
Axis traitAxis = classCube.getAxis(TRAIT_AXIS)
List traitNames = new ArrayList<>()
for (Column c:traitAxis.getColumns()) {
String traitName = c.getValue().toString()
if (loadAllTraits || MINIMAL_TRAITS.contains(traitName)){
traitNames.add(traitName)
}
}
return traitNames
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
* Get the 'proper' requiredScope from NCube. In addition to getting the scope
* keys (Strings), the associated Set is all the values used for the given scope
* key, akin to all enums in an enum list.
*/
private static Set getRequiredScope(NCube ncube, Map scope, Map output)
{
Set requiredScope = new CaseInsensitiveSet(ncube.getRequiredScope(scope, output))
// Although 'field' and 'trait' are axes on the ncube defining the class/enum/rel, they are system
// scope, not business scope.
requiredScope.remove(FIELD_AXIS)
requiredScope.remove(TRAIT_AXIS)
return requiredScope
}
/**
* MODIFIED: From Dynamis 5.2.0. Modified to throw InvalidCoordinateException
* Ensure that enough scope is provided. This will check that the original scope key set
* has all the keys required to reach all cells in the defining ncube.
*/
private static void ensureEnoughScopeProvided(String cubeType, String className, Map scope, Set requiredScope)
{
if (!scope.keySet().containsAll(requiredScope))
{
Set missingScope = new CaseInsensitiveSet(requiredScope)
Set scopeKeySet = scope.keySet()
for (String scopeKey : scopeKeySet)
{
missingScope.remove(scopeKey)
}
String cubeName = "${cubeType}.${className}"
throw new InvalidCoordinateException("Not enough scope was provided to create class/enum/rel: ${className}, missing scope keys: ${missingScope}", cubeName, scopeKeySet, requiredScope)
}
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
* Bulk loads value of r:exists for all fields defined
*/
private void populateExistsTrait(String className, String axisName, Map scope, Map> fieldAndTraits, NCube classCube, Map output) {
Axis traitAxis = classCube.getAxis(TRAIT_AXIS)
if (traitAxis==null || traitAxis.findColumn(R_EXISTS)==null) {
return
}
Map coord = new LinkedHashMap<>(scope)
coord.put(TRAIT_AXIS, R_EXISTS)
for (Column c : classCube.getAxis(axisName).getColumns()) {
String fieldName = (String) c.getValue()
Map fieldTraits = fieldAndTraits.get(fieldName)
if (!fieldTraits.containsKey(R_EXISTS)) {
coord.put(axisName,fieldName)
Boolean exists = getExistsValue(fieldName, className, classCube.getCell(coord, output, NOT_DEFINED))
if (exists!=null) {
fieldTraits.put(R_EXISTS, exists)
}
}
}
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
* Returns the value of the r:exists trait, or null if not set
* @return the boolean value of r:exists, if it exists; otherwise, null
*/
private static Boolean getExistsValue(String fieldName, String className, Object exists) {
if (CLASS_TRAITS.equals(fieldName)) {
return true
}
if (exists==null)
{
throw new IllegalStateException(R_EXISTS + EXISTS_TRAIT_CONTAINS_NULL_VALUE + "field: "+ fieldName + ", rpmClass: "+ className)
}
if (!hasValue(exists)) {
return null
}
if (exists instanceof String)
{
exists = Boolean.valueOf((String)exists)
}
else if(!(exists instanceof Boolean))
{
throw new IllegalStateException(R_EXISTS + " must be boolean or string. field: "+ fieldName + ", rpmClass: "+ className)
}
return (Boolean)exists
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
* Utility method to return true if the field value exists and doesn't match default
*/
private static boolean hasValue(Object value) {
return !(value != null && NOT_DEFINED.equals(value))
}
/**
* COPIED: From Dynamis 5.2.0 (except for slight modifications)
*/
private class ComparableVersion
implements Comparable {
private String value
private String canonical
private ListItem items
private interface Item {
int INTEGER_ITEM = 0
int STRING_ITEM = 1
int LIST_ITEM = 2
int compareTo(Item item);
int getType();
boolean isNull();
}
/**
* Represents a numeric item in the version item list.
*/
private static class IntegerItem
implements Item {
private static final BigInteger BIG_INTEGER_ZERO = new BigInteger("0")
private final BigInteger value
public static final IntegerItem ZERO = new IntegerItem()
private IntegerItem() {
this.value = BIG_INTEGER_ZERO
}
IntegerItem(String str) {
this.value = new BigInteger(str)
}
int getType() {
return INTEGER_ITEM
}
boolean isNull() {
return BIG_INTEGER_ZERO.equals(value)
}
int compareTo(Item item) {
if (item == null) {
return BIG_INTEGER_ZERO.equals(value) ? 0 : 1 // 1.0 == 1, 1.1 > 1
}
switch (item.getType()) {
case INTEGER_ITEM:
return value.compareTo(((IntegerItem) item).value)
case STRING_ITEM:
return 1 // 1.1 > 1-sp
case LIST_ITEM:
return 1 // 1.1 > 1-1
default:
throw new RuntimeException("invalid item: ${item.class.name}")
}
}
String toString() {
return value.toString()
}
}
/**
* Represents a string in the version item list, usually a qualifier.
*/
private static class StringItem
implements Item {
private static final String[] QUALIFIERS = ['alpha', 'beta', 'milestone', 'rc', 'snapshot', '', 'sp' ]
@SuppressWarnings("checkstyle:constantname")
private static final List _QUALIFIERS = Arrays.asList(QUALIFIERS)
private static final Properties ALIASES = new Properties()
static
{
ALIASES.put("ga", "")
ALIASES.put("final", "")
ALIASES.put("cr", "rc")
}
/**
* A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes
* the version older than one without a qualifier, or more recent.
*/
private static final String RELEASE_VERSION_INDEX = String.valueOf(_QUALIFIERS.indexOf(""))
private String value
StringItem(String value, boolean followedByDigit) {
if (followedByDigit && value.length() == 1) {
// a1 = alpha-1, b1 = beta-1, m1 = milestone-1
switch (value.charAt(0)) {
case 'a':
value = "alpha"
break
case 'b':
value = "beta"
break
case 'm':
value = "milestone"
break
default:
break
}
}
this.value = ALIASES.getProperty(value, value)
}
int getType() {
return STRING_ITEM
}
boolean isNull() {
return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0)
}
/**
* Returns a comparable value for a qualifier.
*
* This method takes into account the ordering of known qualifiers then unknown qualifiers with lexical
* ordering.
*
* 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 resort to lexical ordering. Most comparisons are decided by the first character,
* so this is still fast. If more characters are needed then it requires a lexical sort anyway.
*
* @param qualifier
* @return an equivalent value that can be used with lexical comparison
*/
static String comparableQualifier(String qualifier) {
int i = _QUALIFIERS.indexOf(qualifier)
return i == -1 ? (_QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i)
}
int compareTo(Item item) {
if (item == null) {
// 1-rc < 1, 1-ga > 1
return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX)
}
switch (item.getType()) {
case INTEGER_ITEM:
return -1 // 1.any < 1.1 ?
case STRING_ITEM:
return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value))
case LIST_ITEM:
return -1 // 1.any < 1-1
default:
throw new RuntimeException("invalid item: " + item.getClass())
}
}
String toString() {
return value
}
}
/**
* 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 specification).
*/
private static class ListItem
extends ArrayList-
implements Item {
int getType() {
return LIST_ITEM
}
boolean isNull() {
return (size() == 0)
}
void normalize() {
for (int i = size() - 1; i >= 0; i--) {
Item lastItem = get(i)
if (lastItem.isNull()) {
// remove null trailing items: 0, "", empty list
remove(i)
} else if (!(lastItem instanceof ListItem)) {
break
}
}
}
int compareTo(Item item) {
if (item == null) {
if (size() == 0) {
return 0 // 1-0 = 1- (normalize) = 1
}
Item first = get(0)
return first.compareTo(null)
}
switch (item.getType()) {
case INTEGER_ITEM:
return -1 // 1-1 < 1.0.x
case STRING_ITEM:
return 1 // 1-1 > 1-sp
case LIST_ITEM:
Iterator
- left = iterator()
Iterator
- right = ((ListItem) item).iterator()
while (left.hasNext() || right.hasNext()) {
Item l = left.hasNext() ? left.next() : null
Item r = right.hasNext() ? right.next() : null
// if this is shorter, then invert the compare and mul with -1
int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r)
if (result != 0) {
return result
}
}
return 0
default:
throw new RuntimeException("invalid item: ${item?.class.name}")
}
}
String toString() {
StringBuilder buffer = new StringBuilder()
for (Item item : this) {
if (buffer.length() > 0) {
buffer.append((item instanceof ListItem) ? '-' : '.')
}
buffer.append(item)
}
return buffer.toString()
}
}
ComparableVersion(String version) {
parseVersion(version)
}
final void parseVersion(String version)
{
this.value = version
items = new ListItem()
version = version.toLowerCase(Locale.ENGLISH)
ListItem list = items
Stack stack = new Stack<>()
stack.push(list)
boolean isDigit = false
int startIndex = 0
for (int i = 0; i < version.length(); i++)
{
char c = version.charAt(i)
if (c == '.' as char)
{
if (i == startIndex)
{
list.add(IntegerItem.ZERO)
}
else
{
list.add(parseItem(isDigit, version.substring(startIndex, i)))
}
startIndex = i + 1
}
else if (c == '-' as char)
{
if (i == startIndex)
{
list.add(IntegerItem.ZERO)
}
else
{
list.add(parseItem(isDigit, version.substring(startIndex, i)))
}
startIndex = i + 1
list.add(list = new ListItem())
stack.push(list)
}
else if (Character.isDigit(c))
{
if (!isDigit && i > startIndex)
{
list.add(new StringItem(version.substring(startIndex, i), true))
startIndex = i
list.add(list = new ListItem())
stack.push(list)
}
isDigit = true
}
else
{
if (isDigit && i > startIndex)
{
list.add(parseItem(true, version.substring(startIndex, i)))
startIndex = i
list.add(list = new ListItem())
stack.push(list)
}
isDigit = false
}
}
if (version.length() > startIndex)
{
list.add(parseItem(isDigit, version.substring(startIndex)))
}
while (!stack.empty)
{
list = (ListItem) stack.pop()
list.normalize()
}
canonical = items.toString()
}
private static Item parseItem(boolean isDigit, String buf) {
return isDigit ? new IntegerItem(buf) : new StringItem(buf, false)
}
int compareTo(ComparableVersion o) {
return items.compareTo(o.items)
}
String toString() {
return value
}
String getCanonical() {
return canonical
}
boolean equals(Object o) {
return (o instanceof ComparableVersion) && canonical.equals(((ComparableVersion) o).canonical)
}
int hashCode() {
return canonical.hashCode()
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy