Skip to content
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package com.netgrif.application.engine.configuration;

import com.netgrif.application.engine.configuration.cache.WorkerConcurrentCacheManager;
import com.netgrif.application.engine.configuration.properties.CacheConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.cache.interceptor.CacheResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import java.util.Set;


@Configuration
@EnableCaching
public class CacheConfiguration extends CachingConfigurerSupport {
Expand All @@ -24,7 +27,7 @@ public CacheConfiguration(CacheConfigurationProperties properties) {
@Primary
@Override
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager(properties.getAllCaches().toArray(String[]::new));
return new WorkerConcurrentCacheManager(Set.of(properties.getNamespaceFunctions()), properties.getAllCaches().toArray(String[]::new));
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.netgrif.application.engine.configuration.cache;

import org.springframework.cache.Cache;

import java.util.concurrent.Callable;

public class ReadOnlyCache implements Cache {

private final Cache cacheDelegate;

public ReadOnlyCache(Cache cacheDelegate) {
this.cacheDelegate = cacheDelegate;
}

@Override
public String getName() { return cacheDelegate.getName(); }

@Override
public Object getNativeCache() { return cacheDelegate.getNativeCache(); }

@Override
public ValueWrapper get(Object key) { return cacheDelegate.get(key); }

@Override
public <T> T get(Object key, Class<T> type) { return cacheDelegate.get(key, type); }

@Override
public <T> T get(Object key, Callable<T> loader) { return cacheDelegate.get(key, loader); }

@Override
public void put(Object key, Object value) { cacheDelegate.put(key, value); }

@Override
public void evict(Object key) {
throw new UnsupportedOperationException("Evict not allowed on worker for " + getName());
}

@Override
public void clear() {
throw new UnsupportedOperationException("Clear not allowed on worker for " + getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.netgrif.application.engine.configuration.cache;

import org.springframework.cache.Cache;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;

import java.util.Objects;
import java.util.Set;

public class WorkerConcurrentCacheManager extends ConcurrentMapCacheManager {
private final Set<String> readOnlyCacheNames;

public WorkerConcurrentCacheManager() {
super();
this.readOnlyCacheNames = Set.of();
}

public WorkerConcurrentCacheManager(Set<String> readOnlyCacheNames) {
super();
this.readOnlyCacheNames = Set.copyOf(
Objects.requireNonNull(readOnlyCacheNames, "readOnlyCacheNames must not be null.")
);
}

public WorkerConcurrentCacheManager(Set<String> readOnlyCacheNames, String... cacheNames) {
super(cacheNames);
this.readOnlyCacheNames = Set.copyOf(
Objects.requireNonNull(readOnlyCacheNames, "readOnlyCacheNames must not be null.")
);
}

@Override
protected Cache createConcurrentMapCache(String name) {
Cache base = super.createConcurrentMapCache(name);
if (readOnlyCacheNames != null && readOnlyCacheNames.contains(name)) {
return new ReadOnlyCache(base);
}
return base;
}
Comment on lines +31 to +38
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify ReadOnlyCache correctly prevents all write operations.

The wrapper logic correctly delegates wrapping to ReadOnlyCache for caches in the read-only set. Ensure that ReadOnlyCache properly blocks all mutating operations (put, evict, clear, etc.) to maintain the read-only guarantee.

Run the following script to examine the ReadOnlyCache implementation:


🏁 Script executed:

#!/bin/bash
# Description: Verify ReadOnlyCache blocks all write operations.

# Search for ReadOnlyCache class and its method implementations
ast-grep --pattern $'class ReadOnlyCache {
  $$$
}'

# Check for methods that throw UnsupportedOperationException
rg -nP -A3 'class ReadOnlyCache' --type=java
rg -nP 'UnsupportedOperationException' --type=java -g '**/ReadOnlyCache.java'

Length of output: 913


🏁 Script executed:

#!/bin/bash
# Display the full ReadOnlyCache implementation to verify write-blocking methods
sed -n '1,200p' application-engine/src/main/java/com/netgrif/application/engine/configuration/cache/ReadOnlyCache.java

Length of output: 1198


🏁 Script executed:

#!/bin/bash
rg -n 'putIfAbsent' -g 'application-engine/src/main/java/com/netgrif/application/engine/configuration/cache/ReadOnlyCache.java'

Length of output: 0


Enforce read-only behavior by blocking all mutating cache operations in ReadOnlyCache
ReadOnlyCache currently delegates put(Object,Object) and inherits default putIfAbsent(Object,Object), so write operations still succeed. Override both methods to throw UnsupportedOperationException to uphold the read-only guarantee.
File: application-engine/src/main/java/com/netgrif/application/engine/configuration/cache/ReadOnlyCache.java

🤖 Prompt for AI Agents
In
application-engine/src/main/java/com/netgrif/application/engine/configuration/cache/ReadOnlyCache.java
(around the class definition), the ReadOnlyCache currently delegates
put(Object,Object) and inherits putIfAbsent(Object,Object) which allows writes;
override both put(K,V) and putIfAbsent(K,V) to throw
UnsupportedOperationException to enforce true read-only behavior (ensure
signatures match the Cache interface used and include clear exception messages),
and run tests to confirm write attempts now fail.

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public class CacheConfigurationProperties {
*/
private String loadedModules = "loadedModules";

/**
* Default cache name for caching namespace functions.
*/
private String namespaceFunctions = "namespaceFunctions";

/**
* A list of additional custom cache names.
* Allows users to define their own cache names for specific use cases.
Expand All @@ -54,7 +59,7 @@ public class CacheConfigurationProperties {
* @return a {@link Set} of all cache names.
*/
public Set<String> getAllCaches() {
Set<String> caches = new LinkedHashSet<>(Arrays.asList(petriNetById, petriNetByIdentifier, petriNetNewest, petriNetCache, loadedModules));
Set<String> caches = new LinkedHashSet<>(Arrays.asList(petriNetById, petriNetByIdentifier, petriNetNewest, petriNetCache, loadedModules, namespaceFunctions));
caches.addAll(additional);
Comment on lines 61 to 63
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Guard against future drift and blank values

If users override namespaceFunctions in config, the read‑only wrapping must follow that value. Recommend passing this property into the CacheManager instead of relying on a constant. Also consider ignoring blank entries to avoid empty‑named caches.

Possible follow-up (see proposed constructor in WorkerConcurrentCacheManager and bean wiring in CacheConfiguration).

return caches;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
Expand All @@ -30,7 +33,6 @@ public class FieldActionsCacheService implements IFieldActionsCacheService {
private final RunnerConfigurationProperties.FieldRunnerProperties properties;

private IPetriNetService petriNetService;

private Map<String, Closure> actionsCache;
private Map<String, List<CachedFunction>> namespaceFunctionsCache;
private Map<String, CachedFunction> functionsCache;
Expand Down Expand Up @@ -96,6 +98,28 @@ public List<CachedFunction> getCachedFunctions(List<com.netgrif.application.engi
return cachedFunctions;
}

@Override
public void cacheAllPetriNetsFunctions() {
Pageable pageable = PageRequest.of(0, 500);
Page<PetriNet> page = petriNetService.getAll(pageable);

while (!page.isEmpty()) {
for (PetriNet petriNet : page) {
try {
cachePetriNetFunctions(petriNet);
} catch (Exception e) {
log.warn("Failed to cache functions for PetriNet id={}", petriNet.getStringId(), e);
}
}

if (!page.hasNext()) {
break;
}
pageable = pageable.next();
page = petriNetService.getAll(pageable);
}
}

@Override
public void evaluateFunctions(List<Function> functions) {
evaluateCachedFunctions(functions.stream().map(function -> CachedFunction.build(shell, function)).collect(Collectors.toList()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@ public interface IFieldActionsCacheService {

void clearNamespaceFunctionCache();

void cacheAllPetriNetsFunctions();

Comment on lines +30 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Define the contract (scope, version, and cache semantics) for the new bulk method.

Please document whether this:

  • processes only newest PetriNet versions or all historical ones,
  • clears stale entries for deleted PetriNets before rebuilding,
  • is intended to be long‑running and should be invoked off the request thread.

Consider a tighter name for consistency with the singular: cacheAllPetriNetFunctions().

Apply this diff to add Javadoc:

-    void cacheAllPetriNetsFunctions();
+    /**
+     * Pre-populates the namespace function cache for all PetriNets.
+     * Contract:
+     * - Only newest version per PetriNet identifier SHOULD be cached.
+     * - Stale cache entries for non-existing PetriNets SHOULD be cleared or overwritten.
+     * - This operation MAY be long-running; invoke off request threads.
+     */
+    void cacheAllPetriNetsFunctions();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void cacheAllPetriNetsFunctions();
/**
* Pre-populates the namespace function cache for all PetriNets.
* Contract:
* - Only newest version per PetriNet identifier SHOULD be cached.
* - Stale cache entries for non-existing PetriNets SHOULD be cleared or overwritten.
* - This operation MAY be long-running; invoke off request threads.
*/
void cacheAllPetriNetsFunctions();
🤖 Prompt for AI Agents
In
application-engine/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IFieldActionsCacheService.java
around lines 30-31, add Javadoc for the bulk caching method that defines its
contract: clarify whether it processes only the newest PetriNet versions or all
historical versions, state whether it clears stale entries for deleted PetriNets
before rebuilding the cache, and indicate that it is long-running and should be
called off the request thread (or provide async variant). Also rename the method
to cacheAllPetriNetFunctions() for consistency with the singular form and
reflect the chosen semantics (scope, version policy, cache eviction strategy,
and threading expectations) in the Javadoc summary and
parameter/return/exception notes.

void clearFunctionCache();
}
Loading