From 083951c70cbcabdf90ab333bafaa1837904f078f Mon Sep 17 00:00:00 2001 From: Andrew Psaltis Date: Wed, 3 May 2017 11:08:06 -0400 Subject: [PATCH] NIFI-3536 Adding GetSOAP processor which supports generically invoking SOAP endpoints --- nifi-assembly/NOTICE | 11 + .../nifi-soap-bundle/nifi-soap-nar/pom.xml | 40 ++ .../src/main/resources/META-INF/LICENSE | 240 +++++++++ .../src/main/resources/META-INF/NOTICE | 43 ++ .../nifi-soap-processors/pom.xml | 128 +++++ .../apache/nifi/processors/soap/GetSOAP.java | 508 ++++++++++++++++++ .../org.apache.nifi.processor.Processor | 15 + .../nifi/processors/soap/GetSOAPIT.java | 170 ++++++ .../nifi/processors/soap/GetSOAPTest.java | 283 ++++++++++ .../soap/VerifyRequestCallback.java | 66 +++ nifi-nar-bundles/nifi-soap-bundle/pom.xml | 34 ++ nifi-nar-bundles/pom.xml | 1 + 12 files changed, 1539 insertions(+) create mode 100644 nifi-nar-bundles/nifi-soap-bundle/nifi-soap-nar/pom.xml create mode 100644 nifi-nar-bundles/nifi-soap-bundle/nifi-soap-nar/src/main/resources/META-INF/LICENSE create mode 100644 nifi-nar-bundles/nifi-soap-bundle/nifi-soap-nar/src/main/resources/META-INF/NOTICE create mode 100644 nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/pom.xml create mode 100644 nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/main/java/org/apache/nifi/processors/soap/GetSOAP.java create mode 100644 nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor create mode 100644 nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/test/java/org/apache/nifi/processors/soap/GetSOAPIT.java create mode 100644 nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/test/java/org/apache/nifi/processors/soap/GetSOAPTest.java create mode 100644 nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/test/java/org/apache/nifi/processors/soap/VerifyRequestCallback.java create mode 100644 nifi-nar-bundles/nifi-soap-bundle/pom.xml diff --git a/nifi-assembly/NOTICE b/nifi-assembly/NOTICE index bd7ccc2629c2..3ff385bb5716 100644 --- a/nifi-assembly/NOTICE +++ b/nifi-assembly/NOTICE @@ -1275,6 +1275,17 @@ The following binary components are provided under the Apache Software License v Hortonworks Schema Registry Copyright 2016-2017 Hortonworks, Inc. + (ASLv2) Apache Axis2 + The following NOTICE information applies: + Apache Axis2 + Copyright 2002-2016 The Apache Software Foundation + + (ASLv2) Apache Commons Axiom + The following NOTICE information applies: + Apache Commons Axiom + Copyright 2001-2015 The Apache Software Foundation + + ************************ Common Development and Distribution License 1.1 ************************ diff --git a/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-nar/pom.xml b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-nar/pom.xml new file mode 100644 index 000000000000..f70bbc4357e9 --- /dev/null +++ b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-nar/pom.xml @@ -0,0 +1,40 @@ + + + + 4.0.0 + + + org.apache.nifi + nifi-soap-bundle + 1.3.0-SNAPSHOT + + + nifi-soap-nar + nar + + true + true + + + + + org.apache.nifi + nifi-soap-processors + 1.3.0-SNAPSHOT + + + + diff --git a/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-nar/src/main/resources/META-INF/LICENSE b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-nar/src/main/resources/META-INF/LICENSE new file mode 100644 index 000000000000..70db055e37ff --- /dev/null +++ b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-nar/src/main/resources/META-INF/LICENSE @@ -0,0 +1,240 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. + + APACHE NIFI SUBCOMPONENTS: + +The Apache NiFi project contains subcomponents with separate copyright +notices and license terms. Your use of the source code for the these +subcomponents is subject to the terms and conditions of the following +licenses. + + The binary distribution of this product bundles 'Paranamer Core' which is available + under a BSD style license. + + Copyright (c) 2006 Paul Hammant & ThoughtWorks Inc + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-nar/src/main/resources/META-INF/NOTICE b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-nar/src/main/resources/META-INF/NOTICE new file mode 100644 index 000000000000..de01effd1c65 --- /dev/null +++ b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-nar/src/main/resources/META-INF/NOTICE @@ -0,0 +1,43 @@ +nifi-soap-nar +Copyright 2014-2017 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +=========================================== +Apache Software License v2 +=========================================== + +The following binary components are provided under the Apache Software License v2 + + (ASLv2) Apache Axis2 + The following NOTICE information applies: + Apache Axis2 + Copyright 2002-2016 The Apache Software Foundation + + (ASLv2) Apache Commons Axiom + The following NOTICE information applies: + Apache Commons Axiom + Copyright 2001-2015 The Apache Software Foundation + + (ASLv2) Jackson JSON processor + The following NOTICE information applies: + # Jackson JSON processor + + Jackson is a high-performance, Free/Open Source JSON processing library. + It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has + been in development since 2007. + It is currently developed by a community of developers, as well as supported + commercially by FasterXML.com. + + ## Licensing + + Jackson core and extension components may licensed under different licenses. + To find the details that apply to this artifact see the accompanying LICENSE file. + For more information, including possible other licensing options, contact + FasterXML.com (http://fasterxml.com). + + ## Credits + + A list of contributors may be found from CREDITS file, which is included + in some artifacts (usually source distributions); but is always available + from the source code management (SCM) system project uses. \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/pom.xml b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/pom.xml new file mode 100644 index 000000000000..c7626bfacf4a --- /dev/null +++ b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/pom.xml @@ -0,0 +1,128 @@ + + + + 4.0.0 + + + org.apache.nifi + nifi-soap-bundle + 1.3.0-SNAPSHOT + + + nifi-soap-processors + jar + + + 1.7.1 + 1.2.18 + 2.7.4 + + + + org.apache.axis2 + axis2-kernel + ${axis2.version} + + + org.apache.axis2 + axis2-transport-http + ${axis2.version} + + + org.apache.axis2 + axis2-jaxws + ${axis2.version} + + + org.apache.ws.commons.axiom + axiom-api + ${axiom.version} + + + org.apache.ws.commons.axiom + axiom-dom + ${axiom.version} + + + org.apache.axis2 + axis2-fastinfoset + ${axis2.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + + + org.apache.nifi + nifi-api + + + org.apache.nifi + nifi-processor-utils + + + + + org.apache.nifi + nifi-mock + test + + + org.slf4j + slf4j-log4j12 + test + + + junit + junit + 4.11 + test + + + + org.mock-server + mockserver-netty + 3.10.4 + test + + + org.mockito + mockito-core + 1.10.19 + test + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + ${jackson.version} + test + + + + diff --git a/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/main/java/org/apache/nifi/processors/soap/GetSOAP.java b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/main/java/org/apache/nifi/processors/soap/GetSOAP.java new file mode 100644 index 000000000000..316944c9cd02 --- /dev/null +++ b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/main/java/org/apache/nifi/processors/soap/GetSOAP.java @@ -0,0 +1,508 @@ +/* + * 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.nifi.processors.soap; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.axiom.om.OMAbstractFactory; +import org.apache.axiom.om.OMElement; +import org.apache.axiom.om.OMFactory; +import org.apache.axiom.om.OMNamespace; +import org.apache.axis2.AxisFault; +import org.apache.axis2.Constants; +import org.apache.axis2.addressing.EndpointReference; +import org.apache.axis2.client.Options; +import org.apache.axis2.client.ServiceClient; +import org.apache.axis2.transport.http.HTTPConstants; +import org.apache.axis2.transport.http.impl.httpclient3.HttpTransportPropertiesImpl; +import org.apache.nifi.annotation.behavior.DynamicProperty; +import org.apache.nifi.annotation.behavior.InputRequirement; +import org.apache.nifi.annotation.behavior.SupportsBatching; +import org.apache.nifi.annotation.behavior.WritesAttribute; +import org.apache.nifi.annotation.behavior.WritesAttributes; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.components.Validator; +import org.apache.nifi.flowfile.FlowFile; +import org.apache.nifi.flowfile.attributes.CoreAttributes; +import org.apache.nifi.logging.ComponentLog; +import org.apache.nifi.processor.AbstractProcessor; +import org.apache.nifi.processor.ProcessContext; +import org.apache.nifi.processor.ProcessSession; +import org.apache.nifi.processor.ProcessorInitializationContext; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processor.util.StandardValidators; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@SupportsBatching +@InputRequirement(InputRequirement.Requirement.INPUT_ALLOWED) +@Tags({"SOAP", "Get", "Ingest", "Ingress"}) +@CapabilityDescription( + "Execute provided request against the SOAP endpoint. The result will be left in it's orginal form and all " + + "headers that are returned will be written as attributes to the resulting flow file.") +@WritesAttributes({@WritesAttribute(attribute = "getsoap.status.code", description = "The status code that is returned"), + @WritesAttribute(attribute = "getsoap.status.message", description = "The status message that is returned"), + @WritesAttribute(attribute = "getsoap.request.wsdl_url", description = "The request WSDL URL"), + @WritesAttribute(attribute = "getsoap.request.method", description = "The request method"), + @WritesAttribute(attribute = "mime.type", description = "Sets mime type to text/xml")}) +@DynamicProperty(name = "The name of a input parameter the needs to be passed to the SOAP method being invoked.", + value = "The value for this parameter '=' and ',' are not considered valid values and must be escaped . " + + "Note, if the value of parameter needs to be an array the format should be key1=value1,key2=value2. ", + description = "The name provided will be the name sent in the SOAP method, therefore please make sure " + + "it matches the wsdl documentation for the SOAP service being called. In the case of arrays " + + "the name will be the name of the array and the key's specified in the value will be the element " + + "names passed.") + +public class GetSOAP extends AbstractProcessor { + + protected static final PropertyDescriptor ENDPOINT_URL = new PropertyDescriptor + .Builder() + .name("endpoint-url") + .displayName("Endpoint URL") + .description("The endpoint url that hosts the web service(s) that should be called.") + .required(true) + .expressionLanguageSupported(false) + .addValidator(StandardValidators.URL_VALIDATOR) + .build(); + + protected static final PropertyDescriptor WSDL_URL = new PropertyDescriptor + .Builder() + .name("wsdl-url") + .displayName("WSDL URL") + .description("The url where the wsdl file can be retrieved and referenced.") + .required(true) + .expressionLanguageSupported(false) + .addValidator(StandardValidators.URL_VALIDATOR) + .build(); + + protected static final PropertyDescriptor METHOD_NAME = new PropertyDescriptor + .Builder() + .name("soap-method-name") + .displayName("SOAP Method Name") + .description("The method exposed by the SOAP webservice that should be invoked.") + .required(true) + .expressionLanguageSupported(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + protected static final PropertyDescriptor USER_NAME = new PropertyDescriptor + .Builder() + .name("username") + .displayName("Username") + .sensitive(true) + .description("The username to use in the case of basic Auth") + .required(false) + .expressionLanguageSupported(false) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + protected static final PropertyDescriptor PASSWORD = new PropertyDescriptor + .Builder() + .name("password") + .displayName("Password") + .sensitive(true) + .description("The password to use in the case of basic Auth") + .required(false) + .expressionLanguageSupported(false) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + protected static final PropertyDescriptor USER_AGENT = new PropertyDescriptor + .Builder() + .name("user-agent") + .displayName("User Agent") + .defaultValue("NiFi SOAP Processor") + .description("The user agent string to use, the default is Nifi SOAP Processor") + .required(false) + .expressionLanguageSupported(false) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + protected static final PropertyDescriptor SO_TIMEOUT = new PropertyDescriptor + .Builder() + .name("socket-timeout") + .displayName("Socket Timeout") + .defaultValue("60000") + .description("The timeout value to use waiting for data from the webservice") + .required(false) + .expressionLanguageSupported(false) + .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) + .build(); + + protected static final PropertyDescriptor CONNECTION_TIMEOUT = new PropertyDescriptor + .Builder() + .name("connection-timeout") + .displayName("Connection Timeout") + .defaultValue("60000") + .description("The timeout value to use waiting to establish a connection to the web service") + .required(false) + .expressionLanguageSupported(false) + .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) + .build(); + + protected static final PropertyDescriptor AUTH_BY_HEADER = new PropertyDescriptor.Builder() + .name("header-authentication") + .displayName("Header Authentication") + .description("If you need to do authentication with SOAP headers, this must be a json string " + + "defining the structure since it varies from service to service. Username and Password " + + "must be set in the separate parameters and referenced here " + + "as variables ${Username} and ${Password}. For example, " + + "{ \"UserAuthentication\": { \"username\": \"${Username}\", \"password\": \"${Password}\"," + + " \"some_other_custom_field\": \"value\" }") + .required(false) + .expressionLanguageSupported(true) + .addValidator(new Validator() { + @Override + public ValidationResult validate(String subject, String value, ValidationContext context) { + try { + return (new ValidationResult.Builder()) + .subject(subject) + .input(value) + .explanation("Header Authentication must be empty or valid json") + .valid("".equals(value) || mapper.readTree(value) != null).build(); + } catch (IOException e) { + return (new ValidationResult.Builder()) + .subject(subject) + .input(value) + .explanation("Header Authentication must be empty or valid json: " + e.getMessage()) + .valid(false).build(); + } + } + }) + .build(); + + protected static final PropertyDescriptor API_NAMESPACE = new PropertyDescriptor.Builder() + .name("namespace") + .displayName("Namespace") + .description("XML Namespace.") + .required(true) + .expressionLanguageSupported(false) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .dynamic(false) + .build(); + + protected static final PropertyDescriptor SKIP_FIRST_ELEMENT = new PropertyDescriptor.Builder() + .name("skip-first-element") + .displayName("Skip First Element") + .description("Return the XML that comes after the first element.") + .required(true) + .defaultValue("false") + .allowableValues("true", "false") + .expressionLanguageSupported(false) + .addValidator(StandardValidators.BOOLEAN_VALIDATOR) + .dynamic(false) + .build(); + + public static final Relationship REL_SUCCESS = new Relationship.Builder() + .name("success") + .description("All FlowFiles that are created are routed to this relationship") + .build(); + + private List descriptors; + + private static final ObjectMapper mapper = new ObjectMapper(); + + @Override + protected void init(final ProcessorInitializationContext context) { + final List descriptors = new ArrayList<>(); + descriptors.add(ENDPOINT_URL); + descriptors.add(WSDL_URL); + descriptors.add(METHOD_NAME); + descriptors.add(USER_NAME); + descriptors.add(PASSWORD); + descriptors.add(USER_AGENT); + descriptors.add(SO_TIMEOUT); + descriptors.add(CONNECTION_TIMEOUT); + descriptors.add(AUTH_BY_HEADER); + descriptors.add(API_NAMESPACE); + descriptors.add(SKIP_FIRST_ELEMENT); + this.descriptors = Collections.unmodifiableList(descriptors); + } + + @Override + public Set getRelationships() { + final Set relationships = new HashSet<>(1); + relationships.add(REL_SUCCESS); + return relationships; + } + + @Override + public final List getSupportedPropertyDescriptors() { + return descriptors; + } + + @Override + protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) { + return new PropertyDescriptor.Builder() + .description("Specifies the method name and parameter names and values for '" + + propertyDescriptorName + + "' the SOAP method being called.") + .name(propertyDescriptorName) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .dynamic(true) + .expressionLanguageSupported(true) + .build(); + } + + @Override + protected final Collection customValidate(final ValidationContext context) { + final List problems = new ArrayList<>(super.customValidate(context)); + + + if ((!context.getProperty(USER_NAME).isSet() && !context.getProperty(PASSWORD).isSet()) + && context.getProperty(AUTH_BY_HEADER).isSet()) { + problems.add( + new ValidationResult.Builder() + .subject(AUTH_BY_HEADER.getName()) + .input(context.getProperty(AUTH_BY_HEADER).getValue()) + .valid(false) + .explanation("To use " + AUTH_BY_HEADER.getDisplayName() + " you must also set " + + USER_NAME.getDisplayName() + " and " + PASSWORD.getDisplayName()) + .build() + ); + } + + return problems; + } + + + @Override + public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { + final ComponentLog logger = getLogger(); + FlowFile flowFile = session.get(); + if (flowFile == null) { + flowFile = session.create(); + } + + final Options options = getOptions(context); + final OMFactory fac = OMAbstractFactory.getOMFactory(); + final String apiNamespace = context.getProperty(API_NAMESPACE).getValue(); + final OMNamespace omNamespace = fac.createOMNamespace(apiNamespace, "ns1"); + + + final OMElement authHeader = getAuthHeader(context, fac, omNamespace); + + ServiceClient serviceClient; + try { + serviceClient = new ServiceClient(); + serviceClient.setOptions(options); + } catch (AxisFault axisFault) { + getLogger().error( + "Failed to create webservice client, please check that the service endpoint is available and " + + "the property is valid.", + axisFault); + throw new ProcessException(axisFault); + } + final String methodName = context.getProperty(METHOD_NAME) + .evaluateAttributeExpressions(flowFile) + .getValue(); + + try { + + serviceClient.getOptions().setAction(apiNamespace + methodName); + + //get the dynamic properties, execute the call and return the results + final OMElement method = getSoapMethod(fac, omNamespace, methodName); + + //now we need to walk the arguments and add them + addArgumentsToMethod(context, fac, omNamespace, method, flowFile); + final OMElement result = executeSoapMethod(serviceClient, authHeader, method); + logger.debug("RESULT" + result); + final boolean skipFirstElement = context.getProperty(SKIP_FIRST_ELEMENT).asBoolean(); + flowFile = processSoapRequest(session, result, flowFile, skipFirstElement); + + + session.transfer(flowFile, REL_SUCCESS); + } catch (final Throwable t) { + context.yield(); + session.rollback(); + logger.error("Failed to process due to {}; rolling back session", new Object[]{t.getMessage()}, t); + throw t; + } + } + + private OMElement getAuthHeader(ProcessContext context, OMFactory fac, OMNamespace omNamespace) { + OMElement authHeader = null; + if (context.getProperty(AUTH_BY_HEADER).isSet()) { + + Map attributes = new HashMap<>(); + //get the username and password -- they both must be populated if using basic auth. + + final String userName = context.getProperty(USER_NAME).getValue(); + final String password = context.getProperty(PASSWORD).getValue(); + + attributes.put("Username", userName); + attributes.put("Password", password); + // Evaluate the contents of authByHeader and allow variable replacement with $Username $Password + final String authByHeader = context.getProperty(AUTH_BY_HEADER) + .evaluateAttributeExpressions(attributes) + .getValue(); + + + if (authByHeader != null && !"".equals(authByHeader)) { + Map map = null; + try { + map = mapper.readValue(authByHeader, + new TypeReference>() { + }); + } catch (IOException e) { + getLogger().error(e.getMessage(), e); + } + + authHeader = createAuthHeader(fac, omNamespace, null, map); + } + } + return authHeader; + } + + private Options getOptions(ProcessContext context) { + Options options = new Options(); + + final String endpointURL = context.getProperty(ENDPOINT_URL).getValue(); + options.setTo(new EndpointReference(endpointURL)); + + if (isHTTPS(endpointURL)) { + options.setTransportInProtocol(Constants.TRANSPORT_HTTPS); + } else { + options.setTransportInProtocol(Constants.TRANSPORT_HTTP); + } + + options.setCallTransportCleanup(true); + options.setProperty(HTTPConstants.CHUNKED, false); + + options.setProperty(HTTPConstants.USER_AGENT, context.getProperty(USER_AGENT).getValue()); + options.setProperty(HTTPConstants.SO_TIMEOUT, context.getProperty(SO_TIMEOUT).asInteger()); + options.setProperty(HTTPConstants.CONNECTION_TIMEOUT, context.getProperty(CONNECTION_TIMEOUT).asInteger()); + + if((context.getProperty(USER_NAME).isSet() && context.getProperty(PASSWORD).isSet()) + && !context.getProperty(AUTH_BY_HEADER).isSet()){ + final String userName = context.getProperty(USER_NAME).getValue(); + final String password = context.getProperty(PASSWORD).getValue(); + HttpTransportPropertiesImpl.Authenticator + auth = new HttpTransportPropertiesImpl.Authenticator(); + auth.setUsername(userName); + auth.setPassword(password); + options.setProperty(org.apache.axis2.transport.http.HTTPConstants.AUTHENTICATE, auth); + } + return options; + } + + protected OMElement createAuthHeader(final OMFactory fac, final OMNamespace omNamespace, final OMElement parent, Map map) { + OMElement element = null; + if (map != null) { + for (String key : map.keySet()) { + element = fac.createOMElement(key, omNamespace); + Object value = map.get(key); + if (value instanceof String) { + element.setText(value.toString()); + if (parent != null) { + parent.addChild(element); + } + } else { + OMElement childElement = createAuthHeader(fac, omNamespace, element, (Map) value); + element.addChild(childElement); + } + } + } + return element; + } + + FlowFile processSoapRequest(ProcessSession session, final OMElement result, FlowFile flowFile, final boolean skipFirstElement) { + flowFile = session.write(flowFile, out -> { + try { + if (skipFirstElement) { + String response = result.getFirstElement().getText(); + out.write(response.getBytes()); + } else { + out.write(result.toString().getBytes()); + } + } catch (AxisFault axisFault) { + final ComponentLog logger = getLogger(); + if (null != logger) { + logger.error("Failed parsing the data that came back from the web service method", axisFault); + } + throw new ProcessException(axisFault); + } + }); + + final Map attributes = new HashMap<>(); + attributes.put(CoreAttributes.MIME_TYPE.key(), "application/xml"); + return session.putAllAttributes(flowFile, attributes); + } + + OMElement executeSoapMethod(final ServiceClient serviceClient, final OMElement authHeader, final OMElement method) { + try { + if (authHeader != null) { + serviceClient.addHeader(authHeader); + } + getLogger().info("Sending authHeader: " + authHeader); + getLogger().info("Sending method: " + method); + return serviceClient.sendReceive(method); + } catch (AxisFault axisFault) { + final ComponentLog logger = getLogger(); + if (null != logger) { + logger.error("Failed invoking the web service method", axisFault); + } + throw new ProcessException(axisFault); + } + } + + void addArgumentsToMethod(ProcessContext context, + OMFactory fac, + OMNamespace omNamespace, + OMElement method, + FlowFile flowFile) { + final ComponentLog logger = getLogger(); + for (final Map.Entry entry : context.getProperties().entrySet()) { + PropertyDescriptor descriptor = entry.getKey(); + if (descriptor.isDynamic()) { + if (null != logger) { + logger.debug("Processing dynamic property: " + + descriptor.getName() + + " with value: " + + entry.getValue()); + } + OMElement value = getSoapMethod(fac, omNamespace, descriptor.getName()); + // Allow for expression language in dynamic parameters + String v = context.getProperty(descriptor).evaluateAttributeExpressions(flowFile).getValue(); + value.addChild(fac.createOMText(value, v)); + method.addChild(value); + } + } + } + + OMElement getSoapMethod(OMFactory fac, OMNamespace omNamespace, String value) { + return fac.createOMElement(value, omNamespace); + } + + private static boolean isHTTPS(final String url) { + return url.charAt(4) == ':'; + } +} diff --git a/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor new file mode 100644 index 000000000000..7f044d9822ce --- /dev/null +++ b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor @@ -0,0 +1,15 @@ +# 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. +org.apache.nifi.processors.soap.GetSOAP \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/test/java/org/apache/nifi/processors/soap/GetSOAPIT.java b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/test/java/org/apache/nifi/processors/soap/GetSOAPIT.java new file mode 100644 index 000000000000..466f167f0e70 --- /dev/null +++ b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/test/java/org/apache/nifi/processors/soap/GetSOAPIT.java @@ -0,0 +1,170 @@ +/* + * 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.nifi.processors.soap; + +import org.apache.nifi.util.MockFlowFile; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +import static org.apache.nifi.processors.soap.GetSOAP.REL_SUCCESS; + +/** + * Integration test that will query live soap services. + */ +public class GetSOAPIT { + + private static final Logger logger = LoggerFactory.getLogger(GetSOAPIT.class); + + private TestRunner testRunner; + + @Before + public void init() { + testRunner = TestRunners.newTestRunner(GetSOAP.class); + } + + @After + public void after() { + testRunner.shutdown(); + } + + /** + * Public service that does not require authentication. + */ + @Test + public void testWeather() { + testRunner.setProperty(GetSOAP.ENDPOINT_URL, + "https://graphical.weather.gov:443/xml/SOAP_server/ndfdXMLserver.php"); + testRunner.setProperty(GetSOAP.WSDL_URL, "http://graphical.weather.gov/xml/DWMLgen/wsdl/ndfdXML.wsdl"); + testRunner.setProperty(GetSOAP.METHOD_NAME, "LatLonListZipCode"); + testRunner.setProperty(GetSOAP.API_NAMESPACE, "http://graphical.weather.gov/xml/DWMLgen/wsdl/ndfdXML.wsdl"); + testRunner.setProperty(GetSOAP.SKIP_FIRST_ELEMENT, "true"); + testRunner.setProperty("zipCodeList", "90720"); + + testRunner.enqueue(""); + testRunner.run(); + + testRunner.assertAllFlowFilesTransferred(REL_SUCCESS, 1); + List flowFileList = testRunner.getFlowFilesForRelationship(REL_SUCCESS); + assert (null != flowFileList); + for (MockFlowFile flowFile : flowFileList) { + String data = new String(testRunner.getContentAsByteArray(flowFile)); + logger.info("Data returned: {}", data); + } + } + + /** + * Public service that does not require authentication. This service required the SOAPAction header to be set. + * The others didn't seem to require it. + */ + //Using Ignore as we do not want this to run on a build server. + @Ignore + @Test + public void testDilbert() { + testRunner.setProperty(GetSOAP.ENDPOINT_URL, "http://gcomputer.net/webservices/dilbert.asmx"); + testRunner.setProperty(GetSOAP.WSDL_URL, "http://www.gcomputer.net/webservices/dilbert.asmx?WSDL"); + testRunner.setProperty(GetSOAP.METHOD_NAME, "DailyDilbert"); + testRunner.setProperty(GetSOAP.API_NAMESPACE, "http://gcomputer.net/webservices/"); + testRunner.setProperty("ADate", "2017-02-01T00:00:00"); + testRunner.enqueue(""); + testRunner.run(); + + testRunner.assertAllFlowFilesTransferred(REL_SUCCESS, 1); + List flowFileList = testRunner.getFlowFilesForRelationship(REL_SUCCESS); + assert (null != flowFileList); + for (MockFlowFile flowFile : flowFileList) { + String data = new String(testRunner.getContentAsByteArray(flowFile)); + logger.info("Data returned: {}", data); + } + } + + /** + * Public service that does not require authentication. + */ + //Using Ignore as we do not want this to run on a build server. + //Also FYI -- often-times even when using the test page for this WS found at the ENDPOINT_URL + //it returns an exception. + @Ignore + @Test + public void testStockQuote() { + testRunner.setProperty(GetSOAP.ENDPOINT_URL, "http://www.webservicex.com/stockquote.asmx"); + testRunner.setProperty(GetSOAP.WSDL_URL, "http://www.webservicex.com/stockquote.asmx?WSDL"); + testRunner.setProperty(GetSOAP.METHOD_NAME, "GetQuote"); + testRunner.setProperty(GetSOAP.API_NAMESPACE, "http://www.webserviceX.NET/"); + testRunner.setProperty(GetSOAP.SKIP_FIRST_ELEMENT, "true"); + testRunner.setProperty("symbol", "YHOO"); // Maybe delisted soon? lol + + testRunner.enqueue(""); + testRunner.run(); + + testRunner.assertAllFlowFilesTransferred(REL_SUCCESS, 1); + List flowFileList = testRunner.getFlowFilesForRelationship(REL_SUCCESS); + assert (null != flowFileList); + for (MockFlowFile flowFile : flowFileList) { + String data = new String(testRunner.getContentAsByteArray(flowFile)); + logger.debug("Data returned: {}", data); + Assert.assertTrue(data.startsWith("YHOO")); + } + } + + /** + * This is a test of a private service that does require authentication. In order to run this test, + * you will need to fill in the endpoint, wsdl, username, password, etc. + */ + //Using Ignore as we do not want this to run on a build server. + @Ignore + @Test + public void testServiceWithAuthentication() { + testRunner.setProperty(GetSOAP.ENDPOINT_URL, "http://api.someservice.com/v3/SomeService"); + testRunner.setProperty(GetSOAP.WSDL_URL, "http://api.someservice.com/v3/SomeService?wsdl"); + testRunner.setProperty(GetSOAP.METHOD_NAME, "getTransactionList"); + testRunner.setProperty(GetSOAP.API_NAMESPACE, "http://api.someservice.com/"); + // Testing auth by header and passing username/password as variables. + testRunner.setProperty(GetSOAP.AUTH_BY_HEADER, "{\"UserAuthentication\": { " + + " \"iId\": \"${Username}\", " + + " \"sPassword\": \"${Password}\", " + + " \"sType\": \"merchant\" " + + " }" + + "}"); + // Username/Password must be set if referenced above + testRunner.setProperty(GetSOAP.USER_NAME, "PUT_YOUR_USER_NAME_HERE"); + testRunner.setProperty(GetSOAP.PASSWORD, "PUT_YOUR_PASSWORD_HERE"); + testRunner.setProperty("sDateType", "transaction"); + // Make sure that dynamic properties can evaluate expression language + testRunner.setProperty("dStartDate", "${now():toNumber():minus(86400000):format(\"yyyy-MM-dd'T'HH:mm:ss\")}"); + testRunner.setProperty("dEndDate", "${now():format(\"yyyy-MM-dd'T'HH:mm:ss\")}"); + + testRunner.enqueue(""); + testRunner.run(); + + testRunner.assertAllFlowFilesTransferred(GetSOAP.REL_SUCCESS); + List flowFileList = testRunner.getFlowFilesForRelationship(GetSOAP.REL_SUCCESS); + Assert.assertNotNull(flowFileList); + for (MockFlowFile flowFile : flowFileList) { + String data = new String(testRunner.getContentAsByteArray(flowFile)); + logger.info("DATA" + data); + } + } +} diff --git a/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/test/java/org/apache/nifi/processors/soap/GetSOAPTest.java b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/test/java/org/apache/nifi/processors/soap/GetSOAPTest.java new file mode 100644 index 000000000000..ab15db624cbe --- /dev/null +++ b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/test/java/org/apache/nifi/processors/soap/GetSOAPTest.java @@ -0,0 +1,283 @@ +/* + * 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.nifi.processors.soap; + +import org.apache.axiom.om.OMAbstractFactory; +import org.apache.axiom.om.OMElement; +import org.apache.axiom.om.OMFactory; +import org.apache.axiom.om.OMNamespace; +import org.apache.axiom.om.impl.common.OMNamespaceImpl; +import org.apache.axiom.om.impl.llom.OMElementImpl; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.util.MockFlowFile; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.HttpCallback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.apache.nifi.processors.soap.GetSOAP.REL_SUCCESS; +import static org.mockserver.integration.ClientAndServer.startClientAndServer; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; + +public class GetSOAPTest { + + private static final Logger logger = LoggerFactory.getLogger(GetSOAPTest.class); + + private TestRunner testRunner; + + private static ClientAndServer mockServer; + + @BeforeClass + public static void setup() { + mockServer = startClientAndServer(1080); + + final String xmlBody = "\n" + + "\n" + + " \n" + + " \n" + + " <?xml version='1.0'?><dwml version='1.0' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:noNamespaceSchemaLocation='http://graphical.weather.gov/xml/DWMLgen/schema/DWML.xsd'><latLonList>35.9153,-79.0838</latLonList></dwml>\n" + + " \n" + + " \n" + + ""; + + mockServer.when(request().withMethod("POST").withPath("/test_path")) + .respond(response().withBody(xmlBody)); + + // This callback will verify the request is valid XML + mockServer.when(request().withMethod("POST").withPath("/test_path_callback")) + .callback(HttpCallback.callback() + .withCallbackClass(VerifyRequestCallback.class.getName())); + } + + @AfterClass + public static void tearDown() { + mockServer.stop(); + } + + @Before + public void init() { + testRunner = TestRunners.newTestRunner(GetSOAP.class); + } + + @After + public void after() { + testRunner.shutdown(); + } + + /** + * Verify that invalid json for the auth header throws an AssertionError + */ + @Test(expected = AssertionError.class) + public void testInvalidHeader() { + + testRunner.setProperty(GetSOAP.ENDPOINT_URL, "http://localhost:1080/test_path"); + testRunner.setProperty(GetSOAP.WSDL_URL, "http://localhost:1080/test_path.wsdl"); + testRunner.setProperty(GetSOAP.METHOD_NAME, "testMethod"); + testRunner.setProperty(GetSOAP.AUTH_BY_HEADER, "{ invalid_json: \"\""); + testRunner.setProperty(GetSOAP.API_NAMESPACE, "http://localhost:1080/"); + + testRunner.enqueue(""); + testRunner.run(); + } + + /** + * This test verifies a method name set using expression language works. + */ + @Test + public void testExpressionMethodName() { + + testRunner.setProperty(GetSOAP.ENDPOINT_URL, "http://localhost:1080/test_path_callback"); + testRunner.setProperty(GetSOAP.WSDL_URL, "http://localhost:1080/test_path_callback.wsdl"); + testRunner.setProperty(GetSOAP.METHOD_NAME, "${method_name}"); + testRunner.setProperty(GetSOAP.API_NAMESPACE, "http://localhost:1080/"); + + Map attributes = new HashMap<>(); + attributes.put("method_name", "getTransactionList"); + + // Here we are specifying the method name in an attribute of the flow file. It should succeed. + testRunner.enqueue("", attributes); + testRunner.run(); + + testRunner.assertAllFlowFilesTransferred(REL_SUCCESS, 1); + List flowFileList = testRunner.getFlowFilesForRelationship(REL_SUCCESS); + assert (null != flowFileList); + } + + @Test(expected = AssertionError.class) + public void testInvalidMethodName() { + + testRunner.setProperty(GetSOAP.ENDPOINT_URL, "http://localhost:1080/test_path_callback"); + testRunner.setProperty(GetSOAP.WSDL_URL, "http://localhost:1080/test_path_callback.wsdl"); + testRunner.setProperty(GetSOAP.METHOD_NAME, "${method_name}"); + testRunner.setProperty(GetSOAP.API_NAMESPACE, "http://localhost:1080/"); + + // Verify that if we don't specify the method name that an error is thrown. + testRunner.enqueue(""); + testRunner.run(); + } + + @Test + public void testValidHeaderAuth() { + + testRunner.setProperty(GetSOAP.USER_NAME, "PUT_YOUR_USER_NAME_HERE"); + testRunner.setProperty(GetSOAP.PASSWORD, "PUT_YOUR_PASSWORD_HERE"); + testRunner.setProperty(GetSOAP.AUTH_BY_HEADER, "{\n" + + " \"UserAuthentication\": {\n" + + " \"iId\": \"${Username}\",\n" + + " \"sPassword\": \"${Password}\",\n" + + " \"sType\": \"merchant\"\n" + + " }\n" + + "}"); + + testRunner.setProperty(GetSOAP.ENDPOINT_URL, "http://localhost:1080/test_path"); + testRunner.setProperty(GetSOAP.WSDL_URL, "http://localhost:1080/test_path.wsdl"); + testRunner.setProperty(GetSOAP.METHOD_NAME, "testMethod"); + testRunner.setProperty(GetSOAP.API_NAMESPACE, "http://localhost:1080/"); + + testRunner.enqueue(""); + testRunner.run(); + + testRunner.assertAllFlowFilesTransferred(REL_SUCCESS, 1); + List flowFileList = testRunner.getFlowFilesForRelationship(REL_SUCCESS); + assert (null != flowFileList); + } + + @Test + public void testHTTPUsernamePasswordProcessor() throws IOException { + + testRunner.setProperty(GetSOAP.ENDPOINT_URL, "http://localhost:1080/test_path"); + testRunner.setProperty(GetSOAP.WSDL_URL, "http://localhost:1080/test_path.wsdl"); + testRunner.setProperty(GetSOAP.METHOD_NAME, "testMethod"); + testRunner.setProperty(GetSOAP.API_NAMESPACE, "http://localhost:1080/"); + testRunner.setProperty(GetSOAP.SKIP_FIRST_ELEMENT, "true"); + + testRunner.enqueue(""); + testRunner.run(); + + testRunner.assertAllFlowFilesTransferred(REL_SUCCESS, 1); + List flowFileList = testRunner.getFlowFilesForRelationship(REL_SUCCESS); + assert (null != flowFileList); + + final String expectedBody = "35.9153,-79.0838"; + String contents = new String(testRunner.getContentAsByteArray(flowFileList.get(0))); + logger.debug("Contents: {}", contents); + flowFileList.get(0).assertContentEquals(expectedBody.getBytes()); + + } + + @Test + public void testHTTPWithUsernamePasswordProcessor() throws IOException { + + testRunner.setProperty(GetSOAP.ENDPOINT_URL, "http://localhost:1080/test_path"); + testRunner.setProperty(GetSOAP.WSDL_URL, "http://localhost:1080/test_path.wsdl"); + testRunner.setProperty(GetSOAP.METHOD_NAME, "testMethod"); + testRunner.setProperty(GetSOAP.USER_NAME, "username"); + testRunner.setProperty(GetSOAP.PASSWORD, "password"); + testRunner.setProperty(GetSOAP.API_NAMESPACE, "http://localhost:1080/"); + testRunner.setProperty(GetSOAP.SKIP_FIRST_ELEMENT, "true"); + + testRunner.enqueue(""); + testRunner.run(); + + testRunner.assertAllFlowFilesTransferred(REL_SUCCESS, 1); + List flowFileList = testRunner.getFlowFilesForRelationship(REL_SUCCESS); + assert (null != flowFileList); + + final String expectedBody = "35.9153,-79.0838"; + flowFileList.get(0).assertContentEquals(expectedBody.getBytes()); + + } + + @Test + public void testRelationships() { + GetSOAP getSOAP = new GetSOAP(); + Set relationshipSet = getSOAP.getRelationships(); + assert (null != relationshipSet); + assert (1 == relationshipSet.size()); + assert (0 == relationshipSet.iterator().next().compareTo(REL_SUCCESS)); + } + + @Test + public void testGetSoapMethod() { + final String namespaceUrl = "http://localhost.com/stockquote.wsdl"; + final String namespacePrefix = "nifi"; + final String localName = "testMethod"; + OMElement expectedElement = new OMElementImpl(); + expectedElement.setNamespace(new OMNamespaceImpl(namespaceUrl, namespacePrefix)); + expectedElement.setLocalName(localName); + + OMFactory fac = OMAbstractFactory.getOMFactory(); + OMNamespace omNamespace = fac.createOMNamespace(namespaceUrl, namespacePrefix); + + GetSOAP getSOAP = new GetSOAP(); + OMElement element = getSOAP.getSoapMethod(fac, omNamespace, "testMethod"); + assert (null != element); + assert (namespaceUrl.contentEquals(element.getNamespaceURI())); + assert (localName.contentEquals(element.getLocalName())); + } + + @Test + public void testAddArguments() { + final String namespaceUrl = "http://localhost.com/stockquote.wsdl"; + final String namespacePrefix = "nifi"; + final String localName = "testMethod"; + OMFactory fac = OMAbstractFactory.getOMFactory(); + OMNamespace omNamespace = fac.createOMNamespace(namespaceUrl, namespacePrefix); + OMElement expectedElement = new OMElementImpl(); + expectedElement.setNamespace(new OMNamespaceImpl(namespaceUrl, namespacePrefix)); + expectedElement.setLocalName(localName); + + PropertyDescriptor arg1 = new PropertyDescriptor + .Builder() + .name("Argument1") + .defaultValue("60000") + .description("The timeout value to use waiting to establish a connection to the web service") + .dynamic(true) + .expressionLanguageSupported(false) + .build(); + + testRunner.setProperty(arg1, "111"); + + GetSOAP getSOAP = new GetSOAP(); + getSOAP.addArgumentsToMethod(testRunner.getProcessContext(), + fac, + omNamespace, + expectedElement, + new MockFlowFile(1)); + Iterator childItr = expectedElement.getChildElements(); + assert (null != childItr); + assert (childItr.hasNext()); + assert (arg1.getName().contentEquals(childItr.next().getLocalName())); + assert (!childItr.hasNext()); + } +} diff --git a/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/test/java/org/apache/nifi/processors/soap/VerifyRequestCallback.java b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/test/java/org/apache/nifi/processors/soap/VerifyRequestCallback.java new file mode 100644 index 000000000000..d2973890748d --- /dev/null +++ b/nifi-nar-bundles/nifi-soap-bundle/nifi-soap-processors/src/test/java/org/apache/nifi/processors/soap/VerifyRequestCallback.java @@ -0,0 +1,66 @@ +/* + * 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.nifi.processors.soap; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import org.mockserver.mock.action.ExpectationCallback; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Map; + +/** + * This callback is used to verify that the request sent to the soap server is valid XML. + */ +public class VerifyRequestCallback implements ExpectationCallback { + + private static final Logger logger = LoggerFactory.getLogger(VerifyRequestCallback.class); + + private final String xmlBody = + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + public VerifyRequestCallback() { + } + + @Override + public HttpResponse handle(HttpRequest httpRequest) { + String soapRequest = httpRequest.getBodyAsString(); + logger.info("PATH: {}", httpRequest.getPath().getValue()); + XmlMapper mapper = new XmlMapper(); + try { + mapper.readValue(soapRequest, new TypeReference>() { + }); + } + catch (IOException e) { + logger.error("REQUEST: " + soapRequest); + logger.error(e.getMessage(), e); + return HttpResponse.notFoundResponse(); + } + return HttpResponse.response(xmlBody); + } +} diff --git a/nifi-nar-bundles/nifi-soap-bundle/pom.xml b/nifi-nar-bundles/nifi-soap-bundle/pom.xml new file mode 100644 index 000000000000..407c240668ec --- /dev/null +++ b/nifi-nar-bundles/nifi-soap-bundle/pom.xml @@ -0,0 +1,34 @@ + + + + 4.0.0 + + + org.apache.nifi + nifi-nar-bundles + 1.3.0-SNAPSHOT + + + org.apache.nifi + nifi-soap-bundle + pom + + + nifi-soap-processors + nifi-soap-nar + + + diff --git a/nifi-nar-bundles/pom.xml b/nifi-nar-bundles/pom.xml index a2887b8385da..59b3582bc6cf 100755 --- a/nifi-nar-bundles/pom.xml +++ b/nifi-nar-bundles/pom.xml @@ -83,6 +83,7 @@ nifi-cybersecurity-bundle nifi-parquet-bundle nifi-extension-utils + nifi-soap-bundle