├── .gitignore ├── src ├── test │ ├── resources │ │ ├── feeds │ │ │ ├── empty-attachments.xml │ │ │ ├── issue-attachments.xml │ │ │ ├── issue-feed-attachment-only.xml │ │ │ ├── issue-feed-comment-attachment-only.xml │ │ │ ├── issue-feed-with-new-ticket-one-comment.xml │ │ │ ├── empty-issue-changes.xml │ │ │ ├── issue-changes-no-changes.xml │ │ │ ├── issue-change-new-item-with-comment-no-changes.xml │ │ │ ├── issue-details-with-link.xml │ │ │ ├── issue-changes-comment-attachment-only.xml │ │ │ ├── issue-changes-t1-t3.xml │ │ │ ├── issue-changes-t2.xml │ │ │ ├── issue-changes-t1.xml │ │ │ ├── issue1-timeline-changes.xml │ │ │ ├── issue2-timeline-changes.xml │ │ │ ├── issue-changes2.xml │ │ │ ├── issue-details.xml │ │ │ ├── issue-changes-t2-t4.xml │ │ │ └── issue-with-comments-ap-22.xml │ │ ├── application.properties │ │ └── logback-test.xml │ ├── java.none │ │ └── readme.md │ └── java │ │ ├── ontometrics │ │ ├── jobs │ │ │ └── TrackingEventListener.java │ │ ├── test │ │ │ └── util │ │ │ │ ├── ExtractionStreamWrapperTests.java │ │ │ │ ├── UrlStreamProvider.java │ │ │ │ ├── TestUtil.java │ │ │ │ └── ExtractorUtilTests.java │ │ └── integrations │ │ │ └── sources │ │ │ ├── ProcessEventTest.java │ │ │ ├── ExternalStreamProviderTest.java │ │ │ └── ChannelMapperFactoryTest.java │ │ └── com │ │ └── ontometrics │ │ └── integrations │ │ ├── configuration │ │ ├── ObjectWrapper.java │ │ ├── ConfigurationTest.java │ │ ├── EmptyChatServer.java │ │ ├── CheckDuplicateMessagesChatServer.java │ │ ├── YouTrackInstanceTest.java │ │ ├── SimpleMockIssueTracker.java │ │ ├── SlackInstanceTest.java │ │ └── EventProcessorConfigurationTest.java │ │ ├── StemmerSanityTests.java │ │ ├── events │ │ ├── ProcessEventChangeTest.java │ │ ├── IssueEditSessionTest.java │ │ └── TestDataFactory.java │ │ └── sources │ │ └── EditSessionsExtractorIntegrationTest.java └── main │ ├── java │ └── com │ │ └── ontometrics │ │ ├── integrations │ │ ├── jobs │ │ │ ├── EventListener.java │ │ │ ├── InvalidConfigurationException.java │ │ │ ├── ProjectProvider.java │ │ │ ├── WebContextJobStarter.java │ │ │ ├── JobStarter.java │ │ │ └── EventListenerImpl.java │ │ ├── sources │ │ │ ├── InputStreamHandler.java │ │ │ ├── Authenticator.java │ │ │ ├── StreamProvider.java │ │ │ ├── ChannelMapper.java │ │ │ ├── ChannelMapperFactory.java │ │ │ ├── HubAuthenticator.java │ │ │ ├── AuthenticatedHttpStreamProvider.java │ │ │ └── NonAuthenticatedHttpStreamProvider.java │ │ ├── configuration │ │ │ ├── ConfigurationAccessError.java │ │ │ ├── YouTrackInstanceFactory.java │ │ │ ├── IssueTracker.java │ │ │ ├── ConfigurationFactory.java │ │ │ ├── StreamProviderFactory.java │ │ │ ├── ChatServer.java │ │ │ ├── YouTrackInstance.java │ │ │ └── EventProcessorConfiguration.java │ │ ├── model │ │ │ ├── IssueFieldValue.java │ │ │ ├── IssueList.java │ │ │ ├── IssueField.java │ │ │ ├── ProjectList.java │ │ │ └── Issue.java │ │ ├── events │ │ │ ├── IssueLink.java │ │ │ ├── AttachmentEvent.java │ │ │ ├── IssueEdit.java │ │ │ ├── ProcessEventChangeSet.java │ │ │ ├── ProcessEvent.java │ │ │ ├── Comment.java │ │ │ ├── ProcessEventChange.java │ │ │ ├── Issue.java │ │ │ └── IssueEditSession.java │ │ └── youtrack │ │ │ └── YouTrackImageServlet.java │ │ ├── util │ │ ├── ExtractorUtils.java │ │ ├── Mapper.java │ │ ├── HttpUtil.java │ │ ├── BadResponseException.java │ │ ├── TextUtil.java │ │ └── DateBuilder.java │ │ └── db │ │ └── MapDb.java │ ├── webapp │ └── WEB-INF │ │ └── web.xml │ └── resources │ ├── application.properties │ └── logback.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target -------------------------------------------------------------------------------- /src/test/resources/feeds/empty-attachments.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/test/java.none/readme.md: -------------------------------------------------------------------------------- 1 | This is a temp empty director for java tests (during updates which break all tests) -------------------------------------------------------------------------------- /src/test/java/ontometrics/jobs/TrackingEventListener.java: -------------------------------------------------------------------------------- 1 | package ontometrics.jobs; 2 | 3 | /** 4 | * TrackingEventListener.java 5 | * Created on 08 27, 2014 by Andrey Chorniy 6 | */ 7 | public class TrackingEventListener { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/jobs/EventListener.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.jobs; 2 | 3 | /** 4 | * Created on 8/18/14. 5 | */ 6 | public interface EventListener { 7 | 8 | public int checkForNewEvents() throws Exception; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/util/ExtractorUtils.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.util; 2 | 3 | /** 4 | * Created by Rob on 8/19/14. 5 | * Copyright (c) ontometrics, 2014 All Rights Reserved 6 | */ 7 | public class ExtractorUtils { 8 | 9 | public String getElementType(){ 10 | return ""; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/ontometrics/test/util/ExtractionStreamWrapperTests.java: -------------------------------------------------------------------------------- 1 | package ontometrics.test.util; 2 | 3 | /** 4 | * User: Rob 5 | * Date: 9/11/14 6 | * Time: 8:49 PM 7 | *

8 | * (c) ontometrics 2014, All Rights Reserved 9 | */ 10 | public class ExtractionStreamWrapperTests { 11 | 12 | 13 | 14 | 15 | } 16 | 17 | class Extractor { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/sources/InputStreamHandler.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.sources; 2 | 3 | import java.io.InputStream; 4 | 5 | /** 6 | * Handler of {@link java.io.InputStream} which produces a result 7 | * 8 | */ 9 | public interface InputStreamHandler { 10 | RES handleStream(InputStream is, int responseCode) throws Exception; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/configuration/ConfigurationAccessError.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | /** 4 | * ConfigurationAccessError.java 5 | */ 6 | public class ConfigurationAccessError extends RuntimeException{ 7 | public ConfigurationAccessError(String message, Throwable cause) { 8 | super(message, cause); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/com/ontometrics/integrations/configuration/ObjectWrapper.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | /** 4 | * Object wrapper 5 | * ObjectWrapper.java 6 | */ 7 | public class ObjectWrapper { 8 | private T object; 9 | 10 | public T get() { 11 | return object; 12 | } 13 | 14 | public void set(T object) { 15 | this.object = object; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/jobs/InvalidConfigurationException.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.jobs; 2 | 3 | 4 | public class InvalidConfigurationException extends RuntimeException { 5 | public InvalidConfigurationException(String message) { 6 | super(message); 7 | } 8 | 9 | public InvalidConfigurationException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/sources/Authenticator.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.sources; 2 | 3 | import org.apache.http.client.fluent.Executor; 4 | import org.apache.http.client.fluent.Request; 5 | 6 | import java.net.URL; 7 | 8 | /** 9 | * Authenticates request issued by {@link com.ontometrics.integrations.sources.SourceEventMapper} 10 | */ 11 | public interface Authenticator { 12 | Request authenticate(URL resourceUrl, Executor httpExecutor, Request request); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/util/Mapper.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.util; 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.dataformat.xml.XmlMapper; 6 | 7 | /** 8 | * 9 | */ 10 | public class Mapper { 11 | 12 | 13 | public static ObjectMapper createXmlMapper() { 14 | return new XmlMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/util/HttpUtil.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.util; 2 | 3 | import org.apache.http.HttpStatus; 4 | 5 | import java.net.URL; 6 | 7 | /** 8 | * 9 | */ 10 | public class HttpUtil { 11 | 12 | 13 | public static void checkResponseCode(int responseCode, URL requestUrl) throws BadResponseException { 14 | if (responseCode != HttpStatus.SC_OK){ 15 | //we got not normal response from server 16 | throw new BadResponseException(requestUrl, responseCode); 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue-attachments.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue-feed-attachment-only.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | YouTrack 4 | http://ontometrics.com:8085/_rss/issues 5 | 6 | 7 | 8 | HA-663: Detect lead duplication across different sources 9 | 10 | http://ontometrics.com:8085/issue/HA-663 11 | 12 | 13 | 14 | Sun, 25 Aug 2014 23:13:29 UT 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/configuration/YouTrackInstanceFactory.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | import org.apache.commons.configuration.Configuration; 4 | 5 | public class YouTrackInstanceFactory { 6 | private static final String YT_FEED_URL = "https://issuetracker.ontometrics.com"; 7 | 8 | public static YouTrackInstance createYouTrackInstance(Configuration configuration) { 9 | return new YouTrackInstance.Builder().baseUrl( 10 | configuration.getString("PROP.YOUTRACK_URL", YT_FEED_URL)) 11 | .externalBaseUrl( 12 | configuration.getString("PROP.YOUTRACK_EXTERNAL_URL", YT_FEED_URL)).build(); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/ontometrics/integrations/configuration/ConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import org.junit.Test; 5 | 6 | import java.util.List; 7 | 8 | import static org.hamcrest.MatcherAssert.assertThat; 9 | import static org.hamcrest.Matchers.contains; 10 | 11 | public class ConfigurationTest { 12 | @Test 13 | public void testThatExcludedYoutrackPropertiesAreRead() { 14 | String[] excludedFields = ConfigurationFactory.get().getStringArray("excluded-youtrack-fields"); 15 | List excludedList = ImmutableList.copyOf(excludedFields); 16 | assertThat(excludedList, contains("Estimate Time", "Spent")); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | PROP.APP_DATA_DIR=./target 2 | PROP.YOUTRACK_USERNAME=changeme 3 | PROP.YOUTRACK_PASSWORD=changeme 4 | PROP.YOUTRACK_URL=http://ontoserver.ontometrics.com:8085 5 | PROP.YOUTRACK_EXTERNAL_URL=http://ontoserver.ontometrics.com:8085 6 | PROP.SLACK_AUTH_TOKEN=changeme 7 | 8 | PROP.AUTH_TYPE=credentials 9 | 10 | # Time In minutes - how deep should we look for issues in the past. If set to 10, it means that issues and changes 11 | # that happened not longer than 10 minutes will be posted to chat server 12 | PROP.ISSUE_HISTORY_WINDOW=10 13 | 14 | youtrack-slack.channel-mappings=HA->jobspider;ASOC->vixlet;DMAN->dminder;AP->agent-portal 15 | youtrack-slack.default-channel=process 16 | excluded-youtrack-fields=Estimate Time; Spent -------------------------------------------------------------------------------- /src/test/java/com/ontometrics/integrations/configuration/EmptyChatServer.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | import com.ontometrics.integrations.events.Issue; 4 | import com.ontometrics.integrations.events.IssueEditSession; 5 | import com.ontometrics.integrations.sources.ChannelMapper; 6 | 7 | /** 8 | * ChatServer with no operations 9 | * 10 | * EmptyChatServer.java 11 | */ 12 | public class EmptyChatServer implements ChatServer { 13 | @Override 14 | public void postIssueCreation(Issue issue) { 15 | 16 | } 17 | 18 | @Override 19 | public void post(IssueEditSession issueEditSession) { 20 | 21 | } 22 | 23 | @Override 24 | public ChannelMapper getChannelMapper() { 25 | return null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/util/BadResponseException.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.util; 2 | 3 | import org.apache.http.client.HttpResponseException; 4 | 5 | import java.net.URL; 6 | 7 | /** 8 | * Signal about bad response from server 9 | */ 10 | public class BadResponseException extends HttpResponseException{ 11 | private URL url; 12 | 13 | public BadResponseException(URL url, int responseCode) { 14 | this("Got response code "+responseCode+ " in response to "+url.toExternalForm(), url, responseCode); 15 | } 16 | 17 | public BadResponseException(String message, URL url, int responseCode) { 18 | super(responseCode, message); 19 | this.url = url; 20 | } 21 | 22 | public URL getUrl() { 23 | return url; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/configuration/IssueTracker.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | import com.ontometrics.integrations.events.Issue; 4 | 5 | import java.net.URL; 6 | import java.util.Date; 7 | 8 | /** 9 | * Created by rob on 8/19/14. 10 | * Copyright (c) ontometrics, 2014 All Rights Reserved 11 | */ 12 | public interface IssueTracker { 13 | 14 | URL getBaseUrl(); 15 | 16 | URL getExternalBaseUrl(); 17 | 18 | URL getFeedUrl(String project, Date sinceDate); 19 | 20 | URL getChangesUrl(Issue issue); 21 | 22 | URL getAttachmentsUrl(Issue issue); 23 | 24 | String getIssueRestUrl(Issue issue); 25 | 26 | URL getIssueUrl(String issueIdentifier); 27 | 28 | URL getExternalIssueUrl(String issueIdentifier); 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/ontometrics/integrations/StemmerSanityTests.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations; 2 | 3 | import org.junit.Test; 4 | import org.tartarus.snowball.SnowballStemmer; 5 | import org.tartarus.snowball.ext.englishStemmer; 6 | 7 | import static org.hamcrest.CoreMatchers.is; 8 | import static org.hamcrest.MatcherAssert.assertThat; 9 | 10 | /** 11 | * Created by Rob on 9/7/14. 12 | * Copyright (c) ontometrics, 2014 All Rights Reserved 13 | */ 14 | public class StemmerSanityTests { 15 | 16 | @Test 17 | public void englishSanityCheck() { 18 | 19 | SnowballStemmer snowballStemmer = new englishStemmer(); 20 | snowballStemmer.setCurrent("Jumps"); 21 | snowballStemmer.stem(); 22 | String result = snowballStemmer.getCurrent(); 23 | 24 | assertThat(result, is("Jump")); 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/model/IssueFieldValue.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.model; 2 | 3 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; 4 | 5 | import javax.xml.bind.annotation.XmlAttribute; 6 | 7 | public class IssueFieldValue { 8 | @JacksonXmlText 9 | private String text; 10 | 11 | @XmlAttribute 12 | private String url; 13 | 14 | public IssueFieldValue(String text) { 15 | this.text = text; 16 | } 17 | 18 | public IssueFieldValue() { 19 | } 20 | 21 | public String getText() { 22 | return text; 23 | } 24 | 25 | public void setText(String text) { 26 | this.text = text; 27 | } 28 | 29 | public String getUrl() { 30 | return url; 31 | } 32 | 33 | public void setUrl(String url) { 34 | this.url = url; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 5 | Slack Integration 6 | 7 | 8 | com.ontometrics.integrations.jobs.WebContextJobStarter 9 | 10 | 11 | 12 | youTrackImageServlet 13 | com.ontometrics.integrations.youtrack.YouTrackImageServlet 14 | 15 | 16 | youTrackImageServlet 17 | /youtrack-image/* 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/model/IssueList.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.model; 2 | 3 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; 4 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; 5 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; 6 | 7 | import javax.xml.bind.annotation.XmlSeeAlso; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | @JacksonXmlRootElement(localName = "issues") 12 | @XmlSeeAlso({Issue.class, IssueField.class, IssueFieldValue.class}) 13 | public class IssueList { 14 | @JacksonXmlProperty(localName = "issue") 15 | @JacksonXmlElementWrapper(useWrapping = false) 16 | private List issues = new ArrayList<>(); 17 | 18 | public List getIssues() { 19 | return issues; 20 | } 21 | 22 | public void setIssues(List issues) { 23 | this.issues = issues; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/sources/StreamProvider.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.sources; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | 6 | /** 7 | * Provides a stream to resource handled by {@link com.ontometrics.integrations.sources.InputStreamHandler} 8 | * 9 | * StreamProvider.java 10 | * 11 | */ 12 | public interface StreamProvider { 13 | /** 14 | * Opens a resource and provides its {@link java.io.InputStream} in the call to 15 | * {@link com.ontometrics.integrations.sources.InputStreamHandler#handleStream(java.io.InputStream, int)} 16 | * @param resourceUrl url of the resource to be accessed/processed by inputStreamHandler) 17 | * @param inputStreamHandler resource stream handler 18 | * @param class of resource stream handling result 19 | * @return result 20 | * @throws IOException in case if source operation failed 21 | */ 22 | RES openResourceStream(URL resourceUrl, final InputStreamHandler inputStreamHandler) throws Exception; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/configuration/ConfigurationFactory.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | import org.apache.commons.configuration.Configuration; 4 | import org.apache.commons.configuration.ConfigurationException; 5 | import org.apache.commons.configuration.PropertiesConfiguration; 6 | 7 | /** 8 | * Factory for obtaining a global application config 9 | */ 10 | public class ConfigurationFactory { 11 | private static PropertiesConfiguration CONFIGURATION; 12 | 13 | /** 14 | * @return application configuration 15 | */ 16 | public static Configuration get() { 17 | if (CONFIGURATION == null) { 18 | try { 19 | CONFIGURATION = new PropertiesConfiguration(); 20 | CONFIGURATION.setListDelimiter(';'); 21 | CONFIGURATION.load("application.properties"); 22 | } catch (ConfigurationException e) { 23 | throw new RuntimeException("Failed to load configuration", e); 24 | } 25 | } 26 | return CONFIGURATION; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/util/TextUtil.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.util; 2 | 3 | import java.io.UnsupportedEncodingException; 4 | import java.net.URL; 5 | import java.net.URLDecoder; 6 | 7 | public class TextUtil { 8 | /** 9 | * Resolves first parameter of url 10 | * @param url url 11 | * @param paramName query param 12 | * @return param value or null if it's absent 13 | * @throws UnsupportedEncodingException if url is invalid 14 | */ 15 | public static String resolveUrlParameter(URL url, String paramName) throws UnsupportedEncodingException { 16 | final String[] pairs = url.getQuery().split("&"); 17 | for (String pair : pairs) { 18 | final int idx = pair.indexOf("="); 19 | final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), "UTF-8") : pair; 20 | if (paramName.equals(key)) { 21 | return idx > 0 && pair.length() > idx + 1 ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") : null; 22 | } 23 | } 24 | return null; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/com/ontometrics/integrations/events/ProcessEventChangeTest.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.events; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.Date; 6 | 7 | import static org.hamcrest.CoreMatchers.is; 8 | import static org.junit.Assert.*; 9 | 10 | public class ProcessEventChangeTest { 11 | 12 | @Test 13 | public void testThatWeCanBuild(){ 14 | Issue issue = new Issue.Builder() 15 | .projectPrefix("ASOC") 16 | .id(408) 17 | .title("ProcessEvents need to be persisted") 18 | .description("Right now we are pulling them from the stream, we have to save them.") 19 | .build(); 20 | ProcessEventChange change = new ProcessEventChange.Builder() 21 | .issue(issue) 22 | .field("State") 23 | .priorValue("Assigned") 24 | .currentValue("Fixed") 25 | .updated(new Date()) 26 | .updater("Noura") 27 | .build(); 28 | 29 | assertThat(change.getIssue().getId(), is(408)); 30 | 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/test/java/ontometrics/test/util/UrlStreamProvider.java: -------------------------------------------------------------------------------- 1 | package ontometrics.test.util; 2 | 3 | import com.ontometrics.integrations.sources.InputStreamHandler; 4 | import com.ontometrics.integrations.sources.StreamProvider; 5 | import org.apache.commons.io.IOUtils; 6 | import org.apache.http.HttpStatus; 7 | 8 | import java.io.InputStream; 9 | import java.net.URL; 10 | 11 | /** 12 | * URL resource provider, opens resource using {@link java.net.URL#openStream()} 13 | * 14 | * UrlResourceProvider.java 15 | */ 16 | public class UrlStreamProvider implements StreamProvider { 17 | 18 | private UrlStreamProvider() {} 19 | 20 | public static UrlStreamProvider instance (){ 21 | return new UrlStreamProvider(); 22 | } 23 | 24 | @Override 25 | public RES openResourceStream(URL resourceUrl, InputStreamHandler inputStreamHandler) throws Exception { 26 | InputStream is = null; 27 | try { 28 | return inputStreamHandler.handleStream(is = resourceUrl.openStream(), HttpStatus.SC_OK); 29 | } finally { 30 | IOUtils.closeQuietly(is); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/ontometrics/integrations/sources/ProcessEventTest.java: -------------------------------------------------------------------------------- 1 | package ontometrics.integrations.sources; 2 | 3 | import com.ontometrics.integrations.events.Issue; 4 | import com.ontometrics.integrations.events.ProcessEvent; 5 | import org.junit.Test; 6 | 7 | import java.net.URL; 8 | import java.util.Date; 9 | 10 | import static org.hamcrest.CoreMatchers.containsString; 11 | import static org.junit.Assert.assertThat; 12 | 13 | public class ProcessEventTest { 14 | 15 | @Test 16 | public void testGetID() throws Exception { 17 | Issue issue = new Issue.Builder().projectPrefix("ASOC").id(28) 18 | .title("ASOC-28: User searches for Users by name") 19 | .link(new URL("http://ontometrics.com:8085/issue/ASOC-28")) 20 | .description("lot of things changing here...") 21 | .build(); 22 | ProcessEvent processEvent = new ProcessEvent.Builder() 23 | .issue(issue) 24 | .published(new Date()) 25 | .build(); 26 | 27 | assertThat(processEvent.getIssue().toString(), containsString("ASOC-28")); 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | PROP.APP_DATA_DIR=${APP_DATA_DIR} 2 | PROP.YOUTRACK_URL=${YOUTRACK_URL} 3 | PROP.YOUTRACK_EXTERNAL_URL=${YOUTRACK_EXTERNAL_URL} 4 | PROP.APP_EXTERNAL_URL=${APP_EXTERNAL_URL} 5 | PROP.SLACK_WEBHOOK_PATH=${SLACK_WEBHOOK_PATH} 6 | PROP.AUTH_TYPE=${AUTH_TYPE} 7 | 8 | PROP.YOUTRACK_USERNAME=${YOUTRACK_USERNAME} 9 | PROP.YOUTRACK_PASSWORD=${YOUTRACK_PASSWORD} 10 | 11 | #Hub Properties 12 | PROP.HUB_URL=${HUB_URL} 13 | PROP.HUB_OAUTH_RESOURCE_SERVER_SERVICE_ID=${HUB_OAUTH_RESOURCE_SERVER_SERVICE_ID} 14 | PROP.HUB_OAUTH_CLIENT_SERVICE_ID=${HUB_OAUTH_CLIENT_SERVICE_ID} 15 | PROP.HUB_OAUTH_CLIENT_SERVICE_SECRET=${HUB_OAUTH_CLIENT_SERVICE_SECRET} 16 | 17 | 18 | # Time In minutes - how deep 19 | #should we look for issues in the past. If set to 10, it means that issues and changes 20 | # that happened not longer than 10 minutes will be posted to chat server 21 | PROP.ISSUE_HISTORY_WINDOW=${ISSUE_HISTORY_WINDOW} 22 | 23 | youtrack-slack.channel-mappings=${YOUTRACK_TO_SLACK_CHANNELS} 24 | youtrack-slack.default-channel=${DEFAULT_SLACK_CHANNEL} 25 | youtrack-slack.icon=${SLACKBOT_ICON} 26 | 27 | #semicolon separated fields 28 | excluded-youtrack-fields=${EXCLUDED_YOUTRACK_FIELDS} -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/db/MapDb.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.db; 2 | 3 | import com.ontometrics.integrations.configuration.ConfigurationFactory; 4 | import org.mapdb.DB; 5 | import org.mapdb.DBMaker; 6 | 7 | import java.io.File; 8 | import java.util.Map; 9 | 10 | /** 11 | * Instance of mapDB 12 | */ 13 | public class MapDb { 14 | private static final MapDb instance = new MapDb(); 15 | private static final String DB_NAME = "app_db"; 16 | private static final String ATTACHMENT_MAP = "attachments"; 17 | 18 | private DB db; 19 | private Map attachmentMap; 20 | 21 | public static MapDb instance() { 22 | return instance; 23 | } 24 | 25 | @SuppressWarnings("unchecked") 26 | private MapDb() { 27 | String dataDir = ConfigurationFactory.get().getString("PROP.APP_DATA_DIR"); 28 | db = DBMaker.fileDB(new File(dataDir, DB_NAME)).make(); 29 | attachmentMap = (Map)db.hashMap(ATTACHMENT_MAP).createOrOpen(); 30 | } 31 | 32 | public DB getDb() { 33 | return db; 34 | } 35 | 36 | public Map getAttachmentMap() { 37 | return attachmentMap; 38 | } 39 | 40 | public void close() { 41 | db.close(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/ontometrics/test/util/TestUtil.java: -------------------------------------------------------------------------------- 1 | package ontometrics.test.util; 2 | 3 | import com.ontometrics.integrations.configuration.EventProcessorConfiguration; 4 | import com.ontometrics.util.DateBuilder; 5 | 6 | import java.net.URL; 7 | import java.util.Date; 8 | 9 | /** 10 | * Created by rob on 7/14/14. 11 | */ 12 | public class TestUtil { 13 | 14 | public static URL getFileAsURL(String path) { 15 | URL url = TestUtil.class.getResource(path); 16 | if (url == null && !path.startsWith("/resources")) { 17 | url = TestUtil.class.getResource("/resources" + path); 18 | } 19 | return url; 20 | } 21 | 22 | /** 23 | * Sets {@link com.ontometrics.integrations.configuration.EventProcessorConfiguration#getIssueHistoryWindowInMinutes()} 24 | * setting such value which will be enough to fetch all events from the feed 25 | */ 26 | public static void setIssueHistoryWindowSettingToCoverAllIssues() { 27 | Date oldestIssueDate = new DateBuilder().year(2013).build(); 28 | final int offsetInMinutes = (int) ((new Date().getTime() - oldestIssueDate.getTime()) / 1000 / 60) + 5; 29 | EventProcessorConfiguration.instance().setIssueHistoryWindowInMinutes(offsetInMinutes); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/model/IssueField.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.model; 2 | 3 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; 4 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; 5 | 6 | import javax.xml.bind.annotation.XmlAttribute; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class IssueField { 11 | @XmlAttribute 12 | private String name; 13 | 14 | @JacksonXmlProperty(localName = "value") 15 | @JacksonXmlElementWrapper(useWrapping = false) 16 | private List values = new ArrayList<>(); 17 | 18 | public String getName() { 19 | return name; 20 | } 21 | 22 | public void setName(String name) { 23 | this.name = name; 24 | } 25 | 26 | public List getValues() { 27 | return values; 28 | } 29 | 30 | public void setValues(List values) { 31 | this.values = values; 32 | } 33 | 34 | 35 | /** 36 | * @return first value of the values in this IssueField 37 | */ 38 | public String getValue() { 39 | if (!values.isEmpty()){ 40 | return values.get(0).getText(); 41 | } 42 | return null; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/ontometrics/integrations/events/IssueEditSessionTest.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.events; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.Matchers.hasSize; 5 | import static org.junit.Assert.*; 6 | 7 | import com.ontometrics.util.DateBuilder; 8 | import org.junit.Test; 9 | 10 | import java.net.MalformedURLException; 11 | import java.util.Date; 12 | 13 | public class IssueEditSessionTest { 14 | 15 | @Test 16 | public void canBuildSession() throws MalformedURLException { 17 | 18 | IssueEditSession issueEditSession = TestDataFactory.build(); 19 | 20 | assertThat(issueEditSession.getChanges(), hasSize(2)); 21 | assertThat(issueEditSession.getUpdater(), is("Noura")); 22 | 23 | } 24 | 25 | @Test 26 | public void canDetectNewTickets() { 27 | Date justNow = new Date(); 28 | Date threeMinutesAgo = new DateBuilder().start(justNow).addMinutes(-3).build(); 29 | Issue issue = new Issue.Builder().projectPrefix("AIA").id(721).creator("Rob").created(threeMinutesAgo).build(); 30 | IssueEditSession issueEditSession = new IssueEditSession.Builder().issue(issue).updated(justNow).build(); 31 | 32 | assertThat(issueEditSession.isCreationEdit(), is(true)); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/configuration/StreamProviderFactory.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | import com.ontometrics.integrations.sources.AuthenticatedHttpStreamProvider; 4 | import com.ontometrics.integrations.sources.StreamProvider; 5 | import org.apache.commons.configuration.Configuration; 6 | 7 | public class StreamProviderFactory { 8 | 9 | private static final String CREDENTIALS_AUTH_TYPE = "credentials"; 10 | 11 | public static StreamProvider createStreamProvider(Configuration configuration) { 12 | if (configuration.getString("PROP.AUTH_TYPE", CREDENTIALS_AUTH_TYPE).equalsIgnoreCase(CREDENTIALS_AUTH_TYPE)) { 13 | return AuthenticatedHttpStreamProvider.basicAuthenticatedHttpStreamProvider( 14 | configuration.getString("PROP.YOUTRACK_USERNAME"), configuration.getString("PROP.YOUTRACK_PASSWORD") 15 | ); 16 | } 17 | 18 | return AuthenticatedHttpStreamProvider.hubAuthenticatedHttpStreamProvider( 19 | configuration.getString("PROP.HUB_OAUTH_CLIENT_SERVICE_ID"), 20 | configuration.getString("PROP.HUB_OAUTH_CLIENT_SERVICE_SECRET"), 21 | configuration.getString("PROP.HUB_OAUTH_RESOURCE_SERVER_SERVICE_ID"), 22 | configuration.getString("PROP.HUB_URL") 23 | ); 24 | } 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/model/ProjectList.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.model; 2 | 3 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; 4 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; 5 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; 6 | 7 | import javax.xml.bind.annotation.XmlAttribute; 8 | import javax.xml.bind.annotation.XmlSeeAlso; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | @JacksonXmlRootElement(localName = "projects") 13 | @XmlSeeAlso({Issue.class, IssueField.class, IssueFieldValue.class}) 14 | public class ProjectList { 15 | @JacksonXmlProperty(localName = "project") 16 | @JacksonXmlElementWrapper(useWrapping = false) 17 | private List projects = new ArrayList<>(); 18 | 19 | 20 | public List getProjects() { 21 | return projects; 22 | } 23 | 24 | public void setProjects(List projects) { 25 | this.projects = projects; 26 | } 27 | 28 | public static class Project { 29 | @XmlAttribute 30 | private String shortName; 31 | 32 | public String getShortName() { 33 | return shortName; 34 | } 35 | 36 | public void setShortName(String shortName) { 37 | this.shortName = shortName; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | target/logs/application.log 5 | 6 | 7 | target/logs/application.log.%d{yyyy-MM-dd}.%i.log.gz 8 | 10 | 11 | 100MB 12 | 13 | 14 | 100 15 | 16 | 17 | %date - [%level] - from %logger in %thread %n%message%n%xException%n 18 | 19 | 20 | 21 | 22 | 23 | %date - [%level] - %logger{15} - %message%n%xException{5} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/model/Issue.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.model; 2 | 3 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; 4 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; 5 | import com.google.common.base.Predicate; 6 | import com.google.common.collect.Iterables; 7 | 8 | import javax.xml.bind.annotation.XmlAttribute; 9 | import java.util.List; 10 | 11 | public class Issue { 12 | @XmlAttribute 13 | private String id; 14 | 15 | @JacksonXmlProperty(localName = "field") 16 | @JacksonXmlElementWrapper(useWrapping = false) 17 | private List fields; 18 | 19 | public String getId() { 20 | return id; 21 | } 22 | 23 | public void setId(String id) { 24 | this.id = id; 25 | } 26 | 27 | public List getFields() { 28 | return fields; 29 | } 30 | 31 | public void setFields(List fields) { 32 | this.fields = fields; 33 | } 34 | 35 | public String getFieldValue(final String fieldName) { 36 | IssueField field = findIssueField(fieldName); 37 | if (field != null){ 38 | return field.getValue(); 39 | } 40 | return null; 41 | } 42 | 43 | public IssueField findIssueField(final String field) { 44 | return Iterables.tryFind(fields, new Predicate() { 45 | @Override 46 | public boolean apply(IssueField issueField) { 47 | return issueField.getName().equals(field); 48 | } 49 | }).orNull(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/configuration/ChatServer.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | import com.ontometrics.integrations.events.Issue; 4 | import com.ontometrics.integrations.events.IssueEditSession; 5 | import com.ontometrics.integrations.sources.ChannelMapper; 6 | 7 | /** 8 | *

9 | * Provides a means of interfacing to the Chat Server that is going to be 10 | * the outbound channel we use to communicate with the team. 11 | *

12 | *

13 | * Mapping of messages to channels is done internally. The post function assumes that we 14 | * know where we want the message to go inside the various channels/rooms, and thus, who 15 | * will see it. 16 | *

17 | *

18 | * Created by Rob on 8/23/14. 19 | * Copyright (c) ontometrics, 2014 All Rights Reserved 20 | */ 21 | public interface ChatServer { 22 | 23 | /** 24 | * Put a message about creation of the issue 25 | * 26 | * @param issue created issue 27 | */ 28 | void postIssueCreation(Issue issue); 29 | 30 | /** 31 | * Put a message about a change out to the chat server. 32 | * 33 | * @param issueEditSession information about the things changed in a single edit session 34 | */ 35 | void post(IssueEditSession issueEditSession); 36 | 37 | /** 38 | * Provides a list of the Users that are members of our chat server team. 39 | * 40 | * @return the usernames that are known members right now 41 | */ 42 | // List getUsers(); 43 | 44 | /** 45 | * 46 | * @return channel mapper 47 | */ 48 | ChannelMapper getChannelMapper(); 49 | } 50 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue-feed-comment-attachment-only.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | YouTrack 4 | http://ontometrics.com:8085/_rss/issues 5 | 6 | 7 | AIA-38: Support for Attachments 8 | http://ontometrics.com:8085/issue/AIA-38 9 | 10 | Reporter Rob Williams (rob) Rob Williams (rob) Created Sep 4, 2014 9:03:42 PM Updated Sep 5, 2014 12:44:52 PM Resolved Sep 4, 2014 9:05:08 PM Priority Normal Type Task State Fixed Assignee Rob Williams (rob) Subsystem No subsystem Fix versions 0.0.3 Affected versions Unknown Fixed in build Next Build

Currently, attachments do not appear in changes. So we have to have some logic that just looks for attachments that were added in the time window of interest and simply add them as an edit.
12 | ]]> 13 | 14 | Fri, 05 Sep 2014 19:44:52 UT 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/test/java/ontometrics/integrations/sources/ExternalStreamProviderTest.java: -------------------------------------------------------------------------------- 1 | package ontometrics.integrations.sources; 2 | 3 | import com.ontometrics.integrations.sources.AuthenticatedHttpStreamProvider; 4 | import com.ontometrics.integrations.sources.Authenticator; 5 | import com.ontometrics.integrations.sources.InputStreamHandler; 6 | import org.apache.commons.io.IOUtils; 7 | import org.apache.http.client.fluent.Executor; 8 | import org.apache.http.client.fluent.Request; 9 | import org.junit.Test; 10 | 11 | import java.io.InputStream; 12 | import java.net.URL; 13 | 14 | import static org.hamcrest.CoreMatchers.*; 15 | import static org.hamcrest.MatcherAssert.assertThat; 16 | 17 | /** 18 | * Test for {@link com.ontometrics.integrations.sources.AuthenticatedHttpStreamProvider} 19 | * ExternalResourceInputStreamProviderTest.java 20 | */ 21 | public class ExternalStreamProviderTest { 22 | /* 23 | @Test 24 | public void testExternalResourceWorks() throws Exception { 25 | String res; 26 | res = new AuthenticatedHttpStreamProvider(new Authenticator() { 27 | @Override 28 | public Request authenticate(Executor httpExecutor, Request request) { 29 | return request; 30 | } 31 | }).openResourceStream(new URL("http://ya.ru"), new InputStreamHandler() { 32 | @Override 33 | public String handleStream(InputStream is, int responseCode) throws Exception { 34 | return IOUtils.toString(is); 35 | } 36 | }); 37 | assertThat(res, notNullValue()); 38 | assertThat(res.length(), not(is(0))); 39 | } 40 | */ 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/sources/ChannelMapper.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.sources; 2 | 3 | import com.ontometrics.integrations.events.Issue; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * Created by rob on 7/17/14. 12 | * Copyright (c) ontometrics, 2014 All Rights Reserved 13 | */ 14 | public class ChannelMapper { 15 | 16 | private final Map mappings; 17 | private final String defaultChannel; 18 | private static final Logger log = LoggerFactory.getLogger(ChannelMapper.class); 19 | 20 | public ChannelMapper(Builder builder) { 21 | mappings = builder.mappings; 22 | defaultChannel = builder.defaultChannel; 23 | } 24 | 25 | public static class Builder { 26 | 27 | Map mappings = new HashMap<>(); 28 | private String defaultChannel; 29 | 30 | public Builder defaultChannel(String defaultChannel){ 31 | this.defaultChannel = defaultChannel; 32 | return this; 33 | } 34 | 35 | public Builder addMapping(String from, String to){ 36 | mappings.put(from, to); 37 | return this; 38 | } 39 | 40 | public ChannelMapper build(){ 41 | return new ChannelMapper(this); 42 | } 43 | } 44 | 45 | 46 | public String getChannel(final Issue issue){ 47 | String targetChannel = mappings.get(issue.getPrefix()); 48 | log.debug("Source: {} Target: {}", issue.getPrefix(), targetChannel); 49 | return targetChannel != null ? targetChannel : defaultChannel; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/sources/ChannelMapperFactory.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.sources; 2 | 3 | import org.apache.commons.configuration.Configuration; 4 | 5 | /** 6 | * Factory for creation of {@link com.ontometrics.integrations.sources.ChannelMapper} 7 | * 8 | * ChannelMapperFactory.java 9 | */ 10 | public class ChannelMapperFactory { 11 | 12 | 13 | public static final String CHANNEL_MAPPINGS = "channel-mappings"; 14 | public static final String DEFAULT_CHANNEL = "default-channel"; 15 | 16 | /** 17 | * "default-slack-channel" specify list of mappings in the format "${youtrack.project.prefix}->${slack.channel.name}" 18 | * delimited by ";" (or whatever delimiter which as treated as list delimiter by passed configuration instance" 19 | * For example "ASOC->vixlet;HA->jobspider;DMAN->dminder" 20 | * @param configuration configuration 21 | * @return ChannelMapper instance created from properties "youtrack-to-slack-channels" and "default-slack-channel" 22 | */ 23 | public static ChannelMapper fromConfiguration(Configuration configuration, String propertyPrefix) { 24 | String [] mappings = configuration.getStringArray(propertyPrefix + CHANNEL_MAPPINGS); 25 | String defaultChannel = configuration.getString(propertyPrefix + DEFAULT_CHANNEL); 26 | ChannelMapper.Builder builder = new ChannelMapper.Builder().defaultChannel(defaultChannel); 27 | for (String mapping : mappings) { 28 | String [] keyValue = mapping.split("->"); 29 | if (keyValue.length == 2) { 30 | builder.addMapping(keyValue[0], keyValue[1]); 31 | } 32 | } 33 | 34 | return builder.build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ${catalina.base}/logs/application.log 5 | 6 | 7 | ${catalina.base}/logs/application.log.%d{yyyy-MM-dd}.%i.log.gz 8 | 10 | 11 | 100MB 12 | 13 | 14 | 100 15 | 16 | 17 | %date - [%level] - from %logger in %thread %n%message%n%xException%n 18 | 19 | 20 | 21 | 22 | 23 | %date - [%level] - %logger{15} - %message%n%xException{5} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/test/java/com/ontometrics/integrations/configuration/CheckDuplicateMessagesChatServer.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | import com.ontometrics.integrations.events.Issue; 4 | import com.ontometrics.integrations.events.IssueEditSession; 5 | import com.ontometrics.integrations.sources.ChannelMapper; 6 | import org.junit.Assert; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * Chat server which will fail test if it will got any duplicates messages 13 | * TrackingChatServer.java 14 | */ 15 | public class CheckDuplicateMessagesChatServer implements ChatServer { 16 | 17 | private List createdIssues = new ArrayList<>(); 18 | private List postedIssueEditSessions = new ArrayList<>(); 19 | 20 | @Override 21 | public void postIssueCreation(Issue issue) { 22 | if (!createdIssues.contains(issue)) { 23 | createdIssues.add(issue); 24 | } else { 25 | Assert.fail("Issue "+issue+" has been reported as created before"); 26 | } 27 | } 28 | 29 | @Override 30 | public void post(IssueEditSession issueEditSession) { 31 | if (!postedIssueEditSessions.contains(issueEditSession)) { 32 | postedIssueEditSessions.add(issueEditSession); 33 | } else { 34 | Assert.fail("IssueEditSession "+issueEditSession+" has been reported before"); 35 | } 36 | 37 | } 38 | 39 | @Override 40 | public ChannelMapper getChannelMapper() { 41 | return null; 42 | } 43 | 44 | public List getCreatedIssues() { 45 | return createdIssues; 46 | } 47 | 48 | public List getPostedIssueEditSessions() { 49 | return postedIssueEditSessions; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/com/ontometrics/integrations/events/TestDataFactory.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.events; 2 | 3 | import java.net.MalformedURLException; 4 | import java.net.URL; 5 | import java.util.ArrayList; 6 | import java.util.Date; 7 | import java.util.List; 8 | 9 | /** 10 | * User: Rob 11 | * Date: 8/23/14 12 | * Time: 8:38 PM 13 | *

14 | * (c) ontometrics 2014, All Rights Reserved 15 | */ 16 | public class TestDataFactory { 17 | 18 | public static IssueEditSession build() throws MalformedURLException { 19 | URL linkUrl = new URL("http://ontometrics.com:8085/issues/ASOC-408"); 20 | Issue issue = new Issue.Builder().projectPrefix("ASOC").id(408) 21 | .title("ASOC-408: Need to toggle follow button") 22 | .description("Right now the button does not change from Follow to Unfollow.") 23 | .link(linkUrl) 24 | .build(); 25 | 26 | IssueEdit edit1 = new IssueEdit.Builder() 27 | .issue(issue) 28 | .field("State") 29 | .priorValue("Assigned") 30 | .currentValue("Fixed") 31 | .build(); 32 | 33 | IssueEdit edit2 = new IssueEdit.Builder() 34 | .issue(issue) 35 | .field("Priority") 36 | .priorValue("Normal") 37 | .currentValue("Critical") 38 | .build(); 39 | 40 | List edits = new ArrayList<>(); 41 | edits.add(edit1); 42 | edits.add(edit2); 43 | 44 | return new IssueEditSession.Builder() 45 | .issue(issue) 46 | .changes(edits) 47 | .updated(new Date()) 48 | .updater("Noura") 49 | .build(); 50 | 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/events/IssueLink.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.events; 2 | 3 | /** 4 | * User: Rob 5 | * Date: 9/11/14 6 | * Time: 4:41 PM 7 | *

8 | * (c) ontometrics 2014, All Rights Reserved 9 | */ 10 | public class IssueLink { 11 | 12 | private final String type; 13 | private final String role; 14 | private final String relatedIssueID; 15 | 16 | public IssueLink(Builder builder) { 17 | type = builder.type; 18 | role = builder.role; 19 | relatedIssueID = builder.relatedIssueID; 20 | } 21 | 22 | public static class Builder { 23 | 24 | private String type; 25 | private String role; 26 | private String relatedIssueID; 27 | 28 | public Builder type(String type){ 29 | this.type = type; 30 | return this; 31 | } 32 | 33 | public Builder role(String role){ 34 | this.role = role; 35 | return this; 36 | } 37 | 38 | public Builder relatedIssue(String relatedIssueID){ 39 | this.relatedIssueID = relatedIssueID; 40 | return this; 41 | } 42 | 43 | public IssueLink build(){ 44 | return new IssueLink(this); 45 | } 46 | } 47 | 48 | public String getType() { 49 | return type; 50 | } 51 | 52 | public String getRole() { 53 | return role; 54 | } 55 | 56 | public String getRelatedIssueID() { 57 | return relatedIssueID; 58 | } 59 | 60 | @Override 61 | public String toString() { 62 | return "IssueLink{" + 63 | "type='" + type + '\'' + 64 | ", role='" + role + '\'' + 65 | ", relatedIssueID='" + relatedIssueID + '\'' + 66 | '}'; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/ontometrics/integrations/configuration/YouTrackInstanceTest.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | import com.ontometrics.integrations.events.Issue; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import java.net.MalformedURLException; 8 | import java.net.URL; 9 | 10 | import static org.hamcrest.CoreMatchers.is; 11 | import static org.junit.Assert.*; 12 | 13 | public class YouTrackInstanceTest { 14 | 15 | private YouTrackInstance youTrackInstance; 16 | 17 | @Before 18 | public void setup(){ 19 | youTrackInstance = new YouTrackInstance.Builder().baseUrl("http://ontometrics.com:8085").build(); 20 | } 21 | 22 | @Test 23 | public void testGetBaseUrl() throws Exception { 24 | assertThat(youTrackInstance.getBaseUrl(), is(new URL("http://ontometrics.com:8085"))); 25 | } 26 | 27 | /* 28 | @Test 29 | public void testGetFeedUrl() throws Exception { 30 | assertThat(youTrackInstance.getFeedUrl(), is(new URL("http://ontometrics.com:8085/_rss/issues"))); 31 | } 32 | */ 33 | @Test 34 | public void testGetChangesUrl() throws Exception { 35 | assertThat(youTrackInstance.getChangesUrl(new Issue.Builder().projectPrefix("ASOC").id(505).build()), 36 | is(new URL("http://ontometrics.com:8085/rest/issue/ASOC-505/changes"))); 37 | } 38 | 39 | @Test 40 | public void testAttachmentsUrlBuilder() throws MalformedURLException { 41 | assertThat(youTrackInstance.getAttachmentsUrl(new Issue.Builder().projectPrefix("ASOC").id(480).build()), 42 | is(new URL("http://ontometrics.com:8085/rest/issue/ASOC-480/attachment"))); 43 | } 44 | 45 | @Test 46 | public void testThatNoPortWorks() throws MalformedURLException { 47 | youTrackInstance = new YouTrackInstance.Builder().baseUrl("http://ontometrics.com").build(); 48 | 49 | assertThat(youTrackInstance.getBaseUrl(), is(new URL("http://ontometrics.com"))); 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/events/AttachmentEvent.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.events; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * User: Rob 7 | * Date: 9/1/14 8 | * Time: 4:43 PM 9 | *

10 | * (c) ontometrics 2014, All Rights Reserved 11 | */ 12 | public class AttachmentEvent { 13 | 14 | private final String name; 15 | private final Date created; 16 | private final String author; 17 | private final String fileUrl; 18 | 19 | public AttachmentEvent(Builder builder) { 20 | name = builder.name; 21 | created = builder.created; 22 | author = builder.author; 23 | fileUrl = builder.fileUrl; 24 | } 25 | 26 | public static class Builder { 27 | 28 | private Date created; 29 | private String fileUrl; 30 | private String name; 31 | private String author; 32 | 33 | public Builder name(String name){ 34 | this.name = name; 35 | return this; 36 | } 37 | 38 | public Builder created(Date created){ 39 | this.created = created; 40 | return this; 41 | } 42 | 43 | public Builder url(String fileUrl){ 44 | this.fileUrl = fileUrl; 45 | return this; 46 | } 47 | 48 | public Builder author(String author){ 49 | this.author = author; 50 | return this; 51 | } 52 | 53 | public AttachmentEvent build(){ 54 | return new AttachmentEvent(this); 55 | } 56 | } 57 | 58 | public String getName() { 59 | return name; 60 | } 61 | 62 | public Date getCreated() { 63 | return created; 64 | } 65 | 66 | public String getAuthor() { 67 | return author; 68 | } 69 | 70 | public String getFileUrl() { 71 | return fileUrl; 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | return "AttachmentEvent{" + 77 | "name='" + name + '\'' + 78 | ", created=" + created + 79 | ", author='" + author + '\'' + 80 | ", fileUrl='" + fileUrl + '\'' + 81 | '}'; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/ontometrics/test/util/ExtractorUtilTests.java: -------------------------------------------------------------------------------- 1 | package ontometrics.test.util; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import javax.xml.stream.*; 9 | import javax.xml.stream.events.XMLEvent; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.net.URL; 13 | 14 | /** 15 | * Created by rob on 8/19/14. 16 | * Copyright (c) ontometrics, 2014 All Rights Reserved 17 | */ 18 | public class ExtractorUtilTests { 19 | private Logger log = LoggerFactory.getLogger(ExtractorUtilTests.class); 20 | private URL changesUrl; 21 | private XMLEventReader reader; 22 | 23 | @Before 24 | public void setup() throws IOException, XMLStreamException { 25 | changesUrl = TestUtil.getFileAsURL("/feeds/issue-changes.xml"); 26 | InputStream inputStream = changesUrl.openStream(); 27 | XMLInputFactory inputFactory = XMLInputFactory.newInstance(); 28 | reader = inputFactory.createXMLEventReader(inputStream); 29 | } 30 | 31 | @Test 32 | public void canExtractChangesAsNestedOperations() throws IOException, XMLStreamException { 33 | int elementCounter = 0; 34 | String currentElementName = ""; 35 | while (reader.hasNext()) { 36 | XMLEvent event = reader.nextEvent(); 37 | if (event.getEventType()== XMLStreamConstants.START_ELEMENT){ 38 | currentElementName = event.asStartElement().getName().getLocalPart(); 39 | if (currentElementName.equals("changes")){ 40 | extractChanges(event); 41 | } 42 | } 43 | 44 | } 45 | 46 | } 47 | 48 | private void extractChanges(XMLEvent event) { 49 | log.info("changes extraction on: {}", event); 50 | StringBuilder stringBuilder = new StringBuilder(); 51 | stringBuilder.append(buildIssueLink()); 52 | stringBuilder.append(extractIndividualChanges()); 53 | } 54 | 55 | private String extractIndividualChanges() { 56 | return ""; 57 | } 58 | 59 | private String buildIssueLink() { 60 | return ""; 61 | } 62 | 63 | @Test 64 | public void canExtractXSITypeFromElement(){ 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/events/IssueEdit.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.events; 2 | 3 | /** 4 | * Created by Rob on 8/24/14. 5 | * Copyright (c) ontometrics, 2014 All Rights Reserved 6 | */ 7 | public class IssueEdit { 8 | 9 | private final Issue issue; 10 | private final String field; 11 | private final String priorValue; 12 | private final String currentValue; 13 | 14 | public IssueEdit(Builder builder) { 15 | issue = builder.issue; 16 | field = builder.field; 17 | priorValue = builder.priorValue; 18 | currentValue = builder.currentValue; 19 | } 20 | 21 | public static class Builder { 22 | 23 | private Issue issue; 24 | private String field; 25 | private String priorValue; 26 | private String currentValue; 27 | 28 | public Builder issue(Issue issue){ 29 | this.issue = issue; 30 | return this; 31 | } 32 | 33 | public Builder field(String field){ 34 | this.field = field; 35 | return this; 36 | } 37 | 38 | public Builder priorValue(String priorValue){ 39 | this.priorValue = priorValue; 40 | return this; 41 | } 42 | 43 | public Builder currentValue(String currentValue){ 44 | this.currentValue = currentValue; 45 | return this; 46 | } 47 | 48 | public IssueEdit build(){ 49 | return new IssueEdit(this); 50 | } 51 | } 52 | 53 | public Issue getIssue() { 54 | return issue; 55 | } 56 | 57 | public String getField() { 58 | return field; 59 | } 60 | 61 | public String getPriorValue() { 62 | return priorValue; 63 | } 64 | 65 | public String getCurrentValue() { 66 | return currentValue; 67 | } 68 | 69 | @Override 70 | public String toString() { 71 | StringBuilder b = new StringBuilder(); 72 | if (getPriorValue().length() > 0){ 73 | b.append(getField()).append(":"); 74 | b.append(String.format(" %s -> ", getPriorValue())); 75 | } else { 76 | b.append("set ").append(getField()).append(" to "); 77 | } 78 | b.append(getCurrentValue()); 79 | return b.toString(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue-feed-with-new-ticket-one-comment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | YouTrack 4 | http://ontometrics.com:8085/_rss/issues 5 | 6 | 7 | 8 | HA-696: Searching for Company and Contact names is difficult using the search bar 9 | 10 | http://ontometrics.com:8085/issue/HA-696 11 | 12 | Reporter Darren Mabashov (dmabashov) Darren Mabashov (dmabashov) Created Sep 2, 2014 3:17:56 PM Updated Sep 2, 2014 4:27:31 PM Priority Normal Type Bug State Open Assignee Unassigned Subsystem No subsystem Fix versions Unscheduled Affected versions Unknown Fixed in build Next Build aggregatetimespent No aggregatetimespent aggregatetimeestimate No aggregatetimeestimate resolution No resolution timespent No timespent timeestimate No timeestimate

The search bar feature won't bring up results unless the term is spelled correctly. Perhaps a potential solution would be to add in functionality similar to what we have when we create a new lead (see screenshot)

Perhaps an auto-suggest function (see screen shot) similar to the functionality of when you are creating a lead would make sense here?

I think it would be great as well that if we spelled a word incorrectly the system will either auto correct and say like "did you mean...." or bring up results from a partial word we bring up. Just some thoughts.
14 | ]]> 15 | 16 | Tue, 02 Sep 2014 23:27:31 UT 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/test/java/ontometrics/integrations/sources/ChannelMapperFactoryTest.java: -------------------------------------------------------------------------------- 1 | package ontometrics.integrations.sources; 2 | 3 | import com.ontometrics.integrations.configuration.ConfigurationFactory; 4 | import com.ontometrics.integrations.events.Issue; 5 | import com.ontometrics.integrations.sources.ChannelMapper; 6 | import com.ontometrics.integrations.sources.ChannelMapperFactory; 7 | import org.apache.commons.configuration.PropertiesConfiguration; 8 | import org.junit.Test; 9 | 10 | import static org.hamcrest.CoreMatchers.*; 11 | import static org.hamcrest.MatcherAssert.assertThat; 12 | 13 | /** 14 | * ChannelMapperFactoryTest.java 15 | */ 16 | public class ChannelMapperFactoryTest { 17 | 18 | @Test 19 | public void canCreateMapperFromConfiguration(){ 20 | checkCanCreatePropertiesConfiguration(""); 21 | checkCanCreatePropertiesConfiguration("prefix."); 22 | } 23 | 24 | private void checkCanCreatePropertiesConfiguration(String mapperPrefix) { 25 | PropertiesConfiguration configuration = new PropertiesConfiguration(); 26 | configuration.setListDelimiter(';'); 27 | configuration.setProperty(mapperPrefix + ChannelMapperFactory.CHANNEL_MAPPINGS, "ABC->xyz;XYZ->abc"); 28 | configuration.setProperty(mapperPrefix + ChannelMapperFactory.DEFAULT_CHANNEL, "apple"); 29 | 30 | ChannelMapper mapper = ChannelMapperFactory.fromConfiguration(configuration, mapperPrefix); 31 | assertThat(mapper, notNullValue()); 32 | assertThat(mapper.getChannel(issue("ABC")), is("xyz")); 33 | assertThat(mapper.getChannel(issue("XYZ")), is("abc")); 34 | assertThat(mapper.getChannel(issue("microsoft")), is("apple")); 35 | assertThat(mapper.getChannel(issue("nothing")), is("apple")); 36 | } 37 | 38 | @Test 39 | public void testThatMapperCreatedFromDefaultAppProperties () { 40 | String mapperPrefix = "youtrack-slack."; 41 | ChannelMapper mapper = ChannelMapperFactory.fromConfiguration(ConfigurationFactory.get(), mapperPrefix); 42 | assertThat(mapper, notNullValue()); 43 | assertThat(mapper.getChannel(issue("HA")), is("jobspider")); 44 | assertThat(mapper.getChannel(issue("ASOC")), is("vixlet")); 45 | assertThat(mapper.getChannel(issue("DMAN")), is("dminder")); 46 | assertThat(mapper.getChannel(issue("nothing")), is("process")); 47 | } 48 | 49 | 50 | private static Issue issue(String prefix) { 51 | return new Issue.Builder().projectPrefix(prefix).build(); 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/events/ProcessEventChangeSet.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.events; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Date; 5 | import java.util.List; 6 | 7 | /** 8 | * Created by Rob on 7/23/14. 9 | * Copyright (c) ontometrics, 2014 All Rights Reserved 10 | */ 11 | public class ProcessEventChangeSet { 12 | 13 | private final Date changedOn; 14 | private final String editor; 15 | private final Issue issue; 16 | private final List changes; 17 | 18 | public ProcessEventChangeSet(Builder builder) { 19 | changedOn = builder.changedOn; 20 | editor = builder.editor; 21 | changes = builder.changes; 22 | issue = builder.issue; 23 | } 24 | 25 | public static class Builder { 26 | 27 | private String editor; 28 | private Date changedOn; 29 | private List changes; 30 | private Issue issue; 31 | 32 | public Builder issue(Issue issue){ 33 | this.issue = issue; 34 | return this; 35 | } 36 | 37 | public Builder editor(String editor){ 38 | this.editor = editor; 39 | return this; 40 | } 41 | 42 | public Builder changedOn(Date changedOn){ 43 | this.changedOn = changedOn; 44 | return this; 45 | } 46 | 47 | public Builder changes(List changes){ 48 | this.changes = changes; 49 | return this; 50 | } 51 | 52 | public Builder change(ProcessEventChange change) { 53 | changes = new ArrayList<>(1); 54 | changes.add(change); 55 | return this; 56 | } 57 | 58 | public ProcessEventChangeSet build(){ 59 | return new ProcessEventChangeSet(this); 60 | } 61 | 62 | } 63 | 64 | public Issue getIssue() { 65 | return issue; 66 | } 67 | 68 | public Date getChangedOn() { 69 | return changedOn; 70 | } 71 | 72 | public String getEditor() { 73 | return editor; 74 | } 75 | 76 | public List getChanges() { 77 | return changes; 78 | } 79 | 80 | @Override 81 | public String toString() { 82 | return "ProcessEventChangeSet{" + 83 | "issue=" + issue + 84 | ", changedOn=" + changedOn + 85 | ", editor='" + editor + '\'' + 86 | ", changes=" + changes + 87 | '}'; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/sources/HubAuthenticator.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.sources; 2 | 3 | import com.intellij.hub.auth.oauth2.token.AccessToken; 4 | import jetbrains.jetpass.client.hub.HubClient; 5 | import jetbrains.jetpass.client.oauth2.OAuth2Client; 6 | import jetbrains.jetpass.client.oauth2.token.OAuth2ClientFlow; 7 | import org.apache.http.client.fluent.Executor; 8 | import org.apache.http.client.fluent.Request; 9 | import org.joda.time.DateTime; 10 | 11 | import java.net.URL; 12 | 13 | 14 | /** 15 | * This use Hub Client Credentials OAuth 16 | * https://www.jetbrains.com/hub/help/1.0/Client-Credentials.html 17 | */ 18 | public class HubAuthenticator implements Authenticator { 19 | 20 | 21 | private String clientServiceId; 22 | private String clientServiceSecret; 23 | private String resourceServerServiceId; 24 | private String hubUrl; 25 | 26 | private AccessToken accessToken; 27 | 28 | 29 | public HubAuthenticator(String clientServiceId, String clientServiceSecret, String resourceServerServiceId, String hubUrl) { 30 | this.clientServiceId = clientServiceId; 31 | this.clientServiceSecret = clientServiceSecret; 32 | this.resourceServerServiceId = resourceServerServiceId; 33 | this.hubUrl = hubUrl; 34 | } 35 | 36 | @Override 37 | public Request authenticate(URL resourceUrl, Executor httpExecutor, Request request) { 38 | if (accessToken == null || isExpired(accessToken)) { 39 | accessToken = resolveAccessToken(); 40 | } 41 | 42 | return request.addHeader("Authorization", AccessToken.encodeHeader(accessToken)); 43 | } 44 | 45 | private boolean isExpired(AccessToken accessToken) { 46 | return new DateTime().plusSeconds(30).isAfter(accessToken.getExpirationDate().getTime()); 47 | } 48 | 49 | private AccessToken resolveAccessToken() { 50 | HubClient hubClient = HubClient.builder().baseUrl(hubUrl).build(); 51 | OAuth2Client oauthClient = hubClient.getOAuthClient(); 52 | 53 | OAuth2ClientFlow.Builder clientFlowBuilder = oauthClient.clientFlow(); 54 | 55 | clientFlowBuilder.clientId(clientServiceId); 56 | 57 | clientFlowBuilder.clientSecret(clientServiceSecret); 58 | 59 | // An id of service that will be accessed, e.g. YouTrack, TeamCity, UpSource, etc. 60 | clientFlowBuilder.addScope(resourceServerServiceId); 61 | 62 | OAuth2ClientFlow clientFlow = clientFlowBuilder.build(); 63 | 64 | this.accessToken = clientFlow.getToken(); 65 | return this.accessToken; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/com/ontometrics/integrations/configuration/SimpleMockIssueTracker.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | import com.ontometrics.integrations.events.Issue; 4 | import ontometrics.test.util.TestUtil; 5 | 6 | import java.net.URL; 7 | import java.util.Date; 8 | 9 | /** 10 | * Mock issue tracker 11 | * 12 | * MockIssueTracker.java 13 | */ 14 | public class SimpleMockIssueTracker implements IssueTracker { 15 | private String filePathToFeed; 16 | private String filePathToChanges; 17 | private String filePathToAttachment; 18 | 19 | public SimpleMockIssueTracker(Builder builder) { 20 | filePathToFeed = builder.filePathToFeed; 21 | filePathToChanges = builder.filePathToChanges; 22 | filePathToAttachment = builder.filePathToAttachment; 23 | } 24 | 25 | public static class Builder { 26 | 27 | private String filePathToFeed; 28 | private String filePathToChanges; 29 | private String filePathToAttachment; 30 | 31 | public Builder feed(String filePathToFeed){ 32 | this.filePathToFeed = filePathToFeed; 33 | return this; 34 | } 35 | 36 | public Builder changes(String filePathToChanges){ 37 | this.filePathToChanges = filePathToChanges; 38 | return this; 39 | } 40 | 41 | public Builder attachments(String filePathToAttachment){ 42 | this.filePathToAttachment = filePathToAttachment; 43 | return this; 44 | } 45 | 46 | public SimpleMockIssueTracker build(){ 47 | return new SimpleMockIssueTracker(this); 48 | } 49 | } 50 | 51 | @Override 52 | public URL getBaseUrl() { 53 | return null; 54 | } 55 | 56 | @Override 57 | public URL getExternalBaseUrl() { 58 | return null; 59 | } 60 | 61 | @Override 62 | public URL getFeedUrl(String project, Date sinceDate) { 63 | return null; 64 | } 65 | 66 | @Override 67 | public URL getChangesUrl(Issue issue) { 68 | return TestUtil.getFileAsURL(filePathToChanges); 69 | } 70 | 71 | @Override 72 | public URL getAttachmentsUrl(Issue issue) { 73 | return TestUtil.getFileAsURL(filePathToAttachment); 74 | } 75 | 76 | @Override 77 | public String getIssueRestUrl(Issue issue) { 78 | return null; 79 | } 80 | 81 | @Override 82 | public URL getIssueUrl(String issueIdentifier) { 83 | return null; 84 | } 85 | 86 | @Override 87 | public URL getExternalIssueUrl(String issueIdentifier) { 88 | return null; 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/jobs/ProjectProvider.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.jobs; 2 | 3 | import com.google.common.base.Function; 4 | import com.google.common.collect.Lists; 5 | import com.google.common.collect.Sets; 6 | import com.ontometrics.integrations.configuration.IssueTracker; 7 | import com.ontometrics.integrations.model.ProjectList; 8 | import com.ontometrics.integrations.sources.InputStreamHandler; 9 | import com.ontometrics.integrations.sources.StreamProvider; 10 | import com.ontometrics.util.HttpUtil; 11 | import com.ontometrics.util.Mapper; 12 | 13 | import java.io.InputStream; 14 | import java.net.URL; 15 | import java.util.List; 16 | import java.util.Set; 17 | 18 | /** 19 | * Provider of the list of projects (cached) 20 | */ 21 | public class ProjectProvider { 22 | 23 | /** 24 | * 1 hour 25 | */ 26 | private static final long PROJECT_REFRESH_INTERVAL = 3600*1000; 27 | 28 | private Set projects; 29 | private IssueTracker issueTracker; 30 | private StreamProvider streamProvider; 31 | 32 | private Long nextUpdateTime; 33 | 34 | public ProjectProvider(IssueTracker issueTracker, StreamProvider streamProvider) { 35 | this.issueTracker = issueTracker; 36 | this.streamProvider = streamProvider; 37 | } 38 | 39 | /** 40 | * @return set of all project IDs 41 | */ 42 | public Set all() throws Exception { 43 | if (projects == null || (nextUpdateTime != null && System.currentTimeMillis() > nextUpdateTime)) { 44 | projects = loadProjectList(); 45 | nextUpdateTime = System.currentTimeMillis() + PROJECT_REFRESH_INTERVAL; 46 | } 47 | return projects; 48 | } 49 | 50 | public Set loadProjectList() throws Exception { 51 | final URL url = new URL(String.format("%s/rest/project/all", issueTracker.getBaseUrl())); 52 | return Sets.newHashSet(streamProvider.openResourceStream(url, new InputStreamHandler>() { 53 | @Override 54 | public List handleStream(InputStream is, int responseCode) throws Exception { 55 | HttpUtil.checkResponseCode(responseCode, url); 56 | ProjectList projectList = Mapper.createXmlMapper().readValue(is, ProjectList.class); 57 | return Lists.transform(projectList.getProjects(), new Function() { 58 | @Override 59 | public String apply(ProjectList.Project project) { 60 | return project.getShortName(); 61 | } 62 | }); 63 | } 64 | })); 65 | } 66 | 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/events/ProcessEvent.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.events; 2 | 3 | import java.text.DateFormat; 4 | import java.text.SimpleDateFormat; 5 | import java.util.Date; 6 | import java.util.Locale; 7 | import java.util.TimeZone; 8 | 9 | /** 10 | *

11 | * Represents the record found in the feed that indicates that an 12 | * Issue was touched. 13 | *

14 | *

15 | * Instances of this class are created when the feed is parsed. 16 | *

17 | *

18 | * Because we have to gather more information about the changes that 19 | * were made, this class is not used to communicate the substance of 20 | * what occurred through the chat server. 21 | *

22 | * 23 | * @see com.ontometrics.integrations.events.IssueEditSession 24 | * 25 | * User: Rob 26 | * Date: 7/15/14 27 | * Time: 8:42 PM 28 | *

29 | * (c) ontometrics 2014, All Rights Reserved 30 | */ 31 | public class ProcessEvent { 32 | 33 | private static final String KEY_FIELD_SEPARATOR = "::"; 34 | 35 | private final Issue issue; 36 | private Date publishDate; 37 | 38 | public ProcessEvent(Builder builder) { 39 | issue = builder.issue; 40 | publishDate = builder.publishDate; 41 | } 42 | 43 | public static class Builder { 44 | 45 | private Date publishDate; 46 | private Issue issue; 47 | 48 | public Builder issue(Issue issue){ 49 | this.issue = issue; 50 | return this; 51 | } 52 | 53 | public Builder published(Date publishDate){ 54 | this.publishDate = publishDate; 55 | return this; 56 | } 57 | 58 | public ProcessEvent build(){ 59 | return new ProcessEvent(this); 60 | } 61 | } 62 | 63 | public Issue getIssue() { 64 | return issue; 65 | } 66 | 67 | public Date getPublishDate() { 68 | return publishDate; 69 | } 70 | 71 | public void setPublishDate(Date publishDate) { 72 | this.publishDate = publishDate; 73 | } 74 | 75 | /** 76 | * @return Unique key of the event: combination of issueID and publish Date 77 | */ 78 | public String getKey() { 79 | return (issue==null) ? "" : getIssue().getId() + KEY_FIELD_SEPARATOR + createDateFormat().format(publishDate); 80 | } 81 | 82 | private DateFormat createDateFormat() { 83 | SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH); 84 | dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); 85 | return dateFormat; 86 | } 87 | 88 | @Override 89 | public String toString() { 90 | return "ProcessEvent{" + 91 | " issue=" + getIssue() + 92 | ", publishDate=" + getPublishDate() + 93 | '}'; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/sources/AuthenticatedHttpStreamProvider.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.sources; 2 | 3 | import org.apache.http.HttpHost; 4 | import org.apache.http.client.fluent.Executor; 5 | import org.apache.http.client.fluent.Request; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.io.IOException; 10 | import java.net.URL; 11 | 12 | /** 13 | * External http resource stream provider. Http call performed by {@link org.apache.http.client.fluent.Executor} and may be 14 | * authenticated with {@link com.ontometrics.integrations.sources.Authenticator} 15 | *

16 | * ExternalStreamProvider.java 17 | */ 18 | public class AuthenticatedHttpStreamProvider implements StreamProvider { 19 | 20 | private static final Logger logger = LoggerFactory.getLogger(AuthenticatedHttpStreamProvider.class); 21 | 22 | private Executor httpExecutor; 23 | private Authenticator authenticator; 24 | /** 25 | * @param authenticator instance which will configure this instance to make authenticated requests 26 | */ 27 | public AuthenticatedHttpStreamProvider(Authenticator authenticator) { 28 | this.httpExecutor = Executor.newInstance(); 29 | this.authenticator = authenticator; 30 | } 31 | 32 | public static AuthenticatedHttpStreamProvider basicAuthenticatedHttpStreamProvider 33 | (final String login, final String password) { 34 | return new AuthenticatedHttpStreamProvider( new Authenticator() { 35 | @Override 36 | public Request authenticate(URL resourceUrl, Executor httpExecutor, Request request) { 37 | httpExecutor.auth(login,password); 38 | httpExecutor.authPreemptive(new HttpHost(resourceUrl.getHost(), resourceUrl.getPort(), resourceUrl.getProtocol())); 39 | return request; 40 | } 41 | } 42 | ); 43 | } 44 | 45 | public static AuthenticatedHttpStreamProvider hubAuthenticatedHttpStreamProvider 46 | (String clientServiceId, String clientServiceSecret, String resourceServerServiceId, String hubUrl) { 47 | return new AuthenticatedHttpStreamProvider( 48 | new HubAuthenticator(clientServiceId, clientServiceSecret, resourceServerServiceId, hubUrl)); 49 | } 50 | 51 | /** 52 | * @throws IOException 53 | */ 54 | @Override 55 | public RES openResourceStream(URL resourceUrl, final InputStreamHandler inputStreamHandler) throws Exception { 56 | Request request = Request.Get(resourceUrl.toExternalForm()); 57 | request = this.authenticator.authenticate(resourceUrl, httpExecutor, request); 58 | return NonAuthenticatedHttpStreamProvider.openResourceStream(request, inputStreamHandler, httpExecutor); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/events/Comment.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.events; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * Created by rob on 8/28/14. 7 | * Copyright (c) ontometrics, 2014 All Rights Reserved 8 | */ 9 | public class Comment { 10 | private String id; 11 | private final String author; 12 | private final Date created; 13 | private final String text; 14 | private boolean deleted; 15 | 16 | 17 | public Comment(Builder builder) { 18 | id = builder.id; 19 | author = builder.author; 20 | created = builder.created; 21 | text = builder.text; 22 | deleted = builder.deleted; 23 | } 24 | 25 | public static class Builder { 26 | private String id; 27 | private Date created; 28 | private String author; 29 | private String text; 30 | private boolean deleted; 31 | 32 | 33 | public Builder id(String id){ 34 | this.id = id; 35 | return this; 36 | } 37 | 38 | public Builder author(String author){ 39 | this.author = author; 40 | return this; 41 | } 42 | 43 | public Builder text(String text){ 44 | this.text = text; 45 | return this; 46 | } 47 | 48 | public Builder created(Date created){ 49 | this.created = created; 50 | return this; 51 | } 52 | 53 | public Builder deleted(boolean deleted) { 54 | this.deleted = deleted; 55 | return this; 56 | } 57 | 58 | public Comment build(){ 59 | return new Comment(this); 60 | } 61 | } 62 | 63 | public String getAuthor() { 64 | return author; 65 | } 66 | 67 | public Date getCreated() { 68 | return created; 69 | } 70 | 71 | public String getText() { 72 | return text; 73 | } 74 | 75 | public boolean isDeleted() { 76 | return deleted; 77 | } 78 | 79 | public void setDeleted(boolean deleted) { 80 | this.deleted = deleted; 81 | } 82 | 83 | public String getId() { 84 | return id; 85 | } 86 | 87 | @Override 88 | public boolean equals(Object o) { 89 | if (this == o) return true; 90 | if (o == null || getClass() != o.getClass()) return false; 91 | 92 | Comment comment = (Comment) o; 93 | 94 | return id.equals(comment.id); 95 | 96 | } 97 | 98 | @Override 99 | public int hashCode() { 100 | return id.hashCode(); 101 | } 102 | 103 | @Override 104 | public String toString() { 105 | return String.format("%s on %s: %s%s", author, created, text, System.lineSeparator()); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/sources/NonAuthenticatedHttpStreamProvider.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.sources; 2 | 3 | import org.apache.commons.lang.StringUtils; 4 | import org.apache.http.HttpHost; 5 | import org.apache.http.HttpResponse; 6 | import org.apache.http.StatusLine; 7 | import org.apache.http.client.ResponseHandler; 8 | import org.apache.http.client.fluent.Executor; 9 | import org.apache.http.client.fluent.Request; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import java.io.IOException; 14 | import java.net.URL; 15 | 16 | /** 17 | * ExternalStreamProvider.java 18 | */ 19 | public class NonAuthenticatedHttpStreamProvider implements StreamProvider { 20 | 21 | private static final Logger logger = LoggerFactory.getLogger(NonAuthenticatedHttpStreamProvider.class); 22 | 23 | private Executor httpExecutor; 24 | public NonAuthenticatedHttpStreamProvider() { 25 | this.httpExecutor = Executor.newInstance(); 26 | } 27 | 28 | /** 29 | * @throws IOException 30 | */ 31 | @Override 32 | public RES openResourceStream(URL resourceUrl, final InputStreamHandler inputStreamHandler) throws Exception { 33 | Request request = Request.Get(resourceUrl.toExternalForm()); 34 | return openResourceStream(request, inputStreamHandler, httpExecutor); 35 | } 36 | 37 | public static RES openResourceStream(Request request, final InputStreamHandler inputStreamHandler, Executor httpExecutor) throws Exception { 38 | return httpExecutor.execute(request) 39 | .handleResponse( 40 | new ResponseHandler() { 41 | @Override 42 | public RES handleResponse(HttpResponse httpResponse) throws IOException { 43 | try { 44 | StatusLine statusLine = httpResponse.getStatusLine(); 45 | if (StringUtils.isNotBlank(statusLine.getReasonPhrase())) { 46 | logger.debug("Got response with code {} reason: {}", statusLine.getStatusCode(), statusLine.getReasonPhrase()); 47 | } else { 48 | logger.debug("Got response with code {}", statusLine.getStatusCode()); 49 | } 50 | return inputStreamHandler.handleStream(httpResponse.getEntity().getContent(), statusLine.getStatusCode()); 51 | } catch (IOException e) { 52 | throw e; 53 | } catch (Exception e) { 54 | throw new RuntimeException(e); 55 | } 56 | } 57 | } 58 | ); 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/jobs/WebContextJobStarter.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.jobs; 2 | 3 | import com.ontometrics.db.MapDb; 4 | import com.ontometrics.integrations.configuration.ConfigurationFactory; 5 | import com.ontometrics.integrations.configuration.EventProcessorConfiguration; 6 | import com.ontometrics.integrations.configuration.YouTrackInstance; 7 | import com.ontometrics.integrations.configuration.YouTrackInstanceFactory; 8 | import org.apache.commons.configuration.Configuration; 9 | import org.mapdb.DB; 10 | import org.mapdb.DBMaker; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import javax.servlet.ServletContextEvent; 15 | import javax.servlet.ServletContextListener; 16 | import java.util.Date; 17 | 18 | /** 19 | * Create and schedule tasks on web-application startup with call to {@link JobStarter#scheduleTasks()} 20 | * WebContextJobStarter.java 21 | */ 22 | public class WebContextJobStarter implements ServletContextListener { 23 | private static Logger logger = LoggerFactory.getLogger(WebContextJobStarter.class); 24 | 25 | private JobStarter jobStarter; 26 | 27 | @Override 28 | public void contextInitialized(ServletContextEvent servletContextEvent) { 29 | logger.info("Starting up, checking that configuration is correct"); 30 | try { 31 | checkConfiguration(); 32 | this.jobStarter = new JobStarter(); 33 | jobStarter.scheduleTasks(); 34 | } catch (Exception ex) { 35 | logger.error("Failed to initialize application", ex); 36 | throw ex; 37 | } 38 | } 39 | 40 | private void checkConfiguration() throws InvalidConfigurationException { 41 | Configuration configuration = ConfigurationFactory.get(); 42 | try { 43 | EventProcessorConfiguration.instance(); 44 | } catch (Exception ex) { 45 | throw new InvalidConfigurationException("Could not initialize event processor configuration", ex); 46 | } 47 | YouTrackInstance youTrackInstance = null; 48 | try { 49 | youTrackInstance = YouTrackInstanceFactory.createYouTrackInstance(configuration); 50 | } catch (Exception ex) { 51 | throw new InvalidConfigurationException("Invalid YouTrack configuration, please check that URL is correct", ex); 52 | } 53 | try { 54 | youTrackInstance.getBaseUrl(); 55 | } catch (Exception ex) { 56 | throw new InvalidConfigurationException("Invalid YouTrack base url", ex); 57 | } 58 | try { 59 | youTrackInstance.getFeedUrl("TEST", new Date()); 60 | } catch (Exception ex) { 61 | throw new InvalidConfigurationException("Invalid YouTrack feed url", ex); 62 | } 63 | } 64 | 65 | 66 | @Override 67 | public void contextDestroyed(ServletContextEvent servletContextEvent) { 68 | logger.info("Shutting down"); 69 | jobStarter.dispose(); 70 | MapDb.instance().close(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/com/ontometrics/integrations/sources/EditSessionsExtractorIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.sources; 2 | 3 | import com.ontometrics.integrations.configuration.ConfigurationFactory; 4 | import com.ontometrics.integrations.configuration.YouTrackInstance; 5 | import com.ontometrics.integrations.events.IssueEditSession; 6 | import com.ontometrics.integrations.events.ProcessEvent; 7 | import org.apache.commons.configuration.Configuration; 8 | import org.junit.Before; 9 | import org.junit.Ignore; 10 | import org.junit.Test; 11 | import org.slf4j.Logger; 12 | 13 | import java.util.Date; 14 | import java.util.List; 15 | 16 | import static org.hamcrest.CoreMatchers.not; 17 | import static org.hamcrest.MatcherAssert.assertThat; 18 | import static org.hamcrest.Matchers.empty; 19 | import static org.slf4j.LoggerFactory.getLogger; 20 | 21 | /** 22 | * This test check that code can access real server. it is disbaled by default, if you want to run it, please make sure that 23 | * application.propertied has correct values for following properties 24 | *

    25 | *
  • PROP.YOUTRACK_USERNAME
  • 26 | *
  • PROP.YOUTRACK_PASSWORD
  • 27 | *
  • PROP.YOUTRACK_URL
  • 28 | *
29 | */ 30 | @Ignore 31 | public class EditSessionsExtractorIntegrationTest { 32 | private static final Logger log = getLogger(EditSessionsExtractorIntegrationTest.class); 33 | 34 | 35 | private YouTrackInstance youTrackInstance; 36 | private StreamProvider streamProvider; 37 | 38 | @Before 39 | public void setup(){ 40 | Configuration configuration = ConfigurationFactory.get(); 41 | 42 | streamProvider = AuthenticatedHttpStreamProvider.basicAuthenticatedHttpStreamProvider( 43 | configuration.getString("PROP.YOUTRACK_USERNAME"), 44 | configuration.getString("PROP.YOUTRACK_PASSWORD") 45 | ); 46 | 47 | youTrackInstance = new YouTrackInstance.Builder().baseUrl( 48 | configuration.getString("PROP.YOUTRACK_URL")).build(); 49 | } 50 | 51 | /* 52 | @Test 53 | public void testThatWeCanGetEventsFromRealFeed() throws Exception { 54 | EditSessionsExtractor sourceEventMapper = new EditSessionsExtractor(youTrackInstance, streamProvider); 55 | List changes = sourceEventMapper.getLatestEvents(); 56 | assertThat(changes, not(empty())); 57 | } 58 | 59 | @Test 60 | public void testThatWeCanGetEditSessionsFromRealFeed() throws Exception { 61 | EditSessionsExtractor sourceEventMapper = new EditSessionsExtractor(youTrackInstance, streamProvider); 62 | 63 | //NOTE: min date has been set to "1 day before" to minimize the number of requests 64 | //adjust it, if yours server has no data within that period 65 | Date minDate = new Date(System.currentTimeMillis() - 24*3600*1000); 66 | 67 | List edits = sourceEventMapper.getLatestEdits(minDate); 68 | assertThat(edits, not(empty())); 69 | log.info("found {} edits: {}", edits.size(), edits); 70 | } 71 | 72 | */ 73 | } -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/events/ProcessEventChange.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.events; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * Created by rob on 7/23/14. 7 | * Copyright (c) ontometrics, 2014 All Rights Reserved 8 | */ 9 | public class ProcessEventChange { 10 | 11 | private Issue issue; 12 | private final String field; 13 | private final String priorValue; 14 | private final String currentValue; 15 | private final String updater; 16 | private final Date updated; 17 | 18 | public ProcessEventChange(Builder builder) { 19 | issue = builder.issue; 20 | updater = builder.updater; 21 | updated = builder.updated; 22 | field = builder.field; 23 | priorValue = builder.priorValue; 24 | currentValue = builder.currentValue; 25 | } 26 | 27 | public static class Builder { 28 | 29 | private String field; 30 | private String priorValue; 31 | private String currentValue; 32 | private String updater; 33 | private Date updated; 34 | private Issue issue; 35 | 36 | public Builder issue(Issue issue){ 37 | this.issue = issue; 38 | return this; 39 | } 40 | 41 | public Builder updater(String updater){ 42 | this.updater = updater; 43 | return this; 44 | } 45 | 46 | public Builder updated(Date updated){ 47 | this.updated = updated; 48 | return this; 49 | } 50 | 51 | public Builder field(String field){ 52 | this.field = field; 53 | return this; 54 | } 55 | 56 | public Builder priorValue(String priorValue){ 57 | this.priorValue = priorValue; 58 | return this; 59 | } 60 | 61 | public Builder currentValue(String currentVale){ 62 | this.currentValue = currentVale; 63 | return this; 64 | } 65 | 66 | public ProcessEventChange build(){ 67 | return new ProcessEventChange(this); 68 | } 69 | } 70 | 71 | public Issue getIssue() { 72 | return issue; 73 | } 74 | 75 | public String getField() { 76 | return field; 77 | } 78 | 79 | public String getPriorValue() { 80 | return priorValue; 81 | } 82 | 83 | public String getCurrentValue() { 84 | return currentValue; 85 | } 86 | 87 | public Date getUpdated() { 88 | return updated; 89 | } 90 | 91 | public String getUpdater() { 92 | return updater; 93 | } 94 | 95 | @Override 96 | public String toString() { 97 | StringBuilder stringBuilder = new StringBuilder(String.format("%s changed %s ", updater, field)); 98 | if (priorValue.length() > 0) { 99 | stringBuilder.append("from ").append(priorValue).append(" "); 100 | } 101 | stringBuilder.append("to ").append(currentValue); 102 | return stringBuilder.toString(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/resources/feeds/empty-issue-changes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DMAN 6 | 7 | 8 | 584 9 | 10 | 11 | Display major events (Equinox / Solstice) 12 | 13 | 14 | 1404394803932 15 | 16 | 17 | 1409073351938 18 | 19 | 20 | nikolay.chorniy@gmail.com 21 | 22 | 23 | Nikolay Chorniy 24 | 25 | 26 | noura 27 | 28 | 29 | Noura Hassan 30 | 31 | 32 | 0 33 | 34 | 35 | 0 36 | 37 | 38 | DMAN-573 39 | 40 | 41 | Normal 42 | 43 | 44 | Task 45 | 46 | 47 | Reopened 48 | 49 | 50 | No subsystem 51 | 52 | 53 | 1.0.23 54 | 55 | Star 56 | 57 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue-changes-no-changes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | HA 4 | 5 | 6 | 689 7 | 8 | 9 | Fix scraping of title in Monster source 10 | 11 | 12 | 13 | Monster source scraper adds company name and address to the job posting title. Avoid it. 14 | 15 | 16 | 17 | 1409264541170 18 | 19 | 20 | 1409264541170 21 | 22 | 23 | nikolay.chorniy@gmail.com 24 | 25 | 26 | Nikolay Chorniy 27 | 28 | 29 | nikolay.chorniy@gmail.com 30 | 31 | 32 | Nikolay Chorniy 33 | 34 | 35 | 0 36 | 37 | 38 | 0 39 | 40 | 41 | HA-663 42 | 43 | 44 | Normal 45 | 46 | 47 | Bug 48 | 49 | 50 | In Progress 51 | 52 | 53 | nikolay.chorniy@gmail.com 54 | 55 | 56 | No subsystem 57 | 58 | 59 | 2.2 60 | 61 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue-change-new-item-with-comment-no-changes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | AIA 4 | 5 | 6 | 19 7 | 8 | 9 | Need to make a monitor Agent 10 | 11 | 12 | 13 | So on JobSpider, we are constantly deploying builds only to have the Customer tell us later that no data is coming through. We need to write a simple monitor that counts the # of items found, imported, geocoded, etc., and then posts a message into the channel from time-to-time. This agent should also have the ability to detect idle periods that are too long, so for example post a message if we have not seen any new items in 5 hours. 14 | 15 | 16 | 17 | 1408804483646 18 | 19 | 20 | 1409328580340 21 | 22 | 23 | rob 24 | 25 | 26 | Rob Williams 27 | 28 | 29 | rob 30 | 31 | 32 | Rob Williams 33 | 34 | 35 | 1 36 | 37 | 38 | 0 39 | 40 | 41 | Normal 42 | 43 | 44 | Story 45 | 46 | 47 | Open 48 | 49 | 50 | Services 51 | 52 | 53 | 0.0.2 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue-details-with-link.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | HA 4 | 5 | 6 | 740 7 | 8 | 9 | 10 | Need way for Admins to train the System about Posting Classifications 11 | 12 | 13 | 14 | 15 | We are going to start with the system literally just guessing the Classification. Then we will have an interface that shows the items that have been classified but not verified. Users will have to go through and either click Ok, to agree with the classification, or select the correct classification, which is how the System will train and make better predictions. Eventually, we will make it so that Classifications that obtained a score above a certain threshold will go into the feed immediately (automatically). At first, nothing will be above that threshold so admins will in effect have to approve each item for it to appear in the correct feeds. 16 | 17 | 18 | 19 | 1410471757247 20 | 21 | 22 | 1410472462486 23 | 24 | 25 | rob 26 | 27 | 28 | Rob Williams 29 | 30 | 31 | rob 32 | 33 | 34 | Rob Williams 35 | 36 | 37 | 0 38 | 39 | 40 | 0 41 | 42 | 43 | HA-741 44 | 45 | 46 | Normal 47 | 48 | 49 | Story 50 | 51 | 52 | Open 53 | 54 | 55 | User Interface 56 | 57 | 58 | 2.5.0 59 | 60 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/events/Issue.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.events; 2 | 3 | import java.net.URL; 4 | import java.util.Date; 5 | 6 | /** 7 | * Created by Rob on 8/19/14. 8 | * Copyright (c) ontometrics, 2014 All Rights Reserved 9 | */ 10 | public class Issue { 11 | 12 | private final int id; 13 | private final String prefix; 14 | private final String title; 15 | private final String description; 16 | private final URL link; 17 | private final String creator; 18 | private final Date created; 19 | 20 | public Issue(Builder builder) { 21 | id = builder.id; 22 | prefix = builder.prefix; 23 | creator = builder.creator; 24 | created = builder.created; 25 | title = builder.title; 26 | description = builder.description; 27 | link = builder.link; 28 | } 29 | 30 | public static class Builder { 31 | 32 | private int id; 33 | private String prefix; 34 | private String title; 35 | private String description; 36 | private URL link; 37 | private String creator; 38 | private Date created; 39 | 40 | public Builder id(int id){ 41 | this.id = id; 42 | return this; 43 | } 44 | 45 | public Builder projectPrefix(String prefix){ 46 | this.prefix = prefix; 47 | return this; 48 | } 49 | 50 | public Builder creator(String creator){ 51 | this.creator = creator; 52 | return this; 53 | } 54 | 55 | public Builder created(Date created){ 56 | this.created = created; 57 | return this; 58 | } 59 | 60 | public Builder title(String title){ 61 | this.title = title; 62 | return this; 63 | } 64 | 65 | public Builder description(String description){ 66 | this.description = description; 67 | return this; 68 | } 69 | 70 | public Builder link(URL link){ 71 | this.link = link; 72 | return this; 73 | } 74 | 75 | public Issue build(){ 76 | return new Issue(this); 77 | } 78 | } 79 | 80 | public int getId() { 81 | return id; 82 | } 83 | 84 | public String getPrefix() { 85 | return prefix; 86 | } 87 | 88 | public String getCreator() { 89 | return creator; 90 | } 91 | 92 | public Date getCreated() { 93 | return created; 94 | } 95 | 96 | public String getTitle() { 97 | return title; 98 | } 99 | 100 | public String getDescription() { 101 | return description; 102 | } 103 | 104 | public URL getLink() { 105 | return link; 106 | } 107 | 108 | @Override 109 | public boolean equals(Object o) { 110 | if (this == o) return true; 111 | if (o == null || getClass() != o.getClass()) return false; 112 | 113 | Issue issue = (Issue) o; 114 | 115 | return id == issue.id && !(prefix != null ? !prefix.equals(issue.prefix) : issue.prefix != null); 116 | 117 | } 118 | 119 | @Override 120 | public int hashCode() { 121 | int result = id; 122 | result = 31 * result + (prefix != null ? prefix.hashCode() : 0); 123 | return result; 124 | } 125 | 126 | @Override 127 | public String toString() { 128 | return "Issue{" + 129 | "id=" + id + 130 | ", prefix='" + prefix + '\'' + 131 | ", title='" + title + '\'' + 132 | ", description='" + description + '\'' + 133 | ", link=" + link + 134 | ", creator='" + creator + '\'' + 135 | ", created=" + created + 136 | '}'; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue-changes-comment-attachment-only.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | AIA 4 | 5 | 6 | 38 7 | 8 | 9 | Support for Attachments 10 | 11 | 12 | 13 | Currently, attachments do not appear in changes. So we have to have some logic that just looks for attachments that were added in the time window of interest and simply add them as an edit. 14 | 15 | 16 | 17 | 1409889822124 18 | 19 | 20 | 1409946292661 21 | 22 | 23 | rob 24 | 25 | 26 | Rob Williams 27 | 28 | 29 | 1409889908795 30 | 31 | 32 | rob 33 | 34 | 35 | Rob Williams 36 | 37 | 38 | 1 39 | 40 | 41 | 0 42 | 43 | 44 | AIA-39 45 | 46 | 47 | Normal 48 | 49 | 50 | Task 51 | 52 | 53 | Fixed 54 | 55 | 56 | rob 57 | 58 | 59 | No subsystem 60 | 61 | 62 | 0.0.3 63 | 64 | 65 | Screen Shot 2014-08-27 at 4.17.53 PM.png 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/util/DateBuilder.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.util; 2 | 3 | import com.ontometrics.integrations.events.IssueEdit; 4 | 5 | import java.util.Calendar; 6 | import java.util.Date; 7 | import java.util.TimeZone; 8 | 9 | /** 10 | * Provides a fluent builder interface for constructing Dates and DateTimes. 11 | */ 12 | public class DateBuilder { 13 | 14 | private Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); 15 | 16 | /** 17 | * Provides means of starting from a given date, then changing some subset 18 | * of the fields 19 | * 20 | * @param date 21 | * the starting point date 22 | * @return this, for chaining 23 | */ 24 | public DateBuilder start(Date date) { 25 | calendar.setTime(date); 26 | return this; 27 | } 28 | 29 | /** 30 | * So to build the first of the year, you would set this to 1. 31 | * 32 | * @param dayOfMonth 33 | * a number between 1 and 31 (depending on the calendar month of 34 | * course) 35 | * @return this, for chaining 36 | */ 37 | public DateBuilder day(int dayOfMonth) { 38 | calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); 39 | return this; 40 | } 41 | 42 | /** 43 | * Provide the month of the date you are building, a number between 1 and 44 | * 12. 45 | * 46 | * @param month 47 | * the value desired in the resulting date, e.g. 5 for May 48 | * @return this, for chaining 49 | */ 50 | public DateBuilder month(int month) { 51 | calendar.set(Calendar.MONTH, month); 52 | return this; 53 | } 54 | 55 | /** 56 | * Provide the year of the date you are building, e.g. 1980. 57 | * 58 | * @param year 59 | * the value desired in the resulting date 60 | * @return this, for chaining 61 | */ 62 | public DateBuilder year(int year) { 63 | calendar.set(Calendar.YEAR, year); 64 | return this; 65 | } 66 | 67 | /** 68 | * Provide the hour, in this case of the day. Note the JDK calls this hour 69 | * of the day. 70 | * 71 | * @param hour 72 | * the value desired in the resulting date, e.g. 14 for 2 pm 73 | * @return this, for chaining 74 | */ 75 | public DateBuilder hour(int hour) { 76 | calendar.set(Calendar.HOUR_OF_DAY, hour); 77 | return this; 78 | } 79 | 80 | /** 81 | * Provide the minutes within the hour of the datetime being constructed 82 | * 83 | * @param minutes 84 | * a number between 0 and 59, e.g. if building 12:49, this would 85 | * be 49. 86 | * @return this, for chaining 87 | */ 88 | public DateBuilder minutes(int minutes) { 89 | calendar.set(Calendar.MINUTE, minutes); 90 | return this; 91 | } 92 | 93 | /** 94 | * Provide the seconds within the hour of the datetime being constructed 95 | * 96 | * @param seconds 97 | * a number between 0 and 59 98 | * @return this, for chaining 99 | */ 100 | public DateBuilder seconds(int seconds) { 101 | calendar.set(Calendar.SECOND, seconds); 102 | return this; 103 | } 104 | 105 | /** 106 | * Adds specified amount of minutes to the built date 107 | * @param minutes minutes 108 | * 109 | * @return this, for chaining 110 | */ 111 | public DateBuilder addMinutes(int minutes) { 112 | calendar.add(Calendar.MINUTE, minutes); 113 | return this; 114 | } 115 | 116 | public DateBuilder addDays(int daysToAdd) { 117 | calendar.add(Calendar.DAY_OF_YEAR, daysToAdd); 118 | return this; 119 | } 120 | 121 | /** 122 | * Sets specified timestamp 123 | * @param time timestamp 124 | * 125 | * @return this, for chaining 126 | */ 127 | public DateBuilder time(Date time) { 128 | calendar.setTime(time); 129 | return this; 130 | } 131 | 132 | /** 133 | * Provides access to the final product. 134 | * 135 | * @return the constructed date with all the desired values 136 | */ 137 | public Date build() { 138 | return calendar.getTime(); 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | slack-youtrack 2 | ============== 3 | 4 | Integration of slack and you track. 5 | 6 | Main functionality here is change management: 7 | 8 | * show ticket changes 9 | * linkify references to issues 10 | * map the changes to the appropriate channel 11 | 12 | Of course, to do this we have an agent being woken up regularly that: 13 | 14 | 1. Request list of updated issues for each project 15 | 1. for each item found there, calls into the REST interface to get changes 16 | 1. formats the change info 17 | 1. posts it to the channel 18 | 1. load list of youtrack projects sometimes 19 | 1. Retrieve AccessToken from HUB (for "hub-oauth2" type of authentication) 20 | 21 | Installation and configuration 22 | ------------ 23 | 1. List of required properties 24 | * SLACK_WEBHOOK_PATH - path for Slack webhook excluding first slash. e.g. "services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" (see https://api.slack.com/incoming-webhooks) 25 | * YOUTRACK_URL - YouTrack server url. e.g. "https://mycompany.com/youtrack". This can be inner URL (for example local IP-based) 26 | * YOUTRACK_EXTERNAL_URL - YouTrack server external URL. It should be accessible for Slack Users, so consider it to be public DNS-based URL. This can be the same as YOUTRACK_URL however. 27 | * YOUTRACK_TO_SLACK_CHANNELS - mappings between YouTrack projects and Slack channels. For example "APL->#apple;SUP->#support" (please note that "#" or "@" should be included in the slack-channel name 28 | * DEFAULT_SLACK_CHANNEL - Default slack channel to post in. e.g. "#general". Projects without mappings will be posted here 29 | * AUTH_TYPE - authentication / authorization method. Available values: {"credentials", "hub-oauth2"} 30 | * APP_EXTERNAL_URL - external url of deployed slack-youtrack web application. This url is used to render attachment images 31 | * EXCLUDED_YOUTRACK_FIELDS - semicolon separated fields, changing of which should not be reported, e.g. "Spent Time, Estimate Time" 32 | 33 | For "credentials" authentication type such properties have to be created 34 | 35 | * YOUTRACK_USERNAME - YouTrack username 36 | * YOUTRACK_PASSWORD - YouTrack password 37 | * SLACK_WEBHOOK_PATH - path for Slack webhook excluding first slash. e.g. "services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" 38 | 39 | For "hub-oauth2" authentication type (Hub authentication OAuth 2.0) following properties are required. 40 | See https://www.jetbrains.com/hub/help/1.0/OAuth-2.0-Authorization.html 41 | 42 | * HUB_URL - JetBrains Hub url e.g. "https://mycompany.com/hub" 43 | * HUB_OAUTH_RESOURCE_SERVER_SERVICE_ID - Hub OAuth resource server service id 44 | * HUB_OAUTH_CLIENT_SERVICE_ID - Hub OAuth clientServiceId 45 | * HUB_OAUTH_CLIENT_SERVICE_SECRET - Hub OAuth clientServiceSecret DEFAULT_SLACK_CHANNEL - Default slack channel to post in. e.g. "#general" 46 | 47 | Generic attributes 48 | * ISSUE_HISTORY_WINDOW - Time in minutes - how deep should we look for issues in the past. If set to 10, it means that issues and changes that happened not longer than 10 minutes will be posted to chat server 49 | * APP_DATA_DIR - directory where app will store it's data-files (configuration). e.g. "/opt/slack-youtrack" 50 | * SLACKBOT_ICON - URL of icon used for the posts in the YouTrack channel 51 | 52 | 2. Create maven profile with described properties or directly define them like below 53 | 54 | Run "mvn -DYOUTRACK_USERNAME=usr -DYOUTRACK_PASSWORD=pwd -DSLACK_WEBHOOK_PATH=services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX -DAPP_DATA_DIR=/opt/slack-youtrack -DYOUTRACK_URL=http://company.myjetbrains.com/youtrack -DISSUE_HISTORY_WINDOW=10 -DDEFAULT_SLACK_CHANNEL=#general package" to build war file 55 | 56 | 3. Drop war file into servlet container "webapps" directory 57 | 58 | That's it. 59 | 60 | Troubleshooting 61 | ------------ 62 | 63 | If you experience any problems, e.g. YouTrack updates are not posted to Slack channel, rebuild the project setting http client log level to DEBUG (so that all requests and responses are logged), redeploy and feel free to file an issue with information from the log. To set the log level to DEBUG, edit [src/main/resources/logback.xml](https://github.com/ontometrics/slack-youtrack/blob/master/src/main/resources/logback.xml) and uncomment lines 64 | 65 | ```xml 66 | 67 | 68 | ``` 69 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/configuration/YouTrackInstance.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | import com.ontometrics.integrations.events.Issue; 4 | import org.slf4j.Logger; 5 | 6 | import java.net.MalformedURLException; 7 | import java.net.URL; 8 | import java.util.Date; 9 | 10 | import static org.slf4j.LoggerFactory.getLogger; 11 | 12 | /** 13 | * Created by rob on 8/19/14. 14 | * Copyright (c) ontometrics, 2014 All Rights Reserved 15 | */ 16 | public class YouTrackInstance implements IssueTracker { 17 | 18 | private Logger log = getLogger(YouTrackInstance.class); 19 | private final String baseUrl; 20 | private final String externalBaseUrl; 21 | private final String issueBase; 22 | 23 | public YouTrackInstance(Builder builder) { 24 | baseUrl = builder.baseUrl; 25 | externalBaseUrl = builder.externalBaseUrl; 26 | issueBase = getBaseUrl() + "/rest/issue/%s"; 27 | } 28 | 29 | 30 | @Override 31 | public URL getIssueUrl(String issueIdentifier) { 32 | URL url; 33 | try { 34 | url = new URL(String.format("%s/issue/%s", getBaseUrl(), issueIdentifier)); 35 | } catch (MalformedURLException e) { 36 | throw new RuntimeException(e); 37 | } 38 | return url; 39 | } 40 | 41 | @Override 42 | public URL getExternalIssueUrl(String issueIdentifier) { 43 | URL url; 44 | try { 45 | url = new URL(String.format("%s/issue/%s", getExternalBaseUrl(), issueIdentifier)); 46 | } catch (MalformedURLException e) { 47 | throw new RuntimeException(e); 48 | } 49 | return url; 50 | } 51 | 52 | public String getIssueRestUrl(Issue issue) { 53 | return String.format(issueBase, issue.getPrefix() + "-" + issue.getId()); 54 | } 55 | 56 | public static class Builder { 57 | 58 | private String baseUrl; 59 | private String externalBaseUrl; 60 | 61 | public Builder baseUrl(String baseUrl) { 62 | this.baseUrl = baseUrl; 63 | return this; 64 | } 65 | 66 | public Builder externalBaseUrl(String externalBaseUrl) { 67 | this.externalBaseUrl = externalBaseUrl; 68 | return this; 69 | } 70 | 71 | public YouTrackInstance build(){ 72 | return new YouTrackInstance(this); 73 | } 74 | } 75 | 76 | @Override 77 | public URL getBaseUrl() { 78 | URL url; 79 | try { 80 | url = new URL(baseUrl); 81 | } catch (MalformedURLException e) { 82 | throw new RuntimeException(e); 83 | } 84 | return url; 85 | } 86 | 87 | @Override 88 | public URL getExternalBaseUrl() { 89 | URL url; 90 | try { 91 | url = new URL(externalBaseUrl); 92 | } catch (MalformedURLException e) { 93 | throw new RuntimeException(e); 94 | } 95 | return url; 96 | } 97 | 98 | @Override 99 | public URL getFeedUrl(String project, Date sinceDate) { 100 | URL url; 101 | try { 102 | url = new URL(String.format("%s/rest/issue/byproject/%s?updatedAfter=%s", 103 | getBaseUrl(), project, Long.toString(sinceDate.getTime()))); 104 | } catch (MalformedURLException e) { 105 | throw new RuntimeException(e); 106 | } 107 | return url; 108 | } 109 | 110 | @Override 111 | public URL getChangesUrl(Issue issue){ 112 | URL url; 113 | try { 114 | url = new URL(String.format("%s/rest/issue/%s/changes", getBaseUrl(), issue.getPrefix() + "-" + issue.getId())); 115 | } catch (MalformedURLException e) { 116 | throw new RuntimeException(e); 117 | } 118 | return url; 119 | } 120 | 121 | @Override 122 | public URL getAttachmentsUrl(Issue issue) { 123 | return buildIssueURL(issue, "%s/attachment"); 124 | } 125 | 126 | 127 | 128 | private URL buildIssueURL(Issue issue, String urlTemplate) { 129 | URL url = null; 130 | try { 131 | String base = getIssueRestUrl(issue); 132 | url = new URL(urlTemplate.replace("%s", base)); 133 | } catch (MalformedURLException e) { 134 | log.error("Error building issue URL", e); 135 | } 136 | return url; 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/jobs/JobStarter.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.jobs; 2 | 3 | import com.ontometrics.integrations.configuration.ConfigurationAccessError; 4 | import com.ontometrics.integrations.configuration.ConfigurationFactory; 5 | import com.ontometrics.integrations.configuration.SlackInstance; 6 | import com.ontometrics.integrations.configuration.StreamProviderFactory; 7 | import com.ontometrics.integrations.sources.ChannelMapper; 8 | import com.ontometrics.integrations.sources.ChannelMapperFactory; 9 | import com.ontometrics.integrations.sources.StreamProvider; 10 | import org.apache.commons.configuration.Configuration; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.net.MalformedURLException; 15 | import java.net.URL; 16 | import java.util.concurrent.Executors; 17 | import java.util.concurrent.ScheduledExecutorService; 18 | import java.util.concurrent.ScheduledFuture; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | /** 22 | * Create and schedule timer which will execute list of {@link EventListener}s 23 | * JobStarter.java 24 | */ 25 | public class JobStarter { 26 | private static Logger logger = LoggerFactory.getLogger(JobStarter.class); 27 | 28 | //TODO move to configuration params 29 | private static final long EXECUTION_DELAY = 2 * 1000; 30 | private static final long REPEAT_INTERVAL = 90 * 1000; 31 | private ScheduledExecutorService scheduledExecutorService; 32 | private ScheduledFuture scheduledTask; 33 | 34 | public JobStarter() { 35 | initialize(); 36 | } 37 | 38 | /** 39 | * Schedules periodic tasks to fetch the events 40 | */ 41 | public void scheduleTasks() { 42 | final Configuration configuration = ConfigurationFactory.get(); 43 | StreamProvider streamProvider = StreamProviderFactory.createStreamProvider(configuration); 44 | 45 | ChannelMapper channelMapper = ChannelMapperFactory.fromConfiguration(configuration, "youtrack-slack."); 46 | 47 | SlackInstance chatServer = new SlackInstance.Builder().channelMapper(channelMapper) 48 | .icon(resolveSlackBotIcon(configuration)).build(); 49 | scheduleTask(new EventListenerImpl(streamProvider, chatServer)); 50 | } 51 | 52 | private String resolveSlackBotIcon(Configuration configuration) { 53 | String slackBotIcon = configuration.getString("youtrack-slack.icon"); 54 | try { 55 | new URL(slackBotIcon); 56 | } catch (MalformedURLException e) { 57 | slackBotIcon = SlackInstance.DEFAULT_ICON_URL; 58 | } 59 | return slackBotIcon; 60 | } 61 | 62 | 63 | private void initialize() { 64 | } 65 | 66 | /** 67 | * Schedules a periodic task {@link com.ontometrics.integrations.jobs.EventListener#checkForNewEvents()} 68 | * @param eventListener event listener 69 | */ 70 | private void scheduleTask(EventListener eventListener) { 71 | logger.info("Scheduling EventListener task"); 72 | EventTask eventTask = new EventTask(eventListener); 73 | scheduledExecutorService = Executors.newScheduledThreadPool(1); 74 | 75 | scheduledTask = scheduledExecutorService 76 | .scheduleWithFixedDelay(eventTask, EXECUTION_DELAY, REPEAT_INTERVAL, 77 | TimeUnit.MILLISECONDS); 78 | } 79 | 80 | private static class EventTask implements Runnable { 81 | private EventListener eventListener; 82 | 83 | private EventTask(EventListener eventListener) { 84 | this.eventListener = eventListener; 85 | } 86 | 87 | @Override 88 | public void run() { 89 | logger.info("Event processing started"); 90 | try { 91 | this.eventListener.checkForNewEvents(); 92 | logger.info("Event processing finished"); 93 | } catch (ConfigurationAccessError error) { 94 | //this is critical error 95 | throw error; 96 | } catch (Throwable ex) { 97 | logger.error("Failed to process", ex); 98 | } 99 | } 100 | } 101 | 102 | public void dispose () { 103 | //cancelling all previously launched tasks and timer 104 | if (scheduledTask != null) { 105 | scheduledTask.cancel(false); 106 | } 107 | 108 | if (scheduledExecutorService != null) { 109 | scheduledExecutorService.shutdown(); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/configuration/EventProcessorConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | import com.ontometrics.util.DateBuilder; 4 | import org.apache.commons.configuration.ConfigurationException; 5 | import org.apache.commons.configuration.PropertiesConfiguration; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.io.File; 10 | import java.util.Date; 11 | 12 | /** 13 | * EventProcessorConfiguration.java 14 | * Organize access (read/write) to properties/state required for processing of input/output streams 15 | * 16 | */ 17 | public class EventProcessorConfiguration { 18 | private static final Logger logger = LoggerFactory.getLogger(EventProcessorConfiguration.class); 19 | 20 | private static final EventProcessorConfiguration instance = new EventProcessorConfiguration(); 21 | 22 | private static final String LAST_EVENT_DATE = "last.event.date"; 23 | 24 | public static final String PROP_ISSUE_HISTORY_WINDOW = "PROP.ISSUE_HISTORY_WINDOW"; 25 | 26 | private PropertiesConfiguration lastEventConfiguration; 27 | 28 | //being used in tests to override value from properties 29 | private Integer issueHistoryWindowInMinutes; 30 | 31 | private EventProcessorConfiguration() { 32 | initialize(); 33 | } 34 | 35 | private void initialize() throws ConfigurationAccessError { 36 | try { 37 | File dataDir = new File(ConfigurationFactory.get().getString("PROP.APP_DATA_DIR", ".")); 38 | File file = new File(dataDir, "lastEvent.properties"); 39 | logger.info("Going to load properties from file {}", file.getAbsolutePath()); 40 | lastEventConfiguration = new PropertiesConfiguration(file); 41 | logger.info("Initialized EventProcessorConfiguration"); 42 | } catch (ConfigurationException e) { 43 | throw new ConfigurationAccessError("Failed to access properties", e); 44 | } 45 | } 46 | 47 | 48 | public static EventProcessorConfiguration instance() { 49 | return instance; 50 | } 51 | 52 | /** 53 | * @return last event processed (issue) or null if not available for specified project 54 | */ 55 | public Date loadLastProcessedDate(String project) { 56 | Long lastEventDate = lastEventConfiguration.getLong(getProjectLastEventDateKey(project), null); 57 | if (lastEventDate != null && lastEventDate > 0) { 58 | return new Date(lastEventDate); 59 | } 60 | return null; 61 | } 62 | 63 | 64 | 65 | public void saveLastProcessedEventDate(Date lastProcessedEventDate, String project) throws ConfigurationException { 66 | Date currentLastProcessedDate = loadLastProcessedDate(project); 67 | if (currentLastProcessedDate == null || currentLastProcessedDate.before(lastProcessedEventDate)) { 68 | lastEventConfiguration.setProperty(getProjectLastEventDateKey(project), lastProcessedEventDate.getTime()); 69 | lastEventConfiguration.save(); 70 | } 71 | } 72 | 73 | private String getProjectLastEventDateKey(String project) { 74 | return LAST_EVENT_DATE+"."+project; 75 | } 76 | 77 | /** 78 | * "maximum-allowed window" defined/configured by the configuration property PROP.ISSUE_HISTORY_WINDOW 79 | * @param date date 80 | * @return date if it is after the "maximum-allowed window" or date which define the lower bound of "maximum-allowed window" 81 | */ 82 | public Date resolveMinimumAllowedDate(Date date) { 83 | Date oldestDateInThePast = oldestDateInThePast(); 84 | if (date == null) { 85 | return oldestDateInThePast; 86 | } else if (date.before(oldestDateInThePast)) { 87 | return oldestDateInThePast; 88 | } 89 | return date; 90 | } 91 | 92 | /** 93 | * @return Date in the past - N minutes before now. Where N - defined by the property "PROP.ISSUE_HISTORY_WINDOW" 94 | */ 95 | public Date oldestDateInThePast() { 96 | return new DateBuilder().addMinutes(-getIssueHistoryWindowInMinutes()).build(); 97 | } 98 | 99 | public void clear() throws ConfigurationException { 100 | lastEventConfiguration.clear(); 101 | lastEventConfiguration.save(); 102 | } 103 | 104 | public int getIssueHistoryWindowInMinutes() { 105 | //3 days by default 106 | if (issueHistoryWindowInMinutes == null) { 107 | return ConfigurationFactory.get().getInt(PROP_ISSUE_HISTORY_WINDOW, 60 * 24 * 3); 108 | } 109 | return issueHistoryWindowInMinutes; 110 | } 111 | 112 | /** 113 | * Disposes itself and re-initializes 114 | */ 115 | public void reload() { 116 | initialize(); 117 | } 118 | 119 | public void setIssueHistoryWindowInMinutes(int issueHistoryWindowInMinutes) { 120 | this.issueHistoryWindowInMinutes = issueHistoryWindowInMinutes; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue-changes-t1-t3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DMAN 6 | 7 | 8 | 584 9 | 10 | 11 | Display major events (Equinox / Solstice) 12 | 13 | 14 | 1404394803932 15 | 16 | 17 | 1409073351938 18 | 19 | 20 | nikolay.chorniy@gmail.com 21 | 22 | 23 | Nikolay Chorniy 24 | 25 | 26 | noura 27 | 28 | 29 | Noura Hassan 30 | 31 | 32 | 0 33 | 34 | 35 | 0 36 | 37 | 38 | DMAN-573 39 | 40 | 41 | Normal 42 | 43 | 44 | Task 45 | 46 | 47 | Reopened 48 | 49 | 50 | No subsystem 51 | 52 | 53 | 1.0.23 54 | 55 | Star 56 | 57 | 58 | 59 | rob 60 | 61 | 62 | 1404927516756 63 | 64 | 65 | 0.1.13 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | rob 83 | 84 | 85 | 1406227765001 86 | 87 | 88 | 0.1.16 89 | 0.1.19 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/jobs/EventListenerImpl.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.jobs; 2 | 3 | import com.ontometrics.integrations.configuration.ChatServer; 4 | import com.ontometrics.integrations.configuration.ConfigurationFactory; 5 | import com.ontometrics.integrations.configuration.EventProcessorConfiguration; 6 | import com.ontometrics.integrations.configuration.YouTrackInstanceFactory; 7 | import com.ontometrics.integrations.events.IssueEditSession; 8 | import com.ontometrics.integrations.sources.EditSessionsExtractor; 9 | import com.ontometrics.integrations.sources.StreamProvider; 10 | import org.apache.commons.configuration.Configuration; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.util.*; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | 17 | 18 | /** 19 | * Created on 8/18/14. 20 | * 21 | */ 22 | public class EventListenerImpl implements EventListener { 23 | private static final Logger log = LoggerFactory.getLogger(EventListenerImpl.class); 24 | 25 | private static final Comparator CREATED_TIME_COMPARATOR = new Comparator() { 26 | @Override 27 | public int compare(IssueEditSession s1, IssueEditSession s2) { 28 | return s1.getUpdated().compareTo(s2.getUpdated()); 29 | } 30 | }; 31 | 32 | private ChatServer chatServer; 33 | 34 | private ProjectProvider projectProvider; 35 | 36 | private EditSessionsExtractor editSessionsExtractor; 37 | 38 | /** 39 | * @param feedStreamProvider feed resource provider 40 | */ 41 | public EventListenerImpl(StreamProvider feedStreamProvider, ChatServer chatServer) { 42 | this(createEditSessionExtractor(feedStreamProvider)); 43 | this.chatServer = chatServer; 44 | } 45 | 46 | private static EditSessionsExtractor createEditSessionExtractor(StreamProvider feedStreamProvider) { 47 | if(feedStreamProvider == null) { 48 | throw new IllegalArgumentException("You must provide feedStreamProvider."); 49 | } 50 | Configuration configuration = ConfigurationFactory.get(); 51 | return new EditSessionsExtractor(YouTrackInstanceFactory.createYouTrackInstance(configuration), feedStreamProvider); 52 | } 53 | 54 | /** 55 | * @param editSessionsExtractor editSessionsExtractor 56 | */ 57 | public EventListenerImpl(EditSessionsExtractor editSessionsExtractor) { 58 | if(editSessionsExtractor == null ) { 59 | throw new IllegalArgumentException("You must provide sourceURL and chatServer."); 60 | } 61 | this.editSessionsExtractor = editSessionsExtractor; 62 | this.projectProvider = new ProjectProvider(editSessionsExtractor.getIssueTracker(), 63 | editSessionsExtractor.getStreamProvider()); 64 | } 65 | 66 | /** 67 | *

68 | * On wake, the job of this agent is simply to get any edits that have occurred since its last run from 69 | * the ticketing system (using the {@link com.ontometrics.integrations.sources.EditSessionsExtractor} and 70 | * then post them to the {@link com.ontometrics.integrations.configuration.ChatServer}. 71 | *

72 | *

73 | * This should stay simple: if we can't process a session for any reason we should skip it. 74 | *

75 | * 76 | * @return the number of sessions that were processed 77 | * @throws Exception if it fails to save the last event date 78 | */ 79 | @Override 80 | public int checkForNewEvents() throws Exception { 81 | Set projects = projectProvider.all(); 82 | final AtomicInteger processedSessionsCount = new AtomicInteger(0); 83 | 84 | for (String project: projects) { 85 | List editSessions = editSessionsExtractor.getLatestEdits(project); 86 | log.info("Found {} edit sessions to post.", editSessions.size()); 87 | if (editSessions.size() > 0) { 88 | Collections.sort(editSessions, CREATED_TIME_COMPARATOR); 89 | log.debug("sessions: {}", editSessions); 90 | Date lastProcessedSessionDate = null; 91 | for (IssueEditSession session : editSessions) { 92 | if (session.isCreationEdit()) { 93 | chatServer.postIssueCreation(session.getIssue()); 94 | } else { 95 | chatServer.post(session); 96 | } 97 | lastProcessedSessionDate = session.getUpdated(); 98 | processedSessionsCount.incrementAndGet(); 99 | } 100 | 101 | log.debug("setting last processed date for project {} to: {}", project, lastProcessedSessionDate); 102 | EventProcessorConfiguration.instance().saveLastProcessedEventDate(lastProcessedSessionDate, project); 103 | } 104 | 105 | 106 | } 107 | return processedSessionsCount.get(); 108 | } 109 | 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue-changes-t2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DMAN 6 | 7 | 8 | 584 9 | 10 | 11 | Display major events (Equinox / Solstice) 12 | 13 | 14 | 1404394803932 15 | 16 | 17 | 1409073351938 18 | 19 | 20 | nikolay.chorniy@gmail.com 21 | 22 | 23 | Nikolay Chorniy 24 | 25 | 26 | noura 27 | 28 | 29 | Noura Hassan 30 | 31 | 32 | 0 33 | 34 | 35 | 0 36 | 37 | 38 | DMAN-573 39 | 40 | 41 | Normal 42 | 43 | 44 | Task 45 | 46 | 47 | Reopened 48 | 49 | 50 | No subsystem 51 | 52 | 53 | 1.0.23 54 | 55 | Star 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | rob 71 | 72 | 73 | 1405025458837 74 | 75 | 76 | 0.1.13 77 | 0.1.16 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue-changes-t1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DMAN 6 | 7 | 8 | 584 9 | 10 | 11 | Display major events (Equinox / Solstice) 12 | 13 | 14 | 1404394803932 15 | 16 | 17 | 1409073351938 18 | 19 | 20 | nikolay.chorniy@gmail.com 21 | 22 | 23 | Nikolay Chorniy 24 | 25 | 26 | noura 27 | 28 | 29 | Noura Hassan 30 | 31 | 32 | 0 33 | 34 | 35 | 0 36 | 37 | 38 | DMAN-573 39 | 40 | 41 | Normal 42 | 43 | 44 | Task 45 | 46 | 47 | Reopened 48 | 49 | 50 | No subsystem 51 | 52 | 53 | 1.0.23 54 | 55 | Star 56 | 57 | 58 | 59 | rob 60 | 61 | 62 | 1404927516756 63 | 64 | 65 | 0.1.13 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue1-timeline-changes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DMAN 6 | 7 | 8 | 584 9 | 10 | 11 | Display major events (Equinox / Solstice) 12 | 13 | 14 | 1404394803932 15 | 16 | 17 | 1409073351938 18 | 19 | 20 | nikolay.chorniy@gmail.com 21 | 22 | 23 | Nikolay Chorniy 24 | 25 | 26 | noura 27 | 28 | 29 | Noura Hassan 30 | 31 | 32 | 0 33 | 34 | 35 | 0 36 | 37 | 38 | DMAN-573 39 | 40 | 41 | Normal 42 | 43 | 44 | Task 45 | 46 | 47 | Reopened 48 | 49 | 50 | No subsystem 51 | 52 | 53 | 1.0.23 54 | 55 | Star 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | rob 72 | 73 | 74 | 75 | 1404927529000 76 | 77 | 78 | 0.1.13 79 | 0.1.16 80 | 81 | 82 | 83 | 84 | rob 85 | 86 | 87 | 88 | 1404927549000 89 | 90 | 91 | 0.1.16 92 | 0.1.19 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue2-timeline-changes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DMAN 6 | 7 | 8 | 584 9 | 10 | 11 | Display major events (Equinox / Solstice) 12 | 13 | 14 | 1404394803932 15 | 16 | 17 | 1409073351938 18 | 19 | 20 | nikolay.chorniy@gmail.com 21 | 22 | 23 | Nikolay Chorniy 24 | 25 | 26 | noura 27 | 28 | 29 | Noura Hassan 30 | 31 | 32 | 0 33 | 34 | 35 | 0 36 | 37 | 38 | DMAN-573 39 | 40 | 41 | Normal 42 | 43 | 44 | Task 45 | 46 | 47 | Reopened 48 | 49 | 50 | No subsystem 51 | 52 | 53 | 1.0.23 54 | 55 | Star 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | rob 72 | 73 | 74 | 75 | 1404927539000 76 | 77 | 78 | 0.1.13 79 | 0.1.16 80 | 81 | 82 | 83 | 84 | rob 85 | 86 | 87 | 88 | 1404927559000 89 | 90 | 91 | 0.1.16 92 | 0.1.19 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue-changes2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HA665Scraper does not fetch Glassdoor urls 5 | When scraper tries to fetch contents of Glassdoor job listing url, it gets HTTP status 403 response 6 | 7 | {code}java.io.IOException: Server returned HTTP response code: 403 for URL: 8 | http://www.glassdoor.com/Job/california-java-developer-jobs-SRCH_IL.0,10_IS2280_KO11,25_IP6.htm 9 | {code} 10 | 1408106353707 11 | 1408568585767 12 | nikolay.chorniy@gmail.com 13 | Nikolay Chorniy 14 | nikolay.chorniy@gmail.com 15 | Nikolay Chorniy 16 | 10MajorBug 17 | OpenNo subsystem2.1 18 | 19 | 20 | 21 | 1408106436989 22 | 23 | When scraper tries to fetch contents of Glassdoor job listing url, it gets 403 response 24 | 25 | 26 | {code} 27 | java.io.IOException: Server returned HTTP response code: 403 for URL: 28 | http://www.glassdoor.com/Job/california-java-developer-jobs-SRCH_IL.0,10_IS2280_KO11,25_IP6.htm 29 | {code}When scraper tries to fetch contents of Glassdoor job listing url, it gets HTTP status 403 response 30 | 31 | {code} 32 | java.io.IOException: Server returned HTTP response code: 403 for URL: 33 | http://www.glassdoor.com/Job/california-java-developer-jobs-SRCH_IL.0,10_IS2280_KO11,25_IP6.htm 34 | {code} 35 | nikolay.chorniy@gmail.com1408568585769When scraper tries to fetch contents of Glassdoor job listing url, it gets HTTP status 403 response 36 | 37 | {code} 38 | java.io.IOException: Server returned HTTP response code: 403 for URL: 39 | http://www.glassdoor.com/Job/california-java-developer-jobs-SRCH_IL.0,10_IS2280_KO11,25_IP6.htm 40 | {code}When scraper tries to fetch contents of Glassdoor job listing url, it gets HTTP status 403 response 41 | 42 | {code}java.io.IOException: Server returned HTTP response code: 403 for URL: 43 | http://www.glassdoor.com/Job/california-java-developer-jobs-SRCH_IL.0,10_IS2280_KO11,25_IP6.htm 44 | {code} 45 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue-details.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | HA 4 | 5 | 6 | 663 7 | 8 | 9 | Detect lead duplication across different sources 10 | 11 | 12 | 13 | *Problem:* There are the cases when different sources has similar (duplicate jobs) http://insidehire.com/lead/53eb1529e4b0b6b12d015c04_524d7e2476ec77212be0a7f9 http://insidehire.com/lead/53eac0e9e4b0b6b12d015462_524d7e2476ec77212be0a7f9 System should autodetect that and show only one posting instead. System should maintain the links to the "duplicates" from different sources in the job-posting which will not be filtered out and show them as additional source icons in the "job-posting" - that icns will be a links to that "filtered" job-postings. *Notes:* # on the moment system do not have full-descriptions for both sourcesto compare (we may need to extract them from HTML for any source) # Indeed have short description and we really do not extract job-details from Indeed postings. We need to extract full job-description out of job-details page 14 | 15 | 16 | 17 | 1408046616632 18 | 19 | 20 | 1409255012201 21 | 22 | 23 | rob 24 | 25 | 26 | Rob Williams 27 | 28 | 29 | andrey.chorniy 30 | 31 | 32 | Andrey Chorniy 33 | 34 | 35 | 3 36 | 37 | 38 | 0 39 | 40 | 41 | Normal 42 | 43 | 44 | Task 45 | 46 | 47 | In Progress 48 | 49 | 50 | No subsystem 51 | 52 | 53 | 2.2 54 | 55 | 56 | 2.1 57 | 58 | 59 | image1.png 60 | Screen Shot 2014-08-25 at 2.28.54 PM.png 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | Star 75 | -------------------------------------------------------------------------------- /src/test/java/com/ontometrics/integrations/configuration/SlackInstanceTest.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | import com.ontometrics.integrations.events.Comment; 4 | import com.ontometrics.integrations.events.Issue; 5 | import com.ontometrics.integrations.events.IssueEditSession; 6 | import com.ontometrics.integrations.events.TestDataFactory; 7 | import com.ontometrics.integrations.sources.ChannelMapper; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.slf4j.Logger; 11 | 12 | import java.net.MalformedURLException; 13 | import java.net.URL; 14 | import java.util.Date; 15 | 16 | import static org.hamcrest.CoreMatchers.*; 17 | import static org.hamcrest.MatcherAssert.assertThat; 18 | import static org.slf4j.LoggerFactory.getLogger; 19 | 20 | public class SlackInstanceTest { 21 | 22 | private Logger log = getLogger(SlackInstanceTest.class); 23 | private SlackInstance slackInstance; 24 | 25 | @Before 26 | public void setUp() throws Exception { 27 | ChannelMapper channelMapper = new ChannelMapper.Builder() 28 | .addMapping("ASOC", "vixlet") 29 | .addMapping("DMIN", "dminder") 30 | .addMapping("DMAN", "dminder") 31 | .build(); 32 | 33 | slackInstance = new SlackInstance.Builder().channelMapper(channelMapper).build(); 34 | 35 | } 36 | 37 | @Test 38 | public void testPost() throws Exception { 39 | 40 | IssueEditSession session = TestDataFactory.build(); 41 | 42 | String message = slackInstance.buildSessionMessage(session); 43 | 44 | log.info("message: {}", message); 45 | 46 | } 47 | 48 | @Test 49 | public void testNewIssueMessage() throws MalformedURLException { 50 | String issueDescription = "Right now there is too much work to get what you want."; 51 | Issue issue = new Issue.Builder() 52 | .projectPrefix("ASOC") 53 | .id(492) 54 | .title("ASOC-492: Title autosuggest and normalization") 55 | .description(issueDescription) 56 | .created(new Date()) 57 | .creator("Noura") 58 | .link(new URL("http://ontometrics.com:8085/issue/ASOC-408")) 59 | .build(); 60 | String newIssueMessage = slackInstance.buildNewIssueMessage(issue); 61 | 62 | log.info("new issue: {}", newIssueMessage); 63 | 64 | assertThat(newIssueMessage, is("*Noura* created : Title autosuggest and normalization" + System.lineSeparator() + issueDescription)); 65 | 66 | } 67 | 68 | @Test 69 | public void testThatWeCanPostASessionThatContainsAComment() throws MalformedURLException { 70 | Issue issue = new Issue.Builder() 71 | .projectPrefix("ASOC") 72 | .id(492) 73 | .title("ASOC-492: Title autosuggest and normalization") 74 | .created(new Date()) 75 | .creator("Noura") 76 | .link(new URL("http://ontometrics.com:8085/issue/ASOC-408")) 77 | .build(); 78 | 79 | Comment comment = new Comment.Builder().author("Noura").text("This code is impossible to understand").created(new Date()).build(); 80 | IssueEditSession session = new IssueEditSession.Builder() 81 | .issue(issue) 82 | .updater("Noura") 83 | .updated(new Date()) 84 | .comment(comment) 85 | .build(); 86 | 87 | log.info("comment issue edit session: {}", slackInstance.buildSessionMessage(session)); 88 | 89 | } 90 | 91 | @Test 92 | public void testGetUsers() throws Exception { 93 | 94 | } 95 | 96 | @Test 97 | /** 98 | * Verifies that {@link com.ontometrics.integrations.configuration.SlackInstance#buildNewIssueMessage(com.ontometrics.integrations.events.Issue)} 99 | * returns correct text 100 | */ 101 | public void testBuildNewIssueMessage() throws MalformedURLException { 102 | String message = slackInstance.buildNewIssueMessage(new Issue.Builder().created(new Date()).creator("Johann Bach") 103 | .title("HA-492: Prelude and Fugue in C major").link(new URL("http://google.com")).projectPrefix("HA").build()); 104 | assertThat(message, allOf(containsString("Johann Bach"), containsString("created"), 105 | containsString("Prelude and Fugue in C major"), containsString("http://google.com"))); 106 | } 107 | 108 | @Test 109 | public void testThatDeletedCommentsAreNotPosted() throws MalformedURLException { 110 | Issue issue = new Issue.Builder() 111 | .projectPrefix("ASOC") 112 | .id(492) 113 | .title("ASOC-492: Title autosuggest and normalization") 114 | .created(new Date()) 115 | .creator("Noura") 116 | .link(new URL("http://ontometrics.com:8085/issue/ASOC-408")) 117 | .build(); 118 | 119 | Comment comment = new Comment.Builder().author("Noura").text("This code is impossible to understand") 120 | .deleted(true).created(new Date()).build(); 121 | IssueEditSession session = new IssueEditSession.Builder() 122 | .issue(issue) 123 | .updater("Noura") 124 | .updated(new Date()) 125 | .comment(comment) 126 | .build(); 127 | 128 | assertThat(slackInstance.buildSessionMessage(session), not(containsString("*Noura* commented"))); 129 | assertThat(slackInstance.buildSessionMessage(session), containsString("*Noura* updated")); 130 | } 131 | 132 | } -------------------------------------------------------------------------------- /src/test/java/com/ontometrics/integrations/configuration/EventProcessorConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.configuration; 2 | 3 | import com.ontometrics.integrations.events.Issue; 4 | import com.ontometrics.integrations.events.ProcessEvent; 5 | import org.apache.commons.configuration.ConfigurationException; 6 | import org.apache.commons.lang.time.DateUtils; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | 10 | import java.net.MalformedURLException; 11 | import java.net.URL; 12 | import java.util.Calendar; 13 | import java.util.Date; 14 | 15 | import static org.hamcrest.CoreMatchers.is; 16 | import static org.hamcrest.CoreMatchers.notNullValue; 17 | import static org.hamcrest.CoreMatchers.nullValue; 18 | import static org.hamcrest.MatcherAssert.assertThat; 19 | 20 | /** 21 | * Tests for {@link com.ontometrics.integrations.configuration.EventProcessorConfiguration} 22 | * EventProcessorConfigurationTest.java 23 | */ 24 | public class EventProcessorConfigurationTest { 25 | private EventProcessorConfiguration configuration; 26 | 27 | @Before 28 | public void setUp() throws ConfigurationException { 29 | this.configuration = EventProcessorConfiguration.instance(); 30 | configuration.clear(); 31 | } 32 | 33 | /** 34 | * Verifies that last event change date is stored (even after database is restarted) 35 | */ 36 | /* 37 | @Test 38 | public void testThatLastEventChangeDateIsStored() throws ConfigurationException, MalformedURLException { 39 | // configuration.clearLastProcessEvent(); 40 | // assertThat(configuration.loadLastProcessedEvent(), nullValue()); 41 | 42 | 43 | Calendar lastEventChangeTime = Calendar.getInstance(); 44 | lastEventChangeTime.add(Calendar.MINUTE, -2); 45 | 46 | Issue issue = new Issue.Builder().projectPrefix("ASOC").id(148) 47 | .link(new URL("http://ontometrics.com:8085/issue/ASOC-148")) 48 | .title("ASOC-148: New Embedding requirement") 49 | .build(); 50 | 51 | ProcessEvent event1 = new ProcessEvent.Builder() 52 | .issue(issue) 53 | .published(new Date()) 54 | .build(); 55 | 56 | configuration.saveEventChangeDate(event1, lastEventChangeTime.getTime()); 57 | 58 | //restarting the configuration and database to make sure that even after server restart correct 59 | // date will be retrieved for event 60 | configuration.reload(); 61 | Date storedChangeDate = configuration.getEventChangeDate(event1); 62 | assertThat(storedChangeDate, notNullValue()); 63 | assertThat(storedChangeDate, is(lastEventChangeTime.getTime())); 64 | 65 | Issue issue2 = new Issue.Builder().projectPrefix("ASOC").id(149) 66 | .link(new URL("http://ontometrics.com:8085/issue/ASOC-149")) 67 | .title("ASOC-149: Newer Embedding requirement") 68 | .build(); 69 | 70 | 71 | ProcessEvent event2 = new ProcessEvent.Builder() 72 | .issue(issue2) 73 | .published(new Date()) 74 | .build(); 75 | assertThat(configuration.getEventChangeDate(event2), nullValue()); 76 | 77 | } 78 | */ 79 | 80 | /** 81 | * Verifies that last processed date is stored if it is after current one or current one is not defined 82 | */ 83 | /* 84 | @Test 85 | public void testThatOnlyDatesAfterTheCurrentLastProcessedDateAreStored() throws ConfigurationException, MalformedURLException { 86 | 87 | Date date_1 = new Date(10000); 88 | Date date_2 = new Date(20000); 89 | Date date_3 = new Date(20000); 90 | 91 | assertThat(configuration.loadLastProcessedDate(), nullValue()); 92 | configuration.saveLastProcessedEventDate(date_1); 93 | assertThat(configuration.loadLastProcessedDate(), is(date_1)); 94 | configuration.reload(); 95 | assertThat(configuration.loadLastProcessedDate(), is(date_1)); 96 | 97 | configuration.saveLastProcessedEventDate(date_3); 98 | assertThat(configuration.loadLastProcessedDate(), is(date_3)); 99 | configuration.reload(); 100 | assertThat(configuration.loadLastProcessedDate(), is(date_3)); 101 | 102 | configuration.saveLastProcessedEventDate(date_2); 103 | assertThat(configuration.loadLastProcessedDate(), is(date_3)); 104 | configuration.reload(); 105 | assertThat(configuration.loadLastProcessedDate(), is(date_3)); 106 | } 107 | */ 108 | 109 | 110 | @Test 111 | public void testThatMinimumAllowedDateCorrectlyResolved(){ 112 | EventProcessorConfiguration configuration = EventProcessorConfiguration.instance(); 113 | ConfigurationFactory.get().setProperty(EventProcessorConfiguration.PROP_ISSUE_HISTORY_WINDOW, "10"); 114 | 115 | Date elevenMinutesBefore = DateUtils.addMinutes(new Date(), -11); 116 | Date eightMinutesBefore = DateUtils.addMinutes(new Date(), -8); 117 | assertThat(configuration.resolveMinimumAllowedDate(eightMinutesBefore), is(eightMinutesBefore)); 118 | 119 | Date tenMinutesBefore = DateUtils.addMinutes(new Date(), -10); 120 | assertDatesAreAlmostEqual(configuration.resolveMinimumAllowedDate(elevenMinutesBefore), tenMinutesBefore, 10); 121 | 122 | assertDatesAreAlmostEqual(configuration.resolveMinimumAllowedDate(null), tenMinutesBefore, 10); 123 | } 124 | 125 | private void assertDatesAreAlmostEqual(Date date1, Date date2, int maxDiff) { 126 | if (Math.abs(date1.getTime() - date2.getTime()) > maxDiff) { 127 | assertThat("Dates are not equals", date1, is(date2)); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue-changes-t2-t4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DMAN 6 | 7 | 8 | 584 9 | 10 | 11 | Display major events (Equinox / Solstice) 12 | 13 | 14 | 1404394803932 15 | 16 | 17 | 1409073351938 18 | 19 | 20 | nikolay.chorniy@gmail.com 21 | 22 | 23 | Nikolay Chorniy 24 | 25 | 26 | noura 27 | 28 | 29 | Noura Hassan 30 | 31 | 32 | 0 33 | 34 | 35 | 0 36 | 37 | 38 | DMAN-573 39 | 40 | 41 | Normal 42 | 43 | 44 | Task 45 | 46 | 47 | Reopened 48 | 49 | 50 | No subsystem 51 | 52 | 53 | 1.0.23 54 | 55 | Star 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | rob 71 | 72 | 73 | 1405025458837 74 | 75 | 76 | 0.1.13 77 | 0.1.16 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | rob 95 | 96 | 97 | 1407626732316 98 | 99 | 100 | 1.0.20 101 | 1.0.21 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/events/IssueEditSession.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.events; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.Date; 6 | import java.util.List; 7 | 8 | /** 9 | *

10 | * Represents a single User (updater) making one or more changes to 11 | * an {@link com.ontometrics.integrations.events.Issue} at a given 12 | * time. 13 | *

14 | * 15 | * User: Rob 16 | * Date: 8/23/14 17 | * Time: 7:25 PM 18 | *

19 | * (c) ontometrics 2014, All Rights Reserved 20 | */ 21 | public class IssueEditSession { 22 | 23 | private final Issue issue; 24 | private final String updater; 25 | private final List changes; 26 | private final Comment comment; 27 | private final Date updated; 28 | private final List attachments; 29 | private final List links; 30 | 31 | public IssueEditSession(Builder builder) { 32 | issue = builder.issue; 33 | updater = builder.updater; 34 | updated = builder.updated; 35 | changes = new ArrayList<>(builder.changes); 36 | comment = builder.comment; 37 | attachments = new ArrayList<>(builder.attachments); 38 | links = new ArrayList<>(builder.links); 39 | } 40 | 41 | public IssueEditSession removeAttachments() { 42 | attachments.clear(); 43 | return this; 44 | } 45 | 46 | public static class Builder { 47 | 48 | private Issue issue; 49 | private String updater; 50 | private Date updated; 51 | private List changes = Collections.emptyList(); 52 | private Comment comment; 53 | private List attachments = Collections.emptyList(); 54 | private List links = Collections.emptyList(); 55 | 56 | public Builder issue(Issue issue){ 57 | this.issue = issue; 58 | return this; 59 | } 60 | 61 | public Builder updater(String updater){ 62 | this.updater = updater; 63 | return this; 64 | } 65 | 66 | public Builder updated(Date updated){ 67 | this.updated = updated; 68 | return this; 69 | } 70 | 71 | public Builder changes(List changes){ 72 | this.changes = changes; 73 | return this; 74 | } 75 | 76 | public Builder comment(Comment comment){ 77 | this.comment = comment; 78 | return this; 79 | } 80 | 81 | public Builder attachments(List attachments){ 82 | this.attachments = attachments; 83 | return this; 84 | } 85 | 86 | public Builder links(List links){ 87 | this.links = links; 88 | return this; 89 | } 90 | 91 | public IssueEditSession build(){ 92 | return new IssueEditSession(this); 93 | } 94 | 95 | 96 | } 97 | 98 | public Issue getIssue() { 99 | return issue; 100 | } 101 | 102 | public String getUpdater() { 103 | return updater; 104 | } 105 | 106 | public Date getUpdated() { 107 | return updated; 108 | } 109 | 110 | /** 111 | * The changes that were made in this session. 112 | * 113 | * @return all edits made by the updater in this session 114 | */ 115 | public List getChanges() { 116 | return changes; 117 | } 118 | 119 | public Comment getComment() { 120 | return comment; 121 | } 122 | 123 | public List getAttachments() { 124 | return attachments; 125 | } 126 | 127 | public List getLinks() { 128 | return links; 129 | } 130 | 131 | public boolean isCreationEdit(){ 132 | return getIssue().getCreated()!=null && !getIssue().getCreator().isEmpty() && ((getUpdated().getTime()-getIssue().getCreated().getTime())/(1000*60*60) < 5); 133 | } 134 | 135 | public boolean hasChanges(){ 136 | return getChanges().size() > 0 || (getComment() != null && !getComment().isDeleted()); 137 | } 138 | 139 | @Override 140 | public boolean equals(Object o) { 141 | if (this == o) return true; 142 | if (o == null || getClass() != o.getClass()) return false; 143 | 144 | IssueEditSession that = (IssueEditSession) o; 145 | 146 | return issue.equals(that.issue) && updated.equals(that.updated) && !(updater != null ? !updater.equals(that.updater) : that.updater != null); 147 | 148 | } 149 | 150 | @Override 151 | public int hashCode() { 152 | int result = issue.hashCode(); 153 | result = 31 * result + (updater != null ? updater.hashCode() : 0); 154 | result = 31 * result + updated.hashCode(); 155 | return result; 156 | } 157 | 158 | @Override 159 | public String toString() { 160 | StringBuilder b = new StringBuilder(issue.toString()); 161 | b.append(String.format(" %s ", updater)); 162 | int changeCounter = 0; 163 | for (IssueEdit edit : changes){ 164 | b.append(edit.toString()); 165 | if (changeCounter++ < changes.size()-1){ 166 | b.append(", "); 167 | } 168 | } 169 | if (getComment()!=null){ 170 | b.append(comment.toString()).append(System.lineSeparator()); 171 | } 172 | for (AttachmentEvent attachmentEvent : attachments){ 173 | b.append(attachmentEvent.toString()).append(System.lineSeparator()); 174 | } 175 | for (IssueLink link : links){ 176 | b.append(link.toString()).append(System.lineSeparator()); 177 | } 178 | return b.toString(); 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/test/resources/feeds/issue-with-comments-ap-22.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AP 6 | 7 | 8 | 22 9 | 10 | 11 | Agents must be able to generate Proposals 12 | 13 | 14 | This is the beginning of the road to a Contract. 15 | 16 | The template will be merged with the basic information that has been gathered. 17 | 18 | We need to think about how we could potentially keep a few options in orbit, gracefully. It's very 19 | possible someone might ask for a few proposals and then decide to go with one of them. 20 | 21 | The basic information needed at the time of proposal is laid out in the requirements; there are few 22 | surprises: 23 | 24 | * Customer (Company) 25 | * Talent 26 | * Fees 27 | * Event information 28 | 29 | Merging the results of these input variables and the template produces the Proposal. 30 | 31 | 32 | 33 | 1407425265769 34 | 35 | 36 | 1415631357818 37 | 38 | 39 | nikolay.chorniy@gmail.com 40 | 41 | 42 | Nikolay Chorniy 43 | 44 | 45 | rob 46 | 47 | 48 | Rob Williams 49 | 50 | 51 | 2 52 | 53 | 54 | 0 55 | 56 | 57 | AP-23 58 | 59 | 60 | Normal 61 | Normal 62 | 63 | #ebf4dd 64 | #64992C 65 | 66 | 67 | 68 | Story 69 | Story 70 | 71 | 72 | In Progress 73 | In Progress 74 | 75 | 76 | No subsystem 77 | 78 | 79 | 0.0.6 80 | 81 | 84 | 85 | 86 | 88 | 89 | 90 | Star 91 | 92 | 93 | 94 | rob 95 | 96 | 97 | 1407425449468 98 | 99 | 100 | AP-23 101 | 102 | 103 | 104 | 105 | rob 106 | 107 | 108 | 1414179732983 109 | 110 | 111 | Bug 112 | Story 113 | 114 | 115 | 0.0.4 116 | 117 | 118 | 119 | 120 | andrey.chorniy 121 | 122 | 123 | 1414516357474 124 | 125 | 126 | Submitted 127 | In Progress 128 | 129 | 130 | 131 | 132 | rob 133 | 134 | 135 | 1414789266374 136 | 137 | 138 | 0.0.4 139 | 0.0.5 140 | 141 | 142 | 143 | 144 | rob 145 | 146 | 147 | 1415508872698 148 | 149 | 150 | 0.0.5 151 | 0.0.6 152 | 153 | 154 | -------------------------------------------------------------------------------- /src/main/java/com/ontometrics/integrations/youtrack/YouTrackImageServlet.java: -------------------------------------------------------------------------------- 1 | package com.ontometrics.integrations.youtrack; 2 | 3 | import com.ontometrics.db.MapDb; 4 | import com.ontometrics.integrations.configuration.ConfigurationFactory; 5 | import com.ontometrics.integrations.configuration.StreamProviderFactory; 6 | import com.ontometrics.integrations.configuration.YouTrackInstance; 7 | import com.ontometrics.integrations.configuration.YouTrackInstanceFactory; 8 | import com.ontometrics.integrations.sources.InputStreamHandler; 9 | import com.ontometrics.integrations.sources.StreamProvider; 10 | import org.apache.commons.configuration.Configuration; 11 | import org.apache.commons.io.IOUtils; 12 | import org.apache.commons.lang.StringUtils; 13 | import org.apache.http.client.methods.HttpGet; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import javax.servlet.ServletException; 18 | import javax.servlet.http.HttpServlet; 19 | import javax.servlet.http.HttpServletRequest; 20 | import javax.servlet.http.HttpServletResponse; 21 | import java.io.IOException; 22 | import java.io.InputStream; 23 | import java.net.MalformedURLException; 24 | import java.net.URL; 25 | import java.net.URLEncoder; 26 | import java.nio.charset.Charset; 27 | 28 | /** 29 | * 30 | */ 31 | public class YouTrackImageServlet extends HttpServlet { 32 | private static final Logger logger = LoggerFactory.getLogger(YouTrackImageServlet.class); 33 | 34 | private YouTrackInstance youTrackInstance; 35 | /** 36 | * Attachment URL example "http://issuetracker.com/_persistent/image.png?file=78-496" 37 | */ 38 | private StreamProvider streamProvider; 39 | 40 | @Override 41 | public void init() throws ServletException { 42 | super.init(); 43 | final Configuration configuration = ConfigurationFactory.get(); 44 | 45 | youTrackInstance = YouTrackInstanceFactory.createYouTrackInstance(configuration); 46 | streamProvider = StreamProviderFactory.createStreamProvider(configuration); 47 | 48 | } 49 | 50 | @Override 51 | protected void service(HttpServletRequest servletRequest, final HttpServletResponse servletResponse) 52 | throws ServletException, IOException { 53 | if (!servletRequest.getMethod().equals(HttpGet.METHOD_NAME)){ 54 | servletResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "Only GET method supported"); 55 | return; 56 | } 57 | 58 | String imageId = servletRequest.getParameter("rid"); 59 | if (StringUtils.isBlank(imageId)){ 60 | servletResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "Resource id not specified"); 61 | return; 62 | } 63 | 64 | URL youTrackUrl = resolveYouTrackAttachmentUri(imageId); 65 | logger.debug("Generated URI {0}", youTrackUrl); 66 | 67 | try { 68 | String name = servletRequest.getParameter("name"); 69 | if (StringUtils.isNotBlank(name)) { 70 | servletResponse.addHeader("Content-Disposition", buildContentDisposition(CONTENT_DISPOSITION_ATTACHMENT, name, Charset.forName("UTF-8"))); 71 | } 72 | streamProvider.openResourceStream(youTrackUrl, new InputStreamHandler() { 73 | @Override 74 | public Void handleStream(InputStream is, int responseCode) throws Exception { 75 | servletResponse.setStatus(responseCode); 76 | copyStream(is, servletResponse); 77 | return null; 78 | } 79 | }); 80 | } catch (Exception e) { 81 | logger.error("Failed to process request", e); 82 | servletResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 83 | } 84 | 85 | } 86 | 87 | private void copyStream(InputStream is, HttpServletResponse servletResponse) { 88 | //TODO we may generate thumbnail here if needed, for now we just stream it back 89 | try { 90 | IOUtils.copy(is, servletResponse.getOutputStream()); 91 | } catch (IOException e) { 92 | logger.error("Failed to write response", e); 93 | } 94 | } 95 | 96 | 97 | private URL resolveYouTrackAttachmentUri(String externalImageId) throws MalformedURLException { 98 | String youTrackImageId = resolveYouTrackImageId(externalImageId); 99 | if (StringUtils.isBlank(externalImageId)) { 100 | throw new RuntimeException("Image not found"); 101 | } 102 | return new URL(String.format("%s/_persistent/%s?file=%s", youTrackInstance.getBaseUrl(), "image.png", 103 | youTrackImageId)); 104 | } 105 | 106 | @SuppressWarnings("unused") 107 | private String resolveYouTrackImageId(String externalImageId) { 108 | return MapDb.instance().getAttachmentMap().get(externalImageId); 109 | } 110 | 111 | 112 | public static final String CONTENT_DISPOSITION_INLINE = "inline"; 113 | public static final String CONTENT_DISPOSITION_ATTACHMENT = "attachment"; 114 | 115 | /** 116 | * Set the (new) value of the {@code Content-Disposition} header 117 | * for {@code main body}, optionally encoding the filename using the RFC 5987. 118 | *

Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported. 119 | * 120 | * @param type content disposition type 121 | * @param filename the filename (may be {@code null}) 122 | * @param charset the charset used for the filename (may be {@code null}) 123 | * @see RFC 7230 Section 3.2.4 124 | * @since 4.3.3 125 | */ 126 | private static String buildContentDisposition(String type, String filename, Charset charset){ 127 | 128 | if (!CONTENT_DISPOSITION_INLINE.equals(type) && !CONTENT_DISPOSITION_ATTACHMENT.equals(type)) { 129 | throw new IllegalArgumentException("type must be inline or attachment"); 130 | } 131 | 132 | StringBuilder builder = new StringBuilder(type); 133 | if (filename != null) { 134 | builder.append("; "); 135 | 136 | if (charset == null || charset.name().equals("US-ASCII")) { 137 | builder.append("filename=\""); 138 | builder.append(filename).append('\"'); 139 | } else { 140 | builder.append("filename*="); 141 | builder.append(encodeHeaderFieldParam(filename, charset)); 142 | } 143 | } 144 | return builder.toString(); 145 | } 146 | 147 | /** 148 | * Copied from Spring {@link org.springframework.http.HttpHeaders} 149 | * 150 | * Encode the given header field param as describe in RFC 5987. 151 | * 152 | * @param input the header field param 153 | * @param charset the charset of the header field param string 154 | * @return the encoded header field param 155 | * @see RFC 5987 156 | */ 157 | private static String encodeHeaderFieldParam(String input, Charset charset) { 158 | if (charset.name().equals("US-ASCII")) { 159 | return input; 160 | } 161 | byte[] source = input.getBytes(charset); 162 | int len = source.length; 163 | StringBuilder sb = new StringBuilder(len << 1); 164 | sb.append(charset.name()); 165 | sb.append("''"); 166 | for (byte b : source) { 167 | if (isRFC5987AttrChar(b)) { 168 | sb.append((char) b); 169 | } else { 170 | sb.append('%'); 171 | char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16)); 172 | char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16)); 173 | sb.append(hex1); 174 | sb.append(hex2); 175 | } 176 | } 177 | return sb.toString(); 178 | } 179 | 180 | /** 181 | * Copied from Spring {@link org.springframework.http.HttpHeaders} 182 | */ 183 | private static boolean isRFC5987AttrChar(byte c) { 184 | return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || 185 | c == '!' || c == '#' || c == '$' || c == '&' || c == '+' || c == '-' || 186 | c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~'; 187 | } 188 | } 189 | --------------------------------------------------------------------------------