Skip to content
Open
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
28904f6
SAK-00000 EntityBroker remove reflectutils usage
ottenhoff Oct 2, 2025
71025bc
NOJIRA EntityBroker replace reflectutils encoding
ottenhoff Oct 2, 2025
20a522b
SAK-00000 EntityBroker remove reflectutils managers
ottenhoff Oct 2, 2025
dc3b23e
SAK-00000 EntityBroker replace reflectutils annotations
ottenhoff Oct 2, 2025
0e26aa2
SAK-00000 EntityBroker replace reflectutils helpers
ottenhoff Oct 2, 2025
e1d41aa
NOJIRA EntityBroker remove reflectutils utilities
ottenhoff Oct 2, 2025
480acb1
NOJIRA EntityBroker replace reflectutils in dev helper
ottenhoff Oct 2, 2025
f36f6ce
NOJIRA EntityBroker replace reflectutils in handlers
ottenhoff Oct 2, 2025
731adf3
NOJIRA Kernel remove reflectutils helpers
ottenhoff Oct 2, 2025
7c53022
NOJIRA core Remove reflectutils dependency
ottenhoff Oct 2, 2025
98c6b00
working build
Oct 2, 2025
c53de58
jsonp protect
Oct 2, 2025
f3ea268
minor
Oct 2, 2025
98b185f
use more spring
Oct 2, 2025
63d4cbf
fix tests
Oct 2, 2025
13bc2f9
fix commons json encoding
Oct 3, 2025
fcf954b
restore test
Oct 3, 2025
e133972
root encoding issue
Oct 3, 2025
2cf0102
fix commons and json encoding issues
Oct 3, 2025
3f63c68
fix tests in kernel
Oct 3, 2025
b279107
Coderabbit
Oct 3, 2025
6833bab
coderabbit
Oct 3, 2025
3c895cc
earle comment
Oct 3, 2025
e979696
more coderabbit
Oct 3, 2025
a9fb337
Delete JDK21-migration-plan.md
ottenhoff Oct 3, 2025
2c4b02b
Merge branch 'master' into codex/remove-reflectutils-library-from-cod…
ottenhoff Oct 4, 2025
4ee27eb
coderabbit
Oct 4, 2025
6ab431c
more coderabbit
Oct 4, 2025
7203edb
coderabbit
Oct 5, 2025
7edc4d3
Merge branch 'master' into codex/remove-reflectutils-library-from-cod…
ottenhoff Oct 8, 2025
c35d17a
Merge branch 'master' into codex/remove-reflectutils-library-from-cod…
ottenhoff Oct 24, 2025
de98b57
entitybroker remove reflection from site owner update (#55)
ottenhoff Oct 24, 2025
bd35301
NOJIRA entitybroker remove separateIdEid flag (#56)
ottenhoff Oct 24, 2025
eeb0799
hand-drawing XML :(
Oct 24, 2025
dcafc65
nits
Oct 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions content-review/impl/turnitin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,6 @@
<groupId>org.sakaiproject.common</groupId>
<artifactId>sakai-common-api</artifactId>
</dependency>
<dependency>
<groupId>org.sakaiproject</groupId>
<artifactId>reflectutils</artifactId>
<version>${reflectutils.version}</version>
</dependency>
<!-- Apache commons dependencies -->
<dependency>
<groupId>org.apache.poi</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.TimeZone;
import java.util.Map.Entry;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
Expand All @@ -50,11 +51,10 @@

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.io.IOUtils;

import org.azeckoski.reflectutils.transcoders.XMLTranscoder;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.sakaiproject.content.api.ContentResource;
import org.sakaiproject.contentreview.exception.SubmissionException;
Expand Down Expand Up @@ -177,24 +177,12 @@ public static String getMD5(String md5_string) throws NoSuchAlgorithmException {
return md5;
}

public static Map callTurnitinReturnMap(String apiURL, Map<String,Object> parameters,
String secretKey, int timeout, Proxy proxy) throws TransientSubmissionException, SubmissionException
{
XMLTranscoder xmlt = new XMLTranscoder();

try (InputStream inputStream = callTurnitinReturnInputStream(apiURL, parameters, secretKey, timeout, proxy, false)) {
Map togo = xmlt.decode(IOUtils.toString(inputStream));
log.debug("Turnitin Result Payload: " + togo);
return togo;
} catch (Exception t) {
// Could be 'java.lang.IllegalArgumentException: xml cannot be null or empty' from IO errors
throw new TransientSubmissionException ("Cannot parse Turnitin response. Assuming call was unsuccessful", t);
}
}

public static Document callTurnitinReturnDocument(String apiURL, Map<String,Object> parameters,
public static Map<String, Object> callTurnitinReturnMap(String apiURL, Map<String,Object> parameters,
String secretKey, int timeout, Proxy proxy) throws TransientSubmissionException, SubmissionException {
return callTurnitinReturnDocument(apiURL, parameters, secretKey, timeout, proxy, false);
Document document = callTurnitinReturnDocument(apiURL, parameters, secretKey, timeout, proxy, false);
Map<String, Object> togo = flattenResponse(document);
log.debug("Turnitin Result Payload: {}", togo);
return togo;
}

public static String buildTurnitinURL(String apiURL, Map<String,Object> parameters, String secretKey) {
Expand Down Expand Up @@ -257,35 +245,89 @@ public static String buildTurnitinURL(String apiURL, Map<String,Object> paramete
return sb.toString();
}

public static Document callTurnitinReturnDocument(String apiURL, Map<String,Object> parameters,
String secretKey, int timeout, Proxy proxy, boolean isMultipart) throws TransientSubmissionException, SubmissionException {
InputStream inputStream = callTurnitinReturnInputStream(apiURL, parameters, secretKey, timeout, proxy, isMultipart);
public static Document callTurnitinReturnDocument(String apiURL, Map<String,Object> parameters,
String secretKey, int timeout, Proxy proxy, boolean isMultipart) throws TransientSubmissionException, SubmissionException {
InputStream inputStream = callTurnitinReturnInputStream(apiURL, parameters, secretKey, timeout, proxy, isMultipart);

BufferedReader in;
in = new BufferedReader(new InputStreamReader(inputStream));
Document document = null;
try {
try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream))) {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder parser = documentBuilderFactory.newDocumentBuilder();
DocumentBuilder parser = documentBuilderFactory.newDocumentBuilder();
document = parser.parse(new org.xml.sax.InputSource(in));
}
catch (ParserConfigurationException pce){
} catch (ParserConfigurationException pce) {
log.error("parser configuration error: " + pce.getMessage());
throw new TransientSubmissionException ("Parser configuration error", pce);
throw new TransientSubmissionException("Parser configuration error", pce);
} catch (Exception t) {
throw new TransientSubmissionException ("Cannot parse Turnitin response. Assuming call was unsuccessful", t);
throw new TransientSubmissionException("Cannot parse Turnitin response. Assuming call was unsuccessful", t);
}

if (log.isDebugEnabled()) {
log.debug(" Result from call: " + Xml.writeDocumentToString(document));
log.debug("Result from call: {}", Xml.writeDocumentToString(document));

return document;
}

return document;
}
private static Map<String, Object> flattenResponse(Document document) {
Map<String, Object> response = new LinkedHashMap<>();
if (document == null) {
return response;
}
Element root = document.getDocumentElement();
if (root == null) {
return response;
}
NodeList children = root.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
Element element = (Element) node;
mergeValue(response, element.getNodeName(), extractValue(element));
}
return response;
}

private static Object extractValue(Element element) {
NodeList childNodes = element.getChildNodes();
List<Element> elementChildren = new ArrayList<>();
for (int i = 0; i < childNodes.getLength(); i++) {
Node node = childNodes.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
elementChildren.add((Element) node);
}
}
if (elementChildren.isEmpty()) {
String text = element.getTextContent();
return text == null ? "" : text.trim();
}
Map<String, Object> nested = new LinkedHashMap<>();
for (Element child : elementChildren) {
mergeValue(nested, child.getNodeName(), extractValue(child));
}
return nested;
}

private static void mergeValue(Map<String, Object> target, String key, Object value) {
if (target.containsKey(key)) {
Object existing = target.get(key);
if (existing instanceof List) {
@SuppressWarnings("unchecked")
List<Object> list = (List<Object>) existing;
list.add(value);
} else {
List<Object> list = new ArrayList<>();
list.add(existing);
list.add(value);
target.put(key, list);
}
} else {
target.put(key, value);
}
}

public static InputStream callTurnitinReturnInputStream(String apiURL, Map<String,Object> parameters,
String secretKey, int timeout, Proxy proxy, boolean isMultipart) throws TransientSubmissionException, SubmissionException {
InputStream togo = null;
public static InputStream callTurnitinReturnInputStream(String apiURL, Map<String,Object> parameters,
String secretKey, int timeout, Proxy proxy, boolean isMultipart) throws TransientSubmissionException, SubmissionException {
InputStream togo = null;

StringBuilder apiDebugSB = new StringBuilder();

Expand Down
1 change: 0 additions & 1 deletion docker/tomcat/conf/catalina.properties
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,6 @@ reactive-streams-*.jar,\
reactor-core-*.jar,\
recaptcha4j-*.jar,\
recordrtc-*.jar,\
reflectutils-*.jar,\
regenerator-runtime-*.jar,\
rhino-*.jar,\
rome-*.jar,\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,18 @@ public boolean isDataOnly() {
return dataOnly;
}

/**
* No-arg constructor required for serialization
*/
public EntityData() {
// Default constructor for serialization frameworks
}

/**
* Minimal constructor - used for most basic cases<br/>
* Use the setters to add in properties or the entity if desired
*
* @param reference a globally unique reference to an entity,
*
* @param reference a globally unique reference to an entity,
* consists of the entity prefix and id (e.g. /prefix/id)
* @param entityDisplayTitle a string which is suitable for display and provides a short summary of the entity,
* typically 100 chars or less, this may be the name or title of the entity represented by an entity
Expand Down
4 changes: 0 additions & 4 deletions entitybroker/core-providers/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@
<groupId>org.sakaiproject.profile2</groupId>
<artifactId>profile2-api</artifactId>
</dependency>
<dependency>
<groupId>org.sakaiproject</groupId>
<artifactId>reflectutils</artifactId>
</dependency>
<dependency>
<groupId>org.sakaiproject.edu-services.course-management</groupId>
<artifactId>coursemanagement-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.sakaiproject.entitybroker.providers;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand All @@ -34,8 +35,6 @@

import org.apache.commons.lang3.StringUtils;

import org.azeckoski.reflectutils.ReflectUtils;

import org.sakaiproject.authz.api.AuthzGroup;
import org.sakaiproject.authz.api.AuthzGroupService;
import org.sakaiproject.authz.api.AuthzPermissionException;
Expand Down Expand Up @@ -90,6 +89,7 @@
import org.sakaiproject.tool.api.Tool;
import org.sakaiproject.util.api.FormattedText;
import org.sakaiproject.util.comparator.AlphaNumericComparator;
import org.springframework.util.ReflectionUtils;


/**
Expand Down Expand Up @@ -121,6 +121,19 @@ public String getEntityPrefix() {
return PREFIX;
}

private void setCreatedUserId(Site site, String ownerId) {
Field field = ReflectionUtils.findField(site.getClass(), "m_createdUserId");
if (field == null) {
throw new IllegalStateException("Unable to locate the site owner field");
}
ReflectionUtils.makeAccessible(field);
try {
ReflectionUtils.setField(field, site, ownerId);
} catch (IllegalArgumentException e) {
throw new IllegalStateException("Unable to set the site owner field", e);
}
}

private static final String GROUP_PROP_WSETUP_CREATED = "group_prop_wsetup_created";

/** Property to set the default page size for lists of entities. */
Expand Down Expand Up @@ -855,7 +868,7 @@ public String createEntity(EntityReference ref, Object entity, Map<String, Objec
if (ownerID == null) {
throw new IllegalArgumentException("Invalid userId supplied for owner of site: " + ownerID);
}
ReflectUtils.getInstance().setFieldValue(s, "m_createdUserId", ownerID);
setCreatedUserId(s, ownerID);
}

// attempt to set provider ID as requested. rules are:
Expand Down Expand Up @@ -1093,7 +1106,7 @@ public void updateEntity(EntityReference ref, Object entity, Map<String, Object>
throw new IllegalArgumentException(
"Invalid userId supplied for owner of site: " + site.getOwner());
}
ReflectUtils.getInstance().setFieldValue(s, "m_createdUserId", ownerUserId);
setCreatedUserId(s, ownerUserId);
}

// new publish status
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.sakaiproject.entitybroker.providers;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand All @@ -24,9 +25,6 @@
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import org.azeckoski.reflectutils.FieldUtils;
import org.azeckoski.reflectutils.ReflectUtils;

import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.entity.api.ResourcePropertiesEdit;
import org.sakaiproject.entitybroker.DeveloperHelperService;
Expand All @@ -52,6 +50,7 @@
import org.sakaiproject.user.api.UserLockedException;
import org.sakaiproject.user.api.UserNotDefinedException;
import org.sakaiproject.user.api.UserPermissionException;
import org.springframework.util.ReflectionUtils;

/**
* Entity Provider for users
Expand Down Expand Up @@ -383,16 +382,16 @@ private boolean isUsingSameIdEid() {
String config = developerHelperService.getConfigurationSetting("[email protected]", (String)null);
if (config != null) {
try {
usesSeparateIdEid = ReflectUtils.getInstance().convert(config, Boolean.class);
} catch (UnsupportedOperationException e) {
usesSeparateIdEid = parseBoolean(config);
} catch (IllegalArgumentException e) {
// oh well
usesSeparateIdEid = null;
}
}
if (usesSeparateIdEid == null) {
// could not get the stupid setting so attempt to check the service itself
try {
usesSeparateIdEid = FieldUtils.getInstance().getFieldValue(userDirectoryService, "m_separateIdEid", Boolean.class);
usesSeparateIdEid = readBooleanField(userDirectoryService, "m_separateIdEid");
} catch (RuntimeException e) {
// no luck here
usesSeparateIdEid = null;
Expand All @@ -403,6 +402,43 @@ private boolean isUsingSameIdEid() {
return ! usesSeparateIdEid.booleanValue();
}

private Boolean readBooleanField(Object target, String fieldName) {
Field field = ReflectionUtils.findField(target.getClass(), fieldName);
if (field == null) {
return null;
}
ReflectionUtils.makeAccessible(field);
try {
Object value = ReflectionUtils.getField(field, target);
if (value instanceof Boolean) {
return (Boolean) value;
}
if (value instanceof String) {
return parseBoolean((String) value);
}
return (value != null) ? Boolean.valueOf(value.toString()) : null;
} catch (IllegalArgumentException e) {
return null;
}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 3, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unwrap Spring proxies before resolving the field

When userDirectoryService is delivered as a JDK dynamic proxy (a common case for interface-based beans), target.getClass() is com.sun.proxy.$Proxy…, which has no m_separateIdEid field. ReflectionUtils.findField(...) therefore returns null, the fallback never fires, and usesSeparateIdEid is forced to false. Previously FieldUtils handled proxy unwrapping, so this is a regression on any deployment using JDK proxies and relying on the runtime field value.

Please resolve the user class before the lookup, e.g.

-import org.springframework.util.ReflectionUtils;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ReflectionUtils;-        Field field = ReflectionUtils.findField(target.getClass(), fieldName);
+        Class<?> targetClass = ClassUtils.getUserClass(target);
+        Field field = ReflectionUtils.findField(targetClass, fieldName);

This preserves the old behavior across both proxied and non-proxied services.

Copy link
Contributor

@ern ern Oct 3, 2025

Choose a reason for hiding this comment

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

userDirectoryService should never be deserialized, should be skipped

Copy link
Contributor

Choose a reason for hiding this comment

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

@ern, you're absolutely right that userDirectoryService should never be deserialized—it's injected as a Spring-managed service bean, not serialized/deserialized.

However, CodeRabbit's concern about proxies refers to Spring's runtime AOP proxies, not serialization proxies. When Spring wraps userDirectoryService in a JDK dynamic proxy (common for interface-based beans with transactions/AOP), calling target.getClass() returns the proxy class (e.g., $Proxy123) instead of the actual implementation class that contains the m_separateIdEid field. This would cause ReflectionUtils.findField(...) to return null.

CodeRabbit's suggestion to use ClassUtils.getUserClass(target) unwraps the Spring proxy to get the real implementation class, ensuring the field lookup works correctly whether the service is proxied or not. This is a valid concern distinct from serialization.

Would you like to apply the proxy-unwrapping fix, or is the userDirectoryService guaranteed to never be proxied in your deployment?


private Boolean parseBoolean(String value) {
if (value == null) {
return null;
}
String normalized = value.trim();
if (normalized.isEmpty()) {
return null;
}
if ("true".equalsIgnoreCase(normalized)) {
return Boolean.TRUE;
}
if ("false".equalsIgnoreCase(normalized)) {
return Boolean.FALSE;
}
throw new IllegalArgumentException("Value is not a boolean: " + value);
}

/**
* Will check that a userId/eid is valid and will produce a valid userId from the check
*
Expand Down
Loading
Loading