From 09b8c50963633777b36fd668f6903eece450eeb0 Mon Sep 17 00:00:00 2001 From: labkey-matthewb Date: Mon, 9 Feb 2026 17:01:51 -0800 Subject: [PATCH 1/4] prefer convert to java.sql.Date/Time/Timestamp over java.util.Date --- .../org/labkey/api/data/ColumnInfoTests.jsp | 49 ++++++++++++++++--- api/src/org/labkey/api/exp/PropertyType.java | 4 +- api/src/org/labkey/api/util/DateUtil.java | 2 +- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/api/src/org/labkey/api/data/ColumnInfoTests.jsp b/api/src/org/labkey/api/data/ColumnInfoTests.jsp index d7c901cd073..82ef6a79d29 100644 --- a/api/src/org/labkey/api/data/ColumnInfoTests.jsp +++ b/api/src/org/labkey/api/data/ColumnInfoTests.jsp @@ -25,6 +25,7 @@ This tests uses MockRequest to test some expected Headers and Meta tags for vari var result = col.convert(val); assertNotNull(result); //assertEquals(col.getJdbcType().getJavaClass(), result.getClass()); + assertEquals(expected.getClass(), result.getClass()); assertEquals(expected, result); } @@ -159,9 +160,27 @@ This tests uses MockRequest to test some expected Headers and Meta tags for vari case INTEGER -> {} case REAL -> {} case SMALLINT, TINYINT -> {} - case DATE -> {} - case TIME -> {} - case TIMESTAMP -> {} + case DATE -> + { + testConvert(type, java.sql.Date.valueOf("2024-01-15"), "2024-01-15"); + testConvert(type, java.sql.Date.valueOf("2024-01-15"), java.sql.Date.valueOf("2024-01-15")); + testConvertsToNull(type, null); + testConvertsToNull(type, ""); + } + case TIME -> + { + testConvert(type, java.sql.Time.valueOf("14:30:00"), "14:30:00"); + testConvert(type, java.sql.Time.valueOf("14:30:00"), java.sql.Time.valueOf("14:30:00")); + testConvertsToNull(type, null); + testConvertsToNull(type, ""); + } + case TIMESTAMP -> + { + testConvert(type, java.sql.Timestamp.valueOf("2024-01-15 14:30:00"), "2024-01-15 14:30:00"); + testConvert(type, java.sql.Timestamp.valueOf("2024-01-15 14:30:00"), java.sql.Timestamp.valueOf("2024-01-15 14:30:00")); + testConvertsToNull(type, null); + testConvertsToNull(type, ""); + } case GUID -> {} case ARRAY, NULL, OTHER -> { /* ignore */ } default -> fail("We missed a JdbcType: " + type.name()); @@ -203,9 +222,27 @@ This tests uses MockRequest to test some expected Headers and Meta tags for vari case BINARY -> {} case FILE_LINK -> {} case ATTACHMENT -> {} - case DATE_TIME -> {} - case DATE -> {} - case TIME -> {} + case DATE_TIME -> + { + testConvert(type, java.sql.Timestamp.valueOf("2024-01-15 14:30:00"), "2024-01-15 14:30:00"); + testConvert(type, java.sql.Timestamp.valueOf("2024-01-15 14:30:00"), java.sql.Timestamp.valueOf("2024-01-15 14:30:00")); + testConvertsToNull(type, null); + testConvertsToNull(type, ""); + } + case DATE -> + { + testConvert(type, java.sql.Date.valueOf("2024-01-15"), "2024-01-15"); + testConvert(type, java.sql.Date.valueOf("2024-01-15"), java.sql.Date.valueOf("2024-01-15")); + testConvertsToNull(type, null); + testConvertsToNull(type, ""); + } + case TIME -> + { + testConvert(type, java.sql.Time.valueOf("14:30:00"), "14:30:00"); + testConvert(type, java.sql.Time.valueOf("14:30:00"), java.sql.Time.valueOf("14:30:00")); + testConvertsToNull(type, null); + testConvertsToNull(type, ""); + } case DOUBLE -> {} case FLOAT -> {} case DECIMAL -> {} diff --git a/api/src/org/labkey/api/exp/PropertyType.java b/api/src/org/labkey/api/exp/PropertyType.java index 6cf8a5dd32c..8d42cc373a2 100644 --- a/api/src/org/labkey/api/exp/PropertyType.java +++ b/api/src/org/labkey/api/exp/PropertyType.java @@ -593,8 +593,6 @@ protected Object convertExcelValue(Cell cell) throws ConversionException { throw new ConversionException(e); } -// int offset = TimeZone.getDefault().getOffset(date.getTime()); -// date.setTime(date.getTime() - offset); } return date; } @@ -611,7 +609,7 @@ public Object convert(Object value) throws ConversionException String strVal = value.toString(); if (DateUtil.isSignedDuration(strVal)) strVal = JdbcType.TIMESTAMP.convert(value).toString(); - return ConvertUtils.convert(strVal, Date.class); + return ConvertUtils.convert(strVal, java.sql.Timestamp.class); } } diff --git a/api/src/org/labkey/api/util/DateUtil.java b/api/src/org/labkey/api/util/DateUtil.java index c919028cde2..0e4ab7c1755 100644 --- a/api/src/org/labkey/api/util/DateUtil.java +++ b/api/src/org/labkey/api/util/DateUtil.java @@ -1444,7 +1444,7 @@ public static Date getDateOnly(@Nullable Date fullDate) int month = fullDate.getMonth(); int date = fullDate.getDate(); - return new Date(year, month, date); + return new java.sql.Date(year, month, date); } public static TimeZone getTimeZone() From 191a27901dd39c753fdc7409cb9cdf53cd4ce7eb Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 23 Feb 2026 09:41:40 -0800 Subject: [PATCH 2/4] Always format datetime with datetime format --- .../labkey/api/util/SubstitutionFormat.java | 1378 +++++++++-------- 1 file changed, 690 insertions(+), 688 deletions(-) diff --git a/api/src/org/labkey/api/util/SubstitutionFormat.java b/api/src/org/labkey/api/util/SubstitutionFormat.java index 1dc46cf36f9..034fca6b7f9 100644 --- a/api/src/org/labkey/api/util/SubstitutionFormat.java +++ b/api/src/org/labkey/api/util/SubstitutionFormat.java @@ -1,688 +1,690 @@ -/* - * Copyright (c) 2013-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.util; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.labkey.api.collections.CaseInsensitiveHashMap; -import org.labkey.api.data.NameGenerator; -import org.labkey.api.exp.api.SampleTypeService; - -import java.sql.Time; -import java.text.DecimalFormat; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAccessor; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.labkey.api.data.NameGenerator.SAMPLE_COUNTER_SUBSTITUTIONS; - -/** - * These are the supported formatting functions that can be used with string substitution, for example, when substituting - * values into a details URL or the javaScriptEvents property of a JavaScriptDisplayColumnFactory. The function definitions - * are patterned off Ext.util.Format (formats used in ExtJs templates), http://docs.sencha.com/extjs/4.2.1/#!/api/Ext.util.Format - * - * Examples: - *
- *  ${Name:htmlEncode}
- *  ${MyParam:urlEncode}
- *
- *  ${MyParam:defaultValue('foo')}
- *
- *  ${MyDate:date}
- *  ${MyDate:date('yy-MM d')}
- *
- *  ${MyNumber:number('')}
- *
- *  ${MyParam:trim}
- *
- *  ${MyParam:prefix('-')}
- *  ${MyParam:suffix('-')}
- *  ${MyParam:join('-')}
- *
- *  ${MyParam:first}
- *  ${MyParam:rest}
- *  ${MyParam:last}
- *
- * 
- * We should add more functions and allow parametrized functions. As we add functions, we should use the Ext names and - * parameters if at all possible. - * - * User: adam - * Date: 6/20/13 - */ -public class SubstitutionFormat -{ - static final SubstitutionFormat passThrough = new SubstitutionFormat("passThrough", "none") { - @Override - public Object format(Object value) - { - if (value instanceof Time) - return defaultTimeFormat.format(value); - else if (value instanceof java.sql.Date) - return date.format(value); - else if (value instanceof Date dateVal) - { - // both date and datetime column type are of type Date, format based on time portion of the date value - LocalDateTime ldt = LocalDateTime.ofInstant(dateVal.toInstant(), ZoneId.systemDefault()); - boolean isDateOnly = ldt.toLocalTime().equals(java.time.LocalTime.MIDNIGHT); - - if (isDateOnly) - return date.format(value); - else - return defaultDateTimeFormat.format(value).replace('T', ' '); // replace T with whitespace for human readability - } - - return value; - } - - @Override - public int argumentCount() - { - return 0; - } - }; - - static final SubstitutionFormat htmlEncode = new SubstitutionFormat("htmlEncode", "html") - { - @Override - public String format(Object value) - { - if (value == null) - return null; - return PageFlowUtil.filter(value); - } - - @Override - public int argumentCount() - { - return 0; - } - }; - - static final SubstitutionFormat jsString = new SubstitutionFormat("jsString", "jsString") - { - @Override - public String format(Object value) - { - if (value == null) - return null; - return PageFlowUtil.jsString(String.valueOf(value)); - } - - - @Override - public int argumentCount() - { - return 0; - } - }; - - static final SubstitutionFormat urlEncode = new SubstitutionFormat("urlEncode", "path") - { - @Override - public String format(Object value) - { - if (value == null) - return null; - return PageFlowUtil.encodePath(String.valueOf(value)); - } - - - @Override - public int argumentCount() - { - return 0; - } - }; - - // like javascript encodeURIComponent - static final SubstitutionFormat encodeURIComponent = new SubstitutionFormat("encodeURIComponent", "uricomponent") - { - @Override - public String format(Object value) - { - if (value == null) - return null; - return PageFlowUtil.encodeURIComponent(String.valueOf(value)); - } - - - @Override - public int argumentCount() - { - return 0; - } - }; - - // like javascript encodeURI - static final SubstitutionFormat encodeURI = new SubstitutionFormat("encodeURI", "uri") - { - @Override - public String format(Object value) - { - if (value == null) - return null; - return PageFlowUtil.encodeURI(String.valueOf(value)); - } - - - @Override - public int argumentCount() - { - return 0; - } - }; - - static final SubstitutionFormat first = new SubstitutionFormat("first") - { - @Override - public Object format(Object value) - { - if (value == null) - return null; - - if (!(value instanceof Collection)) - return value; - - Collection c = (Collection)value; - return c.stream().findFirst().orElse(null); - } - - - @Override - public int argumentCount() - { - return 0; - } - }; - - static final SubstitutionFormat rest = new SubstitutionFormat("rest") - { - @Override - public Object format(Object value) - { - if (value == null) - return null; - - if (!(value instanceof Collection)) - return value; - - Collection c = (Collection)value; - return c.stream().skip(1).collect(Collectors.toList()); - } - - @Override - public int argumentCount() - { - return 0; - } - }; - - static final SubstitutionFormat last = new SubstitutionFormat("last") - { - @Override - public Object format(Object value) - { - if (value == null) - return null; - - if (!(value instanceof Collection)) - return value; - - Collection c = (Collection)value; - return c.stream().reduce((a, b) -> b).orElse(null); - } - - @Override - public int argumentCount() - { - return 0; - } - }; - - static final SubstitutionFormat trim = new SubstitutionFormat("trim") - { - @Override - public String format(Object value) - { - if (value == null) - return null; - - if (!(value instanceof String)) - throw new IllegalArgumentException("Expected string: " + value); - - return ((String)value).trim(); - } - - @Override - public int argumentCount() - { - return 0; - } - }; - - public static class DefaultSubstitutionFormat extends SubstitutionFormat - { - private final String _default; - - public DefaultSubstitutionFormat(@NotNull String def) - { - super("defaultValue"); - _default = def; - } - - @Override - public Object format(Object value) - { - if (value == null || "".equals(value)) - return _default; - - return value; - } - } - static final SubstitutionFormat defaultValue = new DefaultSubstitutionFormat(""); - - public static class MinValueSubstitutionFormat extends SubstitutionFormat - { - private final long _min; - - public MinValueSubstitutionFormat(@NotNull String minValue) - { - super("minValue"); - long value = 0; - try - { - value = (long) Float.parseFloat(minValue); - } - catch (NumberFormatException e) - { - } - - _min = value; - } - - @Override - public Object format(Object value) - { - if (value == null) - return null; - - if (!(value instanceof Number)) - throw new IllegalArgumentException("Expected number: " + value); - - long numberVal = ((Number) value).longValue(); - return Math.max(_min, numberVal); - } - - @Override - public boolean argumentQuotesOptional() { return true; } - } - static final SubstitutionFormat minValue = new MinValueSubstitutionFormat(""); - - public static class JoinSubstitutionFormat extends SubstitutionFormat - { - private final String _sep; - private final String _prefix; - private final String _suffix; - - public JoinSubstitutionFormat(@NotNull String sep) - { - super("join"); - _sep = sep; - _prefix = ""; - _suffix = ""; - } - - public JoinSubstitutionFormat(@NotNull String sep, @NotNull String prefix, @NotNull String suffix) - { - super("join"); - _sep = sep; - _prefix = prefix; - _suffix = suffix; - } - - @Override - public String format(Object value) - { - if (value == null) - return null; - - Stream ss; - if (value instanceof Collection) - { - Collection c = (Collection)value; - if (c.isEmpty()) - return ""; - - ss = c.stream().map(String::valueOf); - } - else - { - String s = String.valueOf(value); - if (s.isEmpty()) - return ""; - - ss = Stream.of(s); - } - - return ss.collect(Collectors.joining(_sep, _prefix, _suffix)); - } - } - static final SubstitutionFormat join = new JoinSubstitutionFormat(""); - static final SubstitutionFormat prefix = new JoinSubstitutionFormat("", "", ""); - static final SubstitutionFormat suffix = new JoinSubstitutionFormat("", "", ""); - - public static class DateSubstitutionFormat extends SubstitutionFormat - { - // NOTE: We use DateTimeFormatter since it is thread-safe unlike SimpleDateFormat - final DateTimeFormatter _format; - - public DateSubstitutionFormat(@NotNull DateTimeFormatter format) - { - super("date"); - _format = format; - } - - @Override - public String format(Object value) - { - if (value == null) - return null; - - if (value instanceof java.sql.Date || value instanceof Time) - value = new Date(((Date)value).getTime()); - - TemporalAccessor temporal; - if (value instanceof TemporalAccessor) - temporal = (TemporalAccessor) value; - else if (value instanceof Date) - temporal = LocalDateTime.ofInstant(((Date)value).toInstant(), ZoneId.systemDefault()); - else - throw new IllegalArgumentException("Expected date: " + value); - - return _format.format(temporal); - } - - @Override - public boolean argumentOptional() { return true; } - } - - static final SubstitutionFormat date = new DateSubstitutionFormat(DateTimeFormatter.BASIC_ISO_DATE.withZone(ZoneId.systemDefault())); - static final DateSubstitutionFormat defaultDateTimeFormat = new SubstitutionFormat.DateSubstitutionFormat(DateTimeFormatter.ISO_DATE_TIME); - static final DateSubstitutionFormat defaultTimeFormat = new SubstitutionFormat.DateSubstitutionFormat(DateTimeFormatter.ISO_TIME); - - public static class NumberSubstitutionFormat extends SubstitutionFormat - { - final DecimalFormat _format; - - public NumberSubstitutionFormat(String formatString) - { - super("number"); - _format = new DecimalFormat(formatString); - } - - @Override - public Object format(Object value) - { - if (value == null) - return null; - - if (!(value instanceof Number)) - throw new IllegalArgumentException("Expected number: " + value); - - return _format.format(value); - } - } - static final SubstitutionFormat number = new NumberSubstitutionFormat(""); //used for validation only - - public static class SampleCountSubstitutionFormat extends SubstitutionFormat - { - private final Object _previewCount; - SampleCountSubstitutionFormat(String name, Object previewCount) - { - super(name); - _previewCount = previewCount; - } - - @Override - public boolean hasSideEffects() - { - return true; - } - - @Override - public Object format(Object value) - { - Date date = null; - if (value instanceof Date) - date = (Date)value; - - // Increment sample counters for the given date or today's date if null - // TODO: How can we check if we have incremented sample counters for this same date within the current context/row? - Map counts = SampleTypeService.get().incrementSampleCounts(date); - return counts.get(_name); - } - - @Override - public Object format(Object value, boolean _isPreview) - { - if (_isPreview) - return _previewCount; - - return format(value); - } - - @Override - public int argumentCount() { return 0; } - } - - public static SampleCountSubstitutionFormat dailySampleCount = new SampleCountSubstitutionFormat("dailySampleCount", NameGenerator.SubstitutionValue.dailySampleCount.getPreviewValue()); - public static SampleCountSubstitutionFormat weeklySampleCount = new SampleCountSubstitutionFormat("weeklySampleCount", NameGenerator.SubstitutionValue.weeklySampleCount.getPreviewValue()); - public static SampleCountSubstitutionFormat monthlySampleCount = new SampleCountSubstitutionFormat("monthlySampleCount", NameGenerator.SubstitutionValue.monthlySampleCount.getPreviewValue()); - public static SampleCountSubstitutionFormat yearlySampleCount = new SampleCountSubstitutionFormat("yearlySampleCount", NameGenerator.SubstitutionValue.yearlySampleCount.getPreviewValue()); - - public static SampleCountSubstitutionFormat rootSampleCount = new SampleCountSubstitutionFormat("rootSampleCount", NameGenerator.SubstitutionValue.rootSampleCount.getPreviewValue()); - public static SampleCountSubstitutionFormat sampleCount = new SampleCountSubstitutionFormat("sampleCount", NameGenerator.SubstitutionValue.sampleCount.getPreviewValue()); - - final String _name; - final String _shortName; - - SubstitutionFormat(String name) - { - _name = name; - _shortName = null; - } - - SubstitutionFormat(String name, String shortName) - { - _name = name; - _shortName = shortName; - } - - public String name() - { - return _name; - } - - public Object format(Object value) - { - return value; - } - - public Object format(Object value, boolean isPreview) - { - return format(value); - } - - public boolean hasSideEffects() - { - return false; - } - - public boolean argumentOptional() { return false; } - - public boolean argumentQuotesOptional() { return false; } - - public int argumentCount() { return 1; } - - public static List validateSyntax(@NotNull String formatName, @NotNull String nameExpression, int index) - { - SubstitutionFormat format = getFormat(formatName); - if (format == null) - return Collections.emptyList(); - - // if at the beginning of the naming pattern, we'll assume it is meant to be a literal - if (index <= 0) - return Collections.emptyList(); - - return validateSyntax(formatName, nameExpression, index, format, format.argumentQuotesOptional()); - } - - public static List validateSyntax(@NotNull String formatName, @NotNull String nameExpression, int index, @NotNull SubstitutionFormat format, boolean isArgumentQuoteOptional) - { - int argumentCount = format.argumentCount(); - boolean isArgumentOptional = format.argumentOptional(); - // if at the beginning of the naming pattern, we'll assume it is meant to be a literal - if (index <= 0) - return Collections.emptyList(); - - return validateFunctionalSyntax(formatName, nameExpression, index, argumentCount, isArgumentOptional, isArgumentQuoteOptional); - } - - public static List validateFunctionalSyntax(String formatName, String nameExpression, int start, int argumentCount, boolean isArgumentOptional, boolean isArgumentQuoteOptional) - { - List messages = new ArrayList<>(); - - if (argumentCount > 0) - { - int startParen = start + formatName.length() + 1; - boolean hasArg = nameExpression.length() >= startParen && nameExpression.charAt(startParen) == '('; - if (!hasArg) - { - if (!isArgumentOptional) - messages.add(String.format("No starting parentheses found for the '%s' substitution pattern starting at index %d.", formatName, start)); - else - return messages; - } - int endParen = nameExpression.indexOf(")", start); - if (endParen == -1) - messages.add(String.format("No ending parentheses found for the '%s' substitution pattern starting at index %d.", formatName, start)); - - if (!isArgumentQuoteOptional) - { - if (startParen < nameExpression.length()-1 && nameExpression.charAt(startParen+1) != '\'') - messages.add(String.format("Value in parentheses starting at index %d should be enclosed in single quotes.", startParen)); - else if (endParen > -1 && nameExpression.charAt(endParen-1) != '\'') - messages.add(String.format("No ending quote for the '%s' substitution pattern value starting at index %d.", formatName, start)); - } - } - return messages; - } - - public static List validateNonFunctionalSyntax(String formatName, String nameExpression, int start, String noun, boolean allowLookup) - { - List messages = new ArrayList<>(); - if (start < 2 || !(nameExpression.startsWith("${", start-2) || nameExpression.startsWith("${\\", start-3))) - { - if (allowLookup && nameExpression.startsWith("/", start-1)) - return messages; - - if (SAMPLE_COUNTER_SUBSTITUTIONS.contains(formatName) && nameExpression.startsWith(":", start-1)) - return messages; - - if (NameGenerator.SubstitutionValue.DataInputs.name().equals(formatName) || NameGenerator.SubstitutionValue.MaterialInputs.name().equals(formatName)) - { - // check for ancestor lookup - if (nameExpression.startsWith("..[", start-3) || nameExpression.startsWith("..[\\", start-4)) - return messages; - // check for ancestor search - if (nameExpression.startsWith("${~", start-3) || nameExpression.startsWith("${~\\", start-4)) - return messages; - } - if (!formatName.startsWith("${")) - messages.add(String.format("'%s' is recognized as a %s. Use ${%s} if you want to include the %s value in the naming pattern.", formatName, noun, formatName, noun)); - } - // missing ending brace check handled by general check for matching begin and end braces - return messages; - } - public static List validateNonFunctionalSyntax(String formatName, String nameExpression, int start) - { - return validateNonFunctionalSyntax(formatName, nameExpression, start, "substitution token", false); - } - - private final static Map _map = new CaseInsensitiveHashMap<>(); - - public static Map getSubstitutionFormats() - { - return _map; - } - - private static void register(SubstitutionFormat fmt) - { - _map.put(fmt.name(), fmt); - if (fmt._shortName != null) - _map.put(fmt._shortName, fmt); - } - - static - { - register(SubstitutionFormat.date); - register(SubstitutionFormat.encodeURI); - register(SubstitutionFormat.encodeURIComponent); - register(SubstitutionFormat.first); - register(SubstitutionFormat.htmlEncode); - register(SubstitutionFormat.jsString); - register(SubstitutionFormat.last); - register(SubstitutionFormat.passThrough); - register(SubstitutionFormat.rest); - register(SubstitutionFormat.trim); - register(SubstitutionFormat.urlEncode); - - // sample counters - register(SubstitutionFormat.dailySampleCount); - register(SubstitutionFormat.weeklySampleCount); - register(SubstitutionFormat.monthlySampleCount); - register(SubstitutionFormat.yearlySampleCount); - - // with arguments, for validation purpose only - register(SubstitutionFormat.defaultValue); - register(SubstitutionFormat.minValue); - register(SubstitutionFormat.number); - register(SubstitutionFormat.prefix); - register(SubstitutionFormat.suffix); - register(SubstitutionFormat.join); - } - - // More lenient than SubstitutionFormat.valueOf(), returns null for non-match - public static @Nullable SubstitutionFormat getFormat(String formatName) - { - return _map.get(formatName); - } - - public static Collection getFormatNames() - { - return _map.keySet(); - } -} +/* + * Copyright (c) 2013-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.util; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.api.data.NameGenerator; +import org.labkey.api.exp.api.SampleTypeService; + +import java.sql.Time; +import java.text.DecimalFormat; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.labkey.api.data.NameGenerator.SAMPLE_COUNTER_SUBSTITUTIONS; + +/** + * These are the supported formatting functions that can be used with string substitution, for example, when substituting + * values into a details URL or the javaScriptEvents property of a JavaScriptDisplayColumnFactory. The function definitions + * are patterned off Ext.util.Format (formats used in ExtJs templates), http://docs.sencha.com/extjs/4.2.1/#!/api/Ext.util.Format + * + * Examples: + *
+ *  ${Name:htmlEncode}
+ *  ${MyParam:urlEncode}
+ *
+ *  ${MyParam:defaultValue('foo')}
+ *
+ *  ${MyDate:date}
+ *  ${MyDate:date('yy-MM d')}
+ *
+ *  ${MyNumber:number('')}
+ *
+ *  ${MyParam:trim}
+ *
+ *  ${MyParam:prefix('-')}
+ *  ${MyParam:suffix('-')}
+ *  ${MyParam:join('-')}
+ *
+ *  ${MyParam:first}
+ *  ${MyParam:rest}
+ *  ${MyParam:last}
+ *
+ * 
+ * We should add more functions and allow parametrized functions. As we add functions, we should use the Ext names and + * parameters if at all possible. + * + * User: adam + * Date: 6/20/13 + */ +public class SubstitutionFormat +{ + static final SubstitutionFormat passThrough = new SubstitutionFormat("passThrough", "none") { + @Override + public Object format(Object value) + { + if (value instanceof Time) + return defaultTimeFormat.format(value); + else if (value instanceof java.sql.Date) + return date.format(value); + else if (value instanceof java.sql.Timestamp) + return defaultDateTimeFormat.format(value).replace('T', ' '); // replace T with whitespace for human readability + else if (value instanceof Date dateVal) + { + // both date and datetime column type are of type Date, format based on time portion of the date value + LocalDateTime ldt = LocalDateTime.ofInstant(dateVal.toInstant(), ZoneId.systemDefault()); + boolean isDateOnly = ldt.toLocalTime().equals(java.time.LocalTime.MIDNIGHT); + + if (isDateOnly) + return date.format(value); + else + return defaultDateTimeFormat.format(value).replace('T', ' '); // replace T with whitespace for human readability + } + + return value; + } + + @Override + public int argumentCount() + { + return 0; + } + }; + + static final SubstitutionFormat htmlEncode = new SubstitutionFormat("htmlEncode", "html") + { + @Override + public String format(Object value) + { + if (value == null) + return null; + return PageFlowUtil.filter(value); + } + + @Override + public int argumentCount() + { + return 0; + } + }; + + static final SubstitutionFormat jsString = new SubstitutionFormat("jsString", "jsString") + { + @Override + public String format(Object value) + { + if (value == null) + return null; + return PageFlowUtil.jsString(String.valueOf(value)); + } + + + @Override + public int argumentCount() + { + return 0; + } + }; + + static final SubstitutionFormat urlEncode = new SubstitutionFormat("urlEncode", "path") + { + @Override + public String format(Object value) + { + if (value == null) + return null; + return PageFlowUtil.encodePath(String.valueOf(value)); + } + + + @Override + public int argumentCount() + { + return 0; + } + }; + + // like javascript encodeURIComponent + static final SubstitutionFormat encodeURIComponent = new SubstitutionFormat("encodeURIComponent", "uricomponent") + { + @Override + public String format(Object value) + { + if (value == null) + return null; + return PageFlowUtil.encodeURIComponent(String.valueOf(value)); + } + + + @Override + public int argumentCount() + { + return 0; + } + }; + + // like javascript encodeURI + static final SubstitutionFormat encodeURI = new SubstitutionFormat("encodeURI", "uri") + { + @Override + public String format(Object value) + { + if (value == null) + return null; + return PageFlowUtil.encodeURI(String.valueOf(value)); + } + + + @Override + public int argumentCount() + { + return 0; + } + }; + + static final SubstitutionFormat first = new SubstitutionFormat("first") + { + @Override + public Object format(Object value) + { + if (value == null) + return null; + + if (!(value instanceof Collection)) + return value; + + Collection c = (Collection)value; + return c.stream().findFirst().orElse(null); + } + + + @Override + public int argumentCount() + { + return 0; + } + }; + + static final SubstitutionFormat rest = new SubstitutionFormat("rest") + { + @Override + public Object format(Object value) + { + if (value == null) + return null; + + if (!(value instanceof Collection)) + return value; + + Collection c = (Collection)value; + return c.stream().skip(1).collect(Collectors.toList()); + } + + @Override + public int argumentCount() + { + return 0; + } + }; + + static final SubstitutionFormat last = new SubstitutionFormat("last") + { + @Override + public Object format(Object value) + { + if (value == null) + return null; + + if (!(value instanceof Collection)) + return value; + + Collection c = (Collection)value; + return c.stream().reduce((a, b) -> b).orElse(null); + } + + @Override + public int argumentCount() + { + return 0; + } + }; + + static final SubstitutionFormat trim = new SubstitutionFormat("trim") + { + @Override + public String format(Object value) + { + if (value == null) + return null; + + if (!(value instanceof String)) + throw new IllegalArgumentException("Expected string: " + value); + + return ((String)value).trim(); + } + + @Override + public int argumentCount() + { + return 0; + } + }; + + public static class DefaultSubstitutionFormat extends SubstitutionFormat + { + private final String _default; + + public DefaultSubstitutionFormat(@NotNull String def) + { + super("defaultValue"); + _default = def; + } + + @Override + public Object format(Object value) + { + if (value == null || "".equals(value)) + return _default; + + return value; + } + } + static final SubstitutionFormat defaultValue = new DefaultSubstitutionFormat(""); + + public static class MinValueSubstitutionFormat extends SubstitutionFormat + { + private final long _min; + + public MinValueSubstitutionFormat(@NotNull String minValue) + { + super("minValue"); + long value = 0; + try + { + value = (long) Float.parseFloat(minValue); + } + catch (NumberFormatException e) + { + } + + _min = value; + } + + @Override + public Object format(Object value) + { + if (value == null) + return null; + + if (!(value instanceof Number)) + throw new IllegalArgumentException("Expected number: " + value); + + long numberVal = ((Number) value).longValue(); + return Math.max(_min, numberVal); + } + + @Override + public boolean argumentQuotesOptional() { return true; } + } + static final SubstitutionFormat minValue = new MinValueSubstitutionFormat(""); + + public static class JoinSubstitutionFormat extends SubstitutionFormat + { + private final String _sep; + private final String _prefix; + private final String _suffix; + + public JoinSubstitutionFormat(@NotNull String sep) + { + super("join"); + _sep = sep; + _prefix = ""; + _suffix = ""; + } + + public JoinSubstitutionFormat(@NotNull String sep, @NotNull String prefix, @NotNull String suffix) + { + super("join"); + _sep = sep; + _prefix = prefix; + _suffix = suffix; + } + + @Override + public String format(Object value) + { + if (value == null) + return null; + + Stream ss; + if (value instanceof Collection) + { + Collection c = (Collection)value; + if (c.isEmpty()) + return ""; + + ss = c.stream().map(String::valueOf); + } + else + { + String s = String.valueOf(value); + if (s.isEmpty()) + return ""; + + ss = Stream.of(s); + } + + return ss.collect(Collectors.joining(_sep, _prefix, _suffix)); + } + } + static final SubstitutionFormat join = new JoinSubstitutionFormat(""); + static final SubstitutionFormat prefix = new JoinSubstitutionFormat("", "", ""); + static final SubstitutionFormat suffix = new JoinSubstitutionFormat("", "", ""); + + public static class DateSubstitutionFormat extends SubstitutionFormat + { + // NOTE: We use DateTimeFormatter since it is thread-safe unlike SimpleDateFormat + final DateTimeFormatter _format; + + public DateSubstitutionFormat(@NotNull DateTimeFormatter format) + { + super("date"); + _format = format; + } + + @Override + public String format(Object value) + { + if (value == null) + return null; + + if (value instanceof java.sql.Date || value instanceof Time) + value = new Date(((Date)value).getTime()); + + TemporalAccessor temporal; + if (value instanceof TemporalAccessor) + temporal = (TemporalAccessor) value; + else if (value instanceof Date) + temporal = LocalDateTime.ofInstant(((Date)value).toInstant(), ZoneId.systemDefault()); + else + throw new IllegalArgumentException("Expected date: " + value); + + return _format.format(temporal); + } + + @Override + public boolean argumentOptional() { return true; } + } + + static final SubstitutionFormat date = new DateSubstitutionFormat(DateTimeFormatter.BASIC_ISO_DATE.withZone(ZoneId.systemDefault())); + static final DateSubstitutionFormat defaultDateTimeFormat = new SubstitutionFormat.DateSubstitutionFormat(DateTimeFormatter.ISO_DATE_TIME); + static final DateSubstitutionFormat defaultTimeFormat = new SubstitutionFormat.DateSubstitutionFormat(DateTimeFormatter.ISO_TIME); + + public static class NumberSubstitutionFormat extends SubstitutionFormat + { + final DecimalFormat _format; + + public NumberSubstitutionFormat(String formatString) + { + super("number"); + _format = new DecimalFormat(formatString); + } + + @Override + public Object format(Object value) + { + if (value == null) + return null; + + if (!(value instanceof Number)) + throw new IllegalArgumentException("Expected number: " + value); + + return _format.format(value); + } + } + static final SubstitutionFormat number = new NumberSubstitutionFormat(""); //used for validation only + + public static class SampleCountSubstitutionFormat extends SubstitutionFormat + { + private final Object _previewCount; + SampleCountSubstitutionFormat(String name, Object previewCount) + { + super(name); + _previewCount = previewCount; + } + + @Override + public boolean hasSideEffects() + { + return true; + } + + @Override + public Object format(Object value) + { + Date date = null; + if (value instanceof Date) + date = (Date)value; + + // Increment sample counters for the given date or today's date if null + // TODO: How can we check if we have incremented sample counters for this same date within the current context/row? + Map counts = SampleTypeService.get().incrementSampleCounts(date); + return counts.get(_name); + } + + @Override + public Object format(Object value, boolean _isPreview) + { + if (_isPreview) + return _previewCount; + + return format(value); + } + + @Override + public int argumentCount() { return 0; } + } + + public static SampleCountSubstitutionFormat dailySampleCount = new SampleCountSubstitutionFormat("dailySampleCount", NameGenerator.SubstitutionValue.dailySampleCount.getPreviewValue()); + public static SampleCountSubstitutionFormat weeklySampleCount = new SampleCountSubstitutionFormat("weeklySampleCount", NameGenerator.SubstitutionValue.weeklySampleCount.getPreviewValue()); + public static SampleCountSubstitutionFormat monthlySampleCount = new SampleCountSubstitutionFormat("monthlySampleCount", NameGenerator.SubstitutionValue.monthlySampleCount.getPreviewValue()); + public static SampleCountSubstitutionFormat yearlySampleCount = new SampleCountSubstitutionFormat("yearlySampleCount", NameGenerator.SubstitutionValue.yearlySampleCount.getPreviewValue()); + + public static SampleCountSubstitutionFormat rootSampleCount = new SampleCountSubstitutionFormat("rootSampleCount", NameGenerator.SubstitutionValue.rootSampleCount.getPreviewValue()); + public static SampleCountSubstitutionFormat sampleCount = new SampleCountSubstitutionFormat("sampleCount", NameGenerator.SubstitutionValue.sampleCount.getPreviewValue()); + + final String _name; + final String _shortName; + + SubstitutionFormat(String name) + { + _name = name; + _shortName = null; + } + + SubstitutionFormat(String name, String shortName) + { + _name = name; + _shortName = shortName; + } + + public String name() + { + return _name; + } + + public Object format(Object value) + { + return value; + } + + public Object format(Object value, boolean isPreview) + { + return format(value); + } + + public boolean hasSideEffects() + { + return false; + } + + public boolean argumentOptional() { return false; } + + public boolean argumentQuotesOptional() { return false; } + + public int argumentCount() { return 1; } + + public static List validateSyntax(@NotNull String formatName, @NotNull String nameExpression, int index) + { + SubstitutionFormat format = getFormat(formatName); + if (format == null) + return Collections.emptyList(); + + // if at the beginning of the naming pattern, we'll assume it is meant to be a literal + if (index <= 0) + return Collections.emptyList(); + + return validateSyntax(formatName, nameExpression, index, format, format.argumentQuotesOptional()); + } + + public static List validateSyntax(@NotNull String formatName, @NotNull String nameExpression, int index, @NotNull SubstitutionFormat format, boolean isArgumentQuoteOptional) + { + int argumentCount = format.argumentCount(); + boolean isArgumentOptional = format.argumentOptional(); + // if at the beginning of the naming pattern, we'll assume it is meant to be a literal + if (index <= 0) + return Collections.emptyList(); + + return validateFunctionalSyntax(formatName, nameExpression, index, argumentCount, isArgumentOptional, isArgumentQuoteOptional); + } + + public static List validateFunctionalSyntax(String formatName, String nameExpression, int start, int argumentCount, boolean isArgumentOptional, boolean isArgumentQuoteOptional) + { + List messages = new ArrayList<>(); + + if (argumentCount > 0) + { + int startParen = start + formatName.length() + 1; + boolean hasArg = nameExpression.length() >= startParen && nameExpression.charAt(startParen) == '('; + if (!hasArg) + { + if (!isArgumentOptional) + messages.add(String.format("No starting parentheses found for the '%s' substitution pattern starting at index %d.", formatName, start)); + else + return messages; + } + int endParen = nameExpression.indexOf(")", start); + if (endParen == -1) + messages.add(String.format("No ending parentheses found for the '%s' substitution pattern starting at index %d.", formatName, start)); + + if (!isArgumentQuoteOptional) + { + if (startParen < nameExpression.length()-1 && nameExpression.charAt(startParen+1) != '\'') + messages.add(String.format("Value in parentheses starting at index %d should be enclosed in single quotes.", startParen)); + else if (endParen > -1 && nameExpression.charAt(endParen-1) != '\'') + messages.add(String.format("No ending quote for the '%s' substitution pattern value starting at index %d.", formatName, start)); + } + } + return messages; + } + + public static List validateNonFunctionalSyntax(String formatName, String nameExpression, int start, String noun, boolean allowLookup) + { + List messages = new ArrayList<>(); + if (start < 2 || !(nameExpression.startsWith("${", start-2) || nameExpression.startsWith("${\\", start-3))) + { + if (allowLookup && nameExpression.startsWith("/", start-1)) + return messages; + + if (SAMPLE_COUNTER_SUBSTITUTIONS.contains(formatName) && nameExpression.startsWith(":", start-1)) + return messages; + + if (NameGenerator.SubstitutionValue.DataInputs.name().equals(formatName) || NameGenerator.SubstitutionValue.MaterialInputs.name().equals(formatName)) + { + // check for ancestor lookup + if (nameExpression.startsWith("..[", start-3) || nameExpression.startsWith("..[\\", start-4)) + return messages; + // check for ancestor search + if (nameExpression.startsWith("${~", start-3) || nameExpression.startsWith("${~\\", start-4)) + return messages; + } + if (!formatName.startsWith("${")) + messages.add(String.format("'%s' is recognized as a %s. Use ${%s} if you want to include the %s value in the naming pattern.", formatName, noun, formatName, noun)); + } + // missing ending brace check handled by general check for matching begin and end braces + return messages; + } + public static List validateNonFunctionalSyntax(String formatName, String nameExpression, int start) + { + return validateNonFunctionalSyntax(formatName, nameExpression, start, "substitution token", false); + } + + private final static Map _map = new CaseInsensitiveHashMap<>(); + + public static Map getSubstitutionFormats() + { + return _map; + } + + private static void register(SubstitutionFormat fmt) + { + _map.put(fmt.name(), fmt); + if (fmt._shortName != null) + _map.put(fmt._shortName, fmt); + } + + static + { + register(SubstitutionFormat.date); + register(SubstitutionFormat.encodeURI); + register(SubstitutionFormat.encodeURIComponent); + register(SubstitutionFormat.first); + register(SubstitutionFormat.htmlEncode); + register(SubstitutionFormat.jsString); + register(SubstitutionFormat.last); + register(SubstitutionFormat.passThrough); + register(SubstitutionFormat.rest); + register(SubstitutionFormat.trim); + register(SubstitutionFormat.urlEncode); + + // sample counters + register(SubstitutionFormat.dailySampleCount); + register(SubstitutionFormat.weeklySampleCount); + register(SubstitutionFormat.monthlySampleCount); + register(SubstitutionFormat.yearlySampleCount); + + // with arguments, for validation purpose only + register(SubstitutionFormat.defaultValue); + register(SubstitutionFormat.minValue); + register(SubstitutionFormat.number); + register(SubstitutionFormat.prefix); + register(SubstitutionFormat.suffix); + register(SubstitutionFormat.join); + } + + // More lenient than SubstitutionFormat.valueOf(), returns null for non-match + public static @Nullable SubstitutionFormat getFormat(String formatName) + { + return _map.get(formatName); + } + + public static Collection getFormatNames() + { + return _map.keySet(); + } +} From c376f810e71533d86ffd1bce7c6523d7aca5fe1f Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 23 Feb 2026 09:42:05 -0800 Subject: [PATCH 3/4] CRLF --- .../labkey/api/util/SubstitutionFormat.java | 1380 ++++++++--------- 1 file changed, 690 insertions(+), 690 deletions(-) diff --git a/api/src/org/labkey/api/util/SubstitutionFormat.java b/api/src/org/labkey/api/util/SubstitutionFormat.java index 034fca6b7f9..83eea5691b6 100644 --- a/api/src/org/labkey/api/util/SubstitutionFormat.java +++ b/api/src/org/labkey/api/util/SubstitutionFormat.java @@ -1,690 +1,690 @@ -/* - * Copyright (c) 2013-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.util; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.labkey.api.collections.CaseInsensitiveHashMap; -import org.labkey.api.data.NameGenerator; -import org.labkey.api.exp.api.SampleTypeService; - -import java.sql.Time; -import java.text.DecimalFormat; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAccessor; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.labkey.api.data.NameGenerator.SAMPLE_COUNTER_SUBSTITUTIONS; - -/** - * These are the supported formatting functions that can be used with string substitution, for example, when substituting - * values into a details URL or the javaScriptEvents property of a JavaScriptDisplayColumnFactory. The function definitions - * are patterned off Ext.util.Format (formats used in ExtJs templates), http://docs.sencha.com/extjs/4.2.1/#!/api/Ext.util.Format - * - * Examples: - *
- *  ${Name:htmlEncode}
- *  ${MyParam:urlEncode}
- *
- *  ${MyParam:defaultValue('foo')}
- *
- *  ${MyDate:date}
- *  ${MyDate:date('yy-MM d')}
- *
- *  ${MyNumber:number('')}
- *
- *  ${MyParam:trim}
- *
- *  ${MyParam:prefix('-')}
- *  ${MyParam:suffix('-')}
- *  ${MyParam:join('-')}
- *
- *  ${MyParam:first}
- *  ${MyParam:rest}
- *  ${MyParam:last}
- *
- * 
- * We should add more functions and allow parametrized functions. As we add functions, we should use the Ext names and - * parameters if at all possible. - * - * User: adam - * Date: 6/20/13 - */ -public class SubstitutionFormat -{ - static final SubstitutionFormat passThrough = new SubstitutionFormat("passThrough", "none") { - @Override - public Object format(Object value) - { - if (value instanceof Time) - return defaultTimeFormat.format(value); - else if (value instanceof java.sql.Date) - return date.format(value); - else if (value instanceof java.sql.Timestamp) - return defaultDateTimeFormat.format(value).replace('T', ' '); // replace T with whitespace for human readability - else if (value instanceof Date dateVal) - { - // both date and datetime column type are of type Date, format based on time portion of the date value - LocalDateTime ldt = LocalDateTime.ofInstant(dateVal.toInstant(), ZoneId.systemDefault()); - boolean isDateOnly = ldt.toLocalTime().equals(java.time.LocalTime.MIDNIGHT); - - if (isDateOnly) - return date.format(value); - else - return defaultDateTimeFormat.format(value).replace('T', ' '); // replace T with whitespace for human readability - } - - return value; - } - - @Override - public int argumentCount() - { - return 0; - } - }; - - static final SubstitutionFormat htmlEncode = new SubstitutionFormat("htmlEncode", "html") - { - @Override - public String format(Object value) - { - if (value == null) - return null; - return PageFlowUtil.filter(value); - } - - @Override - public int argumentCount() - { - return 0; - } - }; - - static final SubstitutionFormat jsString = new SubstitutionFormat("jsString", "jsString") - { - @Override - public String format(Object value) - { - if (value == null) - return null; - return PageFlowUtil.jsString(String.valueOf(value)); - } - - - @Override - public int argumentCount() - { - return 0; - } - }; - - static final SubstitutionFormat urlEncode = new SubstitutionFormat("urlEncode", "path") - { - @Override - public String format(Object value) - { - if (value == null) - return null; - return PageFlowUtil.encodePath(String.valueOf(value)); - } - - - @Override - public int argumentCount() - { - return 0; - } - }; - - // like javascript encodeURIComponent - static final SubstitutionFormat encodeURIComponent = new SubstitutionFormat("encodeURIComponent", "uricomponent") - { - @Override - public String format(Object value) - { - if (value == null) - return null; - return PageFlowUtil.encodeURIComponent(String.valueOf(value)); - } - - - @Override - public int argumentCount() - { - return 0; - } - }; - - // like javascript encodeURI - static final SubstitutionFormat encodeURI = new SubstitutionFormat("encodeURI", "uri") - { - @Override - public String format(Object value) - { - if (value == null) - return null; - return PageFlowUtil.encodeURI(String.valueOf(value)); - } - - - @Override - public int argumentCount() - { - return 0; - } - }; - - static final SubstitutionFormat first = new SubstitutionFormat("first") - { - @Override - public Object format(Object value) - { - if (value == null) - return null; - - if (!(value instanceof Collection)) - return value; - - Collection c = (Collection)value; - return c.stream().findFirst().orElse(null); - } - - - @Override - public int argumentCount() - { - return 0; - } - }; - - static final SubstitutionFormat rest = new SubstitutionFormat("rest") - { - @Override - public Object format(Object value) - { - if (value == null) - return null; - - if (!(value instanceof Collection)) - return value; - - Collection c = (Collection)value; - return c.stream().skip(1).collect(Collectors.toList()); - } - - @Override - public int argumentCount() - { - return 0; - } - }; - - static final SubstitutionFormat last = new SubstitutionFormat("last") - { - @Override - public Object format(Object value) - { - if (value == null) - return null; - - if (!(value instanceof Collection)) - return value; - - Collection c = (Collection)value; - return c.stream().reduce((a, b) -> b).orElse(null); - } - - @Override - public int argumentCount() - { - return 0; - } - }; - - static final SubstitutionFormat trim = new SubstitutionFormat("trim") - { - @Override - public String format(Object value) - { - if (value == null) - return null; - - if (!(value instanceof String)) - throw new IllegalArgumentException("Expected string: " + value); - - return ((String)value).trim(); - } - - @Override - public int argumentCount() - { - return 0; - } - }; - - public static class DefaultSubstitutionFormat extends SubstitutionFormat - { - private final String _default; - - public DefaultSubstitutionFormat(@NotNull String def) - { - super("defaultValue"); - _default = def; - } - - @Override - public Object format(Object value) - { - if (value == null || "".equals(value)) - return _default; - - return value; - } - } - static final SubstitutionFormat defaultValue = new DefaultSubstitutionFormat(""); - - public static class MinValueSubstitutionFormat extends SubstitutionFormat - { - private final long _min; - - public MinValueSubstitutionFormat(@NotNull String minValue) - { - super("minValue"); - long value = 0; - try - { - value = (long) Float.parseFloat(minValue); - } - catch (NumberFormatException e) - { - } - - _min = value; - } - - @Override - public Object format(Object value) - { - if (value == null) - return null; - - if (!(value instanceof Number)) - throw new IllegalArgumentException("Expected number: " + value); - - long numberVal = ((Number) value).longValue(); - return Math.max(_min, numberVal); - } - - @Override - public boolean argumentQuotesOptional() { return true; } - } - static final SubstitutionFormat minValue = new MinValueSubstitutionFormat(""); - - public static class JoinSubstitutionFormat extends SubstitutionFormat - { - private final String _sep; - private final String _prefix; - private final String _suffix; - - public JoinSubstitutionFormat(@NotNull String sep) - { - super("join"); - _sep = sep; - _prefix = ""; - _suffix = ""; - } - - public JoinSubstitutionFormat(@NotNull String sep, @NotNull String prefix, @NotNull String suffix) - { - super("join"); - _sep = sep; - _prefix = prefix; - _suffix = suffix; - } - - @Override - public String format(Object value) - { - if (value == null) - return null; - - Stream ss; - if (value instanceof Collection) - { - Collection c = (Collection)value; - if (c.isEmpty()) - return ""; - - ss = c.stream().map(String::valueOf); - } - else - { - String s = String.valueOf(value); - if (s.isEmpty()) - return ""; - - ss = Stream.of(s); - } - - return ss.collect(Collectors.joining(_sep, _prefix, _suffix)); - } - } - static final SubstitutionFormat join = new JoinSubstitutionFormat(""); - static final SubstitutionFormat prefix = new JoinSubstitutionFormat("", "", ""); - static final SubstitutionFormat suffix = new JoinSubstitutionFormat("", "", ""); - - public static class DateSubstitutionFormat extends SubstitutionFormat - { - // NOTE: We use DateTimeFormatter since it is thread-safe unlike SimpleDateFormat - final DateTimeFormatter _format; - - public DateSubstitutionFormat(@NotNull DateTimeFormatter format) - { - super("date"); - _format = format; - } - - @Override - public String format(Object value) - { - if (value == null) - return null; - - if (value instanceof java.sql.Date || value instanceof Time) - value = new Date(((Date)value).getTime()); - - TemporalAccessor temporal; - if (value instanceof TemporalAccessor) - temporal = (TemporalAccessor) value; - else if (value instanceof Date) - temporal = LocalDateTime.ofInstant(((Date)value).toInstant(), ZoneId.systemDefault()); - else - throw new IllegalArgumentException("Expected date: " + value); - - return _format.format(temporal); - } - - @Override - public boolean argumentOptional() { return true; } - } - - static final SubstitutionFormat date = new DateSubstitutionFormat(DateTimeFormatter.BASIC_ISO_DATE.withZone(ZoneId.systemDefault())); - static final DateSubstitutionFormat defaultDateTimeFormat = new SubstitutionFormat.DateSubstitutionFormat(DateTimeFormatter.ISO_DATE_TIME); - static final DateSubstitutionFormat defaultTimeFormat = new SubstitutionFormat.DateSubstitutionFormat(DateTimeFormatter.ISO_TIME); - - public static class NumberSubstitutionFormat extends SubstitutionFormat - { - final DecimalFormat _format; - - public NumberSubstitutionFormat(String formatString) - { - super("number"); - _format = new DecimalFormat(formatString); - } - - @Override - public Object format(Object value) - { - if (value == null) - return null; - - if (!(value instanceof Number)) - throw new IllegalArgumentException("Expected number: " + value); - - return _format.format(value); - } - } - static final SubstitutionFormat number = new NumberSubstitutionFormat(""); //used for validation only - - public static class SampleCountSubstitutionFormat extends SubstitutionFormat - { - private final Object _previewCount; - SampleCountSubstitutionFormat(String name, Object previewCount) - { - super(name); - _previewCount = previewCount; - } - - @Override - public boolean hasSideEffects() - { - return true; - } - - @Override - public Object format(Object value) - { - Date date = null; - if (value instanceof Date) - date = (Date)value; - - // Increment sample counters for the given date or today's date if null - // TODO: How can we check if we have incremented sample counters for this same date within the current context/row? - Map counts = SampleTypeService.get().incrementSampleCounts(date); - return counts.get(_name); - } - - @Override - public Object format(Object value, boolean _isPreview) - { - if (_isPreview) - return _previewCount; - - return format(value); - } - - @Override - public int argumentCount() { return 0; } - } - - public static SampleCountSubstitutionFormat dailySampleCount = new SampleCountSubstitutionFormat("dailySampleCount", NameGenerator.SubstitutionValue.dailySampleCount.getPreviewValue()); - public static SampleCountSubstitutionFormat weeklySampleCount = new SampleCountSubstitutionFormat("weeklySampleCount", NameGenerator.SubstitutionValue.weeklySampleCount.getPreviewValue()); - public static SampleCountSubstitutionFormat monthlySampleCount = new SampleCountSubstitutionFormat("monthlySampleCount", NameGenerator.SubstitutionValue.monthlySampleCount.getPreviewValue()); - public static SampleCountSubstitutionFormat yearlySampleCount = new SampleCountSubstitutionFormat("yearlySampleCount", NameGenerator.SubstitutionValue.yearlySampleCount.getPreviewValue()); - - public static SampleCountSubstitutionFormat rootSampleCount = new SampleCountSubstitutionFormat("rootSampleCount", NameGenerator.SubstitutionValue.rootSampleCount.getPreviewValue()); - public static SampleCountSubstitutionFormat sampleCount = new SampleCountSubstitutionFormat("sampleCount", NameGenerator.SubstitutionValue.sampleCount.getPreviewValue()); - - final String _name; - final String _shortName; - - SubstitutionFormat(String name) - { - _name = name; - _shortName = null; - } - - SubstitutionFormat(String name, String shortName) - { - _name = name; - _shortName = shortName; - } - - public String name() - { - return _name; - } - - public Object format(Object value) - { - return value; - } - - public Object format(Object value, boolean isPreview) - { - return format(value); - } - - public boolean hasSideEffects() - { - return false; - } - - public boolean argumentOptional() { return false; } - - public boolean argumentQuotesOptional() { return false; } - - public int argumentCount() { return 1; } - - public static List validateSyntax(@NotNull String formatName, @NotNull String nameExpression, int index) - { - SubstitutionFormat format = getFormat(formatName); - if (format == null) - return Collections.emptyList(); - - // if at the beginning of the naming pattern, we'll assume it is meant to be a literal - if (index <= 0) - return Collections.emptyList(); - - return validateSyntax(formatName, nameExpression, index, format, format.argumentQuotesOptional()); - } - - public static List validateSyntax(@NotNull String formatName, @NotNull String nameExpression, int index, @NotNull SubstitutionFormat format, boolean isArgumentQuoteOptional) - { - int argumentCount = format.argumentCount(); - boolean isArgumentOptional = format.argumentOptional(); - // if at the beginning of the naming pattern, we'll assume it is meant to be a literal - if (index <= 0) - return Collections.emptyList(); - - return validateFunctionalSyntax(formatName, nameExpression, index, argumentCount, isArgumentOptional, isArgumentQuoteOptional); - } - - public static List validateFunctionalSyntax(String formatName, String nameExpression, int start, int argumentCount, boolean isArgumentOptional, boolean isArgumentQuoteOptional) - { - List messages = new ArrayList<>(); - - if (argumentCount > 0) - { - int startParen = start + formatName.length() + 1; - boolean hasArg = nameExpression.length() >= startParen && nameExpression.charAt(startParen) == '('; - if (!hasArg) - { - if (!isArgumentOptional) - messages.add(String.format("No starting parentheses found for the '%s' substitution pattern starting at index %d.", formatName, start)); - else - return messages; - } - int endParen = nameExpression.indexOf(")", start); - if (endParen == -1) - messages.add(String.format("No ending parentheses found for the '%s' substitution pattern starting at index %d.", formatName, start)); - - if (!isArgumentQuoteOptional) - { - if (startParen < nameExpression.length()-1 && nameExpression.charAt(startParen+1) != '\'') - messages.add(String.format("Value in parentheses starting at index %d should be enclosed in single quotes.", startParen)); - else if (endParen > -1 && nameExpression.charAt(endParen-1) != '\'') - messages.add(String.format("No ending quote for the '%s' substitution pattern value starting at index %d.", formatName, start)); - } - } - return messages; - } - - public static List validateNonFunctionalSyntax(String formatName, String nameExpression, int start, String noun, boolean allowLookup) - { - List messages = new ArrayList<>(); - if (start < 2 || !(nameExpression.startsWith("${", start-2) || nameExpression.startsWith("${\\", start-3))) - { - if (allowLookup && nameExpression.startsWith("/", start-1)) - return messages; - - if (SAMPLE_COUNTER_SUBSTITUTIONS.contains(formatName) && nameExpression.startsWith(":", start-1)) - return messages; - - if (NameGenerator.SubstitutionValue.DataInputs.name().equals(formatName) || NameGenerator.SubstitutionValue.MaterialInputs.name().equals(formatName)) - { - // check for ancestor lookup - if (nameExpression.startsWith("..[", start-3) || nameExpression.startsWith("..[\\", start-4)) - return messages; - // check for ancestor search - if (nameExpression.startsWith("${~", start-3) || nameExpression.startsWith("${~\\", start-4)) - return messages; - } - if (!formatName.startsWith("${")) - messages.add(String.format("'%s' is recognized as a %s. Use ${%s} if you want to include the %s value in the naming pattern.", formatName, noun, formatName, noun)); - } - // missing ending brace check handled by general check for matching begin and end braces - return messages; - } - public static List validateNonFunctionalSyntax(String formatName, String nameExpression, int start) - { - return validateNonFunctionalSyntax(formatName, nameExpression, start, "substitution token", false); - } - - private final static Map _map = new CaseInsensitiveHashMap<>(); - - public static Map getSubstitutionFormats() - { - return _map; - } - - private static void register(SubstitutionFormat fmt) - { - _map.put(fmt.name(), fmt); - if (fmt._shortName != null) - _map.put(fmt._shortName, fmt); - } - - static - { - register(SubstitutionFormat.date); - register(SubstitutionFormat.encodeURI); - register(SubstitutionFormat.encodeURIComponent); - register(SubstitutionFormat.first); - register(SubstitutionFormat.htmlEncode); - register(SubstitutionFormat.jsString); - register(SubstitutionFormat.last); - register(SubstitutionFormat.passThrough); - register(SubstitutionFormat.rest); - register(SubstitutionFormat.trim); - register(SubstitutionFormat.urlEncode); - - // sample counters - register(SubstitutionFormat.dailySampleCount); - register(SubstitutionFormat.weeklySampleCount); - register(SubstitutionFormat.monthlySampleCount); - register(SubstitutionFormat.yearlySampleCount); - - // with arguments, for validation purpose only - register(SubstitutionFormat.defaultValue); - register(SubstitutionFormat.minValue); - register(SubstitutionFormat.number); - register(SubstitutionFormat.prefix); - register(SubstitutionFormat.suffix); - register(SubstitutionFormat.join); - } - - // More lenient than SubstitutionFormat.valueOf(), returns null for non-match - public static @Nullable SubstitutionFormat getFormat(String formatName) - { - return _map.get(formatName); - } - - public static Collection getFormatNames() - { - return _map.keySet(); - } -} +/* + * Copyright (c) 2013-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.util; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.api.data.NameGenerator; +import org.labkey.api.exp.api.SampleTypeService; + +import java.sql.Time; +import java.text.DecimalFormat; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.labkey.api.data.NameGenerator.SAMPLE_COUNTER_SUBSTITUTIONS; + +/** + * These are the supported formatting functions that can be used with string substitution, for example, when substituting + * values into a details URL or the javaScriptEvents property of a JavaScriptDisplayColumnFactory. The function definitions + * are patterned off Ext.util.Format (formats used in ExtJs templates), http://docs.sencha.com/extjs/4.2.1/#!/api/Ext.util.Format + * + * Examples: + *
+ *  ${Name:htmlEncode}
+ *  ${MyParam:urlEncode}
+ *
+ *  ${MyParam:defaultValue('foo')}
+ *
+ *  ${MyDate:date}
+ *  ${MyDate:date('yy-MM d')}
+ *
+ *  ${MyNumber:number('')}
+ *
+ *  ${MyParam:trim}
+ *
+ *  ${MyParam:prefix('-')}
+ *  ${MyParam:suffix('-')}
+ *  ${MyParam:join('-')}
+ *
+ *  ${MyParam:first}
+ *  ${MyParam:rest}
+ *  ${MyParam:last}
+ *
+ * 
+ * We should add more functions and allow parametrized functions. As we add functions, we should use the Ext names and + * parameters if at all possible. + * + * User: adam + * Date: 6/20/13 + */ +public class SubstitutionFormat +{ + static final SubstitutionFormat passThrough = new SubstitutionFormat("passThrough", "none") { + @Override + public Object format(Object value) + { + if (value instanceof Time) + return defaultTimeFormat.format(value); + else if (value instanceof java.sql.Date) + return date.format(value); + else if (value instanceof java.sql.Timestamp) + return defaultDateTimeFormat.format(value).replace('T', ' '); // replace T with whitespace for human readability + else if (value instanceof Date dateVal) + { + // both date and datetime column type are of type Date, format based on time portion of the date value + LocalDateTime ldt = LocalDateTime.ofInstant(dateVal.toInstant(), ZoneId.systemDefault()); + boolean isDateOnly = ldt.toLocalTime().equals(java.time.LocalTime.MIDNIGHT); + + if (isDateOnly) + return date.format(value); + else + return defaultDateTimeFormat.format(value).replace('T', ' '); // replace T with whitespace for human readability + } + + return value; + } + + @Override + public int argumentCount() + { + return 0; + } + }; + + static final SubstitutionFormat htmlEncode = new SubstitutionFormat("htmlEncode", "html") + { + @Override + public String format(Object value) + { + if (value == null) + return null; + return PageFlowUtil.filter(value); + } + + @Override + public int argumentCount() + { + return 0; + } + }; + + static final SubstitutionFormat jsString = new SubstitutionFormat("jsString", "jsString") + { + @Override + public String format(Object value) + { + if (value == null) + return null; + return PageFlowUtil.jsString(String.valueOf(value)); + } + + + @Override + public int argumentCount() + { + return 0; + } + }; + + static final SubstitutionFormat urlEncode = new SubstitutionFormat("urlEncode", "path") + { + @Override + public String format(Object value) + { + if (value == null) + return null; + return PageFlowUtil.encodePath(String.valueOf(value)); + } + + + @Override + public int argumentCount() + { + return 0; + } + }; + + // like javascript encodeURIComponent + static final SubstitutionFormat encodeURIComponent = new SubstitutionFormat("encodeURIComponent", "uricomponent") + { + @Override + public String format(Object value) + { + if (value == null) + return null; + return PageFlowUtil.encodeURIComponent(String.valueOf(value)); + } + + + @Override + public int argumentCount() + { + return 0; + } + }; + + // like javascript encodeURI + static final SubstitutionFormat encodeURI = new SubstitutionFormat("encodeURI", "uri") + { + @Override + public String format(Object value) + { + if (value == null) + return null; + return PageFlowUtil.encodeURI(String.valueOf(value)); + } + + + @Override + public int argumentCount() + { + return 0; + } + }; + + static final SubstitutionFormat first = new SubstitutionFormat("first") + { + @Override + public Object format(Object value) + { + if (value == null) + return null; + + if (!(value instanceof Collection)) + return value; + + Collection c = (Collection)value; + return c.stream().findFirst().orElse(null); + } + + + @Override + public int argumentCount() + { + return 0; + } + }; + + static final SubstitutionFormat rest = new SubstitutionFormat("rest") + { + @Override + public Object format(Object value) + { + if (value == null) + return null; + + if (!(value instanceof Collection)) + return value; + + Collection c = (Collection)value; + return c.stream().skip(1).collect(Collectors.toList()); + } + + @Override + public int argumentCount() + { + return 0; + } + }; + + static final SubstitutionFormat last = new SubstitutionFormat("last") + { + @Override + public Object format(Object value) + { + if (value == null) + return null; + + if (!(value instanceof Collection)) + return value; + + Collection c = (Collection)value; + return c.stream().reduce((a, b) -> b).orElse(null); + } + + @Override + public int argumentCount() + { + return 0; + } + }; + + static final SubstitutionFormat trim = new SubstitutionFormat("trim") + { + @Override + public String format(Object value) + { + if (value == null) + return null; + + if (!(value instanceof String)) + throw new IllegalArgumentException("Expected string: " + value); + + return ((String)value).trim(); + } + + @Override + public int argumentCount() + { + return 0; + } + }; + + public static class DefaultSubstitutionFormat extends SubstitutionFormat + { + private final String _default; + + public DefaultSubstitutionFormat(@NotNull String def) + { + super("defaultValue"); + _default = def; + } + + @Override + public Object format(Object value) + { + if (value == null || "".equals(value)) + return _default; + + return value; + } + } + static final SubstitutionFormat defaultValue = new DefaultSubstitutionFormat(""); + + public static class MinValueSubstitutionFormat extends SubstitutionFormat + { + private final long _min; + + public MinValueSubstitutionFormat(@NotNull String minValue) + { + super("minValue"); + long value = 0; + try + { + value = (long) Float.parseFloat(minValue); + } + catch (NumberFormatException e) + { + } + + _min = value; + } + + @Override + public Object format(Object value) + { + if (value == null) + return null; + + if (!(value instanceof Number)) + throw new IllegalArgumentException("Expected number: " + value); + + long numberVal = ((Number) value).longValue(); + return Math.max(_min, numberVal); + } + + @Override + public boolean argumentQuotesOptional() { return true; } + } + static final SubstitutionFormat minValue = new MinValueSubstitutionFormat(""); + + public static class JoinSubstitutionFormat extends SubstitutionFormat + { + private final String _sep; + private final String _prefix; + private final String _suffix; + + public JoinSubstitutionFormat(@NotNull String sep) + { + super("join"); + _sep = sep; + _prefix = ""; + _suffix = ""; + } + + public JoinSubstitutionFormat(@NotNull String sep, @NotNull String prefix, @NotNull String suffix) + { + super("join"); + _sep = sep; + _prefix = prefix; + _suffix = suffix; + } + + @Override + public String format(Object value) + { + if (value == null) + return null; + + Stream ss; + if (value instanceof Collection) + { + Collection c = (Collection)value; + if (c.isEmpty()) + return ""; + + ss = c.stream().map(String::valueOf); + } + else + { + String s = String.valueOf(value); + if (s.isEmpty()) + return ""; + + ss = Stream.of(s); + } + + return ss.collect(Collectors.joining(_sep, _prefix, _suffix)); + } + } + static final SubstitutionFormat join = new JoinSubstitutionFormat(""); + static final SubstitutionFormat prefix = new JoinSubstitutionFormat("", "", ""); + static final SubstitutionFormat suffix = new JoinSubstitutionFormat("", "", ""); + + public static class DateSubstitutionFormat extends SubstitutionFormat + { + // NOTE: We use DateTimeFormatter since it is thread-safe unlike SimpleDateFormat + final DateTimeFormatter _format; + + public DateSubstitutionFormat(@NotNull DateTimeFormatter format) + { + super("date"); + _format = format; + } + + @Override + public String format(Object value) + { + if (value == null) + return null; + + if (value instanceof java.sql.Date || value instanceof Time) + value = new Date(((Date)value).getTime()); + + TemporalAccessor temporal; + if (value instanceof TemporalAccessor) + temporal = (TemporalAccessor) value; + else if (value instanceof Date) + temporal = LocalDateTime.ofInstant(((Date)value).toInstant(), ZoneId.systemDefault()); + else + throw new IllegalArgumentException("Expected date: " + value); + + return _format.format(temporal); + } + + @Override + public boolean argumentOptional() { return true; } + } + + static final SubstitutionFormat date = new DateSubstitutionFormat(DateTimeFormatter.BASIC_ISO_DATE.withZone(ZoneId.systemDefault())); + static final DateSubstitutionFormat defaultDateTimeFormat = new SubstitutionFormat.DateSubstitutionFormat(DateTimeFormatter.ISO_DATE_TIME); + static final DateSubstitutionFormat defaultTimeFormat = new SubstitutionFormat.DateSubstitutionFormat(DateTimeFormatter.ISO_TIME); + + public static class NumberSubstitutionFormat extends SubstitutionFormat + { + final DecimalFormat _format; + + public NumberSubstitutionFormat(String formatString) + { + super("number"); + _format = new DecimalFormat(formatString); + } + + @Override + public Object format(Object value) + { + if (value == null) + return null; + + if (!(value instanceof Number)) + throw new IllegalArgumentException("Expected number: " + value); + + return _format.format(value); + } + } + static final SubstitutionFormat number = new NumberSubstitutionFormat(""); //used for validation only + + public static class SampleCountSubstitutionFormat extends SubstitutionFormat + { + private final Object _previewCount; + SampleCountSubstitutionFormat(String name, Object previewCount) + { + super(name); + _previewCount = previewCount; + } + + @Override + public boolean hasSideEffects() + { + return true; + } + + @Override + public Object format(Object value) + { + Date date = null; + if (value instanceof Date) + date = (Date)value; + + // Increment sample counters for the given date or today's date if null + // TODO: How can we check if we have incremented sample counters for this same date within the current context/row? + Map counts = SampleTypeService.get().incrementSampleCounts(date); + return counts.get(_name); + } + + @Override + public Object format(Object value, boolean _isPreview) + { + if (_isPreview) + return _previewCount; + + return format(value); + } + + @Override + public int argumentCount() { return 0; } + } + + public static SampleCountSubstitutionFormat dailySampleCount = new SampleCountSubstitutionFormat("dailySampleCount", NameGenerator.SubstitutionValue.dailySampleCount.getPreviewValue()); + public static SampleCountSubstitutionFormat weeklySampleCount = new SampleCountSubstitutionFormat("weeklySampleCount", NameGenerator.SubstitutionValue.weeklySampleCount.getPreviewValue()); + public static SampleCountSubstitutionFormat monthlySampleCount = new SampleCountSubstitutionFormat("monthlySampleCount", NameGenerator.SubstitutionValue.monthlySampleCount.getPreviewValue()); + public static SampleCountSubstitutionFormat yearlySampleCount = new SampleCountSubstitutionFormat("yearlySampleCount", NameGenerator.SubstitutionValue.yearlySampleCount.getPreviewValue()); + + public static SampleCountSubstitutionFormat rootSampleCount = new SampleCountSubstitutionFormat("rootSampleCount", NameGenerator.SubstitutionValue.rootSampleCount.getPreviewValue()); + public static SampleCountSubstitutionFormat sampleCount = new SampleCountSubstitutionFormat("sampleCount", NameGenerator.SubstitutionValue.sampleCount.getPreviewValue()); + + final String _name; + final String _shortName; + + SubstitutionFormat(String name) + { + _name = name; + _shortName = null; + } + + SubstitutionFormat(String name, String shortName) + { + _name = name; + _shortName = shortName; + } + + public String name() + { + return _name; + } + + public Object format(Object value) + { + return value; + } + + public Object format(Object value, boolean isPreview) + { + return format(value); + } + + public boolean hasSideEffects() + { + return false; + } + + public boolean argumentOptional() { return false; } + + public boolean argumentQuotesOptional() { return false; } + + public int argumentCount() { return 1; } + + public static List validateSyntax(@NotNull String formatName, @NotNull String nameExpression, int index) + { + SubstitutionFormat format = getFormat(formatName); + if (format == null) + return Collections.emptyList(); + + // if at the beginning of the naming pattern, we'll assume it is meant to be a literal + if (index <= 0) + return Collections.emptyList(); + + return validateSyntax(formatName, nameExpression, index, format, format.argumentQuotesOptional()); + } + + public static List validateSyntax(@NotNull String formatName, @NotNull String nameExpression, int index, @NotNull SubstitutionFormat format, boolean isArgumentQuoteOptional) + { + int argumentCount = format.argumentCount(); + boolean isArgumentOptional = format.argumentOptional(); + // if at the beginning of the naming pattern, we'll assume it is meant to be a literal + if (index <= 0) + return Collections.emptyList(); + + return validateFunctionalSyntax(formatName, nameExpression, index, argumentCount, isArgumentOptional, isArgumentQuoteOptional); + } + + public static List validateFunctionalSyntax(String formatName, String nameExpression, int start, int argumentCount, boolean isArgumentOptional, boolean isArgumentQuoteOptional) + { + List messages = new ArrayList<>(); + + if (argumentCount > 0) + { + int startParen = start + formatName.length() + 1; + boolean hasArg = nameExpression.length() >= startParen && nameExpression.charAt(startParen) == '('; + if (!hasArg) + { + if (!isArgumentOptional) + messages.add(String.format("No starting parentheses found for the '%s' substitution pattern starting at index %d.", formatName, start)); + else + return messages; + } + int endParen = nameExpression.indexOf(")", start); + if (endParen == -1) + messages.add(String.format("No ending parentheses found for the '%s' substitution pattern starting at index %d.", formatName, start)); + + if (!isArgumentQuoteOptional) + { + if (startParen < nameExpression.length()-1 && nameExpression.charAt(startParen+1) != '\'') + messages.add(String.format("Value in parentheses starting at index %d should be enclosed in single quotes.", startParen)); + else if (endParen > -1 && nameExpression.charAt(endParen-1) != '\'') + messages.add(String.format("No ending quote for the '%s' substitution pattern value starting at index %d.", formatName, start)); + } + } + return messages; + } + + public static List validateNonFunctionalSyntax(String formatName, String nameExpression, int start, String noun, boolean allowLookup) + { + List messages = new ArrayList<>(); + if (start < 2 || !(nameExpression.startsWith("${", start-2) || nameExpression.startsWith("${\\", start-3))) + { + if (allowLookup && nameExpression.startsWith("/", start-1)) + return messages; + + if (SAMPLE_COUNTER_SUBSTITUTIONS.contains(formatName) && nameExpression.startsWith(":", start-1)) + return messages; + + if (NameGenerator.SubstitutionValue.DataInputs.name().equals(formatName) || NameGenerator.SubstitutionValue.MaterialInputs.name().equals(formatName)) + { + // check for ancestor lookup + if (nameExpression.startsWith("..[", start-3) || nameExpression.startsWith("..[\\", start-4)) + return messages; + // check for ancestor search + if (nameExpression.startsWith("${~", start-3) || nameExpression.startsWith("${~\\", start-4)) + return messages; + } + if (!formatName.startsWith("${")) + messages.add(String.format("'%s' is recognized as a %s. Use ${%s} if you want to include the %s value in the naming pattern.", formatName, noun, formatName, noun)); + } + // missing ending brace check handled by general check for matching begin and end braces + return messages; + } + public static List validateNonFunctionalSyntax(String formatName, String nameExpression, int start) + { + return validateNonFunctionalSyntax(formatName, nameExpression, start, "substitution token", false); + } + + private final static Map _map = new CaseInsensitiveHashMap<>(); + + public static Map getSubstitutionFormats() + { + return _map; + } + + private static void register(SubstitutionFormat fmt) + { + _map.put(fmt.name(), fmt); + if (fmt._shortName != null) + _map.put(fmt._shortName, fmt); + } + + static + { + register(SubstitutionFormat.date); + register(SubstitutionFormat.encodeURI); + register(SubstitutionFormat.encodeURIComponent); + register(SubstitutionFormat.first); + register(SubstitutionFormat.htmlEncode); + register(SubstitutionFormat.jsString); + register(SubstitutionFormat.last); + register(SubstitutionFormat.passThrough); + register(SubstitutionFormat.rest); + register(SubstitutionFormat.trim); + register(SubstitutionFormat.urlEncode); + + // sample counters + register(SubstitutionFormat.dailySampleCount); + register(SubstitutionFormat.weeklySampleCount); + register(SubstitutionFormat.monthlySampleCount); + register(SubstitutionFormat.yearlySampleCount); + + // with arguments, for validation purpose only + register(SubstitutionFormat.defaultValue); + register(SubstitutionFormat.minValue); + register(SubstitutionFormat.number); + register(SubstitutionFormat.prefix); + register(SubstitutionFormat.suffix); + register(SubstitutionFormat.join); + } + + // More lenient than SubstitutionFormat.valueOf(), returns null for non-match + public static @Nullable SubstitutionFormat getFormat(String formatName) + { + return _map.get(formatName); + } + + public static Collection getFormatNames() + { + return _map.keySet(); + } +} From 50a7dd009319a0c8e4065dc1c7bf9545baa73e5a Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 23 Feb 2026 11:07:45 -0800 Subject: [PATCH 4/4] fix junit tests --- api/src/org/labkey/api/util/DateUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/org/labkey/api/util/DateUtil.java b/api/src/org/labkey/api/util/DateUtil.java index 0e4ab7c1755..1669e2bccca 100644 --- a/api/src/org/labkey/api/util/DateUtil.java +++ b/api/src/org/labkey/api/util/DateUtil.java @@ -1427,7 +1427,7 @@ public static Date combineDateTime(@Nullable Date date, @Nullable Date time) return date; if (null == date) return time; - Date newDate = (Date)date.clone(); + Date newDate = new Date(date.getTime()); newDate.setHours(time.getHours()); newDate.setMinutes(time.getMinutes()); newDate.setSeconds(time.getSeconds());