From f4a2e90debe8e15e3114abff5eece0339c1cd91b Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 4 Mar 2026 14:15:51 -0800 Subject: [PATCH 1/6] GitHub Issue 898: SampleFinder: MVFK over MVTC with special characters result in bad sql error --- .../api/data/MultiValuedRenderContext.java | 482 +++++++++--------- api/src/org/labkey/api/util/PageFlowUtil.java | 16 + core/package-lock.json | 8 +- core/package.json | 2 +- experiment/package-lock.json | 8 +- experiment/package.json | 2 +- 6 files changed, 270 insertions(+), 248 deletions(-) diff --git a/api/src/org/labkey/api/data/MultiValuedRenderContext.java b/api/src/org/labkey/api/data/MultiValuedRenderContext.java index 2217b7f3af7..9421c573ea6 100644 --- a/api/src/org/labkey/api/data/MultiValuedRenderContext.java +++ b/api/src/org/labkey/api/data/MultiValuedRenderContext.java @@ -1,238 +1,244 @@ -/* - * Copyright (c) 2010-2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.labkey.api.data; - -import org.apache.commons.beanutils.ConvertUtils; -import org.apache.commons.collections4.iterators.ArrayIterator; -import org.junit.Assert; -import org.junit.Test; -import org.labkey.api.exp.PropertyType; -import org.labkey.api.query.FieldKey; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; - -/** - * Wrapper for another {@link RenderContext} that allows multiple values to be rendered for a single row's column. - * Used in conjunction with {@link MultiValuedDisplayColumn}. - * User: adam - * Date: Sep 7, 2010 - * Time: 10:02:25 AM - */ -public class MultiValuedRenderContext extends RenderContextDecorator -{ - private final Map> _iterators = new HashMap<>(); - private final Map _currentValues = new HashMap<>(); - - public static final String VALUE_DELIMITER = "{@~^"; - public static final String VALUE_DELIMITER_REGEX = "\\Q" + VALUE_DELIMITER + "\\E"; - - public MultiValuedRenderContext(RenderContext ctx, Set requiredFieldKeys) - { - super(ctx); - - // For each required column (e.g., display value, rowId), retrieve the concatenated values, split them, and - // stash away an iterator of those values. - int length = -1; - Set nullFieldKeys = new HashSet<>(); - for (FieldKey fieldKey : requiredFieldKeys) - { - Object value = ctx.get(fieldKey); - if (value == null || "".equals(value)) - { - nullFieldKeys.add(fieldKey); - } - else - { - // Use -1 as the limit so that we pick up back-to-back delimiters as empty strings in the returned array, - // which lets us understand there were rows with nulls. - String[] values = value.toString().split(VALUE_DELIMITER_REGEX, -1); - if (length != -1 && values.length != length) - { - throw new IllegalStateException("Expected all columns to have the same number of values, but '" + fieldKey + "' has " + values.length + " and " + _iterators.keySet() + " had " + length); - } - length = values.length; - _iterators.put(fieldKey, new ArrayIterator<>(values)); - } - - for (FieldKey nullFieldKey : nullFieldKeys) - { - _iterators.put(nullFieldKey, new ArrayIterator<>(new String[length == -1 ? 0 : length])); - } - } - } - - // Advance all the iterators, if another value is present. Check that all iterators are in lock step. - public boolean next() - { - Boolean previousHasNext = null; - - for (Map.Entry> entry : _iterators.entrySet()) - { - Iterator iter = entry.getValue(); - boolean hasNext = iter.hasNext(); - - if (hasNext) - _currentValues.put(entry.getKey(), iter.next()); - - if (null == previousHasNext) - previousHasNext = hasNext; - else - assert previousHasNext == hasNext : "Mismatch in number of values for " + entry.getKey() + " compared with other fields: " + _iterators.keySet(); - } - - return null != previousHasNext && previousHasNext; - } - - @Override - public Object get(Object key) - { - Object value = _currentValues.get(key); - - if (null != value) - { - // empty string values map to null - if ("".equals(value)) - value = null; - - if (getFieldMap() != null) - { - ColumnInfo columnInfo = getFieldMap().get(key); - if (columnInfo != null && columnInfo.getPropertyType() == PropertyType.MULTI_CHOICE && value instanceof String strVal) - { - // Multi-choice values array is converted to string: "{value1,value2,...}", so strip off the braces before converting - if (strVal.startsWith("{") && strVal.endsWith("}")) - return columnInfo.convert(strVal.substring(1, strVal.length() - 1)); - } - // The value was concatenated with others, so it's become a string. - // Do conversion to switch it back to the expected type. - if (value != null && columnInfo != null && !columnInfo.getJavaClass().isInstance(value)) - { - value = columnInfo.convert(value); - } - } - } - else - { - value = super.get(key); - } - - return value; - } - - public static class TestCase extends Assert - { - private final FieldKey _fk1 = FieldKey.fromParts("Parent", "Child"); - private final FieldKey _fk2 = FieldKey.fromParts("Standalone"); - private final FieldKey _otherFK = FieldKey.fromParts("NotInRow"); - - @Test - public void testMatchingValues() - { - Set fieldKeys = new HashSet<>(); - fieldKeys.add(_fk1); - fieldKeys.add(_fk2); - Map values = new HashMap<>(); - values.put(_fk1, "1" + VALUE_DELIMITER + "2" + VALUE_DELIMITER + "3"); - values.put(_fk2, "a" + VALUE_DELIMITER + "b" + VALUE_DELIMITER + "c"); - MultiValuedRenderContext mvContext = new MultiValuedRenderContext(new TestRenderContext(values), fieldKeys); - assertTrue(mvContext.next()); - assertEquals(1, mvContext.get(_fk1)); - assertEquals("a", mvContext.get(_fk2)); - assertTrue(mvContext.next()); - assertEquals(2, mvContext.get(_fk1)); - assertEquals("b", mvContext.get(_fk2)); - assertTrue(mvContext.next()); - assertEquals(3, mvContext.get(_fk1)); - assertEquals("c", mvContext.get(_fk2)); - assertFalse(mvContext.next()); - } - - @Test - public void testMissingColumn() - { - // Be sure that if there's a column that couldn't be found, we don't blow up - Set fieldKeys = new HashSet<>(); - fieldKeys.add(_fk1); - fieldKeys.add(_fk2); - fieldKeys.add(_otherFK); - Map values = new HashMap<>(); - values.put(_fk1, "1" + VALUE_DELIMITER + "2" + VALUE_DELIMITER + "3"); - values.put(_fk2, "a" + VALUE_DELIMITER + "b" + VALUE_DELIMITER + "c"); - MultiValuedRenderContext mvContext = new MultiValuedRenderContext(new TestRenderContext(values), fieldKeys); - assertTrue(mvContext.next()); - assertEquals(1, mvContext.get(_fk1)); - assertEquals("a", mvContext.get(_fk2)); - assertNull(mvContext.get(_otherFK)); - assertTrue(mvContext.next()); - assertEquals(2, mvContext.get(_fk1)); - assertEquals("b", mvContext.get(_fk2)); - assertNull(mvContext.get(_otherFK)); - assertTrue(mvContext.next()); - assertEquals(3, mvContext.get(_fk1)); - assertEquals("c", mvContext.get(_fk2)); - assertNull(mvContext.get(_otherFK)); - assertFalse(mvContext.next()); - } - - @Test - public void testMismatchedValues() - { - Set fieldKeys = new HashSet<>(); - fieldKeys.add(_fk1); - fieldKeys.add(_fk2); - Map values = new HashMap<>(); - values.put(_fk1, "1" + VALUE_DELIMITER + "2" + VALUE_DELIMITER + "3"); - values.put(_fk2, "a" + VALUE_DELIMITER + "b" + VALUE_DELIMITER + "c" + VALUE_DELIMITER + "d"); - try - { - new MultiValuedRenderContext(new TestRenderContext(values), fieldKeys); - fail("Should have gotten an exception"); - } - catch (IllegalStateException ignored) {} - } - - private class TestRenderContext extends RenderContext - { - private final Map _values; - - public TestRenderContext(Map values) - { - _values = values; - } - - @Override - public Object get(Object key) - { - return _values.get(key); - } - - @Override - public Map getFieldMap() - { - Map result = new HashMap<>(); - ColumnInfo col1 = new BaseColumnInfo(_fk1, JdbcType.INTEGER); - result.put(_fk1, col1); - ColumnInfo col2 = new BaseColumnInfo(_fk2, JdbcType.VARCHAR); - result.put(_fk2, col2); - return result; - } - } - } -} +/* + * Copyright (c) 2010-2019 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.api.data; + +import org.apache.commons.beanutils.ConvertUtils; +import org.apache.commons.collections4.iterators.ArrayIterator; +import org.junit.Assert; +import org.junit.Test; +import org.labkey.api.exp.PropertyType; +import org.labkey.api.query.FieldKey; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * Wrapper for another {@link RenderContext} that allows multiple values to be rendered for a single row's column. + * Used in conjunction with {@link MultiValuedDisplayColumn}. + * User: adam + * Date: Sep 7, 2010 + * Time: 10:02:25 AM + */ +public class MultiValuedRenderContext extends RenderContextDecorator +{ + private final Map> _iterators = new HashMap<>(); + private final Map _currentValues = new HashMap<>(); + + public static final String VALUE_DELIMITER = "{@~^"; + public static final String VALUE_DELIMITER_REGEX = "\\Q" + VALUE_DELIMITER + "\\E"; + + public MultiValuedRenderContext(RenderContext ctx, Set requiredFieldKeys) + { + super(ctx); + + // For each required column (e.g., display value, rowId), retrieve the concatenated values, split them, and + // stash away an iterator of those values. + int length = -1; + Set nullFieldKeys = new HashSet<>(); + for (FieldKey fieldKey : requiredFieldKeys) + { + Object value = ctx.get(fieldKey); + if (value == null || "".equals(value)) + { + nullFieldKeys.add(fieldKey); + } + else + { + // Use -1 as the limit so that we pick up back-to-back delimiters as empty strings in the returned array, + // which lets us understand there were rows with nulls. + String[] values = value.toString().split(VALUE_DELIMITER_REGEX, -1); + if (length != -1 && values.length != length) + { + throw new IllegalStateException("Expected all columns to have the same number of values, but '" + fieldKey + "' has " + values.length + " and " + _iterators.keySet() + " had " + length); + } + length = values.length; + _iterators.put(fieldKey, new ArrayIterator<>(values)); + } + + for (FieldKey nullFieldKey : nullFieldKeys) + { + _iterators.put(nullFieldKey, new ArrayIterator<>(new String[length == -1 ? 0 : length])); + } + } + } + + // Advance all the iterators, if another value is present. Check that all iterators are in lock step. + public boolean next() + { + Boolean previousHasNext = null; + + for (Map.Entry> entry : _iterators.entrySet()) + { + Iterator iter = entry.getValue(); + boolean hasNext = iter.hasNext(); + + if (hasNext) + _currentValues.put(entry.getKey(), iter.next()); + + if (null == previousHasNext) + previousHasNext = hasNext; + else + assert previousHasNext == hasNext : "Mismatch in number of values for " + entry.getKey() + " compared with other fields: " + _iterators.keySet(); + } + + return null != previousHasNext && previousHasNext; + } + + @Override + public Object get(Object key) + { + Object value = _currentValues.get(key); + + if (null != value) + { + // empty string values map to null + if ("".equals(value)) + value = null; + + if (getFieldMap() != null) + { + ColumnInfo columnInfo = getFieldMap().get(key); + if (columnInfo != null && columnInfo.getPropertyType() == PropertyType.MULTI_CHOICE && value instanceof String strVal) + { + // Multi-choice values array is converted to string: "{value1,value2,...}", so strip off the braces before converting + if (strVal.startsWith("{") && strVal.endsWith("}")) + { + String pgArrayStr = strVal.substring(1, strVal.length() - 1); + // pgArrayStr escapes double quote with \", but MultiChoice.Array.from expects GoogleSheetMultiValue, which escapes quote with double quotes "" + pgArrayStr = pgArrayStr.replace("\\\"", "\"\""); + return columnInfo.convert(pgArrayStr); + } + + } + // The value was concatenated with others, so it's become a string. + // Do conversion to switch it back to the expected type. + if (value != null && columnInfo != null && !columnInfo.getJavaClass().isInstance(value)) + { + value = columnInfo.convert(value); + } + } + } + else + { + value = super.get(key); + } + + return value; + } + + public static class TestCase extends Assert + { + private final FieldKey _fk1 = FieldKey.fromParts("Parent", "Child"); + private final FieldKey _fk2 = FieldKey.fromParts("Standalone"); + private final FieldKey _otherFK = FieldKey.fromParts("NotInRow"); + + @Test + public void testMatchingValues() + { + Set fieldKeys = new HashSet<>(); + fieldKeys.add(_fk1); + fieldKeys.add(_fk2); + Map values = new HashMap<>(); + values.put(_fk1, "1" + VALUE_DELIMITER + "2" + VALUE_DELIMITER + "3"); + values.put(_fk2, "a" + VALUE_DELIMITER + "b" + VALUE_DELIMITER + "c"); + MultiValuedRenderContext mvContext = new MultiValuedRenderContext(new TestRenderContext(values), fieldKeys); + assertTrue(mvContext.next()); + assertEquals(1, mvContext.get(_fk1)); + assertEquals("a", mvContext.get(_fk2)); + assertTrue(mvContext.next()); + assertEquals(2, mvContext.get(_fk1)); + assertEquals("b", mvContext.get(_fk2)); + assertTrue(mvContext.next()); + assertEquals(3, mvContext.get(_fk1)); + assertEquals("c", mvContext.get(_fk2)); + assertFalse(mvContext.next()); + } + + @Test + public void testMissingColumn() + { + // Be sure that if there's a column that couldn't be found, we don't blow up + Set fieldKeys = new HashSet<>(); + fieldKeys.add(_fk1); + fieldKeys.add(_fk2); + fieldKeys.add(_otherFK); + Map values = new HashMap<>(); + values.put(_fk1, "1" + VALUE_DELIMITER + "2" + VALUE_DELIMITER + "3"); + values.put(_fk2, "a" + VALUE_DELIMITER + "b" + VALUE_DELIMITER + "c"); + MultiValuedRenderContext mvContext = new MultiValuedRenderContext(new TestRenderContext(values), fieldKeys); + assertTrue(mvContext.next()); + assertEquals(1, mvContext.get(_fk1)); + assertEquals("a", mvContext.get(_fk2)); + assertNull(mvContext.get(_otherFK)); + assertTrue(mvContext.next()); + assertEquals(2, mvContext.get(_fk1)); + assertEquals("b", mvContext.get(_fk2)); + assertNull(mvContext.get(_otherFK)); + assertTrue(mvContext.next()); + assertEquals(3, mvContext.get(_fk1)); + assertEquals("c", mvContext.get(_fk2)); + assertNull(mvContext.get(_otherFK)); + assertFalse(mvContext.next()); + } + + @Test + public void testMismatchedValues() + { + Set fieldKeys = new HashSet<>(); + fieldKeys.add(_fk1); + fieldKeys.add(_fk2); + Map values = new HashMap<>(); + values.put(_fk1, "1" + VALUE_DELIMITER + "2" + VALUE_DELIMITER + "3"); + values.put(_fk2, "a" + VALUE_DELIMITER + "b" + VALUE_DELIMITER + "c" + VALUE_DELIMITER + "d"); + try + { + new MultiValuedRenderContext(new TestRenderContext(values), fieldKeys); + fail("Should have gotten an exception"); + } + catch (IllegalStateException ignored) {} + } + + private class TestRenderContext extends RenderContext + { + private final Map _values; + + public TestRenderContext(Map values) + { + _values = values; + } + + @Override + public Object get(Object key) + { + return _values.get(key); + } + + @Override + public Map getFieldMap() + { + Map result = new HashMap<>(); + ColumnInfo col1 = new BaseColumnInfo(_fk1, JdbcType.INTEGER); + result.put(_fk1, col1); + ColumnInfo col2 = new BaseColumnInfo(_fk2, JdbcType.VARCHAR); + result.put(_fk2, col2); + return result; + } + } + } +} diff --git a/api/src/org/labkey/api/util/PageFlowUtil.java b/api/src/org/labkey/api/util/PageFlowUtil.java index eb2ff42bb80..a39139f9d5f 100644 --- a/api/src/org/labkey/api/util/PageFlowUtil.java +++ b/api/src/org/labkey/api/util/PageFlowUtil.java @@ -3045,6 +3045,22 @@ public void testGoogleSheetMultiValue() ); for (List test : quickTests) assertEquals(test, splitStringToValuesForImport(joinValuesToStringForExport(test))); + + List specialCharArrays = Arrays.asList( + "&^G'{\"И<2&)&]#~%:\uD83D\uDC7E*!안GaC;", + ",~-", + "<=0\\!41%d!By&]b", + "A)D'z:&", + "b$Dyf)D;C@", + "c_x-eИ", + "d[dF2cは=&G&1", + "e^\"#x" + ); + + String specialCharStr = "\"&^G'{\"\"И<2&)&]#~%:\uD83D\uDC7E*!안GaC;\", \",~-\", <=0\\!41%d!By&]b, A)D'z:&, b$Dyf)D;C@, c_x-eИ, d[dF2cは=&G&1, \"e^\"\"#x\""; + + assertEquals(specialCharStr, joinValuesToStringForExport(specialCharArrays)); + assertEquals(specialCharArrays, splitStringToValuesForImport(specialCharStr)); } @Test diff --git a/core/package-lock.json b/core/package-lock.json index 24da56b166c..a5a45c9e536 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -8,7 +8,7 @@ "name": "labkey-core", "version": "0.0.0", "dependencies": { - "@labkey/components": "7.21.0", + "@labkey/components": "7.22.0-fb-mvtcMVFK.1", "@labkey/themes": "1.7.0" }, "devDependencies": { @@ -3760,9 +3760,9 @@ } }, "node_modules/@labkey/components": { - "version": "7.21.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.21.0.tgz", - "integrity": "sha512-LK2ul66TvykGJk2/jWU3Gdq4w5maw8+qbElEol0eGAQAPQ9M0qCIzW75XVO8HMZ+VEXXXm6cKmbK1nF1m/pZRQ==", + "version": "7.22.0-fb-mvtcMVFK.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.22.0-fb-mvtcMVFK.1.tgz", + "integrity": "sha512-aCRfTgOjD6DjDdSA5ED/gpInGu45O3FZ4LES2463Xg3jXifWL3LageMdOMempbz8jo2YV4h7HnGLza8r4teaEw==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/core/package.json b/core/package.json index e3b8310db46..4a71e4c7e8c 100644 --- a/core/package.json +++ b/core/package.json @@ -53,7 +53,7 @@ } }, "dependencies": { - "@labkey/components": "7.21.0", + "@labkey/components": "7.22.0-fb-mvtcMVFK.1", "@labkey/themes": "1.7.0" }, "devDependencies": { diff --git a/experiment/package-lock.json b/experiment/package-lock.json index 08f4d14fd23..6f3d8eed2d9 100644 --- a/experiment/package-lock.json +++ b/experiment/package-lock.json @@ -8,7 +8,7 @@ "name": "experiment", "version": "0.0.0", "dependencies": { - "@labkey/components": "7.21.0" + "@labkey/components": "7.22.0-fb-mvtcMVFK.1" }, "devDependencies": { "@labkey/build": "8.9.0", @@ -3611,9 +3611,9 @@ } }, "node_modules/@labkey/components": { - "version": "7.21.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.21.0.tgz", - "integrity": "sha512-LK2ul66TvykGJk2/jWU3Gdq4w5maw8+qbElEol0eGAQAPQ9M0qCIzW75XVO8HMZ+VEXXXm6cKmbK1nF1m/pZRQ==", + "version": "7.22.0-fb-mvtcMVFK.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.22.0-fb-mvtcMVFK.1.tgz", + "integrity": "sha512-aCRfTgOjD6DjDdSA5ED/gpInGu45O3FZ4LES2463Xg3jXifWL3LageMdOMempbz8jo2YV4h7HnGLza8r4teaEw==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/experiment/package.json b/experiment/package.json index 2ec3b1510e5..99b5044be1a 100644 --- a/experiment/package.json +++ b/experiment/package.json @@ -13,7 +13,7 @@ "test-integration": "cross-env NODE_ENV=test jest --ci --runInBand -c test/js/jest.config.integration.js" }, "dependencies": { - "@labkey/components": "7.21.0" + "@labkey/components": "7.22.0-fb-mvtcMVFK.1" }, "devDependencies": { "@labkey/build": "8.9.0", From 10bd568b8d1a7a7d79633840f6b6346c07c499f5 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 4 Mar 2026 14:17:42 -0800 Subject: [PATCH 2/6] crlf --- .../api/data/MultiValuedRenderContext.java | 488 +++++++++--------- 1 file changed, 244 insertions(+), 244 deletions(-) diff --git a/api/src/org/labkey/api/data/MultiValuedRenderContext.java b/api/src/org/labkey/api/data/MultiValuedRenderContext.java index 9421c573ea6..72c0072afde 100644 --- a/api/src/org/labkey/api/data/MultiValuedRenderContext.java +++ b/api/src/org/labkey/api/data/MultiValuedRenderContext.java @@ -1,244 +1,244 @@ -/* - * Copyright (c) 2010-2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.labkey.api.data; - -import org.apache.commons.beanutils.ConvertUtils; -import org.apache.commons.collections4.iterators.ArrayIterator; -import org.junit.Assert; -import org.junit.Test; -import org.labkey.api.exp.PropertyType; -import org.labkey.api.query.FieldKey; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; - -/** - * Wrapper for another {@link RenderContext} that allows multiple values to be rendered for a single row's column. - * Used in conjunction with {@link MultiValuedDisplayColumn}. - * User: adam - * Date: Sep 7, 2010 - * Time: 10:02:25 AM - */ -public class MultiValuedRenderContext extends RenderContextDecorator -{ - private final Map> _iterators = new HashMap<>(); - private final Map _currentValues = new HashMap<>(); - - public static final String VALUE_DELIMITER = "{@~^"; - public static final String VALUE_DELIMITER_REGEX = "\\Q" + VALUE_DELIMITER + "\\E"; - - public MultiValuedRenderContext(RenderContext ctx, Set requiredFieldKeys) - { - super(ctx); - - // For each required column (e.g., display value, rowId), retrieve the concatenated values, split them, and - // stash away an iterator of those values. - int length = -1; - Set nullFieldKeys = new HashSet<>(); - for (FieldKey fieldKey : requiredFieldKeys) - { - Object value = ctx.get(fieldKey); - if (value == null || "".equals(value)) - { - nullFieldKeys.add(fieldKey); - } - else - { - // Use -1 as the limit so that we pick up back-to-back delimiters as empty strings in the returned array, - // which lets us understand there were rows with nulls. - String[] values = value.toString().split(VALUE_DELIMITER_REGEX, -1); - if (length != -1 && values.length != length) - { - throw new IllegalStateException("Expected all columns to have the same number of values, but '" + fieldKey + "' has " + values.length + " and " + _iterators.keySet() + " had " + length); - } - length = values.length; - _iterators.put(fieldKey, new ArrayIterator<>(values)); - } - - for (FieldKey nullFieldKey : nullFieldKeys) - { - _iterators.put(nullFieldKey, new ArrayIterator<>(new String[length == -1 ? 0 : length])); - } - } - } - - // Advance all the iterators, if another value is present. Check that all iterators are in lock step. - public boolean next() - { - Boolean previousHasNext = null; - - for (Map.Entry> entry : _iterators.entrySet()) - { - Iterator iter = entry.getValue(); - boolean hasNext = iter.hasNext(); - - if (hasNext) - _currentValues.put(entry.getKey(), iter.next()); - - if (null == previousHasNext) - previousHasNext = hasNext; - else - assert previousHasNext == hasNext : "Mismatch in number of values for " + entry.getKey() + " compared with other fields: " + _iterators.keySet(); - } - - return null != previousHasNext && previousHasNext; - } - - @Override - public Object get(Object key) - { - Object value = _currentValues.get(key); - - if (null != value) - { - // empty string values map to null - if ("".equals(value)) - value = null; - - if (getFieldMap() != null) - { - ColumnInfo columnInfo = getFieldMap().get(key); - if (columnInfo != null && columnInfo.getPropertyType() == PropertyType.MULTI_CHOICE && value instanceof String strVal) - { - // Multi-choice values array is converted to string: "{value1,value2,...}", so strip off the braces before converting - if (strVal.startsWith("{") && strVal.endsWith("}")) - { - String pgArrayStr = strVal.substring(1, strVal.length() - 1); - // pgArrayStr escapes double quote with \", but MultiChoice.Array.from expects GoogleSheetMultiValue, which escapes quote with double quotes "" - pgArrayStr = pgArrayStr.replace("\\\"", "\"\""); - return columnInfo.convert(pgArrayStr); - } - - } - // The value was concatenated with others, so it's become a string. - // Do conversion to switch it back to the expected type. - if (value != null && columnInfo != null && !columnInfo.getJavaClass().isInstance(value)) - { - value = columnInfo.convert(value); - } - } - } - else - { - value = super.get(key); - } - - return value; - } - - public static class TestCase extends Assert - { - private final FieldKey _fk1 = FieldKey.fromParts("Parent", "Child"); - private final FieldKey _fk2 = FieldKey.fromParts("Standalone"); - private final FieldKey _otherFK = FieldKey.fromParts("NotInRow"); - - @Test - public void testMatchingValues() - { - Set fieldKeys = new HashSet<>(); - fieldKeys.add(_fk1); - fieldKeys.add(_fk2); - Map values = new HashMap<>(); - values.put(_fk1, "1" + VALUE_DELIMITER + "2" + VALUE_DELIMITER + "3"); - values.put(_fk2, "a" + VALUE_DELIMITER + "b" + VALUE_DELIMITER + "c"); - MultiValuedRenderContext mvContext = new MultiValuedRenderContext(new TestRenderContext(values), fieldKeys); - assertTrue(mvContext.next()); - assertEquals(1, mvContext.get(_fk1)); - assertEquals("a", mvContext.get(_fk2)); - assertTrue(mvContext.next()); - assertEquals(2, mvContext.get(_fk1)); - assertEquals("b", mvContext.get(_fk2)); - assertTrue(mvContext.next()); - assertEquals(3, mvContext.get(_fk1)); - assertEquals("c", mvContext.get(_fk2)); - assertFalse(mvContext.next()); - } - - @Test - public void testMissingColumn() - { - // Be sure that if there's a column that couldn't be found, we don't blow up - Set fieldKeys = new HashSet<>(); - fieldKeys.add(_fk1); - fieldKeys.add(_fk2); - fieldKeys.add(_otherFK); - Map values = new HashMap<>(); - values.put(_fk1, "1" + VALUE_DELIMITER + "2" + VALUE_DELIMITER + "3"); - values.put(_fk2, "a" + VALUE_DELIMITER + "b" + VALUE_DELIMITER + "c"); - MultiValuedRenderContext mvContext = new MultiValuedRenderContext(new TestRenderContext(values), fieldKeys); - assertTrue(mvContext.next()); - assertEquals(1, mvContext.get(_fk1)); - assertEquals("a", mvContext.get(_fk2)); - assertNull(mvContext.get(_otherFK)); - assertTrue(mvContext.next()); - assertEquals(2, mvContext.get(_fk1)); - assertEquals("b", mvContext.get(_fk2)); - assertNull(mvContext.get(_otherFK)); - assertTrue(mvContext.next()); - assertEquals(3, mvContext.get(_fk1)); - assertEquals("c", mvContext.get(_fk2)); - assertNull(mvContext.get(_otherFK)); - assertFalse(mvContext.next()); - } - - @Test - public void testMismatchedValues() - { - Set fieldKeys = new HashSet<>(); - fieldKeys.add(_fk1); - fieldKeys.add(_fk2); - Map values = new HashMap<>(); - values.put(_fk1, "1" + VALUE_DELIMITER + "2" + VALUE_DELIMITER + "3"); - values.put(_fk2, "a" + VALUE_DELIMITER + "b" + VALUE_DELIMITER + "c" + VALUE_DELIMITER + "d"); - try - { - new MultiValuedRenderContext(new TestRenderContext(values), fieldKeys); - fail("Should have gotten an exception"); - } - catch (IllegalStateException ignored) {} - } - - private class TestRenderContext extends RenderContext - { - private final Map _values; - - public TestRenderContext(Map values) - { - _values = values; - } - - @Override - public Object get(Object key) - { - return _values.get(key); - } - - @Override - public Map getFieldMap() - { - Map result = new HashMap<>(); - ColumnInfo col1 = new BaseColumnInfo(_fk1, JdbcType.INTEGER); - result.put(_fk1, col1); - ColumnInfo col2 = new BaseColumnInfo(_fk2, JdbcType.VARCHAR); - result.put(_fk2, col2); - return result; - } - } - } -} +/* + * Copyright (c) 2010-2019 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.api.data; + +import org.apache.commons.beanutils.ConvertUtils; +import org.apache.commons.collections4.iterators.ArrayIterator; +import org.junit.Assert; +import org.junit.Test; +import org.labkey.api.exp.PropertyType; +import org.labkey.api.query.FieldKey; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * Wrapper for another {@link RenderContext} that allows multiple values to be rendered for a single row's column. + * Used in conjunction with {@link MultiValuedDisplayColumn}. + * User: adam + * Date: Sep 7, 2010 + * Time: 10:02:25 AM + */ +public class MultiValuedRenderContext extends RenderContextDecorator +{ + private final Map> _iterators = new HashMap<>(); + private final Map _currentValues = new HashMap<>(); + + public static final String VALUE_DELIMITER = "{@~^"; + public static final String VALUE_DELIMITER_REGEX = "\\Q" + VALUE_DELIMITER + "\\E"; + + public MultiValuedRenderContext(RenderContext ctx, Set requiredFieldKeys) + { + super(ctx); + + // For each required column (e.g., display value, rowId), retrieve the concatenated values, split them, and + // stash away an iterator of those values. + int length = -1; + Set nullFieldKeys = new HashSet<>(); + for (FieldKey fieldKey : requiredFieldKeys) + { + Object value = ctx.get(fieldKey); + if (value == null || "".equals(value)) + { + nullFieldKeys.add(fieldKey); + } + else + { + // Use -1 as the limit so that we pick up back-to-back delimiters as empty strings in the returned array, + // which lets us understand there were rows with nulls. + String[] values = value.toString().split(VALUE_DELIMITER_REGEX, -1); + if (length != -1 && values.length != length) + { + throw new IllegalStateException("Expected all columns to have the same number of values, but '" + fieldKey + "' has " + values.length + " and " + _iterators.keySet() + " had " + length); + } + length = values.length; + _iterators.put(fieldKey, new ArrayIterator<>(values)); + } + + for (FieldKey nullFieldKey : nullFieldKeys) + { + _iterators.put(nullFieldKey, new ArrayIterator<>(new String[length == -1 ? 0 : length])); + } + } + } + + // Advance all the iterators, if another value is present. Check that all iterators are in lock step. + public boolean next() + { + Boolean previousHasNext = null; + + for (Map.Entry> entry : _iterators.entrySet()) + { + Iterator iter = entry.getValue(); + boolean hasNext = iter.hasNext(); + + if (hasNext) + _currentValues.put(entry.getKey(), iter.next()); + + if (null == previousHasNext) + previousHasNext = hasNext; + else + assert previousHasNext == hasNext : "Mismatch in number of values for " + entry.getKey() + " compared with other fields: " + _iterators.keySet(); + } + + return null != previousHasNext && previousHasNext; + } + + @Override + public Object get(Object key) + { + Object value = _currentValues.get(key); + + if (null != value) + { + // empty string values map to null + if ("".equals(value)) + value = null; + + if (getFieldMap() != null) + { + ColumnInfo columnInfo = getFieldMap().get(key); + if (columnInfo != null && columnInfo.getPropertyType() == PropertyType.MULTI_CHOICE && value instanceof String strVal) + { + // Multi-choice values array is converted to string: "{value1,value2,...}", so strip off the braces before converting + if (strVal.startsWith("{") && strVal.endsWith("}")) + { + String pgArrayStr = strVal.substring(1, strVal.length() - 1); + // pgArrayStr escapes double quote with \", but MultiChoice.Array.from expects GoogleSheetMultiValue, which escapes quote with double quotes "" + pgArrayStr = pgArrayStr.replace("\\\"", "\"\""); + return columnInfo.convert(pgArrayStr); + } + + } + // The value was concatenated with others, so it's become a string. + // Do conversion to switch it back to the expected type. + if (value != null && columnInfo != null && !columnInfo.getJavaClass().isInstance(value)) + { + value = columnInfo.convert(value); + } + } + } + else + { + value = super.get(key); + } + + return value; + } + + public static class TestCase extends Assert + { + private final FieldKey _fk1 = FieldKey.fromParts("Parent", "Child"); + private final FieldKey _fk2 = FieldKey.fromParts("Standalone"); + private final FieldKey _otherFK = FieldKey.fromParts("NotInRow"); + + @Test + public void testMatchingValues() + { + Set fieldKeys = new HashSet<>(); + fieldKeys.add(_fk1); + fieldKeys.add(_fk2); + Map values = new HashMap<>(); + values.put(_fk1, "1" + VALUE_DELIMITER + "2" + VALUE_DELIMITER + "3"); + values.put(_fk2, "a" + VALUE_DELIMITER + "b" + VALUE_DELIMITER + "c"); + MultiValuedRenderContext mvContext = new MultiValuedRenderContext(new TestRenderContext(values), fieldKeys); + assertTrue(mvContext.next()); + assertEquals(1, mvContext.get(_fk1)); + assertEquals("a", mvContext.get(_fk2)); + assertTrue(mvContext.next()); + assertEquals(2, mvContext.get(_fk1)); + assertEquals("b", mvContext.get(_fk2)); + assertTrue(mvContext.next()); + assertEquals(3, mvContext.get(_fk1)); + assertEquals("c", mvContext.get(_fk2)); + assertFalse(mvContext.next()); + } + + @Test + public void testMissingColumn() + { + // Be sure that if there's a column that couldn't be found, we don't blow up + Set fieldKeys = new HashSet<>(); + fieldKeys.add(_fk1); + fieldKeys.add(_fk2); + fieldKeys.add(_otherFK); + Map values = new HashMap<>(); + values.put(_fk1, "1" + VALUE_DELIMITER + "2" + VALUE_DELIMITER + "3"); + values.put(_fk2, "a" + VALUE_DELIMITER + "b" + VALUE_DELIMITER + "c"); + MultiValuedRenderContext mvContext = new MultiValuedRenderContext(new TestRenderContext(values), fieldKeys); + assertTrue(mvContext.next()); + assertEquals(1, mvContext.get(_fk1)); + assertEquals("a", mvContext.get(_fk2)); + assertNull(mvContext.get(_otherFK)); + assertTrue(mvContext.next()); + assertEquals(2, mvContext.get(_fk1)); + assertEquals("b", mvContext.get(_fk2)); + assertNull(mvContext.get(_otherFK)); + assertTrue(mvContext.next()); + assertEquals(3, mvContext.get(_fk1)); + assertEquals("c", mvContext.get(_fk2)); + assertNull(mvContext.get(_otherFK)); + assertFalse(mvContext.next()); + } + + @Test + public void testMismatchedValues() + { + Set fieldKeys = new HashSet<>(); + fieldKeys.add(_fk1); + fieldKeys.add(_fk2); + Map values = new HashMap<>(); + values.put(_fk1, "1" + VALUE_DELIMITER + "2" + VALUE_DELIMITER + "3"); + values.put(_fk2, "a" + VALUE_DELIMITER + "b" + VALUE_DELIMITER + "c" + VALUE_DELIMITER + "d"); + try + { + new MultiValuedRenderContext(new TestRenderContext(values), fieldKeys); + fail("Should have gotten an exception"); + } + catch (IllegalStateException ignored) {} + } + + private class TestRenderContext extends RenderContext + { + private final Map _values; + + public TestRenderContext(Map values) + { + _values = values; + } + + @Override + public Object get(Object key) + { + return _values.get(key); + } + + @Override + public Map getFieldMap() + { + Map result = new HashMap<>(); + ColumnInfo col1 = new BaseColumnInfo(_fk1, JdbcType.INTEGER); + result.put(_fk1, col1); + ColumnInfo col2 = new BaseColumnInfo(_fk2, JdbcType.VARCHAR); + result.put(_fk2, col2); + return result; + } + } + } +} From 0ce1fa97d530e3568e328221dd26bacf04690015 Mon Sep 17 00:00:00 2001 From: XingY Date: Thu, 5 Mar 2026 12:26:35 -0800 Subject: [PATCH 3/6] Add parsePgArray --- api/src/org/labkey/api/data/MultiChoice.java | 161 ++++++++++++++++++ .../api/data/MultiValuedRenderContext.java | 24 +-- 2 files changed, 173 insertions(+), 12 deletions(-) diff --git a/api/src/org/labkey/api/data/MultiChoice.java b/api/src/org/labkey/api/data/MultiChoice.java index 7612d13fc1b..1e09ac40ac1 100644 --- a/api/src/org/labkey/api/data/MultiChoice.java +++ b/api/src/org/labkey/api/data/MultiChoice.java @@ -1,5 +1,6 @@ package org.labkey.api.data; +import org.apache.commons.beanutils.ConversionException; import org.apache.commons.beanutils.ConvertUtils; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; @@ -273,6 +274,17 @@ public static Array from(@NotNull String s) { if (isBlank(s)) return EMPTY; + if (s.startsWith("{") && s.endsWith("}")) + { + try + { + return parsePgArray(s); + } + catch (ConversionException ignore) + { + } + + } List split = PageFlowUtil.splitStringToValuesForImport(s); return from(split.toArray()); } @@ -302,6 +314,94 @@ public static Array from(@NotNull java.sql.Array sqlArray) } } + /** + * Parse a PostgreSQL array text representation, e.g. {@code {a,b,"c d"}}. + *

+ * PostgreSQL uses backslash escaping inside quoted elements ({@code \"} for a literal + * double-quote, {@code \\} for a literal backslash), unlike CSV which doubles quotes. + * Unquoted elements are trimmed of whitespace. + */ + public static Array parsePgArray(@NotNull String s) + { + if (isBlank(s)) + return EMPTY; + + s = s.trim(); + if (!s.startsWith("{") || !s.endsWith("}")) + throw new ConversionException("PostgreSQL array literal must be wrapped in {}: " + s); + + String inner = s.substring(1, s.length() - 1); + if (inner.isEmpty()) + return EMPTY; + + List values = new ArrayList<>(); + int len = inner.length(); + int i = 0; + + while (i < len) + { + // skip leading whitespace before element + while (i < len && Character.isWhitespace(inner.charAt(i))) + i++; + + if (i >= len) + break; + + if (inner.charAt(i) == '"') + { + // quoted element — backslash escaping + i++; // skip opening quote + StringBuilder sb = new StringBuilder(); + while (i < len) + { + char c = inner.charAt(i); + if (c == '\\' && i + 1 < len) + { + sb.append(inner.charAt(i + 1)); + i += 2; + } + else if (c == '"') + { + i++; // skip closing quote + break; + } + else + { + sb.append(c); + i++; + } + if (i >= len) + throw new ConversionException("Unterminated quoted string in PostgreSQL array literal"); + } + values.add(sb.toString()); + + // after closing quote, expect comma or end + while (i < len && Character.isWhitespace(inner.charAt(i))) + i++; + if (i < len) + { + if (inner.charAt(i) == ',') + i++; // consume delimiter + else + throw new ConversionException("Unexpected character after closing quote in PostgreSQL array literal at position " + i); + } + } + else + { + // unquoted element — read until comma + int start = i; + while (i < len && inner.charAt(i) != ',') + i++; + String token = inner.substring(start, i).trim(); + values.add(token); + if (i < len) + i++; // skip comma + } + } + + return from(values.toArray()); + } + // // implements List // @@ -587,6 +687,67 @@ public void testConvert() throws Exception assertEquals(0, _converter.convert(Array.class, null).size()); } + @Test + public void testParsePgArray() + { + // simple unquoted values + assertEquals(Array.from(new String[]{"a", "b", "c"}), Array.parsePgArray("{a,b,c}")); + + // quoted values with special chars: comma, escaped quote, escaped backslash + assertEquals(Array.from(new String[]{"a,b", "c\"d", "e\\f"}), Array.parsePgArray("{\"a,b\",\"c\\\"d\",\"e\\\\f\"}")); + + // empty array + assertEquals(Array.EMPTY, Array.parsePgArray("{}")); + + // blank/empty input + assertEquals(Array.EMPTY, Array.parsePgArray("")); + assertEquals(Array.EMPTY, Array.parsePgArray(" ")); + + // quoted empty string — filtered by Array constructor (trimToNull) + assertEquals(Array.from(new String[]{"a"}), Array.parsePgArray("{\"\",a}")); + + // whitespace in quoted elements — trimmed by Array constructor + assertEquals(Array.from(new String[]{"a", "b"}), Array.parsePgArray("{\" a \",b}")); + + // whitespace around braces + assertEquals(Array.from(new String[]{"x", "y"}), Array.parsePgArray(" {x,y} ")); + + // round-trip: values from testConvert + Array expected = Array.from(new String[]{"a,", "b\"", "c "}); + assertEquals(expected, Array.parsePgArray("{\"a,\",\"b\\\"\",\"c \"}")); + + + List specialCharArrays = Array.from(new String[]{ + "&^G'{\"И<2&)&]#~%:\uD83D\uDC7E*!안GaC;", + ",~-", + "<=0\\!41%d!By&]b", + "A)D'z:&", + "b$Dyf)D;C@", + "c_x-eИ", + "d[dF2cは=&G&1", + "e^\"#x" + }); + String expectedSpecialCharStr = "{\"&^G'{\\\"И<2&)&]#~%:\uD83D\uDC7E*!안GaC;\",\"\\,~-\",\"<=0\\\\!41%d!By&]b\",\"A)D'z:&\",\"b$Dyf)D;C@\",\"c_x-eИ\",\"d[dF2cは=&G&1\",\"e^\\\"#x\"}"; + assertEquals(specialCharArrays, Array.parsePgArray(expectedSpecialCharStr)); + + + // error: missing braces + try + { + Array.parsePgArray("a,b,c"); + fail("Expected ConversionException for missing braces"); + } + catch (ConversionException ignored) {} + + // error: unterminated quote + try + { + Array.parsePgArray("{\"abc}"); + fail("Expected ConversionException for unterminated quote"); + } + catch (ConversionException ignored) {} + } + @Test public void testCSV() throws Exception { diff --git a/api/src/org/labkey/api/data/MultiValuedRenderContext.java b/api/src/org/labkey/api/data/MultiValuedRenderContext.java index 72c0072afde..fccac1237e1 100644 --- a/api/src/org/labkey/api/data/MultiValuedRenderContext.java +++ b/api/src/org/labkey/api/data/MultiValuedRenderContext.java @@ -114,18 +114,18 @@ public Object get(Object key) if (getFieldMap() != null) { ColumnInfo columnInfo = getFieldMap().get(key); - if (columnInfo != null && columnInfo.getPropertyType() == PropertyType.MULTI_CHOICE && value instanceof String strVal) - { - // Multi-choice values array is converted to string: "{value1,value2,...}", so strip off the braces before converting - if (strVal.startsWith("{") && strVal.endsWith("}")) - { - String pgArrayStr = strVal.substring(1, strVal.length() - 1); - // pgArrayStr escapes double quote with \", but MultiChoice.Array.from expects GoogleSheetMultiValue, which escapes quote with double quotes "" - pgArrayStr = pgArrayStr.replace("\\\"", "\"\""); - return columnInfo.convert(pgArrayStr); - } - - } +// if (columnInfo != null && columnInfo.getPropertyType() == PropertyType.MULTI_CHOICE && value instanceof String strVal) +// { +// // Multi-choice values array is converted to string: "{value1,value2,...}", so strip off the braces before converting +// if (strVal.startsWith("{") && strVal.endsWith("}")) +// { +// String pgArrayStr = strVal.substring(1, strVal.length() - 1); +// // pgArrayStr escapes double quote with \", but MultiChoice.Array.from expects GoogleSheetMultiValue, which escapes quote with double quotes "" +// pgArrayStr = pgArrayStr.replace("\\\"", "\"\""); +// return columnInfo.convert(pgArrayStr); +// } +// +// } // The value was concatenated with others, so it's become a string. // Do conversion to switch it back to the expected type. if (value != null && columnInfo != null && !columnInfo.getJavaClass().isInstance(value)) From b5ac0caf81166db7b1ffe03313a99496c1316fbc Mon Sep 17 00:00:00 2001 From: XingY Date: Thu, 5 Mar 2026 12:29:48 -0800 Subject: [PATCH 4/6] clean --- .../labkey/api/data/MultiValuedRenderContext.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/api/src/org/labkey/api/data/MultiValuedRenderContext.java b/api/src/org/labkey/api/data/MultiValuedRenderContext.java index fccac1237e1..20746d971eb 100644 --- a/api/src/org/labkey/api/data/MultiValuedRenderContext.java +++ b/api/src/org/labkey/api/data/MultiValuedRenderContext.java @@ -114,18 +114,6 @@ public Object get(Object key) if (getFieldMap() != null) { ColumnInfo columnInfo = getFieldMap().get(key); -// if (columnInfo != null && columnInfo.getPropertyType() == PropertyType.MULTI_CHOICE && value instanceof String strVal) -// { -// // Multi-choice values array is converted to string: "{value1,value2,...}", so strip off the braces before converting -// if (strVal.startsWith("{") && strVal.endsWith("}")) -// { -// String pgArrayStr = strVal.substring(1, strVal.length() - 1); -// // pgArrayStr escapes double quote with \", but MultiChoice.Array.from expects GoogleSheetMultiValue, which escapes quote with double quotes "" -// pgArrayStr = pgArrayStr.replace("\\\"", "\"\""); -// return columnInfo.convert(pgArrayStr); -// } -// -// } // The value was concatenated with others, so it's become a string. // Do conversion to switch it back to the expected type. if (value != null && columnInfo != null && !columnInfo.getJavaClass().isInstance(value)) From 8c7e6264f1a312f856ee7f66063d5e77e0672a42 Mon Sep 17 00:00:00 2001 From: XingY Date: Thu, 5 Mar 2026 14:18:08 -0800 Subject: [PATCH 5/6] Disallow multi-choice as 3rd key on save --- study/src/org/labkey/study/model/DatasetDomainKind.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/study/src/org/labkey/study/model/DatasetDomainKind.java b/study/src/org/labkey/study/model/DatasetDomainKind.java index c55fb877f22..657d8a15a70 100644 --- a/study/src/org/labkey/study/model/DatasetDomainKind.java +++ b/study/src/org/labkey/study/model/DatasetDomainKind.java @@ -37,6 +37,7 @@ import org.labkey.api.di.DataIntegrationService; import org.labkey.api.exp.Lsid; import org.labkey.api.exp.PropertyDescriptor; +import org.labkey.api.exp.PropertyType; import org.labkey.api.exp.TemplateInfo; import org.labkey.api.exp.api.ExperimentService; import org.labkey.api.exp.api.StorageProvisioner; @@ -618,6 +619,12 @@ private void validateDatasetProperties(DatasetDomainKindProperties datasetProper if (!(rangeURI.endsWith("int") || rangeURI.endsWith("double") || rangeURI.endsWith("string"))) throw new IllegalArgumentException("If Additional Key Column is managed, the column type must be numeric or text-based."); } + else if (!isDemographicData && !useTimeKeyField && null != keyPropertyName) + { + String rangeURI = domain.getFieldByName(keyPropertyName).getRangeURI(); + if (PropertyType.MULTI_CHOICE.getTypeUri().equals(rangeURI)) + throw new IllegalArgumentException("Additional Key Column cannot be a multi-choice column."); + } // Other exception(s) From 3e6158e32d2f825072c9b001ffbb382e81a8539a Mon Sep 17 00:00:00 2001 From: XingY Date: Fri, 6 Mar 2026 15:58:57 -0800 Subject: [PATCH 6/6] publish --- core/package-lock.json | 8 ++++---- core/package.json | 2 +- experiment/package-lock.json | 8 ++++---- experiment/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/package-lock.json b/core/package-lock.json index a5a45c9e536..de79ab3c4a3 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -8,7 +8,7 @@ "name": "labkey-core", "version": "0.0.0", "dependencies": { - "@labkey/components": "7.22.0-fb-mvtcMVFK.1", + "@labkey/components": "7.22.1", "@labkey/themes": "1.7.0" }, "devDependencies": { @@ -3760,9 +3760,9 @@ } }, "node_modules/@labkey/components": { - "version": "7.22.0-fb-mvtcMVFK.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.22.0-fb-mvtcMVFK.1.tgz", - "integrity": "sha512-aCRfTgOjD6DjDdSA5ED/gpInGu45O3FZ4LES2463Xg3jXifWL3LageMdOMempbz8jo2YV4h7HnGLza8r4teaEw==", + "version": "7.22.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.22.1.tgz", + "integrity": "sha512-ksLTrQELG7Ct5/gNB3KcVkjg0Gu5KvnYoar6KKhw7vEu3oFGcPqz4lWTrRZWvoDEgH8Cgg2+POH+W/BE1dYoXw==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/core/package.json b/core/package.json index 4a71e4c7e8c..57c8ef59a80 100644 --- a/core/package.json +++ b/core/package.json @@ -53,7 +53,7 @@ } }, "dependencies": { - "@labkey/components": "7.22.0-fb-mvtcMVFK.1", + "@labkey/components": "7.22.1", "@labkey/themes": "1.7.0" }, "devDependencies": { diff --git a/experiment/package-lock.json b/experiment/package-lock.json index 6f3d8eed2d9..d917381f45b 100644 --- a/experiment/package-lock.json +++ b/experiment/package-lock.json @@ -8,7 +8,7 @@ "name": "experiment", "version": "0.0.0", "dependencies": { - "@labkey/components": "7.22.0-fb-mvtcMVFK.1" + "@labkey/components": "7.22.1" }, "devDependencies": { "@labkey/build": "8.9.0", @@ -3611,9 +3611,9 @@ } }, "node_modules/@labkey/components": { - "version": "7.22.0-fb-mvtcMVFK.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.22.0-fb-mvtcMVFK.1.tgz", - "integrity": "sha512-aCRfTgOjD6DjDdSA5ED/gpInGu45O3FZ4LES2463Xg3jXifWL3LageMdOMempbz8jo2YV4h7HnGLza8r4teaEw==", + "version": "7.22.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.22.1.tgz", + "integrity": "sha512-ksLTrQELG7Ct5/gNB3KcVkjg0Gu5KvnYoar6KKhw7vEu3oFGcPqz4lWTrRZWvoDEgH8Cgg2+POH+W/BE1dYoXw==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/experiment/package.json b/experiment/package.json index 99b5044be1a..dad240b9c34 100644 --- a/experiment/package.json +++ b/experiment/package.json @@ -13,7 +13,7 @@ "test-integration": "cross-env NODE_ENV=test jest --ci --runInBand -c test/js/jest.config.integration.js" }, "dependencies": { - "@labkey/components": "7.22.0-fb-mvtcMVFK.1" + "@labkey/components": "7.22.1" }, "devDependencies": { "@labkey/build": "8.9.0",