This is an automated email from the ASF dual-hosted git repository.
ashishvijaywargiya pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/trunk by this push:
new ccd8f036d8 Groovy DSL Enhancements (#2010)
ccd8f036d8 is described below
commit ccd8f036d82769674e4776967f01325bb9a72066
Author: Ashish Vijaywargiya <[email protected]>
AuthorDate: Tue Sep 15 18:42:51 2026 +0530
Groovy DSL Enhancements (#2010)
Groovy DSL Enhancements - Here comes one more pass after so many years!!
1) Added resource/key i18n overloads to success()/error()/failure() in
the Groovy DSL, resolving the long-standing TODOs in GroovyBaseScript.
2) Added a fluent write-side entity DSL: create(),
update().where().set(), and delete().where(), matching the existing
read-side select()/from()/where().
3) Added require()/fail() control-flow sugar backed by a new
ServiceErrorException, so a failed check unwinds past nested closures
instead of only exiting them like return error(...) does.
4) Added runAsyncService() and the runAsync service:, with: calling form
for persisted (Job Scheduler backed) async service calls, mirroring
run()/runService().
5) Covered by new unit and integration tests in
GroovyDslServiceEngineTests, all passing.
---
framework/service/servicedef/services_test_se.xml | 68 ++++++
.../EntityDeleteBuilder.groovy} | 41 ++--
.../service/engine/EntityUpdateBuilder.groovy | 60 ++++++
.../ofbiz/service/engine/GroovyBaseScript.groovy | 92 ++++++--
.../apache/ofbiz/service/test/TestServices.groovy | 78 +++++++
.../ofbiz/service/ServiceErrorException.java} | 37 ++--
.../service/test/GroovyDslServiceEngineTests.java | 236 +++++++++++++++++++++
7 files changed, 553 insertions(+), 59 deletions(-)
diff --git a/framework/service/servicedef/services_test_se.xml
b/framework/service/servicedef/services_test_se.xml
index 463a97ab05..303bbd2d88 100644
--- a/framework/service/servicedef/services_test_se.xml
+++ b/framework/service/servicedef/services_test_se.xml
@@ -214,6 +214,74 @@ under the License.
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testPingErrorWithDSLCall">
<implements service="testGroovyPingSuccess"/>
</service>
+ <service name="testGroovyPingSuccessWithAsyncDSLCall" engine="groovy"
+
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testPingSuccessWithAsyncDSLCall">
+ <implements service="testGroovyPingSuccess"/>
+ <attribute name="pong" mode="OUT" type="String" optional="true"/>
+ </service>
+ <service name="testGroovyPingSuccessWithAsyncMapDSLCall" engine="groovy"
+
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testPingSuccessWithAsyncMapDSLCall">
+ <implements service="testGroovyPingSuccess"/>
+ <attribute name="pong" mode="OUT" type="String" optional="true"/>
+ </service>
+
+ <!--Test resource/key i18n overloads on success()/error()/failure() -->
+ <service name="testGroovyPingSuccessWithI18n" engine="groovy" auth="false"
+
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testPingSuccessWithI18n">
+ </service>
+ <service name="testGroovyPingSuccessWithI18nContext" engine="groovy"
auth="false"
+
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testPingSuccessWithI18nContext">
+ <attribute name="parameterName" mode="IN" type="String"/>
+ <attribute name="errorDetails" mode="IN" type="String"/>
+ </service>
+ <service name="testGroovyPingErrorWithI18n" engine="groovy" auth="false"
+
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testPingErrorWithI18n">
+ </service>
+ <service name="testGroovyPingErrorWithI18nContext" engine="groovy"
auth="false"
+
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testPingErrorWithI18nContext">
+ <attribute name="parameterName" mode="IN" type="String"/>
+ <attribute name="errorDetails" mode="IN" type="String"/>
+ </service>
+ <service name="testGroovyPingFailureWithI18n" engine="groovy" auth="false"
+
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testPingFailureWithI18n">
+ </service>
+
+ <!--Test require()/fail() control-flow sugar -->
+ <service name="testGroovyFail" engine="groovy" auth="false"
+
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testFail">
+ </service>
+ <service name="testGroovyRequire" engine="groovy" auth="false"
+
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testRequire">
+ <attribute name="conditionMet" mode="IN" type="Boolean"/>
+ </service>
+ <service name="testGroovyFailFromNestedClosure" engine="groovy"
auth="false"
+
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testFailFromNestedClosure">
+ </service>
+ <service name="testGroovyFailWithI18n" engine="groovy" auth="false"
+
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testFailWithI18n">
+ </service>
+ <service name="testGroovyRequireWithI18nContext" engine="groovy"
auth="false"
+
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testRequireWithI18nContext">
+ <attribute name="parameterName" mode="IN" type="String"/>
+ <attribute name="errorDetails" mode="IN" type="String"/>
+ </service>
+
+ <!--Test write-side entity DSL: create()/update()/delete() -->
+ <service name="testGroovyEntityDslCreate" engine="groovy" auth="false"
+
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testEntityDslCreate">
+ <attribute name="testingId" mode="IN" type="String"/>
+ <attribute name="testingName" mode="IN" type="String" optional="true"/>
+ </service>
+ <service name="testGroovyEntityDslUpdate" engine="groovy" auth="false"
+
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testEntityDslUpdate">
+ <attribute name="testingId" mode="IN" type="String"/>
+ <attribute name="testingName" mode="IN" type="String" optional="true"/>
+ </service>
+ <service name="testGroovyEntityDslDelete" engine="groovy" auth="false"
+
location="component://service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy"
invoke="testEntityDslDelete">
+ <attribute name="testingId" mode="IN" type="String"/>
+ <attribute name="rowsRemoved" mode="OUT" type="Integer"/>
+ </service>
<!--Test job tracker engine service -->
<service name="TestTopLevelServiceThatPlansTrackedServices"
engine="groovy-tracker" auth="true"
diff --git
a/framework/service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy
b/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/EntityDeleteBuilder.groovy
similarity index 52%
copy from
framework/service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy
copy to
framework/service/src/main/groovy/org/apache/ofbiz/service/engine/EntityDeleteBuilder.groovy
index 44fde00ea7..f8949ed522 100644
---
a/framework/service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy
+++
b/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/EntityDeleteBuilder.groovy
@@ -15,31 +15,30 @@
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
-*/
-package org.apache.ofbiz.service.test
+
*******************************************************************************/
+package org.apache.ofbiz.service.engine
-// Despite it's name, this class is not for test, it's only services
+import org.apache.ofbiz.entity.Delegator
+import org.apache.ofbiz.entity.GenericEntityException
-Map testPingSuccess() {
- Map returnMap = success('Service result success')
- if (parameters.ping) {
- returnMap.pong = parameters.ping
- }
- return returnMap
-}
+/**
+ * Fluent write-side helper backing {@link GroovyBaseScript#delete(String)}.
+ *
+ * Usage: {@code delete('SomeEntity').where(fields)} — bulk-removes every
matching record and
+ * returns the number of rows removed.
+ */
+class EntityDeleteBuilder {
+
+ private final Delegator delegator
+ private final String entityName
-Map testPingError() {
- Map returnMap = error('Service result error')
- if (parameters.ping) {
- returnMap.pong = parameters.ping
+ EntityDeleteBuilder(Delegator delegator, String entityName) {
+ this.delegator = delegator
+ this.entityName = entityName
}
- return returnMap
-}
-Map testPingSuccessWithDSLCall() {
- run service: 'testGroovyPingSuccess', with: parameters
-}
+ int where(Map<String, Object> fields) throws GenericEntityException {
+ return delegator.removeByAnd(entityName, fields)
+ }
-Map testPingErrorWithDSLCall() {
- run service: 'testGroovyPingError', with: parameters
}
diff --git
a/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/EntityUpdateBuilder.groovy
b/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/EntityUpdateBuilder.groovy
new file mode 100644
index 0000000000..8edf04a474
--- /dev/null
+++
b/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/EntityUpdateBuilder.groovy
@@ -0,0 +1,60 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.ofbiz.service.engine
+
+import org.apache.ofbiz.entity.Delegator
+import org.apache.ofbiz.entity.GenericValue
+import org.apache.ofbiz.entity.util.EntityQuery
+import org.apache.ofbiz.service.ServiceErrorException
+
+/**
+ * Fluent write-side helper backing {@link GroovyBaseScript#update(String)}.
+ *
+ * Usage: {@code update('SomeEntity').where(pkFields).set(fieldsToChange)} —
queries the record,
+ * merges the given fields into it, stores it and returns the updated {@link
GenericValue}.
+ * Throws {@link ServiceErrorException} if no record matches {@code where()},
so a missing record
+ * fails fast instead of silently no-op'ing.
+ */
+class EntityUpdateBuilder {
+
+ private final Delegator delegator
+ private final String entityName
+ private Map<String, Object> whereFields
+
+ EntityUpdateBuilder(Delegator delegator, String entityName) {
+ this.delegator = delegator
+ this.entityName = entityName
+ }
+
+ EntityUpdateBuilder where(Map<String, Object> whereFields) {
+ this.whereFields = whereFields
+ return this
+ }
+
+ GenericValue set(Map<String, Object> fields) throws ServiceErrorException {
+ GenericValue existing =
EntityQuery.use(delegator).from(entityName).where(whereFields).queryOne()
+ if (existing == null) {
+ throw new ServiceErrorException("No ${entityName} found matching
${whereFields}" as String)
+ }
+ existing.putAll(fields)
+ existing.store()
+ return existing
+ }
+
+}
diff --git
a/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/GroovyBaseScript.groovy
b/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/GroovyBaseScript.groovy
index 83ff751e47..4339b77515 100644
---
a/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/GroovyBaseScript.groovy
+++
b/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/GroovyBaseScript.groovy
@@ -20,16 +20,24 @@ package org.apache.ofbiz.service.engine
import org.apache.ofbiz.base.util.Debug
import org.apache.ofbiz.base.util.UtilProperties
+import org.apache.ofbiz.entity.Delegator
+import org.apache.ofbiz.entity.GenericEntityException
import org.apache.ofbiz.entity.GenericValue
import org.apache.ofbiz.entity.model.DynamicViewEntity
import org.apache.ofbiz.entity.util.EntityQuery
import org.apache.ofbiz.service.DispatchContext
import org.apache.ofbiz.service.ExecutionServiceException
+import org.apache.ofbiz.service.GenericServiceException
import org.apache.ofbiz.service.LocalDispatcher
import org.apache.ofbiz.service.ModelService
+import org.apache.ofbiz.service.ServiceErrorException
import org.apache.ofbiz.service.ServiceUtil
// codenarc-disable AbstractClassWithoutAbstractMethod
+// This class is the DSL surface itself (see the 2014 cwiki doc), so it is
expected to keep
+// accumulating small facade methods as new DSL verbs are added -
MethodCount's default threshold
+// does not fit that shape.
+// codenarc-disable MethodCount
abstract class GroovyBaseScript extends Script {
static final String MODULE = GroovyBaseScript.getName()
@@ -40,17 +48,7 @@ abstract class GroovyBaseScript extends Script {
Map runService(String serviceName, Map inputMap) throws
ExecutionServiceException {
LocalDispatcher dispatcher = binding.getVariable('dispatcher')
- DispatchContext dctx = dispatcher.getDispatchContext()
- inputMap.userLogin = inputMap.userLogin ?:
this.binding.hasVariable('userLogin')
- ? this.binding.getVariable('userLogin')
- : this.binding.getVariable('parameters').userLogin
- inputMap.timeZone = inputMap.timeZone ?:
this.binding.hasVariable('timeZone')
- ? this.binding.getVariable('timeZone')
- : this.binding.getVariable('parameters').timeZone
- inputMap.locale = inputMap.locale ?: this.binding.hasVariable('locale')
- ? this.binding.getVariable('locale')
- : this.binding.getVariable('parameters').locale
- Map serviceContext = dctx.makeValidContext(serviceName,
ModelService.IN_PARAM, inputMap)
+ Map serviceContext = buildServiceContext(dispatcher, serviceName,
inputMap)
Map result = dispatcher.runSync(serviceName, serviceContext)
if (ServiceUtil.isError(result)) {
throw new
ExecutionServiceException(ServiceUtil.getErrorMessage(result))
@@ -58,10 +56,20 @@ abstract class GroovyBaseScript extends Script {
return result
}
+ void runAsyncService(String serviceName, Map inputMap) throws
GenericServiceException {
+ LocalDispatcher dispatcher = binding.getVariable('dispatcher')
+ Map serviceContext = buildServiceContext(dispatcher, serviceName,
inputMap)
+ dispatcher.runAsync(serviceName, serviceContext, true) // persist =
true
+ }
+
Map run(Map args) throws ExecutionServiceException {
return runService((String)args.get('service'), (Map)args.get('with',
[:]))
}
+ void runAsync(Map args) throws GenericServiceException {
+ runAsyncService((String)args.get('service'), (Map)args.get('with',
[:]))
+ }
+
Map makeValue(String entityName) throws ExecutionServiceException {
return binding.getVariable('delegator').makeValue(entityName)
}
@@ -86,6 +94,19 @@ abstract class GroovyBaseScript extends Script {
return EntityQuery.use(binding.getVariable('delegator')).select(fields)
}
+ GenericValue create(String entityName, Map fields) throws
GenericEntityException {
+ Delegator delegator = binding.getVariable('delegator')
+ return delegator.makeValidValue(entityName, fields).create()
+ }
+
+ EntityUpdateBuilder update(String entityName) {
+ return new EntityUpdateBuilder(binding.getVariable('delegator'),
entityName)
+ }
+
+ EntityDeleteBuilder delete(String entityName) {
+ return new EntityDeleteBuilder(binding.getVariable('delegator'),
entityName)
+ }
+
@Deprecated
GenericValue findOne(String entityName, Map<String, ? extends Object>
fields, boolean useCache) {
return from(entityName).where(fields).cache(useCache).queryOne()
@@ -95,8 +116,13 @@ abstract class GroovyBaseScript extends Script {
def success(Map returnValues) {
return success(null, returnValues)
}
+ def success(String resource, String key, Map returnValues = [:]) {
+ return success(label(resource, key), returnValues)
+ }
+ def success(String resource, String key, Map context, Map returnValues) {
+ return success(label(resource, key, context), returnValues)
+ }
def success(String message = '', Map returnValues = [:]) {
- // TODO: implement some clever i18n mechanism based on the userLogin
and locale in the binding
if (this.binding.hasVariable('request')) {
// the script is invoked as an "event"
if (message) {
@@ -117,8 +143,10 @@ abstract class GroovyBaseScript extends Script {
return result
}
/* codenarc-enable */
+ Map failure(String resource, String key, Map returnValues = [:]) {
+ return failure(label(resource, key), returnValues)
+ }
Map failure(String message, Map returnValues = [:]) {
- // TODO: implement some clever i18n mechanism based on the userLogin
and locale in the binding
Map result = message
? ServiceUtil.returnFailure(message)
: ServiceUtil.returnFailure()
@@ -127,9 +155,14 @@ abstract class GroovyBaseScript extends Script {
}
return result
}
+ Map error(String resource, String key) {
+ return error(label(resource, key))
+ }
+ Map error(String resource, String key, Map context) {
+ return error(label(resource, key, context))
+ }
/* codenarc-disable NoDef, MethodReturnTypeRequired */
def error(String message) {
- // TODO: implement some clever i18n mechanism based on the userLogin
and locale in the binding
if (this.binding.hasVariable('request')) {
// the script is invoked as an "event"
if (message) {
@@ -174,4 +207,35 @@ abstract class GroovyBaseScript extends Script {
return UtilProperties.getMessage(ressource, message, locale)
}
+ void fail(String message) throws ServiceErrorException {
+ throw new ServiceErrorException(message)
+ }
+ void fail(String resource, String key, Map context = [:]) throws
ServiceErrorException {
+ fail(label(resource, key, context))
+ }
+ void require(boolean condition, String message) throws
ServiceErrorException {
+ if (!condition) {
+ fail(message)
+ }
+ }
+ void require(boolean condition, String resource, String key, Map context =
[:]) throws ServiceErrorException {
+ if (!condition) {
+ fail(resource, key, context)
+ }
+ }
+
+ private Map buildServiceContext(LocalDispatcher dispatcher, String
serviceName, Map inputMap) {
+ DispatchContext dctx = dispatcher.getDispatchContext()
+ inputMap.userLogin = inputMap.userLogin ?:
this.binding.hasVariable('userLogin')
+ ? this.binding.getVariable('userLogin')
+ : this.binding.getVariable('parameters').userLogin
+ inputMap.timeZone = inputMap.timeZone ?:
this.binding.hasVariable('timeZone')
+ ? this.binding.getVariable('timeZone')
+ : this.binding.getVariable('parameters').timeZone
+ inputMap.locale = inputMap.locale ?: this.binding.hasVariable('locale')
+ ? this.binding.getVariable('locale')
+ : this.binding.getVariable('parameters').locale
+ return dctx.makeValidContext(serviceName, ModelService.IN_PARAM,
inputMap)
+ }
+
}
diff --git
a/framework/service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy
b/framework/service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy
index 44fde00ea7..758cde9191 100644
---
a/framework/service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy
+++
b/framework/service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy
@@ -43,3 +43,81 @@ Map testPingSuccessWithDSLCall() {
Map testPingErrorWithDSLCall() {
run service: 'testGroovyPingError', with: parameters
}
+
+Map testPingSuccessWithAsyncDSLCall() {
+ runAsyncService('testGroovyPingSuccess', parameters)
+ return success()
+}
+
+Map testPingSuccessWithAsyncMapDSLCall() {
+ runAsync service: 'testGroovyPingSuccess', with: parameters
+ return success()
+}
+
+Map testPingSuccessWithI18n() {
+ return success('ServiceErrorUiLabels', 'ServiceValueNotFound')
+}
+
+Map testPingSuccessWithI18nContext() {
+ return success('ServiceErrorUiLabels', 'ServiceParameterValueNotValid',
+ [parameterName: parameters.parameterName, errorDetails:
parameters.errorDetails], [:])
+}
+
+Map testPingErrorWithI18n() {
+ return error('ServiceErrorUiLabels', 'ServiceValueNotFound')
+}
+
+Map testPingErrorWithI18nContext() {
+ return error('ServiceErrorUiLabels', 'ServiceParameterValueNotValid',
+ [parameterName: parameters.parameterName, errorDetails:
parameters.errorDetails])
+}
+
+Map testPingFailureWithI18n() {
+ return failure('ServiceErrorUiLabels', 'ServiceValueNotFound')
+}
+
+Map testFail() {
+ fail('Direct failure message')
+ return success()
+}
+
+Map testRequire() {
+ require(parameters.conditionMet, 'Required condition was not met')
+ return success()
+}
+
+Map testFailFromNestedClosure() {
+ // Proves the throw unwinds past the closure boundary, unlike `return
error(...)` which would
+ // only exit this closure and let execution fall through to the success()
below.
+ Closure innerCheck = {
+ fail('Nested failure message')
+ }
+ innerCheck()
+ return success()
+}
+
+Map testFailWithI18n() {
+ fail('ServiceErrorUiLabels', 'ServiceValueNotFound')
+ return success()
+}
+
+Map testRequireWithI18nContext() {
+ require(false, 'ServiceErrorUiLabels', 'ServiceParameterValueNotValid',
+ [parameterName: parameters.parameterName, errorDetails:
parameters.errorDetails])
+ return success()
+}
+
+Map testEntityDslCreate() {
+ create('Testing', [testingId: parameters.testingId, testingName:
parameters.testingName])
+ return success()
+}
+
+Map testEntityDslUpdate() {
+ update('Testing').where([testingId:
parameters.testingId]).set([testingName: parameters.testingName])
+ return success()
+}
+
+Map testEntityDslDelete() {
+ int rowsRemoved = delete('Testing').where([testingId:
parameters.testingId])
+ return success([rowsRemoved: rowsRemoved])
+}
diff --git
a/framework/service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy
b/framework/service/src/main/java/org/apache/ofbiz/service/ServiceErrorException.java
similarity index 54%
copy from
framework/service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy
copy to
framework/service/src/main/java/org/apache/ofbiz/service/ServiceErrorException.java
index 44fde00ea7..4d4cb7ab79 100644
---
a/framework/service/src/main/groovy/org/apache/ofbiz/service/test/TestServices.groovy
+++
b/framework/service/src/main/java/org/apache/ofbiz/service/ServiceErrorException.java
@@ -15,31 +15,20 @@
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
-*/
-package org.apache.ofbiz.service.test
+
*******************************************************************************/
+package org.apache.ofbiz.service;
-// Despite it's name, this class is not for test, it's only services
+/**
+ * Thrown by the Groovy DSL's {@code fail()}/{@code require()} control-flow
helpers. Extends
+ * {@link ExecutionServiceException} so it is recognized, with no engine
changes, by the same
+ * {@code instanceof ExecutionServiceException} check {@code GroovyEngine}
already uses to convert
+ * a nested {@code runService()} failure into a service error result, and is
caught generically by
+ * {@code GroovyEventHandler}'s existing catch-all exception handling for
events.
+ */
+@SuppressWarnings("serial")
+public class ServiceErrorException extends ExecutionServiceException {
-Map testPingSuccess() {
- Map returnMap = success('Service result success')
- if (parameters.ping) {
- returnMap.pong = parameters.ping
+ public ServiceErrorException(String str) {
+ super(str);
}
- return returnMap
-}
-
-Map testPingError() {
- Map returnMap = error('Service result error')
- if (parameters.ping) {
- returnMap.pong = parameters.ping
- }
- return returnMap
-}
-
-Map testPingSuccessWithDSLCall() {
- run service: 'testGroovyPingSuccess', with: parameters
-}
-
-Map testPingErrorWithDSLCall() {
- run service: 'testGroovyPingError', with: parameters
}
diff --git
a/framework/service/src/test/java/org/apache/ofbiz/service/test/GroovyDslServiceEngineTests.java
b/framework/service/src/test/java/org/apache/ofbiz/service/test/GroovyDslServiceEngineTests.java
index 8b888b19c0..56e991f563 100644
---
a/framework/service/src/test/java/org/apache/ofbiz/service/test/GroovyDslServiceEngineTests.java
+++
b/framework/service/src/test/java/org/apache/ofbiz/service/test/GroovyDslServiceEngineTests.java
@@ -19,11 +19,21 @@
package org.apache.ofbiz.service.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.sql.Timestamp;
+import java.util.Locale;
import java.util.Map;
+import java.util.UUID;
+import org.apache.ofbiz.base.util.UtilDateTime;
import org.apache.ofbiz.base.util.UtilMisc;
+import org.apache.ofbiz.base.util.UtilProperties;
+import org.apache.ofbiz.entity.GenericValue;
+import org.apache.ofbiz.entity.condition.EntityCondition;
+import org.apache.ofbiz.entity.condition.EntityOperator;
import org.apache.ofbiz.service.ModelService;
import org.apache.ofbiz.service.ServiceUtil;
import org.apache.ofbiz.testtools.JunitJupiterTest;
@@ -61,4 +71,230 @@ public class GroovyDslServiceEngineTests implements
JupiterTestHelper {
assertTrue(ServiceUtil.isError(result));
assertEquals("Service result error",
result.get(ModelService.ERROR_MESSAGE));
}
+
+ @Test
+ public final void testGroovyServiceAsyncDSLCall() throws Exception {
+ String pingMsg = "Unit Test Async";
+ Map<String, Object> pingMap = UtilMisc.toMap("ping", pingMsg);
+
+ // ofbiz --test does not wipe JobSandbox between runs, and JobPoller
never picks up a
+ // pending job during a test run (see below) - so a prior run's
leftover SERVICE_PENDING
+ // row for this same service would otherwise satisfy every assertion
below even if
+ // runAsyncService() were broken. Scoping to runTime >= beforeCall
makes the test hermetic.
+ Timestamp beforeCall = UtilDateTime.nowTimestamp();
+ Map<String, Object> result =
getDispatcher().runSync("testGroovyPingSuccessWithAsyncDSLCall", pingMap);
+ assertTrue(ServiceUtil.isSuccess(result));
+
+ // Verifying actual pickup/completion by the Job Scheduler is not
possible from inside an
+ // "ofbiz --test" run: JobPoller's loop is gated on
Start.getInstance().getCurrentState()
+ // == RUNNING, but StartupControlPanel.loadContainers() only flips the
server to RUNNING
+ // after ContainerLoader.load() returns - and that method runs
TestRunContainer (which runs
+ // every test suite synchronously on that same thread) before
returning. So the server never
+ // reaches RUNNING until after this entire test suite has already
finished, and JobPoller
+ // never gets to poll during the run. Instead, this asserts that
runAsyncService() itself
+ // persisted a well-formed job request - the only thing observable
from within the test -
+ // matching the existing precedent in ServicePurgeTest.groovy and
+ // ServiceMultipleNodeRecoveryTest.groovy, neither of which waits for
live completion either.
+ GenericValue jobSandbox = from("JobSandbox")
+ .where(EntityCondition.makeCondition("serviceName",
"testGroovyPingSuccess"),
+ EntityCondition.makeCondition("runTime",
EntityOperator.GREATER_THAN_EQUAL_TO, beforeCall))
+ .orderBy("-runTime")
+ .queryFirst();
+ assertNotNull(jobSandbox, "Expected runAsyncService() to persist a
JobSandbox record for testGroovyPingSuccess");
+ assertEquals("SERVICE_PENDING", jobSandbox.getString("statusId"));
+ assertNotNull(jobSandbox.getString("runtimeDataId"),
+ "Expected the async call's context to be persisted as
RuntimeData");
+ }
+
+ @Test
+ public final void testGroovyServiceAsyncMapDSLCall() throws Exception {
+ // Same as testGroovyServiceAsyncDSLCall above, but exercises the
`runAsync service: ...,
+ // with: ...` map-argument calling form instead of the positional
runAsyncService(name, map)
+ // form - see runAsync(Map) in GroovyBaseScript.
+ String pingMsg = "Unit Test Async Map";
+ Map<String, Object> pingMap = UtilMisc.toMap("ping", pingMsg);
+
+ Timestamp beforeCall = UtilDateTime.nowTimestamp();
+ Map<String, Object> result =
getDispatcher().runSync("testGroovyPingSuccessWithAsyncMapDSLCall", pingMap);
+ assertTrue(ServiceUtil.isSuccess(result));
+
+ GenericValue jobSandbox = from("JobSandbox")
+ .where(EntityCondition.makeCondition("serviceName",
"testGroovyPingSuccess"),
+ EntityCondition.makeCondition("runTime",
EntityOperator.GREATER_THAN_EQUAL_TO, beforeCall))
+ .orderBy("-runTime")
+ .queryFirst();
+ assertNotNull(jobSandbox, "Expected runAsync service:/with: to persist
a JobSandbox record for testGroovyPingSuccess");
+ assertEquals("SERVICE_PENDING", jobSandbox.getString("statusId"));
+ assertNotNull(jobSandbox.getString("runtimeDataId"),
+ "Expected the async call's context to be persisted as
RuntimeData");
+ }
+
+ @Test
+ public final void testGroovyPingSuccessWithI18n() throws Exception {
+ // Two different locales for the same resource/key prove the message
is actually being
+ // resolved through UtilProperties, not just echoed back as a literal
string.
+ Map<String, Object> resultEn =
getDispatcher().runSync("testGroovyPingSuccessWithI18n",
+ UtilMisc.toMap("locale", Locale.ENGLISH));
+ assertTrue(ServiceUtil.isSuccess(resultEn));
+ assertEquals(UtilProperties.getMessage("ServiceErrorUiLabels",
"ServiceValueNotFound", Locale.ENGLISH),
+ resultEn.get(ModelService.SUCCESS_MESSAGE));
+
+ Map<String, Object> resultFr =
getDispatcher().runSync("testGroovyPingSuccessWithI18n",
+ UtilMisc.toMap("locale", Locale.FRENCH));
+ assertTrue(ServiceUtil.isSuccess(resultFr));
+ assertEquals(UtilProperties.getMessage("ServiceErrorUiLabels",
"ServiceValueNotFound", Locale.FRENCH),
+ resultFr.get(ModelService.SUCCESS_MESSAGE));
+ }
+
+ @Test
+ public final void testGroovyPingSuccessWithI18nContext() throws Exception {
+ Map<String, Object> input = UtilMisc.toMap("locale", Locale.ENGLISH,
+ "parameterName", "productId", "errorDetails", "must not be
empty");
+
+ Map<String, Object> result =
getDispatcher().runSync("testGroovyPingSuccessWithI18nContext", input);
+
+ assertTrue(ServiceUtil.isSuccess(result));
+ Map<String, Object> expectedContext = UtilMisc.toMap("parameterName",
"productId", "errorDetails", "must not be empty");
+ assertEquals(UtilProperties.getMessage("ServiceErrorUiLabels",
"ServiceParameterValueNotValid", expectedContext, Locale.ENGLISH),
+ result.get(ModelService.SUCCESS_MESSAGE));
+ }
+
+ @Test
+ public final void testGroovyPingErrorWithI18n() throws Exception {
+ Map<String, Object> result =
getDispatcher().runSync("testGroovyPingErrorWithI18n",
+ UtilMisc.toMap("locale", Locale.FRENCH));
+ assertTrue(ServiceUtil.isError(result));
+ assertEquals(UtilProperties.getMessage("ServiceErrorUiLabels",
"ServiceValueNotFound", Locale.FRENCH),
+ ServiceUtil.getErrorMessage(result));
+ }
+
+ @Test
+ public final void testGroovyPingErrorWithI18nContext() throws Exception {
+ Map<String, Object> input = UtilMisc.toMap("locale", Locale.ENGLISH,
+ "parameterName", "quantity", "errorDetails", "must be a
positive number");
+
+ Map<String, Object> result =
getDispatcher().runSync("testGroovyPingErrorWithI18nContext", input);
+
+ assertTrue(ServiceUtil.isError(result));
+ Map<String, Object> expectedContext = UtilMisc.toMap("parameterName",
"quantity", "errorDetails", "must be a positive number");
+ assertEquals(UtilProperties.getMessage("ServiceErrorUiLabels",
"ServiceParameterValueNotValid", expectedContext, Locale.ENGLISH),
+ ServiceUtil.getErrorMessage(result));
+ }
+
+ @Test
+ public final void testGroovyPingFailureWithI18n() throws Exception {
+ Map<String, Object> result =
getDispatcher().runSync("testGroovyPingFailureWithI18n",
+ UtilMisc.toMap("locale", Locale.ENGLISH));
+ assertTrue(ServiceUtil.isFailure(result));
+ assertEquals(UtilProperties.getMessage("ServiceErrorUiLabels",
"ServiceValueNotFound", Locale.ENGLISH),
+ ServiceUtil.getErrorMessage(result));
+ }
+
+ @Test
+ public final void testGroovyFail() throws Exception {
+ Map<String, Object> result = getDispatcher().runSync("testGroovyFail",
UtilMisc.toMap());
+ assertTrue(ServiceUtil.isError(result));
+ assertEquals("Direct failure message",
ServiceUtil.getErrorMessage(result));
+ }
+
+ @Test
+ public final void testGroovyRequireConditionFalse() throws Exception {
+ Map<String, Object> result =
getDispatcher().runSync("testGroovyRequire", UtilMisc.toMap("conditionMet",
false));
+ assertTrue(ServiceUtil.isError(result));
+ assertEquals("Required condition was not met",
ServiceUtil.getErrorMessage(result));
+ }
+
+ @Test
+ public final void testGroovyRequireConditionTrue() throws Exception {
+ Map<String, Object> result =
getDispatcher().runSync("testGroovyRequire", UtilMisc.toMap("conditionMet",
true));
+ assertTrue(ServiceUtil.isSuccess(result));
+ }
+
+ @Test
+ public final void testGroovyFailFromNestedClosure() throws Exception {
+ // The whole point of throwing rather than returning: a plain `return
error(...)` inside
+ // innerCheck() would only exit that closure and fall through to
success() in the test
+ // service - this proves the throw actually unwinds past the closure
boundary instead.
+ Map<String, Object> result =
getDispatcher().runSync("testGroovyFailFromNestedClosure", UtilMisc.toMap());
+ assertTrue(ServiceUtil.isError(result));
+ assertEquals("Nested failure message",
ServiceUtil.getErrorMessage(result));
+ }
+
+ @Test
+ public final void testGroovyFailWithI18n() throws Exception {
+ Map<String, Object> result =
getDispatcher().runSync("testGroovyFailWithI18n",
+ UtilMisc.toMap("locale", Locale.FRENCH));
+ assertTrue(ServiceUtil.isError(result));
+ assertEquals(UtilProperties.getMessage("ServiceErrorUiLabels",
"ServiceValueNotFound", Locale.FRENCH),
+ ServiceUtil.getErrorMessage(result));
+ }
+
+ @Test
+ public final void testGroovyRequireWithI18nContext() throws Exception {
+ Map<String, Object> input = UtilMisc.toMap("locale", Locale.ENGLISH,
+ "parameterName", "quantity", "errorDetails", "must be a
positive number");
+
+ Map<String, Object> result =
getDispatcher().runSync("testGroovyRequireWithI18nContext", input);
+
+ assertTrue(ServiceUtil.isError(result));
+ Map<String, Object> expectedContext = UtilMisc.toMap("parameterName",
"quantity", "errorDetails", "must be a positive number");
+ assertEquals(UtilProperties.getMessage("ServiceErrorUiLabels",
"ServiceParameterValueNotValid", expectedContext, Locale.ENGLISH),
+ ServiceUtil.getErrorMessage(result));
+ }
+
+ // testingId is a VARCHAR(20) pk, so test ids need to stay short - an
8-char UUID fragment
+ // per prefix is plenty of uniqueness for a single test run.
+ private String shortTestingId(String prefix) {
+ return prefix + UUID.randomUUID().toString().substring(0, 8);
+ }
+
+ @Test
+ public final void testGroovyEntityDslCreate() throws Exception {
+ String testingId = shortTestingId("gdCr-");
+ Map<String, Object> input = UtilMisc.toMap("testingId", testingId,
"testingName", "Created via DSL");
+
+ Map<String, Object> result =
getDispatcher().runSync("testGroovyEntityDslCreate", input);
+ assertTrue(ServiceUtil.isSuccess(result));
+
+ GenericValue created = from("Testing").where("testingId",
testingId).queryOne();
+ assertNotNull(created, "Expected create() to persist a new Testing
record");
+ assertEquals("Created via DSL", created.getString("testingName"));
+ }
+
+ @Test
+ public final void testGroovyEntityDslUpdate() throws Exception {
+ String testingId = shortTestingId("gdUp-");
+ getDelegator().create("Testing", UtilMisc.toMap("testingId",
testingId, "testingName", "Before"));
+
+ Map<String, Object> result =
getDispatcher().runSync("testGroovyEntityDslUpdate",
+ UtilMisc.toMap("testingId", testingId, "testingName",
"After"));
+ assertTrue(ServiceUtil.isSuccess(result));
+
+ GenericValue updated = from("Testing").where("testingId",
testingId).queryOne();
+ assertEquals("After", updated.getString("testingName"));
+ }
+
+ @Test
+ public final void testGroovyEntityDslUpdateNotFound() throws Exception {
+ // requireNewTransaction=true so the thrown-and-caught
ExecutionServiceException's
+ // rollback-only marking does not poison this test method's own
transaction, matching
+ // the existing testGroovyPingErrorWithDSLCall precedent above.
+ Map<String, Object> result =
getDispatcher().runSync("testGroovyEntityDslUpdate",
+ UtilMisc.toMap("testingId", shortTestingId("gdMiss-"),
"testingName", "Irrelevant"), 60, true);
+ assertTrue(ServiceUtil.isError(result));
+ }
+
+ @Test
+ public final void testGroovyEntityDslDelete() throws Exception {
+ String testingId = shortTestingId("gdDel-");
+ getDelegator().create("Testing", UtilMisc.toMap("testingId",
testingId, "testingName", "To be removed"));
+
+ Map<String, Object> result =
getDispatcher().runSync("testGroovyEntityDslDelete",
+ UtilMisc.toMap("testingId", testingId));
+ assertTrue(ServiceUtil.isSuccess(result));
+ assertEquals(1, result.get("rowsRemoved"));
+
+ GenericValue afterDelete = from("Testing").where("testingId",
testingId).queryOne();
+ assertNull(afterDelete, "Expected delete() to remove the Testing
record");
+ }
}