From 4b8af33c4e6853887db73ddc00abd583904b12cc Mon Sep 17 00:00:00 2001 From: hengsin Date: Tue, 14 Sep 2021 19:51:00 +0800 Subject: [PATCH] IDEMPIERE-4955 Add Rest+Json support to iDempiere Web Services (#873) --- .../server.product.functionaltest.launch | 1 + .../server.product.launch | 1 + .../maven.locations.xml | 6 + .../org.idempiere.p2.targetplatform.target | 8 +- org.idempiere.test/idempiere.unit.test.launch | 1 + org.idempiere.webservices-feature/feature.xml | 7 + .../META-INF/MANIFEST.MF | 3 +- .../META-INF/cxf/rest-context.xml | 4 + .../idempiere/adinterface/ModelADService.java | 4 +- .../xmlbeans/XMLBeansJSONProvider.java | 201 ++++++++++++++++++ .../CustomMappedNamespaceConvention.java | 104 +++++++++ .../mapped/MappedXMLInputFactory.java | 76 +++++++ .../iDempiereWebServices-soapui-project.xml | 54 ++++- 13 files changed, 455 insertions(+), 15 deletions(-) create mode 100644 org.idempiere.webservices/WEB-INF/src/org/idempiere/jaxrs/provider/xmlbeans/XMLBeansJSONProvider.java create mode 100644 org.idempiere.webservices/WEB-INF/src/org/idempiere/jettison/mapped/CustomMappedNamespaceConvention.java create mode 100644 org.idempiere.webservices/WEB-INF/src/org/idempiere/jettison/mapped/MappedXMLInputFactory.java diff --git a/org.adempiere.server-feature/server.product.functionaltest.launch b/org.adempiere.server-feature/server.product.functionaltest.launch index 28861aa5ac..36707c3a71 100644 --- a/org.adempiere.server-feature/server.product.functionaltest.launch +++ b/org.adempiere.server-feature/server.product.functionaltest.launch @@ -197,6 +197,7 @@ + diff --git a/org.adempiere.server-feature/server.product.launch b/org.adempiere.server-feature/server.product.launch index fef50c0513..c49b806bcd 100644 --- a/org.adempiere.server-feature/server.product.launch +++ b/org.adempiere.server-feature/server.product.launch @@ -201,6 +201,7 @@ + diff --git a/org.idempiere.p2.targetplatform/maven.locations.xml b/org.idempiere.p2.targetplatform/maven.locations.xml index 0c53948298..aa930b3714 100644 --- a/org.idempiere.p2.targetplatform/maven.locations.xml +++ b/org.idempiere.p2.targetplatform/maven.locations.xml @@ -834,6 +834,12 @@ Export-Package: *;version="${version}";-noimport:=true 3.1.18 jar + + org.codehaus.jettison + jettison + 1.4.1 + jar + org.dom4j dom4j diff --git a/org.idempiere.p2.targetplatform/org.idempiere.p2.targetplatform.target b/org.idempiere.p2.targetplatform/org.idempiere.p2.targetplatform.target index f27d9cd1b9..1c7b18a166 100644 --- a/org.idempiere.p2.targetplatform/org.idempiere.p2.targetplatform.target +++ b/org.idempiere.p2.targetplatform/org.idempiere.p2.targetplatform.target @@ -1,7 +1,7 @@ - + @@ -967,6 +967,12 @@ Export-Package: *;version="${version}";-noimport:=true 3.1.18 jar + + org.codehaus.jettison + jettison + 1.4.1 + jar + org.dom4j dom4j diff --git a/org.idempiere.test/idempiere.unit.test.launch b/org.idempiere.test/idempiere.unit.test.launch index a7ba0cc5f2..4b3251cbce 100644 --- a/org.idempiere.test/idempiere.unit.test.launch +++ b/org.idempiere.test/idempiere.unit.test.launch @@ -159,6 +159,7 @@ + diff --git a/org.idempiere.webservices-feature/feature.xml b/org.idempiere.webservices-feature/feature.xml index c23652b400..b385d7d022 100644 --- a/org.idempiere.webservices-feature/feature.xml +++ b/org.idempiere.webservices-feature/feature.xml @@ -332,4 +332,11 @@ version="0.0.0" unpack="false"/> + + diff --git a/org.idempiere.webservices/META-INF/MANIFEST.MF b/org.idempiere.webservices/META-INF/MANIFEST.MF index 9419b8a0a1..2da1abadf0 100644 --- a/org.idempiere.webservices/META-INF/MANIFEST.MF +++ b/org.idempiere.webservices/META-INF/MANIFEST.MF @@ -105,7 +105,8 @@ Require-Bundle: org.adempiere.base;bundle-version="0.0.0", wrapped.org.springframework.spring-core;bundle-version="5.2.15", wrapped.org.springframework.spring-expression;bundle-version="5.2.15", wrapped.org.apache.xmlbeans.xmlbeans;bundle-version="3.1.0", - wrapped.org.springframework.spring-web;bundle-version="5.2.15" + wrapped.org.springframework.spring-web;bundle-version="5.2.15", + org.codehaus.jettison.jettison;bundle-version="1.4.1" Bundle-ClassPath: ., lib/idempiere-xmlbeans.jar Export-Package: org.compiere.model, diff --git a/org.idempiere.webservices/META-INF/cxf/rest-context.xml b/org.idempiere.webservices/META-INF/cxf/rest-context.xml index 6d22ac05f4..0deccfe408 100644 --- a/org.idempiere.webservices/META-INF/cxf/rest-context.xml +++ b/org.idempiere.webservices/META-INF/cxf/rest-context.xml @@ -19,6 +19,7 @@ http://cxf.apache.org/schemas/jaxrs.xsd"> + @@ -27,4 +28,7 @@ http://cxf.apache.org/schemas/jaxrs.xsd"> + + \ No newline at end of file diff --git a/org.idempiere.webservices/WEB-INF/src/org/idempiere/adinterface/ModelADService.java b/org.idempiere.webservices/WEB-INF/src/org/idempiere/adinterface/ModelADService.java index 1f8f5ccf9a..155d459b31 100644 --- a/org.idempiere.webservices/WEB-INF/src/org/idempiere/adinterface/ModelADService.java +++ b/org.idempiere.webservices/WEB-INF/src/org/idempiere/adinterface/ModelADService.java @@ -49,8 +49,8 @@ import org.idempiere.adInterface.x10.StandardResponseDocument; import org.idempiere.adInterface.x10.WindowTabDataDocument; @Path("/model_adservice/") -@Consumes("application/xml") -@Produces("application/xml") +@Consumes({"application/xml", "application/json"}) +@Produces({"application/xml", "application/json"}) @WebService(targetNamespace="http://idempiere.org/ADInterface/1_0") @SOAPBinding(style=Style.RPC,use=Use.LITERAL,parameterStyle=ParameterStyle.WRAPPED) public interface ModelADService { diff --git a/org.idempiere.webservices/WEB-INF/src/org/idempiere/jaxrs/provider/xmlbeans/XMLBeansJSONProvider.java b/org.idempiere.webservices/WEB-INF/src/org/idempiere/jaxrs/provider/xmlbeans/XMLBeansJSONProvider.java new file mode 100644 index 0000000000..bb5aac378f --- /dev/null +++ b/org.idempiere.webservices/WEB-INF/src/org/idempiere/jaxrs/provider/xmlbeans/XMLBeansJSONProvider.java @@ -0,0 +1,201 @@ +/*********************************************************************** + * This file is part of iDempiere ERP Open Source * + * http://www.idempiere.org * + * * + * Copyright (C) Contributors * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301, USA. * + * * + * Contributors: * + * - hengsin * + **********************************************************************/ +package org.idempiere.jaxrs.provider.xmlbeans; + +/** + * @author hengsin + * + */ +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.net.HttpURLConnection; +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.Provider; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamWriter; + +import org.apache.cxf.jaxrs.provider.xmlbeans.XMLBeanStreamSerializer; +import org.apache.cxf.jaxrs.provider.xmlbeans.XMLBeansElementProvider; +import org.apache.xmlbeans.XmlObject; +import org.codehaus.jettison.mapped.Configuration; +import org.codehaus.jettison.mapped.MappedXMLOutputFactory; +import org.idempiere.jettison.mapped.CustomMappedNamespaceConvention; +import org.idempiere.jettison.mapped.MappedXMLInputFactory; + +/** + * JSON provider for XMLBeans data objects. + */ +@Produces("application/json") +@Consumes("application/json") +@Provider +public class XMLBeansJSONProvider extends XMLBeansElementProvider { + + private static final String AD_INTERFACE_1_0_NAMESPACE = "http://idempiere.org/ADInterface/1_0"; + + /** {@inheritDoc} */ + @Override + public XmlObject readFrom(Class type, Type genericType, + Annotation[] annotations, MediaType m, + MultivaluedMap headers, InputStream is) + throws IOException { + XmlObject result = null; + + try { + + Map nstojns = new HashMap(); + nstojns.put(AD_INTERFACE_1_0_NAMESPACE, ""); + + Configuration conf = new Configuration(nstojns); + conf.setIgnoreNamespaces(false); + CustomMappedNamespaceConvention convention = new CustomMappedNamespaceConvention(conf); + convention.addNamespacePrefix("i", AD_INTERFACE_1_0_NAMESPACE); + MappedXMLInputFactory factory = new MappedXMLInputFactory(convention); + XMLStreamReader xsr = factory.createXMLStreamReader(is); + result = parseXmlBean(type, xsr); + + xsr.close(); + } catch (XMLStreamException e) { + throw new WebApplicationException(HttpURLConnection.HTTP_INTERNAL_ERROR); + } + + return result; + } + + /** + * Create an XMLBean data object using a stream Reader + * + * @param type declared type of the desired XMLBean data object + * @param reader + * @return an instance of the required object, otherwise null + */ + protected XmlObject parseXmlBean(Class type, XMLStreamReader xsr) { + XmlObject result = null; + + Class factory = getFactory(type); + + try { + + // get factory method parse(InputStream) + Method m = factory.getMethod("parse", XMLStreamReader.class); + Object[] args = {xsr}; + Object obj = m.invoke(type, args); + + if (obj instanceof XmlObject) { + result = (XmlObject)obj; + } + + } catch (NoSuchMethodException nsme) { + nsme.printStackTrace(); + } catch (InvocationTargetException ite) { + ite.printStackTrace(); + } catch (IllegalAccessException iae) { + iae.printStackTrace(); + } + + return result; + } + + /** + * Locate the XMLBean Factory inner class. + * + * @param type + * @return the Factory class if present, otherwise null. + */ + private Class getFactory(Class type) { + Class result = null; + + Class[] declared = type.getDeclaredClasses(); + for (Class c : declared) { + + if (c.getSimpleName().equals("Factory")) { + result = c; + } + } + + if (result == null) { + Class[] interfaces = type.getInterfaces(); + + // look for XMLBeans inner class Factory + for (Class inter : interfaces) { + + declared = inter.getDeclaredClasses(); + + for (Class c : declared) { + + if (c.getSimpleName().equals("Factory")) { + result = c; + } + } + } + } + + return result; + } + + /** {@inheritDoc} */ + @Override + public void writeTo(XmlObject obj, Class cls, Type genericType, Annotation[] annotations, + MediaType m, MultivaluedMap headers, OutputStream os) { + + try { + + // Set up the JSON StAX implementation + Map nstojns = new HashMap(); + Configuration conf = new Configuration(nstojns); + conf.setIgnoreNamespaces(true); + XMLOutputFactory factory = new MappedXMLOutputFactory(conf); + XMLStreamWriter xsw = factory.createXMLStreamWriter(os); + xsw.writeStartDocument(); + if (obj instanceof XmlObject) { + + XmlObject xObj = obj; + XMLBeanStreamSerializer ser = new XMLBeanStreamSerializer(); + ser.serialize(xObj, xsw); + } + + xsw.flush(); + xsw.close(); + + } catch (XMLStreamException e) { + throw new WebApplicationException(HttpURLConnection.HTTP_INTERNAL_ERROR); + } catch (IOException ioe) { + throw new WebApplicationException(HttpURLConnection.HTTP_INTERNAL_ERROR); + } + } +} diff --git a/org.idempiere.webservices/WEB-INF/src/org/idempiere/jettison/mapped/CustomMappedNamespaceConvention.java b/org.idempiere.webservices/WEB-INF/src/org/idempiere/jettison/mapped/CustomMappedNamespaceConvention.java new file mode 100644 index 0000000000..3d8058e848 --- /dev/null +++ b/org.idempiere.webservices/WEB-INF/src/org/idempiere/jettison/mapped/CustomMappedNamespaceConvention.java @@ -0,0 +1,104 @@ +/*********************************************************************** + * This file is part of iDempiere ERP Open Source * + * http://www.idempiere.org * + * * + * Copyright (C) Contributors * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301, USA. * + * * + * Contributors: * + * - hengsin * + **********************************************************************/ +package org.idempiere.jettison.mapped; + +import java.util.HashMap; +import java.util.Map; + +import javax.xml.namespace.QName; + +import org.codehaus.jettison.Node; +import org.codehaus.jettison.mapped.Configuration; +import org.codehaus.jettison.mapped.MappedNamespaceConvention; + +/** + * @author hengsin + * + */ +public class CustomMappedNamespaceConvention extends MappedNamespaceConvention { + + private Map namespacePrefix; + private Map namespacePrefixReversed; + + /** + * @param config + */ + public CustomMappedNamespaceConvention(Configuration config) { + super(config); + namespacePrefix = new HashMap(); + namespacePrefixReversed = new HashMap(); + } + + /** + * @param prefix namespace prefix + * @param uri namespace uri + */ + public void addNamespacePrefix(String prefix, String uri) { + namespacePrefix.put(uri, prefix); + namespacePrefixReversed.put(prefix, uri); + } + + /* (non-Javadoc) + * @see org.codehaus.jettison.mapped.MappedNamespaceConvention#createQName(java.lang.String, org.codehaus.jettison.Node) + */ + @Override + public QName createQName(String rootName, Node node) { + int dot = rootName.lastIndexOf( '.' ); + QName qname = null; + String local = rootName; + + if ( dot == -1 ) { + dot = 0; + } + else { + local = local.substring( dot + 1 ); + } + + String jns = rootName.substring( 0, dot ); + String xns = (String) getNamespaceURI( jns ); + + if ( xns == null ) { + qname = new QName( rootName ); + } + else { + String prefix = namespacePrefix.get(xns); + if (prefix != null && prefix.trim().length() > 0) + { + qname = new QName( xns, local, prefix ); + if (node.getObject() != null && namespacePrefixReversed.isEmpty()) + node.setNamespace(prefix, xns); + } + else + qname = new QName( xns, local ); + } + + if (!namespacePrefixReversed.isEmpty()) { + node.setNamespaces(namespacePrefixReversed); + } + return qname; + } + + +} diff --git a/org.idempiere.webservices/WEB-INF/src/org/idempiere/jettison/mapped/MappedXMLInputFactory.java b/org.idempiere.webservices/WEB-INF/src/org/idempiere/jettison/mapped/MappedXMLInputFactory.java new file mode 100644 index 0000000000..dddaffe1fd --- /dev/null +++ b/org.idempiere.webservices/WEB-INF/src/org/idempiere/jettison/mapped/MappedXMLInputFactory.java @@ -0,0 +1,76 @@ +/*********************************************************************** + * This file is part of iDempiere ERP Open Source * + * http://www.idempiere.org * + * * + * Copyright (C) Contributors * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301, USA. * + * * + * Contributors: * + * - hengsin * + **********************************************************************/ +package org.idempiere.jettison.mapped; + +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.codehaus.jettison.AbstractXMLInputFactory; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.codehaus.jettison.json.JSONTokener; +import org.codehaus.jettison.mapped.MappedNamespaceConvention; +import org.codehaus.jettison.mapped.MappedXMLStreamReader; + +/** + * @author hengsin + * + */ +public class MappedXMLInputFactory extends AbstractXMLInputFactory { + + + private MappedNamespaceConvention convention; + + /** + * + * @param convention + */ + public MappedXMLInputFactory(MappedNamespaceConvention convention) { + this.convention = convention; + } + + /** + * @param tokener + * @return {@link XMLStreamReader} + */ + public XMLStreamReader createXMLStreamReader(JSONTokener tokener) throws XMLStreamException { + try { + JSONObject root = createJSONObject(tokener); + return new MappedXMLStreamReader(root, convention); + } catch (JSONException e) { + throw new XMLStreamException(e); + } + } + + /** + * + * @param tokener + * @return {@link JSONObject} + * @throws JSONException + */ + protected JSONObject createJSONObject(JSONTokener tokener) throws JSONException { + return new JSONObject(tokener); + } +} diff --git a/org.idempiere.webservices/testScripts/iDempiereWebServices-soapui-project.xml b/org.idempiere.webservices/testScripts/iDempiereWebServices-soapui-project.xml index 0839ad9ffd..2a84e146a2 100644 --- a/org.idempiere.webservices/testScripts/iDempiereWebServices-soapui-project.xml +++ b/org.idempiere.webservices/testScripts/iDempiereWebServices-soapui-project.xml @@ -1,5 +1,5 @@ -https://localhost:8443/ADInterface/services/ModelADService?wsdl +https://localhost:8443/ADInterface/services/ModelADService?wsdl @@ -461,7 +461,7 @@ -]]>http://schemas.xmlsoap.org/wsdl/https://localhost:8443/ADInterface/services/ModelADService<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService +]]>http://schemas.xmlsoap.org/wsdl/https://localhost:8443/ADInterface/services/ModelADService<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService <_0:createData> @@ -511,7 +511,7 @@ -]]>Global HTTP Settings<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService +]]>Global HTTP Settings<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService <_0:deleteData> @@ -535,7 +535,7 @@ -]]>Global HTTP Settings<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService +]]>Global HTTP Settings<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService <_0:getList> @@ -558,7 +558,7 @@ -]]>Global HTTP Settings<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService +]]>Global HTTP Settings<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService <_0:queryData> @@ -587,7 +587,7 @@ -]]>Global HTTP Settings<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService +]]>BasicBasicGlobal HTTP Settings<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService <_0:readData> @@ -611,7 +611,7 @@ -]]>Global HTTP SettingsExample on how to run report Storage Detail with HQ Warehouse and Patio Chair as parameters You need to define web service security for: Web Service Type: RunStorageDetail Web Service Parameters: AD_Process_ID Constant 236 AD_Menu_ID Constant 0 AD_Record_ID Constant 0 And allow execution to the WebService role on the report. <xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService +]]>BasicBasicGlobal HTTP SettingsExample on how to run report Storage Detail with HQ Warehouse and Patio Chair as parameters You need to define web service security for: Web Service Type: RunStorageDetail Web Service Parameters: AD_Process_ID Constant 236 AD_Menu_ID Constant 0 AD_Record_ID Constant 0 And allow execution to the WebService role on the report. <xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService <_0:runProcess> @@ -637,7 +637,7 @@ -]]>Global HTTP Settings<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService +]]>Global HTTP Settings<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService <_0:runProcess> @@ -658,7 +658,7 @@ -]]>Global HTTP Settings<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService +]]>Global HTTP Settings<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService <_0:setDocAction> @@ -682,7 +682,7 @@ -]]>Global HTTP Settings<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService +]]>Global HTTP Settings<xml-fragment/>UTF-8https://localhost:8443/ADInterface/services/ModelADService <_0:updateData> @@ -711,4 +711,36 @@ -]]>Global HTTP Settings \ No newline at end of file +]]>Global HTTP Settings<xml-fragment xmlns:con="http://eviware.com/soapui/config"> + <con:entry key="Accept" value="application/json"/> + <con:entry key="Content-Type" value="application/json"/> +</xml-fragment>UTF-8https://localhost:8443/ADInterface/services/rest/model_adservice/query_data{ + "ModelCRUDRequest": { + "ModelCRUD": { + "serviceType": "QueryBPartner", + "TableName": "C_BPartner", + "Filter": "name < 'S'", + "Action": "Read", + "DataRow": { //optional filter by column values + "field": [ + { + "@column": "C_BP_Group_ID", + "val": "103" + } + ] + } + }, + "ADLoginRequest": { + "user": "WebService", + "pass": "WebService", + "lang": "en_US", + "ClientID": "11", + "RoleID": "50004", + "OrgID": "11", + "WarehouseID": "103", + "stage": "9" + } + } +} + +BasicBasicGlobal HTTP Settings \ No newline at end of file