View Javadoc
1   package fr.ifremer.quadrige3.synchro.intercept.system;
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.collect.ArrayListMultimap;
28  import com.google.common.collect.ImmutableList;
29  import com.google.common.collect.Multimap;
30  import com.google.common.collect.MultimapBuilder;
31  import com.google.common.eventbus.Subscribe;
32  import fr.ifremer.common.synchro.intercept.SynchroInterceptorBase;
33  import fr.ifremer.common.synchro.meta.SynchroJoinMetadata;
34  import fr.ifremer.common.synchro.meta.SynchroMetadataUtils;
35  import fr.ifremer.common.synchro.meta.SynchroTableMetadata;
36  import fr.ifremer.common.synchro.meta.event.CreateQueryEvent;
37  import fr.ifremer.common.synchro.meta.event.LoadJoinEvent;
38  import fr.ifremer.common.synchro.meta.event.LoadTableEvent;
39  import fr.ifremer.common.synchro.query.SynchroQueryBuilder;
40  import fr.ifremer.common.synchro.query.SynchroQueryName;
41  import fr.ifremer.common.synchro.query.SynchroQueryOperator;
42  import fr.ifremer.common.synchro.service.SynchroDatabaseConfiguration;
43  import fr.ifremer.quadrige3.synchro.intercept.referential.AbstractReferentialInterceptor;
44  import fr.ifremer.quadrige3.synchro.intercept.referential.internal.ImportFromFileFkInterceptor;
45  import fr.ifremer.quadrige3.synchro.meta.DatabaseColumns;
46  import fr.ifremer.quadrige3.synchro.meta.system.RuleSynchroTables;
47  import fr.ifremer.quadrige3.synchro.service.SynchroDirection;
48  import fr.ifremer.quadrige3.synchro.service.referential.ReferentialSynchroDatabaseConfiguration;
49  import org.apache.commons.collections4.CollectionUtils;
50  import org.apache.commons.logging.Log;
51  import org.apache.commons.logging.LogFactory;
52  
53  import java.util.Collection;
54  import java.util.Set;
55  
56  /**
57   * Manage only table on rules
58   * 
59   * @author Benoit Lavenier <benoit.lavenier@e-is.pro>
60   * @since 1.0
61   */
62  public class RuleTableInterceptor extends AbstractReferentialInterceptor {
63  
64  	// Define all natural Id, by table
65  	/** Constant <code>naturalIds</code> */
66  	private static final Multimap<String, String> naturalIds;
67  	/** Constant <code>childJoinIncludes</code> */
68  	private static final Multimap<String, String> childJoinIncludes;
69  	/** Constant <code>childJoinExcludes</code> */
70  	private static final Multimap<String, String> childJoinExcludes;
71  	private static final Log log = LogFactory.getLog(RuleTableInterceptor.class);
72  
73  	static {
74  		childJoinIncludes = initChildJoinIncludes();
75  		childJoinExcludes = initChildJoinExcludes();
76  		naturalIds = initNaturalIds();
77  	}
78  
79  	private String statusWhereClause;
80  
81  	/**
82  	 * <p>
83  	 * Constructor for RuleTableInterceptor.
84  	 * </p>
85  	 */
86  	public RuleTableInterceptor() {
87  		super(RuleSynchroTables.tableNames());
88  	}
89  
90  	/**
91  	 * <p>
92  	 * initChildJoinIncludes.
93  	 * </p>
94  	 * 
95  	 * @return a {@link com.google.common.collect.Multimap} object.
96  	 */
97  	protected static Multimap<String, String> initChildJoinIncludes() {
98  		Multimap<String, String> result = ArrayListMultimap.create();
99  
100 		// Only this child link will be kept (other are invalidate)
101 		result.put(RuleSynchroTables.RULE_LIST_RESP_DEP.name(), DatabaseColumns.RULE_LIST_CD.name());
102 		result.put(RuleSynchroTables.RULE_LIST_PROG.name(), DatabaseColumns.RULE_LIST_CD.name());
103 		result.put(RuleSynchroTables.RULE_LIST_RESP_QUSER.name(), DatabaseColumns.RULE_LIST_CD.name());
104 		result.put(RuleSynchroTables.RULE_LIST_CONTROLED_DEP.name(), DatabaseColumns.RULE_LIST_CD.name());
105 		result.put(RuleSynchroTables.RULE.name(), DatabaseColumns.RULE_LIST_CD.name());
106 		result.put(RuleSynchroTables.RULE_PMFM.name(), DatabaseColumns.RULE_CD.name());
107 		result.put(RuleSynchroTables.RULE_PARAMETER.name(), DatabaseColumns.RULE_CD.name());
108 		result.put(RuleSynchroTables.RULE_PRECONDITION.name(), DatabaseColumns.RULE_CD.name());
109 		result.put(RuleSynchroTables.RULE_GROUP.name(), DatabaseColumns.RULE_CD.name());
110 
111 		return result;
112 	}
113 
114 	/**
115 	 * <p>
116 	 * initChildJoinExcludes.
117 	 * </p>
118 	 * 
119 	 * @return a {@link com.google.common.collect.Multimap} object.
120 	 */
121 	protected static Multimap<String, String> initChildJoinExcludes() {
122 
123 		Multimap<String, String> result = ArrayListMultimap.create();
124 
125 		// Ignore back link of rule_precondition.used_rule_cd
126 		result.put(RuleSynchroTables.RULE_PRECONDITION.name(), DatabaseColumns.USED_RULE_CD.name());
127 
128 		return result;
129 	}
130 
131 	/**
132 	 * <p>
133 	 * initNaturalIds.
134 	 * </p>
135 	 * 
136 	 * @return a {@link com.google.common.collect.Multimap} object.
137 	 */
138 	protected static Multimap<String, String> initNaturalIds() {
139 
140 		Multimap<String, String> result = MultimapBuilder.hashKeys().arrayListValues().build();
141 
142 		// RULE_LIST
143 		result.putAll(RuleSynchroTables.RULE_LIST.name(), ImmutableList.of(
144 				DatabaseColumns.RULE_LIST_CD.name()));
145 		// RULE
146 		result.putAll(RuleSynchroTables.RULE.name(), ImmutableList.of(
147 				DatabaseColumns.RULE_CD.name()));
148 
149 		return result;
150 	}
151 
152 	/** {@inheritDoc} */
153 	@Override
154 	protected void init(ReferentialSynchroDatabaseConfiguration config) {
155 		super.init(config);
156 		statusWhereClause = createStatusWhereClause(config);
157 		setEnableOnWrite(true);
158 	}
159 
160 	/** {@inheritDoc} */
161 	@Override
162 	public SynchroInterceptorBase clone() {
163 		RuleTableInterceptor result = (RuleTableInterceptor) super.clone();
164 		result.statusWhereClause = this.statusWhereClause;
165 		return result;
166 	}
167 
168 	/* -- Internal methods -- */
169 
170 	/**
171 	 * <p>
172 	 * handleQuery.
173 	 * </p>
174 	 * 
175 	 * @param e
176 	 *            a {@link fr.ifremer.common.synchro.meta.event.CreateQueryEvent} object.
177 	 */
178 	@Subscribe
179 	public void handleQuery(CreateQueryEvent e) {
180 
181 		switch (e.queryName) {
182 		case count:
183 		case countFromUpdateDate:
184 		case select:
185 		case selectFromUpdateDate:
186 		case selectMaxUpdateDate:
187 			// Add restriction
188 			e.sql = addRestriction(e.source, e.queryName, e.sql);
189 
190 		default:
191 			break;
192 		}
193 	}
194 
195 	/**
196 	 * <p>
197 	 * handleTableLoad.
198 	 * </p>
199 	 * 
200 	 * @param e
201 	 *            a {@link fr.ifremer.common.synchro.meta.event.LoadTableEvent} object.
202 	 */
203 	@Subscribe
204 	public void handleTableLoad(LoadTableEvent e) {
205 
206 		SynchroTableMetadata table = e.table;
207 		SynchroDirection direction = getConfig().getDirection();
208 
209 		boolean hasStatusFilter = statusWhereClause != null;
210 		boolean hasUpdateDateColumn = hasColumns(table, DatabaseColumns.UPDATE_DT.name());
211 		boolean hasStatusCdColumn = hasColumns(table, DatabaseColumns.STATUS_CD.name());
212 		boolean isRoot = hasUpdateDateColumn && (!hasStatusFilter || hasStatusCdColumn);
213 
214 		// Set isRoot :
215 		// - if not already done - e.g. DeletedItemHistoryInterceptor already set as true,
216 		// so do not override this to false !
217 		// - if has an 'update_dt' column
218 		// - if NO status filter, or has 'status_cd' column
219 		if (!table.isRoot() && isRoot) {
220 			table.setRoot(true);
221 		}
222 
223 		// Special case for some tables without status_cd (.e.g RULE_LIST)
224 		// LP 05/07/2018: this could not happen anymore because RULE_LIST has now a STATUS_CD
225 		String tableName = table.getName();
226 		boolean isRootWithoutStatusCd = !hasStatusCdColumn && hasStatusFilter
227 				&& (tableName.equalsIgnoreCase(RuleSynchroTables.RULE_LIST.name()));
228 		if (!table.isRoot() && isRootWithoutStatusCd) {
229 			table.setRoot(true);
230 		}
231 
232 		// Define natural Id
233 		if (direction == SynchroDirection.IMPORT_FILE2LOCAL) {
234 			Collection<String> columnNames = naturalIds.get(table.getName());
235 			if (CollectionUtils.isNotEmpty(columnNames)) {
236 				table.addUniqueConstraint("NATURAL_ID_UNIQUE_C", ImmutableList.copyOf(columnNames),
237 						SynchroTableMetadata.DuplicateKeyStrategy.REPLACE_AND_REMAP);
238 			}
239 		}
240 	}
241 
242 	/**
243 	 * <p>
244 	 * handleJoinLoad.
245 	 * </p>
246 	 * 
247 	 * @param e
248 	 *            a {@link fr.ifremer.common.synchro.meta.event.LoadJoinEvent} object.
249 	 */
250 	@Subscribe
251 	public void handleJoinLoad(LoadJoinEvent e) {
252 		SynchroJoinMetadata join = e.join;
253 
254 		SynchroTableMetadata fkTable = join.getFkTable();
255 		String fkTableName = join.getFkTable().getName().toUpperCase();
256 		String pkTableName = join.getPkTable().getName().toUpperCase();
257 		String fkColumnName = join.getFkColumn().getName().toUpperCase();
258 
259 		boolean hasStatusFilter = statusWhereClause != null;
260 		boolean fkTableHasUpdateDateColumn = hasColumns(join.getFkTable(), DatabaseColumns.UPDATE_DT.name());
261 		boolean fkTableHasStatusCdColumn = hasColumns(join.getFkTable(), DatabaseColumns.STATUS_CD.name());
262 		boolean fkTableIsRoot = fkTableHasUpdateDateColumn && (!hasStatusFilter || fkTableHasStatusCdColumn);
263 		boolean isNumericColumn = SynchroMetadataUtils.isNumericType(fkTable.getColumnMetadata(fkColumnName));
264 		SynchroDirection direction = getConfig().getDirection();
265 
266 		// Disable link to itself
267 		if (join.getFkTable() == join.getPkTable()) {
268 			if (log.isDebugEnabled()) {
269 				log.debug("Disable join: " + join.toString());
270 			}
271 			join.setIsValid(false);
272 			return;
273 		}
274 
275 		// Disable all join to a root table
276 		if (join.isChild() && fkTableIsRoot) {
277 			if (log.isDebugEnabled()) {
278 				log.debug("Disable join: " + join.toString());
279 			}
280 			join.setIsValid(false);
281 			return;
282 		} else if (join.isChild() && fkTableHasUpdateDateColumn && hasStatusFilter) {
283 			// Keep the join
284 			return;
285 		}
286 
287 		// Keep a child join if its includes
288 		if (join.isChild()) {
289 			// Is join explicitly include ?
290 			Collection<String> columnIncludes = childJoinIncludes.get(fkTableName);
291 			boolean explicitlyInclude = CollectionUtils.isNotEmpty(columnIncludes) && columnIncludes.contains(fkColumnName);
292 
293 			// Is join explicitly exclude ?
294 			Collection<String> columnExcludes = childJoinExcludes.get(fkTableName);
295 			boolean explicitlyExclude = CollectionUtils.isNotEmpty(columnExcludes) && columnExcludes.contains(fkColumnName);
296 
297 			if (!explicitlyInclude || explicitlyExclude) {
298 				if (log.isDebugEnabled()) {
299 					log.debug("Disable join: " + join.toString());
300 				}
301 				join.setIsValid(false);
302 			}
303 		}
304 
305 		// Mantis #0029644 : add ImportFromFileFkInterceptor for Rules Tables
306 		// If the FK table is the current table
307 		if (fkTable == e.source) {
308 
309 			// File DB -> Local DB
310 			if (direction == SynchroDirection.IMPORT_FILE2LOCAL
311 					&& isNumericColumn
312 					&& join.getPkTable().isSimpleKey()) {
313 				int fkColumnIndex = fkTable.getSelectColumnIndex(fkColumnName);
314 
315 				ImportFromFileFkInterceptor fkInterceptor = new ImportFromFileFkInterceptor(
316 						pkTableName,
317 						fkColumnIndex,
318 						getConfig());
319 
320 				if (!fkTable.containsInterceptor(fkInterceptor)) {
321 					fkTable.addInterceptor(fkInterceptor);
322 				}
323 			}
324 		}
325 	}
326 
327 	/**
328 	 * <p>
329 	 * createStatusWhereClause.
330 	 * </p>
331 	 * 
332 	 * @param config
333 	 *            a {@link fr.ifremer.common.synchro.service.SynchroDatabaseConfiguration} object.
334 	 * @return a {@link java.lang.String} object.
335 	 */
336 	protected String createStatusWhereClause(SynchroDatabaseConfiguration config) {
337 
338 		Set<String> statusToInclude = getConfig().getStatusCodeIncludes();
339 		if (CollectionUtils.isEmpty(statusToInclude)) {
340 			return null;
341 		}
342 
343 		return String.format("t.%s IN ('%s')",
344 				DatabaseColumns.STATUS_CD,
345 				Joiner.on("','").join(statusToInclude));
346 	}
347 
348 	/**
349 	 * <p>
350 	 * addRestriction.
351 	 * </p>
352 	 * 
353 	 * @param table
354 	 *            a {@link fr.ifremer.common.synchro.meta.SynchroTableMetadata} object.
355 	 * @param queryName
356 	 *            a {@link fr.ifremer.common.synchro.query.SynchroQueryName} object.
357 	 * @param sql
358 	 *            a {@link java.lang.String} object.
359 	 * @return a {@link java.lang.String} object.
360 	 */
361 	protected String addRestriction(SynchroTableMetadata table, SynchroQueryName queryName, String sql) {
362 		SynchroQueryBuilder queryBuilder = SynchroQueryBuilder.newBuilder(sql);
363 
364 		// where: add status filter
365 		if (statusWhereClause != null
366 				&& hasColumns(table, DatabaseColumns.STATUS_CD.name())) {
367 			queryBuilder.addWhere(SynchroQueryOperator.AND, statusWhereClause);
368 		}
369 
370 		return queryBuilder.build();
371 	}
372 }