embulk-output-mailchimp 0.2.3 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|