View Javadoc

1   /*
2    * SymmetricDS is an open source database synchronization solution.
3    *   
4    * Copyright (C) Chris Henson <chenson42@users.sourceforge.net>,
5    *               Eric Long <erilong@users.sourceforge.net>
6    *
7    * This library is free software; you can redistribute it and/or
8    * modify it under the terms of the GNU Lesser General Public
9    * License as published by the Free Software Foundation; either
10   * version 3 of the License, or (at your option) any later version.
11   *
12   * This library is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15   * Lesser General Public License for more details.
16   *
17   * You should have received a copy of the GNU Lesser General Public
18   * License along with this library; if not, see
19   * <http://www.gnu.org/licenses/>.
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         // lets do this every time init is called.
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                     // TODO should probably check to see if the time stamp on
187                     // the symmetric-dialects.xml is newer than the
188                     // create time on the audit record.
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                 // TODO
232                 //nodeService.insertIdentity();
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             // We had no trigger_hist row, lets validate that the trigger as
324             // defined in the trigger
325             // does not exist as well.
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 }