e;
51 |
52 | while (it.hasNext()){
53 | e = it.next();
54 | config.setProperty("hibernate." + e.getKey(), e.getValue());
55 | }
56 | }
57 |
58 | /**
59 | * Connect to database using hibernate
60 | */
61 | public void establishSession() {
62 |
63 | LOG.info("Opening hibernate session");
64 |
65 | serviceRegistry = new StandardServiceRegistryBuilder()
66 | .applySettings(config.getProperties()).build();
67 | factory = config.buildSessionFactory(serviceRegistry);
68 | session = factory.openSession();
69 | session.setCacheMode(CacheMode.IGNORE);
70 |
71 | session.setDefaultReadOnly(sqlSourceHelper.isReadOnlySession());
72 | }
73 |
74 | /**
75 | * Close database connection
76 | */
77 | public void closeSession() {
78 |
79 | LOG.info("Closing hibernate session");
80 |
81 | session.close();
82 | factory.close();
83 | }
84 |
85 | /**
86 | * Execute the selection query in the database
87 | * @return The query result. Each Object is a cell content.
88 | * The cell contents use database types (date,int,string...),
89 | * keep in mind in case of future conversions/castings.
90 | * @throws InterruptedException
91 | */
92 | @SuppressWarnings("unchecked")
93 | public List> executeQuery() throws InterruptedException {
94 |
95 | List> rowsList = new ArrayList>() ;
96 | Query query;
97 |
98 | if (!session.isConnected()){
99 | resetConnection();
100 | }
101 |
102 | if (sqlSourceHelper.isCustomQuerySet()){
103 |
104 | query = session.createSQLQuery(sqlSourceHelper.buildQuery());
105 |
106 | if (sqlSourceHelper.getMaxRows() != 0){
107 | query = query.setMaxResults(sqlSourceHelper.getMaxRows());
108 | }
109 | }
110 | else
111 | {
112 | query = session
113 | .createSQLQuery(sqlSourceHelper.getQuery())
114 | .setFirstResult(Integer.parseInt(sqlSourceHelper.getCurrentIndex()));
115 |
116 | if (sqlSourceHelper.getMaxRows() != 0){
117 | query = query.setMaxResults(sqlSourceHelper.getMaxRows());
118 | }
119 | }
120 |
121 | try {
122 | rowsList = query.setFetchSize(sqlSourceHelper.getMaxRows()).setResultTransformer(Transformers.TO_LIST).list();
123 | }catch (Exception e){
124 | LOG.error("Exception thrown, resetting connection.",e);
125 | resetConnection();
126 | }
127 |
128 | if (!rowsList.isEmpty()){
129 | if (sqlSourceHelper.isCustomQuerySet()){
130 | sqlSourceHelper.setCurrentIndex(rowsList.get(rowsList.size()-1).get(0).toString());
131 | }
132 | else
133 | {
134 | sqlSourceHelper.setCurrentIndex(Integer.toString((Integer.parseInt(sqlSourceHelper.getCurrentIndex())
135 | + rowsList.size())));
136 | }
137 | }
138 |
139 | return rowsList;
140 | }
141 |
142 | /**
143 | * Execute the selection query in the database
144 | * @return The query result. Each Object is a cell content.
145 | * The cell contents use database types (date,int,string...),
146 | * keep in mind in case of future conversions/castings.
147 | * @throws InterruptedException
148 | */
149 | public List> executeQueryForJson() throws InterruptedException {
150 |
151 | List> rowsList = new ArrayList>() ;
152 | Query query;
153 |
154 | if (!session.isConnected()){
155 | resetConnection();
156 | }
157 |
158 | if (sqlSourceHelper.isCustomQuerySet()){
159 |
160 | query = session.createSQLQuery(sqlSourceHelper.buildQuery());
161 |
162 | if (sqlSourceHelper.getMaxRows() != 0){
163 | query = query.setMaxResults(sqlSourceHelper.getMaxRows());
164 | }
165 | }
166 | else
167 | {
168 | query = session
169 | .createSQLQuery(sqlSourceHelper.getQuery())
170 | .setFirstResult(Integer.parseInt(sqlSourceHelper.getCurrentIndex()));
171 |
172 | if (sqlSourceHelper.getMaxRows() != 0){
173 | query = query.setMaxResults(sqlSourceHelper.getMaxRows());
174 | }
175 | }
176 |
177 | try {
178 | rowsList = query.setFetchSize(sqlSourceHelper.getMaxRows()).setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP).list();
179 | }catch (Exception e){
180 | LOG.error("Exception thrown, resetting connection.",e);
181 | resetConnection();
182 | }
183 |
184 | if (!rowsList.isEmpty()){
185 | if (sqlSourceHelper.isCustomQuerySet()){
186 | sqlSourceHelper.setCurrentIndex(rowsList.get(rowsList.size()-1).get(sqlSourceHelper.getOrderBy()).toString());
187 | }
188 | else
189 | {
190 | sqlSourceHelper.setCurrentIndex(Integer.toString((Integer.parseInt(sqlSourceHelper.getCurrentIndex())
191 | + rowsList.size())));
192 | }
193 | }
194 |
195 | return rowsList;
196 | }
197 |
198 | private void resetConnection() throws InterruptedException{
199 | session.close();
200 | factory.close();
201 | establishSession();
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/src/main/java/org/keedio/flume/source/SQLSource.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Licensed to the Apache Software Foundation (ASF) under one
3 | * or more contributor license agreements. See the NOTICE file
4 | * distributed with this work for additional information
5 | * regarding copyright ownership. The ASF licenses this file
6 | * to you under the Apache License, Version 2.0 (the
7 | * "License"); you may not use this file except in compliance
8 | * with the License. You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing,
13 | * software distributed under the License is distributed on an
14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | * KIND, either express or implied. See the License for the
16 | * specific language governing permissions and limitations
17 | * under the License.
18 | *******************************************************************************/
19 | package org.keedio.flume.source;
20 |
21 | import org.apache.flume.Context;
22 | import org.apache.flume.Event;
23 | import org.apache.flume.EventDeliveryException;
24 | import org.apache.flume.PollableSource;
25 | import org.apache.flume.conf.Configurable;
26 | import org.apache.flume.event.SimpleEvent;
27 | import org.apache.flume.source.AbstractSource;
28 | import org.keedio.flume.metrics.SqlSourceCounter;
29 | import org.slf4j.Logger;
30 | import org.slf4j.LoggerFactory;
31 |
32 | import java.io.IOException;
33 | import java.io.PrintWriter;
34 | import java.io.Writer;
35 | import java.util.ArrayList;
36 | import java.util.HashMap;
37 | import java.util.List;
38 | import java.util.Map;
39 |
40 |
41 | /**
42 | * A Source to read data from a SQL database. This source ask for new data in a table each configured time.
43 | *
44 | * @author Marcelo Valle
45 | */
46 | public class SQLSource extends AbstractSource implements Configurable, PollableSource {
47 |
48 | private static final Logger LOG = LoggerFactory.getLogger(SQLSource.class);
49 | protected SQLSourceHelper sqlSourceHelper;
50 | private SqlSourceCounter sqlSourceCounter;
51 | // private CSVWriter csvWriter;
52 | private PrintWriter printWriter ;
53 | private HibernateHelper hibernateHelper;
54 |
55 | /**
56 | * Configure the source, load configuration properties and establish connection with database
57 | */
58 | @Override
59 | public void configure(Context context) {
60 |
61 | LOG.getName();
62 |
63 | LOG.info("Reading and processing configuration values for source " + getName());
64 |
65 | /* Initialize configuration parameters */
66 | sqlSourceHelper = new SQLSourceHelper(context, this.getName());
67 |
68 | /* Initialize metric counters */
69 | sqlSourceCounter = new SqlSourceCounter("SOURCESQL." + this.getName());
70 |
71 | /* Establish connection with database */
72 | hibernateHelper = new HibernateHelper(sqlSourceHelper);
73 | hibernateHelper.establishSession();
74 |
75 | /* Instantiate the CSV Writer */
76 | // csvWriter = new CSVWriter(new ChannelWriter(),sqlSourceHelper.getDelimiterEntry().charAt(0));
77 | printWriter = new PrintWriter(new ChannelWriter());
78 | }
79 |
80 | /**
81 | * Process a batch of events performing SQL Queries
82 | */
83 | @Override
84 | public Status process() throws EventDeliveryException {
85 |
86 | try {
87 | sqlSourceCounter.startProcess();
88 |
89 | // List> result = hibernateHelper.executeQue
90 | List> result = hibernateHelper.executeQueryForJson();
91 | if (!result.isEmpty())
92 | {
93 | sqlSourceHelper.writeAllRows(result,printWriter);
94 | printWriter.flush();
95 | // csvWriter.writeAll(sqlSourceHelper.getAllRows(result),sqlSourceHelper.encloseByQuotes());
96 | // csvWriter.flush();
97 | sqlSourceCounter.incrementEventCount(result.size());
98 | sqlSourceHelper.updateStatusFile();
99 | }
100 |
101 | sqlSourceCounter.endProcess(result.size());
102 |
103 | if (result.size() < sqlSourceHelper.getMaxRows()){
104 | Thread.sleep(sqlSourceHelper.getRunQueryDelay());
105 | }
106 | return Status.READY;
107 |
108 | } catch (InterruptedException e) {
109 | LOG.error("Error procesing row", e);
110 | return Status.BACKOFF;
111 | }
112 | }
113 |
114 | @Override
115 | public long getBackOffSleepIncrement() {
116 | return 1000;
117 | }
118 |
119 | @Override
120 | public long getMaxBackOffSleepInterval() {
121 | return 5000;
122 | }
123 |
124 | /**
125 | * Starts the source. Starts the metrics counter.
126 | */
127 | @Override
128 | public void start() {
129 |
130 | LOG.info("Starting sql source {} ...", getName());
131 | sqlSourceCounter.start();
132 | super.start();
133 | }
134 |
135 | /**
136 | * Stop the source. Close database connection and stop metrics counter.
137 | */
138 | @Override
139 | public void stop() {
140 |
141 | LOG.info("Stopping sql source {} ...", getName());
142 |
143 | try
144 | {
145 | hibernateHelper.closeSession();
146 | // csvWriter.close();
147 | printWriter.close();
148 | } catch (Exception e) {
149 | LOG.warn("Error CSVWriter object ", e);
150 | } finally {
151 | this.sqlSourceCounter.stop();
152 | super.stop();
153 | }
154 | }
155 |
156 | private class ChannelWriter extends Writer{
157 | private List events = new ArrayList<>();
158 |
159 | @Override
160 | public void write(char[] cbuf, int off, int len) throws IOException {
161 | Event event = new SimpleEvent();
162 |
163 | String s = new String(cbuf);
164 | // The reason for this number [len] minus one is that the source string adds the line_end
165 | event.setBody(s.substring(off, len-1).getBytes());
166 | // LOG.info("event body:{}||",new String(event.getBody()));
167 |
168 | Map headers;
169 | headers = new HashMap();
170 | headers.put("timestamp", String.valueOf(System.currentTimeMillis()));
171 | event.setHeaders(headers);
172 | events.add(event);
173 | if (events.size() >= sqlSourceHelper.getBatchSize()) {
174 | flush();
175 | }
176 | }
177 |
178 | @Override
179 | public void flush() throws IOException {
180 | getChannelProcessor().processEventBatch(events);
181 | events.clear();
182 | }
183 |
184 | @Override
185 | public void close() throws IOException {
186 | flush();
187 | }
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/dependency-reduced-pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 | org.keedio.flume.flume-ng-sources
5 | flume-ng-sql-source-json
6 | Flume SQL Source
7 | 1.0
8 | This project is used for flume-ng to import data from SQL databases.
9 | https://github.com/keedio/flume-ng-sql-source
10 |
11 |
12 | Luis Alfonso Lázaro Medina
13 | lalazaro@keedio.org
14 | Keedio
15 | http://www.keedio.com
16 |
17 |
18 | Marcelo Valle Ávila
19 | mvalle@keedio.org
20 | Keedio
21 | http://www.keedio.com
22 |
23 |
24 | Luca Rosellini
25 | lrosellini@keedio.org
26 | Keedio
27 | http://www.keedio.com
28 |
29 |
30 |
31 |
32 | The Apache License, Version 2.0
33 | http://www.apache.org/licenses/LICENSE-2.0.txt
34 |
35 |
36 |
37 | scm:git:git@github.com:keedio/flume-ng-sql-source.git
38 | scm:git:git@github.com:keedio/flume-ng-sql-source.git
39 | https://github.com/keedio/flume-ng-sql-source.git
40 |
41 |
42 | Keedio
43 | www.keedio.org
44 |
45 |
46 |
47 |
48 | maven-compiler-plugin
49 | 3.1
50 |
51 | 1.7
52 | 1.7
53 |
54 |
55 |
56 | maven-shade-plugin
57 | 2.3
58 |
59 |
60 | package
61 |
62 | shade
63 |
64 |
65 |
66 |
67 |
68 | maven-surefire-plugin
69 | 2.18.1
70 |
71 |
72 | maven-source-plugin
73 | 2.2.1
74 |
75 |
76 | attach-sources
77 |
78 | jar
79 |
80 |
81 |
82 |
83 |
84 | maven-javadoc-plugin
85 | 2.9.1
86 |
87 |
88 | attach-javadocs
89 |
90 | jar
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | org.apache.flume
100 | flume-ng-core
101 | 1.8.0
102 | provided
103 |
104 |
105 | flume-ng-sdk
106 | org.apache.flume
107 |
108 |
109 | flume-ng-configuration
110 | org.apache.flume
111 |
112 |
113 | flume-ng-auth
114 | org.apache.flume
115 |
116 |
117 | slf4j-api
118 | org.slf4j
119 |
120 |
121 | guava
122 | com.google.guava
123 |
124 |
125 | commons-io
126 | commons-io
127 |
128 |
129 | commons-codec
130 | commons-codec
131 |
132 |
133 | log4j
134 | log4j
135 |
136 |
137 | slf4j-log4j12
138 | org.slf4j
139 |
140 |
141 | commons-cli
142 | commons-cli
143 |
144 |
145 | commons-lang
146 | commons-lang
147 |
148 |
149 | avro
150 | org.apache.avro
151 |
152 |
153 | avro-ipc
154 | org.apache.avro
155 |
156 |
157 | netty
158 | io.netty
159 |
160 |
161 | joda-time
162 | joda-time
163 |
164 |
165 | servlet-api
166 | org.mortbay.jetty
167 |
168 |
169 | jetty-util
170 | org.mortbay.jetty
171 |
172 |
173 | jetty
174 | org.mortbay.jetty
175 |
176 |
177 | gson
178 | com.google.code.gson
179 |
180 |
181 | libthrift
182 | org.apache.thrift
183 |
184 |
185 | mina-core
186 | org.apache.mina
187 |
188 |
189 |
190 |
191 | mysql
192 | mysql-connector-java
193 | 5.1.43
194 | provided
195 |
196 |
197 | org.mockito
198 | mockito-core
199 | 1.10.19
200 | test
201 |
202 |
203 | objenesis
204 | org.objenesis
205 |
206 |
207 |
208 |
209 | org.powermock
210 | powermock-module-junit4
211 | 1.6.2
212 | test
213 |
214 |
215 | powermock-module-junit4-common
216 | org.powermock
217 |
218 |
219 |
220 |
221 | org.powermock
222 | powermock-api-mockito
223 | 1.6.2
224 | test
225 |
226 |
227 | mockito-all
228 | org.mockito
229 |
230 |
231 | powermock-api-support
232 | org.powermock
233 |
234 |
235 |
236 |
237 |
238 |
239 |
--------------------------------------------------------------------------------
/src/test/java/org/keedio/flume/source/SQLSourceHelperTest.java:
--------------------------------------------------------------------------------
1 | package org.keedio.flume.source;
2 |
3 | import com.google.common.collect.ImmutableMap;
4 | import com.google.common.collect.Maps;
5 | import com.google.gson.Gson;
6 | import com.google.gson.reflect.TypeToken;
7 | import com.google.gson.stream.JsonWriter;
8 | import org.apache.commons.io.FileUtils;
9 | import org.apache.flume.Context;
10 | import org.apache.flume.Event;
11 | import org.apache.flume.conf.ConfigurationException;
12 | import org.apache.flume.event.SimpleEvent;
13 | import org.junit.After;
14 | import org.junit.Before;
15 | import org.junit.Test;
16 |
17 | import java.io.*;
18 | import java.lang.reflect.Type;
19 | import java.sql.Date;
20 | import java.text.SimpleDateFormat;
21 | import java.util.ArrayList;
22 | import java.util.HashMap;
23 | import java.util.List;
24 | import java.util.Map;
25 |
26 | import static org.junit.Assert.assertArrayEquals;
27 | import static org.junit.Assert.assertEquals;
28 | import static org.mockito.Mockito.mock;
29 | import static org.mockito.Mockito.when;
30 |
31 | /**
32 | * @author Marcelo Valle https://github.com/mvalleavila
33 | */
34 |
35 | //@RunWith(PowerMockRunner.class)
36 | public class SQLSourceHelperTest {
37 |
38 | Context context = mock(Context.class);
39 |
40 | @Before
41 | public void setup() {
42 |
43 | when(context.getString("status.file.name")).thenReturn("statusFileName.txt");
44 | when(context.getString("hibernate.connection.url")).thenReturn("jdbc:mysql://localhost:3306/database");
45 | when(context.getString("table")).thenReturn("my_table");
46 | when(context.getString("incremental.column.name")).thenReturn("incrementalColumName");
47 | when(context.getString("status.file.path", "/var/lib/flume")).thenReturn("/tmp/flume");
48 | when(context.getString("columns.to.select", "*")).thenReturn("*");
49 | when(context.getInteger("run.query.delay", 10000)).thenReturn(10000);
50 | when(context.getInteger("batch.size", 100)).thenReturn(100);
51 | when(context.getInteger("max.rows", 100)).thenReturn(100);
52 | when(context.getString("incremental.value", "0")).thenReturn("0");
53 | when(context.getString("start.from", "0")).thenReturn("0");
54 | }
55 |
56 | /*
57 | @Test
58 | public void checkNotCreatedDirectory() throws Exception {
59 |
60 | SQLSourceHelper sqlSourceUtils = new SQLSourceHelper(context,"Source Name");
61 | SQLSourceHelper sqlSourceUtilsSpy = PowerMockito.spy(sqlSourceUtils);
62 |
63 | PowerMockito.verifyPrivate(sqlSourceUtilsSpy, Mockito.times(1)).invoke("createDirectory");
64 | }*/
65 |
66 | @Test
67 | public void getConnectionURL() {
68 | SQLSourceHelper sqlSourceHelper = new SQLSourceHelper(context,"Source Name");
69 | assertEquals("jdbc:mysql://host:3306/database", sqlSourceHelper.getConnectionURL());
70 | }
71 |
72 | @Test
73 | public void getCurrentIndex() {
74 | SQLSourceHelper sqlSourceHelper = new SQLSourceHelper(context,"Source Name");
75 | assertEquals("0",sqlSourceHelper.getCurrentIndex());
76 | }
77 |
78 | @Test
79 | public void setCurrentIndex() {
80 | SQLSourceHelper sqlSourceHelper = new SQLSourceHelper(context,"Source Name");
81 | sqlSourceHelper.setCurrentIndex("10");
82 | assertEquals("10",sqlSourceHelper.getCurrentIndex());
83 | }
84 |
85 | @Test
86 | public void getRunQueryDelay() {
87 | SQLSourceHelper sqlSourceHelper = new SQLSourceHelper(context,"Source Name");
88 | assertEquals(10000,sqlSourceHelper.getRunQueryDelay());
89 | }
90 |
91 | @Test
92 | public void getBatchSize() {
93 | SQLSourceHelper sqlSourceHelper = new SQLSourceHelper(context,"Source Name");
94 | assertEquals(100,sqlSourceHelper.getBatchSize());
95 | }
96 |
97 | @Test
98 | public void getQuery() {
99 | SQLSourceHelper sqlSourceHelper = new SQLSourceHelper(context,"Source Name");
100 | assertEquals("SELECT * FROM table",sqlSourceHelper.getQuery());
101 | }
102 |
103 | @Test
104 | public void execQuery() {
105 | Map hibernate = Maps.newHashMap();
106 | hibernate.put("connection.url", "jdbc:mysql://localhost/mydatabase?useSSL=false");
107 | hibernate.put("connection.user", "test");
108 | hibernate.put("connection.password", "test");
109 | hibernate.put("connection.autocommit", "true");
110 | hibernate.put("dialect", "org.hibernate.dialect.MySQL5Dialect");
111 | hibernate.put("connection.driver_class", "com.mysql.jdbc.Driver");
112 | hibernate.put("connection.provider_class", "org.hibernate.connection.C3P0ConnectionProvider");
113 | hibernate.put("c3p0.min_size","5");
114 | hibernate.put("c3p0.max_size","20");
115 | when(context.getSubProperties("hibernate.")).thenReturn(ImmutableMap.copyOf(hibernate));
116 |
117 | SQLSourceHelper sqlSourceHelper = new SQLSourceHelper(context,"Source Name");
118 | HibernateHelper hibernateHelper = new HibernateHelper(sqlSourceHelper);
119 | hibernateHelper.establishSession();
120 | try {
121 | List> lists = hibernateHelper.executeQueryForJson();
122 | // testPrintWriter(lists);
123 | SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
124 | for (Map item :lists
125 | ) {
126 | for (Map.Entry entry:item.entrySet()
127 | ) {
128 | Object value = entry.getValue();
129 | if (value instanceof java.util.Date){
130 | entry.setValue(SDF.format(value));
131 | }
132 | }
133 | System.out.println(item);
134 | }
135 | } catch (InterruptedException e) {
136 | e.printStackTrace();
137 | }
138 | }
139 |
140 |
141 | public void testJsonWriter(List> lists) throws IOException {
142 | File file = new File("d:\\gson.txt");// 把json保存文本
143 | OutputStream out = new FileOutputStream(file);
144 | JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"));//设置编码
145 | Gson gson = new Gson();
146 | Type type = new TypeToken(){}.getType();
147 | for (Map item :lists
148 | ) {
149 | String json = gson.toJson(item);
150 | gson.toJson(json, type, writer);//把值写进去
151 | }
152 | writer.flush();
153 | writer.close();
154 | }
155 |
156 | public void testPrintWriter(List> lists) throws IOException {
157 | File file = new File("d:\\gson.txt");// 把json保存文本
158 | OutputStream out = new FileOutputStream(file);
159 | PrintWriter writer = new PrintWriter(new ChannelWriter());//设置编码
160 | Gson gson = new Gson();
161 | for (Map item :lists
162 | ) {
163 | writer.write(gson.toJson(item));
164 | }
165 | writer.flush();
166 | writer.close();
167 | }
168 |
169 | @Test
170 | public void getCustomQuery() {
171 | when(context.getString("custom.query")).thenReturn("SELECT column FROM table");
172 | when(context.getString("incremental.column")).thenReturn("incremental");
173 | SQLSourceHelper sqlSourceHelper = new SQLSourceHelper(context,"Source Name");
174 | assertEquals("SELECT column FROM table",sqlSourceHelper.getQuery());
175 | }
176 |
177 | @Test
178 | public void chekGetAllRowsWithNullParam() {
179 | SQLSourceHelper sqlSourceHelper = new SQLSourceHelper(context,"Source Name");
180 | assertEquals(new ArrayList(),sqlSourceHelper.getAllRows(null));
181 | }
182 |
183 | @Test(expected = ConfigurationException.class)
184 | public void checkStatusFileNameNotSet() {
185 | when(context.getString("status.file.name")).thenReturn(null);
186 | new SQLSourceHelper(context,"Source Name");
187 | }
188 |
189 | @Test(expected = ConfigurationException.class)
190 | public void connectionURLNotSet() {
191 | when(context.getString("hibernate.connection.url")).thenReturn(null);
192 | new SQLSourceHelper(context,"Source Name");
193 | }
194 |
195 | @Test(expected = ConfigurationException.class)
196 | public void tableNotSet() {
197 | when(context.getString("table")).thenReturn(null);
198 | new SQLSourceHelper(context,"Source Name");
199 | }
200 |
201 | @Test
202 | public void chekGetAllRowsWithEmptyParam() {
203 |
204 | SQLSourceHelper sqlSourceHelper = new SQLSourceHelper(context,"Source Name");
205 | assertEquals(new ArrayList(),sqlSourceHelper.getAllRows(new ArrayList>()));
206 | }
207 |
208 | @SuppressWarnings("deprecation")
209 | @Test
210 | public void chekGetAllRows() {
211 |
212 | SQLSourceHelper sqlSourceHelper = new SQLSourceHelper(context,"Source Name");
213 | List> queryResult = new ArrayList>(2);
214 | List expectedResult = new ArrayList(2);
215 | String string1 = "string1";
216 | String string2 = "string2";
217 | int int1 = 1;
218 | int int2 = 2;
219 | Date date1 = new Date(115,0,1);
220 | Date date2 = new Date(115,1,2);
221 |
222 | List row1 = new ArrayList(3);
223 | String[] expectedRow1 = new String[3];
224 | row1.add(string1);
225 | expectedRow1[0] = string1;
226 | row1.add(int1);
227 | expectedRow1[1] = Integer.toString(int1);
228 | row1.add(date1);
229 | expectedRow1[2] = date1.toString();
230 | queryResult.add(row1);
231 | expectedResult.add(expectedRow1);
232 |
233 | List row2 = new ArrayList(3);
234 | String[] expectedRow2 = new String[3];
235 | row2.add(string2);
236 | expectedRow2[0] = string2;
237 | row2.add(int2);
238 | expectedRow2[1] = Integer.toString(int2);
239 | row2.add(date2);
240 | expectedRow2[2] = date2.toString();
241 | queryResult.add(row2);
242 | expectedResult.add(expectedRow2);
243 |
244 | assertArrayEquals(expectedResult.get(0),sqlSourceHelper.getAllRows(queryResult).get(0));
245 | assertArrayEquals(expectedResult.get(1),sqlSourceHelper.getAllRows(queryResult).get(1));
246 | }
247 |
248 | @SuppressWarnings("unused")
249 | @Test
250 | public void createDirectory() {
251 |
252 | SQLSourceHelper sqlSourceHelper = new SQLSourceHelper(context,"Source Name");
253 | File file = new File("/tmp/flume");
254 | assertEquals(true, file.exists());
255 | assertEquals(true, file.isDirectory());
256 | if (file.exists()){
257 | file.delete();
258 | }
259 | }
260 |
261 | @Test
262 | public void checkStatusFileCorrectlyCreated() {
263 |
264 | SQLSourceHelper sqlSourceHelper = new SQLSourceHelper(context,"Source Name");
265 | //sqlSourceHelper.setCurrentIndex(10);
266 |
267 | sqlSourceHelper.updateStatusFile();
268 |
269 | File file = new File("/tmp/flume/statusFileName.txt");
270 | assertEquals(true, file.exists());
271 | if (file.exists()){
272 | file.delete();
273 | file.getParentFile().delete();
274 | }
275 | }
276 |
277 | @Test
278 | public void checkStatusFileCorrectlyUpdated() throws Exception {
279 |
280 | //File file = File.createTempFile("statusFileName", ".txt");
281 |
282 | when(context.getString("status.file.path")).thenReturn("/var/lib/flume");
283 | when(context.getString("hibernate.connection.url")).thenReturn("jdbc:mysql://host:3306/database");
284 | when(context.getString("table")).thenReturn("table");
285 | when(context.getString("status.file.name")).thenReturn("statusFileName");
286 |
287 | SQLSourceHelper sqlSourceHelper = new SQLSourceHelper(context,"Source Name");
288 | sqlSourceHelper.createStatusFile();
289 | sqlSourceHelper.setCurrentIndex("10");
290 |
291 | sqlSourceHelper.updateStatusFile();
292 |
293 | SQLSourceHelper sqlSourceHelper2 = new SQLSourceHelper(context,"Source Name");
294 | assertEquals("10", sqlSourceHelper2.getCurrentIndex());
295 | }
296 |
297 |
298 |
299 | @After
300 | public void deleteDirectory(){
301 | try {
302 |
303 | File file = new File("/tmp/flume");
304 | if (file.exists())
305 | FileUtils.deleteDirectory(file);
306 |
307 | } catch (IOException e) {
308 | e.printStackTrace();
309 | }
310 | }
311 |
312 | private class ChannelWriter extends Writer{
313 | private List events = new ArrayList<>();
314 |
315 | @Override
316 | public void write(char[] cbuf, int off, int len) throws IOException {
317 | Event event = new SimpleEvent();
318 |
319 | String s = new String(cbuf);
320 | // s.substring(off, len-1).getBytes()
321 | event.setBody(s.substring(off, len).getBytes());
322 | System.out.println("111111111111111 event:"+s+"|||||||");
323 |
324 | Map headers;
325 | headers = new HashMap();
326 | headers.put("timestamp", String.valueOf(System.currentTimeMillis()));
327 | event.setHeaders(headers);
328 |
329 | events.add(event);
330 |
331 | }
332 |
333 | @Override
334 | public void flush() throws IOException {
335 | events.clear();
336 | }
337 |
338 | @Override
339 | public void close() throws IOException {
340 | flush();
341 | }
342 | }
343 |
344 | }
345 |
--------------------------------------------------------------------------------
/src/main/java/org/keedio/flume/source/SQLSourceHelper.java:
--------------------------------------------------------------------------------
1 | package org.keedio.flume.source;
2 |
3 |
4 | import com.google.gson.Gson;
5 | import com.google.gson.reflect.TypeToken;
6 | import org.apache.flume.Context;
7 | import org.apache.flume.conf.ConfigurationException;
8 | import org.json.simple.JSONValue;
9 | import org.json.simple.parser.JSONParser;
10 | import org.json.simple.parser.ParseException;
11 | import org.slf4j.Logger;
12 | import org.slf4j.LoggerFactory;
13 |
14 | import java.io.*;
15 | import java.lang.reflect.Type;
16 | import java.text.SimpleDateFormat;
17 | import java.util.ArrayList;
18 | import java.util.LinkedHashMap;
19 | import java.util.List;
20 | import java.util.Map;
21 |
22 | import static org.json.simple.parser.ParseException.ERROR_UNEXPECTED_EXCEPTION;
23 |
24 |
25 | /**
26 | * Helper to manage configuration parameters and utility methods
27 | *
28 | * Configuration parameters readed from flume configuration file:
29 | * type: org.keedio.flume.source.SQLSource
30 | * table: table to read from
31 | * columns.to.select: columns to select for import data (* will import all)
32 | * run.query.delay: delay time to execute each query to database
33 | * status.file.path: Directory to save status file
34 | * status.file.name: Name for status file (saves last row index processed)
35 | * batch.size: Batch size to send events from flume source to flume channel
36 | * max.rows: Max rows to import from DB in one query
37 | * custom.query: Custom query to execute to database (be careful)
38 | *
39 | * @author Marcelo Valle
40 | * @author Luis Lazaro
41 | */
42 |
43 | public class SQLSourceHelper {
44 |
45 | private static final Logger LOG = LoggerFactory.getLogger(SQLSourceHelper.class);
46 |
47 | private File file,directory;
48 | private int runQueryDelay, batchSize, maxRows;
49 | private String startFrom, currentIndex,orderBy;
50 | private String statusFilePath, statusFileName, connectionURL, table,
51 | columnsToSelect, customQuery, query, sourceName, delimiterEntry;
52 | private Boolean encloseByQuotes;
53 |
54 | private Context context;
55 |
56 | private Map statusFileJsonMap = new LinkedHashMap();
57 |
58 | private boolean readOnlySession;
59 |
60 | private static final String DEFAULT_STATUS_DIRECTORY = "/var/lib/flume";
61 | private static final int DEFAULT_QUERY_DELAY = 10000;
62 | private static final int DEFAULT_BATCH_SIZE = 100;
63 | private static final int DEFAULT_MAX_ROWS = 10000;
64 | private static final String DEFAULT_INCREMENTAL_VALUE = "0";
65 | private static final String DEFAULT_DELIMITER_ENTRY = ",";
66 | private static final Boolean DEFAULT_ENCLOSE_BY_QUOTES = true;
67 |
68 | private static final String SOURCE_NAME_STATUS_FILE = "SourceName";
69 | private static final String URL_STATUS_FILE = "URL";
70 | private static final String COLUMNS_TO_SELECT_STATUS_FILE = "ColumnsToSelect";
71 | private static final String TABLE_STATUS_FILE = "Table";
72 | private static final String LAST_INDEX_STATUS_FILE = "LastIndex";
73 | private static final String QUERY_STATUS_FILE = "Query";
74 | private static final String DEFAULT_ORDER_BY_VALUE = "id";
75 |
76 | private final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
77 | private final String JSON_LINE_END = " ";
78 |
79 |
80 | /**
81 | * Builds an SQLSourceHelper containing the configuration parameters and
82 | * usefull utils for SQL Source
83 | * @param context Flume source context, contains the properties from configuration file
84 | * @param sourceName source file name for store status
85 | */
86 | public SQLSourceHelper(Context context, String sourceName){
87 |
88 | this.context = context;
89 |
90 | statusFilePath = context.getString("status.file.path", DEFAULT_STATUS_DIRECTORY);
91 | statusFileName = context.getString("status.file.name");
92 | table = context.getString("table");
93 | columnsToSelect = context.getString("columns.to.select","*");
94 | runQueryDelay = context.getInteger("run.query.delay",DEFAULT_QUERY_DELAY);
95 | directory = new File(statusFilePath);
96 | customQuery = context.getString("custom.query");
97 | batchSize = context.getInteger("batch.size",DEFAULT_BATCH_SIZE);
98 | maxRows = context.getInteger("max.rows",DEFAULT_MAX_ROWS);
99 | connectionURL = context.getString("hibernate.connection.url");
100 | readOnlySession = context.getBoolean("read.only",false);
101 |
102 | this.sourceName = sourceName;
103 | startFrom = context.getString("start.from",DEFAULT_INCREMENTAL_VALUE);
104 | orderBy = context.getString("order.by",DEFAULT_ORDER_BY_VALUE);
105 | delimiterEntry = context.getString("delimiter.entry",DEFAULT_DELIMITER_ENTRY);
106 | encloseByQuotes = context.getBoolean("enclose.by.quotes", DEFAULT_ENCLOSE_BY_QUOTES);
107 | statusFileJsonMap = new LinkedHashMap();
108 |
109 | checkMandatoryProperties();
110 |
111 | if (!(isStatusDirectoryCreated())) {
112 | createDirectory();
113 | }
114 |
115 | file = new File(statusFilePath + "/" + statusFileName);
116 |
117 | if (!isStatusFileCreated()){
118 | LOG.warn("file:{} is not exists.",file.getPath());
119 | currentIndex = startFrom;
120 | createStatusFile();
121 | }
122 | else
123 | currentIndex = getStatusFileIndex(startFrom);
124 | LOG.warn("currentIndex:{} .",currentIndex);
125 | query = buildQuery();
126 | }
127 |
128 |
129 | public String buildQuery() {
130 |
131 | if (customQuery == null){
132 | return "SELECT " + columnsToSelect + " FROM " + table;
133 | }
134 | else {
135 | if (customQuery.contains("$@$") || customQuery.contains("$OB$")){
136 | return customQuery.replace("$@$", currentIndex)
137 | .replace("$OB$", orderBy);
138 | }
139 | else{
140 | return customQuery;
141 | }
142 | }
143 | }
144 |
145 |
146 | private boolean isStatusFileCreated(){
147 | return file.exists() && !file.isDirectory() ? true: false;
148 | }
149 |
150 | private boolean isStatusDirectoryCreated() {
151 | return directory.exists() && !directory.isFile() ? true: false;
152 | }
153 |
154 | /**
155 | * Converter from a List of Object List to a List of String arrays
156 | * Useful for csvWriter
157 | * @param queryResult Query Result from hibernate executeQuery method
158 | * @return A list of String arrays, ready for csvWriter.writeall method
159 | */
160 | public List getAllRows(List> queryResult){
161 |
162 | List allRows = new ArrayList();
163 |
164 | if (queryResult == null || queryResult.isEmpty())
165 | return allRows;
166 |
167 | String[] row=null;
168 |
169 | for (int i=0; i rawRow = queryResult.get(i);
172 | row = new String[rawRow.size()];
173 | for (int j=0; j< rawRow.size(); j++){
174 | if (rawRow.get(j) != null)
175 | row[j] = rawRow.get(j).toString();
176 | else
177 | row[j] = "";
178 | }
179 | allRows.add(row);
180 | }
181 |
182 | return allRows;
183 | }
184 |
185 | /**
186 | * write the query result to the output stream
187 | * @param queryResult Query Result from hibernate executeQuery method
188 | * @param printWriter output stream object
189 | */
190 | public void writeAllRows(List> queryResult,PrintWriter printWriter){
191 |
192 | if (queryResult == null || queryResult.isEmpty()) {
193 | return ;
194 | }
195 | Gson gson = new Gson();
196 | Type type = new TypeToken>(){}.getType();
197 | for (Map item :queryResult) {
198 | for (Map.Entry entry:item.entrySet()) {
199 | Object value = entry.getValue();
200 | // initialize null field
201 | if (value == null){
202 | entry.setValue("");
203 | }
204 | // format time
205 | if (value instanceof java.util.Date){
206 | entry.setValue(SDF.format(value));
207 | }
208 | }
209 | String json = gson.toJson(item, type);
210 | // write to output stream
211 | printWriter.print(json+JSON_LINE_END);
212 | }
213 | }
214 |
215 | /**
216 | * Create status file
217 | */
218 | public void createStatusFile(){
219 |
220 | statusFileJsonMap.put(SOURCE_NAME_STATUS_FILE, sourceName);
221 | statusFileJsonMap.put(URL_STATUS_FILE, connectionURL);
222 | statusFileJsonMap.put(LAST_INDEX_STATUS_FILE, currentIndex);
223 |
224 | if (isCustomQuerySet()){
225 | statusFileJsonMap.put(QUERY_STATUS_FILE,customQuery);
226 | }else{
227 | statusFileJsonMap.put(COLUMNS_TO_SELECT_STATUS_FILE, columnsToSelect);
228 | statusFileJsonMap.put(TABLE_STATUS_FILE, table);
229 | }
230 |
231 |
232 | try {
233 | Writer fileWriter = new FileWriter(file,false);
234 | JSONValue.writeJSONString(statusFileJsonMap, fileWriter);
235 | fileWriter.close();
236 | } catch (IOException e) {
237 | LOG.error("Error creating value to status file!!!",e);
238 | }
239 | }
240 |
241 | /**
242 | * Update status file with last read row index
243 | */
244 | public void updateStatusFile() {
245 |
246 | statusFileJsonMap.put(LAST_INDEX_STATUS_FILE, currentIndex);
247 |
248 | try {
249 | Writer fileWriter = new FileWriter(file,false);
250 | JSONValue.writeJSONString(statusFileJsonMap, fileWriter);
251 | fileWriter.close();
252 | } catch (IOException e) {
253 | LOG.error("Error writing incremental value to status file!!!",e);
254 | }
255 | }
256 |
257 | private String getStatusFileIndex(String configuredStartValue) {
258 |
259 | if (!isStatusFileCreated()) {
260 | LOG.info("Status file not created, using start value from config file and creating file");
261 | return configuredStartValue;
262 | }
263 | else{
264 | try {
265 | FileReader fileReader = new FileReader(file);
266 |
267 | JSONParser jsonParser = new JSONParser();
268 | statusFileJsonMap = (Map)jsonParser.parse(fileReader);
269 | checkJsonValues();
270 | return statusFileJsonMap.get(LAST_INDEX_STATUS_FILE);
271 |
272 | } catch (Exception e) {
273 | LOG.error("Exception reading status file, doing back up and creating new status file", e);
274 | backupStatusFile();
275 | return configuredStartValue;
276 | }
277 | }
278 | }
279 |
280 | private void checkJsonValues() throws ParseException {
281 |
282 | // Check commons values to default and custom query
283 | if (!statusFileJsonMap.containsKey(SOURCE_NAME_STATUS_FILE) || !statusFileJsonMap.containsKey(URL_STATUS_FILE) ||
284 | !statusFileJsonMap.containsKey(LAST_INDEX_STATUS_FILE)) {
285 | LOG.error("Status file doesn't contains all required values");
286 | throw new ParseException(ERROR_UNEXPECTED_EXCEPTION);
287 | }
288 | if (!statusFileJsonMap.get(URL_STATUS_FILE).equals(connectionURL)){
289 | LOG.error("Connection url in status file doesn't match with configured in properties file");
290 | throw new ParseException(ERROR_UNEXPECTED_EXCEPTION);
291 | }else if (!statusFileJsonMap.get(SOURCE_NAME_STATUS_FILE).equals(sourceName)){
292 | LOG.error("Source name in status file doesn't match with configured in properties file");
293 | throw new ParseException(ERROR_UNEXPECTED_EXCEPTION);
294 | }
295 |
296 | // Check default query values
297 | if (customQuery == null)
298 | {
299 | if (!statusFileJsonMap.containsKey(COLUMNS_TO_SELECT_STATUS_FILE) || !statusFileJsonMap.containsKey(TABLE_STATUS_FILE)){
300 | LOG.error("Expected ColumsToSelect and Table fields in status file");
301 | throw new ParseException(ERROR_UNEXPECTED_EXCEPTION);
302 | }
303 | if (!statusFileJsonMap.get(COLUMNS_TO_SELECT_STATUS_FILE).equals(columnsToSelect)){
304 | LOG.error("ColumsToSelect value in status file doesn't match with configured in properties file");
305 | throw new ParseException(ERROR_UNEXPECTED_EXCEPTION);
306 | }
307 | if (!statusFileJsonMap.get(TABLE_STATUS_FILE).equals(table)){
308 | LOG.error("Table value in status file doesn't match with configured in properties file");
309 | throw new ParseException(ERROR_UNEXPECTED_EXCEPTION);
310 | }
311 | return;
312 | }
313 |
314 | // Check custom query values
315 | if (customQuery != null){
316 | if (!statusFileJsonMap.containsKey(QUERY_STATUS_FILE)){
317 | LOG.error("Expected Query field in status file");
318 | throw new ParseException(ERROR_UNEXPECTED_EXCEPTION);
319 | }
320 | if (!statusFileJsonMap.get(QUERY_STATUS_FILE).equals(customQuery)){
321 | LOG.error("Query value in status file doesn't match with configured in properties file");
322 | throw new ParseException(ERROR_UNEXPECTED_EXCEPTION);
323 | }
324 | return;
325 | }
326 | }
327 |
328 | private void backupStatusFile() {
329 | file.renameTo(new File(statusFilePath + "/" + statusFileName + ".bak." + System.currentTimeMillis()));
330 | }
331 |
332 | private void checkMandatoryProperties() {
333 |
334 | if (connectionURL == null){
335 | throw new ConfigurationException("hibernate.connection.url property not set");
336 | }
337 | if (statusFileName == null){
338 | throw new ConfigurationException("status.file.name property not set");
339 | }
340 | if (table == null && customQuery == null){
341 | throw new ConfigurationException("property table not set");
342 | }
343 | }
344 |
345 | /*
346 | * @return boolean pathname into directory
347 | */
348 | private boolean createDirectory() {
349 | return directory.mkdir();
350 | }
351 |
352 | /*
353 | * @return long incremental value as parameter from this
354 | */
355 | String getCurrentIndex() {
356 | return currentIndex;
357 | }
358 |
359 | String getOrderBy() {
360 | return orderBy;
361 | }
362 |
363 | /*
364 | * @void set incrementValue
365 | */
366 | void setCurrentIndex(String newValue) {
367 | currentIndex = newValue;
368 | }
369 |
370 | /*
371 | * @return int delay in ms
372 | */
373 | int getRunQueryDelay() {
374 | return runQueryDelay;
375 | }
376 |
377 | int getBatchSize() {
378 | return batchSize;
379 | }
380 |
381 | int getMaxRows() {
382 | return maxRows;
383 | }
384 |
385 | String getQuery() {
386 | return query;
387 | }
388 |
389 | String getConnectionURL() {
390 | return connectionURL;
391 | }
392 |
393 | boolean isCustomQuerySet() {
394 | return (customQuery != null);
395 | }
396 |
397 | Context getContext() {
398 | return context;
399 | }
400 |
401 |
402 | boolean isReadOnlySession() {
403 | return readOnlySession;
404 | }
405 |
406 | boolean encloseByQuotes() {
407 | return encloseByQuotes;
408 | }
409 |
410 | String getDelimiterEntry() {return delimiterEntry;}
411 |
412 | }
413 |
--------------------------------------------------------------------------------