You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ofbiz.apache.org by lf...@apache.org on 2026/08/03 13:21:09 UTC
(ofbiz-framework) 01/02: Improved: Added support for DomainModelObject in rest-api (OFBIZ-12517)
This is an automated email from the ASF dual-hosted git repository.
Lukas-Finster pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
commit 93f0b9711956417d08677409898f378ee35764eb
Author: Lukas Finster <lu...@ecomify.de>
AuthorDate: Mon Jul 27 11:19:51 2026 +0200
Improved: Added support for DomainModelObject in rest-api (OFBIZ-12517)
* Fascilitating transformation of rawData to full java-objects
---
.../base/conversion/AbstractModelConverter.java | 43 ++++
.../ofbiz/base/conversion/ModelConverter.java | 45 ++++
.../ofbiz/base/conversion/ModelConverters.java | 102 +++++++++
.../org/apache/ofbiz/base/model/DomainModel.java | 23 ++
.../org/apache/ofbiz/base/util/ObjectType.java | 10 +
.../base/conversion/TestDomainModelConversion.java | 234 +++++++++++++++++++++
...rg.apache.ofbiz.base.conversion.ConverterLoader | 1 +
framework/rest-api/dtd/rest-api.xsd | 7 +
.../org/apache/ofbiz/ws/rs/model/ModelApi.java | 27 ++-
.../apache/ofbiz/ws/rs/model/ModelApiReader.java | 9 +
.../org/apache/ofbiz/ws/rs/model/ModelMapping.java | 71 +++++++
.../apache/ofbiz/ws/rs/model/ModelQueryParam.java | 80 +++++++
.../ofbiz/ws/rs/openapi/OFBizOpenApiReader.java | 14 +-
.../org/apache/ofbiz/ws/rs/util/OpenApiUtil.java | 92 +++++++-
.../apache/ofbiz/ws/rs/util/OpenApiUtilTest.java | 75 +++++++
15 files changed, 826 insertions(+), 7 deletions(-)
diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/conversion/AbstractModelConverter.java b/framework/base/src/main/java/org/apache/ofbiz/base/conversion/AbstractModelConverter.java
new file mode 100644
index 0000000000..8feb151b23
--- /dev/null
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/conversion/AbstractModelConverter.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * 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.base.conversion;
+
+
+/** Abstract ModelConverter class. This class handles converter registration
+ * and it implements the <code>canConvert</code>, <code>getSourceClass</code>,
+ * and <code>getTargetClass</code> methods.
+ */
+public abstract class AbstractModelConverter<S, T> extends AbstractConverter<S, T> implements ModelConverter<S, T> {
+
+ protected AbstractModelConverter(Class<? super S> sourceClass, Class<? super T> targetClass) {
+ super(sourceClass, targetClass);
+ }
+
+ /**
+ * Converts <code>obj</code> to <code>T</code>.
+ *
+ * @param targetClass
+ * @param obj
+ * @param targetClassName
+ * @return The converted <code>Object</code>
+ */
+ public T convert(Class<? extends T> targetClass, S obj, String targetClassName) throws ConversionException {
+ return convert(obj, targetClassName);
+ }
+}
diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/conversion/ModelConverter.java b/framework/base/src/main/java/org/apache/ofbiz/base/conversion/ModelConverter.java
new file mode 100644
index 0000000000..8eb68750ca
--- /dev/null
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/conversion/ModelConverter.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * 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.base.conversion;
+
+/** Model converter interface. Classes implement this interface
+ * to convert one object type to another.
+ */
+public interface ModelConverter<S, T> extends Converter<S, T> {
+
+ /** Converts <code>obj</code> to <code>T</code>.
+ *
+ * @param obj The source <code>Object</code> to convert
+ * @param targetClassName The target class name for conversion - must not be <code>null</code>
+ * @return The converted <code>Object</code>
+ * @throws ConversionException
+ */
+ T convert(S obj, String targetClassName) throws ConversionException;
+
+ /** Converts <code>obj</code> to <code>T</code>.
+ *
+ * @param targetClass The <code>Class</code> to convert to
+ * @param obj The source <code>Object</code> to convert
+ * @param targetClassName The target class name for conversion - must not be <code>null</code>
+ * @return The converted <code>Object</code>
+ * @throws ConversionException
+ */
+ T convert(Class<? extends T> targetClass, S obj, String targetClassName) throws ConversionException;
+
+}
diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/conversion/ModelConverters.java b/framework/base/src/main/java/org/apache/ofbiz/base/conversion/ModelConverters.java
new file mode 100644
index 0000000000..dc5a302b79
--- /dev/null
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/conversion/ModelConverters.java
@@ -0,0 +1,102 @@
+/*******************************************************************************
+ * 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.base.conversion;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.ofbiz.base.lang.JSON;
+import org.apache.ofbiz.base.model.DomainModel;
+import org.apache.ofbiz.base.util.GeneralException;
+import org.apache.ofbiz.base.util.ObjectType;
+import org.apache.ofbiz.base.util.UtilGenerics;
+import org.apache.ofbiz.base.util.UtilValidate;
+
+/** Model Converter classes. */
+public class ModelConverters implements ConverterLoader {
+
+ public static class HashMapToDomainModel extends AbstractModelConverter<LinkedHashMap<String, Object>, DomainModel> {
+
+ public HashMapToDomainModel() {
+ super(LinkedHashMap.class, DomainModel.class);
+ }
+
+ @Override
+ public DomainModel convert(LinkedHashMap<String, Object> obj, String targetClassName) throws ConversionException {
+ try {
+ Class<?> modelClass = Class.forName(targetClassName);
+ JSON jsonObj = JSON.from(obj);
+ DomainModel target = UtilGenerics.cast(jsonObj.toObject(modelClass));
+ return target;
+ } catch (IOException | ClassNotFoundException e) {
+ throw new ConversionException(e);
+ }
+ }
+
+ @Override
+ public DomainModel convert(LinkedHashMap<String, Object> obj) throws ConversionException {
+ // cant convert without target class, so abort
+ throw new ConversionException("Need target class to convert from HashMap to DomainModel");
+ }
+ }
+
+ /**
+ * Load this class <code>ModelConverters</code>.
+ */
+ public void loadConverters() {
+ Converters.loadContainedConverters(ModelConverters.class);
+ }
+
+ /**
+ * Method to convert serviceParameter List values to specific DomainModel
+ * @param context the service context map containing the list to convert
+ * @param listName the key under which the raw List is stored in the context
+ * @param domainModelClass the target DomainModel class each element should be converted to
+ * @return a List of converted DomainModel objects; an empty List if the raw list is missing or empty
+ * @throws GeneralException
+ */
+ public static List<DomainModel> convertRawListToDomainModelType(Map<String, Object> context, String listName, Class<?> domainModelClass)
+ throws GeneralException {
+ List<Object> dataList = UtilGenerics.checkCollection(context.get(listName), Object.class);
+
+ return convertRawListToDomainModelType(dataList, domainModelClass);
+ }
+
+ /**
+ * Converts objects in list to specified DomainModel
+ * @param dataList the List of raw objects to convert
+ * @param domainModelClass the target DomainModel class each element should be converted to
+ * @return a List of converted DomainModel objects; an empty List if {@code dataList} is empty
+ * @throws GeneralException
+ */
+ public static List<DomainModel> convertRawListToDomainModelType(List<Object> dataList, Class<?> domainModelClass) throws GeneralException {
+ List<DomainModel> domainModelList = new ArrayList<>();
+ if (UtilValidate.isEmpty(dataList)) {
+ return domainModelList;
+ }
+ for (Object rawData : dataList) {
+ DomainModel domainObject = (DomainModel) ObjectType.simpleTypeOrObjectConvert(rawData, domainModelClass.getName(), null, null);
+ domainModelList.add(domainObject);
+ }
+ return domainModelList;
+ }
+}
diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/model/DomainModel.java b/framework/base/src/main/java/org/apache/ofbiz/base/model/DomainModel.java
new file mode 100644
index 0000000000..98ade52d78
--- /dev/null
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/model/DomainModel.java
@@ -0,0 +1,23 @@
+/*******************************************************************************
+ * 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.base.model;
+
+public abstract class DomainModel {
+
+}
diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/util/ObjectType.java b/framework/base/src/main/java/org/apache/ofbiz/base/util/ObjectType.java
index 01641ce9a4..a63e198f69 100644
--- a/framework/base/src/main/java/org/apache/ofbiz/base/util/ObjectType.java
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/util/ObjectType.java
@@ -31,6 +31,7 @@ import org.apache.ofbiz.base.conversion.ConversionException;
import org.apache.ofbiz.base.conversion.Converter;
import org.apache.ofbiz.base.conversion.Converters;
import org.apache.ofbiz.base.conversion.LocalizedConverter;
+import org.apache.ofbiz.base.conversion.ModelConverter;
import org.apache.ofbiz.base.lang.IsEmpty;
import org.apache.ofbiz.base.lang.SourceMonitored;
import org.w3c.dom.Node;
@@ -349,6 +350,15 @@ public class ObjectType {
Debug.logWarning(e, "Exception thrown while converting type: ", MODULE);
throw new GeneralException(e.getMessage(), e);
}
+ } else if (converter instanceof ModelConverter) {
+ @SuppressWarnings("rawtypes")
+ ModelConverter<Object, Object> modelConverter = (ModelConverter) converter;
+ try {
+ return modelConverter.convert(obj, targetClass.getName());
+ } catch (ConversionException e) {
+ Debug.logWarning(e, "Exception thrown while converting type: ", MODULE);
+ throw new GeneralException(e.getMessage(), e);
+ }
}
try {
diff --git a/framework/base/src/test/java/org/apache/ofbiz/base/conversion/TestDomainModelConversion.java b/framework/base/src/test/java/org/apache/ofbiz/base/conversion/TestDomainModelConversion.java
new file mode 100644
index 0000000000..a6bcd4c319
--- /dev/null
+++ b/framework/base/src/test/java/org/apache/ofbiz/base/conversion/TestDomainModelConversion.java
@@ -0,0 +1,234 @@
+/*******************************************************************************
+ * 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.base.conversion;
+
+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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.ofbiz.base.conversion.ModelConverters.HashMapToDomainModel;
+import org.apache.ofbiz.base.model.DomainModel;
+import org.apache.ofbiz.base.util.GeneralException;
+import org.junit.jupiter.api.Test;
+
+class TestDomainModelConversion {
+
+
+ public static class TestPerson extends DomainModel {
+ private String name;
+ private Integer age;
+ private List<TestAddress> addresses;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Integer getAge() {
+ return age;
+ }
+
+ public void setAge(Integer age) {
+ this.age = age;
+ }
+
+ public List<TestAddress> getAddresses() {
+ return addresses;
+ }
+
+ public void setAddresses(List<TestAddress> addresses) {
+ this.addresses = addresses;
+ }
+ }
+
+ public static class TestAddress extends DomainModel {
+ private String city;
+ private String zip;
+
+ public String getCity() {
+ return city;
+ }
+
+ public void setCity(String city) {
+ this.city = city;
+ }
+
+ public String getZip() {
+ return zip;
+ }
+
+ public void setZip(String zip) {
+ this.zip = zip;
+ }
+ }
+
+ @Test
+ void convertsFlatMapToDomainModel() throws Exception {
+ LinkedHashMap<String, Object> map = new LinkedHashMap<>();
+ map.put("name", "some name");
+ map.put("age", 33);
+
+ HashMapToDomainModel converter = new HashMapToDomainModel();
+ DomainModel result = converter.convert(map, TestPerson.class.getName());
+
+ assertTrue(result instanceof TestPerson);
+ TestPerson person = (TestPerson) result;
+ assertEquals("some name", person.getName());
+ assertEquals(33, person.getAge());
+ }
+
+ @Test
+ void convertsNestedListOfDomainModels() throws Exception {
+ LinkedHashMap<String, Object> address = new LinkedHashMap<>();
+ address.put("city", "Berlin");
+ address.put("zip", "10115");
+
+ LinkedHashMap<String, Object> map = new LinkedHashMap<>();
+ map.put("name", "some name");
+ map.put("addresses", List.of(address));
+
+ HashMapToDomainModel converter = new HashMapToDomainModel();
+ TestPerson person = (TestPerson) converter.convert(map, TestPerson.class.getName());
+
+ assertNotNull(person.getAddresses());
+ assertEquals(1, person.getAddresses().size());
+ assertEquals("Berlin", person.getAddresses().get(0).getCity());
+ }
+
+ @Test
+ void throwsConversionExceptionForUnknownTargetClass() {
+ HashMapToDomainModel converter = new HashMapToDomainModel();
+ assertThrows(ConversionException.class, () -> converter.convert(new LinkedHashMap<>(), "non.existing.Class"));
+ }
+
+ @Test
+ void throwsConversionExceptionForMissingTargetClass() {
+ HashMapToDomainModel converter = new HashMapToDomainModel();
+ assertThrows(ConversionException.class, () -> converter.convert(new LinkedHashMap<>()));
+ }
+
+ @Test
+ void throwsConversionExceptionOnUnknownFields() throws Exception {
+ LinkedHashMap<String, Object> map = new LinkedHashMap<>();
+ map.put("name", "some name");
+ map.put("age", 42);
+ map.put("unknownField", "unknownValue");
+
+ HashMapToDomainModel converter = new HashMapToDomainModel();
+ assertThrows(ConversionException.class, () -> converter.convert(map, TestPerson.class.getName()));
+ }
+
+ @Test
+ void nullValuesInMapMapToNullFields() throws Exception {
+ LinkedHashMap<String, Object> map = new LinkedHashMap<>();
+ map.put("name", null);
+ map.put("age", 30);
+
+ HashMapToDomainModel converter = new HashMapToDomainModel();
+ TestPerson person = (TestPerson) converter.convert(map, TestPerson.class.getName());
+
+ assertNull(person.getName());
+ assertEquals(30, person.getAge());
+ }
+
+ // tests for convertRawKustToDomainModel
+
+ private LinkedHashMap<String, Object> personMap(String name, int age) {
+ LinkedHashMap<String, Object> map = new LinkedHashMap<>();
+ map.put("name", name);
+ map.put("age", age);
+ return map;
+ }
+
+ @Test
+ void convertsListOfMapsToListOfDomainModels() throws Exception {
+ List<Object> raw = List.of(personMap("Some Name", 42), personMap("Other Name", 30));
+
+ List<DomainModel> result = ModelConverters.convertRawListToDomainModelType(raw, TestPerson.class);
+
+ assertEquals(2, result.size());
+ assertEquals("Some Name", ((TestPerson) result.get(0)).getName());
+ assertEquals("Other Name", ((TestPerson) result.get(1)).getName());
+ }
+
+ @Test
+ void returnsEmptyListForNullList() throws Exception {
+ List<DomainModel> result = ModelConverters.convertRawListToDomainModelType((List<Object>) null, TestPerson.class);
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void returnsEmptyListForEmptyList() throws Exception {
+ List<DomainModel> result = ModelConverters.convertRawListToDomainModelType(List.of(), TestPerson.class);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void throwsGeneralExceptionWhenElementIsNotConvertible() {
+ List<Object> raw = List.of("not a map");
+
+ assertThrows(GeneralException.class, () -> ModelConverters.convertRawListToDomainModelType(raw, TestPerson.class));
+ }
+
+ @Test
+ void contextOverloadExtractsNamedListAndConverts() throws Exception {
+ Map<String, Object> context = Map.of("people", List.of(personMap("Some Name", 42)));
+
+ List<DomainModel> result = ModelConverters.convertRawListToDomainModelType(context, "people", TestPerson.class);
+
+ assertEquals(1, result.size());
+ assertEquals("Some Name", ((TestPerson) result.get(0)).getName());
+ }
+
+ @Test
+ void contextOverloadReturnsEmptyListWhenKeyMissing() throws Exception {
+ Map<String, Object> context = Map.of("otherKey", "irrelevant");
+
+ List<DomainModel> result = ModelConverters.convertRawListToDomainModelType(context, "people", TestPerson.class);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void contextOverloadReturnsEmptyListWhenValueForKeyIsNull() throws Exception {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("people", null);
+
+ List<DomainModel> result = ModelConverters.convertRawListToDomainModelType(context, "people", TestPerson.class);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void contextOverloadThrowsIfNamedValueIsNotAList() {
+ Map<String, Object> context = Map.of("people", "not a list at all");
+
+ assertThrows(ClassCastException.class, () -> ModelConverters.convertRawListToDomainModelType(context, "people", TestPerson.class));
+ }
+}
diff --git a/framework/entity/src/main/resources/META-INF/services/org.apache.ofbiz.base.conversion.ConverterLoader b/framework/entity/src/main/resources/META-INF/services/org.apache.ofbiz.base.conversion.ConverterLoader
index 363ccbeb29..1c367b9812 100644
--- a/framework/entity/src/main/resources/META-INF/services/org.apache.ofbiz.base.conversion.ConverterLoader
+++ b/framework/entity/src/main/resources/META-INF/services/org.apache.ofbiz.base.conversion.ConverterLoader
@@ -23,3 +23,4 @@ org.apache.ofbiz.base.conversion.JSONConverters
org.apache.ofbiz.base.conversion.MiscConverters
org.apache.ofbiz.base.conversion.NetConverters
org.apache.ofbiz.base.conversion.NumberConverters
+org.apache.ofbiz.base.conversion.ModelConverters
diff --git a/framework/rest-api/dtd/rest-api.xsd b/framework/rest-api/dtd/rest-api.xsd
index 43273abb34..44c28948a3 100644
--- a/framework/rest-api/dtd/rest-api.xsd
+++ b/framework/rest-api/dtd/rest-api.xsd
@@ -23,6 +23,7 @@ under the License.
<xs:complexType>
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="resource"/>
+ <xs:element minOccurs="0" maxOccurs="unbounded" ref="mapping"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="path" type="xs:string" use="required"/>
@@ -87,4 +88,10 @@ under the License.
<xs:attribute name="name" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
+ <xs:element name="mapping">
+ <xs:complexType>
+ <xs:attribute name="name" type="xs:string" />
+ <xs:attribute name="className" type="xs:string" />
+ </xs:complexType>
+ </xs:element>
</xs:schema>
diff --git a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApi.java b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApi.java
index 985191a8c3..b3341c26be 100644
--- a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApi.java
+++ b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApi.java
@@ -24,6 +24,7 @@ import java.util.List;
public class ModelApi {
private List<ModelResource> resources;
+ private List<ModelMapping> mappings;
private String name;
private String path;
private String displayName;
@@ -38,7 +39,7 @@ public class ModelApi {
*/
public List<ModelResource> getResources() {
if (resources == null) {
- resources = new ArrayList<ModelResource>();
+ resources = new ArrayList<>();
}
return this.resources;
}
@@ -58,6 +59,30 @@ public class ModelApi {
return this;
}
+ /**
+ * Returns the mappings
+ *
+ * @return mappings List
+ */
+ public List<ModelMapping> getMappings() {
+ if (mappings == null) {
+ mappings = new ArrayList<>();
+ }
+ return this.mappings;
+ }
+
+ /**
+ * Adds a mapping List
+ *
+ * @param mapping the {@link ModelMapping} to add
+ */
+ public ModelApi addMapping(ModelMapping mapping) {
+ if (this.mappings == null) {
+ this.mappings = new ArrayList<>();
+ }
+ this.mappings.add(mapping);
+ return this;
+ }
/**
* Returns the name.
*
diff --git a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
index e6110c8cf1..e8987b4962 100644
--- a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
+++ b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
@@ -65,9 +65,18 @@ public final class ModelApiReader {
for (Element resourceEle : UtilXml.childElementList(docElement, "resource")) {
createModelResource(resourceEle, api);
}
+ for (Element mappingEle : UtilXml.childElementList(docElement, "mapping")) {
+ createModelMapping(mappingEle, api);
+ }
return api;
}
+ private static void createModelMapping(Element mappingEle, ModelApi modelApi) {
+ ModelMapping mapping = new ModelMapping().name(UtilXml.checkEmpty(mappingEle.getAttribute("name")).intern()).className(UtilXml.checkEmpty(
+ mappingEle.getAttribute("className")).intern());
+ modelApi.addMapping(mapping);
+ }
+
private static void createModelResource(Element resourceEle, ModelApi api) {
ModelResource resource = buildResource(resourceEle);
createOperations(resourceEle, resource);
diff --git a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelMapping.java b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelMapping.java
new file mode 100644
index 0000000000..782409cb76
--- /dev/null
+++ b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelMapping.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * 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.ws.rs.model;
+
+public class ModelMapping {
+
+ private String name;
+ private String className;
+
+ /**
+ * @return the name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @param name
+ * @return
+ */
+ public ModelMapping name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * @param name the name to set
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * @return the className
+ */
+ public String getClassName() {
+ return className;
+ }
+
+ /**
+ * @param className
+ * @return
+ */
+ public ModelMapping className(String className) {
+ this.className = className;
+ return this;
+ }
+
+ /**
+ * @param className the className to set
+ */
+ public void setClassName(String className) {
+ this.className = className;
+ }
+}
diff --git a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelQueryParam.java b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelQueryParam.java
new file mode 100644
index 0000000000..9ae8d8be49
--- /dev/null
+++ b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelQueryParam.java
@@ -0,0 +1,80 @@
+package org.apache.ofbiz.ws.rs.model;
+
+
+public class ModelQueryParam {
+
+ private String name;
+ private String description;
+ private String type;
+
+ /**
+ * @return the name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @param name the name to set
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * @param name
+ * @return
+ */
+ public ModelQueryParam name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * @return the description
+ */
+ public String getDescription() {
+ return description;
+ }
+
+
+ /**
+ * @param description the description to set
+ */
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ /**
+ * @param description
+ * @return
+ */
+ public ModelQueryParam description(String description) {
+ this.description = description;
+ return this;
+ }
+
+ /**
+ * @return the type
+ */
+ public String getType() {
+ return type != null ? type : "string";
+ }
+
+ /**
+ * @param type the type to set
+ */
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ /**
+ * @param type
+ * @return
+ */
+ public ModelQueryParam type(String type) {
+ this.type = type;
+ return this;
+ }
+
+}
diff --git a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
index 4f262cc1f5..48a934daf3 100644
--- a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
+++ b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
@@ -24,11 +24,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
-import jakarta.servlet.ServletContext;
-import jakarta.ws.rs.HttpMethod;
-import jakarta.ws.rs.core.HttpHeaders;
-import jakarta.ws.rs.core.Response;
-
import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.service.DispatchContext;
import org.apache.ofbiz.service.GenericServiceException;
@@ -39,6 +34,7 @@ import org.apache.ofbiz.webapp.WebAppUtil;
import org.apache.ofbiz.ws.rs.core.OFBizApiConfig;
import org.apache.ofbiz.ws.rs.listener.ApiContextListener;
import org.apache.ofbiz.ws.rs.model.ModelApi;
+import org.apache.ofbiz.ws.rs.model.ModelMapping;
import org.apache.ofbiz.ws.rs.model.ModelOperation;
import org.apache.ofbiz.ws.rs.model.ModelResource;
import org.apache.ofbiz.ws.rs.util.OpenApiUtil;
@@ -65,6 +61,10 @@ import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.tags.Tag;
+import jakarta.servlet.ServletContext;
+import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
public final class OFBizOpenApiReader extends Reader implements OpenApiReader {
private static final String MODULE = OFBizOpenApiReader.class.getName();
@@ -108,6 +108,10 @@ public final class OFBizOpenApiReader extends Reader implements OpenApiReader {
apis.forEach((k, api) -> {
if (!api.isPublish()) return;
+ List<ModelMapping> mappings = api.getMappings();
+ mappings.forEach(modelMapping -> {
+ OpenApiUtil.getListTypes().put(modelMapping.getName(), modelMapping.getClassName());
+ });
List<String> baseSegments = new ArrayList<>();
baseSegments.add(api.getPath());
diff --git a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/OpenApiUtil.java b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/OpenApiUtil.java
index 52d60310a8..694ad507ee 100644
--- a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/OpenApiUtil.java
+++ b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/OpenApiUtil.java
@@ -18,7 +18,12 @@
*******************************************************************************/
package org.apache.ofbiz.ws.rs.util;
+import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -59,6 +64,7 @@ public final class OpenApiUtil {
}
+ private static final Map<String, String> LIST_TYPE_MAP = new HashMap<>();
private static final Map<String, String> CLASS_ALIAS = new HashMap<>();
private static final Map<String, Class<?>> JAVA_OPEN_API_MAP = new HashMap<>();
private static final Map<String, String> FIELD_TYPE_MAP = new HashMap<String, String>();
@@ -282,6 +288,9 @@ public final class OpenApiUtil {
* @return the corresponding OpenAPI schema class, or {@code null} if no mapping exists
*/
public static Class<?> getOpenApiTypeForAttributeType(String attributeType) {
+ if (isTypeDomainModel(attributeType)) {
+ return MapSchema.class;
+ }
return JAVA_OPEN_API_MAP.get(CLASS_ALIAS.get(attributeType));
}
@@ -359,7 +368,11 @@ public final class OpenApiUtil {
Delegator delegator = WebAppUtil.getDelegator(ApiContextListener.getApplicationCntx());
if (schema instanceof ArraySchema) {
ArraySchema arrSch = (ArraySchema) schema;
- arrSch.setItems(children.size() > 0 ? getAttributeSchema(service, children.get(0)) : new StringSchema());
+ if (LIST_TYPE_MAP.containsKey(param.getName())) {
+ arrSch.setItems(getSchemaForModel(LIST_TYPE_MAP.get(param.getName())));
+ } else {
+ arrSch.setItems(children.size() > 0 ? getAttributeSchema(service, children.get(0)) : new StringSchema());
+ }
} else if (schema instanceof MapSchema) {
if (isTypeGenericEntityOrGenericValue(param.getType())) {
if (UtilValidate.isEmpty(param.getEntityName())) {
@@ -370,6 +383,8 @@ public final class OpenApiUtil {
return null;
}
schema = getSchemaForEntity(delegator.getModelEntity(param.getEntityName()));
+ } else if (isTypeDomainModel(param.getType())) {
+ schema = getSchemaForModel(param.getType());
} else if (UtilValidate.isEmpty(param.getChildren())) {
Debug.logWarning(
"Attribute '" + param.getName() + "' ignored as it is declared as '" + param.getType() + "' but does not have "
@@ -427,6 +442,81 @@ public final class OpenApiUtil {
return type.matches("org.apache.ofbiz.entity.GenericValue|GenericValue|org.apache.ofbiz.entity.GenericEntity|GenericEntity");
}
+ private static boolean isTypeDomainModel(String className) {
+ if (className == null || CLASS_ALIAS.containsKey(className)) {
+ return false;
+ }
+ try {
+ Class<?> modelClass = Class.forName(className);
+ Class<?> domainModelClass = Class.forName("org.apache.ofbiz.base.model.DomainModel");
+ return domainModelClass.isAssignableFrom(modelClass);
+ } catch (ClassNotFoundException e) {
+ Debug.logInfo("Class not found while checking DomainModel type: " + className, MODULE);
+ }
+ return false;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Schema<?> getSchemaForModel(String className) {
+ Schema<?> dataSchema = new Schema<>();
+ dataSchema.setType("object");
+ try {
+ Class<?> modelClass = Class.forName(className);
+ List<Field> fields = getFields(modelClass);
+ for (Field field : fields) {
+ String fieldNm = field.getName();
+ Class<?> fieldType = field.getType();
+ Schema<?> schema = null;
+ Class<?> schemaClass = getOpenApiTypeForAttributeType(fieldType.getName());
+ if (schemaClass == null) {
+ continue;
+ }
+ try {
+ schema = (Schema<?>) schemaClass.newInstance();
+ if (schema instanceof ArraySchema) {
+ ParameterizedType genericType = (ParameterizedType) field.getGenericType();
+ Class<? extends Type> genericClass = (Class<? extends Type>) genericType.getActualTypeArguments()[0];
+ ArraySchema arrSch = (ArraySchema) schema;
+ Class<?> listSchemaClass = getOpenApiTypeForAttributeType(genericClass.getName());
+ Schema<?> listSchema = null;
+ if (listSchemaClass == null || isTypeDomainModel(genericClass.getName())) {
+ arrSch.setItems(getSchemaForModel(genericClass.getName()));
+ } else {
+ listSchema = (Schema<?>) listSchemaClass.newInstance();
+ arrSch.setItems(listSchema);
+ }
+ dataSchema.addProperties(fieldNm, arrSch);
+ } else if (schema instanceof MapSchema) {
+ if (isTypeDomainModel(fieldType.getName())) {
+ schema = getSchemaForModel(fieldType.getName());
+ dataSchema.addProperties(fieldNm, schema);
+ }
+ } else {
+ dataSchema.addProperties(fieldNm, schema.description(fieldNm));
+ }
+ } catch (InstantiationException | IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ } catch (ClassNotFoundException e1) {
+ e1.printStackTrace();
+ }
+ return dataSchema;
+ }
+
+ private static <T> List<Field> getFields(Class<?> clazz) {
+ List<Field> fields = new ArrayList<>();
+ while (clazz != Object.class) {
+ fields.addAll(0, Arrays.asList(clazz.getDeclaredFields()));
+ clazz = clazz.getSuperclass();
+ }
+ return fields;
+ }
+
+ public static Map<String, String> getListTypes() {
+ return LIST_TYPE_MAP;
+ }
+
private static Schema<?> getSchemaForEntity(ModelEntity entity) {
Schema<?> dataSchema = new Schema<>();
dataSchema.setType("object");
diff --git a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/OpenApiUtilTest.java b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/OpenApiUtilTest.java
new file mode 100644
index 0000000000..0efbfe30ec
--- /dev/null
+++ b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/OpenApiUtilTest.java
@@ -0,0 +1,75 @@
+/*******************************************************************************
+ * 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.ws.rs.util;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.ofbiz.base.model.DomainModel;
+
+import org.junit.jupiter.api.Test;
+
+import io.swagger.v3.oas.models.media.MapSchema;
+import io.swagger.v3.oas.models.media.StringSchema;
+
+class OpenApiUtilTest {
+
+ public static class TestModel extends DomainModel {
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+
+
+ @Test
+ void domainModelSubclassResolvesToMapSchema() {
+ Class<?> result = OpenApiUtil.getOpenApiTypeForAttributeType(TestModel.class.getName());
+ assertEquals(MapSchema.class, result);
+ }
+
+ @Test
+ void classInAliasMapIsNotTreatedAsDomainModel() {
+ Class<?> result = OpenApiUtil.getOpenApiTypeForAttributeType("java.sql.Date");
+ assertEquals(StringSchema.class, result);
+ }
+
+ @Test
+ void nullClassNameResolvesToNullWithoutThrowing() {
+ Class<?> result = OpenApiUtil.getOpenApiTypeForAttributeType(null);
+ assertNull(result);
+ }
+
+ @Test
+ void nonexistentClassNameResolvesToNullWithoutThrowing() {
+ Class<?> result = OpenApiUtil.getOpenApiTypeForAttributeType("non.existing.Class");
+ assertNull(result);
+ }
+
+ @Test
+ void unmappedUnrelatedClassResolvesToNull() {
+ Class<?> result = OpenApiUtil.getOpenApiTypeForAttributeType(Object.class.getName());
+ assertNull(result);
+ }
+}