View Javadoc
1   package fr.ifremer.quadrige2.synchro.vo;
2   
3   /*-
4    * #%L
5    * Quadrige2 Core :: Quadrige2 Synchro Core
6    * $Id:$
7    * $HeadURL:$
8    * %%
9    * Copyright (C) 2017 Ifremer
10   * %%
11   * This program is free software: you can redistribute it and/or modify
12   * it under the terms of the GNU Affero General Public License as published by
13   * the Free Software Foundation, either version 3 of the License, or
14   * (at your option) any later version.
15   * 
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Public License for more details.
20   * 
21   * You should have received a copy of the GNU Affero General Public License
22   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23   * #L%
24   */
25  
26  import com.google.common.base.Joiner;
27  import com.google.common.base.Preconditions;
28  import com.google.common.base.Splitter;
29  import com.google.common.collect.*;
30  import fr.ifremer.common.synchro.meta.SynchroTableMetadata;
31  import fr.ifremer.common.synchro.service.RejectedRow;
32  import fr.ifremer.quadrige2.core.exception.Quadrige2TechnicalException;
33  import org.apache.commons.collections4.CollectionUtils;
34  import org.apache.commons.io.IOUtils;
35  import org.apache.commons.lang3.StringUtils;
36  
37  import java.io.*;
38  import java.util.*;
39  import java.util.regex.Matcher;
40  import java.util.regex.Pattern;
41  
42  /**
43   * Created by blavenie on 01/09/15.
44   */
45  public class SynchroChangesVO {
46  
47  	private final static char PK_VALUE_SEPARATOR = '|';
48  
49  	private final static String INSERTS_SUFFIX = "inserts";
50  	private final static String UPDATES_SUFFIX = "updates";
51  	private final static String DELETES_SUFFIX = "deletes";
52  	private final static String REJECTS_SUFFIX = "rejects";
53  
54  	private final static String READ_PROPERTY_REGEX = "^([^.]+)[.]([a-zA-Z]+)=(.+)$";
55  
56  	private final Multimap<String, String> inserts = LinkedHashMultimap.create();
57  	private final Multimap<String, String> updates = LinkedHashMultimap.create();
58  	private final Multimap<String, String> deletes = LinkedHashMultimap.create();
59  
60  	private Properties connectionProperties;
61  
62  	/**
63  	 * Rejected rows.
64  	 * <p/>
65  	 * Will be written as a CSV string : [PK_AS_STR];[REJECT_STATUS];[UPDATE_DATE_NANOS_TIME]
66  	 * 25364~~~1;BAD_UPDATE_DATE;12437789901254
67  	 * <p/>
68  	 * see {@link fr.ifremer.common.synchro.service.SynchroResult#getRejectedRows()}
69  	 *
70  	 * @since 3.7
71  	 */
72  	protected final Multimap<String, String> rejectedRows = LinkedHashMultimap.create();
73  
74  	/**
75  	 * All table treated.
76  	 *
77  	 * @since 1.0
78  	 */
79  	protected final Set<String> tableNames = Sets.newHashSet();
80  
81  	/**
82  	 * <p>
83  	 * addTableName.
84  	 * </p>
85  	 *
86  	 * @param tableName
87  	 *            a {@link java.lang.String} object.
88  	 */
89  	public void addTableName(String tableName) {
90  		tableNames.add(tableName);
91  	}
92  
93  	/**
94  	 * <p>
95  	 * addReject.
96  	 * </p>
97  	 *
98  	 * @param tableName
99  	 *            a {@link java.lang.String} object.
100 	 * @param rowInfo
101 	 *            a {@link java.lang.String} object.
102 	 */
103 	public void addReject(String tableName, String... rowInfo) {
104 		rejectedRows.put(tableName, Joiner.on(';').join(rowInfo));
105 	}
106 
107 	/**
108 	 * <p>
109 	 * addReject.
110 	 * </p>
111 	 *
112 	 * @param tableName
113 	 *            a {@link java.lang.String} object.
114 	 * @param rejectedRow
115 	 *            a {@link RejectedRow} object.
116 	 */
117 	public void addReject(String tableName, RejectedRow rejectedRow) {
118 		rejectedRows.put(tableName, rejectedRow.toString());
119 	}
120 
121 	/**
122 	 * <p>
123 	 * addInsert.
124 	 * </p>
125 	 *
126 	 * @param tableName
127 	 *            a {@link java.lang.String} object.
128 	 * @param pk
129 	 *            a {@link java.util.List} object.
130 	 */
131 	public void addInsert(final String tableName, List<Object> pk) {
132 		inserts.put(tableName, SynchroTableMetadata.toPkStr(pk));
133 	}
134 
135 	/**
136 	 * <p>
137 	 * addUpdate.
138 	 * </p>
139 	 *
140 	 * @param tableName
141 	 *            a {@link java.lang.String} object.
142 	 * @param pk
143 	 *            a {@link java.util.List} object.
144 	 */
145 	public void addUpdate(final String tableName, List<Object> pk) {
146 		updates.put(tableName, SynchroTableMetadata.toPkStr(pk));
147 	}
148 
149 	/**
150 	 * <p>
151 	 * addDelete.
152 	 * </p>
153 	 *
154 	 * @param tableName
155 	 *            a {@link java.lang.String} object.
156 	 * @param pk
157 	 *            a {@link java.util.List} object.
158 	 */
159 	public void addDelete(final String tableName, List<Object> pk) {
160 		deletes.put(tableName, SynchroTableMetadata.toPkStr(pk));
161 	}
162 
163 	/**
164 	 * <p>
165 	 * Getter for the field <code>tableNames</code>.
166 	 * </p>
167 	 *
168 	 * @return a {@link java.util.Set} object.
169 	 */
170 	public Set<String> getTableNames() {
171 		return ImmutableSet.copyOf(tableNames);
172 	}
173 
174 	/**
175 	 * <p>
176 	 * getTotalTreated.
177 	 * </p>
178 	 *
179 	 * @return a int.
180 	 */
181 	public int getTotalTreated() {
182 		return getTotalInserts() + getTotalUpdates() + getTotalDeletes() + getTotalRejects();
183 	}
184 
185 	/**
186 	 * <p>
187 	 * getTotalInserts.
188 	 * </p>
189 	 *
190 	 * @return a int.
191 	 */
192 	public int getTotalInserts() {
193 		return inserts == null ? 0 : inserts.size();
194 	}
195 
196 	/**
197 	 * <p>
198 	 * getTotalUpdates.
199 	 * </p>
200 	 *
201 	 * @return a int.
202 	 */
203 	public int getTotalUpdates() {
204 		return updates == null ? 0 : updates.size();
205 	}
206 
207 	/**
208 	 * <p>
209 	 * getTotalDeletes.
210 	 * </p>
211 	 *
212 	 * @return a int.
213 	 */
214 	public int getTotalDeletes() {
215 		return deletes == null ? 0 : deletes.size();
216 	}
217 
218 	/**
219 	 * <p>
220 	 * getTotalRejects.
221 	 * </p>
222 	 *
223 	 * @return a int.
224 	 */
225 	public int getTotalRejects() {
226 		return rejectedRows == null ? 0 : rejectedRows.size();
227 	}
228 
229 	/**
230 	 * <p>
231 	 * getNbRows.
232 	 * </p>
233 	 *
234 	 * @param tableName
235 	 *            a {@link java.lang.String} object.
236 	 * @return a int.
237 	 */
238 	public int getNbRows(String tableName) {
239 		return getNbInserts(tableName) + getNbUpdates(tableName) + getNbDeletes(tableName);
240 	}
241 
242 	/**
243 	 * <p>
244 	 * getNbInserts.
245 	 * </p>
246 	 *
247 	 * @param tableName
248 	 *            a {@link java.lang.String} object.
249 	 * @return a int.
250 	 */
251 	public int getNbInserts(String tableName) {
252 		return inserts.get(tableName).size();
253 	}
254 
255 	/**
256 	 * <p>
257 	 * getNbUpdates.
258 	 * </p>
259 	 *
260 	 * @param tableName
261 	 *            a {@link java.lang.String} object.
262 	 * @return a int.
263 	 */
264 	public int getNbUpdates(String tableName) {
265 		return updates.get(tableName).size();
266 	}
267 
268 	/**
269 	 * <p>
270 	 * getNbDeletes.
271 	 * </p>
272 	 *
273 	 * @param tableName
274 	 *            a {@link java.lang.String} object.
275 	 * @return a int.
276 	 */
277 	public int getNbDeletes(String tableName) {
278 		return deletes.get(tableName).size();
279 	}
280 
281 	/**
282 	 * <p>
283 	 * Getter for the field <code>rejectedRows</code>.
284 	 * </p>
285 	 *
286 	 * @param tableName
287 	 *            a {@link java.lang.String} object.
288 	 * @return a {@link java.lang.String} object.
289 	 */
290 	public String getRejectedRows(String tableName) {
291 		Collection<String> result = rejectedRows.get(tableName);
292 		if (CollectionUtils.isEmpty(result)) {
293 			return "";
294 		}
295 		StringBuilder sb = new StringBuilder();
296 		for(String row: result) {
297 			sb.append('\n').append(row);
298 		}
299 		return sb.substring(1/*remove first newline*/);
300 	}
301 
302 	/**
303 	 * <p>
304 	 * addRejects.
305 	 * </p>
306 	 *
307 	 * @param rejectedRows
308 	 *            a {@link java.util.Map} object.
309 	 */
310 	public void addRejects(Map<String, String> rejectedRows) {
311 		Preconditions.checkNotNull(rejectedRows);
312 
313 		for(String tableName: rejectedRows.keySet()) {
314 			String[] rows = rejectedRows.get(tableName).split("\n");
315 			for (String row: rows) {
316 				this.rejectedRows.put(tableName, row);
317 			}
318 		}
319 		tableNames.addAll(rejectedRows.keySet());
320 	}
321 
322 	/**
323 	 * <p>
324 	 * Getter for the field <code>inserts</code>.
325 	 * </p>
326 	 *
327 	 * @param tableName
328 	 *            a {@link java.lang.String} object.
329 	 * @return a {@link java.util.Collection} object.
330 	 */
331 	public Collection<String> getInserts(String tableName) {
332 		return inserts.get(tableName);
333 	}
334 
335 	/**
336 	 * <p>
337 	 * Getter for the field <code>updates</code>.
338 	 * </p>
339 	 *
340 	 * @param tableName
341 	 *            a {@link java.lang.String} object.
342 	 * @return a {@link java.util.Collection} object.
343 	 */
344 	public Collection<String> getUpdates(String tableName) {
345 		return updates.get(tableName);
346 	}
347 
348 	/**
349 	 * <p>
350 	 * Getter for the field <code>deletes</code>.
351 	 * </p>
352 	 *
353 	 * @param tableName
354 	 *            a {@link java.lang.String} object.
355 	 * @return a {@link java.util.Collection} object.
356 	 */
357 	public Collection<String> getDeletes(String tableName) {
358 		return deletes.get(tableName);
359 	}
360 
361 	/**
362 	 * <p>
363 	 * Getter for the field <code>connectionProperties</code>.
364 	 * </p>
365 	 *
366 	 * @return a {@link java.util.Properties} object.
367 	 */
368 	public Properties getConnectionProperties() {
369 		return connectionProperties;
370 	}
371 
372 	/**
373 	 * <p>
374 	 * Setter for the field <code>connectionProperties</code>.
375 	 * </p>
376 	 *
377 	 * @param connectionProperties
378 	 *            a {@link java.util.Properties} object.
379 	 */
380 	public void setConnectionProperties(Properties connectionProperties) {
381 		this.connectionProperties = connectionProperties;
382 	}
383 
384 	/**
385 	 * <p>
386 	 * writeToFile.
387 	 * </p>
388 	 *
389 	 * @param changeLogFile
390 	 *            a {@link java.io.File} object.
391 	 * @param append
392 	 *            a boolean.
393 	 */
394 	public void writeToFile(File changeLogFile, boolean append) {
395 
396 		// Skip is empty
397 		if (isEmpty()) {
398 			return;
399 		}
400 
401 		Writer writer = null;
402 		try {
403 
404 			// Create the writer for file (append)
405 			writer = new BufferedWriter(new FileWriter(changeLogFile, append && changeLogFile.exists()));
406 
407 			// Inserts
408 			if (!inserts.isEmpty()) {
409 				writeToFile(writer, inserts, INSERTS_SUFFIX);
410 			}
411 
412 			// Updates
413 			if (!updates.isEmpty()) {
414 				writeToFile(writer, updates, UPDATES_SUFFIX);
415 			}
416 
417 			// Deletes
418 			if (!deletes.isEmpty()) {
419 				writeToFile(writer, deletes, DELETES_SUFFIX);
420 			}
421 
422 			// Rejects
423 			if (!rejectedRows.isEmpty()) {
424 				writeToFile(writer, rejectedRows, REJECTS_SUFFIX);
425 			}
426 		} catch (Exception e) {
427 			throw new Quadrige2TechnicalException("Unable to write synchronization diff into file: " + changeLogFile.getPath(), e);
428 		} finally {
429 			IOUtils.closeQuietly(writer);
430 		}
431 	}
432 
433 	/**
434 	 * <p>
435 	 * readFromFile.
436 	 * </p>
437 	 *
438 	 * @param changeLogFile
439 	 *            a {@link java.io.File} object.
440 	 */
441 	public void readFromFile(File changeLogFile) {
442 		readFromFile(changeLogFile, true);
443 	}
444 
445 	/**
446 	 * <p>
447 	 * addFromFile.
448 	 * </p>
449 	 *
450 	 * @param changeLogFile
451 	 *            a {@link java.io.File} object.
452 	 */
453 	public void addFromFile(File changeLogFile) {
454 		readFromFile(changeLogFile, false);
455 	}
456 
457 	/**
458 	 * <p>
459 	 * isEmpty.
460 	 * </p>
461 	 *
462 	 * @return a boolean.
463 	 */
464 	public boolean isEmpty() {
465 		return inserts.isEmpty() && updates.isEmpty() && deletes.isEmpty() && rejectedRows.isEmpty();
466 	}
467 
468 	/**
469 	 * Clear every fields
470 	 */
471 	public void clear() {
472 		inserts.clear();
473 		updates.clear();
474 		deletes.clear();
475 		rejectedRows.clear();
476 		tableNames.clear();
477 	}
478 
479 	/* -- Internal methods -- */
480 
481 	/**
482 	 * <p>
483 	 * readFromFile.
484 	 * </p>
485 	 *
486 	 * @param changeLogFile
487 	 *            a {@link java.io.File} object.
488 	 * @param clearBeforeRead
489 	 *            a boolean.
490 	 */
491 	protected void readFromFile(File changeLogFile, boolean clearBeforeRead) {
492 
493 		// Clear existing data
494 		if (clearBeforeRead) {
495 			clear();
496 		}
497 
498 		BufferedReader reader = null;
499 		try {
500 
501 			// Create the writer for file (append)
502 			reader = new BufferedReader(new FileReader(changeLogFile));
503 
504 			Pattern propertyPattern = Pattern.compile(READ_PROPERTY_REGEX);
505 			Splitter propertyValueSplitter = Splitter.on(PK_VALUE_SEPARATOR).omitEmptyStrings().trimResults();
506 
507 			String line = reader.readLine();
508 			while (line != null) {
509 
510 				// Skip empty lines
511 				if (StringUtils.isNotBlank(line)) {
512 					Matcher propertyMatcher = propertyPattern.matcher(line.trim());
513 					if (propertyMatcher.matches()) {
514 						String tableName = propertyMatcher.group(1);
515 						String propertySuffix = propertyMatcher.group(2);
516 						String propertyValue = propertyMatcher.group(3);
517 
518 						if (StringUtils.isNotBlank(propertyValue)) {
519 							tableNames.add(tableName);
520 
521 							if (INSERTS_SUFFIX.equals(propertySuffix)) {
522 								inserts.putAll(tableName, propertyValueSplitter.split(propertyValue));
523 							}
524 							else if (UPDATES_SUFFIX.equals(propertySuffix)) {
525 								updates.putAll(tableName, propertyValueSplitter.split(propertyValue));
526 							}
527 							else if (DELETES_SUFFIX.equals(propertySuffix)) {
528 								deletes.putAll(tableName, propertyValueSplitter.split(propertyValue));
529 							}
530 							else if (REJECTS_SUFFIX.equals(propertySuffix)) {
531 								rejectedRows.putAll(tableName, propertyValueSplitter.split(propertyValue));
532 							}
533 						}
534 
535 					}
536 				}
537 
538 				// Iterate to next line
539 				line = reader.readLine();
540 
541 			} // while
542 		} catch (Exception e) {
543 			throw new Quadrige2TechnicalException("Unable to write synchronization change log into file: " + changeLogFile.getPath(), e);
544 		} finally {
545 			IOUtils.closeQuietly(reader);
546 		}
547 	}
548 
549 	/**
550 	 * <p>
551 	 * writeToFile.
552 	 * </p>
553 	 *
554 	 * @param writer
555 	 *            a {@link java.io.Writer} object.
556 	 * @param pksByTableName
557 	 *            a {@link com.google.common.collect.Multimap} object.
558 	 * @param propertySuffix
559 	 *            a {@link java.lang.String} object.
560 	 * @throws java.io.IOException
561 	 *             if any.
562 	 */
563 	protected void writeToFile(Writer writer, Multimap<String, String> pksByTableName, String propertySuffix) throws IOException {
564 
565 		StringBuilder sb = new StringBuilder();
566 		for (String tableName : pksByTableName.keySet()) {
567 
568 			// Reset the string builder
569 			sb.setLength(0);
570 			for (String pkStr : pksByTableName.get(tableName)) {
571 				sb.append(PK_VALUE_SEPARATOR).append(pkStr);
572 			}
573 
574 			writer.write(String.format("%s.%s=%s\n",
575 					tableName,
576 					propertySuffix,
577 					sb.substring(1)
578 			));
579 		}
580 	}
581 
582 }