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