embulk-output-mailchimp 0.3.13 → 0.3.14

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: c7ffeeb484184fcbcb739aef1c871a0c915fca74
4
- data.tar.gz: ab5372d39822ac2c231945f1360611ed83f555ec
3
+ metadata.gz: f89e9a5a69ae081a887e26d19c1a57695d265516
4
+ data.tar.gz: b3c53d62e62a9127520163a38b053a12b4be50f3
5
5
  SHA512:
6
- metadata.gz: ff9ea7f57503ac6149b87fb78a67200cfc59fde7fbf66e1bb632d660ab5480a5f4a9e50ddd8d94bdd9d6a4ec2a8b5dc9ee85f4f2c6b1c4f4041e915a991d6483
7
- data.tar.gz: 15126516099f6bb739522ec13e9dfcda26ed365a0f686e5e7d9fe7c3dcb37d68cbe97e14a6a760acfa13cfa8c933b20e9f7dad36c5505846bda60817dcb0dde7
6
+ metadata.gz: d27ebd29fa3ad09b9c942ef2b2276db918f7ab38c2ff8fb819ed4cbfe7ecbea3e4bd25c1926cef43ed317c2d431c030916f015f8c8aacd24f6a125360a8f0326
7
+ data.tar.gz: 376d7dadd0bec805f68d891930d0c1ebb60d618bbeb55519e90b80e92535826aa7e278b7c40d3d115c22a225aa56aed0ef46fff65ad1a01dae5d7022b57e1f6a
@@ -1,3 +1,6 @@
1
+ ## 0.3.14 - 2017-06-26
2
+ - Make clear the log message and renamed class `MailChimpClient` [#29](https://github.com/treasure-data/embulk-output-mailchimp/pull/29)
3
+
1
4
  ## 0.3.13 - 2017-06-16
2
5
  - Upgraded `embulk-base-restclient` to v0.5.3 and fixed hang job [#28](https://github.com/treasure-data/embulk-output-mailchimp/pull/28)
3
6
 
@@ -18,7 +18,7 @@ configurations {
18
18
  provided
19
19
  }
20
20
 
21
- version = "0.3.13"
21
+ version = "0.3.14"
22
22
 
23
23
  sourceCompatibility = 1.7
24
24
  targetCompatibility = 1.7
@@ -0,0 +1,235 @@
1
+ package org.embulk.output.mailchimp;
2
+
3
+ import com.fasterxml.jackson.core.JsonParser;
4
+ import com.fasterxml.jackson.core.JsonProcessingException;
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.ObjectNode;
9
+ import com.google.common.base.Function;
10
+ import com.google.common.base.Joiner;
11
+ import com.google.common.collect.FluentIterable;
12
+ import com.google.common.collect.ImmutableList;
13
+ import com.google.common.collect.Maps;
14
+ import org.eclipse.jetty.http.HttpMethod;
15
+ import org.embulk.config.ConfigException;
16
+ import org.embulk.output.mailchimp.helper.MailChimpHelper;
17
+ import org.embulk.output.mailchimp.model.CategoriesResponse;
18
+ import org.embulk.output.mailchimp.model.ErrorResponse;
19
+ import org.embulk.output.mailchimp.model.InterestCategoriesResponse;
20
+ import org.embulk.output.mailchimp.model.InterestResponse;
21
+ import org.embulk.output.mailchimp.model.InterestsResponse;
22
+ import org.embulk.output.mailchimp.model.MergeField;
23
+ import org.embulk.output.mailchimp.model.MergeFields;
24
+ import org.embulk.output.mailchimp.model.MetaDataResponse;
25
+ import org.embulk.output.mailchimp.model.ReportResponse;
26
+ import org.embulk.spi.Exec;
27
+ import org.slf4j.Logger;
28
+
29
+ import javax.annotation.Nullable;
30
+
31
+ import java.text.MessageFormat;
32
+ import java.util.HashMap;
33
+ import java.util.List;
34
+ import java.util.Map;
35
+
36
+ import static org.embulk.output.mailchimp.model.AuthMethod.API_KEY;
37
+ import static org.embulk.output.mailchimp.model.AuthMethod.OAUTH;
38
+
39
+ /**
40
+ * Created by thangnc on 4/25/17.
41
+ */
42
+ public class MailChimpClient
43
+ {
44
+ private static final Logger LOG = Exec.getLogger(MailChimpClient.class);
45
+ private static final String API_VERSION = "3.0";
46
+ private static String mailchimpEndpoint;
47
+ private MailChimpHttpClient client;
48
+ private final ObjectMapper mapper;
49
+
50
+ /**
51
+ * Instantiates a new Mail chimp client.
52
+ *
53
+ * @param task the task
54
+ */
55
+ public MailChimpClient(final MailChimpOutputPluginDelegate.PluginTask task)
56
+ {
57
+ mailchimpEndpoint = Joiner.on("/").join("https://{0}.api.mailchimp.com", API_VERSION);
58
+ this.client = new MailChimpHttpClient(task);
59
+ this.mapper = new ObjectMapper()
60
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
61
+ .configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, false);
62
+ extractDataCenter(task);
63
+ }
64
+
65
+ /**
66
+ * Build an array of email subscribers and batch insert via bulk MailChimp API
67
+ * Reference: https://developer.mailchimp.com/documentation/mailchimp/reference/lists/#create-post_lists_list_id
68
+ *
69
+ * @param node the data
70
+ * @param task the task
71
+ * @return the report response
72
+ * @throws JsonProcessingException the json processing exception
73
+ */
74
+ ReportResponse push(final ObjectNode node, MailChimpOutputPluginDelegate.PluginTask task)
75
+ throws JsonProcessingException
76
+ {
77
+ String endpoint = MessageFormat.format(mailchimpEndpoint + "/lists/{0}",
78
+ task.getListId());
79
+
80
+ JsonNode response = client.sendRequest(endpoint, HttpMethod.POST, node.toString(), task);
81
+ return mapper.treeToValue(response, ReportResponse.class);
82
+ }
83
+
84
+ /**
85
+ * Handle detail errors after call bulk MailChimp API
86
+ *
87
+ * @param errorResponses the error responses
88
+ */
89
+ void handleErrors(List<ErrorResponse> errorResponses)
90
+ {
91
+ if (!errorResponses.isEmpty()) {
92
+ StringBuilder errorMessage = new StringBuilder();
93
+
94
+ for (ErrorResponse errorResponse : errorResponses) {
95
+ errorMessage.append(MessageFormat.format("`{0}` failed cause `{1}`\n",
96
+ MailChimpHelper.maskEmail(errorResponse.getEmailAddress()),
97
+ MailChimpHelper.maskEmail(errorResponse.getError())));
98
+ }
99
+
100
+ LOG.error("Error response from MailChimp: ");
101
+ LOG.error(errorMessage.toString());
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Extract interest categories by group names. Loop via categories and fetch category details
107
+ * Reference: https://developer.mailchimp.com/documentation/mailchimp/reference/lists/interest-categories/#read-get_lists_list_id_interest_categories
108
+ * https://developer.mailchimp.com/documentation/mailchimp/reference/lists/interest-categories/#read-get_lists_list_id_interest_categories_interest_category_id
109
+ *
110
+ * @param task the task
111
+ * @return the map
112
+ * @throws JsonProcessingException the json processing exception
113
+ */
114
+ Map<String, Map<String, InterestResponse>> extractInterestCategoriesByGroupNames(final MailChimpOutputPluginDelegate.PluginTask task)
115
+ throws JsonProcessingException
116
+ {
117
+ Map<String, Map<String, InterestResponse>> categories = new HashMap<>();
118
+ if (task.getGroupingColumns().isPresent() && !task.getGroupingColumns().get().isEmpty()) {
119
+ List<String> interestCategoryNames = task.getGroupingColumns().get();
120
+
121
+ String endpoint = MessageFormat.format(mailchimpEndpoint + "/lists/{0}/interest-categories",
122
+ task.getListId());
123
+
124
+ JsonNode response = client.sendRequest(endpoint, HttpMethod.GET, task);
125
+ InterestCategoriesResponse interestCategoriesResponse = mapper.treeToValue(response,
126
+ InterestCategoriesResponse.class);
127
+
128
+ Function<CategoriesResponse, String> function = new Function<CategoriesResponse, String>()
129
+ {
130
+ @Override
131
+ public String apply(CategoriesResponse input)
132
+ {
133
+ return input.getTitle().toLowerCase();
134
+ }
135
+ };
136
+
137
+ // Transform to a list of available category names and validate with data that user input
138
+ ImmutableList<String> availableCategories = FluentIterable
139
+ .from(interestCategoriesResponse.getCategories())
140
+ .transform(function)
141
+ .toList();
142
+
143
+ for (String category : interestCategoryNames) {
144
+ if (!availableCategories.contains(category)) {
145
+ throw new ConfigException("Invalid interest category name: '" + category + "'");
146
+ }
147
+ }
148
+
149
+ for (CategoriesResponse categoriesResponse : interestCategoriesResponse.getCategories()) {
150
+ String detailEndpoint = MessageFormat.format(mailchimpEndpoint + "/lists/{0}/interest-categories/{1}/interests",
151
+ task.getListId(),
152
+ categoriesResponse.getId());
153
+ response = client.sendRequest(detailEndpoint, HttpMethod.GET, task);
154
+ InterestsResponse interestsResponse = mapper.treeToValue(response, InterestsResponse.class);
155
+ categories.put(categoriesResponse.getTitle().toLowerCase(),
156
+ convertInterestCategoryToMap(interestsResponse.getInterests()));
157
+ }
158
+ }
159
+
160
+ return categories;
161
+ }
162
+
163
+ /**
164
+ * Extract merge fields from the list, find correct merge fields from API and put into the map to use
165
+ * Reference: https://developer.mailchimp.com/documentation/mailchimp/reference/lists/merge-fields/#read-get_lists_list_id_merge_fields
166
+ *
167
+ * @param task the task
168
+ * @return the map
169
+ * @throws JsonProcessingException the json processing exception
170
+ */
171
+ Map<String, MergeField> extractMergeFieldsFromList(MailChimpOutputPluginDelegate.PluginTask task) throws JsonProcessingException
172
+ {
173
+ String endpoint = MessageFormat.format(mailchimpEndpoint + "/lists/{0}/merge-fields",
174
+ task.getListId());
175
+ JsonNode response = client.sendRequest(endpoint, HttpMethod.GET, task);
176
+ MergeFields mergeFields = mapper.treeToValue(response,
177
+ MergeFields.class);
178
+ return convertMergeFieldToMap(mergeFields.getMergeFields());
179
+ }
180
+
181
+ private void extractDataCenter(MailChimpOutputPluginDelegate.PluginTask task)
182
+ {
183
+ try {
184
+ if (task.getAuthMethod() == OAUTH) {
185
+ // Extract data center from meta data URL
186
+ JsonNode response = client.sendRequest("https://login.mailchimp.com/oauth2/metadata", HttpMethod.GET, task);
187
+ MetaDataResponse metaDataResponse = mapper.treeToValue(response, MetaDataResponse.class);
188
+ mailchimpEndpoint = MessageFormat.format(mailchimpEndpoint, metaDataResponse.getDc());
189
+ }
190
+ else if (task.getAuthMethod() == API_KEY && task.getApikey().isPresent()) {
191
+ // Authenticate and return data center
192
+ String domain = task.getApikey().get().split("-")[1];
193
+ String endpoint = MessageFormat.format(mailchimpEndpoint + "/", domain);
194
+ client.sendRequest(endpoint, HttpMethod.GET, task);
195
+ mailchimpEndpoint = MessageFormat.format(mailchimpEndpoint, domain);
196
+ }
197
+ }
198
+ catch (Exception e) {
199
+ throw new ConfigException("Could not get data center", e);
200
+ }
201
+ }
202
+
203
+ private Map<String, InterestResponse> convertInterestCategoryToMap(final List<InterestResponse> interestResponseList)
204
+ {
205
+ Function<InterestResponse, String> function = new Function<InterestResponse, String>()
206
+ {
207
+ @Override
208
+ public String apply(@Nullable InterestResponse input)
209
+ {
210
+ return input.getName();
211
+ }
212
+ };
213
+
214
+ return Maps.uniqueIndex(FluentIterable.from(interestResponseList)
215
+ .toList(),
216
+ function);
217
+ }
218
+
219
+ private Map<String, MergeField> convertMergeFieldToMap(final List<MergeField> mergeFieldList)
220
+ {
221
+ Function<MergeField, String> function = new Function<MergeField, String>()
222
+ {
223
+ @Nullable
224
+ @Override
225
+ public String apply(@Nullable MergeField input)
226
+ {
227
+ return input.getTag().toLowerCase();
228
+ }
229
+ };
230
+
231
+ return Maps.uniqueIndex(FluentIterable.from(mergeFieldList)
232
+ .toList(),
233
+ function);
234
+ }
235
+ }
@@ -1,208 +1,276 @@
1
1
  package org.embulk.output.mailchimp;
2
2
 
3
+ import com.fasterxml.jackson.core.JsonParser;
3
4
  import com.fasterxml.jackson.core.JsonProcessingException;
5
+ import com.fasterxml.jackson.databind.DeserializationFeature;
4
6
  import com.fasterxml.jackson.databind.JsonNode;
7
+ import com.fasterxml.jackson.databind.ObjectMapper;
8
+ import com.fasterxml.jackson.databind.node.JsonNodeFactory;
9
+ import com.fasterxml.jackson.databind.node.NullNode;
5
10
  import com.fasterxml.jackson.databind.node.ObjectNode;
6
11
  import com.google.common.base.Function;
12
+ import com.google.common.base.Throwables;
7
13
  import com.google.common.collect.FluentIterable;
8
- import com.google.common.collect.ImmutableList;
9
- import com.google.common.collect.Maps;
10
- import org.eclipse.jetty.http.HttpMethod;
11
- import org.embulk.config.ConfigException;
12
- import org.embulk.output.mailchimp.helper.MailChimpHelper;
13
- import org.embulk.output.mailchimp.model.CategoriesResponse;
14
- import org.embulk.output.mailchimp.model.ErrorResponse;
15
- import org.embulk.output.mailchimp.model.InterestCategoriesResponse;
14
+ import org.embulk.base.restclient.jackson.JacksonServiceRecord;
15
+ import org.embulk.base.restclient.record.RecordBuffer;
16
+ import org.embulk.base.restclient.record.ServiceRecord;
17
+ import org.embulk.config.TaskReport;
18
+ import org.embulk.output.mailchimp.model.AddressMergeFieldAttribute;
16
19
  import org.embulk.output.mailchimp.model.InterestResponse;
17
- import org.embulk.output.mailchimp.model.InterestsResponse;
18
20
  import org.embulk.output.mailchimp.model.MergeField;
19
- import org.embulk.output.mailchimp.model.MergeFields;
20
- import org.embulk.output.mailchimp.model.MetaDataResponse;
21
21
  import org.embulk.output.mailchimp.model.ReportResponse;
22
+ import org.embulk.spi.Column;
23
+ import org.embulk.spi.DataException;
22
24
  import org.embulk.spi.Exec;
23
25
  import org.embulk.spi.Schema;
24
26
  import org.slf4j.Logger;
25
27
 
26
- import javax.annotation.Nullable;
27
-
28
- import java.text.MessageFormat;
28
+ import java.io.IOException;
29
+ import java.util.ArrayList;
29
30
  import java.util.HashMap;
30
31
  import java.util.List;
31
32
  import java.util.Map;
32
33
 
33
- import static org.embulk.output.mailchimp.model.AuthMethod.API_KEY;
34
- import static org.embulk.output.mailchimp.model.AuthMethod.OAUTH;
34
+ import static org.embulk.output.mailchimp.MailChimpOutputPluginDelegate.PluginTask;
35
+ import static org.embulk.output.mailchimp.helper.MailChimpHelper.containsCaseInsensitive;
36
+ import static org.embulk.output.mailchimp.helper.MailChimpHelper.fromCommaSeparatedString;
37
+ import static org.embulk.output.mailchimp.helper.MailChimpHelper.orderJsonNode;
38
+ import static org.embulk.output.mailchimp.helper.MailChimpHelper.toJsonNode;
39
+ import static org.embulk.output.mailchimp.model.MemberStatus.PENDING;
40
+ import static org.embulk.output.mailchimp.model.MemberStatus.SUBSCRIBED;
35
41
 
36
42
  /**
37
- * Created by thangnc on 4/25/17.
43
+ * Created by thangnc on 4/14/17.
38
44
  */
39
- public class MailChimpRecordBuffer extends MailChimpAbstractRecordBuffer
45
+ public class MailChimpRecordBuffer
46
+ extends RecordBuffer
40
47
  {
41
48
  private static final Logger LOG = Exec.getLogger(MailChimpRecordBuffer.class);
42
- private MailChimpHttpClient client;
43
-
44
- public MailChimpRecordBuffer(final Schema schema, final MailChimpOutputPluginDelegate.PluginTask task)
45
- {
46
- super(schema, task);
47
- client = new MailChimpHttpClient(task);
48
- }
49
-
50
- @Override
51
- public void finish()
52
- {
53
- }
54
-
55
- @Override
56
- public void close()
57
- {
58
- }
49
+ private static final int MAX_RECORD_PER_BATCH_REQUEST = 500;
50
+ private final MailChimpOutputPluginDelegate.PluginTask task;
51
+ private final MailChimpClient mailChimpClient;
52
+ private final ObjectMapper mapper;
53
+ private final Schema schema;
54
+ private int requestCount;
55
+ private long totalCount;
56
+ private List<JsonNode> records;
57
+ private Map<String, Map<String, InterestResponse>> categories;
58
+ private Map<String, MergeField> availableMergeFields;
59
59
 
60
60
  /**
61
- * Build an array of email subscribers and batch insert via bulk MailChimp API
62
- * Reference: https://developer.mailchimp.com/documentation/mailchimp/reference/lists/#create-post_lists_list_id
61
+ * Instantiates a new Mail chimp abstract record buffer.
63
62
  *
64
- * @param node the data
65
- * @param task the task
66
- * @throws JsonProcessingException the json processing exception
63
+ * @param schema the schema
64
+ * @param task the task
67
65
  */
68
- @Override
69
- public ReportResponse push(final ObjectNode node, MailChimpOutputPluginDelegate.PluginTask task)
70
- throws JsonProcessingException
66
+ public MailChimpRecordBuffer(final Schema schema, final PluginTask task)
71
67
  {
72
- String endpoint = MessageFormat.format(mailchimpEndpoint + "/lists/{0}",
73
- task.getListId());
74
-
75
- JsonNode response = client.sendRequest(endpoint, HttpMethod.POST, node.toString(), task);
76
- return getMapper().treeToValue(response, ReportResponse.class);
68
+ this.schema = schema;
69
+ this.task = task;
70
+ this.mapper = new ObjectMapper()
71
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
72
+ .configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, false);
73
+ this.records = new ArrayList<>();
74
+ this.categories = new HashMap<>();
75
+ this.mailChimpClient = new MailChimpClient(task);
77
76
  }
78
77
 
79
78
  @Override
80
- void handleErrors(List<ErrorResponse> errorResponses)
79
+ public void bufferRecord(ServiceRecord serviceRecord)
81
80
  {
82
- if (!errorResponses.isEmpty()) {
83
- StringBuilder errorMessage = new StringBuilder();
81
+ JacksonServiceRecord jacksonServiceRecord;
84
82
 
85
- for (ErrorResponse errorResponse : errorResponses) {
86
- errorMessage.append(MessageFormat.format("\nEmail `{0}` failed cause `{1}`",
87
- MailChimpHelper.maskEmail(errorResponse.getEmailAddress()),
88
- MailChimpHelper.maskEmail(errorResponse.getError())));
89
- }
83
+ try {
84
+ jacksonServiceRecord = (JacksonServiceRecord) serviceRecord;
85
+ JsonNode record = mapper.readTree(jacksonServiceRecord.toString()).get("record");
90
86
 
91
- LOG.error(errorMessage.toString());
92
- }
93
- }
87
+ requestCount++;
88
+ totalCount++;
94
89
 
95
- Map<String, Map<String, InterestResponse>> extractInterestCategoriesByGroupNames(final MailChimpOutputPluginDelegate.PluginTask task)
96
- throws JsonProcessingException
97
- {
98
- Map<String, Map<String, InterestResponse>> categories = new HashMap<>();
99
- if (task.getGroupingColumns().isPresent() && !task.getGroupingColumns().get().isEmpty()) {
100
- List<String> interestCategoryNames = task.getGroupingColumns().get();
90
+ records.add(record);
91
+ if (requestCount >= MAX_RECORD_PER_BATCH_REQUEST) {
92
+ ObjectNode subcribers = processSubcribers(records, task);
93
+ ReportResponse reportResponse = mailChimpClient.push(subcribers, task);
101
94
 
102
- String endpoint = MessageFormat.format(mailchimpEndpoint + "/lists/{0}/interest-categories",
103
- task.getListId());
104
-
105
- JsonNode response = client.sendRequest(endpoint, HttpMethod.GET, task);
106
- InterestCategoriesResponse interestCategoriesResponse = getMapper().treeToValue(response,
107
- InterestCategoriesResponse.class);
108
-
109
- Function<CategoriesResponse, String> function = new Function<CategoriesResponse, String>()
110
- {
111
- @Override
112
- public String apply(CategoriesResponse input)
113
- {
114
- return input.getTitle().toLowerCase();
95
+ if (totalCount % 1000 == 0) {
96
+ LOG.info("Pushed {} records", totalCount);
115
97
  }
116
- };
117
-
118
- // Transform to a list of available category names and validate with data that user input
119
- ImmutableList<String> availableCategories = FluentIterable
120
- .from(interestCategoriesResponse.getCategories())
121
- .transform(function)
122
- .toList();
123
98
 
124
- for (String category : interestCategoryNames) {
125
- if (!availableCategories.contains(category)) {
126
- throw new ConfigException("Invalid interest category name: '" + category + "'");
127
- }
128
- }
99
+ LOG.info("Response from MailChimp: {} records created, {} records updated, {} records failed",
100
+ reportResponse.getTotalCreated(),
101
+ reportResponse.getTotalUpdated(),
102
+ reportResponse.getErrorCount());
103
+ mailChimpClient.handleErrors(reportResponse.getErrors());
129
104
 
130
- for (CategoriesResponse categoriesResponse : interestCategoriesResponse.getCategories()) {
131
- String detailEndpoint = MessageFormat.format(mailchimpEndpoint + "/lists/{0}/interest-categories/{1}/interests",
132
- task.getListId(),
133
- categoriesResponse.getId());
134
- response = client.sendRequest(detailEndpoint, HttpMethod.GET, task);
135
- InterestsResponse interestsResponse = getMapper().treeToValue(response, InterestsResponse.class);
136
- categories.put(categoriesResponse.getTitle().toLowerCase(),
137
- convertInterestCategoryToMap(interestsResponse.getInterests()));
105
+ records = new ArrayList<>();
106
+ requestCount = 0;
138
107
  }
139
108
  }
140
-
141
- return categories;
109
+ catch (JsonProcessingException jpe) {
110
+ throw new DataException(jpe);
111
+ }
112
+ catch (ClassCastException ex) {
113
+ throw new RuntimeException(ex);
114
+ }
115
+ catch (IOException ex) {
116
+ throw Throwables.propagate(ex);
117
+ }
142
118
  }
143
119
 
144
120
  @Override
145
- String extractDataCenter(MailChimpOutputPluginDelegate.PluginTask task) throws JsonProcessingException
121
+ public TaskReport commitWithTaskReportUpdated(TaskReport taskReport)
146
122
  {
147
- if (task.getAuthMethod() == OAUTH) {
148
- // Extract data center from meta data URL
149
- JsonNode response = client.sendRequest("https://login.mailchimp.com/oauth2/metadata", HttpMethod.GET, task);
150
- MetaDataResponse metaDataResponse = getMapper().treeToValue(response, MetaDataResponse.class);
151
- return metaDataResponse.getDc();
123
+ try {
124
+ if (records.size() > 0) {
125
+ ObjectNode subcribers = processSubcribers(records, task);
126
+ ReportResponse reportResponse = mailChimpClient.push(subcribers, task);
127
+ LOG.info("Pushed {} records", records.size());
128
+ LOG.info("Response from MailChimp: {} records created, {} records updated, {} records failed",
129
+ reportResponse.getTotalCreated(),
130
+ reportResponse.getTotalUpdated(),
131
+ reportResponse.getErrorCount());
132
+ mailChimpClient.handleErrors(reportResponse.getErrors());
133
+ }
134
+
135
+ return Exec.newTaskReport().set("pushed", totalCount);
152
136
  }
153
- else if (task.getAuthMethod() == API_KEY && task.getApikey().isPresent()) {
154
- // Authenticate and return data center
155
- String domain = task.getApikey().get().split("-")[1];
156
- String endpoint = MessageFormat.format(mailchimpEndpoint + "/", domain);
157
- client.sendRequest(endpoint, HttpMethod.GET, task);
158
- return domain;
137
+ catch (JsonProcessingException jpe) {
138
+ throw new DataException(jpe);
159
139
  }
160
- else {
161
- throw new ConfigException("Could not get data center");
140
+ catch (Exception ex) {
141
+ throw Throwables.propagate(ex);
162
142
  }
163
143
  }
164
144
 
165
145
  @Override
166
- Map<String, MergeField> extractMergeFieldsFromList(MailChimpOutputPluginDelegate.PluginTask task) throws JsonProcessingException
146
+ public void finish()
147
+ {
148
+ // Do not close here
149
+ }
150
+
151
+ @Override
152
+ public void close()
167
153
  {
168
- String endpoint = MessageFormat.format(mailchimpEndpoint + "/lists/{0}/merge-fields",
169
- task.getListId());
170
- JsonNode response = client.sendRequest(endpoint, HttpMethod.GET, task);
171
- MergeFields mergeFields = getMapper().treeToValue(response,
172
- MergeFields.class);
173
- return convertMergeFieldToMap(mergeFields.getMergeFields());
154
+ // Do not implement
174
155
  }
175
156
 
176
- private Map<String, InterestResponse> convertInterestCategoryToMap(final List<InterestResponse> interestResponseList)
157
+ /**
158
+ * Receive data and build payload json that contains subscribers
159
+ *
160
+ * @param data the data
161
+ * @param task the task
162
+ * @return the object node
163
+ */
164
+ private ObjectNode processSubcribers(final List<JsonNode> data, final PluginTask task)
165
+ throws JsonProcessingException
177
166
  {
178
- Function<InterestResponse, String> function = new Function<InterestResponse, String>()
179
- {
180
- @Override
181
- public String apply(@Nullable InterestResponse input)
182
- {
183
- return input.getName();
184
- }
185
- };
167
+ LOG.info("Start to process subscriber data");
168
+
169
+ // Should loop the names and get the id of interest categories.
170
+ // The reason why we put categories validation here because we can not share data between instance.
171
+ categories = mailChimpClient.extractInterestCategoriesByGroupNames(task);
172
+
173
+ // Extract merge fields detail
174
+ availableMergeFields = mailChimpClient.extractMergeFieldsFromList(task);
175
+
176
+ // Required merge fields
177
+ Map<String, String> map = new HashMap<>();
178
+ map.put("FNAME", task.getFnameColumn());
179
+ map.put("LNAME", task.getLnameColumn());
186
180
 
187
- return Maps.uniqueIndex(FluentIterable.from(interestResponseList)
188
- .toList(),
189
- function);
181
+ List<JsonNode> subscribersList = FluentIterable.from(data)
182
+ .transform(contactMapper(map))
183
+ .toList();
184
+
185
+ ObjectNode subscribers = JsonNodeFactory.instance.objectNode();
186
+ subscribers.putArray("members").addAll(subscribersList);
187
+ subscribers.put("update_existing", task.getUpdateExisting());
188
+ return subscribers;
190
189
  }
191
190
 
192
- private Map<String, MergeField> convertMergeFieldToMap(final List<MergeField> mergeFieldList)
191
+ private Function<JsonNode, JsonNode> contactMapper(final Map<String, String> allowColumns)
193
192
  {
194
- Function<MergeField, String> function = new Function<MergeField, String>()
193
+ return new Function<JsonNode, JsonNode>()
195
194
  {
196
- @Nullable
197
195
  @Override
198
- public String apply(@Nullable MergeField input)
196
+ public JsonNode apply(JsonNode input)
199
197
  {
200
- return input.getTag().toLowerCase();
198
+ ObjectNode property = JsonNodeFactory.instance.objectNode();
199
+ property.put("email_address", input.findPath(task.getEmailColumn()).asText());
200
+ property.put("status", task.getDoubleOptIn() ? PENDING.getType() : SUBSCRIBED.getType());
201
+ ObjectNode mergeFields = JsonNodeFactory.instance.objectNode();
202
+ for (String allowColumn : allowColumns.keySet()) {
203
+ String value = input.hasNonNull(allowColumns.get(allowColumn)) ? input.findValue(allowColumns.get(allowColumn)).asText() : "";
204
+ mergeFields.put(allowColumn, value);
205
+ }
206
+
207
+ // Update additional merge fields if exist
208
+ if (task.getMergeFields().isPresent() && !task.getMergeFields().get().isEmpty()) {
209
+ for (final Column column : schema.getColumns()) {
210
+ if (!"".equals(containsCaseInsensitive(column.getName(), task.getMergeFields().get()))) {
211
+ String value = input.hasNonNull(column.getName()) ? input.findValue(column.getName()).asText() : "";
212
+
213
+ // Try to convert to Json from string with the merge field's type is address
214
+ if (availableMergeFields.get(column.getName()).getType()
215
+ .equals(MergeField.MergeFieldType.ADDRESS.getType())) {
216
+ JsonNode addressNode = toJsonNode(value);
217
+ if (addressNode instanceof NullNode) {
218
+ mergeFields.put(column.getName().toUpperCase(), value);
219
+ }
220
+ else {
221
+ mergeFields.set(column.getName().toUpperCase(),
222
+ orderJsonNode(addressNode, AddressMergeFieldAttribute.values()));
223
+ }
224
+ }
225
+ else {
226
+ mergeFields.put(column.getName().toUpperCase(), value);
227
+ }
228
+ }
229
+ }
230
+ }
231
+
232
+ property.set("merge_fields", mergeFields);
233
+
234
+ // Update interest categories if exist
235
+ if (task.getGroupingColumns().isPresent() && !task.getGroupingColumns().get().isEmpty()) {
236
+ property.set("interests", buildInterestCategories(task, input));
237
+ }
238
+
239
+ return property;
201
240
  }
202
241
  };
242
+ }
243
+
244
+ private ObjectNode buildInterestCategories(final MailChimpOutputPluginDelegate.PluginTask task,
245
+ final JsonNode input)
246
+ {
247
+ ObjectNode interests = JsonNodeFactory.instance.objectNode();
248
+
249
+ for (String category : task.getGroupingColumns().get()) {
250
+ String inputValue = input.findValue(category).asText();
251
+ List<String> interestValues = fromCommaSeparatedString(inputValue);
252
+ Map<String, InterestResponse> availableCategories = categories.get(category);
253
+
254
+ // Only update user-predefined categories if replace interests != true
255
+ if (!task.getReplaceInterests()) {
256
+ for (String interestValue : interestValues) {
257
+ if (availableCategories.get(interestValue) != null) {
258
+ interests.put(availableCategories.get(interestValue).getId(), true);
259
+ }
260
+ }
261
+ } // Otherwise, force update all categories include user-predefined categories
262
+ else if (task.getReplaceInterests()) {
263
+ for (String availableCategory : availableCategories.keySet()) {
264
+ if (interestValues.contains(availableCategory)) {
265
+ interests.put(availableCategories.get(availableCategory).getId(), true);
266
+ }
267
+ else {
268
+ interests.put(availableCategories.get(availableCategory).getId(), false);
269
+ }
270
+ }
271
+ }
272
+ }
203
273
 
204
- return Maps.uniqueIndex(FluentIterable.from(mergeFieldList)
205
- .toList(),
206
- function);
274
+ return interests;
207
275
  }
208
276
  }
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.13
4
+ version: 0.3.14
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-06-16 00:00:00.000000000 Z
11
+ date: 2017-06-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
@@ -61,7 +61,7 @@ files:
61
61
  - gradlew
62
62
  - gradlew.bat
63
63
  - lib/embulk/output/mailchimp.rb
64
- - src/main/java/org/embulk/output/mailchimp/MailChimpAbstractRecordBuffer.java
64
+ - src/main/java/org/embulk/output/mailchimp/MailChimpClient.java
65
65
  - src/main/java/org/embulk/output/mailchimp/MailChimpHttpClient.java
66
66
  - src/main/java/org/embulk/output/mailchimp/MailChimpOutputPlugin.java
67
67
  - src/main/java/org/embulk/output/mailchimp/MailChimpOutputPluginDelegate.java
@@ -89,7 +89,7 @@ files:
89
89
  - test/override_assert_raise.rb
90
90
  - test/run-test.rb
91
91
  - classpath/embulk-base-restclient-0.5.3.jar
92
- - classpath/embulk-output-mailchimp-0.3.13.jar
92
+ - classpath/embulk-output-mailchimp-0.3.14.jar
93
93
  - classpath/embulk-util-retryhelper-jetty92-0.5.3.jar
94
94
  - classpath/jetty-client-9.2.14.v20151106.jar
95
95
  - classpath/jetty-http-9.2.14.v20151106.jar
@@ -1,338 +0,0 @@
1
- package org.embulk.output.mailchimp;
2
-
3
- import com.fasterxml.jackson.core.JsonParser;
4
- import com.fasterxml.jackson.core.JsonProcessingException;
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.JsonNodeFactory;
9
- import com.fasterxml.jackson.databind.node.NullNode;
10
- import com.fasterxml.jackson.databind.node.ObjectNode;
11
- import com.google.common.base.Function;
12
- import com.google.common.base.Throwables;
13
- import com.google.common.collect.FluentIterable;
14
- import org.embulk.base.restclient.jackson.JacksonServiceRecord;
15
- import org.embulk.base.restclient.record.RecordBuffer;
16
- import org.embulk.base.restclient.record.ServiceRecord;
17
- import org.embulk.config.TaskReport;
18
- import org.embulk.output.mailchimp.model.AddressMergeFieldAttribute;
19
- import org.embulk.output.mailchimp.model.ErrorResponse;
20
- import org.embulk.output.mailchimp.model.InterestResponse;
21
- import org.embulk.output.mailchimp.model.MergeField;
22
- import org.embulk.output.mailchimp.model.ReportResponse;
23
- import org.embulk.spi.Column;
24
- import org.embulk.spi.DataException;
25
- import org.embulk.spi.Exec;
26
- import org.embulk.spi.Schema;
27
- import org.slf4j.Logger;
28
-
29
- import java.io.IOException;
30
- import java.text.MessageFormat;
31
- import java.util.ArrayList;
32
- import java.util.HashMap;
33
- import java.util.List;
34
- import java.util.Map;
35
-
36
- import static org.embulk.output.mailchimp.helper.MailChimpHelper.containsCaseInsensitive;
37
- import static org.embulk.output.mailchimp.helper.MailChimpHelper.fromCommaSeparatedString;
38
- import static org.embulk.output.mailchimp.helper.MailChimpHelper.orderJsonNode;
39
- import static org.embulk.output.mailchimp.helper.MailChimpHelper.toJsonNode;
40
- import static org.embulk.output.mailchimp.model.MemberStatus.PENDING;
41
- import static org.embulk.output.mailchimp.model.MemberStatus.SUBSCRIBED;
42
-
43
- /**
44
- * Created by thangnc on 4/14/17.
45
- */
46
- public abstract class MailChimpAbstractRecordBuffer
47
- extends RecordBuffer
48
- {
49
- private static final Logger LOG = Exec.getLogger(MailChimpAbstractRecordBuffer.class);
50
- private static final int MAX_RECORD_PER_BATCH_REQUEST = 500;
51
- /**
52
- * The constant mailchimpEndpoint.
53
- */
54
- protected static String mailchimpEndpoint = "https://{0}.api.mailchimp.com/3.0";
55
- private final MailChimpOutputPluginDelegate.PluginTask task;
56
- private final ObjectMapper mapper;
57
- private final Schema schema;
58
- private int requestCount;
59
- private long totalCount;
60
- private List<JsonNode> records;
61
- private Map<String, Map<String, InterestResponse>> categories;
62
- private Map<String, MergeField> availableMergeFields;
63
-
64
- /**
65
- * Instantiates a new Mail chimp abstract record buffer.
66
- *
67
- * @param schema the schema
68
- * @param task the task
69
- */
70
- public MailChimpAbstractRecordBuffer(final Schema schema, final MailChimpOutputPluginDelegate.PluginTask task)
71
- {
72
- this.schema = schema;
73
- this.task = task;
74
- this.mapper = new ObjectMapper()
75
- .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
76
- .configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, false);
77
- this.records = new ArrayList<>();
78
- this.categories = new HashMap<>();
79
- }
80
-
81
- @Override
82
- public void bufferRecord(ServiceRecord serviceRecord)
83
- {
84
- JacksonServiceRecord jacksonServiceRecord;
85
-
86
- try {
87
- jacksonServiceRecord = (JacksonServiceRecord) serviceRecord;
88
- JsonNode record = mapper.readTree(jacksonServiceRecord.toString()).get("record");
89
-
90
- requestCount++;
91
- totalCount++;
92
-
93
- records.add(record);
94
- if (requestCount >= MAX_RECORD_PER_BATCH_REQUEST) {
95
- ObjectNode subcribers = processSubcribers(records, task);
96
- ReportResponse reportResponse = push(subcribers, task);
97
-
98
- if (totalCount % 1000 == 0) {
99
- LOG.info("Pushed {} records", totalCount);
100
- }
101
-
102
- LOG.info("{} records created, {} records updated, {} records failed",
103
- reportResponse.getTotalCreated(),
104
- reportResponse.getTotalUpdated(),
105
- reportResponse.getErrorCount());
106
- handleErrors(reportResponse.getErrors());
107
-
108
- records = new ArrayList<>();
109
- requestCount = 0;
110
- }
111
- }
112
- catch (JsonProcessingException jpe) {
113
- throw new DataException(jpe);
114
- }
115
- catch (ClassCastException ex) {
116
- throw new RuntimeException(ex);
117
- }
118
- catch (IOException ex) {
119
- throw Throwables.propagate(ex);
120
- }
121
- }
122
-
123
- @Override
124
- public TaskReport commitWithTaskReportUpdated(TaskReport taskReport)
125
- {
126
- try {
127
- if (records.size() > 0) {
128
- ObjectNode subcribers = processSubcribers(records, task);
129
- ReportResponse reportResponse = push(subcribers, task);
130
- LOG.info("Pushed {} records", records.size());
131
- LOG.info("{} records created, {} records updated, {} records failed",
132
- reportResponse.getTotalCreated(),
133
- reportResponse.getTotalUpdated(),
134
- reportResponse.getErrorCount());
135
- handleErrors(reportResponse.getErrors());
136
- }
137
-
138
- return Exec.newTaskReport().set("pushed", totalCount);
139
- }
140
- catch (JsonProcessingException jpe) {
141
- throw new DataException(jpe);
142
- }
143
- catch (Exception ex) {
144
- throw Throwables.propagate(ex);
145
- }
146
- }
147
-
148
- /**
149
- * Receive data and build payload json that contains subscribers
150
- *
151
- * @param data the data
152
- * @param task the task
153
- * @return the object node
154
- */
155
- ObjectNode processSubcribers(final List<JsonNode> data, final MailChimpOutputPluginDelegate.PluginTask task)
156
- throws JsonProcessingException
157
- {
158
- LOG.info("Start to process subscriber data");
159
- // Extract data center from meta data URL
160
- String dc = extractDataCenter(task);
161
- mailchimpEndpoint = MessageFormat.format(mailchimpEndpoint, dc);
162
-
163
- // Should loop the names and get the id of interest categories.
164
- // The reason why we put categories validation here because we can not share data between instance.
165
- categories = extractInterestCategoriesByGroupNames(task);
166
-
167
- // Extract merge fields detail
168
- availableMergeFields = extractMergeFieldsFromList(task);
169
-
170
- // Required merge fields
171
- Map<String, String> map = new HashMap<>();
172
- map.put("FNAME", task.getFnameColumn());
173
- map.put("LNAME", task.getLnameColumn());
174
-
175
- List<JsonNode> subscribersList = FluentIterable.from(data)
176
- .transform(contactMapper(map))
177
- .toList();
178
-
179
- ObjectNode subscribers = JsonNodeFactory.instance.objectNode();
180
- subscribers.putArray("members").addAll(subscribersList);
181
- subscribers.put("update_existing", task.getUpdateExisting());
182
- return subscribers;
183
- }
184
-
185
- /**
186
- * Gets mapper.
187
- *
188
- * @return the mapper
189
- */
190
- public ObjectMapper getMapper()
191
- {
192
- return mapper;
193
- }
194
-
195
- /**
196
- * Gets categories.
197
- *
198
- * @return the categories
199
- */
200
- public Map<String, Map<String, InterestResponse>> getCategories()
201
- {
202
- return categories;
203
- }
204
-
205
- /**
206
- * Push payload data to MailChimp API and get @{@link ReportResponse}
207
- *
208
- * @param node the content
209
- * @param task the task
210
- * @return the report response
211
- * @throws JsonProcessingException the json processing exception
212
- */
213
- abstract ReportResponse push(final ObjectNode node, final MailChimpOutputPluginDelegate.PluginTask task)
214
- throws JsonProcessingException;
215
-
216
- /**
217
- * Handle @{@link ErrorResponse} from MailChimp API if exists.
218
- *
219
- * @param errorResponses the error responses
220
- */
221
- abstract void handleErrors(final List<ErrorResponse> errorResponses);
222
-
223
- /**
224
- * Find interest category ids by pre-defined group name which user input.
225
- *
226
- * @param task the task
227
- * @return the map
228
- * @throws JsonProcessingException the json processing exception
229
- */
230
- abstract Map<String, Map<String, InterestResponse>> extractInterestCategoriesByGroupNames(final MailChimpOutputPluginDelegate.PluginTask task)
231
- throws JsonProcessingException;
232
-
233
- /**
234
- * Extract data center from MailChimp v3 metadata.
235
- *
236
- * @param task the task
237
- * @return the string
238
- * @throws JsonProcessingException the json processing exception
239
- */
240
- abstract String extractDataCenter(final MailChimpOutputPluginDelegate.PluginTask task)
241
- throws JsonProcessingException;
242
-
243
- /**
244
- * Extract all merge fields from MailChimp list.
245
- *
246
- * @param task the task
247
- * @return the map
248
- * @throws JsonProcessingException the json processing exception
249
- */
250
- abstract Map<String, MergeField> extractMergeFieldsFromList(final MailChimpOutputPluginDelegate.PluginTask task)
251
- throws JsonProcessingException;
252
-
253
- private Function<JsonNode, JsonNode> contactMapper(final Map<String, String> allowColumns)
254
- {
255
- return new Function<JsonNode, JsonNode>()
256
- {
257
- @Override
258
- public JsonNode apply(JsonNode input)
259
- {
260
- ObjectNode property = JsonNodeFactory.instance.objectNode();
261
- property.put("email_address", input.findPath(task.getEmailColumn()).asText());
262
- property.put("status", task.getDoubleOptIn() ? PENDING.getType() : SUBSCRIBED.getType());
263
- ObjectNode mergeFields = JsonNodeFactory.instance.objectNode();
264
- for (String allowColumn : allowColumns.keySet()) {
265
- String value = input.hasNonNull(allowColumns.get(allowColumn)) ? input.findValue(allowColumns.get(allowColumn)).asText() : "";
266
- mergeFields.put(allowColumn, value);
267
- }
268
-
269
- // Update additional merge fields if exist
270
- if (task.getMergeFields().isPresent() && !task.getMergeFields().get().isEmpty()) {
271
- for (final Column column : schema.getColumns()) {
272
- if (!"".equals(containsCaseInsensitive(column.getName(), task.getMergeFields().get()))) {
273
- String value = input.hasNonNull(column.getName()) ? input.findValue(column.getName()).asText() : "";
274
-
275
- // Try to convert to Json from string with the merge field's type is address
276
- if (availableMergeFields.get(column.getName()).getType()
277
- .equals(MergeField.MergeFieldType.ADDRESS.getType())) {
278
- JsonNode addressNode = toJsonNode(value);
279
- if (addressNode instanceof NullNode) {
280
- mergeFields.put(column.getName().toUpperCase(), value);
281
- }
282
- else {
283
- mergeFields.set(column.getName().toUpperCase(),
284
- orderJsonNode(addressNode, AddressMergeFieldAttribute.values()));
285
- }
286
- }
287
- else {
288
- mergeFields.put(column.getName().toUpperCase(), value);
289
- }
290
- }
291
- }
292
- }
293
-
294
- property.set("merge_fields", mergeFields);
295
-
296
- // Update interest categories if exist
297
- if (task.getGroupingColumns().isPresent() && !task.getGroupingColumns().get().isEmpty()) {
298
- property.set("interests", buildInterestCategories(task, input));
299
- }
300
-
301
- return property;
302
- }
303
- };
304
- }
305
-
306
- private ObjectNode buildInterestCategories(final MailChimpOutputPluginDelegate.PluginTask task,
307
- final JsonNode input)
308
- {
309
- ObjectNode interests = JsonNodeFactory.instance.objectNode();
310
-
311
- for (String category : task.getGroupingColumns().get()) {
312
- String inputValue = input.findValue(category).asText();
313
- List<String> interestValues = fromCommaSeparatedString(inputValue);
314
- Map<String, InterestResponse> availableCategories = categories.get(category);
315
-
316
- // Only update user-predefined categories if replace interests != true
317
- if (!task.getReplaceInterests()) {
318
- for (String interestValue : interestValues) {
319
- if (availableCategories.get(interestValue) != null) {
320
- interests.put(availableCategories.get(interestValue).getId(), true);
321
- }
322
- }
323
- } // Otherwise, force update all categories include user-predefined categories
324
- else if (task.getReplaceInterests()) {
325
- for (String availableCategory : availableCategories.keySet()) {
326
- if (interestValues.contains(availableCategory)) {
327
- interests.put(availableCategories.get(availableCategory).getId(), true);
328
- }
329
- else {
330
- interests.put(availableCategories.get(availableCategory).getId(), false);
331
- }
332
- }
333
- }
334
- }
335
-
336
- return interests;
337
- }
338
- }