1   package com.atlassian.config.bootstrap;
2   
3   import com.atlassian.config.ApplicationConfiguration;
4   import com.atlassian.config.ConfigurationException;
5   import com.atlassian.config.HomeLocator;
6   import com.atlassian.config.db.DatabaseDetails;
7   import com.atlassian.config.db.HibernateConfig;
8   import com.atlassian.config.db.HibernateConfigurator;
9   import com.atlassian.config.setup.SetupPersister;
10  import org.apache.commons.lang.StringUtils;
11  import org.slf4j.Logger;
12  import org.slf4j.LoggerFactory;
13  
14  import javax.naming.InitialContext;
15  import javax.naming.NamingException;
16  import javax.sql.DataSource;
17  import java.sql.Connection;
18  import java.sql.DriverManager;
19  import java.sql.SQLException;
20  import java.sql.Statement;
21  import java.util.*;
22  
23  /**
24   * Generic Bootstrap Manager using Spring & Hibernate
25   */
26  public class DefaultAtlassianBootstrapManager implements AtlassianBootstrapManager
27  {
28      /** @deprecated since 0.16. Create a private SLF4J Logger instead. */
29      @Deprecated
30      public static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(DefaultAtlassianBootstrapManager.class);
31      private static final Logger privateLog = LoggerFactory.getLogger(DefaultAtlassianBootstrapManager.class);
32  
33      // ------------------------------------------------------------------------------------------------------- Constants
34      // ------------------------------------------------------------------------------------------------- Type Properties
35      protected boolean bootstrapped;
36      protected String bootstrapFailureReason;
37      /**
38       * A helper operation string, used for conditional moves throughout the bootstrapManager in the controller
39       */
40      private String operation;
41  
42      // --------------------------------------------------------------------------------------------- Injected Properties
43      private List tables;
44  
45      // ---------------------------------------------------------------------------------------------------- Dependencies
46      protected ApplicationConfiguration applicationConfig;
47      protected SetupPersister setupPersister;
48      protected HomeLocator homeLocator;
49      protected HibernateConfigurator hibernateConfigurator;
50      protected HibernateConfig hibernateConfig;
51  
52      // ---------------------------------------------------------------------------------------------------- Constructors
53  
54      // -------------------------------------------------------------------------------------------------- Public Methods
55      public void init() throws BootstrapException
56      {
57          /**
58           * Importantly, we do not throw a bootstrap exception if the home variable has yet to be set.
59           * So, if we can't build a home  we'll let the user know about this via the normal process,
60           * CheckListAction
61           */
62          try
63          {
64              if (StringUtils.isNotEmpty(homeLocator.getHomePath()))
65              {
66                  applicationConfig.setApplicationHome(homeLocator.getHomePath());
67                  applicationConfig.setConfigurationFileName(homeLocator.getConfigFileName());
68  
69                  if (applicationConfig.configFileExists())
70                  {
71                      applicationConfig.load();
72                  }
73  
74                  afterConfigurationLoaded();
75  
76                  setupPersister.setSetupType(applicationConfig.getSetupType());
77                  if (SetupPersister.SETUP_STATE_COMPLETE.equals(setupPersister.getCurrentStep()))
78                  {
79                      // If persistence upgrade fails, do not publish
80                      if (!performPersistenceUpgrade())
81                      {
82                          return;
83                      }
84  
85                      applicationConfig.setSetupComplete(true);
86                      publishConfiguration();
87                  }
88              }
89              else
90              {
91                  // If no Home is set, the bootstrap is still considered successful,
92                  // we just move on to the checklist action that says there's no home set.
93                  privateLog.warn("Unable to set up application config: no home set");
94              }
95  
96              finishBootstrapInitialisation();
97  
98              bootstrapped = true;
99          }
100         catch (ConfigurationException e)
101         {
102             privateLog.error("Home is not configured properly: ", e);
103             bootstrapped = false;
104             bootstrapFailureReason = e.getMessage();
105         }
106     }
107 
108     public void publishConfiguration()
109     {
110         // default implementation does nothing
111     }
112 
113 
114     // --------------------------------------------------------------------------- ApplicationConfig convenience methods
115     /**
116      * Get a single property.
117      */
118     public Object getProperty(String key)
119     {
120         Object o = null;
121         try
122         {
123             o = applicationConfig.getProperty(key);
124         }
125         catch (NullPointerException e)
126         {
127             privateLog.error("BootstrapManager was asked to fetch property ({}) and found a NullPointer", key);
128         }
129 
130         return o;
131     }
132 
133     /**
134      * Set a single property.
135      */
136     public void setProperty(String key, Object value)
137     {
138         if (value == null)
139         {
140             applicationConfig.removeProperty(key);
141         }
142         else
143         {
144             applicationConfig.setProperty(key, value);
145         }
146         if (isSetupComplete())
147         {
148             publishConfiguration();
149         }
150     }
151 
152     public boolean isPropertyTrue(String prop)
153     {
154         return "true".equals(getString(prop));
155     }
156 
157     /**
158      * Remove a single property.
159      */
160     public void removeProperty(String key)
161     {
162         applicationConfig.removeProperty(key);
163     }
164 
165     /**
166      * Convenience method to retrieve a property as a string.
167      */
168     public String getString(String key)
169     {
170         return (String) applicationConfig.getProperty(key);
171     }
172 
173     public String getFilePathProperty(String key)
174     {
175         return getString(key);
176     }
177 
178     /**
179      * Retrieve all property keys.
180      */
181     public Collection getPropertyKeys()
182     {
183         return applicationConfig.getProperties().keySet();
184     }
185 
186     /**
187      * Get a map of all properties with a given prefix.
188      */
189     public Map getPropertiesWithPrefix(String prefix)
190     {
191         return applicationConfig.getPropertiesWithPrefix(prefix);
192     }
193 
194     public void save() throws ConfigurationException
195     {
196         applicationConfig.save();
197     }
198 
199     public String getConfiguredApplicationHome()
200     {
201         return homeLocator.getHomePath();
202     }
203 
204     /**
205      * This should be the first method called before any bootstrapManager logic is performed.
206      *
207      * @return true for a complete bootstrapManager, otherwise false
208      */
209     public boolean isSetupComplete()
210     {
211         return isBootstrapped() && applicationConfig.isSetupComplete();
212     }
213 
214     public void setSetupComplete(boolean complete)
215     {
216         applicationConfig.setSetupComplete(complete);
217     }
218 
219     // ---------------------------------------------------------------------------------------------------- Build Number
220     public String getBuildNumber()
221     {
222         return applicationConfig.getBuildNumber();
223     }
224 
225     public void setBuildNumber(String buildNumber)
226     {
227         applicationConfig.setBuildNumber(buildNumber);
228     }
229 
230 
231     // -------------------------------------------------------------------------------------- Hibernate / Database Setup
232 
233     /**
234      * Gets all hibernate properties from the config starting 'hibernate.'
235      *
236      * @return all hibernate properties in a Properties map
237      */
238     public Properties getHibernateProperties()
239     {
240         Properties props = new Properties();
241         props.putAll(applicationConfig.getPropertiesWithPrefix("hibernate."));
242         return props;
243     }
244 
245     /**
246      * Mediates the call to HibernateConfigurator's instance method, configureDatabase(dbDetails, embedded)
247      *
248      * @param dbDetails - DatabaseDetails object holding connection details
249      * @param embedded  - true if using the default Confluence database, HSQLDB
250      * @throws BootstrapException
251      */
252     public void bootstrapDatabase(DatabaseDetails dbDetails, boolean embedded)
253             throws BootstrapException
254     {
255         try
256         {
257             hibernateConfigurator.configureDatabase(dbDetails, embedded);
258         }
259         catch (ConfigurationException e)
260         {
261             privateLog.error("Could not successfully configure database:\n db: {}\n embedded = {}", dbDetails, embedded);
262             privateLog.error("ConfigurationException reads thus: ", e);
263             hibernateConfigurator.unconfigureDatabase();
264             throw new BootstrapException(e);
265         }
266 
267         Connection conn = null;
268         try
269         {
270             conn = getTestDatabaseConnection(dbDetails);
271             if (!databaseContainsExistingData(conn))
272             {
273                 throw new BootstrapException("Schema creation complete, but database tables don't seem to exist.");
274             }
275         }
276         finally
277         {
278             try
279             {
280                 if (conn != null) conn.close();
281             }
282             catch (SQLException e)
283             {
284                 // ignore exception in finally
285             }
286         }
287 
288         postBootstrapDatabase();
289     }
290 
291 
292 
293     /**
294      * Mediates the call to HibernateConfigurator's instance method, configureDatasource(datasourceName,
295      * hibernateDialect)
296      */
297     public void bootstrapDatasource(String datasourceName, String hibernateDialect)
298             throws BootstrapException
299     {
300         try
301         {
302             hibernateConfigurator.configureDatasource(datasourceName, hibernateDialect);
303         }
304         catch (ConfigurationException e)
305         {
306             privateLog.error("Could not successfully configure datasource:\n db: {}\n dialect = {}", datasourceName, hibernateDialect);
307             privateLog.error("ConfigurationException reads thus: ", e);
308             hibernateConfigurator.unconfigureDatabase();
309             throw new BootstrapException(e);
310         }
311         Connection connection = null;
312         try
313         {
314             connection = getTestDatasourceConnection(datasourceName);
315             if (!databaseContainsExistingData(connection))
316             {
317                 throw new BootstrapException("Schema creation complete, but tables could not be found.");
318             }
319         }
320         finally
321         {
322             try
323             {
324                 if (connection != null) connection.close();
325             }
326             catch (SQLException e)
327             {
328                 // ignore in finally
329             }
330         }
331         postBootstrapDatabase();
332     }
333 
334     /**
335      * Ensure we can open a connection to the configured database. If this fails, then the database is down or we got
336      * the connection string wrong.
337      *
338      * @throws BootstrapException if the connection fails for any reason.
339      */
340     public Connection getTestDatabaseConnection(DatabaseDetails databaseDetails) throws BootstrapException
341     {
342         Connection conn = null;
343         try
344         {
345             Class.forName(databaseDetails.getDriverClassName());
346             conn = DriverManager.getConnection(getDbUrl(databaseDetails),
347                                                databaseDetails.getUserName(),
348                                                databaseDetails.getPassword());
349             if (conn == null)
350             {
351                 throw new BootstrapException("Connection was null. We could not successfully connect to the specified database!");
352             }
353             return conn;
354         }
355         catch (SQLException e)
356         {
357             privateLog.error("Could not successfully test your database: ", e);
358             throw new BootstrapException(e);
359         }
360         catch (ClassNotFoundException e)
361         {
362             privateLog.error("Could not successfully test your database: ", e);
363             throw new BootstrapException(e);
364         }
365     }
366 
367 
368     /**
369      * Gets a test connection to the datasource.
370      *
371      * @throws BootstrapException if a connection cannot be made.
372      */
373     public Connection getTestDatasourceConnection(String datasourceName) throws BootstrapException
374     {
375         DataSource dsrc;
376 
377         privateLog.debug("datasource is {}", datasourceName);
378 
379         try
380         {
381             InitialContext ctx = new InitialContext();
382             dsrc = (DataSource) ctx.lookup(datasourceName);
383 
384             if (dsrc == null)
385             {
386                 throw new NamingException("Could not locate " + datasourceName);
387             }
388         }
389         catch (NamingException e)
390         {
391             privateLog.error("Could not locate datasource: " + datasourceName, e);
392             throw new BootstrapException("Could not locate datasource: " + datasourceName, e);
393         }
394         catch (ClassCastException e)
395         {
396             privateLog.error("Couldn't locate Datasource (" + datasourceName + ") in the initial context. An object was bound to this name but whatever we found, it wasn't a Datasource: ", e);
397             throw new BootstrapException("Couldn't locate Datasource (" + datasourceName + ") in the initial context. An object was bound to this name but whatever we found, it wasn't a Datasource: ", e);
398         }
399 
400         try
401         {
402             Connection conn = dsrc.getConnection();
403             conn.createStatement();
404             return conn;
405         }
406         catch (SQLException e)
407         {
408             privateLog.error("Couldn't open a connection on Datasource (" + datasourceName + "): ", e);
409             throw new BootstrapException("Couldn't open a connection on Datasource (" + datasourceName + "): ", e);
410         }
411         catch (NullPointerException e)
412         {
413             privateLog.error("Couldn't open a connection on Datasource (" + datasourceName + "): ", e);
414             throw new BootstrapException("Couldn't open a connection on Datasource (" + datasourceName + "): ", e);
415         }
416     }
417 
418     /**
419      * Returns true if any of the specified tables exists, otherwise false.
420      *
421      * @param connection the Connection to the Db to check
422      * @see {@link #setTables(List)}
423      */
424     public boolean databaseContainsExistingData(Connection connection)
425     {
426         for (Iterator iterator = getTables().iterator(); iterator.hasNext();)
427         {
428             String table = (String) iterator.next();
429             if (tableExists(connection, table))
430                 return true;
431         }
432         return false;
433     }
434 
435     private boolean tableExists(Connection conn, String table)
436     {
437         Statement st = null;
438         try
439         {
440             st = conn.createStatement();
441             st.executeQuery("select count(*) from " + table);
442             return true;
443         }
444         catch (SQLException e)
445         {
446             return false;
447         }
448         finally
449         {
450             if (st != null)
451             {
452                 try
453                 {
454                     st.close();
455                 }
456                 catch (SQLException e)
457                 {
458                     // ignore
459                 }
460             }
461         }
462     }
463 
464     // ------------------------------------------------------------------------------------------------ Application Home
465     public boolean isApplicationHomeValid()
466     {
467         return applicationConfig.isApplicationHomeValid();
468     }
469 
470     // ------------------------------------------------------------------------------------------------ Extension points
471 
472     /**
473      * Extension point for peforming custom upgardes of dB
474      * @return true if successful, false if failed
475      */
476     protected boolean performPersistenceUpgrade()
477     {
478         // do nudda
479         return true;
480     }
481 
482     /**
483      * Generic extension point to run before bootstrapping can be successful. Common things to do here includes setting
484      * up the license
485      * @throws ConfigurationException is thrown if bootstrapping should fail
486      */
487     protected void finishBootstrapInitialisation() throws ConfigurationException
488     {
489         // Do nudda
490     }
491 
492     /**
493      * Get the URL form the {@link DatabaseDetails} object. Allows sub-classes to post-process the URL
494      */
495     protected String getDbUrl(DatabaseDetails dbDetails)
496     {
497         return dbDetails.getDatabaseUrl();
498     }
499 
500     /**
501      * Allows a custom actions to be performed after bootstrapping the database
502      */
503     protected void postBootstrapDatabase() throws BootstrapException
504     {
505         // Do nudda
506     }
507 
508     /**
509      * Extension point for initialization performed after configuration is loaded
510      */
511     protected void afterConfigurationLoaded() throws ConfigurationException
512     {
513         // Do nudda
514     }
515 
516 
517 
518     // -------------------------------------------------------------------------------------- Basic accessors & mutators
519     public void setApplicationConfig(ApplicationConfiguration applicationConfig)
520     {
521         this.applicationConfig = applicationConfig;
522     }
523 
524     public void setHomeLocator(HomeLocator homeLocator)
525     {
526         this.homeLocator = homeLocator;
527     }
528 
529     public void setSetupPersister(SetupPersister setupPersister)
530     {
531         this.setupPersister = setupPersister;
532     }
533 
534     public HomeLocator getHomeLocator()
535     {
536         return homeLocator;
537     }
538 
539     public ApplicationConfiguration getApplicationConfig()
540     {
541         return applicationConfig;
542     }
543 
544     public String getApplicationHome()
545     {
546         return applicationConfig.getApplicationHome();
547     }
548 
549     public SetupPersister getSetupPersister()
550     {
551         return setupPersister;
552     }
553 
554     /**
555      * @return boolean indicating whether Confluence is bootstrapped.
556      */
557     public boolean isBootstrapped()
558     {
559         return bootstrapped;
560     }
561 
562     public String getOperation()
563     {
564         return operation;
565     }
566 
567     public void setOperation(String operation)
568     {
569         this.operation = operation;
570     }
571 
572     public HibernateConfigurator getHibernateConfigurator()
573     {
574         return hibernateConfigurator;
575     }
576 
577     public void setHibernateConfigurator(HibernateConfigurator hibernateConfigurator)
578     {
579         this.hibernateConfigurator = hibernateConfigurator;
580     }
581 
582     public HibernateConfig getHibernateConfig()
583     {
584         return hibernateConfig;
585     }
586 
587     public void setHibernateConfig(HibernateConfig hibernateConfig)
588     {
589         this.hibernateConfig = hibernateConfig;
590     }
591 
592     public String getBootstrapFailureReason()
593     {
594         return bootstrapFailureReason;
595     }
596 
597     public List getTables()
598     {
599         return tables;
600     }
601 
602     public void setTables(List tables)
603     {
604         this.tables = tables;
605     }
606 }