diff --git a/application-engine/src/main/java/com/netgrif/application/engine/configuration/CacheConfiguration.java b/application-engine/src/main/java/com/netgrif/application/engine/configuration/CacheConfiguration.java index 55909a8a98..9ec851c1bb 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/configuration/CacheConfiguration.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/configuration/CacheConfiguration.java @@ -1,31 +1,61 @@ package com.netgrif.application.engine.configuration; import com.netgrif.application.engine.configuration.properties.CacheConfigurationProperties; +import com.netgrif.application.engine.workflow.domain.CachedFunction; +import groovy.lang.Closure; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.CachingConfigurer; import org.springframework.cache.annotation.EnableCaching; -import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.support.SimpleCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import java.util.*; +import java.util.stream.Collectors; + @Configuration @EnableCaching -public class CacheConfiguration extends CachingConfigurerSupport { - +@RequiredArgsConstructor +public class CacheConfiguration implements CachingConfigurer { private final CacheConfigurationProperties properties; - public CacheConfiguration(CacheConfigurationProperties properties) { - this.properties = properties; - } - @Bean @Primary @Override public CacheManager cacheManager() { - return new ConcurrentMapCacheManager(properties.getAllCaches().toArray(String[]::new)); + Set cacheNames = properties.getAllCaches(); + List caches = cacheNames.stream() + .map(ConcurrentMapCache::new) + .collect(Collectors.toCollection(ArrayList::new)); + + caches.add(new GenericMapCache<>( + CacheMapKeys.ACTIONS, + Closure.class, + null, + properties.getActionCacheSize() + )); + caches.add(new GenericMapCache<>( + CacheMapKeys.FUNCTIONS, + CachedFunction.class, + null, + properties.getFunctionsCacheSize() + )); + caches.add(new GenericMapCache<>( + CacheMapKeys.GLOBAL_FUNCTIONS, + List.class, + CachedFunction.class, + properties.getGlobalFunctionsCacheSize() + )); + + SimpleCacheManager cacheManager = new SimpleCacheManager(); + cacheManager.setCaches(caches); + return cacheManager; } @Bean diff --git a/application-engine/src/main/java/com/netgrif/application/engine/configuration/CacheMapKeys.java b/application-engine/src/main/java/com/netgrif/application/engine/configuration/CacheMapKeys.java new file mode 100644 index 0000000000..bb98f26977 --- /dev/null +++ b/application-engine/src/main/java/com/netgrif/application/engine/configuration/CacheMapKeys.java @@ -0,0 +1,8 @@ +package com.netgrif.application.engine.configuration; + +public final class CacheMapKeys { + private CacheMapKeys() {} + public static final String ACTIONS = "actions"; + public static final String FUNCTIONS = "functions"; + public static final String GLOBAL_FUNCTIONS = "globalFunctions"; +} diff --git a/application-engine/src/main/java/com/netgrif/application/engine/configuration/GenericMapCache.java b/application-engine/src/main/java/com/netgrif/application/engine/configuration/GenericMapCache.java new file mode 100644 index 0000000000..b5fc4bd7d1 --- /dev/null +++ b/application-engine/src/main/java/com/netgrif/application/engine/configuration/GenericMapCache.java @@ -0,0 +1,124 @@ +package com.netgrif.application.engine.configuration; + +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.cache.Cache; +import org.springframework.cache.support.SimpleValueWrapper; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +public class GenericMapCache implements Cache { + private final String name; + private final Class valueType; + private final Class elementType; + private final ConcurrentHashMap map; + + public GenericMapCache(String name, Class valueType, Class elementType, int initialCapacity) { + this.name = name; + this.valueType = valueType; + this.elementType = elementType; + this.map = new ConcurrentHashMap<>(initialCapacity); + } + + @Override public @NotNull String getName() { return name; } + + @Override public @NotNull Object getNativeCache() { return Map.copyOf(map); } + + @Override + public T get(Object key, Callable loader) { + final String stringKey = String.valueOf(key); + try { + V value = map.computeIfAbsent(stringKey, cacheValue -> { + try { + T computed = loader.call(); + if (computed == null) return null; + return safeCast(computed); + } + catch (Exception e) { + throw new RuntimeException(e); + } + }); + return (T) value; + } catch (RuntimeException e) { + Throwable cause = (e.getCause() != null) ? e.getCause() : e; + throw new Cache.ValueRetrievalException(stringKey, loader, cause); + } + } + + @Override + public ValueWrapper get(Object key) { + String stringKey = String.valueOf(key); + Object valueObject = map.get(stringKey); + return valueObject != null ? new SimpleValueWrapper(valueObject) : null; + } + + @Override + public T get(Object key, Class type) { + String stringKey = String.valueOf(key); + Object valueObject = map.get(stringKey); + return valueObject != null ? type.cast(valueObject) : null; + } + + @Override + public void put(Object key, Object value) { + if (value == null) { + evict(key); + return; + } + map.put(String.valueOf(key), safeCast(value)); + } + + @Override + public void evict(Object key) { + map.remove(String.valueOf(key)); + } + + @Override + public void clear() { + map.clear(); + } + + @SuppressWarnings("unchecked") + private V safeCast(Object value) { + if (value == null) { + return null; + } + + if (valueType.isInstance(value)) { + return (V) value; + } + + // Check if the value is a list and the cache type is List + if (value instanceof List && List.class.isAssignableFrom(valueType)) { + List list = (List) value; + + // Check only if the list is non-empty + if (!list.isEmpty()) { + Object firstElement = list.getFirst(); + + // Validate element type + if (elementType != null && !elementType.isInstance(firstElement)) { + throw new ClassCastException( + String.format("Cannot cast list element of type %s to %s", + firstElement.getClass().getName(), + elementType.getName() + ) + ); + } + } + + return (V) list; // Safe cast to desired list type + } + + throw new ClassCastException( + String.format("Cannot cast value of type %s to %s", + value.getClass().getName(), + valueType.getName() + ) + ); + } +} diff --git a/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/CacheConfigurationProperties.java b/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/CacheConfigurationProperties.java index 241bc2f064..9adfedd1c8 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/CacheConfigurationProperties.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/CacheConfigurationProperties.java @@ -1,5 +1,6 @@ package com.netgrif.application.engine.configuration.properties; +import jakarta.validation.constraints.Min; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @@ -46,17 +47,38 @@ public class CacheConfigurationProperties { */ private String loadedModules = "loadedModules"; - /** - * Default cache name for caching global functions of PetriNet global scoped functions. - */ - private String globalFunctions = "globalFunctions"; - /** * A list of additional custom cache names. * Allows users to define their own cache names for specific use cases. */ private List additional = new ArrayList<>(); + /** + * The size of pages used for caching functions when processing large sets of data. + * This property determines the maximum number of functions to include in a single page during caching operations. + * Default value is 500. + */ + @Min(1) + private int functionCachingPageSize = 500; + + /** + * The size of the cache used for handling field runner actions. + */ + @Min(1) + private int actionCacheSize = 500; + + /** + * The size of the cache used for managing field runner functions. + */ + @Min(1) + private int functionsCacheSize = 500; + + /** + * The size of the cache used for managing global Petri net functions. + */ + @Min(1) + private int globalFunctionsCacheSize = 500; + /** * Retrieves a set of all configured cache names. * Includes the default caches and any additional user-defined cache names. @@ -65,7 +87,7 @@ public class CacheConfigurationProperties { */ public Set getAllCaches() { Set caches = new LinkedHashSet<>(Arrays.asList(petriNetById, petriNetByIdentifier, petriNetDefault, - petriNetLatest, petriNetCache, loadedModules, globalFunctions)); + petriNetLatest, petriNetCache, loadedModules)); caches.addAll(additional); return caches; } diff --git a/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/RunnerConfigurationProperties.java b/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/RunnerConfigurationProperties.java index 921cab391c..5e48be820b 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/RunnerConfigurationProperties.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/configuration/properties/RunnerConfigurationProperties.java @@ -17,11 +17,6 @@ public class RunnerConfigurationProperties { */ private ExpressionRunnerProperties expressionRunner = new ExpressionRunnerProperties(); - /** - * Configuration for the field runner, including action, function, and namespace cache sizes. - */ - private FieldRunnerProperties fieldRunner = new FieldRunnerProperties(); - /** * Configuration specific to the expression runner component. */ @@ -36,36 +31,6 @@ public static class ExpressionRunnerProperties { } - /** - * Configuration specific to the field runner component. - */ - @Data - @ConfigurationProperties(prefix = "netgrif.engine.runner.field-runner") - public static class FieldRunnerProperties { - - /** - * The size of the cache used for handling field runner actions. - */ - private int actionCacheSize = 500; - - /** - * The size of the cache used for managing field runner functions. - */ - private int functionsCacheSize = 500; - - /** - * The size of the cache used for managing global Petri net functions. - */ - private int globalFunctionsCacheSize = 500; - - - /** - * The size of pages used for caching functions when processing large sets of data. - * This property determines the maximum number of functions to include in a single page during caching operations. - * Default value is 500. - */ - private int functionCachingPageSize = 500; - } /** * Configuration specific to the application runner component. diff --git a/application-engine/src/main/java/com/netgrif/application/engine/petrinet/service/PetriNetService.java b/application-engine/src/main/java/com/netgrif/application/engine/petrinet/service/PetriNetService.java index 4f0c590019..40c90ca20c 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/petrinet/service/PetriNetService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/petrinet/service/PetriNetService.java @@ -5,6 +5,10 @@ import com.netgrif.application.engine.objects.auth.domain.ActorTransformer; import com.netgrif.application.engine.configuration.properties.CacheConfigurationProperties; import com.netgrif.application.engine.files.minio.StorageConfigurationProperties; +import com.netgrif.application.engine.objects.petrinet.domain.PetriNet; +import com.netgrif.application.engine.objects.petrinet.domain.PetriNetSearch; +import com.netgrif.application.engine.objects.petrinet.domain.Transition; +import com.netgrif.application.engine.objects.petrinet.domain.VersionType; import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService; import com.netgrif.application.engine.petrinet.web.responsebodies.ArcImportReference; import com.netgrif.application.engine.objects.auth.domain.LoggedUser; @@ -15,10 +19,6 @@ import com.netgrif.application.engine.objects.event.events.petrinet.ProcessDeployEvent; import com.netgrif.application.engine.importer.service.Importer; import com.netgrif.application.engine.auth.service.GroupService; -import com.netgrif.application.engine.objects.petrinet.domain.PetriNet; -import com.netgrif.application.engine.objects.petrinet.domain.PetriNetSearch; -import com.netgrif.application.engine.objects.petrinet.domain.Transition; -import com.netgrif.application.engine.objects.petrinet.domain.VersionType; import com.netgrif.application.engine.objects.petrinet.domain.dataset.logic.action.Action; import com.netgrif.application.engine.petrinet.domain.dataset.logic.action.FieldActionsRunner; import com.netgrif.application.engine.objects.petrinet.domain.events.EventPhase; @@ -633,7 +633,7 @@ protected void deletePetriNet(String processId, LoggedUser loggedUser, boolean f publisher.publishEvent(new ProcessDeleteEvent(petriNet, EventPhase.PRE)); repository.deleteBy_id(petriNet.getObjectId()); evictCache(petriNet); - functionCacheService.reloadCachedFunctions(petriNet); + functionCacheService.reloadCachedGlobalFunctions(petriNet); if (petriNet.isDefaultVersion()) { PetriNet processToMakeDefault = self.getLatestVersionByIdentifier(petriNet.getIdentifier()); if (processToMakeDefault != null) { @@ -669,4 +669,5 @@ protected T requireNonNull(T obj, Object... item) { } return obj; } + } diff --git a/application-engine/src/main/java/com/netgrif/application/engine/petrinet/service/interfaces/IPetriNetService.java b/application-engine/src/main/java/com/netgrif/application/engine/petrinet/service/interfaces/IPetriNetService.java index f7b063c2d4..dc1505354d 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/petrinet/service/interfaces/IPetriNetService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/petrinet/service/interfaces/IPetriNetService.java @@ -1,10 +1,7 @@ package com.netgrif.application.engine.petrinet.service.interfaces; import com.netgrif.application.engine.objects.auth.domain.LoggedUser; -import com.netgrif.application.engine.objects.petrinet.domain.PetriNet; -import com.netgrif.application.engine.objects.petrinet.domain.PetriNetSearch; -import com.netgrif.application.engine.objects.petrinet.domain.Transition; -import com.netgrif.application.engine.objects.petrinet.domain.VersionType; +import com.netgrif.application.engine.objects.petrinet.domain.*; import com.netgrif.application.engine.objects.petrinet.domain.dataset.Field; import com.netgrif.application.engine.objects.petrinet.domain.dataset.logic.action.Action; import com.netgrif.application.engine.objects.petrinet.domain.throwable.MissingIconKeyException; diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/FieldActionsCacheService.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/FieldActionsCacheService.java index abc9ec09fb..e477ead50e 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/FieldActionsCacheService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/FieldActionsCacheService.java @@ -1,7 +1,7 @@ package com.netgrif.application.engine.workflow.service; -import com.netgrif.application.engine.configuration.properties.RunnerConfigurationProperties; -import com.netgrif.application.engine.elastic.service.executors.MaxSizeHashMap; +import com.netgrif.application.engine.configuration.CacheMapKeys; +import com.netgrif.application.engine.configuration.properties.CacheConfigurationProperties; import com.netgrif.application.engine.event.IGroovyShellFactory; import com.netgrif.application.engine.objects.petrinet.domain.PetriNet; import com.netgrif.application.engine.objects.petrinet.domain.Function; @@ -14,6 +14,8 @@ import groovy.lang.GroovyShell; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -27,19 +29,16 @@ @Slf4j public class FieldActionsCacheService implements IFieldActionsCacheService { - private final RunnerConfigurationProperties.FieldRunnerProperties properties; + private final CacheConfigurationProperties properties; + private final CacheManager cacheManager; private IPetriNetService petriNetService; - private Map actionsCache; - private Map> globalFunctionsCache; - private Map functionsCache; + private final GroovyShell shell; - public FieldActionsCacheService(RunnerConfigurationProperties.FieldRunnerProperties properties, IGroovyShellFactory shellFactory) { + public FieldActionsCacheService(CacheConfigurationProperties properties, CacheManager cacheManager, IGroovyShellFactory shellFactory) { this.properties = properties; - this.actionsCache = Collections.synchronizedMap(new MaxSizeHashMap<>(properties.getActionCacheSize())); - this.functionsCache = Collections.synchronizedMap(new MaxSizeHashMap<>(properties.getFunctionsCacheSize())); - this.globalFunctionsCache = Collections.synchronizedMap(new MaxSizeHashMap<>(properties.getGlobalFunctionsCacheSize())); + this.cacheManager = cacheManager; this.shell = shellFactory.getGroovyShell(); } @@ -59,39 +58,62 @@ public void cachePetriNetFunctions(PetriNet petriNet) { .map(function -> CachedFunction.build(shell, function)) .collect(Collectors.toList()); + Cache globalFunctionsCache = getRequiredCache(CacheMapKeys.GLOBAL_FUNCTIONS); + if (!functions.isEmpty()) { evaluateCachedFunctions(functions); globalFunctionsCache.put(petriNet.getIdentifier(), functions); } else { - globalFunctionsCache.remove(petriNet.getIdentifier()); + globalFunctionsCache.evictIfPresent(petriNet.getIdentifier()); } } @Override - public void reloadCachedFunctions(PetriNet petriNet) { - globalFunctionsCache.remove(petriNet.getIdentifier()); - cachePetriNetFunctions(petriNetService.getDefaultVersionByIdentifier(petriNet.getIdentifier())); + public void reloadCachedGlobalFunctions(String processIdentifier) { + getRequiredCache(CacheMapKeys.GLOBAL_FUNCTIONS).evictIfPresent(processIdentifier); + PetriNet petriNet = petriNetService.getDefaultVersionByIdentifier(processIdentifier); + if (petriNet != null) { + cachePetriNetFunctions(petriNet); + } + } + + @Override + public void reloadCachedGlobalFunctions(PetriNet petriNet) { + if (petriNet != null) { + this.reloadCachedGlobalFunctions(petriNet.getIdentifier()); + } } @Override public Closure getCompiledAction(Action action, boolean shouldRewriteCachedActions) { String stringId = action.getId().toString(); - if (shouldRewriteCachedActions || !actionsCache.containsKey(stringId)) { + Cache actionsCache = getRequiredCache(CacheMapKeys.ACTIONS); + + Cache.ValueWrapper wrapper = actionsCache.get(stringId); + if (shouldRewriteCachedActions || wrapper == null) { Closure code = (Closure) shell.evaluate("{-> " + action.getDefinition() + "}"); actionsCache.put(stringId, code); + return code; } - return actionsCache.get(stringId); + return (Closure) wrapper.get(); } @Override - public List getCachedFunctions(List functions) { + public List getCachedFunctions(List functions) { List cachedFunctions = new ArrayList<>(functions.size()); - functions.forEach(function -> { - if (!functionsCache.containsKey(function.getStringId())) { - functionsCache.put(function.getStringId(), CachedFunction.build(shell, function)); + Cache functionsCache = getRequiredCache(CacheMapKeys.FUNCTIONS); + + for (Function function : functions) { + Cache.ValueWrapper wrapper = functionsCache.get(function.getStringId()); + CachedFunction cached; + if (wrapper == null) { + cached = CachedFunction.build(shell, function); + functionsCache.put(function.getStringId(), cached); + } else { + cached = (CachedFunction) wrapper.get(); } - cachedFunctions.add(functionsCache.get(function.getStringId())); - }); + cachedFunctions.add(cached); + } return cachedFunctions; } @@ -156,21 +178,35 @@ private String stringifyParameterTypes(Class[] a) { @Override public Map> getGlobalFunctionsCache() { - return new HashMap<>(globalFunctionsCache); + Object nativeCache = getRequiredCache(CacheMapKeys.GLOBAL_FUNCTIONS).getNativeCache(); + if (nativeCache instanceof Map map) { + @SuppressWarnings("unchecked") + Map> typedMap = (Map>) map; + return new HashMap<>(typedMap); + } + return Collections.emptyMap(); } @Override public void clearActionCache() { - this.actionsCache = Collections.synchronizedMap(new MaxSizeHashMap<>(properties.getActionCacheSize())); + getRequiredCache(CacheMapKeys.ACTIONS).clear(); } @Override public void clearGlobalFunctionCache() { - this.globalFunctionsCache = Collections.synchronizedMap(new MaxSizeHashMap<>(properties.getGlobalFunctionsCacheSize())); + getRequiredCache(CacheMapKeys.GLOBAL_FUNCTIONS).clear(); } @Override public void clearFunctionCache() { - this.functionsCache = Collections.synchronizedMap(new MaxSizeHashMap<>(properties.getFunctionsCacheSize())); + getRequiredCache(CacheMapKeys.FUNCTIONS).clear(); + } + + private Cache getRequiredCache(String name) { + Cache cache = cacheManager.getCache(name); + if (cache == null) { + throw new IllegalStateException("Cache '" + name + "' is not configured"); + } + return cache; } } diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java index 162c76b044..218e6facfa 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java @@ -287,7 +287,7 @@ public CreateCaseEventOutcome createCase(String netId, String title, String colo public CreateCaseEventOutcome createCaseByIdentifier(String identifier, String title, String color, LoggedUser user, Locale locale, Map params) { PetriNet net = petriNetService.getDefaultVersionByIdentifier(identifier); if (net == null) { - throw new IllegalArgumentException("Petri net with identifier [" + identifier + "] does not exist."); + throw new IllegalArgumentException("Petri net with identifier [" + identifier + "] does not have default version."); } return this.createCase(net.getStringId(), title != null && !title.equals("") ? title : net.getDefaultCaseName().getTranslation(locale), color, user, params); } diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IFieldActionsCacheService.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IFieldActionsCacheService.java index 9e868e3aa6..e71f0f37a1 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IFieldActionsCacheService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IFieldActionsCacheService.java @@ -13,7 +13,9 @@ public interface IFieldActionsCacheService { void cachePetriNetFunctions(PetriNet petriNet); - void reloadCachedFunctions(PetriNet petriNet); + void reloadCachedGlobalFunctions(String petriNetId); + + void reloadCachedGlobalFunctions(PetriNet petriNet); Closure getCompiledAction(Action action, boolean shouldRewriteCachedActions); diff --git a/application-engine/src/main/resources/application.yaml b/application-engine/src/main/resources/application.yaml index 99a9c01581..d95cf45588 100644 --- a/application-engine/src/main/resources/application.yaml +++ b/application-engine/src/main/resources/application.yaml @@ -118,6 +118,7 @@ netgrif: prometheus: metric exposure: exclude: shutdown + include: "*" endpoint: health: show-details: when_authorized diff --git a/docs/process/functions.md b/docs/process/functions.md index 3d1eb4dcd1..a597343760 100644 --- a/docs/process/functions.md +++ b/docs/process/functions.md @@ -4,7 +4,7 @@ This document contains information about new Petriflow functions introduced in N ## Overview -Petriflow functions provide opportunity to declare your own functions without any changes in applications code. +Petriflow functions provide opportunity to declare your own functions without any changes in applications code. ### Scopes diff --git a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/petrinet/domain/version/Version.java b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/petrinet/domain/version/Version.java index 50dd1f5481..2a54e10fde 100644 --- a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/petrinet/domain/version/Version.java +++ b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/petrinet/domain/version/Version.java @@ -56,7 +56,7 @@ public void increment(VersionType type) { * Compares this version to the other version * * @param other other version to be compared with - * @return 0 if the versions equal, <0 if this is lower than other, >0 if this is higher than other + * @return 0 if the versions equal, < 0 if this is lower than other, > 0 if this is higher than other */ public int compareTo(Version other) { if (this.major != other.major) {