1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.jumpmind.symmetric.service.impl;
22
23 import java.io.File;
24 import java.net.MalformedURLException;
25 import java.net.URL;
26 import java.util.Date;
27 import java.util.List;
28
29 import org.apache.commons.lang.StringUtils;
30 import org.apache.commons.logging.Log;
31 import org.apache.commons.logging.LogFactory;
32 import org.apache.ddlutils.model.Table;
33 import org.jumpmind.symmetric.Version;
34 import org.jumpmind.symmetric.common.Constants;
35 import org.jumpmind.symmetric.common.ParameterConstants;
36 import org.jumpmind.symmetric.db.IDbDialect;
37 import org.jumpmind.symmetric.db.SqlScript;
38 import org.jumpmind.symmetric.model.DataEventType;
39 import org.jumpmind.symmetric.model.Node;
40 import org.jumpmind.symmetric.model.Trigger;
41 import org.jumpmind.symmetric.model.TriggerHistory;
42 import org.jumpmind.symmetric.model.TriggerReBuildReason;
43 import org.jumpmind.symmetric.service.IBootstrapService;
44 import org.jumpmind.symmetric.service.IClusterService;
45 import org.jumpmind.symmetric.service.IConfigurationService;
46 import org.jumpmind.symmetric.service.IDataService;
47 import org.jumpmind.symmetric.service.INodeService;
48 import org.jumpmind.symmetric.service.IRegistrationService;
49 import org.jumpmind.symmetric.service.IUpgradeService;
50 import org.jumpmind.symmetric.service.LockAction;
51 import org.jumpmind.symmetric.util.AppUtils;
52 import org.springframework.transaction.annotation.Transactional;
53
54 public class BootstrapService extends AbstractService implements IBootstrapService {
55
56 static final Log logger = LogFactory.getLog(BootstrapService.class);
57
58 private IDbDialect dbDialect;
59
60 private String tablePrefix;
61
62 private IConfigurationService configurationService;
63
64 private IClusterService clusterService;
65
66 private INodeService nodeService;
67
68 private IDataService dataService;
69
70 private IUpgradeService upgradeService;
71
72 private IRegistrationService registrationService;
73
74 private String triggerPrefix;
75
76 private boolean initialized = false;
77
78 public void setupDatabase() {
79 setupDatabase(false);
80 }
81
82 public void setupDatabase(boolean force) {
83 if (!initialized || force) {
84 if (parameterService.is(ParameterConstants.AUTO_CONFIGURE_DATABASE) || force) {
85 logger.info("Initializing SymmetricDS database.");
86 dbDialect.initConfigDb();
87 parameterService.rereadParameters();
88 logger.info("Done initializing SymmetricDS database.");
89 } else {
90 logger.info("SymmetricDS is not configured to auto create the database.");
91 }
92
93 if (upgradeService.isUpgradeNecessary()) {
94 if (parameterService.is(ParameterConstants.AUTO_UPGRADE)) {
95 try {
96 upgradeService.upgrade();
97 } catch (RuntimeException ex) {
98 logger
99 .fatal(
100 "The upgrade failed. The system may be unstable. Please resolve the problem manually.",
101 ex);
102 throw ex;
103 }
104 } else {
105 throw new RuntimeException("Upgrade of node is necessary. "
106 + "Please set auto.upgrade property to true for an automated upgrade.");
107 }
108 }
109 initialized = true;
110
111 }
112
113
114 clusterService.initLockTable();
115 }
116
117 /***
118 * This is done periodically throughout the day (so it needs to be
119 * efficient). If the trigger is created for the first time (no previous
120 * trigger existed), then should we auto-resync data?
121 */
122 public void syncTriggers() {
123 if (clusterService.lock(LockAction.SYNCTRIGGERS)) {
124 try {
125 logger.info("Synchronizing triggers");
126 configurationService.initTriggerRowsForConfigChannel();
127 removeInactiveTriggers();
128 updateOrCreateTriggers();
129 } finally {
130 clusterService.unlock(LockAction.SYNCTRIGGERS);
131 logger.info("Done synchronizing triggers.");
132 }
133 }
134 }
135
136 private void removeInactiveTriggers() {
137 List<Trigger> triggers = configurationService.getInactiveTriggersForSourceNodeGroup(parameterService
138 .getString(ParameterConstants.NODE_GROUP_ID));
139 for (Trigger trigger : triggers) {
140 TriggerHistory history = configurationService.getLatestHistoryRecordFor(trigger.getTriggerId());
141 logger.info("About to remove triggers for inactivated table: " + history.getSourceTableName());
142 if (history != null) {
143 dbDialect.removeTrigger(history.getSourceCatalogName(), history.getSourceSchemaName(), history
144 .getNameForInsertTrigger(), trigger.getSourceTableName());
145 dbDialect.removeTrigger(history.getSourceCatalogName(), history.getSourceSchemaName(), history
146 .getNameForDeleteTrigger(), trigger.getSourceTableName());
147 dbDialect.removeTrigger(history.getSourceCatalogName(), history.getSourceSchemaName(), history
148 .getNameForUpdateTrigger(), trigger.getSourceTableName());
149 configurationService.inactivateTriggerHistory(history);
150 } else {
151 logger.info("A trigger was inactivated that had not yet been built. Taking no action.");
152 }
153 }
154 }
155
156 private void updateOrCreateTriggers() {
157 List<Trigger> triggers = configurationService.getActiveTriggersForSourceNodeGroup(parameterService
158 .getString(ParameterConstants.NODE_GROUP_ID));
159
160 for (Trigger trigger : triggers) {
161
162 try {
163 TriggerReBuildReason reason = TriggerReBuildReason.NEW_TRIGGERS;
164
165 Table table = dbDialect.getMetaDataFor(trigger.getSourceCatalogName(), trigger.getSourceSchemaName(),
166 trigger.getSourceTableName(), false);
167
168 if (table != null) {
169 TriggerHistory latestHistoryBeforeRebuild = configurationService.getLatestHistoryRecordFor(trigger
170 .getTriggerId());
171
172 boolean forceRebuildOfTriggers = false;
173 if (latestHistoryBeforeRebuild == null) {
174 reason = TriggerReBuildReason.NEW_TRIGGERS;
175 forceRebuildOfTriggers = true;
176
177 } else if (TriggerHistory.calculateTableHashFor(table) != latestHistoryBeforeRebuild.getTableHash()) {
178 reason = TriggerReBuildReason.TABLE_SCHEMA_CHANGED;
179 forceRebuildOfTriggers = true;
180
181 } else if (trigger.hasChangedSinceLastTriggerBuild(latestHistoryBeforeRebuild.getCreateTime())) {
182 reason = TriggerReBuildReason.TABLE_SYNC_CONFIGURATION_CHANGED;
183 forceRebuildOfTriggers = true;
184 }
185
186
187
188
189
190 TriggerHistory newestHistory = rebuildTriggerIfNecessary(forceRebuildOfTriggers, trigger,
191 DataEventType.DELETE, reason, latestHistoryBeforeRebuild, rebuildTriggerIfNecessary(
192 forceRebuildOfTriggers, trigger, DataEventType.UPDATE, reason,
193 latestHistoryBeforeRebuild, rebuildTriggerIfNecessary(forceRebuildOfTriggers,
194 trigger, DataEventType.INSERT, reason, latestHistoryBeforeRebuild, null,
195 trigger.isSyncOnInsert(), table), trigger.isSyncOnUpdate(), table), trigger
196 .isSyncOnDelete(), table);
197
198 if (latestHistoryBeforeRebuild != null && newestHistory != null) {
199 configurationService.inactivateTriggerHistory(latestHistoryBeforeRebuild);
200 }
201
202 } else {
203 logger.error("The configured table does not exist in the datasource that is configured: "
204 + trigger.getSourceTableName());
205 }
206 } catch (Exception ex) {
207 logger.error("Failed to synchronize trigger for " + trigger.getSourceTableName(), ex);
208 }
209
210 }
211 }
212
213 @Deprecated
214 public void register() {
215 validateConfiguration();
216 }
217
218 public void validateConfiguration() {
219 Node node = nodeService.findIdentity();
220 if (node == null && !configurationService.isRegistrationServer()) {
221 if (!parameterService.is(ParameterConstants.START_PULL_JOB)) {
222 registrationService.registerWithServer();
223 }
224 } else if (node != null && parameterService.getExternalId().equals(node.getExternalId())
225 && parameterService.getNodeGroupId().equals(node.getNodeGroupId())) {
226 heartbeat();
227 } else if (node == null) {
228 if (!loadFromScriptIfProvided()) {
229 logger
230 .info("Could not find my identity in the database and this node is configured as a registration server. We are auto inserting the required rows to begin operation.");
231
232
233 }
234 } else {
235 throw new IllegalStateException(
236 "The configured state does not match recorded database state. The recorded external id is "
237 + node.getExternalId() + " while the configured external id is "
238 + parameterService.getExternalId() + ". The recorded node group id is "
239 + node.getNodeGroupId() + " while the configured node group id is "
240 + parameterService.getNodeGroupId());
241 }
242 }
243
244 /***
245 * Give the end use the option to provide a script that will load a
246 * registration server with an initial SymmetricDS setup.
247 *
248 * Look first on the file system, then in the classpath for the SQL file.
249 *
250 * @return true if the script was executed
251 */
252 private boolean loadFromScriptIfProvided() {
253 boolean loaded = false;
254 String sqlScript = parameterService.getString(ParameterConstants.AUTO_CONFIGURE_REGISTRATION_SERVER_SQL_SCRIPT);
255 if (!StringUtils.isBlank(sqlScript)) {
256 File file = new File(sqlScript);
257 URL fileUrl = null;
258 if (file.isFile()) {
259 try {
260 fileUrl = file.toURL();
261 } catch (MalformedURLException e) {
262 throw new RuntimeException(e);
263 }
264 } else {
265 fileUrl = getClass().getResource(sqlScript);
266 }
267
268 if (fileUrl != null) {
269 new SqlScript(fileUrl, jdbcTemplate.getDataSource(), true).execute();
270 loaded = true;
271 }
272 }
273 return loaded;
274 }
275
276 @Transactional
277 public void heartbeat() {
278 Node node = nodeService.findIdentity();
279 if (node != null) {
280 logger.info("Updating my node information and heartbeat time.");
281 node.setHeartbeatTime(new Date());
282 node.setTimezoneOffset(AppUtils.getTimezoneOffset());
283 node.setDatabaseType(dbDialect.getName());
284 node.setDatabaseVersion(dbDialect.getVersion());
285 node.setSchemaVersion(parameterService.getString(ParameterConstants.SCHEMA_VERSION));
286 node.setExternalId(parameterService.getExternalId());
287 node.setNodeGroupId(parameterService.getNodeGroupId());
288 node.setSymmetricVersion(Version.version());
289 if (!StringUtils.isBlank(parameterService.getMyUrl())) {
290 node.setSyncURL(parameterService.getMyUrl());
291 } else {
292 node.setSyncURL(Constants.PROTOCOL_NONE + "://" + AppUtils.getServerId());
293 }
294 nodeService.updateNode(node);
295 logger.info("Done updating my node information and heartbeat time.");
296 if (!configurationService.isRegistrationServer()) {
297 dataService.insertHeartbeatEvent(node);
298 }
299 }
300 }
301
302 private TriggerHistory rebuildTriggerIfNecessary(boolean forceRebuild, Trigger trigger, DataEventType dmlType,
303 TriggerReBuildReason reason, TriggerHistory oldAudit, TriggerHistory audit, boolean create, Table table) {
304
305 boolean triggerExists = false;
306
307 int maxTriggerNameLength = dbDialect.getMaxTriggerNameLength();
308 TriggerHistory newTriggerHist = new TriggerHistory(table, trigger, reason, trigger.getTriggerName(
309 DataEventType.INSERT, triggerPrefix, maxTriggerNameLength).toUpperCase(), trigger.getTriggerName(
310 DataEventType.UPDATE, triggerPrefix, maxTriggerNameLength).toUpperCase(), trigger.getTriggerName(
311 DataEventType.DELETE, triggerPrefix, maxTriggerNameLength).toUpperCase());
312
313 String oldTriggerName = null;
314 String oldSourceSchema = null;
315 String oldCatalogName = null;
316 if (oldAudit != null) {
317 oldTriggerName = oldAudit.getTriggerNameForDmlType(dmlType);
318 oldSourceSchema = oldAudit.getSourceSchemaName();
319 oldCatalogName = oldAudit.getSourceCatalogName();
320 triggerExists = dbDialect.doesTriggerExist(oldCatalogName, oldSourceSchema, oldAudit.getSourceTableName(),
321 oldTriggerName);
322 } else {
323
324
325
326 oldTriggerName = newTriggerHist.getTriggerNameForDmlType(dmlType);
327 oldSourceSchema = trigger.getSourceSchemaName();
328 oldCatalogName = trigger.getSourceCatalogName();
329 triggerExists = dbDialect.doesTriggerExist(oldCatalogName, oldSourceSchema, trigger.getSourceTableName(),
330 oldTriggerName);
331 }
332
333 if (!triggerExists && forceRebuild) {
334 reason = TriggerReBuildReason.TRIGGERS_MISSING;
335 }
336
337 if ((forceRebuild || !create) && triggerExists) {
338 dbDialect.removeTrigger(oldCatalogName, oldSourceSchema, oldTriggerName, trigger.getSourceTableName());
339 triggerExists = false;
340 }
341
342 boolean isDeadTrigger = !trigger.isSyncOnInsert() && !trigger.isSyncOnUpdate() && !trigger.isSyncOnDelete();
343
344 if (audit == null && (oldAudit == null || (!triggerExists && create) || (isDeadTrigger && forceRebuild))) {
345 configurationService.insert(newTriggerHist);
346 audit = configurationService.getLatestHistoryRecordFor(trigger.getTriggerId());
347 }
348
349 if (!triggerExists && create) {
350 dbDialect.initTrigger(dmlType, trigger, audit, tablePrefix, table);
351 }
352
353 return audit;
354 }
355
356 public void setConfigurationService(IConfigurationService configurationService) {
357 this.configurationService = configurationService;
358 }
359
360 public void setDbDialect(IDbDialect dbDialect) {
361 this.dbDialect = dbDialect;
362 }
363
364 public void setNodeService(INodeService nodeService) {
365 this.nodeService = nodeService;
366 }
367
368 public void setTablePrefix(String tablePrefix) {
369 this.tablePrefix = tablePrefix;
370 }
371
372 public void setDataService(IDataService dataService) {
373 this.dataService = dataService;
374 }
375
376 public void setTriggerPrefix(String triggerPrefix) {
377 this.triggerPrefix = triggerPrefix;
378 }
379
380 public void setUpgradeService(IUpgradeService upgradeService) {
381 this.upgradeService = upgradeService;
382 }
383
384 public void setClusterService(IClusterService clusterService) {
385 this.clusterService = clusterService;
386 }
387
388 public void setRegistrationService(IRegistrationService registrationService) {
389 this.registrationService = registrationService;
390 }
391
392 }