Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
108 changes: 77 additions & 31 deletions api/src/org/labkey/api/query/QueryChangeListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,41 @@ public static void handleColumnTypeChange(@NotNull PropertyDescriptor oldValue,
public V getNewValue() { return _newValue; }
}

// URL-encoded fragments used when a single filter value containing ";" is stored as a JSON array for MVTC
String JSON_ARRAY_FILTER_PREFIX = "%7Bjson%3A%5B%22"; // {json:["
String JSON_ARRAY_FILTER_SUFFIX = "%22%5D%7D"; // "]}
String ENCODED_SEMICOLON = "%3B"; // ;

/**
* Utility to update encoded filter string when a column type changes from Multi_Choice to a non Multi_Choice.
* This method performs targeted replacements for the given column name (case-insensitive).
*/
/**
* Resolves the URL-encoded form of {@code columnName} that matches the given filter string.
* <p>
* Tries first with the single quote left unencoded (Issue 943), then with it encoded as {@code %27}
* (Issue 1092 — Chrome and some browsers do encode {@code '} to {@code %27}).
*
* @return the encoded column name whose lowercase form matches the filter prefix, or {@code null} if neither encoding matches
*/
@Nullable
private static String resolveEncodedColumnName(String filterStr, String prefix, String columnName)
{
String sLower = filterStr.toLowerCase();

// GitHub Issue 943: single quote should decode to match url filter
String encoded = PageFlowUtil.encodeURIComponent(QueryKey.encodePart(columnName), true);
if (sLower.startsWith(prefix + "." + encoded.toLowerCase() + "~"))
return encoded;

// GitHub Issue 1092: some browsers (e.g. Chrome) encode ' to %27
encoded = PageFlowUtil.encodeURIComponent(QueryKey.encodePart(columnName), false);
if (sLower.startsWith(prefix + "." + encoded.toLowerCase() + "~"))
return encoded;

return null;
}

private static String getUpdatedFilterStrFromMVTC(String filterStr, String prefix, String columnName, @NotNull PropertyDescriptor oldType, @NotNull PropertyDescriptor newType)
{
if (filterStr == null || columnName == null)
Expand All @@ -224,33 +255,30 @@ private static String getUpdatedFilterStrFromMVTC(String filterStr, String prefi
if (oldType.getPropertyType() != PropertyType.MULTI_CHOICE || newType.getPropertyType() == PropertyType.MULTI_CHOICE)
return filterStr;

// GitHub Issue 943: Converting between TC and MVTC results in bad saved views
// single quote should decode to match url filter
String columnNameEncoded = PageFlowUtil.encodeURIComponent(QueryKey.encodePart(columnName), true);

String colLower = columnNameEncoded.toLowerCase();
String sLower = filterStr.toLowerCase();

// No action if column doesn't match
if (!sLower.startsWith(prefix + "." + colLower + "~"))
String columnNameEncoded = resolveEncodedColumnName(filterStr, prefix, columnName);
if (columnNameEncoded == null)
return filterStr;

// drop arraycontainsall since there is no good match
if (sLower.startsWith(prefix + "." + colLower + "~arraycontainsall"))
if (filterStr.toLowerCase().startsWith(prefix + "." + columnNameEncoded.toLowerCase() + "~arraycontainsall"))
return "";

String updated = filterStr;

// GitHub Issue 1096: Filters in saved views break after MVTC → TC conversion if column value contains semicolon
// if the single value contains ";", the filter is saved as {json:[]} for MVTC, needs to convert to multi value filter types (for example, 'in' instead of 'eq').
boolean isJsonArrayFilterValue = filterStr.indexOf(JSON_ARRAY_FILTER_PREFIX) > 0 && filterStr.endsWith(JSON_ARRAY_FILTER_SUFFIX);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should this be a check that it starts at index 0 instead of just > 0?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This as currently is should be > 0 instead of 0 since the string contains dataregion.query~op prefix.


if (TEXT_CHOICE_CONCEPT_URI.equals(newType.getConceptURI()))
{
// only keep arraymatches/arraynotmatches when converting to a TEXT_CHOICE since current values are guaranteed to be single value
if (containsOp(updated, prefix, columnNameEncoded, "arraymatches"))
{
return replaceOp(updated, prefix, columnNameEncoded, "arraymatches", "eq");
return replaceOp(updated, prefix, columnNameEncoded, "arraymatches", isJsonArrayFilterValue ? "in" : "eq");
}
if (containsOp(updated, prefix, columnNameEncoded, "arraynotmatches"))
{
return replaceOp(updated, prefix, columnNameEncoded, "arraynotmatches", "neqornull");
return replaceOp(updated, prefix, columnNameEncoded, "arraynotmatches", isJsonArrayFilterValue ? "notin" : "neqornull");
}
}

Expand Down Expand Up @@ -287,30 +315,13 @@ private static String getUpdatedMVTCFilterStr(String filterStr, String prefix, S
if (oldType.getPropertyType() == PropertyType.MULTI_CHOICE || newType.getPropertyType() != PropertyType.MULTI_CHOICE)
return filterStr;

String columnNameEncoded = PageFlowUtil.encodeURIComponent(QueryKey.encodePart(columnName), true);

String colLower = columnNameEncoded.toLowerCase();
String sLower = filterStr.toLowerCase();

// No action if column doesn't match
if (!sLower.startsWith(prefix + "." + colLower + "~"))
String columnNameEncoded = resolveEncodedColumnName(filterStr, prefix, columnName);
if (columnNameEncoded == null)
return filterStr;

String updated = filterStr;

// Return on first matching operator for this column
if (containsOp(updated, prefix, columnNameEncoded, "eq"))
{
return replaceOp(updated, prefix, columnNameEncoded, "eq", "arraymatches");
}
if (containsOp(updated, prefix, columnNameEncoded, "neqornull"))
{
return replaceOp(updated, prefix, columnNameEncoded, "neqornull", "arraycontainsnone");
}
if (containsOp(updated, prefix, columnNameEncoded, "neq"))
{
return replaceOp(updated, prefix, columnNameEncoded, "neq", "arraycontainsnone");
}
if (containsOp(updated, prefix, columnNameEncoded, "isblank"))
{
return replaceOp(updated, prefix, columnNameEncoded, "isblank", "arrayisempty");
Expand All @@ -328,10 +339,45 @@ private static String getUpdatedMVTCFilterStr(String filterStr, String prefix, S
return replaceOp(updated, prefix, columnNameEncoded, "notin", "arraycontainsnone");
}

if (containsOp(updated, prefix, columnNameEncoded, "eq"))
{
updated = replaceOp(updated, prefix, columnNameEncoded, "eq", "arraymatches");
return toSafeArrayFilter(updated, "arraymatches");
}
if (containsOp(updated, prefix, columnNameEncoded, "neqornull"))
{
updated = replaceOp(updated, prefix, columnNameEncoded, "neqornull", "arraycontainsnone");
return toSafeArrayFilter(updated, "arraycontainsnone");
}
if (containsOp(updated, prefix, columnNameEncoded, "neq"))
{
updated = replaceOp(updated, prefix, columnNameEncoded, "neq", "arraycontainsnone");
return toSafeArrayFilter(updated, "arraycontainsnone");
}


// No matching operator found for this column, drop the filter
return "";
}

private static String toSafeArrayFilter(String filterStr, String updatedOp)
{
// GitHub Issue 1096: Filters in saved views break after TC → MVTC conversion if column value contains semicolon
String filter = "~" + updatedOp + "=";
String[] parts = filterStr.split(filter);
if (parts.length < 2)
return filterStr;

String valuePart = parts[1];

boolean isJsonArrayFilterValue = valuePart.startsWith(JSON_ARRAY_FILTER_PREFIX) && valuePart.endsWith(JSON_ARRAY_FILTER_SUFFIX);
// if the single value filter value contains ";", drop the filter after converting to array type filter since the filter value is no longer valid
if (!isJsonArrayFilterValue && valuePart.contains(ENCODED_SEMICOLON))
return "";

return filterStr;
}

static String getUpdatedFilterStrOnColumnTypeUpdate(String filterStr, String prefix, String columnName, @NotNull PropertyDescriptor oldType, @NotNull PropertyDescriptor newType)
{
if (oldType.getPropertyType() == PropertyType.MULTI_CHOICE)
Expand Down
6 changes: 4 additions & 2 deletions query/src/org/labkey/query/QueryServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -2105,7 +2105,8 @@ private ColumnInfo resolveFieldKey(FieldKey fieldKey, TableInfo table, Map<Field
columnMap.put(expObjectIdFieldKey, expObjectColumn);
return expObjectColumn;
}
unresolvedColumns.add(new FieldKey(null, "expObject()"));
if (unresolvedColumns != null)
unresolvedColumns.add(new FieldKey(null, "expObject()"));
return null;
}

Expand All @@ -2125,7 +2126,8 @@ private ColumnInfo resolveFieldKey(FieldKey fieldKey, TableInfo table, Map<Field
boolean invalidArrayFilter = (isArrayFilter && !isArrayColumn) || (!isArrayFilter && isArrayColumn);
if (invalidArrayFilter)
{
unresolvedColumns.add(fieldKey);
if (unresolvedColumns != null)
unresolvedColumns.add(fieldKey);
return column; // return column, but mark as unresolvedColumns to drop filters
}
}
Expand Down
Loading