embulk-output-mailchimp 0.2.3 → 0.3.2
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 +4 -4
- data/.coveralls.yml +1 -1
- data/.gitignore +10 -3
- data/.travis.yml +4 -7
- data/CHANGELOG.md +18 -0
- data/README.md +18 -17
- data/build.gradle +124 -0
- data/ci/travis_mailchimp.yml +8 -0
- data/circle.yml +16 -0
- data/classpath/embulk-base-restclient-0.5.0.jar +0 -0
- data/classpath/embulk-output-mailchimp-0.3.2.jar +0 -0
- data/classpath/embulk-util-retryhelper-jetty92-0.5.0.jar +0 -0
- data/classpath/jetty-client-9.2.14.v20151106.jar +0 -0
- data/classpath/jetty-http-9.2.14.v20151106.jar +0 -0
- data/classpath/jetty-io-9.2.14.v20151106.jar +0 -0
- data/classpath/jetty-util-9.2.14.v20151106.jar +0 -0
- data/config/checkstyle/checkstyle.xml +128 -0
- data/config/checkstyle/default.xml +108 -0
- data/gradle/wrapper/gradle-wrapper.jar +0 -0
- data/gradle/wrapper/gradle-wrapper.properties +6 -0
- data/gradlew +160 -0
- data/gradlew.bat +90 -0
- data/lib/embulk/output/mailchimp.rb +3 -172
- data/src/main/java/org/embulk/output/mailchimp/MailChimpAbstractRecordBuffer.java +320 -0
- data/src/main/java/org/embulk/output/mailchimp/MailChimpHttpClient.java +151 -0
- data/src/main/java/org/embulk/output/mailchimp/MailChimpOutputPlugin.java +18 -0
- data/src/main/java/org/embulk/output/mailchimp/MailChimpOutputPluginDelegate.java +164 -0
- data/src/main/java/org/embulk/output/mailchimp/MailChimpRecordBuffer.java +174 -0
- data/src/main/java/org/embulk/output/mailchimp/helper/MailChimpHelper.java +70 -0
- data/src/main/java/org/embulk/output/mailchimp/model/AuthMethod.java +58 -0
- data/src/main/java/org/embulk/output/mailchimp/model/CategoriesResponse.java +30 -0
- data/src/main/java/org/embulk/output/mailchimp/model/ErrorResponse.java +56 -0
- data/src/main/java/org/embulk/output/mailchimp/model/InterestCategoriesResponse.java +24 -0
- data/src/main/java/org/embulk/output/mailchimp/model/InterestResponse.java +30 -0
- data/src/main/java/org/embulk/output/mailchimp/model/InterestsResponse.java +24 -0
- data/src/main/java/org/embulk/output/mailchimp/model/MemberStatus.java +64 -0
- data/src/main/java/org/embulk/output/mailchimp/model/MetaDataResponse.java +36 -0
- data/src/main/java/org/embulk/output/mailchimp/model/ReportResponse.java +105 -0
- data/src/main/java/org/embulk/output/mailchimp/validation/ColumnDataValidator.java +40 -0
- data/src/test/java/org/embulk/output/mailchimp/CircleCICredentials.java +22 -0
- data/src/test/java/org/embulk/output/mailchimp/TestColumnDataValidator.java +43 -0
- data/src/test/java/org/embulk/output/mailchimp/TestMailChimpHelper.java +54 -0
- data/src/test/java/org/embulk/output/mailchimp/TestMailChimpOutputPlugin.java +158 -0
- data/src/test/resources/csv/email.csv +2 -0
- metadata +55 -122
- data/.ruby-version +0 -1
- data/Gemfile +0 -2
- data/Rakefile +0 -29
- data/embulk-output-mailchimp.gemspec +0 -27
@@ -0,0 +1,320 @@
|
|
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.ObjectNode;
|
10
|
+
import com.google.common.base.Function;
|
11
|
+
import com.google.common.base.Throwables;
|
12
|
+
import com.google.common.collect.FluentIterable;
|
13
|
+
import org.embulk.base.restclient.jackson.JacksonServiceRecord;
|
14
|
+
import org.embulk.base.restclient.record.RecordBuffer;
|
15
|
+
import org.embulk.base.restclient.record.ServiceRecord;
|
16
|
+
import org.embulk.config.TaskReport;
|
17
|
+
import org.embulk.output.mailchimp.model.ErrorResponse;
|
18
|
+
import org.embulk.output.mailchimp.model.InterestResponse;
|
19
|
+
import org.embulk.output.mailchimp.model.ReportResponse;
|
20
|
+
import org.embulk.spi.Column;
|
21
|
+
import org.embulk.spi.DataException;
|
22
|
+
import org.embulk.spi.Exec;
|
23
|
+
import org.embulk.spi.Schema;
|
24
|
+
import org.slf4j.Logger;
|
25
|
+
|
26
|
+
import java.io.IOException;
|
27
|
+
import java.text.MessageFormat;
|
28
|
+
import java.util.ArrayList;
|
29
|
+
import java.util.HashMap;
|
30
|
+
import java.util.List;
|
31
|
+
import java.util.Map;
|
32
|
+
|
33
|
+
import static org.embulk.output.mailchimp.helper.MailChimpHelper.containsCaseInsensitive;
|
34
|
+
import static org.embulk.output.mailchimp.model.MemberStatus.PENDING;
|
35
|
+
import static org.embulk.output.mailchimp.model.MemberStatus.SUBSCRIBED;
|
36
|
+
|
37
|
+
/**
|
38
|
+
* Created by thangnc on 4/14/17.
|
39
|
+
*/
|
40
|
+
public abstract class MailChimpAbstractRecordBuffer
|
41
|
+
extends RecordBuffer
|
42
|
+
{
|
43
|
+
private static final Logger LOG = Exec.getLogger(MailChimpAbstractRecordBuffer.class);
|
44
|
+
private static final int MAX_RECORD_PER_BATCH_REQUEST = 500;
|
45
|
+
/**
|
46
|
+
* The constant mailchimpEndpoint.
|
47
|
+
*/
|
48
|
+
protected static String mailchimpEndpoint = "https://{0}.api.mailchimp.com/3.0";
|
49
|
+
private final MailChimpOutputPluginDelegate.PluginTask task;
|
50
|
+
private final ObjectMapper mapper;
|
51
|
+
private final Schema schema;
|
52
|
+
private int requestCount;
|
53
|
+
private long totalCount;
|
54
|
+
private List<JsonNode> records;
|
55
|
+
private Map<String, Map<String, InterestResponse>> categories;
|
56
|
+
|
57
|
+
/**
|
58
|
+
* Instantiates a new Mail chimp abstract record buffer.
|
59
|
+
*
|
60
|
+
* @param schema the schema
|
61
|
+
* @param task the task
|
62
|
+
*/
|
63
|
+
public MailChimpAbstractRecordBuffer(final Schema schema, final MailChimpOutputPluginDelegate.PluginTask task)
|
64
|
+
{
|
65
|
+
this.schema = schema;
|
66
|
+
this.task = task;
|
67
|
+
this.mapper = new ObjectMapper()
|
68
|
+
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
69
|
+
.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, false);
|
70
|
+
this.records = new ArrayList<>();
|
71
|
+
this.categories = new HashMap<>();
|
72
|
+
}
|
73
|
+
|
74
|
+
@Override
|
75
|
+
public void bufferRecord(ServiceRecord serviceRecord)
|
76
|
+
{
|
77
|
+
JacksonServiceRecord jacksonServiceRecord;
|
78
|
+
|
79
|
+
try {
|
80
|
+
jacksonServiceRecord = (JacksonServiceRecord) serviceRecord;
|
81
|
+
JsonNode record = mapper.readTree(jacksonServiceRecord.toString()).get("record");
|
82
|
+
|
83
|
+
requestCount++;
|
84
|
+
totalCount++;
|
85
|
+
|
86
|
+
records.add(record);
|
87
|
+
if (requestCount >= MAX_RECORD_PER_BATCH_REQUEST) {
|
88
|
+
ObjectNode subcribers = processSubcribers(records, task);
|
89
|
+
ReportResponse reportResponse = push(subcribers, task);
|
90
|
+
|
91
|
+
if (totalCount % 1000 == 0) {
|
92
|
+
LOG.info("Pushed {} records", totalCount);
|
93
|
+
}
|
94
|
+
|
95
|
+
LOG.info("{} records created, {} records updated, {} records failed",
|
96
|
+
reportResponse.getTotalCreated(),
|
97
|
+
reportResponse.getTotalUpdated(),
|
98
|
+
reportResponse.getErrorCount());
|
99
|
+
handleErrors(reportResponse.getErrors());
|
100
|
+
|
101
|
+
records = new ArrayList<>();
|
102
|
+
requestCount = 0;
|
103
|
+
}
|
104
|
+
}
|
105
|
+
catch (JsonProcessingException jpe) {
|
106
|
+
throw new DataException(jpe);
|
107
|
+
}
|
108
|
+
catch (ClassCastException ex) {
|
109
|
+
throw new RuntimeException(ex);
|
110
|
+
}
|
111
|
+
catch (IOException ex) {
|
112
|
+
throw Throwables.propagate(ex);
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
@Override
|
117
|
+
public TaskReport commitWithTaskReportUpdated(TaskReport taskReport)
|
118
|
+
{
|
119
|
+
try {
|
120
|
+
if (records.size() > 0) {
|
121
|
+
ObjectNode subcribers = processSubcribers(records, task);
|
122
|
+
ReportResponse reportResponse = push(subcribers, task);
|
123
|
+
LOG.info("Pushed {} records", records.size());
|
124
|
+
LOG.info("{} records created, {} records updated, {} records failed",
|
125
|
+
reportResponse.getTotalCreated(),
|
126
|
+
reportResponse.getTotalUpdated(),
|
127
|
+
reportResponse.getErrorCount());
|
128
|
+
handleErrors(reportResponse.getErrors());
|
129
|
+
}
|
130
|
+
|
131
|
+
cleanUp();
|
132
|
+
return Exec.newTaskReport().set("pushed", totalCount);
|
133
|
+
}
|
134
|
+
catch (JsonProcessingException jpe) {
|
135
|
+
throw new DataException(jpe);
|
136
|
+
}
|
137
|
+
}
|
138
|
+
|
139
|
+
/**
|
140
|
+
* Receive data and build payload json that contains subscribers
|
141
|
+
*
|
142
|
+
* @param data the data
|
143
|
+
* @param task the task
|
144
|
+
* @return the object node
|
145
|
+
*/
|
146
|
+
ObjectNode processSubcribers(final List<JsonNode> data, final MailChimpOutputPluginDelegate.PluginTask task)
|
147
|
+
{
|
148
|
+
LOG.info("Start to process subscriber data");
|
149
|
+
extractDataCenterBasedOnAuthMethod();
|
150
|
+
extractInterestCategories();
|
151
|
+
|
152
|
+
// Required merge fields
|
153
|
+
Map<String, String> map = new HashMap<>();
|
154
|
+
map.put("FNAME", task.getFnameColumn());
|
155
|
+
map.put("LNAME", task.getLnameColumn());
|
156
|
+
|
157
|
+
List<JsonNode> subscribersList = FluentIterable.from(data)
|
158
|
+
.transform(contactMapper(map))
|
159
|
+
.toList();
|
160
|
+
|
161
|
+
ObjectNode subscribers = JsonNodeFactory.instance.objectNode();
|
162
|
+
subscribers.putArray("members").addAll(subscribersList);
|
163
|
+
subscribers.put("update_existing", task.getUpdateExisting());
|
164
|
+
return subscribers;
|
165
|
+
}
|
166
|
+
|
167
|
+
/**
|
168
|
+
* Gets mapper.
|
169
|
+
*
|
170
|
+
* @return the mapper
|
171
|
+
*/
|
172
|
+
public ObjectMapper getMapper()
|
173
|
+
{
|
174
|
+
return mapper;
|
175
|
+
}
|
176
|
+
|
177
|
+
/**
|
178
|
+
* Gets categories.
|
179
|
+
*
|
180
|
+
* @return the categories
|
181
|
+
*/
|
182
|
+
public Map<String, Map<String, InterestResponse>> getCategories()
|
183
|
+
{
|
184
|
+
return categories;
|
185
|
+
}
|
186
|
+
|
187
|
+
/**
|
188
|
+
* Clean up.
|
189
|
+
*/
|
190
|
+
abstract void cleanUp();
|
191
|
+
|
192
|
+
/**
|
193
|
+
* Push payload data to MailChimp API and get @{@link ReportResponse}
|
194
|
+
*
|
195
|
+
* @param node the content
|
196
|
+
* @param task the task
|
197
|
+
* @return the report response
|
198
|
+
* @throws JsonProcessingException the json processing exception
|
199
|
+
*/
|
200
|
+
abstract ReportResponse push(final ObjectNode node, final MailChimpOutputPluginDelegate.PluginTask task)
|
201
|
+
throws JsonProcessingException;
|
202
|
+
|
203
|
+
/**
|
204
|
+
* Handle @{@link ErrorResponse} from MailChimp API if exists.
|
205
|
+
*
|
206
|
+
* @param errorResponses the error responses
|
207
|
+
*/
|
208
|
+
abstract void handleErrors(final List<ErrorResponse> errorResponses);
|
209
|
+
|
210
|
+
/**
|
211
|
+
* Find interest category ids by pre-defined group name which user input.
|
212
|
+
*
|
213
|
+
* @param task the task
|
214
|
+
* @return the map
|
215
|
+
* @throws JsonProcessingException the json processing exception
|
216
|
+
*/
|
217
|
+
abstract Map<String, Map<String, InterestResponse>> extractInterestCategoriesByGroupNames(final MailChimpOutputPluginDelegate.PluginTask task)
|
218
|
+
throws JsonProcessingException;
|
219
|
+
|
220
|
+
/**
|
221
|
+
* Extract data center from MailChimp v3 metadata.
|
222
|
+
*
|
223
|
+
* @param task the task
|
224
|
+
* @return the string
|
225
|
+
* @throws JsonProcessingException the json processing exception
|
226
|
+
*/
|
227
|
+
abstract String extractDataCenter(final MailChimpOutputPluginDelegate.PluginTask task)
|
228
|
+
throws JsonProcessingException;
|
229
|
+
|
230
|
+
private void extractDataCenterBasedOnAuthMethod()
|
231
|
+
{
|
232
|
+
try {
|
233
|
+
// Extract data center from meta data URL
|
234
|
+
String dc = extractDataCenter(task);
|
235
|
+
mailchimpEndpoint = MessageFormat.format(mailchimpEndpoint, dc);
|
236
|
+
}
|
237
|
+
catch (JsonProcessingException jpe) {
|
238
|
+
throw new DataException(jpe);
|
239
|
+
}
|
240
|
+
}
|
241
|
+
|
242
|
+
private void extractInterestCategories()
|
243
|
+
{
|
244
|
+
try {
|
245
|
+
// Should loop the names and get the id of interest categories.
|
246
|
+
// The reason why we put categories validation here because we can not share data between instance.
|
247
|
+
categories = extractInterestCategoriesByGroupNames(task);
|
248
|
+
}
|
249
|
+
catch (JsonProcessingException jpe) {
|
250
|
+
throw new DataException(jpe);
|
251
|
+
}
|
252
|
+
}
|
253
|
+
|
254
|
+
private Function<JsonNode, JsonNode> contactMapper(final Map<String, String> allowColumns)
|
255
|
+
{
|
256
|
+
return new Function<JsonNode, JsonNode>()
|
257
|
+
{
|
258
|
+
@Override
|
259
|
+
public JsonNode apply(JsonNode input)
|
260
|
+
{
|
261
|
+
ObjectNode property = JsonNodeFactory.instance.objectNode();
|
262
|
+
property.put("email_address", input.findPath(task.getEmailColumn()).asText());
|
263
|
+
property.put("status", task.getDoubleOptIn() ? PENDING.getType() : SUBSCRIBED.getType());
|
264
|
+
ObjectNode mergeFields = JsonNodeFactory.instance.objectNode();
|
265
|
+
for (String allowColumn : allowColumns.keySet()) {
|
266
|
+
String value = input.findValue(allowColumns.get(allowColumn)).asText();
|
267
|
+
mergeFields.put(allowColumn, value);
|
268
|
+
}
|
269
|
+
|
270
|
+
// Update additional merge fields if exist
|
271
|
+
if (task.getMergeFields().isPresent() && !task.getMergeFields().get().isEmpty()) {
|
272
|
+
for (final Column column : schema.getColumns()) {
|
273
|
+
if (!"".equals(containsCaseInsensitive(column.getName(), task.getMergeFields().get()))) {
|
274
|
+
String value = input.findValue(column.getName()).asText();
|
275
|
+
mergeFields.put(column.getName().toUpperCase(), value);
|
276
|
+
}
|
277
|
+
}
|
278
|
+
}
|
279
|
+
|
280
|
+
property.set("merge_fields", mergeFields);
|
281
|
+
|
282
|
+
// Update interest categories if exist
|
283
|
+
if (task.getGroupingColumns().isPresent() && !task.getGroupingColumns().get().isEmpty()) {
|
284
|
+
property.set("interests", buildInterestCategories(task, input));
|
285
|
+
}
|
286
|
+
|
287
|
+
return property;
|
288
|
+
}
|
289
|
+
};
|
290
|
+
}
|
291
|
+
|
292
|
+
private ObjectNode buildInterestCategories(final MailChimpOutputPluginDelegate.PluginTask task,
|
293
|
+
final JsonNode input)
|
294
|
+
{
|
295
|
+
ObjectNode interests = JsonNodeFactory.instance.objectNode();
|
296
|
+
|
297
|
+
for (String category : task.getGroupingColumns().get()) {
|
298
|
+
String value = input.findValue(category).asText();
|
299
|
+
Map<String, InterestResponse> availableCategories = categories.get(category);
|
300
|
+
|
301
|
+
// Only update user-predefined categories if replace interests != true
|
302
|
+
// Otherwise, force update all categories include user-predefined categories
|
303
|
+
if (!task.getReplaceInterests() && availableCategories.get(value) != null) {
|
304
|
+
interests.put(availableCategories.get(value).getId(), true);
|
305
|
+
}
|
306
|
+
else if (task.getReplaceInterests()) {
|
307
|
+
for (String availableCategory : availableCategories.keySet()) {
|
308
|
+
if (availableCategory.equals(value)) {
|
309
|
+
interests.put(availableCategories.get(availableCategory).getId(), true);
|
310
|
+
}
|
311
|
+
else {
|
312
|
+
interests.put(availableCategories.get(availableCategory).getId(), false);
|
313
|
+
}
|
314
|
+
}
|
315
|
+
}
|
316
|
+
}
|
317
|
+
|
318
|
+
return interests;
|
319
|
+
}
|
320
|
+
}
|
@@ -0,0 +1,151 @@
|
|
1
|
+
package org.embulk.output.mailchimp;
|
2
|
+
|
3
|
+
import com.fasterxml.jackson.core.Base64Variants;
|
4
|
+
import com.fasterxml.jackson.databind.JsonNode;
|
5
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
6
|
+
import com.fasterxml.jackson.databind.node.MissingNode;
|
7
|
+
import com.google.common.base.Throwables;
|
8
|
+
import org.eclipse.jetty.client.HttpClient;
|
9
|
+
import org.eclipse.jetty.client.api.Request;
|
10
|
+
import org.eclipse.jetty.client.api.Response;
|
11
|
+
import org.eclipse.jetty.client.util.StringContentProvider;
|
12
|
+
import org.eclipse.jetty.http.HttpMethod;
|
13
|
+
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
14
|
+
import org.embulk.config.ConfigException;
|
15
|
+
import org.embulk.spi.DataException;
|
16
|
+
import org.embulk.spi.Exec;
|
17
|
+
import org.embulk.util.retryhelper.jetty92.Jetty92ClientCreator;
|
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.io.IOException;
|
24
|
+
|
25
|
+
/**
|
26
|
+
* Created by thangnc on 4/14/17.
|
27
|
+
*/
|
28
|
+
public class MailChimpHttpClient
|
29
|
+
{
|
30
|
+
private static final Logger LOG = Exec.getLogger(MailChimpHttpClient.class);
|
31
|
+
private final ObjectMapper jsonMapper = new ObjectMapper()
|
32
|
+
.configure(com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, false)
|
33
|
+
.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
34
|
+
private Jetty92RetryHelper retryHelper;
|
35
|
+
|
36
|
+
/**
|
37
|
+
* Instantiates a new Mailchimp http client.
|
38
|
+
*
|
39
|
+
* @param task the task
|
40
|
+
*/
|
41
|
+
public MailChimpHttpClient(MailChimpOutputPluginDelegate.PluginTask task)
|
42
|
+
{
|
43
|
+
retryHelper = createRetryHelper(task);
|
44
|
+
}
|
45
|
+
|
46
|
+
/**
|
47
|
+
* Close @{@link Jetty92RetryHelper} connection
|
48
|
+
*/
|
49
|
+
public void close()
|
50
|
+
{
|
51
|
+
if (retryHelper != null) {
|
52
|
+
retryHelper.close();
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
public JsonNode sendRequest(final String endpoint, final HttpMethod method,
|
57
|
+
final MailChimpOutputPluginDelegate.PluginTask task)
|
58
|
+
{
|
59
|
+
return sendRequest(endpoint, method, "", task);
|
60
|
+
}
|
61
|
+
|
62
|
+
public JsonNode sendRequest(final String endpoint, final HttpMethod method, final String content,
|
63
|
+
final MailChimpOutputPluginDelegate.PluginTask task)
|
64
|
+
{
|
65
|
+
final String authorizationHeader = getAuthorizationHeader(task);
|
66
|
+
|
67
|
+
String responseBody = retryHelper.requestWithRetry(
|
68
|
+
new StringJetty92ResponseEntityReader(task.getTimeoutMillis()),
|
69
|
+
new Jetty92SingleRequester()
|
70
|
+
{
|
71
|
+
@Override
|
72
|
+
public void requestOnce(HttpClient client, Response.Listener responseListener)
|
73
|
+
{
|
74
|
+
Request request = client
|
75
|
+
.newRequest(endpoint)
|
76
|
+
.accept("application/json")
|
77
|
+
.method(method);
|
78
|
+
if (method == HttpMethod.POST || method == HttpMethod.PUT) {
|
79
|
+
request.content(new StringContentProvider(content), "application/json;utf-8");
|
80
|
+
}
|
81
|
+
|
82
|
+
if (!authorizationHeader.isEmpty()) {
|
83
|
+
request.header("Authorization", authorizationHeader);
|
84
|
+
}
|
85
|
+
request.send(responseListener);
|
86
|
+
}
|
87
|
+
|
88
|
+
@Override
|
89
|
+
public boolean isResponseStatusToRetry(Response response)
|
90
|
+
{
|
91
|
+
int status = response.getStatus();
|
92
|
+
return status == 429 || status / 100 != 4;
|
93
|
+
}
|
94
|
+
});
|
95
|
+
|
96
|
+
return responseBody != null && !responseBody.isEmpty() ? parseJson(responseBody) : MissingNode.getInstance();
|
97
|
+
}
|
98
|
+
|
99
|
+
private JsonNode parseJson(final String json)
|
100
|
+
throws DataException
|
101
|
+
{
|
102
|
+
try {
|
103
|
+
return this.jsonMapper.readTree(json);
|
104
|
+
}
|
105
|
+
catch (IOException ex) {
|
106
|
+
throw new DataException(ex);
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
/**
|
111
|
+
* MailChimp API v3 supports non expires access_token. Then no need refresh_token
|
112
|
+
*
|
113
|
+
* @param task
|
114
|
+
* @return
|
115
|
+
*/
|
116
|
+
private String getAuthorizationHeader(final MailChimpOutputPluginDelegate.PluginTask task)
|
117
|
+
{
|
118
|
+
switch (task.getAuthMethod()) {
|
119
|
+
case OAUTH:
|
120
|
+
return "OAuth " + task.getAccessToken().orNull();
|
121
|
+
case API_KEY:
|
122
|
+
return "Basic " + Base64Variants.MIME_NO_LINEFEEDS
|
123
|
+
.encode(("apikey" + ":" + task.getApikey().orNull()).getBytes());
|
124
|
+
default:
|
125
|
+
throw new ConfigException("Not supported method");
|
126
|
+
}
|
127
|
+
}
|
128
|
+
|
129
|
+
private Jetty92RetryHelper createRetryHelper(MailChimpOutputPluginDelegate.PluginTask task)
|
130
|
+
{
|
131
|
+
return new Jetty92RetryHelper(
|
132
|
+
task.getMaximumRetries(),
|
133
|
+
task.getInitialRetryIntervalMillis(),
|
134
|
+
task.getMaximumRetryIntervalMillis(),
|
135
|
+
new Jetty92ClientCreator()
|
136
|
+
{
|
137
|
+
@Override
|
138
|
+
public HttpClient createAndStart()
|
139
|
+
{
|
140
|
+
HttpClient client = new HttpClient(new SslContextFactory());
|
141
|
+
try {
|
142
|
+
client.start();
|
143
|
+
return client;
|
144
|
+
}
|
145
|
+
catch (Exception e) {
|
146
|
+
throw Throwables.propagate(e);
|
147
|
+
}
|
148
|
+
}
|
149
|
+
});
|
150
|
+
}
|
151
|
+
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
package org.embulk.output.mailchimp;
|
2
|
+
|
3
|
+
import org.embulk.base.restclient.RestClientOutputPluginBase;
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Created by thangnc on 4/14/17.
|
7
|
+
*/
|
8
|
+
public class MailChimpOutputPlugin
|
9
|
+
extends RestClientOutputPluginBase<MailChimpOutputPluginDelegate.PluginTask>
|
10
|
+
{
|
11
|
+
/**
|
12
|
+
* Instantiates a new @{@link MailChimpOutputPlugin}.
|
13
|
+
*/
|
14
|
+
public MailChimpOutputPlugin()
|
15
|
+
{
|
16
|
+
super(MailChimpOutputPluginDelegate.PluginTask.class, new MailChimpOutputPluginDelegate());
|
17
|
+
}
|
18
|
+
}
|
@@ -0,0 +1,164 @@
|
|
1
|
+
package org.embulk.output.mailchimp;
|
2
|
+
|
3
|
+
import com.google.common.base.Optional;
|
4
|
+
import org.embulk.base.restclient.RestClientOutputPluginDelegate;
|
5
|
+
import org.embulk.base.restclient.RestClientOutputTaskBase;
|
6
|
+
import org.embulk.base.restclient.jackson.JacksonServiceRequestMapper;
|
7
|
+
import org.embulk.base.restclient.jackson.JacksonTopLevelValueLocator;
|
8
|
+
import org.embulk.base.restclient.jackson.scope.JacksonAllInObjectScope;
|
9
|
+
import org.embulk.base.restclient.record.RecordBuffer;
|
10
|
+
import org.embulk.config.Config;
|
11
|
+
import org.embulk.config.ConfigDefault;
|
12
|
+
import org.embulk.config.ConfigDiff;
|
13
|
+
import org.embulk.config.ConfigException;
|
14
|
+
import org.embulk.config.TaskReport;
|
15
|
+
import org.embulk.output.mailchimp.model.AuthMethod;
|
16
|
+
import org.embulk.spi.Exec;
|
17
|
+
import org.embulk.spi.Schema;
|
18
|
+
import org.slf4j.Logger;
|
19
|
+
|
20
|
+
import java.util.List;
|
21
|
+
|
22
|
+
import static com.google.common.base.Strings.isNullOrEmpty;
|
23
|
+
import static org.embulk.output.mailchimp.validation.ColumnDataValidator.checkExistColumns;
|
24
|
+
|
25
|
+
/**
|
26
|
+
* Created by thangnc on 4/14/17.
|
27
|
+
*/
|
28
|
+
public class MailChimpOutputPluginDelegate
|
29
|
+
implements RestClientOutputPluginDelegate<MailChimpOutputPluginDelegate.PluginTask>
|
30
|
+
{
|
31
|
+
private static final Logger LOG = Exec.getLogger(MailChimpOutputPluginDelegate.class);
|
32
|
+
|
33
|
+
public MailChimpOutputPluginDelegate()
|
34
|
+
{
|
35
|
+
}
|
36
|
+
|
37
|
+
public interface PluginTask
|
38
|
+
extends RestClientOutputTaskBase
|
39
|
+
{
|
40
|
+
@Config("maximum_retries")
|
41
|
+
@ConfigDefault("6")
|
42
|
+
int getMaximumRetries();
|
43
|
+
|
44
|
+
@Config("initial_retry_interval_millis")
|
45
|
+
@ConfigDefault("1000")
|
46
|
+
int getInitialRetryIntervalMillis();
|
47
|
+
|
48
|
+
@Config("maximum_retry_interval_millis")
|
49
|
+
@ConfigDefault("32000")
|
50
|
+
int getMaximumRetryIntervalMillis();
|
51
|
+
|
52
|
+
@Config("timeout_millis")
|
53
|
+
@ConfigDefault("60000")
|
54
|
+
int getTimeoutMillis();
|
55
|
+
|
56
|
+
@Config("auth_method")
|
57
|
+
@ConfigDefault("api_key")
|
58
|
+
AuthMethod getAuthMethod();
|
59
|
+
|
60
|
+
@Config("apikey")
|
61
|
+
@ConfigDefault("null")
|
62
|
+
Optional<String> getApikey();
|
63
|
+
|
64
|
+
@Config("access_token")
|
65
|
+
@ConfigDefault("null")
|
66
|
+
Optional<String> getAccessToken();
|
67
|
+
|
68
|
+
@Config("list_id")
|
69
|
+
String getListId();
|
70
|
+
|
71
|
+
@Config("email_column")
|
72
|
+
@ConfigDefault("email")
|
73
|
+
String getEmailColumn();
|
74
|
+
|
75
|
+
@Config("fname_column")
|
76
|
+
@ConfigDefault("fname")
|
77
|
+
String getFnameColumn();
|
78
|
+
|
79
|
+
@Config("lname_column")
|
80
|
+
@ConfigDefault("lname")
|
81
|
+
String getLnameColumn();
|
82
|
+
|
83
|
+
@Config("merge_fields")
|
84
|
+
@ConfigDefault("null")
|
85
|
+
Optional<List<String>> getMergeFields();
|
86
|
+
|
87
|
+
@Config("grouping_columns")
|
88
|
+
@ConfigDefault("null")
|
89
|
+
Optional<List<String>> getGroupingColumns();
|
90
|
+
|
91
|
+
@Config("double_optin")
|
92
|
+
@ConfigDefault("true")
|
93
|
+
boolean getDoubleOptIn();
|
94
|
+
|
95
|
+
@Config("update_existing")
|
96
|
+
@ConfigDefault("false")
|
97
|
+
boolean getUpdateExisting();
|
98
|
+
|
99
|
+
@Config("replace_interests")
|
100
|
+
@ConfigDefault("true")
|
101
|
+
boolean getReplaceInterests();
|
102
|
+
}
|
103
|
+
|
104
|
+
/**
|
105
|
+
* Override @{@link RestClientOutputPluginDelegate#validateOutputTask(RestClientOutputTaskBase, Schema, int)}
|
106
|
+
* This method not only validates required configurations but also validates required columns
|
107
|
+
*
|
108
|
+
* @param task
|
109
|
+
* @param schema
|
110
|
+
* @param taskCount
|
111
|
+
*/
|
112
|
+
@Override
|
113
|
+
public void validateOutputTask(final PluginTask task, final Schema schema, final int taskCount)
|
114
|
+
{
|
115
|
+
if (task.getAuthMethod() == AuthMethod.OAUTH) {
|
116
|
+
if (!task.getAccessToken().isPresent() || isNullOrEmpty(task.getAccessToken().get())) {
|
117
|
+
throw new ConfigException("'access_token' is required when auth_method is 'oauth'");
|
118
|
+
}
|
119
|
+
}
|
120
|
+
else if (task.getAuthMethod() == AuthMethod.API_KEY) {
|
121
|
+
if (!task.getApikey().isPresent() || isNullOrEmpty(task.getApikey().get())) {
|
122
|
+
throw new ConfigException("'apikey' is required when auth_method is 'api_key'");
|
123
|
+
}
|
124
|
+
}
|
125
|
+
|
126
|
+
if (isNullOrEmpty(task.getListId())) {
|
127
|
+
throw new ConfigException("'list_id' must not be null or empty string");
|
128
|
+
}
|
129
|
+
|
130
|
+
if (!checkExistColumns(schema, task.getEmailColumn(), task.getFnameColumn(), task.getLnameColumn())) {
|
131
|
+
throw new ConfigException("Columns ['email', 'fname', 'lname'] must not be null or empty string");
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
135
|
+
@Override
|
136
|
+
public RecordBuffer buildRecordBuffer(PluginTask task, Schema schema, int taskIndex)
|
137
|
+
{
|
138
|
+
return new MailChimpRecordBuffer(schema, task);
|
139
|
+
}
|
140
|
+
|
141
|
+
@Override
|
142
|
+
public JacksonServiceRequestMapper buildServiceRequestMapper(final PluginTask task)
|
143
|
+
{
|
144
|
+
return JacksonServiceRequestMapper.builder()
|
145
|
+
.add(new JacksonAllInObjectScope(), new JacksonTopLevelValueLocator("record"))
|
146
|
+
.build();
|
147
|
+
}
|
148
|
+
|
149
|
+
@Override
|
150
|
+
public ConfigDiff egestEmbulkData(final PluginTask task, final Schema schema, final int taskCount,
|
151
|
+
final List<TaskReport> taskReports)
|
152
|
+
{
|
153
|
+
long totalInserted = 0;
|
154
|
+
for (TaskReport taskReport : taskReports) {
|
155
|
+
if (taskReport.has("pushed")) {
|
156
|
+
totalInserted += taskReport.get(Long.class, "pushed");
|
157
|
+
}
|
158
|
+
}
|
159
|
+
|
160
|
+
LOG.info("Pushed completed. {} records", totalInserted);
|
161
|
+
|
162
|
+
return Exec.newConfigDiff();
|
163
|
+
}
|
164
|
+
}
|