Sfoglia il codice sorgente

Rework authentication

ext.rascm 4 anni fa
parent
commit
8e371a0856

+ 1 - 1
build.gradle

@@ -15,7 +15,7 @@ dependencies {
15 15
     compile fileTree(dir: 'libs', include: '*.jar')
16 16
     compile 'io.github.openfeign:feign-core:11.1'
17 17
     compile 'io.github.openfeign:feign-httpclient:11.1'
18
-    compile 'io.github.openfeign:feign-jackson-jaxb:11.1'
18
+    compile 'io.github.openfeign:feign-jackson:11.1'
19 19
 
20 20
     compileOnly 'org.projectlombok:lombok:1.18.20'
21 21
     annotationProcessor 'org.projectlombok:lombok:1.18.20'

+ 1 - 2
settings.gradle

@@ -1,2 +1 @@
1
-rootProject.name = 'AcproTasks'
2
-
1
+rootProject.name = 'AcproTasks'

+ 33 - 23
src/main/java/com/armstrongconsulting/acprotasks/AcproRepository.java

@@ -1,11 +1,12 @@
1 1
 package com.armstrongconsulting.acprotasks;
2 2
 
3
+import org.apache.commons.lang.StringUtils;
3 4
 import org.jetbrains.annotations.NotNull;
4 5
 import org.jetbrains.annotations.Nullable;
5
-import org.slf4j.Logger;
6
-import org.slf4j.LoggerFactory;
7 6
 
8 7
 import com.armstrongconsulting.acprotasks.client.AcproConnector;
8
+import com.armstrongconsulting.acprotasks.client.model.ItemsContainer;
9
+import com.intellij.ide.util.PropertiesComponent;
9 10
 import com.intellij.openapi.progress.ProgressIndicator;
10 11
 import com.intellij.tasks.Task;
11 12
 import com.intellij.tasks.TaskRepositoryType;
@@ -13,14 +14,12 @@ import com.intellij.tasks.impl.BaseRepository;
13 14
 import com.intellij.util.xmlb.annotations.Tag;
14 15
 
15 16
 import feign.FeignException;
17
+import lombok.extern.slf4j.Slf4j;
16 18
 
17 19
 @Tag(AcproRepositoryType.NAME)
20
+@Slf4j
18 21
 public class AcproRepository extends BaseRepository
19 22
 {
20
-    private final Logger logger = LoggerFactory.getLogger(AcproRepository.class);
21
-
22
-    private String apiKey = null;
23
-
24 23
     public AcproRepository(TaskRepositoryType type)
25 24
     {
26 25
         super(type);
@@ -29,7 +28,8 @@ public class AcproRepository extends BaseRepository
29 28
     public AcproRepository(AcproRepository other)
30 29
     {
31 30
         super(other);
32
-        this.apiKey = other.apiKey;
31
+        setRepositoryType(other.getRepositoryType());
32
+        setApiKey(other.getApiKey());
33 33
     }
34 34
 
35 35
     public AcproRepository()
@@ -52,14 +52,22 @@ public class AcproRepository extends BaseRepository
52 52
     public Task[] getIssues(@Nullable String query, int offset, int limit, boolean withClosed,
53 53
         @NotNull ProgressIndicator cancelled) throws Exception
54 54
     {
55
-        if (apiKey == null)
55
+        if (StringUtils.isBlank(query))
56
+        {
57
+            return new Task[0];
58
+        }
59
+
60
+        ItemsContainer response = AcproConnector.getClient(this).getItems(query, offset, limit);
61
+        Task[] result = new Task[0];
62
+        if (response.getItems() != null && response.getItems().getItem() != null)
56 63
         {
57
-            apiKey = AcproConnector.getClient(getUrl()).getApiKey(getUsername(), getPassword(), 28800);
64
+            result = response.getItems().getItem().stream().map(AcproTask::new).toArray(Task[]::new);
58 65
         }
59
-        return new Task[0];/*AcproConnector.getClient(getUrl()).getItems(query, offset, limit, apiKey).stream().peek(a -> logger.warn(a.toString())).map(AcproTask::new)
60
-            .toArray(AcproTask[]::new);*/
66
+        return result;
61 67
     }
62 68
 
69
+    private final AcproRepository self = this;
70
+
63 71
     @Override
64 72
     public @Nullable CancellableConnection createCancellableConnection()
65 73
     {
@@ -68,22 +76,20 @@ public class AcproRepository extends BaseRepository
68 76
             @Override
69 77
             protected void doTest() throws Exception
70 78
             {
79
+                // do login and set apiKey
71 80
                 try
72 81
                 {
73
-                    if (apiKey != null)
74
-                    {
75
-                        return;
76
-                    }
77
-
78
-                    apiKey = AcproConnector.getClient(getUrl()).getApiKey(getUsername(), getPassword(), 28800);
82
+                    setApiKey(
83
+                        AcproConnector.getClient(self).getApiKey(getUsername(), getPassword()));
84
+                    log.warn("Fetched api key: {}", getApiKey());
79 85
                 }
80 86
                 catch (FeignException.Unauthorized e)
81 87
                 {
82
-                    throw new RuntimeException("Wrong username or password");
88
+                    throw new RuntimeException("Failed to authenticate", e);
83 89
                 }
84
-                catch (FeignException.FeignClientException e)
90
+                catch (FeignException e)
85 91
                 {
86
-                    throw new RuntimeException("Connection failed");
92
+                    throw new RuntimeException("Failed to connect to server", e);
87 93
                 }
88 94
             }
89 95
 
@@ -95,9 +101,13 @@ public class AcproRepository extends BaseRepository
95 101
         };
96 102
     }
97 103
 
98
-    @Override
99
-    public void initializeRepository()
104
+    public String getApiKey()
105
+    {
106
+        return PropertiesComponent.getInstance().getValue("api_key");
107
+    }
108
+
109
+    public void setApiKey(String apiKey)
100 110
     {
101
-        logger.info("Initialize repository");
111
+        PropertiesComponent.getInstance().setValue("api_key", apiKey);
102 112
     }
103 113
 }

+ 6 - 7
src/main/java/com/armstrongconsulting/acprotasks/client/AcproClient.java

@@ -1,6 +1,6 @@
1 1
 package com.armstrongconsulting.acprotasks.client;
2 2
 
3
-import com.armstrongconsulting.acprotasks.client.model.Items;
3
+import com.armstrongconsulting.acprotasks.client.model.ItemsContainer;
4 4
 
5 5
 import feign.Headers;
6 6
 import feign.Param;
@@ -9,11 +9,10 @@ import feign.RequestLine;
9 9
 @Headers("Accept: application/json")
10 10
 public interface AcproClient
11 11
 {
12
-    @RequestLine("GET /api_key?user={user}&password={password}&expires_after_seconds={expires_after_seconds}")
13
-    String getApiKey(@Param("user") String user, @Param("password") String password,
14
-        @Param("expires_after_seconds") Integer timeToLive);
12
+    @RequestLine("GET /api_key?user={user}&password={password}")
13
+    String getApiKey(@Param("user") String user, @Param("password") String password);
15 14
 
16
-    @RequestLine("GET /items?q=title:{title}&start={offset}&count={limit}&api_key={apiKey}")
17
-    Items getItems(@Param("title") String titleContains, @Param("offset") Integer offset,
18
-        @Param("limit") Integer limit, @Param("apiKey") String apiKey);
15
+    @RequestLine("GET /items?q=title:{title}&start={offset}&count={limit}")
16
+    ItemsContainer getItems(@Param("title") String titleContains, @Param("offset") Integer offset,
17
+        @Param("limit") Integer limit);
19 18
 }

+ 107 - 9
src/main/java/com/armstrongconsulting/acprotasks/client/AcproConnector.java

@@ -1,30 +1,128 @@
1 1
 package com.armstrongconsulting.acprotasks.client;
2 2
 
3
-import com.sun.istack.NotNull;
3
+import java.nio.charset.StandardCharsets;
4
+import java.util.Date;
5
+
6
+import org.apache.commons.io.IOUtils;
7
+import org.apache.commons.lang.StringUtils;
8
+import org.jetbrains.annotations.NotNull;
9
+
10
+import com.armstrongconsulting.acprotasks.AcproRepository;
4 11
 
5 12
 import feign.Feign;
6 13
 import feign.Logger;
14
+import feign.Response;
15
+import feign.RetryableException;
16
+import feign.Retryer;
17
+import feign.codec.ErrorDecoder;
7 18
 import feign.httpclient.ApacheHttpClient;
8
-import feign.jackson.jaxb.JacksonJaxbJsonDecoder;
9
-import feign.jackson.jaxb.JacksonJaxbJsonEncoder;
19
+import feign.jackson.JacksonDecoder;
20
+import feign.jackson.JacksonEncoder;
21
+import lombok.extern.slf4j.Slf4j;
10 22
 
11 23
 public class AcproConnector
12 24
 {
13 25
     private static AcproClient client;
14 26
 
15
-    public static AcproClient getClient(@NotNull String url)
27
+    public static AcproClient getClient(@NotNull AcproRepository repository)
16 28
     {
17 29
         if (client != null)
18 30
         {
19 31
             return client;
20 32
         }
21 33
 
22
-        return Feign.builder()
34
+        client = Feign.builder()
23 35
             .client(new ApacheHttpClient())
24
-            .encoder(new JacksonJaxbJsonEncoder())
25
-            .decoder(new JacksonJaxbJsonDecoder())
26
-            .logger(new Logger.JavaLogger(AcproClient.class))
36
+            .encoder(new JacksonEncoder())
37
+            .decoder((response, type) -> {
38
+                if (type.equals(String.class))
39
+                {
40
+                    return IOUtils.toString(response.body().asInputStream(), StandardCharsets.UTF_8.name());
41
+                }
42
+                else
43
+                {
44
+                    return new JacksonDecoder().decode(response, type);
45
+                }
46
+            })
47
+            .logger(new Logger()
48
+            {
49
+                @Override
50
+                protected void log(String configKey, String format, Object... args)
51
+                {
52
+                    System.out.println(String.format(format, args));
53
+                }
54
+            })
27 55
             .logLevel(Logger.Level.FULL)
28
-            .target(AcproClient.class, url);
56
+            .requestInterceptor(
57
+                requestTemplate -> {
58
+                    if (!StringUtils.isBlank(repository.getApiKey()))
59
+                    {
60
+                        requestTemplate.query("api_key", repository.getApiKey());
61
+                    }
62
+                })
63
+            .errorDecoder(new CustomErrorDecoder())
64
+            .retryer(new ReauthenticateRetryer(repository))
65
+            .target(AcproClient.class, repository.getUrl());
66
+
67
+        return client;
68
+    }
69
+
70
+    private AcproConnector()
71
+    {
72
+    }
73
+
74
+    @Slf4j
75
+    private static class CustomErrorDecoder implements ErrorDecoder
76
+    {
77
+        @Override
78
+        public Exception decode(String methodKey, Response response)
79
+        {
80
+            if (response.status() == 401 && !response.request().url().contains("/api_key"))
81
+            {
82
+                log.warn("Login expired. Throwing retryable exception.");
83
+                throw new RetryableException(response.status(), response.reason(), response.request().httpMethod(),
84
+                    new Date(), response.request());
85
+            }
86
+            return new ErrorDecoder.Default().decode(methodKey, response);
87
+        }
88
+    }
89
+
90
+    @Slf4j
91
+    private static class ReauthenticateRetryer implements Retryer
92
+    {
93
+        private final AcproRepository repository;
94
+        private boolean retried;
95
+
96
+        ReauthenticateRetryer(AcproRepository repository)
97
+        {
98
+            this.repository = repository;
99
+            this.retried = false;
100
+        }
101
+
102
+        ReauthenticateRetryer(ReauthenticateRetryer other)
103
+        {
104
+            this.repository = other.repository;
105
+            this.retried = other.retried;
106
+        }
107
+
108
+        @Override
109
+        public void continueOrPropagate(RetryableException e)
110
+        {
111
+            log.warn("Retrying");
112
+
113
+            if (retried)
114
+            {
115
+                throw e;
116
+            }
117
+
118
+            repository.setApiKey(client.getApiKey(repository.getUsername(), repository.getPassword()));
119
+            this.retried = true;
120
+        }
121
+
122
+        @Override
123
+        public Retryer clone()
124
+        {
125
+            return new ReauthenticateRetryer(this);
126
+        }
29 127
     }
30 128
 }

+ 4 - 1
src/main/java/com/armstrongconsulting/acprotasks/client/model/Item.java

@@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonInclude;
4 4
 import com.fasterxml.jackson.annotation.JsonProperty;
5 5
 import com.fasterxml.jackson.annotation.JsonPropertyOrder;
6 6
 
7
+import lombok.ToString;
8
+
7 9
 @JsonInclude(JsonInclude.Include.NON_NULL)
8 10
 @JsonPropertyOrder({
9 11
     "actualHours",
@@ -26,6 +28,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
26 28
     "unreadByAssignee",
27 29
     "urgency"
28 30
 })
31
+@ToString
29 32
 public class Item
30 33
 {
31 34
     @JsonProperty("actualHours")
@@ -294,4 +297,4 @@ public class Item
294 297
     {
295 298
         this.urgency = urgency;
296 299
     }
297
-}
300
+}

+ 3 - 0
src/main/java/com/armstrongconsulting/acprotasks/client/model/Items.java

@@ -6,6 +6,8 @@ import com.fasterxml.jackson.annotation.JsonInclude;
6 6
 import com.fasterxml.jackson.annotation.JsonProperty;
7 7
 import com.fasterxml.jackson.annotation.JsonPropertyOrder;
8 8
 
9
+import lombok.ToString;
10
+
9 11
 @JsonInclude(JsonInclude.Include.NON_NULL)
10 12
 @JsonPropertyOrder({
11 13
     "@rows",
@@ -14,6 +16,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
14 16
     "@totalNumRows",
15 17
     "item"
16 18
 })
19
+@ToString
17 20
 public class Items
18 21
 {
19 22
     @JsonProperty("@rows")

+ 30 - 0
src/main/java/com/armstrongconsulting/acprotasks/client/model/ItemsContainer.java

@@ -0,0 +1,30 @@
1
+package com.armstrongconsulting.acprotasks.client.model;
2
+
3
+import com.fasterxml.jackson.annotation.JsonInclude;
4
+import com.fasterxml.jackson.annotation.JsonProperty;
5
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
6
+
7
+import lombok.ToString;
8
+
9
+@JsonInclude(JsonInclude.Include.NON_NULL)
10
+@JsonPropertyOrder({
11
+    "items"
12
+})
13
+@ToString
14
+public class ItemsContainer
15
+{
16
+    @JsonProperty("items")
17
+    private Items items;
18
+
19
+    @JsonProperty("items")
20
+    public Items getItems()
21
+    {
22
+        return items;
23
+    }
24
+
25
+    @JsonProperty("items")
26
+    public void setItems(Items items)
27
+    {
28
+        this.items = items;
29
+    }
30
+}