embulk-output-mailchimp 0.3.13 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
- }