You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@nifi.apache.org by "mark-bathori (via GitHub)" <gi...@apache.org> on 2023/08/24 16:44:27 UTC

[GitHub] [nifi] mark-bathori opened a new pull request, #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

mark-bathori opened a new pull request, #7644:
URL: https://github.com/apache/nifi/pull/7644

   <!-- 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. -->
   
   # Summary
   
   [NIFI-11992](https://issues.apache.org/jira/browse/NIFI-11992)
   
   # Tracking
   
   Please complete the following tracking steps prior to pull request creation.
   
   ### Issue Tracking
   
   - [x] [Apache NiFi Jira](https://issues.apache.org/jira/browse/NIFI) issue created
   
   ### Pull Request Tracking
   
   - [ ] Pull Request title starts with Apache NiFi Jira issue number, such as `NIFI-00000`
   - [ ] Pull Request commit message starts with Apache NiFi Jira issue number, as such `NIFI-00000`
   
   ### Pull Request Formatting
   
   - [ ] Pull Request based on current revision of the `main` branch
   - [ ] Pull Request refers to a feature branch with one commit containing changes
   
   # Verification
   
   Please indicate the verification steps performed prior to pull request creation.
   
   ### Build
   
   - [x] Build completed using `mvn clean install -P contrib-check`
     - [x] JDK 17
   
   ### Licensing
   
   - [ ] New dependencies are compatible with the [Apache License 2.0](https://apache.org/licenses/LICENSE-2.0) according to the [License Policy](https://www.apache.org/legal/resolved.html)
   - [ ] New dependencies are documented in applicable `LICENSE` and `NOTICE` files
   
   ### Documentation
   
   - [ ] Documentation formatting appears as expected in rendered files
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1308044680


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/test/java/org/apache/nifi/commons/zendesk/ZendeskRecordPathUtilsTest.java:
##########
@@ -0,0 +1,99 @@
+/*
+ * 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.commons.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.serialization.SimpleRecordSchema;
+import org.apache.nifi.serialization.record.MapRecord;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.RecordSchema;
+import org.apache.nifi.serialization.record.type.ArrayDataType;
+import org.apache.nifi.serialization.record.type.RecordDataType;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class ZendeskRecordPathUtilsTest {
+
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    @Test
+    public void testFieldValueEvaluation() {
+        ObjectNode testNode;
+        Record record = initRecord();
+
+        testNode = mapper.createObjectNode();
+        resolveFieldValue("/a/b", "%{/field1}", testNode, record);
+        assertEquals("{\"a\":{\"b\":\"value1\"}}", testNode.toString());
+
+        testNode = mapper.createObjectNode();
+        resolveFieldValue("/a/b/c", "constant", testNode, record);
+        assertEquals("{\"a\":{\"b\":{\"c\":\"constant\"}}}", testNode.toString());
+
+        ProcessException e1 = assertThrows(ProcessException.class, () -> resolveFieldValue("/a", "%{/field2}", mapper.createObjectNode(), record));
+        assertEquals(e1.getMessage(), "The provided RecordPath [/field2] points to a [ARRAY] type value");
+
+        ProcessException e2 = assertThrows(ProcessException.class, () -> resolveFieldValue("/a", "%{/field3}", mapper.createObjectNode(), record));
+        assertEquals(e2.getMessage(), "The provided RecordPath [/field3] points to a [RECORD] type value");
+
+        ProcessException e3 = assertThrows(ProcessException.class, () -> resolveFieldValue("/a", "%{/field4}", mapper.createObjectNode(), record));
+        assertEquals(e3.getMessage(), "The provided RecordPath [/field4] points to a [CHOICE] type value with Record subtype");
+    }
+
+    private Record initRecord() {

Review Comment:
   Minor: Most of these values can be extracted into a static block.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] turcsanyip commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "turcsanyip (via GitHub)" <gi...@apache.org>.
turcsanyip commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1330557343


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-common/src/main/java/org/apache/nifi/common/zendesk/ZendeskProperties.java:
##########
@@ -0,0 +1,92 @@
+/*
+ * 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.common.zendesk;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+public final class ZendeskProperties {
+
+    public static final String WEB_CLIENT_SERVICE_PROVIDER_NAME = "web-client-service-provider";
+    public static final String ZENDESK_SUBDOMAIN_NAME = "zendesk-subdomain";
+    public static final String ZENDESK_USER_NAME = "zendesk-user";
+    public static final String ZENDESK_AUTHENTICATION_TYPE_NAME = "zendesk-authentication-type-name";
+    public static final String ZENDESK_AUTHENTICATION_CREDENTIAL_NAME = "zendesk-authentication-value-name";
+    public static final String ZENDESK_TICKET_COMMENT_BODY_NAME = "zendesk-comment-body";
+    public static final String ZENDESK_TICKET_SUBJECT_NAME = "zendesk-subject";
+    public static final String ZENDESK_TICKET_PRIORITY_NAME = "zendesk-priority";
+    public static final String ZENDESK_TICKET_TYPE_NAME = "zendesk-type";
+    public static final String HTTPS = "https";
+    public static final String APPLICATION_JSON = "application/json";
+    public static final String ZENDESK_HOST_TEMPLATE = "%s.zendesk.com";
+    public static final String ZENDESK_CREATE_TICKET_RESOURCE = "/api/v2/tickets";
+    public static final String ZENDESK_CREATE_TICKETS_RESOURCE = "/api/v2/tickets/create_many";
+
+    public static final String ZENDESK_TICKET_ROOT_NODE = "/ticket";
+    public static final String ZENDESK_TICKETS_ROOT_NODE = "/tickets";
+
+    public static final String REL_SUCCESS_NAME = "success";
+    public static final String REL_FAILURE_NAME = "failure";
+
+    private ZendeskProperties() {}
+
+    public static final PropertyDescriptor WEB_CLIENT_SERVICE_PROVIDER = new PropertyDescriptor.Builder()
+            .name(WEB_CLIENT_SERVICE_PROVIDER_NAME)
+            .displayName("Web Client Service Provider")
+            .description("Controller service for HTTP client operations.")
+            .identifiesControllerService(WebClientServiceProvider.class)
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_SUBDOMAIN = new PropertyDescriptor.Builder()
+            .name(ZENDESK_SUBDOMAIN_NAME)
+            .displayName("Zendesk Subdomain Name")

Review Comment:
   I know these are not new properties but I would suggest removing the "Zendesk" prefix from the display name. Typically we don't add prefix because the context is already given by the processor and all properties would start with the same tag.



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/processors/zendesk/PutZendeskTicket.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.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+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.common.zendesk.validation.JsonPointerPropertyNameValidator;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.MalformedRecordException;
+import org.apache.nifi.serialization.RecordReader;
+import org.apache.nifi.serialization.RecordReaderFactory;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.nifi.common.zendesk.ZendeskProperties.APPLICATION_JSON;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.REL_FAILURE_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.common.zendesk.util.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.common.zendesk.util.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.getResponseBody;
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+import static org.apache.nifi.flowfile.attributes.CoreAttributes.MIME_TYPE;
+import static org.apache.nifi.processors.zendesk.AbstractZendesk.RECORD_COUNT_ATTRIBUTE_NAME;
+import static org.apache.nifi.processors.zendesk.PutZendeskTicket.ERROR_CODE_ATTRIBUTE_NAME;
+import static org.apache.nifi.processors.zendesk.PutZendeskTicket.ERROR_MESSAGE_ATTRIBUTE_NAME;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.CREATED;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.OK;
+
+@Tags({"zendesk, ticket"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API.")
+@DynamicProperty(
+        name = "The path in the request object to add. The value needs be a valid JsonPointer.",
+        value = "The path in the incoming record to get the value from.",
+        expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
+        description = "Additional property to be added to the Zendesk request object.")
+@WritesAttributes({
+        @WritesAttribute(attribute = RECORD_COUNT_ATTRIBUTE_NAME, description = "The number of records processed."),
+        @WritesAttribute(attribute = ERROR_CODE_ATTRIBUTE_NAME, description = "The error code of from the response."),
+        @WritesAttribute(attribute = ERROR_MESSAGE_ATTRIBUTE_NAME, description = "The error message of from the response.")})
+public class PutZendeskTicket extends AbstractZendesk {
+
+    static final String ZENDESK_RECORD_READER_NAME = "zendesk-record-reader";
+    static final String ERROR_CODE_ATTRIBUTE_NAME = "error.code";
+    static final String ERROR_MESSAGE_ATTRIBUTE_NAME = "error.message";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    static final PropertyDescriptor RECORD_READER = new PropertyDescriptor.Builder()
+            .name(ZENDESK_RECORD_READER_NAME)
+            .displayName("Record Reader")
+            .description("Specifies the Controller Service to use for parsing incoming data and determining the data's schema.")
+            .identifiesControllerService(RecordReaderFactory.class)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_COMMENT_BODY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_COMMENT_BODY_NAME)
+            .displayName("Comment Body")
+            .description("The content or the path to the comment body in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_SUBJECT = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_SUBJECT_NAME)
+            .displayName("Subject")
+            .description("The content or the path to the subject in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_PRIORITY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_PRIORITY_NAME)
+            .displayName("Priority")
+            .description("The content or the path to the priority in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_TYPE = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_TYPE_NAME)
+            .displayName("Type")
+            .description("The content or the path to the type in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+            RECORD_READER,

Review Comment:
   Please move `RECORD_READER` down, just before its dependent properties (`ZENDESK_COMMENT_BODY`).



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-services-nar/src/main/resources/META-INF/NOTICE:
##########
@@ -0,0 +1,39 @@
+nifi-zendesk-nar
+Copyright 2015-2022 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 Commons IO
+    The following NOTICE information applies:
+      Apache Commons IO
+      Copyright 2002-2017 The Apache Software Foundation

Review Comment:
   I cannot see `Commons IO` in the nar but other jars are present that are not mentioned in the NOTICE.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1305733343


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/commons/zendesk/ZendeskProperties.java:
##########
@@ -0,0 +1,97 @@
+/*
+ * 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.commons.zendesk;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.processors.zendesk.ZendeskAuthenticationType;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+import static org.apache.nifi.processor.util.StandardValidators.NON_BLANK_VALIDATOR;
+import static org.apache.nifi.processor.util.StandardValidators.NON_EMPTY_VALIDATOR;
+
+public class ZendeskProperties {

Review Comment:
   Please make this class final and add a private constructor to hide the public implicit one.



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/commons/zendesk/ZendeskUtils.java:
##########
@@ -0,0 +1,134 @@
+/*
+ * 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.commons.zendesk;
+
+import com.fasterxml.jackson.core.JsonPointer;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.context.PropertyContext;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.OptionalLong;
+import java.util.stream.Collectors;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Base64.getEncoder;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.AUTHORIZATION_HEADER_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.BASIC_AUTH_PREFIX;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.CONTENT_TYPE_APPLICATION_JSON;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.CONTENT_TYPE_HEADER_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKETS_ROOT_NODE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_ROOT_NODE;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.addNode;
+
+public class ZendeskUtils {

Review Comment:
   Please make this class final and add a private constructor.



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/resources/docs.org.apache.nifi.processors.zendesk.PutZendeskTicket/additionalDetails.html:
##########
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html lang="en" xmlns="http://www.w3.org/1999/html">
+<!--
+      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.
+    -->
+
+    <head>
+        <meta charset="utf-8"/>
+        <title>GetZendesk</title>
+        <link rel="stylesheet" href="../../../../../css/component-usage.css" type="text/css"/>
+        <style>
+            h2 {margin-top: 4em}
+            h3 {margin-top: 3em}
+            td {text-align: left}
+        </style>
+    </head>
+
+    <body>
+        <h1>PutZendeskTicket</h1>
+
+        <h3>Description</h3>
+
+        <p>
+            The processor uses the Zendesk API to ingest tickets into Zendesk. The processor is capable to send requests directly from the flowFile content or construct the request objects from the incoming records using RecordReader.
+        </p>
+
+        <h3>Authentication</h3>
+
+        <p>
+            Zendesk API uses basic authentication. Either a password or an authentication token have to be provided.
+            Authentication token can be created in Zendesk API Settings, so the users don't have to expose their passwords,
+            and also auth tokens can be revoked quickly if necessary.
+        </p>
+
+        <h3>Property values</h3>
+
+        <p>
+            There are multiple ways of providing property values to the request object:
+            <dl>
+            <dt>Record Path:</dt>
+                <dd>
+                    The property value is going to be evaluated as a record path if the value is provided inside brackets starting with a '%'. <br>
+                    e.g.: if the incoming record is <code>{"foo":{"bar":"example value"}}</code> then the <code>%{/foo/bar}</code> will get the value of "example value" from the record.
+                </dd>
+            <dt>Constant:</dt>
+                <dd>
+                    The property value is going to be treated as a constant if the provided value doesn't match with the record path format. <br>
+                    e.g.: if the property value is <code>"constant value"</code> then the property value in the request object will be "constant value"
+                </dd>
+            </dl>
+        </p>
+
+        <h3>Additional properties</h3>
+
+        <p>
+            The processor provides some commonly used Zendesk ticket attribute in its property list but any number of additional property can be added with dynamic properties.
+            The dynamic property key is treated as a record path, and it represents the path in the request object. The dynamic property value is the same as the predefined property attributes.

Review Comment:
   ```suggestion
               The processor offers a set of frequently used Zendesk ticket attributes within its property list. However, users have the flexibility to include any desired number of additional properties using dynamic properties. 
               
   These dynamic properties utilize their keys as record paths, which denote the paths within the request object. Correspondingly, the values of these dynamic properties align with the predefined property attributes.
   ```
   
   Or something like this



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/commons/zendesk/ZendeskUtils.java:
##########
@@ -0,0 +1,134 @@
+/*
+ * 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.commons.zendesk;
+
+import com.fasterxml.jackson.core.JsonPointer;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.context.PropertyContext;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.OptionalLong;
+import java.util.stream.Collectors;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Base64.getEncoder;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.AUTHORIZATION_HEADER_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.BASIC_AUTH_PREFIX;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.CONTENT_TYPE_APPLICATION_JSON;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.CONTENT_TYPE_HEADER_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKETS_ROOT_NODE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_ROOT_NODE;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.addNode;
+
+public class ZendeskUtils {
+
+    /**
+     * Constructs the header for the Zendesk API call
+     * @param authenticationContext authentication data holder object
+     * @return the constructed header
+     */
+    public static String basicAuthHeaderValue(ZendeskAuthenticationContext authenticationContext) {
+        final String user = authenticationContext.getAuthenticationType().enrichUserName(authenticationContext.getUser());
+        final String userWithPassword = user + ":" + authenticationContext.getAuthenticationCredentials();
+        return BASIC_AUTH_PREFIX + getEncoder().encodeToString(userWithPassword.getBytes());
+    }
+
+    public static String responseBodyToString(HttpResponseEntity response) {
+        try {
+            return IOUtils.toString(response.body(), UTF_8);
+        } catch (IOException e) {
+            throw new UncheckedIOException("Reading response body has failed", e);
+        }
+    }
+
+    /**
+     * Collects every non-blank dynamic property from the context.
+     * @param context property context
+     * @param properties property list
+     * @return list of dynamic properties

Review Comment:
   Doc typo: The return type is map



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/commons/zendesk/ZendeskRecordPathUtils.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * 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.commons.zendesk;
+
+import com.fasterxml.jackson.core.JsonPointer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.RecordPath;
+import org.apache.nifi.record.path.RecordPathResult;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.type.ChoiceDataType;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.lang.String.format;
+import static java.util.stream.Collectors.toList;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKETS_ROOT_NODE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_ROOT_NODE;
+
+public class ZendeskRecordPathUtils {
+
+    private static final String NULL_VALUE = "null";
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final Pattern RECORD_PATH_PATTERN = Pattern.compile("^%\\{(.*?)\\}$");
+
+    /**
+     * Resolves the input field value to be handled as a record path or a constant value.
+     * @param path path in the request object
+     * @param value field value to be resolved
+     * @param baseTicketNode base request object where the field value will be added
+     * @param record record to receive the value from if the field value is a record path
+     */
+    public static void resolveFieldValue(String path, String value, ObjectNode baseTicketNode, Record record) {
+        final Matcher matcher = RECORD_PATH_PATTERN.matcher(value);
+        if (matcher.matches()) {
+            addNode(baseTicketNode, JsonPointer.compile(path), new TextNode(resolveRecordState(matcher.group(1), record)));
+        } else {
+            addNode(baseTicketNode, JsonPointer.compile(path), new TextNode(value));
+        }
+    }
+
+    /**
+     * Adds a user defined dynamic field to the request object. If the user specifies the path in the request object as full path (starting with '/ticket' or '/tickets')
+     * the method removes them since the root path is specified later based on the number of records.
+     * @param path path in the request object
+     * @param value dynamic field value
+     * @param baseTicketNode base request object where the field value will be added
+     * @param record record to receive the value from if the field value is a record path
+     */
+    public static void addDynamicField(String path, String value, ObjectNode baseTicketNode, Record record) {
+        if (path.startsWith(ZENDESK_TICKET_ROOT_NODE)) {
+            path = path.substring(7);
+        } else if (path.startsWith(ZENDESK_TICKETS_ROOT_NODE)) {
+            path = path.substring(8);
+        }
+
+        resolveFieldValue(path, value, baseTicketNode, record);
+    }
+
+    private static String resolveRecordState(String pathValue, final Record record) {
+        final RecordPath recordPath = RecordPath.compile(pathValue);
+        final RecordPathResult result = recordPath.evaluate(record);
+        final List<FieldValue> fieldValues = result.getSelectedFields().collect(toList());
+        final FieldValue fieldValue = getMatchingFieldValue(recordPath, fieldValues);
+
+        if (fieldValue.getValue() == null || fieldValue.getValue() == NULL_VALUE) {
+            return null;
+        }
+
+        return getFieldValue(recordPath, fieldValue);
+    }
+
+    /**
+     * The method checks the field's type and filters out every non-compatible type.
+     * @param recordPath path to the requested field
+     * @param fieldValue record field
+     * @return value of the record field
+     */
+    private static String getFieldValue(final RecordPath recordPath, FieldValue fieldValue) {
+        final RecordFieldType fieldType = fieldValue.getField().getDataType().getFieldType();
+
+        if (fieldType == RecordFieldType.RECORD || fieldType == RecordFieldType.ARRAY || fieldType == RecordFieldType.MAP) {
+            throw new ProcessException(format("The provided RecordPath [%s] points to a [%s] type value", recordPath, fieldType));
+        }
+
+        if (fieldType == RecordFieldType.CHOICE) {
+            final ChoiceDataType choiceDataType = (ChoiceDataType) fieldValue.getField().getDataType();
+            final List<DataType> possibleTypes = choiceDataType.getPossibleSubTypes();
+            if (possibleTypes.stream().anyMatch(type -> type.getFieldType() == RecordFieldType.RECORD)) {
+                throw new ProcessException(format("The provided RecordPath [%s] points to a [CHOICE] type value with Record subtype", recordPath));
+            }
+        }
+
+        return String.valueOf(fieldValue.getValue());
+    }
+
+    /**
+     * The method checks if only one result were received for the give record path.
+     * @param recordPath path to the requested field
+     * @param resultList result list
+     * @return matching field
+     */
+    private static FieldValue getMatchingFieldValue(final RecordPath recordPath, final List<FieldValue> resultList) {
+        if (resultList.isEmpty()) {
+            throw new ProcessException(format("Evaluated RecordPath [%s] against Record but got no results", recordPath));
+        }
+
+        if (resultList.size() > 1) {
+            throw new ProcessException(format("Evaluated RecordPath [%s] against Record and received multiple distinct results [%s]", recordPath, resultList));
+        }
+
+        return resultList.get(0);
+    }
+
+    /**
+     * Adds a new node on the provided path with the give value to the request object.
+     * @param baseNode base object where the new node will be added
+     * @param path path of the new node
+     * @param value value of the new node
+     */
+    public static void addNode(ObjectNode baseNode, JsonPointer path, JsonNode value) {

Review Comment:
   I think `addNodeAtPathWithJsonValue` or similar would be a more descriptive name.



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/commons/zendesk/ZendeskAuthenticationContext.java:
##########
@@ -0,0 +1,56 @@
+/*
+ * 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.commons.zendesk;
+
+import org.apache.nifi.context.PropertyContext;
+import org.apache.nifi.processors.zendesk.ZendeskAuthenticationType;
+
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_USER;
+
+public class ZendeskAuthenticationContext {
+
+    private final String subDomain;
+    private final String user;
+    private final ZendeskAuthenticationType authenticationType;
+    private final String authenticationCredentials;
+
+    public ZendeskAuthenticationContext(PropertyContext context) {

Review Comment:
   I'd avoid passing PropertyContext directly to this class. It introduces a tight coupling between your class and NiFi's specific classes which makes the code less modular and harder to test. Passing specific properties or configuration values is generally considered a better practice.



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/commons/zendesk/ZendeskRecordPathUtils.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * 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.commons.zendesk;
+
+import com.fasterxml.jackson.core.JsonPointer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.RecordPath;
+import org.apache.nifi.record.path.RecordPathResult;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.type.ChoiceDataType;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.lang.String.format;
+import static java.util.stream.Collectors.toList;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKETS_ROOT_NODE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_ROOT_NODE;
+
+public class ZendeskRecordPathUtils {
+
+    private static final String NULL_VALUE = "null";
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final Pattern RECORD_PATH_PATTERN = Pattern.compile("^%\\{(.*?)\\}$");

Review Comment:
   The matches() method inherently performs a full-string match, so the anchors are not necessary in this context. You can simplify the regex pattern to: `%\\{(.*?)\\}`



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/services/zendesk/BaseSinkZendeskTicket.java:
##########
@@ -0,0 +1,55 @@
+/*
+ * 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.services.zendesk;
+
+import org.apache.nifi.context.PropertyContext;
+
+import static org.apache.nifi.services.zendesk.ZendeskRecordSink.ZENDESK_TICKET_COMMENT_BODY;
+import static org.apache.nifi.services.zendesk.ZendeskRecordSink.ZENDESK_TICKET_PRIORITY;
+import static org.apache.nifi.services.zendesk.ZendeskRecordSink.ZENDESK_TICKET_SUBJECT;
+import static org.apache.nifi.services.zendesk.ZendeskRecordSink.ZENDESK_TICKET_TYPE;
+
+public class BaseSinkZendeskTicket {
+
+    private final String commentBody;
+    private final String subject;
+    private final String priority;
+    private final String type;
+
+    public BaseSinkZendeskTicket(PropertyContext context) {

Review Comment:
   I'd only pass the properties here for the same reason I mentioned in ZendeskAuthenticationContext.



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/processors/zendesk/PutZendeskTicket.java:
##########
@@ -0,0 +1,294 @@
+/*
+ * 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.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.commons.zendesk.ZendeskAuthenticationContext;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.MalformedRecordException;
+import org.apache.nifi.serialization.RecordReader;
+import org.apache.nifi.serialization.RecordReaderFactory;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.String.format;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.HTTPS;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_FAILURE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_SUCCESS_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_HOST_TEMPLATE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.performPostRequest;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.responseBodyToString;
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.CREATED;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.OK;
+
+@Tags({"zendesk, ticket"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API.")
+@DynamicProperty(
+        name = "The path in the request object to add.",
+        value = "The path in the incoming record to get the value from.",
+        expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
+        description = "Additional property to be added to the Zendesk request object.")
+public class PutZendeskTicket extends AbstractProcessor {
+
+    static final String ZENDESK_RECORD_READER_NAME = "zendesk-record-reader";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    static final PropertyDescriptor RECORD_READER = new PropertyDescriptor.Builder()
+            .name(ZENDESK_RECORD_READER_NAME)
+            .displayName("Record Reader")
+            .description("Specifies the Controller Service to use for parsing incoming data and determining the data's schema.")
+            .identifiesControllerService(RecordReaderFactory.class)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_COMMENT_BODY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_COMMENT_BODY_NAME)
+            .displayName("Comment Body")
+            .description("The content or the path to the comment body in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_SUBJECT = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_SUBJECT_NAME)
+            .displayName("Subject")
+            .description("The content or the path to the subject in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_PRIORITY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_PRIORITY_NAME)
+            .displayName("Priority")
+            .description("The content or the path to the priority in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_TYPE = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_TYPE_NAME)
+            .displayName("Type")
+            .description("The content or the path to the type in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+            RECORD_READER,
+            WEB_CLIENT_SERVICE_PROVIDER,
+            ZENDESK_SUBDOMAIN,
+            ZENDESK_USER,
+            ZENDESK_AUTHENTICATION_TYPE,
+            ZENDESK_AUTHENTICATION_CREDENTIAL,
+            ZENDESK_TICKET_COMMENT_BODY,
+            ZENDESK_TICKET_SUBJECT,
+            ZENDESK_TICKET_PRIORITY,
+            ZENDESK_TICKET_TYPE
+    ));
+
+    private volatile WebClientServiceProvider webClientServiceProvider;
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name(REL_SUCCESS_NAME)
+            .description("For FlowFiles created as a result of a successful HTTP request.")
+            .build();
+
+    public static final Relationship REL_FAILURE = new Relationship.Builder()
+            .name(REL_FAILURE_NAME)
+            .description("A FlowFile is routed to this relationship if the operation failed and retrying the operation will also fail, such as an invalid data or schema.")
+            .build();
+
+    @Override
+    protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
+        return new PropertyDescriptor.Builder()
+                .name(propertyDescriptorName)
+                .required(false)
+                .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+                .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+                .dynamic(true)
+                .build();
+    }
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    public static final Set<Relationship> RELATIONSHIPS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+            REL_SUCCESS,
+            REL_FAILURE
+    )));
+
+    @Override
+    public List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @OnScheduled
+    public void onScheduled(ProcessContext context) {
+        webClientServiceProvider = context.getProperty(WEB_CLIENT_SERVICE_PROVIDER).asControllerService(WebClientServiceProvider.class);
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) {
+        final FlowFile flowFile = session.get();
+        if (flowFile == null) {
+            return;
+        }
+
+        long startNanos = System.nanoTime();
+        HttpResponseEntity response;
+        URI uri;
+        final RecordReaderFactory readerFactory = context.getProperty(RECORD_READER).asControllerService(RecordReaderFactory.class);
+        final ZendeskAuthenticationContext authenticationContext = new ZendeskAuthenticationContext(context);
+
+        if (readerFactory == null) {
+            try (final InputStream inputStream = session.read(flowFile)) {
+                final HttpUriBuilder uriBuilder = uriBuilder(authenticationContext.getSubDomain(), ZENDESK_CREATE_TICKET_RESOURCE);
+                uri = uriBuilder.build();
+                response = performPostRequest(authenticationContext, webClientServiceProvider, uri, inputStream);
+            } catch (IOException e) {
+                throw new ProcessException("Could not read incoming FlowFile", e);
+            }
+        } else {
+            final String commentBody = context.getProperty(ZENDESK_TICKET_COMMENT_BODY).evaluateAttributeExpressions().getValue();
+            final String subject = context.getProperty(ZENDESK_TICKET_SUBJECT).evaluateAttributeExpressions().getValue();
+            final String priority = context.getProperty(ZENDESK_TICKET_PRIORITY).evaluateAttributeExpressions().getValue();
+            final String type = context.getProperty(ZENDESK_TICKET_TYPE).evaluateAttributeExpressions().getValue();
+            final Map<String, String> dynamicProperties = getDynamicProperties(context, context.getProperties());
+            List<ObjectNode> zendeskTickets = new ArrayList<>();
+
+            try (final InputStream in = session.read(flowFile); final RecordReader reader = readerFactory.createRecordReader(flowFile, in, getLogger())) {
+                Record record;
+
+                while ((record = reader.nextRecord()) != null) {
+                    ObjectNode baseTicketNode = mapper.createObjectNode();
+                    resolveFieldValue("/comment/body", commentBody, baseTicketNode, record);
+
+                    if (subject != null) {
+                        resolveFieldValue("/subject", subject, baseTicketNode, record);
+                    }
+                    if (priority != null) {
+                        resolveFieldValue("/priority", priority, baseTicketNode, record);
+                    }
+                    if (type != null) {
+                        resolveFieldValue("/type", type, baseTicketNode, record);
+                    }
+                    for (Map.Entry<String, String> dynamicProperty : dynamicProperties.entrySet()) {
+                        addDynamicField(dynamicProperty.getKey(), dynamicProperty.getValue(), baseTicketNode, record);
+                    }
+                    zendeskTickets.add(baseTicketNode);
+                }
+
+            } catch (IOException | SchemaNotFoundException | MalformedRecordException e) {
+                getLogger().error("Error occurred while creating zendesk tickets", e);
+                session.transfer(session.penalize(flowFile), REL_FAILURE);
+                return;
+            }
+
+            if (zendeskTickets.size() == 0) {

Review Comment:
   You can use `Collection::isEmpty` here.



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/resources/docs.org.apache.nifi.processors.zendesk.PutZendeskTicket/additionalDetails.html:
##########
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html lang="en" xmlns="http://www.w3.org/1999/html">
+<!--
+      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.
+    -->
+
+    <head>
+        <meta charset="utf-8"/>
+        <title>GetZendesk</title>
+        <link rel="stylesheet" href="../../../../../css/component-usage.css" type="text/css"/>
+        <style>
+            h2 {margin-top: 4em}
+            h3 {margin-top: 3em}
+            td {text-align: left}
+        </style>
+    </head>
+
+    <body>
+        <h1>PutZendeskTicket</h1>
+
+        <h3>Description</h3>
+
+        <p>
+            The processor uses the Zendesk API to ingest tickets into Zendesk. The processor is capable to send requests directly from the flowFile content or construct the request objects from the incoming records using RecordReader.
+        </p>

Review Comment:
   ```suggestion
           <p>
               The processor uses the Zendesk API to ingest tickets into Zendesk. The processor is capable to send requests directly from the FlowFile content or construct the request objects from the incoming records using a RecordReader.
           </p>
   ```



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/processors/zendesk/PutZendeskTicket.java:
##########
@@ -0,0 +1,294 @@
+/*
+ * 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.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.commons.zendesk.ZendeskAuthenticationContext;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.MalformedRecordException;
+import org.apache.nifi.serialization.RecordReader;
+import org.apache.nifi.serialization.RecordReaderFactory;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.String.format;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.HTTPS;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_FAILURE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_SUCCESS_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_HOST_TEMPLATE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.performPostRequest;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.responseBodyToString;
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.CREATED;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.OK;
+
+@Tags({"zendesk, ticket"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API.")
+@DynamicProperty(
+        name = "The path in the request object to add.",
+        value = "The path in the incoming record to get the value from.",
+        expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
+        description = "Additional property to be added to the Zendesk request object.")
+public class PutZendeskTicket extends AbstractProcessor {
+
+    static final String ZENDESK_RECORD_READER_NAME = "zendesk-record-reader";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    static final PropertyDescriptor RECORD_READER = new PropertyDescriptor.Builder()
+            .name(ZENDESK_RECORD_READER_NAME)
+            .displayName("Record Reader")
+            .description("Specifies the Controller Service to use for parsing incoming data and determining the data's schema.")
+            .identifiesControllerService(RecordReaderFactory.class)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_COMMENT_BODY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_COMMENT_BODY_NAME)
+            .displayName("Comment Body")
+            .description("The content or the path to the comment body in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_SUBJECT = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_SUBJECT_NAME)
+            .displayName("Subject")
+            .description("The content or the path to the subject in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_PRIORITY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_PRIORITY_NAME)
+            .displayName("Priority")
+            .description("The content or the path to the priority in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_TYPE = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_TYPE_NAME)
+            .displayName("Type")
+            .description("The content or the path to the type in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+            RECORD_READER,
+            WEB_CLIENT_SERVICE_PROVIDER,
+            ZENDESK_SUBDOMAIN,
+            ZENDESK_USER,
+            ZENDESK_AUTHENTICATION_TYPE,
+            ZENDESK_AUTHENTICATION_CREDENTIAL,
+            ZENDESK_TICKET_COMMENT_BODY,
+            ZENDESK_TICKET_SUBJECT,
+            ZENDESK_TICKET_PRIORITY,
+            ZENDESK_TICKET_TYPE
+    ));
+
+    private volatile WebClientServiceProvider webClientServiceProvider;
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name(REL_SUCCESS_NAME)
+            .description("For FlowFiles created as a result of a successful HTTP request.")
+            .build();
+
+    public static final Relationship REL_FAILURE = new Relationship.Builder()
+            .name(REL_FAILURE_NAME)
+            .description("A FlowFile is routed to this relationship if the operation failed and retrying the operation will also fail, such as an invalid data or schema.")
+            .build();
+
+    @Override
+    protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
+        return new PropertyDescriptor.Builder()
+                .name(propertyDescriptorName)
+                .required(false)
+                .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+                .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+                .dynamic(true)
+                .build();
+    }
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    public static final Set<Relationship> RELATIONSHIPS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+            REL_SUCCESS,
+            REL_FAILURE
+    )));
+
+    @Override
+    public List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @OnScheduled
+    public void onScheduled(ProcessContext context) {
+        webClientServiceProvider = context.getProperty(WEB_CLIENT_SERVICE_PROVIDER).asControllerService(WebClientServiceProvider.class);
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) {
+        final FlowFile flowFile = session.get();
+        if (flowFile == null) {
+            return;
+        }
+
+        long startNanos = System.nanoTime();
+        HttpResponseEntity response;
+        URI uri;
+        final RecordReaderFactory readerFactory = context.getProperty(RECORD_READER).asControllerService(RecordReaderFactory.class);
+        final ZendeskAuthenticationContext authenticationContext = new ZendeskAuthenticationContext(context);
+
+        if (readerFactory == null) {
+            try (final InputStream inputStream = session.read(flowFile)) {
+                final HttpUriBuilder uriBuilder = uriBuilder(authenticationContext.getSubDomain(), ZENDESK_CREATE_TICKET_RESOURCE);
+                uri = uriBuilder.build();
+                response = performPostRequest(authenticationContext, webClientServiceProvider, uri, inputStream);
+            } catch (IOException e) {
+                throw new ProcessException("Could not read incoming FlowFile", e);
+            }
+        } else {
+            final String commentBody = context.getProperty(ZENDESK_TICKET_COMMENT_BODY).evaluateAttributeExpressions().getValue();
+            final String subject = context.getProperty(ZENDESK_TICKET_SUBJECT).evaluateAttributeExpressions().getValue();
+            final String priority = context.getProperty(ZENDESK_TICKET_PRIORITY).evaluateAttributeExpressions().getValue();
+            final String type = context.getProperty(ZENDESK_TICKET_TYPE).evaluateAttributeExpressions().getValue();
+            final Map<String, String> dynamicProperties = getDynamicProperties(context, context.getProperties());
+            List<ObjectNode> zendeskTickets = new ArrayList<>();
+
+            try (final InputStream in = session.read(flowFile); final RecordReader reader = readerFactory.createRecordReader(flowFile, in, getLogger())) {
+                Record record;
+
+                while ((record = reader.nextRecord()) != null) {
+                    ObjectNode baseTicketNode = mapper.createObjectNode();
+                    resolveFieldValue("/comment/body", commentBody, baseTicketNode, record);
+
+                    if (subject != null) {
+                        resolveFieldValue("/subject", subject, baseTicketNode, record);
+                    }
+                    if (priority != null) {
+                        resolveFieldValue("/priority", priority, baseTicketNode, record);
+                    }
+                    if (type != null) {
+                        resolveFieldValue("/type", type, baseTicketNode, record);
+                    }
+                    for (Map.Entry<String, String> dynamicProperty : dynamicProperties.entrySet()) {
+                        addDynamicField(dynamicProperty.getKey(), dynamicProperty.getValue(), baseTicketNode, record);
+                    }
+                    zendeskTickets.add(baseTicketNode);
+                }
+
+            } catch (IOException | SchemaNotFoundException | MalformedRecordException e) {
+                getLogger().error("Error occurred while creating zendesk tickets", e);

Review Comment:
   ```suggestion
                   getLogger().error("Error occurred while creating Zendesk tickets", e);
   ```



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/resources/docs.org.apache.nifi.processors.zendesk.PutZendeskTicket/additionalDetails.html:
##########
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html lang="en" xmlns="http://www.w3.org/1999/html">
+<!--
+      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.
+    -->
+
+    <head>
+        <meta charset="utf-8"/>
+        <title>GetZendesk</title>
+        <link rel="stylesheet" href="../../../../../css/component-usage.css" type="text/css"/>
+        <style>
+            h2 {margin-top: 4em}
+            h3 {margin-top: 3em}
+            td {text-align: left}
+        </style>
+    </head>
+
+    <body>
+        <h1>PutZendeskTicket</h1>
+
+        <h3>Description</h3>
+
+        <p>
+            The processor uses the Zendesk API to ingest tickets into Zendesk. The processor is capable to send requests directly from the flowFile content or construct the request objects from the incoming records using RecordReader.
+        </p>
+
+        <h3>Authentication</h3>
+
+        <p>
+            Zendesk API uses basic authentication. Either a password or an authentication token have to be provided.
+            Authentication token can be created in Zendesk API Settings, so the users don't have to expose their passwords,
+            and also auth tokens can be revoked quickly if necessary.

Review Comment:
   ```suggestion
               Zendesk API uses basic authentication. Either a password or an authentication token has to be provided.
               In Zendesk API Settings, it's possible to generate authentication tokens, eliminating the need for users to expose their passwords. This approach also offers the advantage of fast token revocation when required.
   ```



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/processors/zendesk/PutZendeskTicket.java:
##########
@@ -0,0 +1,294 @@
+/*
+ * 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.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.commons.zendesk.ZendeskAuthenticationContext;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.MalformedRecordException;
+import org.apache.nifi.serialization.RecordReader;
+import org.apache.nifi.serialization.RecordReaderFactory;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.String.format;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.HTTPS;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_FAILURE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_SUCCESS_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_HOST_TEMPLATE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.performPostRequest;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.responseBodyToString;
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.CREATED;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.OK;
+
+@Tags({"zendesk, ticket"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API.")
+@DynamicProperty(
+        name = "The path in the request object to add.",
+        value = "The path in the incoming record to get the value from.",
+        expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
+        description = "Additional property to be added to the Zendesk request object.")
+public class PutZendeskTicket extends AbstractProcessor {
+
+    static final String ZENDESK_RECORD_READER_NAME = "zendesk-record-reader";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    static final PropertyDescriptor RECORD_READER = new PropertyDescriptor.Builder()
+            .name(ZENDESK_RECORD_READER_NAME)
+            .displayName("Record Reader")
+            .description("Specifies the Controller Service to use for parsing incoming data and determining the data's schema.")
+            .identifiesControllerService(RecordReaderFactory.class)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_COMMENT_BODY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_COMMENT_BODY_NAME)
+            .displayName("Comment Body")
+            .description("The content or the path to the comment body in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_SUBJECT = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_SUBJECT_NAME)
+            .displayName("Subject")
+            .description("The content or the path to the subject in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_PRIORITY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_PRIORITY_NAME)
+            .displayName("Priority")
+            .description("The content or the path to the priority in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_TYPE = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_TYPE_NAME)
+            .displayName("Type")
+            .description("The content or the path to the type in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+            RECORD_READER,
+            WEB_CLIENT_SERVICE_PROVIDER,
+            ZENDESK_SUBDOMAIN,
+            ZENDESK_USER,
+            ZENDESK_AUTHENTICATION_TYPE,
+            ZENDESK_AUTHENTICATION_CREDENTIAL,
+            ZENDESK_TICKET_COMMENT_BODY,
+            ZENDESK_TICKET_SUBJECT,
+            ZENDESK_TICKET_PRIORITY,
+            ZENDESK_TICKET_TYPE
+    ));
+
+    private volatile WebClientServiceProvider webClientServiceProvider;
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name(REL_SUCCESS_NAME)
+            .description("For FlowFiles created as a result of a successful HTTP request.")
+            .build();
+
+    public static final Relationship REL_FAILURE = new Relationship.Builder()
+            .name(REL_FAILURE_NAME)
+            .description("A FlowFile is routed to this relationship if the operation failed and retrying the operation will also fail, such as an invalid data or schema.")
+            .build();
+
+    @Override
+    protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
+        return new PropertyDescriptor.Builder()
+                .name(propertyDescriptorName)
+                .required(false)
+                .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+                .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+                .dynamic(true)
+                .build();
+    }
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    public static final Set<Relationship> RELATIONSHIPS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+            REL_SUCCESS,
+            REL_FAILURE
+    )));
+
+    @Override
+    public List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @OnScheduled
+    public void onScheduled(ProcessContext context) {
+        webClientServiceProvider = context.getProperty(WEB_CLIENT_SERVICE_PROVIDER).asControllerService(WebClientServiceProvider.class);
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) {
+        final FlowFile flowFile = session.get();
+        if (flowFile == null) {
+            return;
+        }
+
+        long startNanos = System.nanoTime();
+        HttpResponseEntity response;
+        URI uri;
+        final RecordReaderFactory readerFactory = context.getProperty(RECORD_READER).asControllerService(RecordReaderFactory.class);
+        final ZendeskAuthenticationContext authenticationContext = new ZendeskAuthenticationContext(context);
+
+        if (readerFactory == null) {
+            try (final InputStream inputStream = session.read(flowFile)) {
+                final HttpUriBuilder uriBuilder = uriBuilder(authenticationContext.getSubDomain(), ZENDESK_CREATE_TICKET_RESOURCE);
+                uri = uriBuilder.build();
+                response = performPostRequest(authenticationContext, webClientServiceProvider, uri, inputStream);
+            } catch (IOException e) {
+                throw new ProcessException("Could not read incoming FlowFile", e);
+            }
+        } else {
+            final String commentBody = context.getProperty(ZENDESK_TICKET_COMMENT_BODY).evaluateAttributeExpressions().getValue();
+            final String subject = context.getProperty(ZENDESK_TICKET_SUBJECT).evaluateAttributeExpressions().getValue();
+            final String priority = context.getProperty(ZENDESK_TICKET_PRIORITY).evaluateAttributeExpressions().getValue();
+            final String type = context.getProperty(ZENDESK_TICKET_TYPE).evaluateAttributeExpressions().getValue();
+            final Map<String, String> dynamicProperties = getDynamicProperties(context, context.getProperties());
+            List<ObjectNode> zendeskTickets = new ArrayList<>();
+
+            try (final InputStream in = session.read(flowFile); final RecordReader reader = readerFactory.createRecordReader(flowFile, in, getLogger())) {
+                Record record;
+
+                while ((record = reader.nextRecord()) != null) {
+                    ObjectNode baseTicketNode = mapper.createObjectNode();
+                    resolveFieldValue("/comment/body", commentBody, baseTicketNode, record);
+
+                    if (subject != null) {
+                        resolveFieldValue("/subject", subject, baseTicketNode, record);
+                    }
+                    if (priority != null) {
+                        resolveFieldValue("/priority", priority, baseTicketNode, record);
+                    }
+                    if (type != null) {
+                        resolveFieldValue("/type", type, baseTicketNode, record);
+                    }
+                    for (Map.Entry<String, String> dynamicProperty : dynamicProperties.entrySet()) {
+                        addDynamicField(dynamicProperty.getKey(), dynamicProperty.getValue(), baseTicketNode, record);
+                    }
+                    zendeskTickets.add(baseTicketNode);
+                }
+
+            } catch (IOException | SchemaNotFoundException | MalformedRecordException e) {
+                getLogger().error("Error occurred while creating zendesk tickets", e);
+                session.transfer(session.penalize(flowFile), REL_FAILURE);
+                return;
+            }
+
+            if (zendeskTickets.size() == 0) {
+                getLogger().info("No record were received");
+                return;
+            }
+
+            try {
+                final InputStream inputStream = createRequestObject(mapper, zendeskTickets);
+                uri = createUri(authenticationContext, zendeskTickets.size());
+                response = performPostRequest(authenticationContext, webClientServiceProvider, uri, inputStream);
+            } catch (IOException e) {
+                getLogger().error("Failed to post request to Zendesk", e);
+                session.transfer(session.penalize(flowFile), REL_FAILURE);
+                return;
+            }
+        }
+
+        handleResponse(session, flowFile, response, uri, startNanos);
+    }
+
+    private void handleResponse(ProcessSession session, FlowFile flowFile, HttpResponseEntity response, URI uri, long startNanos) {
+        if (response.statusCode() == CREATED.getCode() || response.statusCode() == OK.getCode()) {
+            session.transfer(flowFile, REL_SUCCESS);
+            long transferMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
+            session.getProvenanceReporter().send(flowFile, uri.toString(), transferMillis);
+        } else {
+            getLogger().error("Zendesk ticket creation returned with error, HTTP status={}, response={}", response.statusCode(), responseBodyToString(response));
+            session.transfer(session.penalize(flowFile), REL_FAILURE);
+        }
+    }
+
+    private URI createUri(ZendeskAuthenticationContext authenticationContext, int numberOfTickets) {
+        final String resource = numberOfTickets > 1 ? ZENDESK_CREATE_TICKETS_RESOURCE : ZENDESK_CREATE_TICKET_RESOURCE;
+        final HttpUriBuilder uriBuilder = uriBuilder(authenticationContext.getSubDomain(), resource);
+        return uriBuilder.build();
+    }
+
+    HttpUriBuilder uriBuilder(String subDomain, String resourcePath) {

Review Comment:
   I suggest creating a new ZendeskClient class dedicated to handling HTTP operations. In doing so, you can relocate the components responsible for creating URLs from PutZendeskTicket and GetZendeskTicket and the method ZendeskUtils::performRequest as well. This approach will lead to better organization and separation of concerns within the codebase.



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/resources/docs.org.apache.nifi.processors.zendesk.PutZendeskTicket/additionalDetails.html:
##########
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html lang="en" xmlns="http://www.w3.org/1999/html">
+<!--
+      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.
+    -->
+
+    <head>
+        <meta charset="utf-8"/>
+        <title>GetZendesk</title>

Review Comment:
   ```suggestion
           <title>PutZendesk</title>
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1308042599


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/processors/zendesk/PutZendeskTicket.java:
##########
@@ -0,0 +1,294 @@
+/*
+ * 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.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.commons.zendesk.ZendeskAuthenticationContext;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.MalformedRecordException;
+import org.apache.nifi.serialization.RecordReader;
+import org.apache.nifi.serialization.RecordReaderFactory;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.String.format;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.HTTPS;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_FAILURE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_SUCCESS_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_HOST_TEMPLATE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.performPostRequest;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.responseBodyToString;
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.CREATED;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.OK;
+
+@Tags({"zendesk, ticket"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API.")
+@DynamicProperty(
+        name = "The path in the request object to add.",
+        value = "The path in the incoming record to get the value from.",
+        expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
+        description = "Additional property to be added to the Zendesk request object.")
+public class PutZendeskTicket extends AbstractProcessor {
+
+    static final String ZENDESK_RECORD_READER_NAME = "zendesk-record-reader";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    static final PropertyDescriptor RECORD_READER = new PropertyDescriptor.Builder()
+            .name(ZENDESK_RECORD_READER_NAME)
+            .displayName("Record Reader")
+            .description("Specifies the Controller Service to use for parsing incoming data and determining the data's schema.")
+            .identifiesControllerService(RecordReaderFactory.class)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_COMMENT_BODY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_COMMENT_BODY_NAME)
+            .displayName("Comment Body")
+            .description("The content or the path to the comment body in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_SUBJECT = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_SUBJECT_NAME)
+            .displayName("Subject")
+            .description("The content or the path to the subject in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_PRIORITY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_PRIORITY_NAME)
+            .displayName("Priority")
+            .description("The content or the path to the priority in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_TYPE = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_TYPE_NAME)
+            .displayName("Type")
+            .description("The content or the path to the type in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+            RECORD_READER,
+            WEB_CLIENT_SERVICE_PROVIDER,
+            ZENDESK_SUBDOMAIN,
+            ZENDESK_USER,
+            ZENDESK_AUTHENTICATION_TYPE,
+            ZENDESK_AUTHENTICATION_CREDENTIAL,
+            ZENDESK_TICKET_COMMENT_BODY,
+            ZENDESK_TICKET_SUBJECT,
+            ZENDESK_TICKET_PRIORITY,
+            ZENDESK_TICKET_TYPE
+    ));
+
+    private volatile WebClientServiceProvider webClientServiceProvider;
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name(REL_SUCCESS_NAME)
+            .description("For FlowFiles created as a result of a successful HTTP request.")
+            .build();
+
+    public static final Relationship REL_FAILURE = new Relationship.Builder()
+            .name(REL_FAILURE_NAME)
+            .description("A FlowFile is routed to this relationship if the operation failed and retrying the operation will also fail, such as an invalid data or schema.")
+            .build();
+
+    @Override
+    protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
+        return new PropertyDescriptor.Builder()
+                .name(propertyDescriptorName)
+                .required(false)
+                .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+                .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+                .dynamic(true)
+                .build();
+    }
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    public static final Set<Relationship> RELATIONSHIPS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+            REL_SUCCESS,
+            REL_FAILURE
+    )));
+
+    @Override
+    public List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @OnScheduled
+    public void onScheduled(ProcessContext context) {
+        webClientServiceProvider = context.getProperty(WEB_CLIENT_SERVICE_PROVIDER).asControllerService(WebClientServiceProvider.class);
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) {
+        final FlowFile flowFile = session.get();
+        if (flowFile == null) {
+            return;
+        }
+
+        long startNanos = System.nanoTime();
+        HttpResponseEntity response;
+        URI uri;
+        final RecordReaderFactory readerFactory = context.getProperty(RECORD_READER).asControllerService(RecordReaderFactory.class);
+        final ZendeskAuthenticationContext authenticationContext = new ZendeskAuthenticationContext(context);
+
+        if (readerFactory == null) {
+            try (final InputStream inputStream = session.read(flowFile)) {
+                final HttpUriBuilder uriBuilder = uriBuilder(authenticationContext.getSubDomain(), ZENDESK_CREATE_TICKET_RESOURCE);
+                uri = uriBuilder.build();
+                response = performPostRequest(authenticationContext, webClientServiceProvider, uri, inputStream);
+            } catch (IOException e) {
+                throw new ProcessException("Could not read incoming FlowFile", e);
+            }
+        } else {
+            final String commentBody = context.getProperty(ZENDESK_TICKET_COMMENT_BODY).evaluateAttributeExpressions().getValue();
+            final String subject = context.getProperty(ZENDESK_TICKET_SUBJECT).evaluateAttributeExpressions().getValue();
+            final String priority = context.getProperty(ZENDESK_TICKET_PRIORITY).evaluateAttributeExpressions().getValue();
+            final String type = context.getProperty(ZENDESK_TICKET_TYPE).evaluateAttributeExpressions().getValue();
+            final Map<String, String> dynamicProperties = getDynamicProperties(context, context.getProperties());
+            List<ObjectNode> zendeskTickets = new ArrayList<>();
+
+            try (final InputStream in = session.read(flowFile); final RecordReader reader = readerFactory.createRecordReader(flowFile, in, getLogger())) {
+                Record record;
+
+                while ((record = reader.nextRecord()) != null) {
+                    ObjectNode baseTicketNode = mapper.createObjectNode();
+                    resolveFieldValue("/comment/body", commentBody, baseTicketNode, record);
+
+                    if (subject != null) {
+                        resolveFieldValue("/subject", subject, baseTicketNode, record);
+                    }
+                    if (priority != null) {
+                        resolveFieldValue("/priority", priority, baseTicketNode, record);
+                    }
+                    if (type != null) {
+                        resolveFieldValue("/type", type, baseTicketNode, record);
+                    }
+                    for (Map.Entry<String, String> dynamicProperty : dynamicProperties.entrySet()) {
+                        addDynamicField(dynamicProperty.getKey(), dynamicProperty.getValue(), baseTicketNode, record);
+                    }
+                    zendeskTickets.add(baseTicketNode);
+                }
+
+            } catch (IOException | SchemaNotFoundException | MalformedRecordException e) {
+                getLogger().error("Error occurred while creating zendesk tickets", e);
+                session.transfer(session.penalize(flowFile), REL_FAILURE);
+                return;
+            }
+
+            if (zendeskTickets.size() == 0) {
+                getLogger().info("No record were received");
+                return;
+            }
+
+            try {
+                final InputStream inputStream = createRequestObject(mapper, zendeskTickets);
+                uri = createUri(authenticationContext, zendeskTickets.size());
+                response = performPostRequest(authenticationContext, webClientServiceProvider, uri, inputStream);
+            } catch (IOException e) {
+                getLogger().error("Failed to post request to Zendesk", e);
+                session.transfer(session.penalize(flowFile), REL_FAILURE);
+                return;
+            }
+        }
+
+        handleResponse(session, flowFile, response, uri, startNanos);
+    }
+
+    private void handleResponse(ProcessSession session, FlowFile flowFile, HttpResponseEntity response, URI uri, long startNanos) {

Review Comment:
   Please add the corresponding mimetype to the FlowFile which should be the mime type of the record reader or JSON.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1315812749


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/processors/zendesk/PutZendeskTicket.java:
##########
@@ -0,0 +1,294 @@
+/*
+ * 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.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.commons.zendesk.ZendeskAuthenticationContext;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.MalformedRecordException;
+import org.apache.nifi.serialization.RecordReader;
+import org.apache.nifi.serialization.RecordReaderFactory;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.String.format;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.HTTPS;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_FAILURE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_SUCCESS_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_HOST_TEMPLATE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.performPostRequest;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.responseBodyToString;
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.CREATED;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.OK;
+
+@Tags({"zendesk, ticket"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API.")
+@DynamicProperty(
+        name = "The path in the request object to add.",
+        value = "The path in the incoming record to get the value from.",
+        expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
+        description = "Additional property to be added to the Zendesk request object.")
+public class PutZendeskTicket extends AbstractProcessor {
+
+    static final String ZENDESK_RECORD_READER_NAME = "zendesk-record-reader";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    static final PropertyDescriptor RECORD_READER = new PropertyDescriptor.Builder()
+            .name(ZENDESK_RECORD_READER_NAME)
+            .displayName("Record Reader")
+            .description("Specifies the Controller Service to use for parsing incoming data and determining the data's schema.")
+            .identifiesControllerService(RecordReaderFactory.class)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_COMMENT_BODY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_COMMENT_BODY_NAME)
+            .displayName("Comment Body")
+            .description("The content or the path to the comment body in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_SUBJECT = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_SUBJECT_NAME)
+            .displayName("Subject")
+            .description("The content or the path to the subject in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_PRIORITY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_PRIORITY_NAME)
+            .displayName("Priority")
+            .description("The content or the path to the priority in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_TYPE = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_TYPE_NAME)
+            .displayName("Type")
+            .description("The content or the path to the type in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+            RECORD_READER,
+            WEB_CLIENT_SERVICE_PROVIDER,
+            ZENDESK_SUBDOMAIN,
+            ZENDESK_USER,
+            ZENDESK_AUTHENTICATION_TYPE,
+            ZENDESK_AUTHENTICATION_CREDENTIAL,
+            ZENDESK_TICKET_COMMENT_BODY,
+            ZENDESK_TICKET_SUBJECT,
+            ZENDESK_TICKET_PRIORITY,
+            ZENDESK_TICKET_TYPE
+    ));
+
+    private volatile WebClientServiceProvider webClientServiceProvider;
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name(REL_SUCCESS_NAME)
+            .description("For FlowFiles created as a result of a successful HTTP request.")
+            .build();
+
+    public static final Relationship REL_FAILURE = new Relationship.Builder()
+            .name(REL_FAILURE_NAME)
+            .description("A FlowFile is routed to this relationship if the operation failed and retrying the operation will also fail, such as an invalid data or schema.")
+            .build();
+
+    @Override
+    protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
+        return new PropertyDescriptor.Builder()
+                .name(propertyDescriptorName)
+                .required(false)
+                .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)

Review Comment:
   A custom validator would make sense here to check if the key (subject) is a RecordPath.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] turcsanyip commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "turcsanyip (via GitHub)" <gi...@apache.org>.
turcsanyip commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1331152815


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-common/src/main/java/org/apache/nifi/common/zendesk/util/ZendeskRecordPathUtils.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * 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.common.zendesk.util;
+
+import com.fasterxml.jackson.core.JsonPointer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.RecordPath;
+import org.apache.nifi.record.path.RecordPathResult;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.type.ChoiceDataType;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.util.stream.Collectors.toList;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKETS_ROOT_NODE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_ROOT_NODE;
+
+public final class ZendeskRecordPathUtils {
+
+    private static final String NULL_VALUE = "null";
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final Pattern RECORD_PATH_PATTERN = Pattern.compile("%\\{(.*?)\\}");

Review Comment:
   As the record path points to a "location" in the record, I suggest using `@` (at sign) for the property value syntax.
   E.g. `@{ticket.type}`



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on PR #7644:
URL: https://github.com/apache/nifi/pull/7644#issuecomment-1696561182

   @mark-bathori, I conducted manual testing on the `PutZendeskTicket` processor and the `ZendeskRecordSink` service. In both basic and advanced scenarios using RecordPath, both components demonstrated effective functionality. I observed two rooms for improvement during testing:
   
   Logically, a sink service doesn't seem to align well within a processor package. Even if both are located in the same NAR, it could be valuable to organize these modules into distinct common, processors, and services sections for improved clarity. (summon @turcsanyip)
   
   To further enhance the sink, it might be beneficial to implement a caching mechanism within the sink service. This would be particularly relevant in situations where there are numerous identical bulletins in the `BulletinRepository`. In such cases, if the `QueryNifiReporting` task runs with the `ZendeskRecordSink`, it could potentially create multiple identical tickets. Implementing a cache could mitigate this redundancy.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] exceptionfactory commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "exceptionfactory (via GitHub)" <gi...@apache.org>.
exceptionfactory commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1304685027


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/pom.xml:
##########
@@ -49,6 +70,16 @@
             <groupId>commons-io</groupId>
             <artifactId>commons-io</artifactId>
         </dependency>
+        <dependency>
+            <groupId>javax.json</groupId>
+            <artifactId>javax.json-api</artifactId>
+            <version>1.1.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish</groupId>
+            <artifactId>javax.json</artifactId>
+            <version>1.1.4</version>
+        </dependency>

Review Comment:
   Are these dependencies used? The JSON processing appears to be using Jackson.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1315685170


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/processors/zendesk/PutZendeskTicket.java:
##########
@@ -0,0 +1,294 @@
+/*
+ * 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.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.commons.zendesk.ZendeskAuthenticationContext;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.MalformedRecordException;
+import org.apache.nifi.serialization.RecordReader;
+import org.apache.nifi.serialization.RecordReaderFactory;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.String.format;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.HTTPS;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_FAILURE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_SUCCESS_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_HOST_TEMPLATE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.performPostRequest;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.responseBodyToString;
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.CREATED;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.OK;
+
+@Tags({"zendesk, ticket"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API.")
+@DynamicProperty(
+        name = "The path in the request object to add.",
+        value = "The path in the incoming record to get the value from.",
+        expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
+        description = "Additional property to be added to the Zendesk request object.")
+public class PutZendeskTicket extends AbstractProcessor {
+
+    static final String ZENDESK_RECORD_READER_NAME = "zendesk-record-reader";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    static final PropertyDescriptor RECORD_READER = new PropertyDescriptor.Builder()
+            .name(ZENDESK_RECORD_READER_NAME)
+            .displayName("Record Reader")
+            .description("Specifies the Controller Service to use for parsing incoming data and determining the data's schema.")
+            .identifiesControllerService(RecordReaderFactory.class)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_COMMENT_BODY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_COMMENT_BODY_NAME)
+            .displayName("Comment Body")
+            .description("The content or the path to the comment body in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_SUBJECT = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_SUBJECT_NAME)
+            .displayName("Subject")
+            .description("The content or the path to the subject in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_PRIORITY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_PRIORITY_NAME)
+            .displayName("Priority")
+            .description("The content or the path to the priority in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_TYPE = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_TYPE_NAME)
+            .displayName("Type")
+            .description("The content or the path to the type in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+            RECORD_READER,
+            WEB_CLIENT_SERVICE_PROVIDER,
+            ZENDESK_SUBDOMAIN,
+            ZENDESK_USER,
+            ZENDESK_AUTHENTICATION_TYPE,
+            ZENDESK_AUTHENTICATION_CREDENTIAL,
+            ZENDESK_TICKET_COMMENT_BODY,
+            ZENDESK_TICKET_SUBJECT,
+            ZENDESK_TICKET_PRIORITY,
+            ZENDESK_TICKET_TYPE
+    ));
+
+    private volatile WebClientServiceProvider webClientServiceProvider;
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name(REL_SUCCESS_NAME)
+            .description("For FlowFiles created as a result of a successful HTTP request.")
+            .build();
+
+    public static final Relationship REL_FAILURE = new Relationship.Builder()
+            .name(REL_FAILURE_NAME)
+            .description("A FlowFile is routed to this relationship if the operation failed and retrying the operation will also fail, such as an invalid data or schema.")
+            .build();
+
+    @Override
+    protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
+        return new PropertyDescriptor.Builder()
+                .name(propertyDescriptorName)
+                .required(false)
+                .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+                .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+                .dynamic(true)
+                .build();
+    }
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    public static final Set<Relationship> RELATIONSHIPS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+            REL_SUCCESS,
+            REL_FAILURE
+    )));
+
+    @Override
+    public List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @OnScheduled
+    public void onScheduled(ProcessContext context) {
+        webClientServiceProvider = context.getProperty(WEB_CLIENT_SERVICE_PROVIDER).asControllerService(WebClientServiceProvider.class);
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) {
+        final FlowFile flowFile = session.get();
+        if (flowFile == null) {
+            return;
+        }
+
+        long startNanos = System.nanoTime();
+        HttpResponseEntity response;
+        URI uri;
+        final RecordReaderFactory readerFactory = context.getProperty(RECORD_READER).asControllerService(RecordReaderFactory.class);
+        final ZendeskAuthenticationContext authenticationContext = new ZendeskAuthenticationContext(context);
+
+        if (readerFactory == null) {
+            try (final InputStream inputStream = session.read(flowFile)) {
+                final HttpUriBuilder uriBuilder = uriBuilder(authenticationContext.getSubDomain(), ZENDESK_CREATE_TICKET_RESOURCE);
+                uri = uriBuilder.build();
+                response = performPostRequest(authenticationContext, webClientServiceProvider, uri, inputStream);
+            } catch (IOException e) {
+                throw new ProcessException("Could not read incoming FlowFile", e);
+            }
+        } else {
+            final String commentBody = context.getProperty(ZENDESK_TICKET_COMMENT_BODY).evaluateAttributeExpressions().getValue();
+            final String subject = context.getProperty(ZENDESK_TICKET_SUBJECT).evaluateAttributeExpressions().getValue();
+            final String priority = context.getProperty(ZENDESK_TICKET_PRIORITY).evaluateAttributeExpressions().getValue();
+            final String type = context.getProperty(ZENDESK_TICKET_TYPE).evaluateAttributeExpressions().getValue();
+            final Map<String, String> dynamicProperties = getDynamicProperties(context, context.getProperties());
+            List<ObjectNode> zendeskTickets = new ArrayList<>();
+
+            try (final InputStream in = session.read(flowFile); final RecordReader reader = readerFactory.createRecordReader(flowFile, in, getLogger())) {
+                Record record;
+
+                while ((record = reader.nextRecord()) != null) {
+                    ObjectNode baseTicketNode = mapper.createObjectNode();
+                    resolveFieldValue("/comment/body", commentBody, baseTicketNode, record);
+
+                    if (subject != null) {
+                        resolveFieldValue("/subject", subject, baseTicketNode, record);
+                    }
+                    if (priority != null) {
+                        resolveFieldValue("/priority", priority, baseTicketNode, record);
+                    }
+                    if (type != null) {
+                        resolveFieldValue("/type", type, baseTicketNode, record);
+                    }
+                    for (Map.Entry<String, String> dynamicProperty : dynamicProperties.entrySet()) {
+                        addDynamicField(dynamicProperty.getKey(), dynamicProperty.getValue(), baseTicketNode, record);
+                    }
+                    zendeskTickets.add(baseTicketNode);
+                }
+
+            } catch (IOException | SchemaNotFoundException | MalformedRecordException e) {
+                getLogger().error("Error occurred while creating zendesk tickets", e);
+                session.transfer(session.penalize(flowFile), REL_FAILURE);
+                return;
+            }
+
+            if (zendeskTickets.size() == 0) {
+                getLogger().info("No record were received");

Review Comment:
   ```suggestion
                   getLogger().info("No records found in incoming FlowFile");
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1307955655


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/resources/docs.org.apache.nifi.processors.zendesk.PutZendeskTicket/additionalDetails.html:
##########
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html lang="en" xmlns="http://www.w3.org/1999/html">
+<!--
+      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.
+    -->
+
+    <head>
+        <meta charset="utf-8"/>
+        <title>GetZendesk</title>

Review Comment:
   Two more things: 
   - Could you please add more examples? 
   -  I can't see the additionalDetails on the UI



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1308040012


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/processors/zendesk/PutZendeskTicket.java:
##########
@@ -0,0 +1,294 @@
+/*
+ * 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.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.commons.zendesk.ZendeskAuthenticationContext;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.MalformedRecordException;
+import org.apache.nifi.serialization.RecordReader;
+import org.apache.nifi.serialization.RecordReaderFactory;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.String.format;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.HTTPS;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_FAILURE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_SUCCESS_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_HOST_TEMPLATE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.performPostRequest;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.responseBodyToString;
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.CREATED;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.OK;
+
+@Tags({"zendesk, ticket"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API.")
+@DynamicProperty(
+        name = "The path in the request object to add.",
+        value = "The path in the incoming record to get the value from.",
+        expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
+        description = "Additional property to be added to the Zendesk request object.")
+public class PutZendeskTicket extends AbstractProcessor {
+
+    static final String ZENDESK_RECORD_READER_NAME = "zendesk-record-reader";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    static final PropertyDescriptor RECORD_READER = new PropertyDescriptor.Builder()
+            .name(ZENDESK_RECORD_READER_NAME)
+            .displayName("Record Reader")
+            .description("Specifies the Controller Service to use for parsing incoming data and determining the data's schema.")
+            .identifiesControllerService(RecordReaderFactory.class)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_COMMENT_BODY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_COMMENT_BODY_NAME)
+            .displayName("Comment Body")
+            .description("The content or the path to the comment body in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_SUBJECT = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_SUBJECT_NAME)
+            .displayName("Subject")
+            .description("The content or the path to the subject in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_PRIORITY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_PRIORITY_NAME)
+            .displayName("Priority")
+            .description("The content or the path to the priority in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_TYPE = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_TYPE_NAME)
+            .displayName("Type")
+            .description("The content or the path to the type in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+            RECORD_READER,
+            WEB_CLIENT_SERVICE_PROVIDER,
+            ZENDESK_SUBDOMAIN,
+            ZENDESK_USER,
+            ZENDESK_AUTHENTICATION_TYPE,
+            ZENDESK_AUTHENTICATION_CREDENTIAL,
+            ZENDESK_TICKET_COMMENT_BODY,
+            ZENDESK_TICKET_SUBJECT,
+            ZENDESK_TICKET_PRIORITY,
+            ZENDESK_TICKET_TYPE
+    ));
+
+    private volatile WebClientServiceProvider webClientServiceProvider;
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name(REL_SUCCESS_NAME)
+            .description("For FlowFiles created as a result of a successful HTTP request.")
+            .build();
+
+    public static final Relationship REL_FAILURE = new Relationship.Builder()
+            .name(REL_FAILURE_NAME)
+            .description("A FlowFile is routed to this relationship if the operation failed and retrying the operation will also fail, such as an invalid data or schema.")
+            .build();
+
+    @Override
+    protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
+        return new PropertyDescriptor.Builder()
+                .name(propertyDescriptorName)
+                .required(false)
+                .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+                .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+                .dynamic(true)
+                .build();
+    }
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    public static final Set<Relationship> RELATIONSHIPS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+            REL_SUCCESS,
+            REL_FAILURE
+    )));
+
+    @Override
+    public List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @OnScheduled
+    public void onScheduled(ProcessContext context) {
+        webClientServiceProvider = context.getProperty(WEB_CLIENT_SERVICE_PROVIDER).asControllerService(WebClientServiceProvider.class);
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) {
+        final FlowFile flowFile = session.get();
+        if (flowFile == null) {
+            return;
+        }
+
+        long startNanos = System.nanoTime();
+        HttpResponseEntity response;
+        URI uri;
+        final RecordReaderFactory readerFactory = context.getProperty(RECORD_READER).asControllerService(RecordReaderFactory.class);
+        final ZendeskAuthenticationContext authenticationContext = new ZendeskAuthenticationContext(context);
+
+        if (readerFactory == null) {
+            try (final InputStream inputStream = session.read(flowFile)) {
+                final HttpUriBuilder uriBuilder = uriBuilder(authenticationContext.getSubDomain(), ZENDESK_CREATE_TICKET_RESOURCE);
+                uri = uriBuilder.build();
+                response = performPostRequest(authenticationContext, webClientServiceProvider, uri, inputStream);
+            } catch (IOException e) {
+                throw new ProcessException("Could not read incoming FlowFile", e);
+            }
+        } else {
+            final String commentBody = context.getProperty(ZENDESK_TICKET_COMMENT_BODY).evaluateAttributeExpressions().getValue();
+            final String subject = context.getProperty(ZENDESK_TICKET_SUBJECT).evaluateAttributeExpressions().getValue();
+            final String priority = context.getProperty(ZENDESK_TICKET_PRIORITY).evaluateAttributeExpressions().getValue();
+            final String type = context.getProperty(ZENDESK_TICKET_TYPE).evaluateAttributeExpressions().getValue();
+            final Map<String, String> dynamicProperties = getDynamicProperties(context, context.getProperties());
+            List<ObjectNode> zendeskTickets = new ArrayList<>();
+
+            try (final InputStream in = session.read(flowFile); final RecordReader reader = readerFactory.createRecordReader(flowFile, in, getLogger())) {
+                Record record;
+
+                while ((record = reader.nextRecord()) != null) {
+                    ObjectNode baseTicketNode = mapper.createObjectNode();
+                    resolveFieldValue("/comment/body", commentBody, baseTicketNode, record);
+
+                    if (subject != null) {
+                        resolveFieldValue("/subject", subject, baseTicketNode, record);
+                    }
+                    if (priority != null) {
+                        resolveFieldValue("/priority", priority, baseTicketNode, record);
+                    }
+                    if (type != null) {
+                        resolveFieldValue("/type", type, baseTicketNode, record);
+                    }
+                    for (Map.Entry<String, String> dynamicProperty : dynamicProperties.entrySet()) {
+                        addDynamicField(dynamicProperty.getKey(), dynamicProperty.getValue(), baseTicketNode, record);
+                    }
+                    zendeskTickets.add(baseTicketNode);
+                }
+
+            } catch (IOException | SchemaNotFoundException | MalformedRecordException e) {
+                getLogger().error("Error occurred while creating zendesk tickets", e);
+                session.transfer(session.penalize(flowFile), REL_FAILURE);
+                return;
+            }
+
+            if (zendeskTickets.size() == 0) {
+                getLogger().info("No record were received");
+                return;
+            }
+
+            try {
+                final InputStream inputStream = createRequestObject(mapper, zendeskTickets);
+                uri = createUri(authenticationContext, zendeskTickets.size());
+                response = performPostRequest(authenticationContext, webClientServiceProvider, uri, inputStream);
+            } catch (IOException e) {
+                getLogger().error("Failed to post request to Zendesk", e);
+                session.transfer(session.penalize(flowFile), REL_FAILURE);
+                return;
+            }
+        }
+
+        handleResponse(session, flowFile, response, uri, startNanos);
+    }
+
+    private void handleResponse(ProcessSession session, FlowFile flowFile, HttpResponseEntity response, URI uri, long startNanos) {
+        if (response.statusCode() == CREATED.getCode() || response.statusCode() == OK.getCode()) {
+            session.transfer(flowFile, REL_SUCCESS);

Review Comment:
   What do you think of adding the constructed payload to the flowfile instead of sending the original one?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1305612019


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/pom.xml:
##########
@@ -26,10 +26,15 @@
     <packaging>jar</packaging>
 
     <dependencies>
+        <!-- Internal dependencies -->
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-record</artifactId>
+        </dependency>
         <dependency>

Review Comment:
   nifi-utils is provided by nifi-standard-services-api-nar through nar hierarchy



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/pom.xml:
##########
@@ -26,10 +26,15 @@
     <packaging>jar</packaging>
 
     <dependencies>
+        <!-- Internal dependencies -->
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-api</artifactId>
         </dependency>
+        <dependency>

Review Comment:
   nifi-record is provided by nifi-standard-services-api-nar through nar hierarchy



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/pom.xml:
##########
@@ -49,6 +70,16 @@
             <groupId>commons-io</groupId>
             <artifactId>commons-io</artifactId>
         </dependency>
+        <dependency>
+            <groupId>javax.json</groupId>
+            <artifactId>javax.json-api</artifactId>
+            <version>1.1.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish</groupId>
+            <artifactId>javax.json</artifactId>
+            <version>1.1.4</version>
+        </dependency>

Review Comment:
   These dependencies can be removed



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/pom.xml:
##########
@@ -26,10 +26,15 @@
     <packaging>jar</packaging>
 
     <dependencies>
+        <!-- Internal dependencies -->
         <dependency>

Review Comment:
   nifi-api can be provided because the nifi-api-jar because it is loaded by the system class loader



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/pom.xml:
##########
@@ -41,6 +46,22 @@
             <version>2.0.0-SNAPSHOT</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>

Review Comment:
   nifi-record-serialization-service-api is provided by nifi-standard-services-api-nar through nar hierarchy



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/pom.xml:
##########
@@ -41,6 +46,22 @@
             <version>2.0.0-SNAPSHOT</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-record-serialization-service-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-record-path</artifactId>
+            <version>2.0.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>

Review Comment:
   nifi-record-sink-api is provided by nifi-standard-services-api-nar through nar hierarchy



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/pom.xml:
##########
@@ -49,6 +70,16 @@
             <groupId>commons-io</groupId>

Review Comment:
   I would remove this dependency since it's only used once in GetZendesk. The IOUtils::toString can be replaced with the following approach.
   ```
   try (InputStream responseBodyStream = response.body();
                        BufferedReader reader = new BufferedReader(new InputStreamReader(responseBodyStream, StandardCharsets.UTF_8))) {
                       final String responseBody = reader.lines().collect(Collectors.joining(System.lineSeparator()));
                       errorMessage = String.format("HTTP %s error for requested URI [%s] with response [%s]", response.statusCode(), uri, responseBody);
                       getLogger().warn(errorMessage);             
   }
   ```



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/pom.xml:
##########
@@ -77,5 +108,10 @@
             <artifactId>mockwebserver</artifactId>

Review Comment:
   This one provides transitive vulnerable dependency to junit:4.13
   https://devhub.checkmarx.com/cve-details/CVE-2020-15250
   According to okhhtp3 Version 5.0.0-alpha.2 [changelogs](https://square.github.io/okhttp/changelogs/changelog/)
   they removed it. We should upgrade to the next release when it's available @exceptionfactory  
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1323690358


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-services/src/main/java/org/apache/nifi/services/zendesk/ZendeskRecordSink.java:
##########
@@ -0,0 +1,280 @@
+/*
+ * 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.services.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnDisabled;
+import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.common.zendesk.ZendeskAuthenticationContext;
+import org.apache.nifi.common.zendesk.ZendeskAuthenticationType;
+import org.apache.nifi.common.zendesk.ZendeskClient;
+import org.apache.nifi.common.zendesk.validation.JsonPointerPropertyNameValidator;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.record.sink.RecordSinkService;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.RecordSetWriter;
+import org.apache.nifi.serialization.RecordSetWriterFactory;
+import org.apache.nifi.serialization.WriteResult;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordSet;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpResponseStatus;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.String.format;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.common.zendesk.util.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.common.zendesk.util.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.getResponseBody;
+
+@Tags({"zendesk", "record", "sink"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API." +
+        "The service requires a Zendesk account with configured access.")
+public class ZendeskRecordSink extends AbstractControllerService implements RecordSinkService {
+
+    private final ObjectMapper mapper = new ObjectMapper();
+    private Map<String, String> dynamicProperties;
+    private volatile RecordSetWriterFactory writerFactory;
+    private Cache<String, ObjectNode> recordCache;
+    private ZendeskClient zendeskClient;
+
+    private String commentBody;
+    private String subject;
+    private String priority;
+    private String type;
+
+    public static final PropertyDescriptor ZENDESK_TICKET_COMMENT_BODY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_COMMENT_BODY_NAME)
+            .displayName("Comment Body")
+            .description("The content or the path to the comment body in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_SUBJECT = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_SUBJECT_NAME)
+            .displayName("Subject")
+            .description("The content or the path to the subject in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_PRIORITY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_PRIORITY_NAME)
+            .displayName("Priority")
+            .description("The content or the path to the priority in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_TYPE = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_TYPE_NAME)
+            .displayName("Type")
+            .description("The content or the path to the type in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .build();
+
+    static final PropertyDescriptor CACHE_SIZE = new PropertyDescriptor.Builder()
+            .name("cache-size")
+            .displayName("Cache Size")
+            .description("Specifies how many Zendesk ticket should be cached.")
+            .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR)
+            .defaultValue("1000")
+            .required(true)
+            .build();
+
+    static final PropertyDescriptor CACHE_EXPIRATION = new PropertyDescriptor.Builder()
+            .name("cache-expiration")
+            .displayName("Cache Expiration")
+            .description("Specifies how long a Zendesk ticket that is cached should remain in the cache.")
+            .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+            .defaultValue("1 hour")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+            RecordSinkService.RECORD_WRITER_FACTORY,
+            WEB_CLIENT_SERVICE_PROVIDER,
+            ZENDESK_SUBDOMAIN,
+            ZENDESK_USER,
+            ZENDESK_AUTHENTICATION_TYPE,
+            ZENDESK_AUTHENTICATION_CREDENTIAL,
+            ZENDESK_TICKET_COMMENT_BODY,
+            ZENDESK_TICKET_SUBJECT,
+            ZENDESK_TICKET_PRIORITY,
+            ZENDESK_TICKET_TYPE,
+            CACHE_SIZE,
+            CACHE_EXPIRATION
+    ));
+
+    @Override
+    protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
+        return new PropertyDescriptor.Builder()
+                .name(propertyDescriptorName)
+                .required(false)
+                .addValidator(new JsonPointerPropertyNameValidator())
+                .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+                .dynamic(true)
+                .build();
+    }
+
+    @Override
+    public List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @Override
+    public WriteResult sendData(RecordSet recordSet, Map<String, String> attributes, boolean sendZeroResults) throws IOException {
+        final WriteResult writeResult;
+        List<ObjectNode> zendeskTickets = new ArrayList<>();
+
+        try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+            try (final RecordSetWriter writer = writerFactory.createWriter(getLogger(), recordSet.getSchema(), out, attributes)) {
+
+                writer.beginRecordSet();
+                Record record;
+                while ((record = recordSet.next()) != null) {
+                    ObjectNode baseTicketNode = mapper.createObjectNode();
+
+                    resolveFieldValue("/comment/body", commentBody, baseTicketNode, record);
+
+                    if (subject != null) {
+                        resolveFieldValue("/subject", subject, baseTicketNode, record);
+                    }
+
+                    if (priority != null) {
+                        resolveFieldValue("/priority", priority, baseTicketNode, record);
+                    }
+                    if (type != null) {
+                        resolveFieldValue("/type", type, baseTicketNode, record);
+                    }
+
+                    for (Map.Entry<String, String> dynamicProperty : dynamicProperties.entrySet()) {
+                        addDynamicField(dynamicProperty.getKey(), dynamicProperty.getValue(), baseTicketNode, record);
+                    }
+
+                    ObjectNode ticketNode = recordCache.getIfPresent(baseTicketNode.toString());
+                    if (ticketNode == null) {
+                        recordCache.put(baseTicketNode.toString(), baseTicketNode);
+                        zendeskTickets.add(baseTicketNode);
+                        writer.write(record);
+                        writer.flush();
+                    }
+                }
+                writeResult = writer.finishRecordSet();
+                writer.flush();
+            }
+        } catch (SchemaNotFoundException e) {
+            final String errorMessage = format("RecordSetWriter could not be created because the schema was not found. The schema name for the RecordSet to write is %s",
+                    recordSet.getSchema().getSchemaName());
+            throw new ProcessException(errorMessage, e);
+        }
+
+        if (zendeskTickets.size() > 0) {

Review Comment:
   Collection.isEmpty can be used here



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11992: Processor and sink service for filing tickets in Zendesk [nifi]

Posted by "turcsanyip (via GitHub)" <gi...@apache.org>.
turcsanyip commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1396967921


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-nar/src/main/resources/META-INF/NOTICE:
##########
@@ -4,36 +4,12 @@ Copyright 2015-2022 The Apache Software Foundation
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
 
-===========================================
-Apache Software License v2
-===========================================
+************************
+BSD License
+************************
 
-The following binary components are provided under the Apache Software License v2
-
-  (ASLv2) Apache Commons IO
-    The following NOTICE information applies:
-      Apache Commons IO
-      Copyright 2002-2017 The Apache Software Foundation
-
-  (ASLv2) Jackson JSON processor
+  (BSD) ANTLR 3 Runtime (org.antlr:antlr-runtime:3.5.3)

Review Comment:
   Based on other nar modules using Antlr, the license entry needs to be added in the `LICENSE` file instead of `NOTICE`.
   Please use [nifi-framework-nar](https://github.com/apache/nifi/blob/63364687d8bbe2af2d0031f745ca3c61144f8b4f/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/LICENSE#L204-L238) as a template.



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-services/src/main/java/org/apache/nifi/services/zendesk/ZendeskRecordSink.java:
##########
@@ -0,0 +1,239 @@
+/*
+ * 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.services.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnDisabled;
+import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.common.zendesk.ZendeskAuthenticationContext;
+import org.apache.nifi.common.zendesk.ZendeskAuthenticationType;
+import org.apache.nifi.common.zendesk.ZendeskClient;
+import org.apache.nifi.common.zendesk.validation.JsonPointerPropertyNameValidator;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.record.sink.RecordSinkService;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.RecordSetWriter;
+import org.apache.nifi.serialization.RecordSetWriterFactory;
+import org.apache.nifi.serialization.WriteResult;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordSet;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpResponseStatus;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.String.format;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.common.zendesk.util.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.common.zendesk.util.ZendeskRecordPathUtils.addField;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.getResponseBody;
+
+@Tags({"zendesk", "record", "sink"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API." +
+        "The service requires a Zendesk account with configured access.")
+public class ZendeskRecordSink extends AbstractControllerService implements RecordSinkService {
+
+    private final ObjectMapper mapper = new ObjectMapper();
+    private Map<String, String> dynamicProperties;
+    private volatile RecordSetWriterFactory writerFactory;
+    private Cache<String, ObjectNode> recordCache;
+    private ZendeskClient zendeskClient;
+
+    private String commentBody;
+    private String subject;
+    private String priority;
+    private String type;
+
+    static final PropertyDescriptor CACHE_SIZE = new PropertyDescriptor.Builder()
+            .name("cache-size")
+            .displayName("Cache Size")
+            .description("Specifies how many Zendesk ticket should be cached.")
+            .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR)
+            .defaultValue("1000")
+            .required(true)
+            .build();
+
+    static final PropertyDescriptor CACHE_EXPIRATION = new PropertyDescriptor.Builder()
+            .name("cache-expiration")
+            .displayName("Cache Expiration")
+            .description("Specifies how long a Zendesk ticket that is cached should remain in the cache.")
+            .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+            .defaultValue("1 hour")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+            RecordSinkService.RECORD_WRITER_FACTORY,

Review Comment:
   The Zendesk API accepts only Json input so it does not really need a configurable record writer. It should be removed and the request json can simply be created in the service without the writer.



##########
nifi-nar-bundles/nifi-extension-utils/nifi-record-path-property/src/main/java/org/apache/nifi/record/path/property/RecordPathPropertyUtil.java:
##########
@@ -0,0 +1,117 @@
+/*
+ * 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.record.path.property;
+
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.RecordPath;
+import org.apache.nifi.record.path.RecordPathResult;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.type.ChoiceDataType;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.util.stream.Collectors.toList;
+
+public final class RecordPathPropertyUtil {
+
+    private static final String NULL_VALUE = "null";
+    private static final Pattern RECORD_PATH_PATTERN = Pattern.compile("@\\{(/.*?)\\}");
+
+    private RecordPathPropertyUtil() {
+    }
+
+    /**
+     * Resolves the property value to be handled as a record path or a constant value.
+     *
+     * @param value  property value to be resolved
+     * @param record record to receive the value from if the field value is a record path
+     */
+    public static String resolvePropertyValue(String value, Record record) {

Review Comment:
   Minor correction in wording but it is a common utility method so it is worth being more precise:
   ```suggestion
        * @param propertyValue property value to be resolved
        * @param record record to resolve the value from if the property value is a record path
        */
       public static String resolvePropertyValue(String propertyValue, Record record) {
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1308043339


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/test/java/org/apache/nifi/commons/zendesk/ZendeskRecordPathUtilsTest.java:
##########
@@ -0,0 +1,99 @@
+/*
+ * 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.commons.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.serialization.SimpleRecordSchema;
+import org.apache.nifi.serialization.record.MapRecord;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.RecordSchema;
+import org.apache.nifi.serialization.record.type.ArrayDataType;
+import org.apache.nifi.serialization.record.type.RecordDataType;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class ZendeskRecordPathUtilsTest {
+
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    @Test
+    public void testFieldValueEvaluation() {
+        ObjectNode testNode;
+        Record record = initRecord();
+
+        testNode = mapper.createObjectNode();
+        resolveFieldValue("/a/b", "%{/field1}", testNode, record);
+        assertEquals("{\"a\":{\"b\":\"value1\"}}", testNode.toString());
+
+        testNode = mapper.createObjectNode();
+        resolveFieldValue("/a/b/c", "constant", testNode, record);
+        assertEquals("{\"a\":{\"b\":{\"c\":\"constant\"}}}", testNode.toString());

Review Comment:
   Please swap these 2 arguments so they are in the correct order: expected value, actual value.



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/test/java/org/apache/nifi/commons/zendesk/ZendeskRecordPathUtilsTest.java:
##########
@@ -0,0 +1,99 @@
+/*
+ * 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.commons.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.serialization.SimpleRecordSchema;
+import org.apache.nifi.serialization.record.MapRecord;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.RecordSchema;
+import org.apache.nifi.serialization.record.type.ArrayDataType;
+import org.apache.nifi.serialization.record.type.RecordDataType;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class ZendeskRecordPathUtilsTest {
+
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    @Test
+    public void testFieldValueEvaluation() {
+        ObjectNode testNode;
+        Record record = initRecord();
+
+        testNode = mapper.createObjectNode();
+        resolveFieldValue("/a/b", "%{/field1}", testNode, record);
+        assertEquals("{\"a\":{\"b\":\"value1\"}}", testNode.toString());
+
+        testNode = mapper.createObjectNode();
+        resolveFieldValue("/a/b/c", "constant", testNode, record);
+        assertEquals("{\"a\":{\"b\":{\"c\":\"constant\"}}}", testNode.toString());
+
+        ProcessException e1 = assertThrows(ProcessException.class, () -> resolveFieldValue("/a", "%{/field2}", mapper.createObjectNode(), record));
+        assertEquals(e1.getMessage(), "The provided RecordPath [/field2] points to a [ARRAY] type value");

Review Comment:
   Please swap these 2 arguments so they are in the correct order: expected value, actual value.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1308043435


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/test/java/org/apache/nifi/commons/zendesk/ZendeskRecordPathUtilsTest.java:
##########
@@ -0,0 +1,99 @@
+/*
+ * 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.commons.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.serialization.SimpleRecordSchema;
+import org.apache.nifi.serialization.record.MapRecord;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.RecordSchema;
+import org.apache.nifi.serialization.record.type.ArrayDataType;
+import org.apache.nifi.serialization.record.type.RecordDataType;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class ZendeskRecordPathUtilsTest {
+
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    @Test
+    public void testFieldValueEvaluation() {
+        ObjectNode testNode;
+        Record record = initRecord();
+
+        testNode = mapper.createObjectNode();
+        resolveFieldValue("/a/b", "%{/field1}", testNode, record);
+        assertEquals("{\"a\":{\"b\":\"value1\"}}", testNode.toString());
+
+        testNode = mapper.createObjectNode();
+        resolveFieldValue("/a/b/c", "constant", testNode, record);
+        assertEquals("{\"a\":{\"b\":{\"c\":\"constant\"}}}", testNode.toString());
+
+        ProcessException e1 = assertThrows(ProcessException.class, () -> resolveFieldValue("/a", "%{/field2}", mapper.createObjectNode(), record));
+        assertEquals(e1.getMessage(), "The provided RecordPath [/field2] points to a [ARRAY] type value");
+
+        ProcessException e2 = assertThrows(ProcessException.class, () -> resolveFieldValue("/a", "%{/field3}", mapper.createObjectNode(), record));
+        assertEquals(e2.getMessage(), "The provided RecordPath [/field3] points to a [RECORD] type value");

Review Comment:
   Please swap these 2 arguments so they are in the correct order: expected value, actual value.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11992: Processor and sink service for filing tickets in Zendesk [nifi]

Posted by "turcsanyip (via GitHub)" <gi...@apache.org>.
turcsanyip commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1348323163


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/processors/zendesk/PutZendeskTicket.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.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+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.common.zendesk.validation.JsonPointerPropertyNameValidator;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.MalformedRecordException;
+import org.apache.nifi.serialization.RecordReader;
+import org.apache.nifi.serialization.RecordReaderFactory;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.nifi.common.zendesk.ZendeskProperties.APPLICATION_JSON;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.REL_FAILURE_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.common.zendesk.util.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.common.zendesk.util.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.getResponseBody;
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+import static org.apache.nifi.flowfile.attributes.CoreAttributes.MIME_TYPE;
+import static org.apache.nifi.processors.zendesk.AbstractZendesk.RECORD_COUNT_ATTRIBUTE_NAME;
+import static org.apache.nifi.processors.zendesk.PutZendeskTicket.ERROR_CODE_ATTRIBUTE_NAME;
+import static org.apache.nifi.processors.zendesk.PutZendeskTicket.ERROR_MESSAGE_ATTRIBUTE_NAME;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.CREATED;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.OK;
+
+@Tags({"zendesk, ticket"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API.")
+@DynamicProperty(
+        name = "The path in the request object to add. The value needs be a valid JsonPointer.",
+        value = "The path in the incoming record to get the value from.",
+        expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
+        description = "Additional property to be added to the Zendesk request object.")
+@WritesAttributes({
+        @WritesAttribute(attribute = RECORD_COUNT_ATTRIBUTE_NAME, description = "The number of records processed."),
+        @WritesAttribute(attribute = ERROR_CODE_ATTRIBUTE_NAME, description = "The error code of from the response."),
+        @WritesAttribute(attribute = ERROR_MESSAGE_ATTRIBUTE_NAME, description = "The error message of from the response.")})
+public class PutZendeskTicket extends AbstractZendesk {
+
+    static final String ZENDESK_RECORD_READER_NAME = "zendesk-record-reader";
+    static final String ERROR_CODE_ATTRIBUTE_NAME = "error.code";
+    static final String ERROR_MESSAGE_ATTRIBUTE_NAME = "error.message";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    static final PropertyDescriptor RECORD_READER = new PropertyDescriptor.Builder()
+            .name(ZENDESK_RECORD_READER_NAME)
+            .displayName("Record Reader")
+            .description("Specifies the Controller Service to use for parsing incoming data and determining the data's schema.")
+            .identifiesControllerService(RecordReaderFactory.class)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_COMMENT_BODY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_COMMENT_BODY_NAME)
+            .displayName("Comment Body")
+            .description("The content or the path to the comment body in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .dependsOn(RECORD_READER)
+            .build();

Review Comment:
   `Comment Body` and further properties below are common with `ZendeskRecordSink` so they can be defined in `ZendeskProperties` and then can be customized here like adding the `dependsOn()` clause (which is specific to this processor).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11992: Processor and sink service for filing tickets in Zendesk [nifi]

Posted by "turcsanyip (via GitHub)" <gi...@apache.org>.
turcsanyip commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1389400340


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/processors/zendesk/AbstractZendesk.java:
##########
@@ -0,0 +1,62 @@
+/*
+ * 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.zendesk;
+
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.common.zendesk.ZendeskAuthenticationContext;
+import org.apache.nifi.common.zendesk.ZendeskAuthenticationType;
+import org.apache.nifi.common.zendesk.ZendeskClient;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import static org.apache.nifi.common.zendesk.ZendeskProperties.REL_SUCCESS_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_USER;
+
+public abstract class AbstractZendesk extends AbstractProcessor {
+
+    static final String RECORD_COUNT_ATTRIBUTE_NAME = "record.count";
+
+    ZendeskClient zendeskClient;
+
+    static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name(REL_SUCCESS_NAME)
+            .description("For FlowFiles created as a result of a successful HTTP request.")
+            .build();
+
+    @OnScheduled
+    public void onScheduled(ProcessContext context) {
+        final WebClientServiceProvider webClientServiceProvider = context.getProperty(WEB_CLIENT_SERVICE_PROVIDER).asControllerService(WebClientServiceProvider.class);
+        final String user = context.getProperty(ZENDESK_USER).evaluateAttributeExpressions().getValue();
+        final ZendeskAuthenticationType authenticationType = ZendeskAuthenticationType.forName(context.getProperty(ZENDESK_AUTHENTICATION_TYPE).getValue());
+        final String authenticationCredentials = context.getProperty(ZENDESK_AUTHENTICATION_CREDENTIAL).evaluateAttributeExpressions().getValue();
+        final String subDomain = context.getProperty(ZENDESK_SUBDOMAIN).evaluateAttributeExpressions().getValue();

Review Comment:
   Minor:
   ```suggestion
           final String subdomain = context.getProperty(ZENDESK_SUBDOMAIN).evaluateAttributeExpressions().getValue();
   ```



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-common/src/main/java/org/apache/nifi/common/zendesk/ZendeskProperties.java:
##########
@@ -0,0 +1,123 @@
+/*
+ * 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.common.zendesk;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+
+public final class ZendeskProperties {
+
+    public static final String WEB_CLIENT_SERVICE_PROVIDER_NAME = "web-client-service-provider";
+    public static final String ZENDESK_SUBDOMAIN_NAME = "zendesk-subdomain";
+    public static final String ZENDESK_USER_NAME = "zendesk-user";
+    public static final String ZENDESK_AUTHENTICATION_TYPE_NAME = "zendesk-authentication-type-name";
+    public static final String ZENDESK_AUTHENTICATION_CREDENTIAL_NAME = "zendesk-authentication-value-name";
+    public static final String ZENDESK_TICKET_COMMENT_BODY_NAME = "zendesk-comment-body";
+    public static final String ZENDESK_TICKET_SUBJECT_NAME = "zendesk-subject";
+    public static final String ZENDESK_TICKET_PRIORITY_NAME = "zendesk-priority";
+    public static final String ZENDESK_TICKET_TYPE_NAME = "zendesk-type";
+    public static final String HTTPS = "https";
+    public static final String APPLICATION_JSON = "application/json";
+    public static final String ZENDESK_HOST_TEMPLATE = "%s.zendesk.com";
+    public static final String ZENDESK_CREATE_TICKET_RESOURCE = "/api/v2/tickets";
+    public static final String ZENDESK_CREATE_TICKETS_RESOURCE = "/api/v2/tickets/create_many";
+
+    public static final String ZENDESK_TICKET_ROOT_NODE = "/ticket";
+    public static final String ZENDESK_TICKETS_ROOT_NODE = "/tickets";
+
+    public static final String REL_SUCCESS_NAME = "success";
+    public static final String REL_FAILURE_NAME = "failure";
+
+    private ZendeskProperties() {}
+
+    public static final PropertyDescriptor WEB_CLIENT_SERVICE_PROVIDER = new PropertyDescriptor.Builder()
+            .name(WEB_CLIENT_SERVICE_PROVIDER_NAME)
+            .displayName("Web Client Service Provider")
+            .description("Controller service for HTTP client operations.")
+            .identifiesControllerService(WebClientServiceProvider.class)
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_SUBDOMAIN = new PropertyDescriptor.Builder()
+            .name(ZENDESK_SUBDOMAIN_NAME)
+            .displayName("Subdomain Name")
+            .description("Name of the Zendesk subdomain.")
+            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_USER = new PropertyDescriptor.Builder()
+            .name(ZENDESK_USER_NAME)
+            .displayName("User Name")
+            .description("Login user to Zendesk subdomain.")
+            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_AUTHENTICATION_TYPE = new PropertyDescriptor.Builder()
+            .name(ZENDESK_AUTHENTICATION_TYPE_NAME)
+            .displayName("Authentication Type")
+            .description("Type of authentication to Zendesk API.")
+            .required(true)
+            .allowableValues(ZendeskAuthenticationType.class)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_AUTHENTICATION_CREDENTIAL = new PropertyDescriptor.Builder()
+            .name(ZENDESK_AUTHENTICATION_CREDENTIAL_NAME)
+            .displayName("Authentication Credential")
+            .description("Password or authentication token for Zendesk login user.")
+            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .sensitive(true)
+            .required(true)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .build();
+
+    public static final PropertyDescriptor.Builder ZENDESK_TICKET_COMMENT_BODY_BUILDER = new PropertyDescriptor.Builder()

Review Comment:
   I would not share the `PropertyDescriptor.Builder` instance but a `PropertyDescriptor` with a basic configuration instead. Then it can be customized via `PropertyDescriptor.Builder.fromPropertyDescriptor(PropertyDescriptor)` which creates a new builder. This way, different processors cannot change each other's builders.
   
   Interfering of builder configurations is not an issue here/now because `PutZendeskTicket` and `ZendeskRecordSink` are located in different NARs and the builder instance is not shared (as the result of the class loader isolation) but it can happen in the future that more processors will be added in the same bundle. Also, `fromPropertyDescriptor()` is more commonly used for sharing and extending  properties.
   
   



##########
nifi-nar-bundles/nifi-extension-utils/nifi-record-path-property/src/main/java/org/apache/nifi/record/RecordPathPropertyUtil.java:
##########
@@ -0,0 +1,121 @@
+/*
+ * 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.record;
+
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.RecordPath;
+import org.apache.nifi.record.path.RecordPathResult;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.type.ChoiceDataType;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.util.stream.Collectors.toList;
+
+public final class RecordPathPropertyUtil {
+
+    private static final String NULL_VALUE = "null";
+    private static final Pattern RECORD_PATH_PATTERN = Pattern.compile("@\\{(/.*?)\\}");
+
+    private RecordPathPropertyUtil() {
+    }
+
+    /**
+     * Resolves the property value to be handled as a record path or a constant value.
+     *
+     * @param propertyValue property value to be resolved
+     * @param record        record to receive the value from if the field value is a record path
+     * @param flowFile      FlowFile to evaluate attribute expressions
+     */
+    public static String resolveFieldValue(PropertyValue propertyValue, Record record, FlowFile flowFile) {

Review Comment:
   It is not really straightforward what "field" means here.
   My suggestion would be (also with the modified parameter list discussed in another thread above in this PR):
   ```suggestion
       public static String resolvePropertyValue(String propertyValue, Record record) {
   ```



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-common/src/main/java/org/apache/nifi/common/zendesk/util/ZendeskRecordPathUtils.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * 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.common.zendesk.util;
+
+import com.fasterxml.jackson.core.JsonPointer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.RecordPath;
+import org.apache.nifi.record.path.RecordPathResult;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.type.ChoiceDataType;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.util.stream.Collectors.toList;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKETS_ROOT_NODE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_ROOT_NODE;
+
+public final class ZendeskRecordPathUtils {

Review Comment:
   @mark-bathori Thanks for extracting `RecordPathPropertyUtil`! It looks good in general. However, it seems it was a bad idea of mine to suggest using `PropertyValue` as parameter type because it led to weird `PropertyValue` member fields in `ZendeskRecordSink`. If you agree, the `String propertyValue` parameter can be restored.



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-services/src/main/java/org/apache/nifi/services/zendesk/ZendeskRecordSink.java:
##########
@@ -0,0 +1,244 @@
+/*
+ * 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.services.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnDisabled;
+import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.common.zendesk.ZendeskAuthenticationContext;
+import org.apache.nifi.common.zendesk.ZendeskAuthenticationType;
+import org.apache.nifi.common.zendesk.ZendeskClient;
+import org.apache.nifi.common.zendesk.validation.JsonPointerPropertyNameValidator;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.record.sink.RecordSinkService;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.RecordSetWriter;
+import org.apache.nifi.serialization.RecordSetWriterFactory;
+import org.apache.nifi.serialization.WriteResult;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordSet;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpResponseStatus;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.String.format;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY_BUILDER;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY_BUILDER;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT_BUILDER;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE_BUILDER;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.common.zendesk.util.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.common.zendesk.util.ZendeskRecordPathUtils.addField;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.getResponseBody;
+
+@Tags({"zendesk", "record", "sink"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API." +
+        "The service requires a Zendesk account with configured access.")
+public class ZendeskRecordSink extends AbstractControllerService implements RecordSinkService {
+
+    private final ObjectMapper mapper = new ObjectMapper();
+    private Map<String, PropertyValue> dynamicProperties;
+    private volatile RecordSetWriterFactory writerFactory;
+    private Cache<String, ObjectNode> recordCache;
+    private ZendeskClient zendeskClient;
+
+    private PropertyValue commentBody;
+    private PropertyValue subject;
+    private PropertyValue priority;
+    private PropertyValue type;
+
+    public static final PropertyDescriptor ZENDESK_TICKET_COMMENT_BODY = ZENDESK_TICKET_COMMENT_BODY_BUILDER.build();
+    public static final PropertyDescriptor ZENDESK_TICKET_SUBJECT = ZENDESK_TICKET_SUBJECT_BUILDER.build();
+    public static final PropertyDescriptor ZENDESK_TICKET_PRIORITY = ZENDESK_TICKET_PRIORITY_BUILDER.build();
+    public static final PropertyDescriptor ZENDESK_TICKET_TYPE = ZENDESK_TICKET_TYPE_BUILDER.build();
+
+    static final PropertyDescriptor CACHE_SIZE = new PropertyDescriptor.Builder()
+            .name("cache-size")
+            .displayName("Cache Size")
+            .description("Specifies how many Zendesk ticket should be cached.")
+            .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR)
+            .defaultValue("1000")
+            .required(true)
+            .build();
+
+    static final PropertyDescriptor CACHE_EXPIRATION = new PropertyDescriptor.Builder()
+            .name("cache-expiration")
+            .displayName("Cache Expiration")
+            .description("Specifies how long a Zendesk ticket that is cached should remain in the cache.")
+            .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+            .defaultValue("1 hour")
+            .required(true)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+            RecordSinkService.RECORD_WRITER_FACTORY,
+            WEB_CLIENT_SERVICE_PROVIDER,
+            ZENDESK_SUBDOMAIN,
+            ZENDESK_USER,
+            ZENDESK_AUTHENTICATION_TYPE,
+            ZENDESK_AUTHENTICATION_CREDENTIAL,
+            ZENDESK_TICKET_COMMENT_BODY,
+            ZENDESK_TICKET_SUBJECT,
+            ZENDESK_TICKET_PRIORITY,
+            ZENDESK_TICKET_TYPE,
+            CACHE_SIZE,
+            CACHE_EXPIRATION
+    ));
+
+    @Override
+    protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
+        return new PropertyDescriptor.Builder()
+                .name(propertyDescriptorName)
+                .required(false)
+                .addValidator(new JsonPointerPropertyNameValidator())
+                .expressionLanguageSupported(ExpressionLanguageScope.ENVIRONMENT)
+                .dynamic(true)
+                .build();
+    }
+
+    @Override
+    public List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @Override
+    public WriteResult sendData(RecordSet recordSet, Map<String, String> attributes, boolean sendZeroResults) throws IOException {
+        final WriteResult writeResult;
+        List<ObjectNode> zendeskTickets = new ArrayList<>();
+
+        try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+            try (final RecordSetWriter writer = writerFactory.createWriter(getLogger(), recordSet.getSchema(), out, attributes)) {
+
+                writer.beginRecordSet();
+                Record record;
+                while ((record = recordSet.next()) != null) {
+                    ObjectNode baseTicketNode = mapper.createObjectNode();
+                    addField("/comment/body", commentBody, baseTicketNode, record);
+                    addField("/subject", subject, baseTicketNode, record);
+                    addField("/priority", priority, baseTicketNode, record);
+                    addField("/type", type, baseTicketNode, record);
+
+                    for (Map.Entry<String, PropertyValue> dynamicProperty : dynamicProperties.entrySet()) {
+                        addDynamicField(dynamicProperty.getKey(), dynamicProperty.getValue(), baseTicketNode, record, null);
+                    }
+
+                    ObjectNode ticketNode = recordCache.getIfPresent(baseTicketNode.toString());
+                    if (ticketNode == null) {
+                        recordCache.put(baseTicketNode.toString(), baseTicketNode);
+                        zendeskTickets.add(baseTicketNode);
+                        writer.write(record);
+                        writer.flush();
+                    }
+                }
+                writeResult = writer.finishRecordSet();
+                writer.flush();
+            }
+        } catch (SchemaNotFoundException e) {
+            final String errorMessage = format("RecordSetWriter could not be created because the schema was not found. The schema name for the RecordSet to write is %s",
+                    recordSet.getSchema().getSchemaName());
+            throw new ProcessException(errorMessage, e);
+        }
+
+        if (!zendeskTickets.isEmpty()) {
+            try {
+                final InputStream inputStream = createRequestObject(zendeskTickets);
+                final URI uri = createUri(zendeskTickets.size());
+                final HttpResponseEntity response = zendeskClient.performPostRequest(uri, inputStream);
+
+                if (response.statusCode() != HttpResponseStatus.CREATED.getCode() && response.statusCode() != HttpResponseStatus.OK.getCode()) {
+                    getLogger().error("Failed to create zendesk ticket, HTTP status={}, response={}", response.statusCode(), getResponseBody(response));
+                }
+            } catch (IOException e) {
+                throw new IOException("Failed to post request to Zendesk", e);
+            }
+        }
+
+        return writeResult;
+    }
+
+    @OnEnabled
+    public void onEnabled(final ConfigurationContext context) {
+        writerFactory = context.getProperty(RecordSinkService.RECORD_WRITER_FACTORY).asControllerService(RecordSetWriterFactory.class);
+        dynamicProperties = getDynamicProperties(context, context.getProperties());
+
+        commentBody = context.getProperty(ZENDESK_TICKET_COMMENT_BODY);
+        subject = context.getProperty(ZENDESK_TICKET_SUBJECT);
+        priority = context.getProperty(ZENDESK_TICKET_PRIORITY);
+        type = context.getProperty(ZENDESK_TICKET_TYPE);
+
+        final String subDomain = context.getProperty(ZENDESK_SUBDOMAIN).evaluateAttributeExpressions().getValue();

Review Comment:
   Minor:
   ```suggestion
           final String subdomain = context.getProperty(ZENDESK_SUBDOMAIN).evaluateAttributeExpressions().getValue();
   ```



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-services-nar/pom.xml:
##########
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-zendesk-bundle</artifactId>
+        <version>2.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>nifi-zendesk-services-nar</artifactId>
+    <packaging>nar</packaging>
+
+    <properties>
+        <maven.javadoc.skip>true</maven.javadoc.skip>
+        <source.skip>true</source.skip>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-zendesk-services</artifactId>
+            <version>2.0.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-standard-services-api-nar</artifactId>

Review Comment:
   The parent NAR needs to be updated to `nifi-standard-shared-nar` due to [NIFI-12266](https://issues.apache.org/jira/browse/NIFI-12266).
   
   Please also rebase the PR branch onto the current main.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11992: Processor and sink service for filing tickets in Zendesk [nifi]

Posted by "asfgit (via GitHub)" <gi...@apache.org>.
asfgit closed pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk
URL: https://github.com/apache/nifi/pull/7644


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] mark-bathori commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "mark-bathori (via GitHub)" <gi...@apache.org>.
mark-bathori commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1305702931


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/pom.xml:
##########
@@ -49,6 +70,16 @@
             <groupId>commons-io</groupId>
             <artifactId>commons-io</artifactId>
         </dependency>
+        <dependency>
+            <groupId>javax.json</groupId>
+            <artifactId>javax.json-api</artifactId>
+            <version>1.1.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish</groupId>
+            <artifactId>javax.json</artifactId>
+            <version>1.1.4</version>
+        </dependency>

Review Comment:
   Yes you are right, I used these dependencies in a previous solution for building Json Request Object but forgot to remove them. Thanks for pointing this out



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1315812749


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/processors/zendesk/PutZendeskTicket.java:
##########
@@ -0,0 +1,294 @@
+/*
+ * 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.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.commons.zendesk.ZendeskAuthenticationContext;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.MalformedRecordException;
+import org.apache.nifi.serialization.RecordReader;
+import org.apache.nifi.serialization.RecordReaderFactory;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.String.format;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.HTTPS;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_FAILURE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_SUCCESS_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_HOST_TEMPLATE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.performPostRequest;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.responseBodyToString;
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.CREATED;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.OK;
+
+@Tags({"zendesk, ticket"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API.")
+@DynamicProperty(
+        name = "The path in the request object to add.",
+        value = "The path in the incoming record to get the value from.",
+        expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
+        description = "Additional property to be added to the Zendesk request object.")
+public class PutZendeskTicket extends AbstractProcessor {
+
+    static final String ZENDESK_RECORD_READER_NAME = "zendesk-record-reader";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    static final PropertyDescriptor RECORD_READER = new PropertyDescriptor.Builder()
+            .name(ZENDESK_RECORD_READER_NAME)
+            .displayName("Record Reader")
+            .description("Specifies the Controller Service to use for parsing incoming data and determining the data's schema.")
+            .identifiesControllerService(RecordReaderFactory.class)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_COMMENT_BODY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_COMMENT_BODY_NAME)
+            .displayName("Comment Body")
+            .description("The content or the path to the comment body in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_SUBJECT = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_SUBJECT_NAME)
+            .displayName("Subject")
+            .description("The content or the path to the subject in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_PRIORITY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_PRIORITY_NAME)
+            .displayName("Priority")
+            .description("The content or the path to the priority in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_TYPE = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_TYPE_NAME)
+            .displayName("Type")
+            .description("The content or the path to the type in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+            RECORD_READER,
+            WEB_CLIENT_SERVICE_PROVIDER,
+            ZENDESK_SUBDOMAIN,
+            ZENDESK_USER,
+            ZENDESK_AUTHENTICATION_TYPE,
+            ZENDESK_AUTHENTICATION_CREDENTIAL,
+            ZENDESK_TICKET_COMMENT_BODY,
+            ZENDESK_TICKET_SUBJECT,
+            ZENDESK_TICKET_PRIORITY,
+            ZENDESK_TICKET_TYPE
+    ));
+
+    private volatile WebClientServiceProvider webClientServiceProvider;
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name(REL_SUCCESS_NAME)
+            .description("For FlowFiles created as a result of a successful HTTP request.")
+            .build();
+
+    public static final Relationship REL_FAILURE = new Relationship.Builder()
+            .name(REL_FAILURE_NAME)
+            .description("A FlowFile is routed to this relationship if the operation failed and retrying the operation will also fail, such as an invalid data or schema.")
+            .build();
+
+    @Override
+    protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
+        return new PropertyDescriptor.Builder()
+                .name(propertyDescriptorName)
+                .required(false)
+                .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)

Review Comment:
   ATTRIBUTE_KEY_PROPERTY_NAME validator can be used here instead



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1307989640


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/processors/zendesk/PutZendeskTicket.java:
##########
@@ -0,0 +1,294 @@
+/*
+ * 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.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.commons.zendesk.ZendeskAuthenticationContext;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.MalformedRecordException;
+import org.apache.nifi.serialization.RecordReader;
+import org.apache.nifi.serialization.RecordReaderFactory;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.String.format;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.HTTPS;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_FAILURE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_SUCCESS_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_HOST_TEMPLATE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.performPostRequest;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.responseBodyToString;
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.CREATED;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.OK;
+
+@Tags({"zendesk, ticket"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API.")
+@DynamicProperty(
+        name = "The path in the request object to add.",
+        value = "The path in the incoming record to get the value from.",
+        expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
+        description = "Additional property to be added to the Zendesk request object.")
+public class PutZendeskTicket extends AbstractProcessor {
+
+    static final String ZENDESK_RECORD_READER_NAME = "zendesk-record-reader";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    static final PropertyDescriptor RECORD_READER = new PropertyDescriptor.Builder()
+            .name(ZENDESK_RECORD_READER_NAME)
+            .displayName("Record Reader")
+            .description("Specifies the Controller Service to use for parsing incoming data and determining the data's schema.")
+            .identifiesControllerService(RecordReaderFactory.class)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_COMMENT_BODY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_COMMENT_BODY_NAME)
+            .displayName("Comment Body")
+            .description("The content or the path to the comment body in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_SUBJECT = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_SUBJECT_NAME)
+            .displayName("Subject")
+            .description("The content or the path to the subject in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_PRIORITY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_PRIORITY_NAME)
+            .displayName("Priority")
+            .description("The content or the path to the priority in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_TYPE = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_TYPE_NAME)
+            .displayName("Type")
+            .description("The content or the path to the type in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+            RECORD_READER,
+            WEB_CLIENT_SERVICE_PROVIDER,
+            ZENDESK_SUBDOMAIN,
+            ZENDESK_USER,
+            ZENDESK_AUTHENTICATION_TYPE,
+            ZENDESK_AUTHENTICATION_CREDENTIAL,
+            ZENDESK_TICKET_COMMENT_BODY,
+            ZENDESK_TICKET_SUBJECT,
+            ZENDESK_TICKET_PRIORITY,
+            ZENDESK_TICKET_TYPE
+    ));
+
+    private volatile WebClientServiceProvider webClientServiceProvider;
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name(REL_SUCCESS_NAME)
+            .description("For FlowFiles created as a result of a successful HTTP request.")
+            .build();
+
+    public static final Relationship REL_FAILURE = new Relationship.Builder()
+            .name(REL_FAILURE_NAME)
+            .description("A FlowFile is routed to this relationship if the operation failed and retrying the operation will also fail, such as an invalid data or schema.")
+            .build();
+
+    @Override
+    protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
+        return new PropertyDescriptor.Builder()
+                .name(propertyDescriptorName)
+                .required(false)
+                .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+                .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+                .dynamic(true)
+                .build();
+    }
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    public static final Set<Relationship> RELATIONSHIPS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+            REL_SUCCESS,
+            REL_FAILURE
+    )));
+
+    @Override
+    public List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @OnScheduled
+    public void onScheduled(ProcessContext context) {
+        webClientServiceProvider = context.getProperty(WEB_CLIENT_SERVICE_PROVIDER).asControllerService(WebClientServiceProvider.class);
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) {
+        final FlowFile flowFile = session.get();
+        if (flowFile == null) {
+            return;
+        }
+
+        long startNanos = System.nanoTime();
+        HttpResponseEntity response;
+        URI uri;
+        final RecordReaderFactory readerFactory = context.getProperty(RECORD_READER).asControllerService(RecordReaderFactory.class);
+        final ZendeskAuthenticationContext authenticationContext = new ZendeskAuthenticationContext(context);
+
+        if (readerFactory == null) {
+            try (final InputStream inputStream = session.read(flowFile)) {
+                final HttpUriBuilder uriBuilder = uriBuilder(authenticationContext.getSubDomain(), ZENDESK_CREATE_TICKET_RESOURCE);
+                uri = uriBuilder.build();
+                response = performPostRequest(authenticationContext, webClientServiceProvider, uri, inputStream);
+            } catch (IOException e) {
+                throw new ProcessException("Could not read incoming FlowFile", e);
+            }
+        } else {
+            final String commentBody = context.getProperty(ZENDESK_TICKET_COMMENT_BODY).evaluateAttributeExpressions().getValue();
+            final String subject = context.getProperty(ZENDESK_TICKET_SUBJECT).evaluateAttributeExpressions().getValue();
+            final String priority = context.getProperty(ZENDESK_TICKET_PRIORITY).evaluateAttributeExpressions().getValue();
+            final String type = context.getProperty(ZENDESK_TICKET_TYPE).evaluateAttributeExpressions().getValue();
+            final Map<String, String> dynamicProperties = getDynamicProperties(context, context.getProperties());

Review Comment:
   Discussion: Should we allow dynamic properties that are not RecordPath properties? They can be added to the outgoing FlowFile.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1323684185


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-common/src/main/java/org/apache/nifi/common/zendesk/util/ZendeskUtils.java:
##########
@@ -0,0 +1,107 @@
+/*
+ * 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.common.zendesk.util;
+
+import com.fasterxml.jackson.core.JsonPointer;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.common.zendesk.ZendeskProperties;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.context.PropertyContext;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public final class ZendeskUtils {
+
+    private static final Logger logger = LoggerFactory.getLogger(ZendeskUtils.class);

Review Comment:
   The logger is not used anymore, it can be removed.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1325809440


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-common/src/main/java/org/apache/nifi/common/zendesk/util/ZendeskRecordPathUtils.java:
##########
@@ -0,0 +1,169 @@
+/*
+ * 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.common.zendesk.util;
+
+import com.fasterxml.jackson.core.JsonPointer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.RecordPath;
+import org.apache.nifi.record.path.RecordPathResult;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.type.ChoiceDataType;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.util.stream.Collectors.toList;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKETS_ROOT_NODE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_ROOT_NODE;
+
+public final class ZendeskRecordPathUtils {
+
+    private static final String NULL_VALUE = "null";
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final Pattern RECORD_PATH_PATTERN = Pattern.compile("%\\{(.*?)\\}");
+
+    private ZendeskRecordPathUtils() {}
+
+    /**
+     * Resolves the input field value to be handled as a record path or a constant value.
+     * @param path path in the request object
+     * @param value field value to be resolved
+     * @param baseTicketNode base request object where the field value will be added
+     * @param record record to receive the value from if the field value is a record path
+     */
+    public static void resolveFieldValue(String path, String value, ObjectNode baseTicketNode, Record record) {
+        final Matcher matcher = RECORD_PATH_PATTERN.matcher(value);
+        if (matcher.matches()) {
+            addNewNodeAtPath(baseTicketNode, JsonPointer.compile(path), new TextNode(resolveRecordState(matcher.group(1), record)));
+        } else {
+            addNewNodeAtPath(baseTicketNode, JsonPointer.compile(path), new TextNode(value));
+        }
+    }
+
+    /**
+     * Adds a user defined dynamic field to the request object. If the user specifies the path in the request object as full path (starting with '/ticket' or '/tickets')
+     * the method removes them since the root path is specified later based on the number of records.
+     * @param path path in the request object
+     * @param value dynamic field value
+     * @param baseTicketNode base request object where the field value will be added
+     * @param record record to receive the value from if the field value is a record path
+     */
+    public static void addDynamicField(String path, String value, ObjectNode baseTicketNode, Record record) {
+        if (path.startsWith(ZENDESK_TICKET_ROOT_NODE)) {
+            path = path.substring(7);
+        } else if (path.startsWith(ZENDESK_TICKETS_ROOT_NODE)) {
+            path = path.substring(8);
+        }
+
+        resolveFieldValue(path, value, baseTicketNode, record);
+    }
+
+    private static String resolveRecordState(String pathValue, final Record record) {
+        final RecordPath recordPath = RecordPath.compile(pathValue);
+        final RecordPathResult result = recordPath.evaluate(record);
+        final List<FieldValue> fieldValues = result.getSelectedFields().collect(toList());
+        final FieldValue fieldValue = getMatchingFieldValue(recordPath, fieldValues);
+
+        if (fieldValue.getValue() == null || fieldValue.getValue() == NULL_VALUE) {
+            return null;
+        }
+
+        return getFieldValue(recordPath, fieldValue);
+    }
+
+    /**
+     * The method checks the field's type and filters out every non-compatible type.
+     * @param recordPath path to the requested field
+     * @param fieldValue record field
+     * @return value of the record field
+     */
+    private static String getFieldValue(final RecordPath recordPath, FieldValue fieldValue) {
+        final RecordFieldType fieldType = fieldValue.getField().getDataType().getFieldType();
+
+        if (fieldType == RecordFieldType.RECORD || fieldType == RecordFieldType.ARRAY || fieldType == RecordFieldType.MAP) {
+            throw new ProcessException(String.format("The provided RecordPath [%s] points to a [%s] type value", recordPath, fieldType));
+        }
+
+        if (fieldType == RecordFieldType.CHOICE) {
+            final ChoiceDataType choiceDataType = (ChoiceDataType) fieldValue.getField().getDataType();
+            final List<DataType> possibleTypes = choiceDataType.getPossibleSubTypes();
+            if (possibleTypes.stream().anyMatch(type -> type.getFieldType() == RecordFieldType.RECORD)) {
+                throw new ProcessException(String.format("The provided RecordPath [%s] points to a [CHOICE] type value with Record subtype", recordPath));
+            }
+        }
+
+        return String.valueOf(fieldValue.getValue());
+    }
+
+    /**
+     * The method checks if only one result were received for the give record path.
+     * @param recordPath path to the requested field
+     * @param resultList result list
+     * @return matching field
+     */
+    private static FieldValue getMatchingFieldValue(final RecordPath recordPath, final List<FieldValue> resultList) {
+        if (resultList.isEmpty()) {
+            throw new ProcessException(String.format("Evaluated RecordPath [%s] against Record but got no results", recordPath));
+        }
+
+        if (resultList.size() > 1) {
+            throw new ProcessException(String.format("Evaluated RecordPath [%s] against Record and received multiple distinct results [%s]", recordPath, resultList));
+        }
+
+        return resultList.get(0);
+    }
+
+    /**
+     * Adds a new node on the provided path with the give value to the request object.
+     * @param baseNode base object where the new node will be added
+     * @param path path of the new node
+     * @param value value of the new node
+     */
+    public static void addNewNodeAtPath(ObjectNode baseNode, JsonPointer path, JsonNode value) {

Review Comment:
   This method could be simplified by extracting several part into new submethods.
   I recommend extracting parent node creation and setting the node into new methods.
   This would also eliminate direct recursion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [nifi] Lehel44 commented on a diff in pull request #7644: NIFI-11992: Processor and sink service for filing tickets in Zendesk

Posted by "Lehel44 (via GitHub)" <gi...@apache.org>.
Lehel44 commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1315685170


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/processors/zendesk/PutZendeskTicket.java:
##########
@@ -0,0 +1,294 @@
+/*
+ * 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.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.commons.zendesk.ZendeskAuthenticationContext;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.MalformedRecordException;
+import org.apache.nifi.serialization.RecordReader;
+import org.apache.nifi.serialization.RecordReaderFactory;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.String.format;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.HTTPS;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_FAILURE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.REL_SUCCESS_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_HOST_TEMPLATE;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE_NAME;
+import static org.apache.nifi.commons.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.commons.zendesk.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.performPostRequest;
+import static org.apache.nifi.commons.zendesk.ZendeskUtils.responseBodyToString;
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.CREATED;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.OK;
+
+@Tags({"zendesk, ticket"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API.")
+@DynamicProperty(
+        name = "The path in the request object to add.",
+        value = "The path in the incoming record to get the value from.",
+        expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
+        description = "Additional property to be added to the Zendesk request object.")
+public class PutZendeskTicket extends AbstractProcessor {
+
+    static final String ZENDESK_RECORD_READER_NAME = "zendesk-record-reader";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    static final PropertyDescriptor RECORD_READER = new PropertyDescriptor.Builder()
+            .name(ZENDESK_RECORD_READER_NAME)
+            .displayName("Record Reader")
+            .description("Specifies the Controller Service to use for parsing incoming data and determining the data's schema.")
+            .identifiesControllerService(RecordReaderFactory.class)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_COMMENT_BODY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_COMMENT_BODY_NAME)
+            .displayName("Comment Body")
+            .description("The content or the path to the comment body in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_SUBJECT = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_SUBJECT_NAME)
+            .displayName("Subject")
+            .description("The content or the path to the subject in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_PRIORITY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_PRIORITY_NAME)
+            .displayName("Priority")
+            .description("The content or the path to the priority in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_TYPE = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_TYPE_NAME)
+            .displayName("Type")
+            .description("The content or the path to the type in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .dependsOn(RECORD_READER)
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
+            RECORD_READER,
+            WEB_CLIENT_SERVICE_PROVIDER,
+            ZENDESK_SUBDOMAIN,
+            ZENDESK_USER,
+            ZENDESK_AUTHENTICATION_TYPE,
+            ZENDESK_AUTHENTICATION_CREDENTIAL,
+            ZENDESK_TICKET_COMMENT_BODY,
+            ZENDESK_TICKET_SUBJECT,
+            ZENDESK_TICKET_PRIORITY,
+            ZENDESK_TICKET_TYPE
+    ));
+
+    private volatile WebClientServiceProvider webClientServiceProvider;
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+            .name(REL_SUCCESS_NAME)
+            .description("For FlowFiles created as a result of a successful HTTP request.")
+            .build();
+
+    public static final Relationship REL_FAILURE = new Relationship.Builder()
+            .name(REL_FAILURE_NAME)
+            .description("A FlowFile is routed to this relationship if the operation failed and retrying the operation will also fail, such as an invalid data or schema.")
+            .build();
+
+    @Override
+    protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
+        return new PropertyDescriptor.Builder()
+                .name(propertyDescriptorName)
+                .required(false)
+                .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+                .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+                .dynamic(true)
+                .build();
+    }
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    public static final Set<Relationship> RELATIONSHIPS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+            REL_SUCCESS,
+            REL_FAILURE
+    )));
+
+    @Override
+    public List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @OnScheduled
+    public void onScheduled(ProcessContext context) {
+        webClientServiceProvider = context.getProperty(WEB_CLIENT_SERVICE_PROVIDER).asControllerService(WebClientServiceProvider.class);
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) {
+        final FlowFile flowFile = session.get();
+        if (flowFile == null) {
+            return;
+        }
+
+        long startNanos = System.nanoTime();
+        HttpResponseEntity response;
+        URI uri;
+        final RecordReaderFactory readerFactory = context.getProperty(RECORD_READER).asControllerService(RecordReaderFactory.class);
+        final ZendeskAuthenticationContext authenticationContext = new ZendeskAuthenticationContext(context);
+
+        if (readerFactory == null) {
+            try (final InputStream inputStream = session.read(flowFile)) {
+                final HttpUriBuilder uriBuilder = uriBuilder(authenticationContext.getSubDomain(), ZENDESK_CREATE_TICKET_RESOURCE);
+                uri = uriBuilder.build();
+                response = performPostRequest(authenticationContext, webClientServiceProvider, uri, inputStream);
+            } catch (IOException e) {
+                throw new ProcessException("Could not read incoming FlowFile", e);
+            }
+        } else {
+            final String commentBody = context.getProperty(ZENDESK_TICKET_COMMENT_BODY).evaluateAttributeExpressions().getValue();
+            final String subject = context.getProperty(ZENDESK_TICKET_SUBJECT).evaluateAttributeExpressions().getValue();
+            final String priority = context.getProperty(ZENDESK_TICKET_PRIORITY).evaluateAttributeExpressions().getValue();
+            final String type = context.getProperty(ZENDESK_TICKET_TYPE).evaluateAttributeExpressions().getValue();
+            final Map<String, String> dynamicProperties = getDynamicProperties(context, context.getProperties());
+            List<ObjectNode> zendeskTickets = new ArrayList<>();
+
+            try (final InputStream in = session.read(flowFile); final RecordReader reader = readerFactory.createRecordReader(flowFile, in, getLogger())) {
+                Record record;
+
+                while ((record = reader.nextRecord()) != null) {
+                    ObjectNode baseTicketNode = mapper.createObjectNode();
+                    resolveFieldValue("/comment/body", commentBody, baseTicketNode, record);
+
+                    if (subject != null) {
+                        resolveFieldValue("/subject", subject, baseTicketNode, record);
+                    }
+                    if (priority != null) {
+                        resolveFieldValue("/priority", priority, baseTicketNode, record);
+                    }
+                    if (type != null) {
+                        resolveFieldValue("/type", type, baseTicketNode, record);
+                    }
+                    for (Map.Entry<String, String> dynamicProperty : dynamicProperties.entrySet()) {
+                        addDynamicField(dynamicProperty.getKey(), dynamicProperty.getValue(), baseTicketNode, record);
+                    }
+                    zendeskTickets.add(baseTicketNode);
+                }
+
+            } catch (IOException | SchemaNotFoundException | MalformedRecordException e) {
+                getLogger().error("Error occurred while creating zendesk tickets", e);
+                session.transfer(session.penalize(flowFile), REL_FAILURE);
+                return;
+            }
+
+            if (zendeskTickets.size() == 0) {
+                getLogger().info("No record were received");

Review Comment:
   ```suggestion
                   getLogger().info("No records were received");
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11992: Processor and sink service for filing tickets in Zendesk [nifi]

Posted by "turcsanyip (via GitHub)" <gi...@apache.org>.
turcsanyip commented on code in PR #7644:
URL: https://github.com/apache/nifi/pull/7644#discussion_r1342722329


##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-common/src/main/java/org/apache/nifi/common/zendesk/ZendeskAuthenticationContext.java:
##########
@@ -0,0 +1,48 @@
+/*
+ * 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.common.zendesk;
+
+public class ZendeskAuthenticationContext {
+
+    private final String subDomain;

Review Comment:
   Minor:
   ```suggestion
       private final String subdomain;
   ```



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-processors/src/main/java/org/apache/nifi/processors/zendesk/PutZendeskTicket.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.zendesk;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+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.common.zendesk.validation.JsonPointerPropertyNameValidator;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.schema.access.SchemaNotFoundException;
+import org.apache.nifi.serialization.MalformedRecordException;
+import org.apache.nifi.serialization.RecordReader;
+import org.apache.nifi.serialization.RecordReaderFactory;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.nifi.common.zendesk.ZendeskProperties.APPLICATION_JSON;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.REL_FAILURE_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.WEB_CLIENT_SERVICE_PROVIDER;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_CREDENTIAL;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_AUTHENTICATION_TYPE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKETS_RESOURCE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_CREATE_TICKET_RESOURCE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_SUBDOMAIN;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_COMMENT_BODY_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_PRIORITY_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_SUBJECT_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_TYPE_NAME;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_USER;
+import static org.apache.nifi.common.zendesk.util.ZendeskRecordPathUtils.addDynamicField;
+import static org.apache.nifi.common.zendesk.util.ZendeskRecordPathUtils.resolveFieldValue;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.createRequestObject;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.getDynamicProperties;
+import static org.apache.nifi.common.zendesk.util.ZendeskUtils.getResponseBody;
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+import static org.apache.nifi.flowfile.attributes.CoreAttributes.MIME_TYPE;
+import static org.apache.nifi.processors.zendesk.AbstractZendesk.RECORD_COUNT_ATTRIBUTE_NAME;
+import static org.apache.nifi.processors.zendesk.PutZendeskTicket.ERROR_CODE_ATTRIBUTE_NAME;
+import static org.apache.nifi.processors.zendesk.PutZendeskTicket.ERROR_MESSAGE_ATTRIBUTE_NAME;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.CREATED;
+import static org.apache.nifi.web.client.api.HttpResponseStatus.OK;
+
+@Tags({"zendesk, ticket"})
+@CapabilityDescription("Create Zendesk tickets using the Zendesk API.")
+@DynamicProperty(
+        name = "The path in the request object to add. The value needs be a valid JsonPointer.",
+        value = "The path in the incoming record to get the value from.",
+        expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
+        description = "Additional property to be added to the Zendesk request object.")
+@WritesAttributes({
+        @WritesAttribute(attribute = RECORD_COUNT_ATTRIBUTE_NAME, description = "The number of records processed."),
+        @WritesAttribute(attribute = ERROR_CODE_ATTRIBUTE_NAME, description = "The error code of from the response."),
+        @WritesAttribute(attribute = ERROR_MESSAGE_ATTRIBUTE_NAME, description = "The error message of from the response.")})
+public class PutZendeskTicket extends AbstractZendesk {
+
+    static final String ZENDESK_RECORD_READER_NAME = "zendesk-record-reader";
+    static final String ERROR_CODE_ATTRIBUTE_NAME = "error.code";
+    static final String ERROR_MESSAGE_ATTRIBUTE_NAME = "error.message";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    static final PropertyDescriptor RECORD_READER = new PropertyDescriptor.Builder()
+            .name(ZENDESK_RECORD_READER_NAME)
+            .displayName("Record Reader")
+            .description("Specifies the Controller Service to use for parsing incoming data and determining the data's schema.")
+            .identifiesControllerService(RecordReaderFactory.class)
+            .build();
+
+    public static final PropertyDescriptor ZENDESK_TICKET_COMMENT_BODY = new PropertyDescriptor.Builder()
+            .name(ZENDESK_TICKET_COMMENT_BODY_NAME)
+            .displayName("Comment Body")
+            .description("The content or the path to the comment body in the incoming record.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+            .required(true)
+            .dependsOn(RECORD_READER)
+            .build();

Review Comment:
   `Comment Body` and further properties below are common with `ZendeskRecordSink` so they can be defined in `ZendeskProperties` and then can be customized here like adding the `dependOn()` clause (which is specific to the processor).



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-common/src/main/java/org/apache/nifi/common/zendesk/util/ZendeskRecordPathUtils.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * 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.common.zendesk.util;
+
+import com.fasterxml.jackson.core.JsonPointer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.RecordPath;
+import org.apache.nifi.record.path.RecordPathResult;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.type.ChoiceDataType;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.util.stream.Collectors.toList;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKETS_ROOT_NODE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_ROOT_NODE;
+
+public final class ZendeskRecordPathUtils {
+
+    private static final String NULL_VALUE = "null";
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final Pattern RECORD_PATH_PATTERN = Pattern.compile("%\\{(.*?)\\}");

Review Comment:
   The pattern does not the require leading `/` in the record path. Is it a valid record path which does not start with `/`?



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-common/src/main/java/org/apache/nifi/common/zendesk/util/ZendeskRecordPathUtils.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * 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.common.zendesk.util;
+
+import com.fasterxml.jackson.core.JsonPointer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.RecordPath;
+import org.apache.nifi.record.path.RecordPathResult;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.type.ChoiceDataType;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.util.stream.Collectors.toList;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKETS_ROOT_NODE;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_TICKET_ROOT_NODE;
+
+public final class ZendeskRecordPathUtils {

Review Comment:
   This util class covers 2 main functionalities in my understanding:
   
   1. resolve a record path (coming from a property value)
   2. add a value (e.g. the resolved record field) to a Json object (using a json pointer to locate the extension point)
   
   I would separate these two areas and promote # 1 to a common module (# 2 may worth being extracted to a common too).
   
   Suggested module for the record path util: `nifi-extension-utils/nifi-record-path-property` 
   Suggested class name: `RecordPathPropertyUtil`
   
   The resolve method may accept `PropertyValue` parameter because it is not a generic record path evaluator but related to component properties.



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-common/src/main/java/org/apache/nifi/common/zendesk/ZendeskClient.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.nifi.common.zendesk;
+
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.OptionalLong;
+
+import static java.lang.String.format;
+import static java.util.Base64.getEncoder;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.HTTPS;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_HOST_TEMPLATE;
+
+public class ZendeskClient {
+
+    private static final String AUTHORIZATION_HEADER_NAME = "Authorization";
+    private static final String CONTENT_TYPE_HEADER_NAME = "Content-Type";
+    private static final String BASIC_AUTH_PREFIX = "Basic ";
+
+    private final WebClientServiceProvider webClientServiceProvider;
+    private final ZendeskAuthenticationContext authenticationContext;
+
+    public ZendeskClient(WebClientServiceProvider webClientServiceProvider, ZendeskAuthenticationContext authenticationContext) {
+        this.webClientServiceProvider = webClientServiceProvider;
+        this.authenticationContext = authenticationContext;
+    }
+
+    /**
+     * Sends the ticket creation POST request to the Zendesk API.
+     *
+     * @param uri         target uri
+     * @param inputStream body of the request
+     * @return response from the Zendesk API
+     * @throws IOException error while performing the POST request
+     */
+    public HttpResponseEntity performPostRequest(URI uri, InputStream inputStream) throws IOException {
+        return webClientServiceProvider.getWebClientService()
+                .post()
+                .uri(uri)
+                .header(CONTENT_TYPE_HEADER_NAME, ZendeskProperties.APPLICATION_JSON)
+                .header(AUTHORIZATION_HEADER_NAME, basicAuthHeaderValue())
+                .body(inputStream, OptionalLong.of(inputStream.available()))
+                .retrieve();
+    }
+
+    /**
+     * Sends a query GET request to the Zendesk API.
+     *
+     * @param uri target uri
+     * @return result of query
+     */
+    public HttpResponseEntity performQuery(URI uri) {

Review Comment:
   It is a generic GET request method. I would suggest the following renaming:
   ```suggestion
       /**
        * Sends a GET request to the Zendesk API.
        *
        * @param uri target uri
        * @return response from the Zendesk API
        */
       public HttpResponseEntity performGetRequest(URI uri) {
   ```



##########
nifi-nar-bundles/nifi-zendesk-bundle/nifi-zendesk-common/src/main/java/org/apache/nifi/common/zendesk/ZendeskClient.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.nifi.common.zendesk;
+
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.OptionalLong;
+
+import static java.lang.String.format;
+import static java.util.Base64.getEncoder;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.HTTPS;
+import static org.apache.nifi.common.zendesk.ZendeskProperties.ZENDESK_HOST_TEMPLATE;
+
+public class ZendeskClient {
+
+    private static final String AUTHORIZATION_HEADER_NAME = "Authorization";
+    private static final String CONTENT_TYPE_HEADER_NAME = "Content-Type";
+    private static final String BASIC_AUTH_PREFIX = "Basic ";
+
+    private final WebClientServiceProvider webClientServiceProvider;
+    private final ZendeskAuthenticationContext authenticationContext;
+
+    public ZendeskClient(WebClientServiceProvider webClientServiceProvider, ZendeskAuthenticationContext authenticationContext) {
+        this.webClientServiceProvider = webClientServiceProvider;
+        this.authenticationContext = authenticationContext;
+    }
+
+    /**
+     * Sends the ticket creation POST request to the Zendesk API.
+     *
+     * @param uri         target uri
+     * @param inputStream body of the request
+     * @return response from the Zendesk API
+     * @throws IOException error while performing the POST request
+     */
+    public HttpResponseEntity performPostRequest(URI uri, InputStream inputStream) throws IOException {

Review Comment:
   As the method name shows, it is a generic POST method and could be used for other requests, not only ticket creation.
   ```suggestion
       /**
        * Sends a POST request to the Zendesk API.
        *
        * @param uri         target uri
        * @param inputStream body of the request
        * @return response from the Zendesk API
        * @throws IOException error while performing the POST request
        */
       public HttpResponseEntity performPostRequest(URI uri, InputStream inputStream) throws IOException {
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org