embulk-output-mailchimp 0.3.20 → 0.3.21

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c279827f48671c120b7574d8de2c05669014e4fe
4
- data.tar.gz: 96c677aa514f29b5588bc0d901383cda9a84b7c4
3
+ metadata.gz: 504301e8e2482b1d046fb9a880ab63a631dac2ca
4
+ data.tar.gz: 053d271506d3a0dcb9020466ed703ccfe4d17469
5
5
  SHA512:
6
- metadata.gz: ea72fabb9264611f2146d3c64e540f7179ce76298701b29bb830417fb25dfede981353c433fc8731cc81c1b0415aa6ad8dda18bc3cab30602419f7001237b508
7
- data.tar.gz: 1c939eb9fd3558cefa4234d07961b7c25e5aa943f1f6cf59519e8191a7abda2376d72f3453e9d71f0ff79ac7baae27af7ee87c4bac434d2ddd850e17d1c309a4
6
+ metadata.gz: c08f3cfe6e2de7f6c2c35b04dac4f50b99ab1457c285e7ba42d8c6aed854d5922e640b3e1c089039f8453048b22996bd52e0a1fb689e9b7af8cab4e5fd50954c
7
+ data.tar.gz: d5dca1a6c14f1c992fe0f3e34a987052e6aa0ee8682bfa6114c0d510fcb790e5945e9d8e46091b7184afab7279143f08fb7fb5f9690724d612d9ec803f0908ea
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ ## 0.3.21 - 2018-03-02
2
+ - Refactor code to improve performance and fix NPE with merge fields that contain case sensitive [#40](https://github.com/treasure-data/embulk-output-mailchimp/pull/40)
3
+
1
4
  ## 0.3.20 - 2017-10-16
2
5
  - Added pagination for interest categories [#39](https://github.com/treasure-data/embulk-output-mailchimp/pull/39)
3
6
  - Refactor check list id to avoid confusing when get 404 error [#38](https://github.com/treasure-data/embulk-output-mailchimp/pull/38)
data/README.md CHANGED
@@ -26,6 +26,7 @@ add e-mail to List in MailChimp.
26
26
  - **language_column**: column name for language (string, optional, default: nil)
27
27
  - **double_optin**: control whether to send an opt-in confirmation email (boolean, default: true)
28
28
  - **max_records_per_request**: The max records per batch request. MailChimp API enables max records is 500 per batch request (int, default: 500)
29
+ - **sleep_between_requests_millis**: The time to sleep between requests to avoid flood MailChimp API (int, default: 3000)
29
30
 
30
31
  ## Example
31
32
 
@@ -51,5 +52,5 @@ out:
51
52
  ## Build
52
53
 
53
54
  ```
54
- $ rake
55
+ $ ./gradlew gem
55
56
  ```
data/build.gradle CHANGED
@@ -18,22 +18,21 @@ configurations {
18
18
  provided
19
19
  }
20
20
 
21
- version = "0.3.20"
21
+ version = "0.3.21"
22
22
 
23
23
  sourceCompatibility = 1.7
24
24
  targetCompatibility = 1.7
25
25
 
26
26
  dependencies {
27
- compile "org.embulk:embulk-core:0.8.15"
28
- provided "org.embulk:embulk-core:0.8.15"
27
+ compile "org.embulk:embulk-core:0.8.25"
28
+ provided "org.embulk:embulk-core:0.8.25"
29
29
  compile "org.embulk.base.restclient:embulk-base-restclient:0.5.3"
30
30
  compile "org.embulk.base.restclient:embulk-util-retryhelper-jetty92:0.5.3"
31
- // compile "YOUR_JAR_DEPENDENCY_GROUP:YOUR_JAR_DEPENDENCY_MODULE:YOUR_JAR_DEPENDENCY_VERSION"
32
31
 
33
32
  testCompile "junit:junit:4.+"
34
- testCompile "org.embulk:embulk-core:0.8.15:tests"
35
- testCompile "org.embulk:embulk-test:0.8.15"
36
- testCompile "org.embulk:embulk-standards:0.8.15"
33
+ testCompile "org.embulk:embulk-core:0.8.25:tests"
34
+ testCompile "org.embulk:embulk-test:0.8.25"
35
+ testCompile "org.embulk:embulk-standards:0.8.25"
37
36
  testCompile "org.mockito:mockito-core:2.+"
38
37
  }
39
38
 
@@ -5,18 +5,17 @@ import com.fasterxml.jackson.core.JsonProcessingException;
5
5
  import com.fasterxml.jackson.databind.DeserializationFeature;
6
6
  import com.fasterxml.jackson.databind.JsonNode;
7
7
  import com.fasterxml.jackson.databind.ObjectMapper;
8
- import com.fasterxml.jackson.databind.node.MissingNode;
9
8
  import com.fasterxml.jackson.databind.node.ObjectNode;
10
9
  import com.google.common.base.Function;
11
- import com.google.common.base.Joiner;
12
10
  import com.google.common.collect.FluentIterable;
13
11
  import com.google.common.collect.ImmutableList;
14
12
  import com.google.common.collect.Maps;
15
13
  import org.eclipse.jetty.client.HttpResponseException;
16
- import org.eclipse.jetty.http.HttpMethod;
14
+ import org.embulk.base.restclient.jackson.StringJsonParser;
17
15
  import org.embulk.config.ConfigException;
18
16
  import org.embulk.output.mailchimp.MailChimpOutputPluginDelegate.PluginTask;
19
17
  import org.embulk.output.mailchimp.helper.MailChimpHelper;
18
+ import org.embulk.output.mailchimp.helper.MailChimpRetryable;
20
19
  import org.embulk.output.mailchimp.model.CategoriesResponse;
21
20
  import org.embulk.output.mailchimp.model.ErrorResponse;
22
21
  import org.embulk.output.mailchimp.model.InterestCategoriesResponse;
@@ -24,7 +23,6 @@ import org.embulk.output.mailchimp.model.InterestResponse;
24
23
  import org.embulk.output.mailchimp.model.InterestsResponse;
25
24
  import org.embulk.output.mailchimp.model.MergeField;
26
25
  import org.embulk.output.mailchimp.model.MergeFields;
27
- import org.embulk.output.mailchimp.model.MetaDataResponse;
28
26
  import org.embulk.output.mailchimp.model.ReportResponse;
29
27
  import org.embulk.spi.DataException;
30
28
  import org.embulk.spi.Exec;
@@ -38,19 +36,16 @@ import java.util.HashMap;
38
36
  import java.util.List;
39
37
  import java.util.Map;
40
38
 
41
- import static org.embulk.output.mailchimp.model.AuthMethod.API_KEY;
42
- import static org.embulk.output.mailchimp.model.AuthMethod.OAUTH;
43
-
44
39
  /**
45
40
  * Created by thangnc on 4/25/17.
46
41
  */
47
42
  public class MailChimpClient
48
43
  {
49
44
  private static final Logger LOG = Exec.getLogger(MailChimpClient.class);
50
- private static final String API_VERSION = "3.0";
51
- private static String mailchimpEndpoint;
52
- private MailChimpHttpClient client;
53
- private final ObjectMapper mapper;
45
+ private final ObjectMapper mapper = new ObjectMapper()
46
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
47
+ .configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, false);
48
+ private StringJsonParser jsonParser = new StringJsonParser();
54
49
 
55
50
  /**
56
51
  * Instantiates a new Mail chimp client.
@@ -59,12 +54,6 @@ public class MailChimpClient
59
54
  */
60
55
  public MailChimpClient(final PluginTask task)
61
56
  {
62
- mailchimpEndpoint = Joiner.on("/").join("https://{0}.api.mailchimp.com", API_VERSION);
63
- this.client = new MailChimpHttpClient(task);
64
- this.mapper = new ObjectMapper()
65
- .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
66
- .configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, false);
67
- extractDataCenter(task);
68
57
  findList(task);
69
58
  }
70
59
 
@@ -75,24 +64,19 @@ public class MailChimpClient
75
64
  * @param node the data
76
65
  * @param task the task
77
66
  * @return the report response
78
- * @throws JsonProcessingException the json processing exception
79
67
  */
80
- ReportResponse push(final ObjectNode node, PluginTask task)
81
- throws JsonProcessingException
68
+ public ReportResponse push(final ObjectNode node, PluginTask task) throws JsonProcessingException
82
69
  {
83
- String endpoint = MessageFormat.format(mailchimpEndpoint + "/lists/{0}",
84
- task.getListId());
85
-
86
- JsonNode response = client.sendRequest(endpoint, HttpMethod.POST, node.toString(), task);
87
- client.avoidFlushAPI("Process next request");
70
+ try (MailChimpRetryable retryable = new MailChimpRetryable(task)) {
71
+ String response = retryable.post(MessageFormat.format("/lists/{0}", task.getListId()),
72
+ "application/json;utf-8",
73
+ node.toString());
74
+ if (response != null && !response.isEmpty()) {
75
+ return mapper.treeToValue(jsonParser.parseJsonObject(response), ReportResponse.class);
76
+ }
88
77
 
89
- if (response instanceof MissingNode) {
90
- ReportResponse reportResponse = new ReportResponse();
91
- reportResponse.setErrors(new ArrayList<ErrorResponse>());
92
- return reportResponse;
78
+ throw new DataException("The json data in response were broken.");
93
79
  }
94
-
95
- return mapper.treeToValue(response, ReportResponse.class);
96
80
  }
97
81
 
98
82
  /**
@@ -100,7 +84,7 @@ public class MailChimpClient
100
84
  *
101
85
  * @param errorResponses the error responses
102
86
  */
103
- void handleErrors(List<ErrorResponse> errorResponses)
87
+ public void handleErrors(List<ErrorResponse> errorResponses)
104
88
  {
105
89
  if (!errorResponses.isEmpty()) {
106
90
  StringBuilder errorMessage = new StringBuilder();
@@ -125,76 +109,77 @@ public class MailChimpClient
125
109
  * @return the map
126
110
  * @throws JsonProcessingException the json processing exception
127
111
  */
128
- Map<String, Map<String, InterestResponse>> extractInterestCategoriesByGroupNames(final PluginTask task)
112
+ public Map<String, Map<String, InterestResponse>> extractInterestCategoriesByGroupNames(final PluginTask task)
129
113
  throws JsonProcessingException
130
114
  {
131
- Map<String, Map<String, InterestResponse>> categories = new HashMap<>();
132
- if (task.getGroupingColumns().isPresent() && !task.getGroupingColumns().get().isEmpty()) {
133
- List<String> interestCategoryNames = task.getGroupingColumns().get();
134
-
135
- int count = 100;
136
- int offset = 0;
137
- int page = 1;
138
- boolean hasMore = true;
139
- JsonNode response;
140
- List<CategoriesResponse> allCategoriesResponse = new ArrayList<>();
141
-
142
- while (hasMore) {
143
- String endpoint = MessageFormat.format(mailchimpEndpoint + "/lists/{0}/interest-categories?count={1}&offset={2}",
115
+ try (MailChimpRetryable retryable = new MailChimpRetryable(task)) {
116
+ Map<String, Map<String, InterestResponse>> categories = new HashMap<>();
117
+ if (task.getGroupingColumns().isPresent() && !task.getGroupingColumns().get().isEmpty()) {
118
+ List<String> interestCategoryNames = task.getGroupingColumns().get();
119
+
120
+ int count = 100;
121
+ int offset = 0;
122
+ int page = 1;
123
+ boolean hasMore = true;
124
+ JsonNode response;
125
+ List<CategoriesResponse> allCategoriesResponse = new ArrayList<>();
126
+
127
+ while (hasMore) {
128
+ String path = MessageFormat.format("/lists/{0}/interest-categories?count={1}&offset={2}",
144
129
  task.getListId(),
145
130
  count,
146
131
  offset);
147
-
148
- response = client.sendRequest(endpoint, HttpMethod.GET, task);
149
- InterestCategoriesResponse interestCategoriesResponse = mapper.treeToValue(response,
150
- InterestCategoriesResponse.class);
151
-
152
- allCategoriesResponse.addAll(interestCategoriesResponse.getCategories());
153
- if (hasMorePage(interestCategoriesResponse.getTotalItems(), count, page)) {
154
- offset = count;
155
- page++;
156
- }
157
- else {
158
- hasMore = false;
132
+ response = jsonParser.parseJsonObject(retryable.get(path));
133
+ InterestCategoriesResponse interestCategoriesResponse = mapper.treeToValue(response,
134
+ InterestCategoriesResponse.class);
135
+
136
+ allCategoriesResponse.addAll(interestCategoriesResponse.getCategories());
137
+ if (hasMorePage(interestCategoriesResponse.getTotalItems(), count, page)) {
138
+ offset = count;
139
+ page++;
140
+ }
141
+ else {
142
+ hasMore = false;
143
+ }
159
144
  }
160
- }
161
145
 
162
- Function<CategoriesResponse, String> function = new Function<CategoriesResponse, String>()
163
- {
164
- @Override
165
- public String apply(CategoriesResponse input)
146
+ Function<CategoriesResponse, String> function = new Function<CategoriesResponse, String>()
166
147
  {
167
- return input.getTitle().toLowerCase();
148
+ @Override
149
+ public String apply(CategoriesResponse input)
150
+ {
151
+ return input.getTitle().toLowerCase();
152
+ }
153
+ };
154
+
155
+ // Transform to a list of available category names and validate with data that user input
156
+ ImmutableList<String> availableCategories = FluentIterable
157
+ .from(allCategoriesResponse)
158
+ .transform(function)
159
+ .toList();
160
+
161
+ for (String category : interestCategoryNames) {
162
+ if (!availableCategories.contains(category)) {
163
+ throw new ConfigException("Invalid interest category name: '" + category + "'");
164
+ }
168
165
  }
169
- };
170
-
171
- // Transform to a list of available category names and validate with data that user input
172
- ImmutableList<String> availableCategories = FluentIterable
173
- .from(allCategoriesResponse)
174
- .transform(function)
175
- .toList();
176
166
 
177
- for (String category : interestCategoryNames) {
178
- if (!availableCategories.contains(category)) {
179
- throw new ConfigException("Invalid interest category name: '" + category + "'");
180
- }
181
- }
182
-
183
- for (CategoriesResponse categoriesResponse : allCategoriesResponse) {
184
- String detailEndpoint = MessageFormat.format(mailchimpEndpoint + "/lists/{0}/interest-categories/{1}/interests",
167
+ for (CategoriesResponse categoriesResponse : allCategoriesResponse) {
168
+ String detailPath = MessageFormat.format("/lists/{0}/interest-categories/{1}/interests",
185
169
  task.getListId(),
186
170
  categoriesResponse.getId());
187
- response = client.sendRequest(detailEndpoint, HttpMethod.GET, task);
171
+ response = jsonParser.parseJsonObject(retryable.get(detailPath));
188
172
 
189
- // Avoid flush MailChimp API
190
- client.avoidFlushAPI("Fetching next category's interests");
191
- InterestsResponse interestsResponse = mapper.treeToValue(response, InterestsResponse.class);
192
- categories.put(categoriesResponse.getTitle().toLowerCase(),
193
- convertInterestCategoryToMap(interestsResponse.getInterests()));
173
+ // Avoid flood MailChimp API
174
+ avoidFloodAPI("Fetching next category's interests", task.getSleepBetweenRequestsMillis());
175
+ InterestsResponse interestsResponse = mapper.treeToValue(response, InterestsResponse.class);
176
+ categories.put(categoriesResponse.getTitle().toLowerCase(),
177
+ convertInterestCategoryToMap(interestsResponse.getInterests()));
178
+ }
194
179
  }
195
- }
196
180
 
197
- return categories;
181
+ return categories;
182
+ }
198
183
  }
199
184
 
200
185
  /**
@@ -205,76 +190,45 @@ public class MailChimpClient
205
190
  * @return the map
206
191
  * @throws JsonProcessingException the json processing exception
207
192
  */
208
- Map<String, MergeField> extractMergeFieldsFromList(PluginTask task) throws JsonProcessingException
193
+ public Map<String, MergeField> extractMergeFieldsFromList(PluginTask task) throws JsonProcessingException
209
194
  {
210
- int count = 100;
211
- int offset = 0;
212
- int page = 1;
213
- boolean hasMore = true;
214
- List<MergeField> allMergeFields = new ArrayList<>();
215
-
216
- while (hasMore) {
217
- String endpoint = MessageFormat.format(mailchimpEndpoint + "/lists/{0}/merge-fields?count={1}&offset={2}",
195
+ try (MailChimpRetryable retryable = new MailChimpRetryable(task)) {
196
+ int count = 100;
197
+ int offset = 0;
198
+ int page = 1;
199
+ boolean hasMore = true;
200
+ List<MergeField> allMergeFields = new ArrayList<>();
201
+
202
+ while (hasMore) {
203
+ String path = MessageFormat.format("/lists/{0}/merge-fields?count={1}&offset={2}",
218
204
  task.getListId(),
219
205
  count,
220
206
  offset);
221
207
 
222
- JsonNode response = client.sendRequest(endpoint, HttpMethod.GET, task);
223
- MergeFields mergeFields = mapper.treeToValue(response,
224
- MergeFields.class);
208
+ JsonNode response = jsonParser.parseJsonObject(retryable.get(path));
209
+ MergeFields mergeFields = mapper.treeToValue(response,
210
+ MergeFields.class);
225
211
 
226
- allMergeFields.addAll(mergeFields.getMergeFields());
212
+ allMergeFields.addAll(mergeFields.getMergeFields());
227
213
 
228
- if (hasMorePage(mergeFields.getTotalItems(), count, page)) {
229
- offset = count;
230
- page++;
231
- }
232
- else {
233
- hasMore = false;
234
- }
235
- }
236
-
237
- return convertMergeFieldToMap(allMergeFields);
238
- }
239
-
240
- private void extractDataCenter(final PluginTask task)
241
- {
242
- if (task.getAuthMethod() == OAUTH) {
243
- // Extract data center from meta data URL
244
- JsonNode response = client.sendRequest("https://login.mailchimp.com/oauth2/metadata", HttpMethod.GET, task);
245
- MetaDataResponse metaDataResponse;
246
- try {
247
- metaDataResponse = mapper.treeToValue(response, MetaDataResponse.class);
248
- mailchimpEndpoint = MessageFormat.format(mailchimpEndpoint, metaDataResponse.getDc());
249
- }
250
- catch (JsonProcessingException e) {
251
- throw new DataException(e);
252
- }
253
- }
254
- else if (task.getAuthMethod() == API_KEY && task.getApikey().isPresent()) {
255
- // Authenticate and return data center
256
- if (!task.getApikey().get().contains("-")) {
257
- throw new ConfigException("API Key format invalid.");
214
+ if (hasMorePage(mergeFields.getTotalItems(), count, page)) {
215
+ offset = count;
216
+ page++;
217
+ }
218
+ else {
219
+ hasMore = false;
220
+ }
258
221
  }
259
222
 
260
- String domain = task.getApikey().get().split("-")[1];
261
- String endpoint = MessageFormat.format(mailchimpEndpoint + "/", domain);
262
- try {
263
- client.sendRequest(endpoint, HttpMethod.GET, task);
264
- mailchimpEndpoint = MessageFormat.format(mailchimpEndpoint, domain);
265
- }
266
- catch (HttpResponseException re) {
267
- throw new ConfigException("Your API key may be invalid, or you've attempted to access the wrong datacenter.");
268
- }
223
+ return convertMergeFieldToMap(allMergeFields);
269
224
  }
270
225
  }
271
226
 
272
227
  private void findList(final PluginTask task)
273
228
  {
274
- String endpoint = MessageFormat.format(mailchimpEndpoint + "/lists/{0}",
275
- task.getListId());
276
- try {
277
- client.sendRequest(endpoint, HttpMethod.GET, task);
229
+ try (MailChimpRetryable retryable = new MailChimpRetryable(task)) {
230
+ jsonParser.parseJsonObject(retryable.get(MessageFormat.format("/lists/{0}",
231
+ task.getListId())));
278
232
  }
279
233
  catch (HttpResponseException hre) {
280
234
  throw new ConfigException("The `list id` could not be found.");
@@ -319,4 +273,15 @@ public class MailChimpClient
319
273
  int totalPage = count / pageSize + (count % pageSize > 0 ? 1 : 0);
320
274
  return page < totalPage;
321
275
  }
276
+
277
+ public void avoidFloodAPI(final String reason, final long millis)
278
+ {
279
+ try {
280
+ LOG.info("{} in {}ms...", reason, millis);
281
+ Thread.sleep(millis);
282
+ }
283
+ catch (InterruptedException e) {
284
+ LOG.warn("Failed to sleep: {}", e.getMessage());
285
+ }
286
+ }
322
287
  }
@@ -107,6 +107,10 @@ public class MailChimpOutputPluginDelegate
107
107
  @Config("max_records_per_request")
108
108
  @ConfigDefault("500")
109
109
  int getMaxRecordsPerRequest();
110
+
111
+ @Config("sleep_between_requests_millis")
112
+ @ConfigDefault("3000")
113
+ int getSleepBetweenRequestsMillis();
110
114
  }
111
115
 
112
116
  /**
@@ -129,6 +133,10 @@ public class MailChimpOutputPluginDelegate
129
133
  if (!task.getApikey().isPresent() || isNullOrEmpty(task.getApikey().get())) {
130
134
  throw new ConfigException("'apikey' is required when auth_method is 'api_key'");
131
135
  }
136
+
137
+ if (!task.getApikey().get().contains("-")) {
138
+ throw new ConfigException("apikey's format invalid.");
139
+ }
132
140
  }
133
141
 
134
142
  if (isNullOrEmpty(task.getListId())) {
@@ -150,7 +150,7 @@ public class MailChimpRecordBuffer
150
150
  @Override
151
151
  public void close()
152
152
  {
153
- // Do not implement
153
+ // // Do not close here
154
154
  }
155
155
 
156
156
  /**
@@ -165,10 +165,14 @@ public class MailChimpRecordBuffer
165
165
  {
166
166
  // Should loop the names and get the id of interest categories.
167
167
  // The reason why we put categories validation here because we can not share data between instance.
168
- categories = mailChimpClient.extractInterestCategoriesByGroupNames(task);
168
+ if (categories == null) {
169
+ categories = mailChimpClient.extractInterestCategoriesByGroupNames(task);
170
+ }
169
171
 
170
172
  // Extract merge fields detail
171
- availableMergeFields = mailChimpClient.extractMergeFieldsFromList(task);
173
+ if (availableMergeFields == null) {
174
+ availableMergeFields = mailChimpClient.extractMergeFieldsFromList(task);
175
+ }
172
176
 
173
177
  // Required merge fields
174
178
  Map<String, String> map = new HashMap<>();
@@ -208,7 +212,7 @@ public class MailChimpRecordBuffer
208
212
  String value = input.hasNonNull(column.getName()) ? input.findValue(column.getName()).asText() : "";
209
213
 
210
214
  // Try to convert to Json from string with the merge field's type is address
211
- if (availableMergeFields.get(column.getName()).getType()
215
+ if (availableMergeFields.get(column.getName().toLowerCase()).getType()
212
216
  .equals(MergeField.MergeFieldType.ADDRESS.getType())) {
213
217
  JsonNode addressNode = toJsonNode(value);
214
218
  if (addressNode instanceof NullNode) {
@@ -243,31 +247,32 @@ public class MailChimpRecordBuffer
243
247
  };
244
248
  }
245
249
 
246
- private ObjectNode buildInterestCategories(final MailChimpOutputPluginDelegate.PluginTask task,
247
- final JsonNode input)
250
+ private ObjectNode buildInterestCategories(final PluginTask task, final JsonNode input)
248
251
  {
249
252
  ObjectNode interests = JsonNodeFactory.instance.objectNode();
250
253
 
251
- for (String category : task.getGroupingColumns().get()) {
252
- String inputValue = input.findValue(category).asText();
253
- List<String> interestValues = fromCommaSeparatedString(inputValue);
254
- Map<String, InterestResponse> availableCategories = categories.get(category);
255
-
256
- // Only update user-predefined categories if replace interests != true
257
- if (!task.getReplaceInterests()) {
258
- for (String interestValue : interestValues) {
259
- if (availableCategories.get(interestValue) != null) {
260
- interests.put(availableCategories.get(interestValue).getId(), true);
261
- }
262
- }
263
- } // Otherwise, force update all categories include user-predefined categories
264
- else if (task.getReplaceInterests()) {
265
- for (String availableCategory : availableCategories.keySet()) {
266
- if (interestValues.contains(availableCategory)) {
267
- interests.put(availableCategories.get(availableCategory).getId(), true);
254
+ if (task.getGroupingColumns().isPresent()) {
255
+ for (String category : task.getGroupingColumns().get()) {
256
+ String inputValue = input.findValue(category).asText();
257
+ List<String> interestValues = fromCommaSeparatedString(inputValue);
258
+ Map<String, InterestResponse> availableCategories = categories.get(category);
259
+
260
+ // Only update user-predefined categories if replace interests != true
261
+ if (!task.getReplaceInterests()) {
262
+ for (String interestValue : interestValues) {
263
+ if (availableCategories.get(interestValue) != null) {
264
+ interests.put(availableCategories.get(interestValue).getId(), true);
265
+ }
268
266
  }
269
- else {
270
- interests.put(availableCategories.get(availableCategory).getId(), false);
267
+ } // Otherwise, force update all categories include user-predefined categories
268
+ else if (task.getReplaceInterests()) {
269
+ for (String availableCategory : availableCategories.keySet()) {
270
+ if (interestValues.contains(availableCategory)) {
271
+ interests.put(availableCategories.get(availableCategory).getId(), true);
272
+ }
273
+ else {
274
+ interests.put(availableCategories.get(availableCategory).getId(), false);
275
+ }
271
276
  }
272
277
  }
273
278
  }
@@ -302,6 +307,8 @@ public class MailChimpRecordBuffer
302
307
  reportResponse.getErrorCount(), System.currentTimeMillis() - startTime);
303
308
  mailChimpClient.handleErrors(reportResponse.getErrors());
304
309
 
310
+ mailChimpClient.avoidFloodAPI("Process next request", task.getSleepBetweenRequestsMillis());
311
+
305
312
  if (duplicatedRecords.size() > 0) {
306
313
  LOG.info("Start to process {} duplicated record(s)", duplicatedRecords.size());
307
314
  for (JsonNode duplicatedRecord : duplicatedRecords) {
@@ -0,0 +1,224 @@
1
+ package org.embulk.output.mailchimp.helper;
2
+
3
+ import com.fasterxml.jackson.core.Base64Variants;
4
+ import com.fasterxml.jackson.databind.node.ObjectNode;
5
+ import com.google.common.base.Charsets;
6
+ import com.google.common.base.Joiner;
7
+ import org.eclipse.jetty.client.HttpClient;
8
+ import org.eclipse.jetty.client.HttpResponseException;
9
+ import org.eclipse.jetty.client.api.ContentResponse;
10
+ import org.eclipse.jetty.client.api.Request;
11
+ import org.eclipse.jetty.client.api.Response;
12
+ import org.eclipse.jetty.client.util.StringContentProvider;
13
+ import org.embulk.base.restclient.jackson.StringJsonParser;
14
+ import org.embulk.config.ConfigException;
15
+ import org.embulk.output.mailchimp.MailChimpOutputPluginDelegate.PluginTask;
16
+ import org.embulk.spi.Exec;
17
+ import org.embulk.util.retryhelper.jetty92.DefaultJetty92ClientCreator;
18
+ import org.embulk.util.retryhelper.jetty92.Jetty92RetryHelper;
19
+ import org.embulk.util.retryhelper.jetty92.Jetty92SingleRequester;
20
+ import org.embulk.util.retryhelper.jetty92.StringJetty92ResponseEntityReader;
21
+ import org.slf4j.Logger;
22
+
23
+ import java.text.MessageFormat;
24
+ import java.util.concurrent.ExecutionException;
25
+ import java.util.concurrent.TimeoutException;
26
+
27
+ import static org.eclipse.jetty.http.HttpHeader.AUTHORIZATION;
28
+ import static org.eclipse.jetty.http.HttpMethod.GET;
29
+ import static org.eclipse.jetty.http.HttpMethod.POST;
30
+ import static org.embulk.output.mailchimp.model.AuthMethod.API_KEY;
31
+ import static org.embulk.output.mailchimp.model.AuthMethod.OAUTH;
32
+
33
+ public class MailChimpRetryable implements AutoCloseable
34
+ {
35
+ private static final Logger LOG = Exec.getLogger(MailChimpRetryable.class);
36
+ private static final int READER_TIMEOUT_MILLIS = 300000;
37
+ private static final String API_VERSION = "3.0";
38
+ private final Jetty92RetryHelper retryHelper;
39
+ private final PluginTask pluginTask;
40
+ private static TokenHolder tokenHolder;
41
+ protected StringJsonParser jsonParser = new StringJsonParser();
42
+ private String authorizationHeader;
43
+
44
+ public MailChimpRetryable(final PluginTask pluginTask)
45
+ {
46
+ this.retryHelper = new Jetty92RetryHelper(pluginTask.getMaximumRetries(),
47
+ pluginTask.getInitialRetryIntervalMillis(),
48
+ pluginTask.getMaximumRetryIntervalMillis(),
49
+ new DefaultJetty92ClientCreator(pluginTask.getTimeoutMillis(),
50
+ pluginTask.getTimeoutMillis()));
51
+ this.pluginTask = pluginTask;
52
+ authorizationHeader = buildAuthorizationHeader(pluginTask);
53
+ }
54
+
55
+ public String get(final String path)
56
+ {
57
+ return sendRequest(path, null);
58
+ }
59
+
60
+ public String post(final String path, String contentType, String body)
61
+ {
62
+ return sendRequest(path, new StringContentProvider(contentType, body, Charsets.UTF_8));
63
+ }
64
+
65
+ private String sendRequest(final String path, final StringContentProvider contentProvider)
66
+ {
67
+ return retryHelper.requestWithRetry(
68
+ new StringJetty92ResponseEntityReader(READER_TIMEOUT_MILLIS),
69
+ new Jetty92SingleRequester()
70
+ {
71
+ @Override
72
+ public void requestOnce(HttpClient client, Response.Listener responseListener)
73
+ {
74
+ createTokenHolder(client);
75
+ Request request = client.newRequest(tokenHolder.getEndpoint() + path)
76
+ .header(AUTHORIZATION, authorizationHeader)
77
+ .method(GET);
78
+ if (contentProvider != null) {
79
+ request = request.method(POST).content(contentProvider);
80
+ }
81
+ request.send(responseListener);
82
+ }
83
+
84
+ @Override
85
+ protected boolean isResponseStatusToRetry(Response response)
86
+ {
87
+ // Retry if it's a server or rate limit exceeded error
88
+ return (response.getStatus() != 500 && response.getStatus() / 100 != 4) || response.getStatus() == 429;
89
+ }
90
+
91
+ @Override
92
+ protected boolean isExceptionToRetry(Exception exception)
93
+ {
94
+ // This check is to make sure if the original exception is retryable, i.e.
95
+ // server not found, internal server error...
96
+ if (exception instanceof ConfigException || exception instanceof ExecutionException) {
97
+ return toRetry((Exception) exception.getCause());
98
+ }
99
+ return exception instanceof TimeoutException || super.isExceptionToRetry(exception);
100
+ }
101
+ });
102
+ }
103
+
104
+ @Override
105
+ public void close()
106
+ {
107
+ if (retryHelper != null) {
108
+ retryHelper.close();
109
+ }
110
+ }
111
+
112
+ /**
113
+ * MailChimp API v3 supports non expires access_token. Then no need refresh_token
114
+ *
115
+ * @param task
116
+ * @return
117
+ */
118
+ private String buildAuthorizationHeader(final PluginTask task)
119
+ {
120
+ switch (task.getAuthMethod()) {
121
+ case OAUTH:
122
+ return "OAuth " + task.getAccessToken().orNull();
123
+ case API_KEY:
124
+ return "Basic " + Base64Variants.MIME_NO_LINEFEEDS
125
+ .encode(("apikey" + ":" + task.getApikey().orNull()).getBytes());
126
+ default:
127
+ throw new ConfigException("Not supported method");
128
+ }
129
+ }
130
+
131
+ private TokenHolder createTokenHolder(final HttpClient client)
132
+ {
133
+ if (tokenHolder != null) {
134
+ return tokenHolder;
135
+ }
136
+
137
+ LOG.info("Create new token holder and extract data center");
138
+
139
+ if (pluginTask.getAuthMethod() == OAUTH) {
140
+ try {
141
+ // Extract data center from meta data URL
142
+ ContentResponse contentResponse = client.newRequest("https://login.mailchimp.com/oauth2/metadata")
143
+ .method(GET)
144
+ .header("Authorization", authorizationHeader)
145
+ .send();
146
+
147
+ if (contentResponse.getStatus() == 200) {
148
+ ObjectNode objectNode = jsonParser.parseJsonObject(contentResponse.getContentAsString());
149
+ String endpoint = MessageFormat.format(Joiner.on("/").join("https://{0}.api.mailchimp.com", API_VERSION),
150
+ objectNode.get("dc").asText());
151
+ tokenHolder = new TokenHolder(pluginTask.getAccessToken().orNull(), null, endpoint);
152
+ return tokenHolder;
153
+ }
154
+
155
+ String message = String.format("%s %d %s",
156
+ contentResponse.getVersion(),
157
+ contentResponse.getStatus(),
158
+ contentResponse.getReason());
159
+ throw new HttpResponseException(message, contentResponse);
160
+ }
161
+ catch (Exception ex) {
162
+ throw new ConfigException("Unable to connect the data center", ex);
163
+ }
164
+ }
165
+ else if (pluginTask.getAuthMethod() == API_KEY) {
166
+ try {
167
+ // Authenticate and return data center
168
+ String domain = pluginTask.getApikey().get().split("-")[1];
169
+ String endpoint = MessageFormat.format(Joiner.on("/").join("https://{0}.api.mailchimp.com", API_VERSION),
170
+ domain);
171
+ ContentResponse contentResponse = client.newRequest(endpoint + "/")
172
+ .method(GET)
173
+ .header("Authorization", "Basic " + Base64Variants.MIME_NO_LINEFEEDS
174
+ .encode(("apikey" + ":" + pluginTask.getApikey().get()).getBytes()))
175
+ .send();
176
+
177
+ if (contentResponse.getStatus() == 200) {
178
+ tokenHolder = new TokenHolder(null, pluginTask.getApikey().orNull(), endpoint);
179
+ return tokenHolder;
180
+ }
181
+
182
+ String message = String.format("%s %d %s",
183
+ contentResponse.getVersion(),
184
+ contentResponse.getStatus(),
185
+ contentResponse.getReason());
186
+ throw new HttpResponseException(message, contentResponse);
187
+ }
188
+ catch (Exception ex) {
189
+ throw new ConfigException("Your API key may be invalid, or you've attempted to access the wrong datacenter.");
190
+ }
191
+ }
192
+
193
+ throw new ConfigException("Not supported auth method");
194
+ }
195
+ }
196
+
197
+ class TokenHolder
198
+ {
199
+ private String accessToken;
200
+ private String apiKey;
201
+ private String endpoint;
202
+
203
+ public TokenHolder(final String accessToken, final String apiKey, final String endpoint)
204
+ {
205
+ this.accessToken = accessToken;
206
+ this.apiKey = apiKey;
207
+ this.endpoint = endpoint;
208
+ }
209
+
210
+ public String getAccessToken()
211
+ {
212
+ return accessToken;
213
+ }
214
+
215
+ public String getApiKey()
216
+ {
217
+ return apiKey;
218
+ }
219
+
220
+ public String getEndpoint()
221
+ {
222
+ return endpoint;
223
+ }
224
+ }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: embulk-output-mailchimp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.20
4
+ version: 0.3.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thang Nguyen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-10-16 00:00:00.000000000 Z
11
+ date: 2018-03-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -62,11 +62,11 @@ files:
62
62
  - gradlew.bat
63
63
  - lib/embulk/output/mailchimp.rb
64
64
  - src/main/java/org/embulk/output/mailchimp/MailChimpClient.java
65
- - src/main/java/org/embulk/output/mailchimp/MailChimpHttpClient.java
66
65
  - src/main/java/org/embulk/output/mailchimp/MailChimpOutputPlugin.java
67
66
  - src/main/java/org/embulk/output/mailchimp/MailChimpOutputPluginDelegate.java
68
67
  - src/main/java/org/embulk/output/mailchimp/MailChimpRecordBuffer.java
69
68
  - src/main/java/org/embulk/output/mailchimp/helper/MailChimpHelper.java
69
+ - src/main/java/org/embulk/output/mailchimp/helper/MailChimpRetryable.java
70
70
  - src/main/java/org/embulk/output/mailchimp/model/AddressMergeFieldAttribute.java
71
71
  - src/main/java/org/embulk/output/mailchimp/model/AuthMethod.java
72
72
  - src/main/java/org/embulk/output/mailchimp/model/CategoriesResponse.java
@@ -89,7 +89,7 @@ files:
89
89
  - test/override_assert_raise.rb
90
90
  - test/run-test.rb
91
91
  - classpath/jetty-io-9.2.14.v20151106.jar
92
- - classpath/embulk-output-mailchimp-0.3.20.jar
92
+ - classpath/embulk-output-mailchimp-0.3.21.jar
93
93
  - classpath/jetty-util-9.2.14.v20151106.jar
94
94
  - classpath/jetty-http-9.2.14.v20151106.jar
95
95
  - classpath/jetty-client-9.2.14.v20151106.jar
@@ -1,200 +0,0 @@
1
- package org.embulk.output.mailchimp;
2
-
3
- import com.fasterxml.jackson.core.Base64Variants;
4
- import com.fasterxml.jackson.core.JsonParser;
5
- import com.fasterxml.jackson.databind.DeserializationFeature;
6
- import com.fasterxml.jackson.databind.JsonNode;
7
- import com.fasterxml.jackson.databind.ObjectMapper;
8
- import com.fasterxml.jackson.databind.node.MissingNode;
9
- import com.google.common.base.Throwables;
10
- import org.eclipse.jetty.client.HttpClient;
11
- import org.eclipse.jetty.client.api.Request;
12
- import org.eclipse.jetty.client.api.Response;
13
- import org.eclipse.jetty.client.util.StringContentProvider;
14
- import org.eclipse.jetty.http.HttpMethod;
15
- import org.eclipse.jetty.util.ssl.SslContextFactory;
16
- import org.embulk.config.ConfigException;
17
- import org.embulk.spi.DataException;
18
- import org.embulk.spi.Exec;
19
- import org.embulk.util.retryhelper.jetty92.Jetty92ClientCreator;
20
- import org.embulk.util.retryhelper.jetty92.Jetty92RetryHelper;
21
- import org.embulk.util.retryhelper.jetty92.Jetty92SingleRequester;
22
- import org.embulk.util.retryhelper.jetty92.StringJetty92ResponseEntityReader;
23
- import org.slf4j.Logger;
24
-
25
- import java.io.IOException;
26
- import java.util.concurrent.ExecutionException;
27
- import java.util.concurrent.TimeUnit;
28
- import java.util.concurrent.TimeoutException;
29
-
30
- /**
31
- * Created by thangnc on 4/14/17.
32
- */
33
- public class MailChimpHttpClient
34
- {
35
- private static final Logger LOG = Exec.getLogger(MailChimpHttpClient.class);
36
- private final ObjectMapper jsonMapper = new ObjectMapper()
37
- .configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, false)
38
- .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
39
-
40
- /**
41
- * Instantiates a new Mailchimp http client.
42
- *
43
- * @param task the task
44
- */
45
- public MailChimpHttpClient(MailChimpOutputPluginDelegate.PluginTask task)
46
- {
47
- }
48
-
49
- public JsonNode sendRequest(final String endpoint, final HttpMethod method,
50
- final MailChimpOutputPluginDelegate.PluginTask task)
51
- {
52
- return sendRequest(endpoint, method, "", task);
53
- }
54
-
55
- public JsonNode sendRequest(final String endpoint, final HttpMethod method, final String content,
56
- final MailChimpOutputPluginDelegate.PluginTask task)
57
- {
58
- try (final Jetty92RetryHelper retryHelper = createRetryHelper(task)) {
59
- final String authorizationHeader = getAuthorizationHeader(task);
60
-
61
- String responseBody = retryHelper.requestWithRetry(
62
- new StringJetty92ResponseEntityReader(task.getTimeoutMillis()),
63
- new Jetty92SingleRequester()
64
- {
65
- @Override
66
- public void requestOnce(HttpClient client, Response.Listener responseListener)
67
- {
68
- Request request = client
69
- .newRequest(endpoint)
70
- .timeout(task.getTimeoutMillis(), TimeUnit.MILLISECONDS)
71
- .accept("application/json")
72
- .method(method);
73
- if (method == HttpMethod.POST || method == HttpMethod.PUT) {
74
- request.content(new StringContentProvider(content), "application/json;utf-8");
75
- }
76
-
77
- if (!authorizationHeader.isEmpty()) {
78
- request.header("Authorization", authorizationHeader);
79
- }
80
- request.send(responseListener);
81
- }
82
-
83
- @Override
84
- public boolean isResponseStatusToRetry(Response response)
85
- {
86
- int status = response.getStatus();
87
-
88
- return status == 429 || status / 100 != 4;
89
- }
90
-
91
- @Override
92
- protected boolean isExceptionToRetry(Exception exception)
93
- {
94
- LOG.error("Exception to retry.", exception);
95
- // This check is to make sure the exception is retryable, e.g: server not found, internal server error...
96
- if (exception instanceof ConfigException || exception instanceof ExecutionException) {
97
- return toRetry((Exception) exception.getCause());
98
- }
99
-
100
- return exception instanceof TimeoutException || super.isExceptionToRetry(exception);
101
- }
102
- });
103
-
104
- return responseBody != null && !responseBody.isEmpty() ? parseJson(responseBody) : MissingNode.getInstance();
105
- }
106
- catch (Exception ex) {
107
- LOG.error("Exception occurred while sending request.", ex);
108
- throw Throwables.propagate(ex);
109
- }
110
- }
111
-
112
- private JsonNode parseJson(final String json)
113
- throws DataException
114
- {
115
- try {
116
- return this.jsonMapper.readTree(json);
117
- }
118
- catch (IOException ex) {
119
- // Try to parse invalid json before throwing exception
120
- return parseInvalidJsonString(json);
121
- }
122
- }
123
-
124
- // Sometimes, the MailChimp API returns invalid JSON when we pushed a large of data. ObjectMapper can not read string json.
125
- // So we have to use this method to parse string and build a json string as ReportResponse
126
- // E.g invalid json response from MailChimp https://gist.github.com/thangnc/dc94026e4b13b728b7303f402b458b05
127
- private JsonNode parseInvalidJsonString(final String json)
128
- {
129
- int totalCreatedIndex = json.indexOf("\"total_created\"");
130
- int totalUpdatedIndex = json.indexOf("\"total_updated\"");
131
- int errorCountIndex = json.indexOf("\"error_count\"");
132
- int errorsIndex = json.indexOf("\"errors\"");
133
-
134
- StringBuilder validJson = new StringBuilder();
135
- validJson.append("{").append(json.substring(errorsIndex, totalCreatedIndex - 1)).append(",");
136
- validJson.append(json.substring(totalCreatedIndex, totalCreatedIndex + "\"total_created\"".length() + 2)).append(",");
137
- validJson.append(json.substring(totalUpdatedIndex, totalUpdatedIndex + "\"total_updated\"".length() + 2)).append(",");
138
- validJson.append(json.substring(errorCountIndex, errorCountIndex + "\"error_count\"".length() + 2)).append("}");
139
-
140
- try {
141
- return this.jsonMapper.readTree(validJson.toString());
142
- }
143
- catch (IOException ex) {
144
- throw new DataException(ex);
145
- }
146
- }
147
-
148
- public void avoidFlushAPI(String reason)
149
- {
150
- try {
151
- LOG.info("{} in 5s...", reason);
152
- Thread.sleep(5000);
153
- }
154
- catch (InterruptedException e) {
155
- LOG.warn("Failed to sleep: {}", e.getMessage());
156
- }
157
- }
158
-
159
- /**
160
- * MailChimp API v3 supports non expires access_token. Then no need refresh_token
161
- *
162
- * @param task
163
- * @return
164
- */
165
- private String getAuthorizationHeader(final MailChimpOutputPluginDelegate.PluginTask task)
166
- {
167
- switch (task.getAuthMethod()) {
168
- case OAUTH:
169
- return "OAuth " + task.getAccessToken().orNull();
170
- case API_KEY:
171
- return "Basic " + Base64Variants.MIME_NO_LINEFEEDS
172
- .encode(("apikey" + ":" + task.getApikey().orNull()).getBytes());
173
- default:
174
- throw new ConfigException("Not supported method");
175
- }
176
- }
177
-
178
- private Jetty92RetryHelper createRetryHelper(MailChimpOutputPluginDelegate.PluginTask task)
179
- {
180
- return new Jetty92RetryHelper(
181
- task.getMaximumRetries(),
182
- task.getInitialRetryIntervalMillis(),
183
- task.getMaximumRetryIntervalMillis(),
184
- new Jetty92ClientCreator()
185
- {
186
- @Override
187
- public HttpClient createAndStart()
188
- {
189
- HttpClient client = new HttpClient(new SslContextFactory());
190
- try {
191
- client.start();
192
- return client;
193
- }
194
- catch (Exception e) {
195
- throw Throwables.propagate(e);
196
- }
197
- }
198
- });
199
- }
200
- }