diff --git a/api/src/org/labkey/api/data/JsonWriter.java b/api/src/org/labkey/api/data/JsonWriter.java index 75fd541fe2e..3cbcc0ec1d7 100644 --- a/api/src/org/labkey/api/data/JsonWriter.java +++ b/api/src/org/labkey/api/data/JsonWriter.java @@ -37,11 +37,13 @@ import org.labkey.data.xml.queryCustomView.FilterType; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; /** @@ -52,7 +54,21 @@ public class JsonWriter { private static final Logger LOG = LogManager.getLogger(JsonWriter.class); - public static Map> getNativeColProps(TableInfo tableInfo, Collection fields, FieldKey fieldKeyPrefix, boolean includeDomainFormat, boolean includeAdditionalQueryColumns) + public static Map getTemplateColProps(TableInfo tableInfo) + { + ColumnInfo col = new BaseColumnInfo("__template", tableInfo, JdbcType.VARCHAR); + DisplayColumn dc = col.getDisplayColumnFactory().createRenderer(col); + Map template = getNativeColProps(List.of(dc), null, false).get(col.getFieldKey()); + template.remove("name"); + template.remove("fieldKey"); + template.remove("fieldKeyArray"); + template.remove("fieldKeyPath"); + template.remove("shortCaption"); + template.remove("caption"); + return template; + } + + public static Map> getNativeColProps(TableInfo tableInfo, Collection fields, FieldKey fieldKeyPrefix, @Nullable Map template, boolean includeDomainFormat, boolean includeAdditionalQueryColumns) { List displayColumns = QueryService.get().getColumns(tableInfo, fields, tableInfo.getColumns()) .values() @@ -61,10 +77,15 @@ public static Map> getNativeColProps(TableInfo tabl .map(cinfo -> cinfo.getDisplayColumnFactory().createRenderer(cinfo)) .collect(Collectors.toList()); - return getNativeColProps(displayColumns, fieldKeyPrefix, includeDomainFormat); + return getNativeColProps(displayColumns, fieldKeyPrefix, template, includeDomainFormat); } public static Map> getNativeColProps(Collection columns, FieldKey fieldKeyPrefix, boolean includeDomainFormat) + { + return getNativeColProps(columns, fieldKeyPrefix, null, includeDomainFormat); + } + + public static Map> getNativeColProps(Collection columns, FieldKey fieldKeyPrefix, @Nullable Map template, boolean includeDomainFormat) { Map> colProps = new LinkedHashMap<>(); for (DisplayColumn displayColumn : columns) @@ -76,15 +97,53 @@ public static Map> getNativeColProps(Collection getMetaData(DisplayColumn dc, FieldKey fieldKeyPrefix, boolean useFriendlyAsType, boolean includeLookup, boolean includeDomainFormat) { + return getMetaData(dc, fieldKeyPrefix, null, useFriendlyAsType, includeLookup, includeDomainFormat ); + } + + + // use helper map class to avoid making the code in getMetaData() less readable */ + static class TemplateMap + { + final Map _template; + final LinkedHashMap _map; + TemplateMap(Map template) + { + _template = null == template || template.isEmpty() ? Map.of() : template; + _map = new LinkedHashMap(); + } + + void put(String key, Object value) + { + if (!_template.containsKey(key) || !Objects.equals(_template.get(key), value)) + _map.put(key, value); + } + + Map getMap() + { + return _map; + } + } + + /** put property into props map if value differs from template */ + void templatePut(Map props, String key, Object value, Map template) + { + if (template == null || !template.containsKey(key) || Objects.equals(template.get(key), value)) + props.put(key, value); + } + + public static Map getMetaData(DisplayColumn dc, FieldKey fieldKeyPrefix, @Nullable Map template, boolean useFriendlyAsType, boolean includeLookup, boolean includeDomainFormat) + { + /* there are already a lot of parameters so use null!=template to indicate that we are "compression" the response */ + boolean compressed = null != template; @Nullable ColumnInfo cinfo = dc.getColumnInfo(); - Map props = new LinkedHashMap<>(); + var props = new TemplateMap(template); JSONObject ext = new JSONObject(); props.put("ext", ext); @@ -315,7 +374,18 @@ else if (cinfo.getFacetingBehaviorType() != null) props.put("conceptLabelColumn", cinfo.getConceptLabelColumn()); } - return props; + var ret = props.getMap(); + if (compressed) + { + if (ret.get("ext") instanceof JSONObject extProp && extProp.isEmpty()) + ret.remove("ext"); + for (String key : List.of("fieldKey", "fieldKeyPath", "shortCaption", "caption")) + { + if (name.equals(ret.get(key))) + ret.remove(key); + } + } + return ret; } @Nullable diff --git a/query/src/org/labkey/query/CustomViewUtil.java b/query/src/org/labkey/query/CustomViewUtil.java index 219cd824e13..6c9fa124c9d 100644 --- a/query/src/org/labkey/query/CustomViewUtil.java +++ b/query/src/org/labkey/query/CustomViewUtil.java @@ -181,7 +181,7 @@ public static Map toMap(ViewContext context, UserSchema schema, } } - Map ret = toMap(view, context.getUser(), includeFieldMeta, columnMetadata); + Map ret = toMap(view, context.getUser(), includeFieldMeta, false, columnMetadata); if (newView) ret.put("doesNotExist", true); @@ -191,10 +191,10 @@ public static Map toMap(ViewContext context, UserSchema schema, public static Map toMap(CustomView view, @NotNull User user, boolean includeFieldMeta) { assert user != null; - return toMap(view, user, includeFieldMeta, new HashMap<>()); + return toMap(view, user, includeFieldMeta, false, new HashMap<>()); } - public static Map toMap(CustomView view, @NotNull User user, boolean includeFieldMeta, Map> columnMetadata) + public static Map toMap(CustomView view, @NotNull User user, boolean includeFieldMeta, boolean compressed, Map> columnMetadata) { assert user != null; Map ret = QueryService.get().getCustomViewProperties(view, user); @@ -239,14 +239,31 @@ public static Map toMap(CustomView view, @NotNull User user, boo allKeys.add(entry.getKey()); Map colInfo = new LinkedHashMap<>(); - colInfo.put("name", entry.getKey().getName()); - colInfo.put("key", entry.getKey().toString()); - colInfo.put("fieldKey", entry.getKey().toString()); - if (!entry.getValue().isEmpty()) + var name = entry.getKey().getName(); + colInfo.put("name", name); + if (compressed) { - String columnTitle = entry.getValue().get(CustomViewInfo.ColumnProperty.columnTitle); - if (columnTitle != null) - colInfo.put("title", columnTitle); + var fieldkey = entry.getKey().toString(); + if (!Objects.equals(name, fieldkey)) + colInfo.put("fieldkey", fieldkey); + if (!entry.getValue().isEmpty()) + { + String columnTitle = entry.getValue().get(CustomViewInfo.ColumnProperty.columnTitle); + if (columnTitle != null && !Objects.equals(columnTitle, name)) + colInfo.put("title", columnTitle); + } + } + else + { + colInfo.put("name", entry.getKey().getName()); + colInfo.put("key", entry.getKey().toString()); + colInfo.put("fieldKey", entry.getKey().toString()); + if (!entry.getValue().isEmpty()) + { + String columnTitle = entry.getValue().get(CustomViewInfo.ColumnProperty.columnTitle); + if (columnTitle != null) + colInfo.put("title", columnTitle); + } } colInfos.add(colInfo); } diff --git a/query/src/org/labkey/query/controllers/GetQueryDetails2Action.java b/query/src/org/labkey/query/controllers/GetQueryDetails2Action.java new file mode 100644 index 00000000000..9b562a54d4f --- /dev/null +++ b/query/src/org/labkey/query/controllers/GetQueryDetails2Action.java @@ -0,0 +1,19 @@ +package org.labkey.query.controllers; + +import org.labkey.api.action.Action; +import org.labkey.api.action.ActionType; +import org.labkey.api.action.ApiResponse; +import org.labkey.api.security.RequiresPermission; +import org.labkey.api.security.permissions.ReadPermission; +import org.springframework.validation.BindException; + +@RequiresPermission(ReadPermission.class) +@Action(ActionType.SelectMetaData.class) +public class GetQueryDetails2Action extends GetQueryDetailsAction +{ + @Override + public ApiResponse execute(Form form, BindException errors) + { + return _execute(form, true); + } +} diff --git a/query/src/org/labkey/query/controllers/GetQueryDetailsAction.java b/query/src/org/labkey/query/controllers/GetQueryDetailsAction.java index c67d692b309..338e3636cc0 100644 --- a/query/src/org/labkey/query/controllers/GetQueryDetailsAction.java +++ b/query/src/org/labkey/query/controllers/GetQueryDetailsAction.java @@ -87,8 +87,12 @@ protected long getLastModified(Form form) return QueryService.get().metadataLastModified(); } - @Override - public ApiResponse execute(Form form, BindException errors) + @Override public ApiResponse execute(Form form, BindException errors) + { + return _execute(form, false); + } + + protected ApiResponse _execute(Form form, boolean compressed) { ApiSimpleResponse resp = new ApiSimpleResponse(); @@ -223,6 +227,13 @@ public ApiResponse execute(Form form, BindException errors) Map> columnMetadata; + Map templateColumn = null; + if (compressed) + { + templateColumn = JsonWriter.getTemplateColProps(tinfo); + resp.put("templateColumn", templateColumn); + } + //if the caller asked us to chase a foreign key, do that. Note that any call to get a lookup table can throw a // QueryParseException, so we wrap all FK accesses in a try/catch. try @@ -270,7 +281,7 @@ public ApiResponse execute(Form form, BindException errors) //now the native columns plus any additional fields requested. Use a copy of the values // in the reference columnMetadata map, as it may get additional entries later when the view columns are serialized - columnMetadata = JsonWriter.getNativeColProps(tinfo, fields, fk, false, form.isIncludeSuggestedQueryColumns()); + columnMetadata = JsonWriter.getNativeColProps(tinfo, fields, fk, templateColumn, false, form.isIncludeSuggestedQueryColumns()); resp.put("columns", new ArrayList<>(columnMetadata.values())); // table indices @@ -316,7 +327,7 @@ public ApiResponse execute(Form form, BindException errors) { //now the columns in the user's default view for this query QueryView qview = new QueryView(schema, settings, null); - resp.put("defaultView", getDefaultViewProps(qview)); + resp.put("defaultView", getDefaultViewProps(qview, templateColumn)); List> viewInfos = new ArrayList<>(); Map allViews = queryDef.getCustomViews(getUser(), getViewContext().getRequest(), true, false); @@ -331,11 +342,11 @@ public ApiResponse execute(Form form, BindException errors) { if (cv.getName() == null) hasDefault = true; - viewInfos.add(CustomViewUtil.toMap(cv, getUser(), true, columnMetadata)); + viewInfos.add(CustomViewUtil.toMap(cv, getUser(), true, compressed, columnMetadata)); } if (!hasDefault) - viewInfos.add(CustomViewUtil.toMap(queryDef.createCustomView(getUser(), null), getUser(), true, columnMetadata)); + viewInfos.add(CustomViewUtil.toMap(queryDef.createCustomView(getUser(), null), getUser(), true, compressed, columnMetadata)); } else { @@ -348,7 +359,7 @@ public ApiResponse execute(Form form, BindException errors) if (null == cv && (viewName == null || form.isInitializeMissingView())) cv = queryDef.createCustomView(getUser(), viewName); if (null != cv) - viewInfos.add(CustomViewUtil.toMap(cv, getUser(), true, columnMetadata)); + viewInfos.add(CustomViewUtil.toMap(cv, getUser(), true, compressed, columnMetadata)); } } @@ -424,20 +435,20 @@ public ApiResponse execute(Form form, BindException errors) return resp; } - protected Map getDefaultViewProps(QueryView view) + protected Map getDefaultViewProps(QueryView view, Map columnTemplate) { Map defViewProps = new HashMap<>(); - defViewProps.put("columns", getDefViewColProps(view)); + defViewProps.put("columns", getDefViewColProps(view, columnTemplate)); return defViewProps; } - protected List> getDefViewColProps(QueryView view) + protected List> getDefViewColProps(QueryView view, Map columnTemplate) { List> colProps = new ArrayList<>(); for (DisplayColumn dc : view.getDisplayColumns()) { if (dc.isQueryColumn() && null != dc.getColumnInfo()) - colProps.add(JsonWriter.getMetaData(dc, null, true, true, false)); + colProps.add(JsonWriter.getMetaData(dc, null, columnTemplate, true, true, false)); } return colProps; } diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index 4edbf1d86a6..91d06024e71 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -262,6 +262,7 @@ public class QueryController extends SpringActionController ValidateQueriesAction.class, GetSchemaQueryTreeAction.class, GetQueryDetailsAction.class, + GetQueryDetails2Action.class, ViewQuerySourceAction.class ); @@ -6867,7 +6868,7 @@ protected Map getQueryProps(QueryDefinition qdef, ActionURL view if (useQueryDetailColumns) { columns = JsonWriter - .getNativeColProps(table, Collections.emptyList(), null, false, false) + .getNativeColProps(table, Collections.emptyList(), null, null, false, false) .values(); } else