diff --git a/framework/src/main/groovy/org/moqui/impl/context/L10nFacadeImpl.java b/framework/src/main/groovy/org/moqui/impl/context/L10nFacadeImpl.java index dde0e54fa..77dcafe8b 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/L10nFacadeImpl.java +++ b/framework/src/main/groovy/org/moqui/impl/context/L10nFacadeImpl.java @@ -145,7 +145,10 @@ public BigDecimal roundCurrency(BigDecimal amount, String uomId, boolean precise @Override public Time parseTime(String input, String format) { - Locale curLocale = getLocale(); + return parseTime(input, format, null); + } + public Time parseTime(String input, String format, Locale locale) { + Locale curLocale = locale != null ? locale : getLocale(); TimeZone curTz = getTimeZone(); if (format == null || format.isEmpty()) format = "HH:mm:ss.SSS"; Calendar cal = calendarValidator.validate(input, format, curLocale, curTz); @@ -172,18 +175,21 @@ public Time parseTime(String input, String format) { return null; } public String formatTime(Time input, String format, Locale locale, TimeZone tz) { - if (locale == null) locale = getLocale(); + Locale curLocale = locale != null ? locale : getLocale(); if (tz == null) tz = getTimeZone(); if (format == null || format.isEmpty()) format = "HH:mm:ss"; - String timeStr = calendarValidator.format(input, format, locale, tz); + String timeStr = calendarValidator.format(input, format, curLocale, tz); // logger.warn("============= formatTime input=${input} timeStr=${timeStr} long=${input.getTime()}") return timeStr; } @Override public java.sql.Date parseDate(String input, String format) { + return parseDate(input, format, null); + } + public java.sql.Date parseDate(String input, String format, Locale locale) { + Locale curLocale = locale != null ? locale : getLocale(); if (format == null || format.isEmpty()) format = "yyyy-MM-dd"; - Locale curLocale = getLocale(); // NOTE DEJ 20150317 Date parsing in terms of time zone causes funny issues because the time part of the long // since epoch representation is lost going to/from the DB, especially since the time portion is set to 0 and @@ -195,9 +201,9 @@ public java.sql.Date parseDate(String input, String format) { /* TimeZone curTz = getTimeZone() Calendar cal = calendarValidator.validate(input, format, curLocale, curTz) - if (cal == null) cal = calendarValidator.validate(input, "MM/dd/yyyy", curLocale, curTz) + if (cal == null) cal = calendarValidator.validate(input, "MM/dd/yyyy", locale, curTz) // also try the full ISO-8601, dates may come in that way - if (cal == null) cal = calendarValidator.validate(input, "yyyy-MM-dd'T'HH:mm:ssZ", curLocale, curTz) + if (cal == null) cal = calendarValidator.validate(input, "yyyy-MM-dd'T'HH:mm:ssZ", locale, curTz) */ Calendar cal = calendarValidator.validate(input, format, curLocale); @@ -304,31 +310,33 @@ public String formatDateTime(Calendar input, String format, Locale locale, TimeZ } @Override public BigDecimal parseNumber(String input, String format) { - return bigDecimalValidator.validate(input, format, getLocale()); } + return parseNumber(input, format, null); + } + @Override public BigDecimal parseNumber(String input, String format, Locale locale) { + Locale curLocale = locale != null? locale: getLocale(); + return bigDecimalValidator.validate(input, format, curLocale); } public String formatNumber(Number input, String format, Locale locale) { - if (locale == null) locale = getLocale(); - return bigDecimalValidator.format(input, format, locale); + Locale curLocale = locale != null ? locale : getLocale(); + return bigDecimalValidator.format(input, format, curLocale); } @Override - public String format(Object value, String format) { - return this.format(value, format, getLocale(), getTimeZone()); - } + public String format(Object value, String format) { return this.format(value, format, null, getTimeZone()); } @Override public String format(Object value, String format, Locale locale, TimeZone tz) { if (value == null) return ""; - if (locale == null) locale = getLocale(); + Locale curLocale = locale != null ? locale : getLocale(); if (tz == null) tz = getTimeZone(); Class valueClass = value.getClass(); if (valueClass == String.class) return (String) value; - if (valueClass == Timestamp.class) return formatTimestamp((Timestamp) value, format, locale, tz); - if (valueClass == java.util.Date.class) return formatTimestamp((java.util.Date) value, format, locale, tz); - if (valueClass == java.sql.Date.class) return formatDate((Date) value, format, locale, tz); - if (valueClass == Time.class) return formatTime((Time) value, format, locale, tz); + if (valueClass == Timestamp.class) return formatTimestamp((Timestamp) value, format, curLocale, tz); + if (valueClass == java.util.Date.class) return formatTimestamp((java.util.Date) value, format, curLocale, tz); + if (valueClass == java.sql.Date.class) return formatDate((Date) value, format, curLocale, tz); + if (valueClass == Time.class) return formatTime((Time) value, format, curLocale, tz); // this one needs to be instanceof to include the many sub-classes of Number - if (value instanceof Number) return formatNumber((Number) value, format, locale); + if (value instanceof Number) return formatNumber((Number) value, format, curLocale); // Calendar is an abstract class, so must use instanceof here as well - if (value instanceof Calendar) return formatDateTime((Calendar) value, format, locale, tz); + if (value instanceof Calendar) return formatDateTime((Calendar) value, format, curLocale, tz); return value.toString(); } } diff --git a/framework/src/main/groovy/org/moqui/impl/entity/EntityDataLoaderImpl.groovy b/framework/src/main/groovy/org/moqui/impl/entity/EntityDataLoaderImpl.groovy index 555f8d8b8..a67bba023 100644 --- a/framework/src/main/groovy/org/moqui/impl/entity/EntityDataLoaderImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/entity/EntityDataLoaderImpl.groovy @@ -44,6 +44,7 @@ import javax.xml.parsers.SAXParserFactory import java.nio.charset.StandardCharsets import java.util.zip.ZipEntry import java.util.zip.ZipInputStream +import java.util.Locale @CompileStatic class EntityDataLoaderImpl implements EntityDataLoader { @@ -383,7 +384,7 @@ class EntityDataLoaderImpl implements EntityDataLoader { ValueHandler(EntityDataLoaderImpl edli) { this.edli = edli } abstract void handleValue(EntityValue value) - abstract void handlePlainMap(String entityName, Map value) + abstract void handlePlainMap(String entityName, Map value, Locale locale) abstract void handleService(ServiceCallSync scs) } static class CheckValueHandler extends ValueHandler { @@ -401,8 +402,8 @@ class EntityDataLoaderImpl implements EntityDataLoader { long getFieldsChecked() { return fieldsChecked } void handleValue(EntityValue value) { value.checkAgainstDatabase(messageList) } - void handlePlainMap(String entityName, Map value) { - EntityList el = edli.getEfi().getValueListFromPlainMap(value, entityName) + void handlePlainMap(String entityName, Map value, Locale locale) { + EntityList el = edli.getEfi().getValueListFromPlainMap(value, entityName, locale) // logger.warn("=========== Check value: ${value}\nel: ${el}") for (EntityValue ev in el) fieldsChecked += ev.checkAgainstDatabase(messageList) } @@ -461,11 +462,11 @@ class EntityDataLoaderImpl implements EntityDataLoader { value.createOrUpdate() } } - void handlePlainMap(String entityName, Map value) { + void handlePlainMap(String entityName, Map value, Locale locale) { EntityDefinition ed = ec.entityFacade.getEntityDefinition(entityName) if (ed == null) throw new BaseException("Could not find entity ${entityName}") if (edli.onlyCreate) { - EntityList el = ec.entityFacade.getValueListFromPlainMap(value, entityName) + EntityList el = ec.entityFacade.getValueListFromPlainMap(value, entityName, locale) int elSize = el.size() for (int i = 0; i < elSize; i++) { EntityValue curValue = (EntityValue) el.get(i) @@ -480,7 +481,7 @@ class EntityDataLoaderImpl implements EntityDataLoader { } } else { Map results = new HashMap() - EntityAutoServiceRunner.storeEntity(ec, ed, value, results, null) + EntityAutoServiceRunner.storeEntity(ec, ed, value, results, null, locale) // no need to call the store auto service, use storeEntity directly: // Map results = sfi.sync().name('store', entityName).parameters(value).call() if (logger.isTraceEnabled()) logger.trace("Called store service for entity [${entityName}] in data load, results: ${results}") @@ -516,9 +517,9 @@ class EntityDataLoaderImpl implements EntityDataLoader { void handleValue(EntityValue value) { el.add(value) } - void handlePlainMap(String entityName, Map value) { + void handlePlainMap(String entityName, Map value, Locale locale) { EntityDefinition ed = edli.getEfi().getEntityDefinition(entityName) - edli.getEfi().addValuesFromPlainMapRecursive(ed, value, el, null) + edli.getEfi().addValuesFromPlainMapRecursive(ed, value, el, null, locale) } void handleService(ServiceCallSync scs) { logger.warn("For load to EntityList not calling service [${scs.getServiceName()}] with parameters ${scs.getCurrentParameters()}") } } @@ -545,6 +546,7 @@ class EntityDataLoaderImpl implements EntityDataLoader { protected long valuesRead = 0 protected List messageList = new LinkedList<>() String location + Locale locale = null protected boolean loadElements = false @@ -560,13 +562,26 @@ class EntityDataLoaderImpl implements EntityDataLoader { void startElement(String ns, String localName, String qName, Attributes attributes) { // logger.info("startElement ns [${ns}], localName [${localName}] qName [${qName}]") String type = null - if (qName == "entity-facade-xml") { type = attributes.getValue("type") } - else if (qName == "seed-data") { type = "seed" } + String localeStr = null + if (qName == "entity-facade-xml") { + type = attributes.getValue("type") + localeStr = attributes.getValue("locale") + } + else if (qName == "seed-data") { + type = "seed" + localeStr = attributes.getValue("locale") + } if (type && edli.dataTypes && !edli.dataTypes.contains(type)) { if (logger.isInfoEnabled()) logger.info("Skipping file [${location}], is a type to skip (${type})") throw new TypeToSkipException() } + if (localeStr != null) { + int usIdx = localeStr.indexOf("_") + locale = (usIdx < 0 ? new Locale(localeStr) : + new Locale(localeStr.substring(0, usIdx), localeStr.substring(usIdx+1).toUpperCase())) + } + if (qName == "entity-facade-xml") { loadElements = true return @@ -758,11 +773,11 @@ class EntityDataLoaderImpl implements EntityDataLoader { // if (currentEntityDef.getFullEntityName().contains("DbForm")) logger.warn("========= DbForm rootValueMap: ${rootValueMap}") if (edli.dummyFks || edli.useTryInsert) { EntityValue curValue = currentEntityDef.makeEntityValue() - curValue.setAll(valueMap) + curValue.setAll(valueMap, locale) valueHandler.handleValue(curValue) valuesRead++ } else { - valueHandler.handlePlainMap(currentEntityDef.getFullEntityName(), valueMap) + valueHandler.handlePlainMap(currentEntityDef.getFullEntityName(), valueMap, getLocale()) valuesRead++ } currentEntityDef = (EntityDefinition) null @@ -794,6 +809,12 @@ class EntityDataLoaderImpl implements EntityDataLoader { } void setDocumentLocator(Locator locator) { this.locator = locator } + + void setLocale(Locale locale) { this.locale = locale } + Locale getLocale() { + if (locale == null) locale = new Locale("en", "US") + return locale + } } static class EntityCsvHandler { @@ -803,6 +824,8 @@ class EntityDataLoaderImpl implements EntityDataLoader { protected long valuesRead = 0 protected List messageList = new LinkedList() + protected Locale locale = null + EntityCsvHandler(EntityDataLoaderImpl edli, ValueHandler valueHandler) { this.edli = edli this.valueHandler = valueHandler @@ -853,6 +876,14 @@ class EntityDataLoaderImpl implements EntityDataLoader { return false } } + + if (firstLineRecord.size() > 2) { + // third field is locale + String localeStr = firstLineRecord.get(2) + int usIdx = localeStr.indexOf("_") + locale = usIdx < 0 ? new Locale(localeStr) : + new Locale(localeStr.substring(0, usIdx), localeStr.substring(usIdx+1).toUpperCase()) + } } Map headerMap = [:] @@ -880,9 +911,9 @@ class EntityDataLoaderImpl implements EntityDataLoader { valuesRead++ } else { EntityValueImpl currentEntityValue = (EntityValueImpl) edli.efi.makeValue(entityName) - if (edli.defaultValues) currentEntityValue.setFields(edli.defaultValues, true, null, null) + if (edli.defaultValues) currentEntityValue.setFields(edli.defaultValues, true, null, null, locale) for (Map.Entry header in headerMap) - currentEntityValue.setString(header.key, record.get(header.value)) + currentEntityValue.setString(header.key, record.get(header.value), locale) if (!currentEntityValue.containsPrimaryKey()) { if (currentEntityValue.getEntityDefinition().getPkFieldNames().size() == 1) { @@ -899,6 +930,13 @@ class EntityDataLoaderImpl implements EntityDataLoader { } return true } + + void setLocale(Locale locale) { this.locale = locale } + Locale getLocale() { + if (locale == null) locale = new Locale("en", "US") + return locale + } + } static class EntityJsonHandler { @@ -908,6 +946,8 @@ class EntityDataLoaderImpl implements EntityDataLoader { protected long valuesRead = 0 protected List messageList = new LinkedList() + protected Locale locale = null + EntityJsonHandler(EntityDataLoaderImpl edli, ValueHandler valueHandler) { this.edli = edli this.valueHandler = valueHandler @@ -929,18 +969,21 @@ class EntityDataLoaderImpl implements EntityDataLoader { } String type = null + String localeStr = null List valueList if (jsonObj instanceof Map) { Map jsonMap = (Map) jsonObj type = jsonMap.get("_dataType") + localeStr = jsonMap.get("_locale") valueList = [jsonObj] } else if (jsonObj instanceof List) { valueList = (List) jsonObj Object firstValue = valueList?.get(0) if (firstValue instanceof Map) { Map firstValMap = (Map) firstValue - if (firstValMap.get("_dataType")) { + if (firstValMap.get("_dataType") || firstValMap.get("_locale")) { type = firstValMap.get("_dataType") + localeStr = firstValMap.get("_locale") valueList.remove((int) 0I) } } @@ -953,6 +996,12 @@ class EntityDataLoaderImpl implements EntityDataLoader { return false } + if (localeStr != null) { + int usIdx = localeStr.indexOf("_") + locale = usIdx < 0 ? new Locale(localeStr) : + new Locale(localeStr.substring(0, usIdx), localeStr.substring(usIdx+1).toUpperCase()) + } + for (Object valueObj in valueList) { if (!(valueObj instanceof Map)) { logger.warn("Found non-Map object in JSON import, skipping: ${valueObj}") @@ -964,6 +1013,13 @@ class EntityDataLoaderImpl implements EntityDataLoader { value.putAll((Map) valueObj) String entityName = value."_entity" + String valueLocaleStr = value."_locale" + Locale valueLocale = null + if (valueLocaleStr) { + int usIdx = valueLocaleStr.indexOf("_") + valueLocale = usIdx < 0 ? new Locale(valueLocaleStr) : + new Locale(valueLocaleStr.substring(0, usIdx), valueLocaleStr.substring(usIdx+1).toUpperCase()) + } boolean isService if (edli.efi.isEntityDefined(entityName)) { isService = false @@ -978,7 +1034,7 @@ class EntityDataLoaderImpl implements EntityDataLoader { valueHandler.handleService(currentScs) valuesRead++ } else { - valueHandler.handlePlainMap(entityName, value) + valueHandler.handlePlainMap(entityName, value, valueLocale?:getLocale()) // TODO: make this more complete, like counting nested Maps? valuesRead++ } @@ -986,5 +1042,12 @@ class EntityDataLoaderImpl implements EntityDataLoader { return true } + + void setLocale(Locale locale) { this.locale = locale } + Locale getLocale() { + if (locale == null) locale = new Locale("en", "US") + return locale + } + } } diff --git a/framework/src/main/groovy/org/moqui/impl/entity/EntityDataWriterImpl.groovy b/framework/src/main/groovy/org/moqui/impl/entity/EntityDataWriterImpl.groovy index b19f07a89..1f426574b 100644 --- a/framework/src/main/groovy/org/moqui/impl/entity/EntityDataWriterImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/entity/EntityDataWriterImpl.groovy @@ -304,7 +304,7 @@ class EntityDataWriterImpl implements EntityDataWriter { writer.println("[") } else { writer.println("") - writer.println("") + writer.println("") } } private void endFile(Writer writer) { diff --git a/framework/src/main/groovy/org/moqui/impl/entity/EntityDefinition.groovy b/framework/src/main/groovy/org/moqui/impl/entity/EntityDefinition.groovy index fc716aa79..f7a58b90f 100644 --- a/framework/src/main/groovy/org/moqui/impl/entity/EntityDefinition.groovy +++ b/framework/src/main/groovy/org/moqui/impl/entity/EntityDefinition.groovy @@ -952,11 +952,12 @@ class EntityDefinition { return mePkFieldToAliasNameMap } - Object convertFieldString(String name, String value, ExecutionContextImpl eci) { + Object convertFieldString(String name, String value, ExecutionContextImpl eci) { return convertFieldString(name, value, eci, null) } + Object convertFieldString(String name, String value, ExecutionContextImpl eci, Locale locale) { if (value == null) return null FieldInfo fieldInfo = getFieldInfo(name) if (fieldInfo == null) throw new EntityException("Invalid field name ${name} for entity ${fullEntityName}") - return fieldInfo.convertFromString(value, eci.l10nFacade) + return fieldInfo.convertFromString(value, eci.l10nFacade, locale) } static String getFieldStringForFile(FieldInfo fieldInfo, Object value) { diff --git a/framework/src/main/groovy/org/moqui/impl/entity/EntityFacadeImpl.groovy b/framework/src/main/groovy/org/moqui/impl/entity/EntityFacadeImpl.groovy index fec3139a6..0d73fdcc1 100644 --- a/framework/src/main/groovy/org/moqui/impl/entity/EntityFacadeImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/entity/EntityFacadeImpl.groovy @@ -1642,8 +1642,9 @@ class EntityFacadeImpl implements EntityFacade { if (operation == 'find') { if (lastEd.containsPrimaryKey(parameters)) { // if we have a full PK lookup by PK and return the single value + Map pkValues = [:] - lastEd.entityInfo.setFields(parameters, pkValues, false, null, true) + lastEd.entityInfo.setFields(parameters, pkValues, false, null, true, new java.util.Locale("en_US")) if (masterName != null && masterName.length() > 0) { Map resultMap = find(lastEd.getFullEntityName()).condition(pkValues).oneMaster(masterName) @@ -1693,7 +1694,7 @@ class EntityFacadeImpl implements EntityFacade { } } - EntityList getValueListFromPlainMap(Map value, String entityName) { + EntityList getValueListFromPlainMap(Map value, String entityName, Locale locale) { if (entityName == null || entityName.length() == 0) entityName = value."_entity" if (entityName == null || entityName.length() == 0) throw new EntityException("No entityName passed and no _entity field in value Map") @@ -1701,10 +1702,10 @@ class EntityFacadeImpl implements EntityFacade { if (ed == null) throw new EntityNotFoundException("Not entity found with name ${entityName}") EntityList valueList = new EntityListImpl(this) - addValuesFromPlainMapRecursive(ed, value, valueList, null) + addValuesFromPlainMapRecursive(ed, value, valueList, null, locale) return valueList } - void addValuesFromPlainMapRecursive(EntityDefinition ed, Map value, EntityList valueList, Map parentPks) { + void addValuesFromPlainMapRecursive(EntityDefinition ed, Map value, EntityList valueList, Map parentPks, Locale locale) { // add in all of the main entity's primary key fields, this is necessary for auto-generated, and to // allow them to be left out of related records if (parentPks != null) { @@ -1713,7 +1714,7 @@ class EntityFacadeImpl implements EntityFacade { } EntityValue newEntityValue = makeValue(ed.getFullEntityName()) - newEntityValue.setFields(value, true, null, null) + newEntityValue.setFields(value, true, null, null, locale) valueList.add(newEntityValue) Map sharedPkMap = newEntityValue.getPrimaryKeys() @@ -1751,11 +1752,11 @@ class EntityFacadeImpl implements EntityFacade { boolean isEntityValue = relParmObj instanceof EntityValue if (relParmObj instanceof Map && !isEntityValue) { - addValuesFromPlainMapRecursive(subEd, (Map) relParmObj, valueList, pkMap) + addValuesFromPlainMapRecursive(subEd, (Map) relParmObj, valueList, pkMap, locale) } else if (relParmObj instanceof List) { for (Object relParmEntry in relParmObj) { if (relParmEntry instanceof Map) { - addValuesFromPlainMapRecursive(subEd, (Map) relParmEntry, valueList, pkMap) + addValuesFromPlainMapRecursive(subEd, (Map) relParmEntry, valueList, pkMap, locale) } else { logger.warn("In entity values from plain map for entity ${ed.getFullEntityName()} found list for sub-object ${entryName} with a non-Map entry: ${relParmEntry}") } @@ -2011,7 +2012,7 @@ class EntityFacadeImpl implements EntityFacade { // NOTE: the following uses the same pattern as EntityDataLoaderImpl.LoadValueHandler if (dummyFks || useTryInsert) { EntityValue curValue = ed.makeEntityValue() - curValue.setAll(entry.getEtlValues()) + curValue.setAll(entry.getEtlValues(), null) if (useTryInsert) { try { curValue.create() diff --git a/framework/src/main/groovy/org/moqui/impl/entity/EntityFindBase.groovy b/framework/src/main/groovy/org/moqui/impl/entity/EntityFindBase.groovy index 1aa73e835..70c3276b0 100644 --- a/framework/src/main/groovy/org/moqui/impl/entity/EntityFindBase.groovy +++ b/framework/src/main/groovy/org/moqui/impl/entity/EntityFindBase.groovy @@ -173,7 +173,7 @@ abstract class EntityFindBase implements EntityFind { singleCondField = (String) null singleCondValue = null } - getEntityDef().entityInfo.setFields(fields, simpleAndMap, true, null, null) + getEntityDef().entityInfo.setFields(fields, simpleAndMap, true, null, null, null) } return this } diff --git a/framework/src/main/groovy/org/moqui/impl/entity/EntityJavaUtil.java b/framework/src/main/groovy/org/moqui/impl/entity/EntityJavaUtil.java index 128045791..f5679d7d8 100644 --- a/framework/src/main/groovy/org/moqui/impl/entity/EntityJavaUtil.java +++ b/framework/src/main/groovy/org/moqui/impl/entity/EntityJavaUtil.java @@ -353,7 +353,7 @@ public static class EntityInfo { } } - void setFields(Map src, Map dest, boolean setIfEmpty, String namePrefix, Boolean pks) { + void setFields(Map src, Map dest, boolean setIfEmpty, String namePrefix, Boolean pks, Locale locale) { if (src == null || dest == null) return; ExecutionContextImpl eci = efi.ecfi.getEci(); @@ -417,7 +417,7 @@ void setFields(Map src, Map dest, boolean setIfE } } - void setFieldsEv(Map src, EntityValueBase dest, Boolean pks) { + void setFieldsEv(Map src, EntityValueBase dest, Boolean pks, Locale locale) { // like above with setIfEmpty=true, namePrefix=null, pks=null if (src == null || dest == null) return; @@ -448,7 +448,7 @@ void setFieldsEv(Map src, EntityValueBase dest, Boolean pks) { if (!isEmpty) { if (isCharSequence) { try { - Object converted = fi.convertFromString(value.toString(), eci.l10nFacade); + Object converted = fi.convertFromString(value.toString(), eci.l10nFacade, locale); dest.putNoCheck(fieldName, converted); } catch (BaseException be) { eci.messageFacade.addValidationError(null, fieldName, null, be.getMessage(), be); diff --git a/framework/src/main/groovy/org/moqui/impl/entity/EntityValueBase.java b/framework/src/main/groovy/org/moqui/impl/entity/EntityValueBase.java index 626777de3..0f443dfa8 100644 --- a/framework/src/main/groovy/org/moqui/impl/entity/EntityValueBase.java +++ b/framework/src/main/groovy/org/moqui/impl/entity/EntityValueBase.java @@ -401,15 +401,17 @@ public boolean primaryKeyMatches(EntityValueBase evb) { } @Override public EntityValue set(String name, Object value) { put(name, value); return this; } - @Override public EntityValue setAll(Map fields) { + @Override public EntityValue setAll(Map fields) { return setAll(fields, null); } + @Override public EntityValue setAll(Map fields, Locale locale) { if (!mutable) throw new EntityException("Cannot set fields, this entity value is not mutable (it is read-only)"); - getEntityDefinition().entityInfo.setFieldsEv(fields, this, null); + getEntityDefinition().entityInfo.setFieldsEv(fields, this, null, locale); return this; } - @Override public EntityValue setString(String name, String value) { + @Override public EntityValue setString(String name, String value) { return setString(name, value, null); } + @Override public EntityValue setString(String name, String value, Locale locale) { // this will do a field name check ExecutionContextImpl eci = getEntityFacadeImpl().ecfi.getEci(); - Object converted = getEntityDefinition().convertFieldString(name, value, eci); + Object converted = getEntityDefinition().convertFieldString(name, value, eci, locale); putNoCheck(name, converted); return this; } @@ -467,10 +469,13 @@ public boolean primaryKeyMatches(EntityValueBase evb) { } @Override public EntityValue setFields(Map fields, boolean setIfEmpty, String namePrefix, Boolean pks) { + return setFields(fields, setIfEmpty, namePrefix, pks, null); + } + @Override public EntityValue setFields(Map fields, boolean setIfEmpty, String namePrefix, Boolean pks, Locale locale) { if (!setIfEmpty && (namePrefix == null || namePrefix.length() == 0)) { - getEntityDefinition().entityInfo.setFields(fields, this, false, namePrefix, pks); + getEntityDefinition().entityInfo.setFields(fields, this, false, namePrefix, pks, locale); } else { - getEntityDefinition().entityInfo.setFieldsEv(fields, this, pks); + getEntityDefinition().entityInfo.setFieldsEv(fields, this, pks, locale); } return this; @@ -506,7 +511,7 @@ public EntityValue setSequencedIdSecondary() { this.remove(seqFieldName); Map otherPkMap = new LinkedHashMap<>(); - getEntityDefinition().entityInfo.setFields(this, otherPkMap, false, null, true); + getEntityDefinition().entityInfo.setFields(this, otherPkMap, false, null, true, null); // temporarily disable authz for this, just doing lookup to get next value and to allow for a // authorize-skip="create" with authorize-skip of view too this is necessary diff --git a/framework/src/main/groovy/org/moqui/impl/entity/FieldInfo.java b/framework/src/main/groovy/org/moqui/impl/entity/FieldInfo.java index c0d3fa1ad..d336b6719 100644 --- a/framework/src/main/groovy/org/moqui/impl/entity/FieldInfo.java +++ b/framework/src/main/groovy/org/moqui/impl/entity/FieldInfo.java @@ -162,6 +162,9 @@ static BigDecimal safeStripZeroes(BigDecimal input) { } public Object convertFromString(String value, L10nFacadeImpl l10n) { + return convertFromString(value, l10n, null); + } + public Object convertFromString(String value, L10nFacadeImpl l10n, Locale locale) { if (value == null) return null; if ("null".equals(value)) return null; @@ -173,17 +176,17 @@ public Object convertFromString(String value, L10nFacadeImpl l10n) { case 1: outValue = value; break; case 2: // outValue = java.sql.Timestamp.valueOf(value); if (isEmpty) { outValue = null; break; } - outValue = l10n.parseTimestamp(value, null); + outValue = l10n.parseTimestamp(value, null, locale, null); if (outValue == null) throw new BaseArtifactException("The value [" + value + "] is not a valid date/time for field " + entityName + "." + name); break; case 3: // outValue = java.sql.Time.valueOf(value); if (isEmpty) { outValue = null; break; } - outValue = l10n.parseTime(value, null); + outValue = l10n.parseTime(value, null, locale); if (outValue == null) throw new BaseArtifactException("The value [" + value + "] is not a valid time for field " + entityName + "." + name); break; case 4: // outValue = java.sql.Date.valueOf(value); if (isEmpty) { outValue = null; break; } - outValue = l10n.parseDate(value, null); + outValue = l10n.parseDate(value, null, locale); if (outValue == null) throw new BaseArtifactException("The value [" + value + "] is not a valid date for field " + entityName + "." + name); break; case 5: // outValue = Integer.valueOf(value); break @@ -192,7 +195,7 @@ public Object convertFromString(String value, L10nFacadeImpl l10n) { case 8: // outValue = Double.valueOf(value); break case 9: // outValue = new BigDecimal(value); break if (isEmpty) { outValue = null; break; } - BigDecimal bdVal = l10n.parseNumber(value, null); + BigDecimal bdVal = l10n.parseNumber(value, null, locale); if (bdVal == null) { throw new BaseArtifactException("The value [" + value + "] is not valid for type " + javaType + " for field " + entityName + "." + name); } else { @@ -220,7 +223,7 @@ public Object convertFromString(String value, L10nFacadeImpl l10n) { case 13: outValue = value; break; case 14: if (isEmpty) { outValue = null; break; } - Timestamp ts = l10n.parseTimestamp(value, null); + Timestamp ts = l10n.parseTimestamp(value, null, locale, null); outValue = new java.util.Date(ts.getTime()); break; // better way for Collection (15)? maybe parse comma separated, but probably doesn't make sense in the first place diff --git a/framework/src/main/groovy/org/moqui/impl/service/runner/EntityAutoServiceRunner.groovy b/framework/src/main/groovy/org/moqui/impl/service/runner/EntityAutoServiceRunner.groovy index 9a677ff0a..58345d103 100644 --- a/framework/src/main/groovy/org/moqui/impl/service/runner/EntityAutoServiceRunner.groovy +++ b/framework/src/main/groovy/org/moqui/impl/service/runner/EntityAutoServiceRunner.groovy @@ -116,7 +116,11 @@ class EntityAutoServiceRunner implements ServiceRunner { } protected static boolean checkAllPkFields(EntityDefinition ed, Map parameters, Map tempResult, - EntityValue newEntityValue, ArrayList outParamNames) { + EntityValue newEntityValue, ArrayList outParamNames) { + return checkAllPkFields(ed, parameters, tempResult, newEntityValue, outParamNames, null); + } + protected static boolean checkAllPkFields(EntityDefinition ed, Map parameters, Map tempResult, + EntityValue newEntityValue, ArrayList outParamNames, Locale locale) { FieldInfo[] pkFieldInfos = ed.entityInfo.pkFieldInfoArray // see if all PK fields were passed in @@ -167,7 +171,7 @@ class EntityAutoServiceRunner implements ServiceRunner { } } else if (allPksIn) { /* **** plain specified primary key **** */ - newEntityValue.setFields(parameters, true, null, true) + newEntityValue.setFields(parameters, true, null, true, locale) } else { logger.error("Entity [${ed.fullEntityName}] auto create pk fields ${ed.getPkFieldNames()} incomplete: ${parameters}" + "\nCould not find a valid combination of primary key settings to do a create operation; options include: " + @@ -305,12 +309,20 @@ class EntityAutoServiceRunner implements ServiceRunner { /** Does a create if record does not exist, or update if it does. */ static void storeEntity(ExecutionContextImpl eci, EntityDefinition ed, Map parameters, - Map result, ArrayList outParamNames) { - storeRecursive(eci.ecfi, eci.getEntityFacade(), ed, parameters, result, outParamNames, null) + Map result, ArrayList outParamNames) { + storeEntity(eci, ed, parameters, result, outParamNames, null) + } + static void storeEntity(ExecutionContextImpl eci, EntityDefinition ed, Map parameters, + Map result, ArrayList outParamNames, Locale locale) { + storeRecursive(eci.ecfi, eci.getEntityFacade(), ed, parameters, result, outParamNames, null, locale) } static void storeRecursive(ExecutionContextFactoryImpl ecfi, EntityFacadeImpl efi, EntityDefinition ed, Map parameters, Map result, ArrayList outParamNames, Map parentPks) { + storeRecursive( ecfi, efi, ed, parameters, result, outParamNames, parentPks, null) + } + static void storeRecursive(ExecutionContextFactoryImpl ecfi, EntityFacadeImpl efi, EntityDefinition ed, Map parameters, + Map result, ArrayList outParamNames, Map parentPks, Locale locale) { EntityValue newEntityValue = efi.makeValue(ed.getFullEntityName()) // add in all of the main entity's primary key fields, this is necessary for auto-generated, and to @@ -323,14 +335,14 @@ class EntityAutoServiceRunner implements ServiceRunner { checkFromDate(ed, parameters, result, ecfi) Map tempResult = [:] - boolean allPksIn = checkAllPkFields(ed, parameters, tempResult, newEntityValue, outParamNames) + boolean allPksIn = checkAllPkFields(ed, parameters, tempResult, newEntityValue, outParamNames, locale) result.putAll(tempResult) if (!allPksIn) { // we had to fill some stuff in, so do a create - newEntityValue.setFields(parameters, true, null, false) + newEntityValue.setFields(parameters, true, null, false, locale) newEntityValue.create() - storeRelated(ecfi, efi, (EntityValueBase) newEntityValue, parameters, result, parentPks) + storeRelated(ecfi, efi, (EntityValueBase) newEntityValue, parameters, result, parentPks, locale) return } @@ -343,23 +355,28 @@ class EntityAutoServiceRunner implements ServiceRunner { checkStatus(ed, parameters, result, outParamNames, lookedUpValue, efi) } else { // no lookedUpValue at this point? doesn't exist so create - newEntityValue.setFields(parameters, true, null, false) + newEntityValue.setFields(parameters, true, null, false, locale) newEntityValue.create() - storeRelated(ecfi, efi, (EntityValueBase) newEntityValue, parameters, result, parentPks) + storeRelated(ecfi, efi, (EntityValueBase) newEntityValue, parameters, result, parentPks, locale) return } } if (lookedUpValue == null) lookedUpValue = newEntityValue - lookedUpValue.setFields(parameters, true, null, false) + lookedUpValue.setFields(parameters, true, null, false, locale) // logger.info("In auto updateEntity lookedUpValue final [${lookedUpValue}] for parameters [${parameters}]") lookedUpValue.createOrUpdate() - storeRelated(ecfi, efi, (EntityValueBase) lookedUpValue, parameters, result, parentPks) + storeRelated(ecfi, efi, (EntityValueBase) lookedUpValue, parameters, result, parentPks, locale) } static void storeRelated(ExecutionContextFactoryImpl ecfi, EntityFacadeImpl efi, EntityValueBase parentValue, Map parameters, Map result, Map parentPks) { + storeRelated(ecfi, efi, parentValue, parameters, result, parentPks, null) + } + static void storeRelated(ExecutionContextFactoryImpl ecfi, EntityFacadeImpl efi, EntityValueBase parentValue, + Map parameters, Map result, Map parentPks, + Locale locale) { EntityDefinition ed = parentValue.getEntityDefinition() // NOTE: keep a separate Map of parent PK values to pass down, can't just be current record's PK fields because @@ -408,14 +425,14 @@ class EntityAutoServiceRunner implements ServiceRunner { boolean isEntityValue = relParmObj instanceof EntityValue if (relParmObj instanceof Map && !isEntityValue) { Map relResults = [:] - storeRecursive(ecfi, efi, subEd, (Map) relParmObj, relResults, null, pkMap) + storeRecursive(ecfi, efi, subEd, (Map) relParmObj, relResults, null, pkMap, locale) result.put(entryName, relResults) } else if (relParmObj instanceof List) { List relResultList = [] for (Object relParmEntry in relParmObj) { Map relResults = [:] if (relParmEntry instanceof Map) { - storeRecursive(ecfi, efi, subEd, (Map) relParmEntry, relResults, null, pkMap) + storeRecursive(ecfi, efi, subEd, (Map) relParmEntry, relResults, null, pkMap, locale) } else { logger.warn("In entity auto create for entity ${ed.getFullEntityName()} found list for sub-object ${entryName} with a non-Map entry: ${relParmEntry}") } diff --git a/framework/src/main/java/org/moqui/context/L10nFacade.java b/framework/src/main/java/org/moqui/context/L10nFacade.java index a95cc6432..d161e70dc 100644 --- a/framework/src/main/java/org/moqui/context/L10nFacade.java +++ b/framework/src/main/java/org/moqui/context/L10nFacade.java @@ -72,4 +72,5 @@ public interface L10nFacade { java.util.Calendar parseDateTime(String input, String format); java.math.BigDecimal parseNumber(String input, String format); + java.math.BigDecimal parseNumber(String input, String format, Locale locale); } diff --git a/framework/src/main/java/org/moqui/entity/EntityValue.java b/framework/src/main/java/org/moqui/entity/EntityValue.java index 9a066c435..7eefa91e6 100644 --- a/framework/src/main/java/org/moqui/entity/EntityValue.java +++ b/framework/src/main/java/org/moqui/entity/EntityValue.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Locale; /** Entity Value Interface - Represents a single database record. */ @@ -90,6 +91,29 @@ public interface EntityValue extends Map, Externalizable, Compar */ EntityValue setAll(Map fields); + /** Sets fields on this entity from the Map of fields passed in using the entity definition to only get valid + * fields from the Map. For any String values passed in this will call setString to convert based on the field + * definition, otherwise it sets the Object as-is. + * + * @param fields The fields Map to get the values from + * @param locale The locale to use when parsing the values + * @return reference to this for convenience + */ + EntityValue setAll(Map fields, Locale locale); + + /** Sets the named field to the passed value, converting the value from a String to the corresponding type using + * Type.valueOf() + * + * If the String "null" is passed in it will be treated the same as a null value. If you really want to set a + * String of "null" then pass in "\null". + * + * @param name The field name to set + * @param value The String value to convert and set + * @param locale The locale to use when parsing the values + * @return reference to this for convenience + */ + EntityValue setString(String name, String value, Locale locale); + /** Sets the named field to the passed value, converting the value from a String to the corresponding type using * Type.valueOf() * @@ -131,6 +155,20 @@ public interface EntityValue extends Map, Externalizable, Compar */ EntityValue setFields(Map fields, boolean setIfEmpty, String namePrefix, Boolean pks); + /** Sets fields on this entity from the Map of fields passed in using the entity definition to only get valid + * fields from the Map. For any String values passed in this will call setString to convert based on the field + * definition, otherwise it sets the Object as-is. + * + * @param fields The fields Map to get the values from + * @param setIfEmpty Used to specify whether empty/null values in the field Map should be set + * @param namePrefix If not null or empty will be pre-pended to each field name (upper-casing the first letter of + * the field name first), and that will be used as the fields Map lookup name instead of the field-name + * @param pks If null, get all values, if TRUE just get PKs, if FALSE just get non-PKs + * @param locale Locale to use for parsing values + * @return reference to this for convenience + */ + EntityValue setFields(Map fields, boolean setIfEmpty, String namePrefix, Boolean pks, Locale locale); + /** Get the next guaranteed unique seq id for this entity, and set it in the primary key field. This will set it in * the first primary key field in the entity definition, but it really should be used for entities with only one * primary key field. diff --git a/framework/xsd/entity-definition-2.1.xsd b/framework/xsd/entity-definition-2.1.xsd index 3208fff11..1881fe0bc 100644 --- a/framework/xsd/entity-definition-2.1.xsd +++ b/framework/xsd/entity-definition-2.1.xsd @@ -336,7 +336,10 @@ along with this software (see the LICENSE.md file). If not, see --> - + + Set the locale to be used when parsing the string data to the corresponding data type. + Relevant mostly for decimal numbers where differing decimal separators result in different interpretations. +