This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/master by this push: new cfd584a CAMEL-15319: Add 'watchUpdates' consumer to camel-jira (#4137) cfd584a is described below commit cfd584ac2da954c9e2a905e2928c86f7ac804ea3 Author: Matej Melko <6814482+mme...@users.noreply.github.com> AuthorDate: Fri Aug 28 10:06:46 2020 +0200 CAMEL-15319: Add 'watchUpdates' consumer to camel-jira (#4137) * CAMEL-JIRA: Add consumer to watch updates in issues defined by jql. Add tests for the new consumer. * CAMEL-JIRA: Update the documentation to include watchUpdates consumer. --- .../component/jira/JiraEndpointConfigurer.java | 10 ++ .../org/apache/camel/component/jira/jira.json | 4 +- .../camel-jira/src/main/docs/jira-component.adoc | 17 ++- .../apache/camel/component/jira/JiraEndpoint.java | 34 ++++- .../org/apache/camel/component/jira/JiraType.java | 2 + .../jira/consumer/WatchUpdatesConsumer.java | 113 ++++++++++++++ .../jira/JiraComponentConfigurationTest.java | 21 +++ .../camel/component/jira/JiraTestConstants.java | 1 + .../org/apache/camel/component/jira/Utils.java | 19 +++ .../jira/consumer/WatchUpdatesConsumerTest.java | 168 +++++++++++++++++++++ .../builder/endpoint/StaticEndpointBuilders.java | 8 +- .../endpoint/dsl/JiraEndpointBuilderFactory.java | 49 +++++- 12 files changed, 434 insertions(+), 12 deletions(-) diff --git a/components/camel-jira/src/generated/java/org/apache/camel/component/jira/JiraEndpointConfigurer.java b/components/camel-jira/src/generated/java/org/apache/camel/component/jira/JiraEndpointConfigurer.java index 40f3c9e..5c56c16 100644 --- a/components/camel-jira/src/generated/java/org/apache/camel/component/jira/JiraEndpointConfigurer.java +++ b/components/camel-jira/src/generated/java/org/apache/camel/component/jira/JiraEndpointConfigurer.java @@ -42,10 +42,14 @@ public class JiraEndpointConfigurer extends PropertyConfigurerSupport implements case "password": target.getConfiguration().setPassword(property(camelContext, java.lang.String.class, value)); return true; case "privatekey": case "privateKey": target.getConfiguration().setPrivateKey(property(camelContext, java.lang.String.class, value)); return true; + case "sendonlyupdatedfield": + case "sendOnlyUpdatedField": target.setSendOnlyUpdatedField(property(camelContext, boolean.class, value)); return true; case "synchronous": target.setSynchronous(property(camelContext, boolean.class, value)); return true; case "username": target.getConfiguration().setUsername(property(camelContext, java.lang.String.class, value)); return true; case "verificationcode": case "verificationCode": target.getConfiguration().setVerificationCode(property(camelContext, java.lang.String.class, value)); return true; + case "watchedfields": + case "watchedFields": target.setWatchedFields(property(camelContext, java.lang.String.class, value)); return true; default: return false; } } @@ -66,9 +70,11 @@ public class JiraEndpointConfigurer extends PropertyConfigurerSupport implements answer.put("maxResults", java.lang.Integer.class); answer.put("password", java.lang.String.class); answer.put("privateKey", java.lang.String.class); + answer.put("sendOnlyUpdatedField", boolean.class); answer.put("synchronous", boolean.class); answer.put("username", java.lang.String.class); answer.put("verificationCode", java.lang.String.class); + answer.put("watchedFields", java.lang.String.class); return answer; } @@ -99,10 +105,14 @@ public class JiraEndpointConfigurer extends PropertyConfigurerSupport implements case "password": return target.getConfiguration().getPassword(); case "privatekey": case "privateKey": return target.getConfiguration().getPrivateKey(); + case "sendonlyupdatedfield": + case "sendOnlyUpdatedField": return target.isSendOnlyUpdatedField(); case "synchronous": return target.isSynchronous(); case "username": return target.getConfiguration().getUsername(); case "verificationcode": case "verificationCode": return target.getConfiguration().getVerificationCode(); + case "watchedfields": + case "watchedFields": return target.getWatchedFields(); default: return null; } } diff --git a/components/camel-jira/src/generated/resources/org/apache/camel/component/jira/jira.json b/components/camel-jira/src/generated/resources/org/apache/camel/component/jira/jira.json index d35a9a4..b802834 100644 --- a/components/camel-jira/src/generated/resources/org/apache/camel/component/jira/jira.json +++ b/components/camel-jira/src/generated/resources/org/apache/camel/component/jira/jira.json @@ -35,12 +35,14 @@ "verificationCode": { "kind": "property", "displayName": "Verification Code", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "secret": true, "configurationClass": "org.apache.camel.component.jira.JiraConfiguration", "configurationField": "configuration", "description": "(OAuth only) The verification code from Jira generated in the first step of the authorization proccess." } }, "properties": { - "type": { "kind": "path", "displayName": "Type", "group": "common", "label": "", "required": true, "type": "object", "javaType": "org.apache.camel.component.jira.JiraType", "enum": [ "ADDCOMMENT", "ADDISSUE", "ATTACH", "DELETEISSUE", "NEWISSUES", "NEWCOMMENTS", "UPDATEISSUE", "TRANSITIONISSUE", "WATCHERS", "ADDISSUELINK", "ADDWORKLOG", "FETCHISSUE", "FETCHCOMMENTS" ], "deprecated": false, "deprecationNote": "", "secret": false, "description": "Operation to perform. Consumers: NewIssu [...] + "type": { "kind": "path", "displayName": "Type", "group": "common", "label": "", "required": true, "type": "object", "javaType": "org.apache.camel.component.jira.JiraType", "enum": [ "ADDCOMMENT", "ADDISSUE", "ATTACH", "DELETEISSUE", "NEWISSUES", "NEWCOMMENTS", "WATCHUPDATES", "UPDATEISSUE", "TRANSITIONISSUE", "WATCHERS", "ADDISSUELINK", "ADDWORKLOG", "FETCHISSUE", "FETCHCOMMENTS" ], "deprecated": false, "deprecationNote": "", "secret": false, "description": "Operation to perform. Co [...] "delay": { "kind": "parameter", "displayName": "Delay", "group": "common", "label": "", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "secret": false, "defaultValue": "6000", "configurationClass": "org.apache.camel.component.jira.JiraConfiguration", "configurationField": "configuration", "description": "Time in milliseconds to elapse for the next poll." }, "jiraUrl": { "kind": "parameter", "displayName": "Jira Url", "group": "common", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "secret": false, "configurationClass": "org.apache.camel.component.jira.JiraConfiguration", "configurationField": "configuration", "description": "The Jira server url, example: http:\/\/my_jira.com:8081" }, "bridgeErrorHandler": { "kind": "parameter", "displayName": "Bridge Error Handler", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "secret": false, "defaultValue": false, "description": "Allows for bridging the consumer to the Camel routing Error Handler, which mean any exceptions occurred while the consumer is trying to pickup incoming messages, or the likes, will now be processed as a message and handled b [...] "jql": { "kind": "parameter", "displayName": "Jql", "group": "consumer", "label": "consumer", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "secret": false, "description": "JQL is the query language from JIRA which allows you to retrieve the data you want. For example jql=project=MyProject Where MyProject is the product key in Jira. It is important to use the RAW() and set the JQL inside it to prevent camel parsing it, example: RAW(project [...] "maxResults": { "kind": "parameter", "displayName": "Max Results", "group": "consumer", "label": "consumer", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "secret": false, "defaultValue": "50", "description": "Max number of issues to search for" }, + "sendOnlyUpdatedField": { "kind": "parameter", "displayName": "Send Only Updated Field", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "secret": false, "defaultValue": "true", "description": "Indicator for sending only changed fields in exchange body or issue object. By default consumer sends only changed fields." }, + "watchedFields": { "kind": "parameter", "displayName": "Watched Fields", "group": "consumer", "label": "consumer", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "secret": false, "defaultValue": "Status,Priority", "description": "Comma separated list of fields to watch for changes. Status,Priority are the defaults." }, "exceptionHandler": { "kind": "parameter", "displayName": "Exception Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", "deprecated": false, "secret": false, "description": "To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By default the consumer will deal with [...] "exchangePattern": { "kind": "parameter", "displayName": "Exchange Pattern", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut", "InOptionalOut" ], "deprecated": false, "secret": false, "description": "Sets the exchange pattern when the consumer creates an exchange." }, "lazyStartProducer": { "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the [...] diff --git a/components/camel-jira/src/main/docs/jira-component.adoc b/components/camel-jira/src/main/docs/jira-component.adoc index 8cb2d44..fd2d3a6 100644 --- a/components/camel-jira/src/main/docs/jira-component.adoc +++ b/components/camel-jira/src/main/docs/jira-component.adoc @@ -78,6 +78,7 @@ For consumers: * newIssues: retrieve only new issues after the route is started * newComments: retrieve only new comments after the route is started +* watchUpdates: retrieve only updated fields/issues based on provided jql For producers: @@ -106,11 +107,11 @@ with the following path and query parameters: [width="100%",cols="2,5,^1,2",options="header"] |=== | Name | Description | Default | Type -| *type* | *Required* Operation to perform. Consumers: NewIssues, NewComments. Producers: AddIssue, AttachFile, DeleteIssue, TransitionIssue, UpdateIssue, Watchers. See this class javadoc description for more information. The value can be one of: ADDCOMMENT, ADDISSUE, ATTACH, DELETEISSUE, NEWISSUES, NEWCOMMENTS, UPDATEISSUE, TRANSITIONISSUE, WATCHERS, ADDISSUELINK, ADDWORKLOG, FETCHISSUE, FETCHCOMMENTS | | JiraType +| *type* | *Required* Operation to perform. Consumers: NewIssues, NewComments. Producers: AddIssue, AttachFile, DeleteIssue, TransitionIssue, UpdateIssue, Watchers. See this class javadoc description for more information. The value can be one of: ADDCOMMENT, ADDISSUE, ATTACH, DELETEISSUE, NEWISSUES, NEWCOMMENTS, WATCHUPDATES, UPDATEISSUE, TRANSITIONISSUE, WATCHERS, ADDISSUELINK, ADDWORKLOG, FETCHISSUE, FETCHCOMMENTS | | JiraType |=== -=== Query Parameters (16 parameters): +=== Query Parameters (18 parameters): [width="100%",cols="2,5,^1,2",options="header"] @@ -121,6 +122,8 @@ with the following path and query parameters: | *bridgeErrorHandler* (consumer) | Allows for bridging the consumer to the Camel routing Error Handler, which mean any exceptions occurred while the consumer is trying to pickup incoming messages, or the likes, will now be processed as a message and handled by the routing Error Handler. By default the consumer will use the org.apache.camel.spi.ExceptionHandler to deal with exceptions, that will be logged at WARN or ERROR level and ignored. | false | boolean | *jql* (consumer) | JQL is the query language from JIRA which allows you to retrieve the data you want. For example jql=project=MyProject Where MyProject is the product key in Jira. It is important to use the RAW() and set the JQL inside it to prevent camel parsing it, example: RAW(project in (MYP, COM) AND resolution = Unresolved) | | String | *maxResults* (consumer) | Max number of issues to search for | 50 | Integer +| *sendOnlyUpdatedField* (consumer) | Indicator for sending only changed fields in exchange body or issue object. By default consumer sends only changed fields. | true | boolean +| *watchedFields* (consumer) | Comma separated list of fields to watch for changes. Status,Priority are the defaults. | Status,Priority | String | *exceptionHandler* (consumer) | To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By default the consumer will deal with exceptions, that will be logged at WARN or ERROR level and ignored. | | ExceptionHandler | *exchangePattern* (consumer) | Sets the exchange pattern when the consumer creates an exchange. The value can be one of: InOnly, InOut, InOptionalOut | | ExchangePattern | *lazyStartProducer* (producer) | Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and [...] @@ -267,5 +270,15 @@ Required: * `IssueWatchersAdd`: A list of strings with the usernames to add to the watcher list. * `IssueWatchersRemove`: A list of strings with the usernames to remove from the watcher list. +== WatchUpdates (consumer) +* `watchedFields` Comma separated list of fields to watch for changes i.e `Status,Priority,Assignee,Components` etc. +* `sendOnlyUpdatedField` By default only changed field is send as the body. + +All messages also contain following headers that add additional info about the change: + +* `issueKey`: Key of the updated issue +* `changed`: name of the updated field (i.e Status) +* `watchedIssues`: list of all issue keys that are watched in the time of update + include::camel-spring-boot::page$jira-starter.adoc[] diff --git a/components/camel-jira/src/main/java/org/apache/camel/component/jira/JiraEndpoint.java b/components/camel-jira/src/main/java/org/apache/camel/component/jira/JiraEndpoint.java index 6128c3f..1cbe6b9 100644 --- a/components/camel-jira/src/main/java/org/apache/camel/component/jira/JiraEndpoint.java +++ b/components/camel-jira/src/main/java/org/apache/camel/component/jira/JiraEndpoint.java @@ -26,6 +26,7 @@ import org.apache.camel.Processor; import org.apache.camel.Producer; import org.apache.camel.component.jira.consumer.NewCommentsConsumer; import org.apache.camel.component.jira.consumer.NewIssuesConsumer; +import org.apache.camel.component.jira.consumer.WatchUpdatesConsumer; import org.apache.camel.component.jira.oauth.JiraOAuthAuthenticationHandler; import org.apache.camel.component.jira.oauth.OAuthAsynchronousJiraRestClientFactory; import org.apache.camel.component.jira.producer.AddCommentProducer; @@ -57,7 +58,8 @@ import static org.apache.camel.component.jira.JiraConstants.JIRA_REST_CLIENT_FAC * include: * <p> * CONSUMERS jira://newIssues (retrieve only new issues after the route is started) jira://newComments (retrieve only - * new comments after the route is started) + * new comments after the route is started) jira://watchChanges (retrieve only defined changes in issues picked base on + * provided jql) * <p> * PRODUCERS jira://addIssue (add an issue) jira://addComment (add a comment on a given issue) jira://attach (add an * attachment on a given issue) jira://deleteIssue (delete a given issue) jira://updateIssue (update fields of a given @@ -82,6 +84,10 @@ public class JiraEndpoint extends DefaultEndpoint { private JiraType type; @UriParam(label = "consumer") private String jql; + @UriParam(label = "consumer", defaultValue = "Status,Priority") + private String watchedFields = "Status,Priority"; + @UriParam(label = "consumer", defaultValue = "true") + private boolean sendOnlyUpdatedField = true; @UriParam(label = "consumer", defaultValue = "50") private Integer maxResults = 50; @UriParam @@ -167,6 +173,8 @@ public class JiraEndpoint extends DefaultEndpoint { consumer = new NewCommentsConsumer(this, processor); } else if (type == JiraType.NEWISSUES) { consumer = new NewIssuesConsumer(this, processor); + } else if (type == JiraType.WATCHUPDATES) { + consumer = new WatchUpdatesConsumer(this, processor); } else { throw new IllegalArgumentException("Consumer does not support type: " + type); } @@ -222,4 +230,28 @@ public class JiraEndpoint extends DefaultEndpoint { public void setMaxResults(Integer maxResults) { this.maxResults = maxResults; } + + /** + * Comma separated list of fields to watch for changes. "Status,Priority" are the defaults. + */ + public String getWatchedFields() { + return watchedFields; + } + + public void setWatchedFields(String watchChange) { + this.watchedFields = watchChange; + } + + /** + * Indicator for sending only changed fields in exchange body or issue object. By default consumer sends only + * changed fields. + */ + public boolean isSendOnlyUpdatedField() { + return sendOnlyUpdatedField; + } + + public void setSendOnlyUpdatedField(boolean sendOnlyUpdatedField) { + this.sendOnlyUpdatedField = sendOnlyUpdatedField; + } + } diff --git a/components/camel-jira/src/main/java/org/apache/camel/component/jira/JiraType.java b/components/camel-jira/src/main/java/org/apache/camel/component/jira/JiraType.java index 363fca8..616d4d9 100644 --- a/components/camel-jira/src/main/java/org/apache/camel/component/jira/JiraType.java +++ b/components/camel-jira/src/main/java/org/apache/camel/component/jira/JiraType.java @@ -30,6 +30,8 @@ public enum JiraType { NEWISSUES, // retrieve recent comments from any issues NEWCOMMENTS, + + WATCHUPDATES, // update the fields of an issue UPDATEISSUE, // transition a status and resolution of an issue diff --git a/components/camel-jira/src/main/java/org/apache/camel/component/jira/consumer/WatchUpdatesConsumer.java b/components/camel-jira/src/main/java/org/apache/camel/component/jira/consumer/WatchUpdatesConsumer.java new file mode 100644 index 0000000..417de78 --- /dev/null +++ b/components/camel-jira/src/main/java/org/apache/camel/component/jira/consumer/WatchUpdatesConsumer.java @@ -0,0 +1,113 @@ +/* + * 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.camel.component.jira.consumer; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import com.atlassian.jira.rest.client.api.domain.Issue; +import org.apache.camel.Exchange; +import org.apache.camel.Processor; +import org.apache.camel.component.jira.JiraEndpoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WatchUpdatesConsumer extends AbstractJiraConsumer { + + private static final transient Logger LOG = LoggerFactory.getLogger(WatchUpdatesConsumer.class); + HashMap<Long, Issue> watchedIssues; + List<String> watchedFieldsList; + String watchedIssuesKeys; + + public WatchUpdatesConsumer(JiraEndpoint endpoint, Processor processor) { + super(endpoint, processor); + this.watchedFieldsList = new ArrayList<>(); + this.watchedFieldsList = Arrays.asList(endpoint.getWatchedFields().split(",")); + initIssues(); + } + + private void initIssues() { + watchedIssues = new HashMap<>(); + List<Issue> issues = getIssues(((JiraEndpoint) getEndpoint()).getJql(), 0, 50, + ((JiraEndpoint) getEndpoint()).getMaxResults()); + issues.forEach(i -> watchedIssues.put(i.getId(), i)); + watchedIssuesKeys = issues.stream() + .map(Issue::getKey) + .collect(Collectors.joining(",")); + } + + @Override + protected int poll() throws Exception { + List<Issue> issues = getIssues(((JiraEndpoint) getEndpoint()).getJql(), 0, 50, + ((JiraEndpoint) getEndpoint()).getMaxResults()); + if (watchedIssues.values().size() != issues.size()) { + init(); + } + for (Issue issue : issues) { + checkIfIssueChanged(issue); + } + return 0; + } + + private void checkIfIssueChanged(Issue issue) throws Exception { + Issue original = watchedIssues.get(issue.getId()); + AtomicBoolean issueChanged = new AtomicBoolean(false); + if (original != null) { + for (String field : this.watchedFieldsList) { + if (hasFieldChanged(issue, original, field)) { + issueChanged.set(true); + } + } + if (issueChanged.get()) { + watchedIssues.put(issue.getId(), issue); + } + } + } + + private boolean hasFieldChanged(Issue changed, Issue original, String fieldName) throws Exception { + Method get = Issue.class.getDeclaredMethod("get" + fieldName); + Object originalField = get.invoke(original); + Object changedField = get.invoke(changed); + + if (!Objects.equals(originalField, changedField)) { + if (!((JiraEndpoint) getEndpoint()).isSendOnlyUpdatedField()) { + processExchange(changed, changed.getKey(), fieldName); + } else { + processExchange(changedField, changed.getKey(), fieldName); + } + return true; + } + return false; + } + + private void processExchange(Object body, String issueKey, String changed) throws Exception { + Exchange e = getEndpoint().createExchange(); + e.getIn().setBody(body); + e.getIn().setHeader("issueKey", issueKey); + e.getIn().setHeader("changed", changed); + e.getIn().setHeader("watchedIssues", watchedIssuesKeys); + LOG.debug(" {}: {} changed to {}", issueKey, changed, body); + getProcessor().process(e); + } +} diff --git a/components/camel-jira/src/test/java/org/apache/camel/component/jira/JiraComponentConfigurationTest.java b/components/camel-jira/src/test/java/org/apache/camel/component/jira/JiraComponentConfigurationTest.java index 06c004c..89081d5 100644 --- a/components/camel-jira/src/test/java/org/apache/camel/component/jira/JiraComponentConfigurationTest.java +++ b/components/camel-jira/src/test/java/org/apache/camel/component/jira/JiraComponentConfigurationTest.java @@ -38,6 +38,10 @@ public class JiraComponentConfigurationTest extends CamelTestSupport { private static final String PRIV_KEY_VALUE = "my_privateKey_test"; private static final String JIRA_URL = "jiraUrl"; private static final String JIRA_URL_VALUE = "http://my_jira_server:8080"; + private static final String WATCHED_FIELDS = "watchedFields"; + private static final String WATCHED_FIELDS_VALUE = "Assignee,Components,Priority"; + private static final String SEND_ONLY_CHANGED_FIELD = "sendOnlyUpdatedField"; + private static final boolean SEND_ONLY_CHANGED_FIELD_VALUE = false; @Test public void createEndpointWithBasicAuthentication() throws Exception { @@ -75,6 +79,23 @@ public class JiraComponentConfigurationTest extends CamelTestSupport { assertEquals(PRIV_KEY_VALUE, endpoint.getConfiguration().getPrivateKey()); } + @Test + public void createWatchChangesEndpoint() throws Exception { + JiraComponent component = new JiraComponent(context); + component.start(); + String query = Joiner.on("&").join( + concat(JIRA_URL, JIRA_URL_VALUE), + concat(USERNAME, USERNAME_VALUE), + concat(PASSWORD, PASSWORD_VALUE), + concat(SEND_ONLY_CHANGED_FIELD, SEND_ONLY_CHANGED_FIELD_VALUE + ""), + concat(WATCHED_FIELDS, WATCHED_FIELDS_VALUE)); + JiraEndpoint endpoint = (JiraEndpoint) component.createEndpoint("jira://watchUpdates?" + query); + + assertEquals("watchupdates", endpoint.getType().name().toLowerCase()); + assertEquals(WATCHED_FIELDS_VALUE, endpoint.getWatchedFields()); + assertEquals(SEND_ONLY_CHANGED_FIELD_VALUE, endpoint.isSendOnlyUpdatedField()); + } + private String concat(String key, String val) { return key + "=" + val; } diff --git a/components/camel-jira/src/test/java/org/apache/camel/component/jira/JiraTestConstants.java b/components/camel-jira/src/test/java/org/apache/camel/component/jira/JiraTestConstants.java index 413b6a9..4e4ee4c 100644 --- a/components/camel-jira/src/test/java/org/apache/camel/component/jira/JiraTestConstants.java +++ b/components/camel-jira/src/test/java/org/apache/camel/component/jira/JiraTestConstants.java @@ -24,4 +24,5 @@ public interface JiraTestConstants { String USERNAME = "someguy"; String PASSWORD = "my_password"; String JIRA_CREDENTIALS = TEST_JIRA_URL + "&username=" + USERNAME + "&password=" + PASSWORD; + String WATCHED_COMPONENTS = "Priority,Status,Resolution"; } diff --git a/components/camel-jira/src/test/java/org/apache/camel/component/jira/Utils.java b/components/camel-jira/src/test/java/org/apache/camel/component/jira/Utils.java index c7d4718..4352f84 100644 --- a/components/camel-jira/src/test/java/org/apache/camel/component/jira/Utils.java +++ b/components/camel-jira/src/test/java/org/apache/camel/component/jira/Utils.java @@ -24,6 +24,7 @@ import java.util.Map; import javax.annotation.Nullable; +import com.atlassian.jira.rest.client.api.StatusCategory; import com.atlassian.jira.rest.client.api.domain.Attachment; import com.atlassian.jira.rest.client.api.domain.BasicComponent; import com.atlassian.jira.rest.client.api.domain.BasicPriority; @@ -33,6 +34,7 @@ import com.atlassian.jira.rest.client.api.domain.Issue; import com.atlassian.jira.rest.client.api.domain.IssueLink; import com.atlassian.jira.rest.client.api.domain.IssueLinkType; import com.atlassian.jira.rest.client.api.domain.IssueType; +import com.atlassian.jira.rest.client.api.domain.Priority; import com.atlassian.jira.rest.client.api.domain.Resolution; import com.atlassian.jira.rest.client.api.domain.Status; import com.atlassian.jira.rest.client.api.domain.User; @@ -83,6 +85,23 @@ public final class Utils { null, null, null, null, null); } + public static Issue setPriority(Issue issue, Priority p) { + return new Issue( + issue.getSummary(), issue.getSelf(), issue.getKey(), issue.getId(), null, issue.getIssueType(), + issue.getStatus(), issue.getDescription(), p, issue.getResolution(), null, null, + issue.getAssignee(), null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null); + } + + public static Issue transitionIssueDone(Issue issue) { + URI doneStatusUri = URI.create(TEST_JIRA_URL + "/rest/api/2/status/1"); + URI doneResolutionUri = URI.create(TEST_JIRA_URL + "/rest/api/2/resolution/1"); + StatusCategory sc = new StatusCategory(doneResolutionUri, "statusCategory", 1L, "SC-1", "GREEN"); + Status status = new Status(doneStatusUri, 1L, "Done", "Done", null, sc); + Resolution resolution = new Resolution(doneResolutionUri, 5L, "Resolution", "Resolution"); + return transitionIssueDone(issue, status, resolution); + } + public static Issue createIssueWithAttachment( long id, String summary, String key, IssueType issueType, String description, BasicPriority priority, User assignee, Collection<Attachment> attachments) { diff --git a/components/camel-jira/src/test/java/org/apache/camel/component/jira/consumer/WatchUpdatesConsumerTest.java b/components/camel-jira/src/test/java/org/apache/camel/component/jira/consumer/WatchUpdatesConsumerTest.java new file mode 100644 index 0000000..01c019a --- /dev/null +++ b/components/camel-jira/src/test/java/org/apache/camel/component/jira/consumer/WatchUpdatesConsumerTest.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.camel.component.jira.consumer; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.atlassian.jira.rest.client.api.JiraRestClient; +import com.atlassian.jira.rest.client.api.JiraRestClientFactory; +import com.atlassian.jira.rest.client.api.SearchRestClient; +import com.atlassian.jira.rest.client.api.domain.Issue; +import com.atlassian.jira.rest.client.api.domain.Priority; +import com.atlassian.jira.rest.client.api.domain.SearchResult; +import io.atlassian.util.concurrent.Promise; +import io.atlassian.util.concurrent.Promises; +import org.apache.camel.CamelContext; +import org.apache.camel.EndpointInject; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.jira.JiraComponent; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.spi.Registry; +import org.apache.camel.test.junit5.CamelTestSupport; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.apache.camel.component.jira.JiraConstants.JIRA; +import static org.apache.camel.component.jira.JiraConstants.JIRA_REST_CLIENT_FACTORY; +import static org.apache.camel.component.jira.JiraTestConstants.*; +import static org.apache.camel.component.jira.Utils.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class WatchUpdatesConsumerTest extends CamelTestSupport { + private static List<Issue> issues = new ArrayList<>(); + + @Mock + private JiraRestClient jiraClient; + + @Mock + private JiraRestClientFactory jiraRestClientFactory; + + @Mock + private SearchRestClient searchRestClient; + + @EndpointInject("mock:result") + private MockEndpoint mockResult; + + @Override + protected void bindToRegistry(Registry registry) { + registry.bind(JIRA_REST_CLIENT_FACTORY, jiraRestClientFactory); + } + + @BeforeAll + public static void beforeAll() { + issues.clear(); + issues.add(createIssue(1L)); + issues.add(createIssue(2L)); + issues.add(createIssue(3L)); + } + + public void setMocks() { + SearchResult result = new SearchResult(0, 50, 100, issues); + Promise<SearchResult> promiseSearchResult = Promises.promise(result); + + when(jiraClient.getSearchClient()).thenReturn(searchRestClient); + when(jiraRestClientFactory.createWithBasicHttpAuthentication(any(), any(), any())).thenReturn(jiraClient); + when(searchRestClient.searchJql(any(), any(), any(), any())).thenReturn(promiseSearchResult); + } + + @Override + protected CamelContext createCamelContext() throws Exception { + setMocks(); + CamelContext camelContext = super.createCamelContext(); + camelContext.disableJMX(); + JiraComponent component = new JiraComponent(camelContext); + camelContext.addComponent(JIRA, component); + return camelContext; + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("jira://watchUpdates?jiraUrl=" + JIRA_CREDENTIALS + + "&jql=project=" + PROJECT + "&delay=5000&watchedFields=" + WATCHED_COMPONENTS) + .to(mockResult); + } + }; + } + + @Test + public void emptyAtStartupTest() throws Exception { + mockResult.expectedMessageCount(0); + mockResult.assertIsSatisfied(); + } + + @Test + public void singleChangeTest() throws Exception { + Issue issue = setPriority(issues.get(0), new Priority( + null, 4L, "High", null, null, null)); + reset(searchRestClient); + AtomicBoolean searched = new AtomicBoolean(false); + when(searchRestClient.searchJql(any(), any(), any(), any())).then(invocation -> { + + if (!searched.get()) { + issues.remove(0); + issues.add(0, issue); + } + SearchResult result = new SearchResult(0, 50, 100, issues); + return Promises.promise(result); + }); + + mockResult.expectedBodiesReceived(issue.getPriority()); + mockResult.expectedHeaderReceived("changed", "Priority"); + mockResult.expectedHeaderReceived("issueKey", "TST-1"); + mockResult.expectedMessageCount(1); + mockResult.assertIsSatisfied(0); + } + + @Test + public void multipleChangesWithAddedNewIssueTest() throws Exception { + final Issue issue = transitionIssueDone(issues.get(1)); + final Issue issue2 = setPriority(issues.get(2), new Priority( + null, 4L, "High", null, null, null)); + + reset(searchRestClient); + AtomicBoolean searched = new AtomicBoolean(false); + when(searchRestClient.searchJql(any(), any(), any(), any())).then(invocation -> { + if (!searched.get()) { + issues.add(createIssue(4L)); + issues.remove(1); + issues.add(1, issue); + issues.remove(2); + issues.add(2, issue2); + searched.set(true); + } + + SearchResult result = new SearchResult(0, 50, 3, issues); + return Promises.promise(result); + }); + + mockResult.expectedMessageCount(3); + mockResult.expectedBodiesReceivedInAnyOrder(issue.getStatus(), issue.getResolution(), issue2.getPriority()); + mockResult.assertIsSatisfied(1000); + } + +} diff --git a/core/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java b/core/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java index 1eed630..dbc0cc1 100644 --- a/core/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java +++ b/core/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java @@ -8262,8 +8262,8 @@ public class StaticEndpointBuilders { * AddIssue, AttachFile, DeleteIssue, TransitionIssue, UpdateIssue, * Watchers. See this class javadoc description for more information. * The value can be one of: ADDCOMMENT, ADDISSUE, ATTACH, DELETEISSUE, - * NEWISSUES, NEWCOMMENTS, UPDATEISSUE, TRANSITIONISSUE, WATCHERS, - * ADDISSUELINK, ADDWORKLOG, FETCHISSUE, FETCHCOMMENTS + * NEWISSUES, NEWCOMMENTS, WATCHUPDATES, UPDATEISSUE, TRANSITIONISSUE, + * WATCHERS, ADDISSUELINK, ADDWORKLOG, FETCHISSUE, FETCHCOMMENTS * * @param path type */ @@ -8286,8 +8286,8 @@ public class StaticEndpointBuilders { * AddIssue, AttachFile, DeleteIssue, TransitionIssue, UpdateIssue, * Watchers. See this class javadoc description for more information. * The value can be one of: ADDCOMMENT, ADDISSUE, ATTACH, DELETEISSUE, - * NEWISSUES, NEWCOMMENTS, UPDATEISSUE, TRANSITIONISSUE, WATCHERS, - * ADDISSUELINK, ADDWORKLOG, FETCHISSUE, FETCHCOMMENTS + * NEWISSUES, NEWCOMMENTS, WATCHUPDATES, UPDATEISSUE, TRANSITIONISSUE, + * WATCHERS, ADDISSUELINK, ADDWORKLOG, FETCHISSUE, FETCHCOMMENTS * * @param componentName to use a custom component name for the endpoint * instead of the default name diff --git a/core/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JiraEndpointBuilderFactory.java b/core/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JiraEndpointBuilderFactory.java index 2b052dd..e5352fb 100644 --- a/core/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JiraEndpointBuilderFactory.java +++ b/core/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JiraEndpointBuilderFactory.java @@ -157,6 +157,47 @@ public interface JiraEndpointBuilderFactory { return this; } /** + * Indicator for sending only changed fields in exchange body or issue + * object. By default consumer sends only changed fields. + * + * The option is a: <code>boolean</code> type. + * + * Default: true + * Group: consumer + */ + default JiraEndpointConsumerBuilder sendOnlyUpdatedField( + boolean sendOnlyUpdatedField) { + doSetProperty("sendOnlyUpdatedField", sendOnlyUpdatedField); + return this; + } + /** + * Indicator for sending only changed fields in exchange body or issue + * object. By default consumer sends only changed fields. + * + * The option will be converted to a <code>boolean</code> type. + * + * Default: true + * Group: consumer + */ + default JiraEndpointConsumerBuilder sendOnlyUpdatedField( + String sendOnlyUpdatedField) { + doSetProperty("sendOnlyUpdatedField", sendOnlyUpdatedField); + return this; + } + /** + * Comma separated list of fields to watch for changes. Status,Priority + * are the defaults. + * + * The option is a: <code>java.lang.String</code> type. + * + * Default: Status,Priority + * Group: consumer + */ + default JiraEndpointConsumerBuilder watchedFields(String watchedFields) { + doSetProperty("watchedFields", watchedFields); + return this; + } + /** * (OAuth only) The access token generated by the Jira server. * * The option is a: <code>java.lang.String</code> type. @@ -788,8 +829,8 @@ public interface JiraEndpointBuilderFactory { * AddIssue, AttachFile, DeleteIssue, TransitionIssue, UpdateIssue, * Watchers. See this class javadoc description for more information. * The value can be one of: ADDCOMMENT, ADDISSUE, ATTACH, DELETEISSUE, - * NEWISSUES, NEWCOMMENTS, UPDATEISSUE, TRANSITIONISSUE, WATCHERS, - * ADDISSUELINK, ADDWORKLOG, FETCHISSUE, FETCHCOMMENTS + * NEWISSUES, NEWCOMMENTS, WATCHUPDATES, UPDATEISSUE, TRANSITIONISSUE, + * WATCHERS, ADDISSUELINK, ADDWORKLOG, FETCHISSUE, FETCHCOMMENTS * * @param path type */ @@ -811,8 +852,8 @@ public interface JiraEndpointBuilderFactory { * AddIssue, AttachFile, DeleteIssue, TransitionIssue, UpdateIssue, * Watchers. See this class javadoc description for more information. * The value can be one of: ADDCOMMENT, ADDISSUE, ATTACH, DELETEISSUE, - * NEWISSUES, NEWCOMMENTS, UPDATEISSUE, TRANSITIONISSUE, WATCHERS, - * ADDISSUELINK, ADDWORKLOG, FETCHISSUE, FETCHCOMMENTS + * NEWISSUES, NEWCOMMENTS, WATCHUPDATES, UPDATEISSUE, TRANSITIONISSUE, + * WATCHERS, ADDISSUELINK, ADDWORKLOG, FETCHISSUE, FETCHCOMMENTS * * @param componentName to use a custom component name for the endpoint * instead of the default name