Skip to content

[bot] Merge 25.8 to develop #6946

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 20, 2025
21 changes: 17 additions & 4 deletions api/src/org/labkey/api/action/BaseApiAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.labkey.api.util.MimeMap;
import org.labkey.api.util.Pair;
import org.labkey.api.util.ResponseHelper;
import org.labkey.api.util.StringUtilsLabKey;
import org.labkey.api.view.BadRequestException;
import org.labkey.api.view.NotFoundException;
import org.labkey.api.view.UnauthorizedException;
Expand All @@ -52,6 +53,7 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.SocketTimeoutException;
import java.time.Duration;
Expand Down Expand Up @@ -511,12 +513,23 @@ protected long getMaximumJsonInputLength()

private @Nullable JSONObject getJsonObject() throws IOException
{
HttpServletRequest request = getViewContext().getRequest();
if (request == null)
return null;

String characterEncoding = request.getCharacterEncoding();
if (characterEncoding == null)
characterEncoding = StringUtilsLabKey.DEFAULT_CHARSET.name();

long maxLength = getMaximumJsonInputLength();
try (Reader r = getViewContext().getRequest().getReader();
Reader jsonReader = maxLength > 0 ? new StrictBoundedReader(r, maxLength) : r)

// Issue 53699: Use request.getInputStream() instead of request.getReader() to
// avoid BufferUnderflowException when processing multibyte character JSON payloads.
try (Reader streamReader = new InputStreamReader(request.getInputStream(), characterEncoding);
Reader jsonReader = maxLength > 0 ? new StrictBoundedReader(streamReader, maxLength) : streamReader)
{
JSONTokener tokener = new JSONTokener(r);
return tokener.more() ? new JSONObject(new JSONTokener(jsonReader)) : null;
JSONTokener tokener = new JSONTokener(jsonReader);
return tokener.more() ? new JSONObject(tokener) : null;
}
}

Expand Down
5 changes: 3 additions & 2 deletions api/src/org/labkey/api/data/validator/LengthValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ public String _validate(int rowNum, Object value)
{
if (value instanceof String s)
{
if (s.length() > scale)
return "Value is too long for column '" + _columnName + "', a maximum length of " + scale + " is allowed. The supplied value, '" + StringUtils.abbreviateMiddle(s, "...", 50) + "', was " + s.length() + " characters long.";
int charLength = Character.codePointCount(s, 0, s.length());
if (charLength > scale)
return "Value is too long for column '" + _columnName + "', a maximum length of " + scale + " is allowed. The supplied value, '" + StringUtils.abbreviateMiddle(s, "...", 50) + "', was " + charLength + " characters long.";
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ public Object getConstantValue(int i)
@Override
public boolean next() throws BatchValidationException
{
if (_context.getErrors().hasErrors())
return false;

// NOTE: we have to call mark() before we call next() if we want the 'next' row to be cached
if (useMark)
_unwrapped.mark(); // unwrapped _delegate
Expand Down
82 changes: 72 additions & 10 deletions assay/src/org/labkey/assay/plate/PlateImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.json.JSONObject;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.labkey.api.assay.plate.Plate;
import org.labkey.api.assay.plate.PlateCustomField;
import org.labkey.api.assay.plate.PlateService;
Expand All @@ -42,6 +46,8 @@
import org.labkey.api.security.User;
import org.labkey.api.security.UserManager;
import org.labkey.api.util.GUID;
import org.labkey.api.util.JunitUtil;
import org.labkey.api.util.TestContext;
import org.labkey.api.util.UnexpectedException;
import org.labkey.api.view.ActionURL;
import org.labkey.assay.PlateController;
Expand All @@ -58,9 +64,7 @@
import java.util.List;
import java.util.Map;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

@RunWith(Enclosed.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class PlateImpl extends PropertySetImpl implements Plate, Cloneable
{
Expand Down Expand Up @@ -160,9 +164,9 @@ public static PlateImpl from(PlateBean bean)
Container container = ContainerManager.getForId(bean.getContainerId());
plate.setContainer(container);
plate.setCreated(bean.getCreated());
plate.setCreatedBy(UserManager.getUser(bean.getCreatedBy()));
plate.setCreatedBy(bean.getCreatedBy());
plate.setModified(bean.getModified());
plate.setModifiedBy(UserManager.getUser(bean.getModifiedBy()));
plate.setModifiedBy(bean.getModifiedBy());

// plate type and plate set objects
PlateType plateType = PlateManager.get().getPlateType(bean.getPlateType());
Expand Down Expand Up @@ -436,9 +440,9 @@ public JSONObject getCreatedBy()
return user != null ? user.getUserProps() : null;
}

public void setCreatedBy(User createdBy)
public void setCreatedBy(int createdBy)
{
_createdBy = createdBy.getUserId();
_createdBy = createdBy;
}

public Date getModified()
Expand All @@ -458,9 +462,9 @@ public JSONObject getModifiedBy()
return user != null ? user.getUserProps() : null;
}

public void setModifiedBy(User modifiedBy)
public void setModifiedBy(int modifiedBy)
{
_modifiedBy = modifiedBy.getUserId();
_modifiedBy = modifiedBy;
}

public String getDataFileId()
Expand Down Expand Up @@ -758,8 +762,17 @@ public boolean isNew()
return _rowId == null || _rowId <= 0;
}

public static final class TestCase
public static class TestCase extends Assert
{
private PlateSetImpl _plateSet;

@Before
public void setup() throws Exception
{
PlateSetImpl plateSet = new PlateSetImpl();
_plateSet = PlateManager.get().createPlateSet(JunitUtil.getTestContainer(), TestContext.get().getUser(), plateSet, null, null);
}

@Test
public void testIdentifierMatch()
{
Expand All @@ -781,5 +794,54 @@ public void testIdentifierMatch()
assertFalse("Expected plate to not match invalid name", plate.isIdentifierMatch(""));
assertFalse("Expected plate to not match invalid name", plate.isIdentifierMatch(null));
}

@Test
public void testFromInitializer()
{
String testContainerId = JunitUtil.getTestContainer().getId();
int testUserId = TestContext.get().getUser().getUserId();
Long plateTypeId = PlateManager.get().getPlateTypes().stream().findFirst().orElseThrow().getRowId();
Long plateSetId = _plateSet.getRowId();

PlateBean bean = new PlateBean();
Long expectedRowId = 1L;
String expectedName = "Test Name";
Boolean archived = null;
bean.setPlateType(plateTypeId);
bean.setPlateSet(plateSetId);
bean.setContainerId(testContainerId);
bean.setCreatedBy(testUserId);
bean.setModifiedBy(testUserId);
bean.setRowId(expectedRowId);
bean.setPlateId("test-id");
bean.setName(expectedName);
bean.setArchived(archived);

PlateImpl plate = PlateImpl.from(bean);
assertNotNull(plate.getCreatedBy());
assertEquals(testUserId, plate.getCreatedBy().getInt("id"));
assertNotNull(plate.getModifiedBy());
assertEquals(testUserId, plate.getModifiedBy().getInt("id"));
assertEquals(expectedName, plate.getName());
assertEquals(expectedRowId, plate.getRowId());
assertFalse(plate.isArchived());
assertFalse(plate.isNew());

// Issue 53633: Verify an invalid user does not cause an error initializing from bean
{
bean = new PlateBean();
bean.setPlateType(plateTypeId);
bean.setPlateSet(plateSetId);
bean.setContainerId(testContainerId);
bean.setCreatedBy(-1);
bean.setModifiedBy(-1);

plate = PlateImpl.from(bean);
assertNull(plate.getCreatedBy());
assertNull(plate.getModifiedBy());
assertFalse(plate.isArchived());
assertTrue(plate.isNew());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,11 @@
import org.labkey.test.categories.Daily;
import org.labkey.test.components.CustomizeView;
import org.labkey.test.components.assay.AssayConstants;
import org.labkey.test.components.ext4.Window;
import org.labkey.test.pages.query.ExecuteQueryPage;
import org.labkey.test.util.ApiPermissionsHelper;
import org.labkey.test.util.DataRegionTable;
import org.labkey.test.util.Ext4Helper;
import org.labkey.test.util.PermissionsHelper;
import org.labkey.test.util.StudyHelper;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebElement;

import java.io.File;
import java.util.List;
Expand Down Expand Up @@ -300,20 +296,8 @@ private void importAssayRun(File runFile, String assayName, String runName)
private void createDatasetCategory(String projectName, String name)
{
goToProjectHome(projectName);
goToManageViews();
Locator.linkWithText("Manage Categories").findElement(getDriver()).click();
_extHelper.waitForExtDialog("Manage Categories");
Window<?> categoryWindow = new Window.WindowFinder(getDriver()).withTitle("Manage Categories").waitFor();
categoryWindow.clickButton("New Category", 0);
WebElement newCategoryField = Locator.input("label").withAttributeContaining("id", "textfield").notHidden().waitForElement(getDriver(), WAIT_FOR_JAVASCRIPT);
actionClear(newCategoryField);
sleep(500);
newCategoryField.sendKeys(name);
sleep(500);
newCategoryField.sendKeys(Keys.ENTER);
waitForElement(Ext4Helper.Locators.window("Manage Categories").append("//div").withText(name));
clickButton("Done", 0);
_extHelper.waitForExtDialogToDisappear("Manage Categories");
goToManageViews()
.createCategory(name);
}

private String getCategory(String projectName, String datasetName)
Expand Down