Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c298fdc
quantity type prototype
labkey-matthewb Dec 20, 2024
37f9cb9
imports
labkey-matthewb May 15, 2025
60b2e5e
Merge remote-tracking branch 'origin/develop' into fb_quantity
labkey-matthewb May 20, 2025
127b97e
Merge remote-tracking branch 'origin/release24.11-SNAPSHOT' into 24.1…
labkey-matthewb May 20, 2025
0bea798
merge patch
labkey-matthewb May 20, 2025
345c146
merge patch
labkey-matthewb May 20, 2025
e37d898
java 17
labkey-matthewb May 20, 2025
4e19852
merge forward
labkey-matthewb May 20, 2025
0327635
update expected result
labkey-matthewb May 20, 2025
c344188
build problem
labkey-matthewb May 22, 2025
a09337c
I wasn't really planning on commiting this for a while. But this mak…
labkey-matthewb May 22, 2025
cedae1c
Merge branch 'develop' into fb_quantity
cnathe Aug 5, 2025
f2ed404
fixup after merge from develop
cnathe Aug 5, 2025
cecd9ac
Merge branch 'develop' into fb_quantity
cnathe Aug 12, 2025
eafbf25
Add QUANTITY_COLUMN_SUFFIX_TESTING experimental feature flag
cnathe Aug 12, 2025
c65b82b
Merge remote-tracking branch 'origin/develop' into fb_quantity
labkey-susanh Aug 13, 2025
cc7c908
Merge remote-tracking branch 'origin/develop' into fb_quantity
labkey-susanh Aug 14, 2025
7f1e3df
Add "unit" unit to reflect current usage and some minor helper methods
labkey-susanh Aug 14, 2025
c28adfb
Quantity.java unit tests
cnathe Aug 14, 2025
ae9d244
Set Unit enum base to "unit" and "ml"
cnathe Aug 14, 2025
8f2d333
fixup Quantity testFormat test case
cnathe Aug 14, 2025
dbe0f43
Unit.java unit tests and set ml base unit value as 1 instead of liter
cnathe Aug 14, 2025
e9cf18d
CR feedback
cnathe Aug 14, 2025
137fd3f
Merge branch 'develop' into fb_quantity
cnathe Aug 15, 2025
1dfd993
remove no_unit from Unit enum
cnathe Aug 15, 2025
d1eae38
Merge branch 'develop' into fb_quantity
cnathe Aug 18, 2025
9c5c41f
update QNode unit test type
cnathe Aug 18, 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
19 changes: 15 additions & 4 deletions api/src/org/labkey/api/data/BaseColumnInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
import org.labkey.api.query.QueryParseException;
import org.labkey.api.query.SchemaKey;
import org.labkey.api.query.column.BuiltInColumnTypes;
import org.labkey.api.settings.AppProps;
import org.labkey.api.settings.OptionalFeatureService;
import org.labkey.api.util.PageFlowUtil;
import org.labkey.api.util.StringExpression;
import org.labkey.api.util.StringExpressionFactory;
Expand Down Expand Up @@ -1353,12 +1355,21 @@ public void setSortFieldKeysFromXml(String xml)

public static String labelFromName(String name)
{
if (name == null)
return null;

if (name.isEmpty())
if (StringUtils.isBlank(name))
return name;

// NOTE: This is just for testing (let the DataRegion/DataColumn do this)
if (OptionalFeatureService.get().isFeatureEnabled(AppProps.QUANTITY_COLUMN_SUFFIX_TESTING))
{
var index = name.indexOf("__");
if (index > 0)
{
String unit = name.substring(index + 2);
if (null != org.labkey.api.ontology.Unit.fromName(unit))
name = name.substring(0, index);
}
}

StringBuilder buf = new StringBuilder(name.length() + 10);
char[] chars = new char[name.length()];
name.getChars(0, name.length(), chars, 0);
Expand Down
145 changes: 144 additions & 1 deletion api/src/org/labkey/api/data/ColumnRenderProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,28 @@
*/
package org.labkey.api.data;

import org.apache.commons.beanutils.ConvertUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.labkey.api.exp.PropertyType;
import org.labkey.api.gwt.client.DefaultScaleType;
import org.labkey.api.gwt.client.FacetingBehaviorType;
import org.labkey.api.ontology.KindOfQuantity;
import org.labkey.api.ontology.OntologyService;
import org.labkey.api.ontology.Quantity;
import org.labkey.api.ontology.Unit;
import org.labkey.api.query.FieldKey;
import org.labkey.api.settings.AppProps;
import org.labkey.api.settings.OptionalFeatureService;
import org.labkey.api.util.StringExpression;
import org.labkey.api.util.logging.LogHelper;

import java.io.File;
import java.math.BigDecimal;
import java.text.DecimalFormatSymbols;
import java.util.Date;
import java.util.Set;
import java.util.function.Function;

import static org.labkey.api.ontology.OntologyService.conceptCodeConceptURI;

Expand Down Expand Up @@ -199,7 +208,7 @@ else if (Date.class.isAssignableFrom(javaClass))
*/
boolean isScannable();

/* Properties loaded by OntologyService */
/* Properties loaded by OntologyService */

// any column can be annotated with PrincipalConceptCode
default String getPrincipalConceptCode()
Expand Down Expand Up @@ -245,11 +254,145 @@ default String getConceptLabelColumn()
{
return null;
}

default KindOfQuantity getKindOfQuantity()
{
var unit = getDisplayUnit();
if (null == unit)
return null;
return unit.getKindOfQuantity();
}

default Unit getDisplayUnit()
{
if (OptionalFeatureService.get().isFeatureEnabled(AppProps.QUANTITY_COLUMN_SUFFIX_TESTING))
{
if (!getJdbcType().isNumeric())
return null;
String name = getName();
var index = name.lastIndexOf("__");
if (index < 0)
return null;
var unitPart = name.substring(index + 2);
try
{
return Unit.valueOf(unitPart);
}
catch (IllegalArgumentException x)
{
// pass
}
}
return null;
}


/* End properties loaded by OntologyService */


default String getDerivationDataScope()
{
return null;
}


/**
* This Format can be used for low-level conversion of the type represented by this column. It does handle
* basic numeric/date conversion including formats, and default display unit handling. That's about it.
* It never produces HTML
* It does not handle compound/column formatting (missing values, oor, etc)
* It does not handle conditional formatting
* This method moves (most) of the work formerly done in DisplayColumn.formatValue() to a shared location (getFormat())
* Likewise for SimpleConvertColumn.simpleConvert() (getConvert())
*/
@Transient
default Function<Object, String> getFormatFn()
{
return getDefaultFormatFn(getName(), getJavaObjectClass(), getDisplayUnit(), getFormat(), null);
}

@Transient
default Function<Object, String> getTsvFormatFn()
{
return getDefaultFormatFn(getName(), getJavaObjectClass(), getDisplayUnit(), getTsvFormatString(), DisplayColumn.tsvFormatSymbols);
}

@Transient
default Function<Object,Object> getConvertFn()
{
return getDefaultConvertFn(this);
}

static Function<Object, String> getDefaultFormatFn(String colName, Class javaObjectClass, final Unit displayUnit, String formatString, DecimalFormatSymbols dfs)
{
final var format = null==formatString ? null : DisplayColumn.createFormat(formatString, javaObjectClass, dfs);

if (null == format && null == displayUnit)
{
return (value) -> null==value ? "" : value instanceof String ? (String)value : ConvertUtils.convert(value);
}

return (value) ->
{
if (null == value)
return "";

@NotNull String formattedString;
if (null != displayUnit && value instanceof Number)
{
Quantity q = (value instanceof Quantity) ?
(Quantity) value :
displayUnit.getKindOfQuantity().toQuantity((Number) value);
var doubleValue = q.doubleValue(displayUnit);
if (null == format)
formattedString = ConvertUtils.convert(doubleValue);
else
formattedString = format.format(doubleValue);
}
else if (null != format)
{
try
{
formattedString = format.format(value);
}
catch (IllegalArgumentException e)
{
LogHelper.getLogger(ColumnRenderProperties.class, "Column metadata").warn("Unable to apply format to {} value \"{}\" for column \"{}\", likely a SQL type mismatch between XML metadata and actual ResultSet", value.getClass().getName(), value, colName);
formattedString = ConvertUtils.convert(value);
}
}
else if (value instanceof String)
{
formattedString = (String) value;
}
else
{
formattedString = ConvertUtils.convert(value);
}

return formattedString;
};
}

/* empty string -> null */
static Function<Object,Object> getDefaultConvertFn(ColumnRenderProperties col)
{
final Class<?> javaClass = col.getJavaObjectClass();
final var defaultUnit = col.getDisplayUnit();
final @NotNull var jdbcType = col.getJdbcType();

if (null == defaultUnit)
{
return (value) ->
{
// quick check for unnecessary conversion
if (value == null || javaClass == value.getClass())
return value;
if (value instanceof CharSequence)
ConvertUtils.convert(value.toString(), javaClass);
return jdbcType.convert(value);
};
}
return defaultUnit::convert;
}
}
21 changes: 19 additions & 2 deletions api/src/org/labkey/api/data/ColumnRenderPropertiesImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.labkey.api.gwt.client.DefaultScaleType;
import org.labkey.api.gwt.client.DefaultValueType;
import org.labkey.api.gwt.client.FacetingBehaviorType;
import org.labkey.api.ontology.Unit;
import org.labkey.api.query.FieldKey;
import org.labkey.api.util.StringExpression;

Expand Down Expand Up @@ -785,11 +786,27 @@ public final Class<?> getJavaClass()
@Override
public Class<?> getJavaClass(boolean isNullable)
{
Class<?> ret;
boolean isNumeric;
PropertyType pt = getPropertyType();
if (pt != null)
return pt.getJavaType();
{
ret = pt.getJavaType();
isNumeric = pt.getJdbcType().isNumeric();
}
else
{
ret = getJdbcType().getJavaClass(isNullable);
isNumeric = getJdbcType().isNumeric();
}

return getJdbcType().getJavaClass(isNullable);
if (isNumeric)
{
Unit unit = getDisplayUnit();
if (null != unit)
return unit.getQuantityClass();
}
return ret;
}

@Override
Expand Down
18 changes: 14 additions & 4 deletions api/src/org/labkey/api/data/DataColumn.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import org.labkey.api.exp.property.PropertyService;
import org.labkey.api.gwt.client.DefaultValueType;
import org.labkey.api.gwt.client.model.PropertyValidatorType;
import org.labkey.api.ontology.Quantity;
import org.labkey.api.ontology.Unit;
import org.labkey.api.query.DetailsURL;
import org.labkey.api.query.FieldKey;
import org.labkey.api.query.QueryParseException;
Expand Down Expand Up @@ -569,7 +571,7 @@ public HtmlString getFormattedHtml(RenderContext ctx)
}
else
{
String formatted = formatValue(ctx, value, getTextExpressionCompiled(ctx), getFormat());
String formatted = formatValue(ctx, value, getTextExpressionCompiled(ctx), getFormat(), getDisplayUnit());

if (getRequiresHtmlFiltering())
formatted = PageFlowUtil.filter(formatted);
Expand Down Expand Up @@ -623,12 +625,18 @@ protected String getSelectInputDisplayValue(NamedObject entry)
return entry.getObject().toString();
}

protected String getStringValue(Object value, boolean disabledInput)
protected String getStringValue(Object value, Unit unit, boolean disabledInput)
{
String strVal = "";
//UNDONE: Should use output format here.
if (null != value)
{
if (unit != null && value instanceof Number num)
{
Quantity quantity = (value instanceof Quantity q) ? q : unit.getKindOfQuantity().toQuantity(num);
value = quantity.value(unit);
}

// 4934: Don't render form input values with formatter since we don't parse formatted inputs on post.
// For now, we can at least render disabled inputs with formatting since a
// hidden input with the actual value is emitted for disabled items.
Expand Down Expand Up @@ -657,7 +665,7 @@ public void renderInputHtml(RenderContext ctx, HtmlWriter out, Object value)

boolean disabledInput = isDisabledInput(ctx);
final String formFieldName = getFormFieldName(ctx);
String strVal = getStringValue(value, disabledInput);
String strVal = getStringValue(value, _boundColumn.getDisplayUnit(), disabledInput);

if (_boundColumn.isAutoIncrement())
{
Expand Down Expand Up @@ -940,7 +948,9 @@ public String getSortHandler(RenderContext ctx, Sort.SortDirection sort)
// TODO: Treat null and empty the same instead?
if (_caption == null)
return null;
String title = _caption.eval(ctx);
var title = _caption.eval(ctx);
if (null != _displayColumn && null != _displayColumn.getDisplayUnit() && !StringUtils.isEmpty(_displayColumn.getDisplayUnit().toString()))
title += " (" + _displayColumn.getDisplayUnit() + ")";
return title.isEmpty() ? HtmlString.NBSP : HtmlString.of(title);
}

Expand Down
Loading