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 <[email protected]> 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); + } +}

