001package com.randomnoun.common.webapp; 002 003/* (c) 2013-15 randomnoun. All Rights Reserved. This work is licensed under a 004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html) 005 */ 006 007import java.beans.PropertyVetoException; 008import java.io.BufferedInputStream; 009import java.io.File; 010import java.io.FileInputStream; 011import java.io.InputStream; 012import java.io.InputStreamReader; 013import java.sql.Connection; 014import java.sql.DriverManager; 015import java.sql.SQLException; 016import java.util.Enumeration; 017import java.util.HashMap; 018import java.util.Iterator; 019import java.util.Map; 020import java.util.Properties; 021import java.util.ResourceBundle; 022 023import javax.naming.Context; 024import javax.naming.InitialContext; 025import javax.naming.NamingException; 026import javax.sql.DataSource; 027 028import com.mchange.v2.c3p0.ComboPooledDataSource; 029 030import org.apache.log4j.Logger; 031import org.apache.log4j.PropertyConfigurator; 032import org.springframework.jdbc.core.JdbcTemplate; 033import org.springframework.jdbc.datasource.SingleConnectionDataSource; 034import org.springframework.dao.DataAccessResourceFailureException; 035 036import com.randomnoun.common.PropertyParser; 037import com.randomnoun.common.Struct; 038import com.randomnoun.common.Text; 039import com.randomnoun.common.jexl.sql.SqlGenerator; 040import com.randomnoun.common.security.SecurityAuthenticator; 041import com.randomnoun.common.security.SecurityContext; 042import com.randomnoun.common.security.SecurityLoader; 043import com.randomnoun.common.security.User; 044import com.randomnoun.common.security.impl.NullSecurityAuthenticatorImpl; 045import com.randomnoun.common.security.impl.NullSecurityLoaderImpl; 046import com.randomnoun.common.security.impl.SpringSecurityAuthenticatorImpl; 047import com.randomnoun.common.security.impl.SpringSecurityLoaderImpl; 048 049 050/** An attempt to standardise config objects across the handful of webapps in this 051 * workspace. 052 * 053 * <p>Holds configuration data for this web application. Is used to look up 054 * resources for use by other application components, including: 055 * 056 * <ul> 057 * <li>database connections (JdbcTemplates) 058 * <li>security contexts (for authentication / authorisation) 059 * </ul> 060 * 061 * @author Greg 062 * 063 */ 064public abstract class AppConfigBase extends Properties { 065 066 /** Generated serialVersionUID */ 067 private static final long serialVersionUID = -5375559262217993785L; 068 069 /** Logger instance for this class */ 070 public static Logger logger = Logger.getLogger(AppConfigBase.class); 071 072 /** Global instance */ 073 public static AppConfigBase instance = null; 074 075 /** JdbcTemplates for this AppConfig */ 076 protected Map<String, JdbcTemplate> jdbcTemplates = new HashMap<String, JdbcTemplate>(); 077 078 /** Datasource underlying the jdbcTemplates */ 079 protected Map<String, DataSource> dataSources = new HashMap<String, DataSource>(); 080 081 /** Security context used for authentication and authorisation */ 082 protected SecurityContext securityContext; 083 084 /** If set to non-null, indicates an exception that occurred during initialisation. */ 085 protected Throwable initialisationFailure = null; 086 087 /** The name of the -D directive which, if present, specifies the folder containing the 088 * configuration file; e.g. "com.randomnoun.appName.configPath" 089 */ 090 public abstract String getSystemPropertyKeyConfigPath(); 091 092 /** The name of the file which configures this application; e.g. "/appName-web.properties" */ 093 public abstract String getConfigResourceLocation(); 094 095 /** Ensure that the database drivers are accessible, for simple and dbcp connection types 096 * 097 * @throws RuntimeExcption if the database drivers are not on the classpath of this webapp 098 */ 099 protected void initDatabase() { 100 logger.info("Initialising database..."); 101 try { 102 String connectionType = getProperty("database.connectionType"); 103 if (connectionType==null || 104 connectionType.equals("simple") || 105 connectionType.equals("dbcp")) { 106 Class.forName(getProperty("database.driver")).getDeclaredConstructor().newInstance(); 107 } 108 } catch (Exception e) { 109 initialisationFailure = e; 110 throw new RuntimeException("Could not load database driver '" + getProperty("database.driver") + "'", e); 111 } 112 } 113 114 /** Initialises log4j 115 */ 116 protected void initLogger() { 117 System.out.println("Initialising log4j..."); 118 119 Properties props = new Properties(); 120 props.putAll(PropertyParser.restrict(this, "log4j", false)); 121 122 // replace ${xxxx}-style placeholders 123 String key, value, varName, varValue; 124 int startIdx, endIdx; 125 for (Enumeration<Object> e = props.keys(); e.hasMoreElements(); ) { 126 key = (String) e.nextElement(); 127 value = (String) props.get(key); 128 startIdx = 0; 129 startIdx = value.indexOf("${", startIdx); 130 while (startIdx > -1) { 131 endIdx = value.indexOf("}", startIdx+2); 132 if (endIdx>-1) { 133 varName = value.substring(startIdx+2, endIdx-startIdx-2); 134 varValue = (String) this.get(varName); 135 if (varValue == null) { varValue = ""; } 136 value = value.substring(0, startIdx) + varValue + value.substring(endIdx + 1); 137 startIdx = value.indexOf("${", startIdx); 138 } else { 139 startIdx = -1; 140 } 141 } 142 } 143 144 PropertyConfigurator.configure(props); 145 logger.debug("log4j initialised"); 146 } 147 148 protected void initSecurityContext() { 149 150 if (securityContext==null) { 151 logger.info("Initialising security context..."); 152 if (!"false".equals(getProperty("auth.enableSecurityContext"))) { 153 Map<String, Object> securityProperties = new HashMap<String, Object>(); 154 // no customerIds, applicationIds, auditUsernames or table suffixes ! Yay ! 155 String dbVendor = (String) this.get("database.vendor"); 156 if (Text.isBlank(dbVendor)) { dbVendor = SqlGenerator.DATABASE_MYSQL; } 157 158 securityProperties.put(SpringSecurityLoaderImpl.INIT_DATABASE_VENDOR, dbVendor); 159 securityProperties.put(SpringSecurityLoaderImpl.INIT_JDBCTEMPLATE, getJdbcTemplate()); 160 161 securityProperties.put(SecurityContext.INIT_CASE_INSENSITIVE, this.get("securityContext.caseInsensitive")); 162 securityProperties.put(SecurityContext.INIT_USER_CACHE_SIZE, this.get("securityContext.userCacheSize")); 163 securityProperties.put(SecurityContext.INIT_USER_CACHE_EXPIRY, this.get("securityContext.userCacheExpiry")); 164 165 SecurityLoader securityLoader = new SpringSecurityLoaderImpl(); 166 SecurityAuthenticator securityAuthenticator = new SpringSecurityAuthenticatorImpl(); 167 168 securityContext = new SecurityContext(securityProperties, securityLoader, securityAuthenticator); 169 } else { 170 Map<String, Object> securityProperties = new HashMap<String, Object>(); 171 SecurityLoader securityLoader = new NullSecurityLoaderImpl(); 172 SecurityAuthenticator securityAuthenticator = new NullSecurityAuthenticatorImpl(); 173 securityContext = new SecurityContext(securityProperties, securityLoader, securityAuthenticator); 174 } 175 } 176 } 177 178 /** Sets the hostname application property. Should be called before {@link #loadProperties()}, 179 * which may have ENVIRONMENT settings that rely on the hostname. 180 */ 181 protected void initHostname() { 182 String hostname; 183 try { 184 java.net.InetAddress localMachine = java.net.InetAddress.getLocalHost(); 185 hostname = localMachine.getHostName(); 186 } catch(java.net.UnknownHostException uhe) { 187 hostname = "localhost"; 188 } 189 this.put("hostname", hostname); 190 } 191 192 /** Returns a bundle to be used for internationalisation. The bundle will initially be 193 * searched for using the standard ResourceBundle.getBundle(name) method (i.e. in the 194 * server's classpath); and, if this fails, will return 195 * ResourceBundle("resources.i18n." + name); which will search the i18n tree of the 196 * eomail project. 197 * 198 * @param i18nBundleName The bundle name to search for (e.g. "user") 199 * @param user The user for whom we are retrieving i18ned messages for (the locale of this 200 * user will be used to determine which resource bundle to use) 201 * @return The ResourceBundle requested. 202 */ 203 public ResourceBundle getBundle(String i18nBundleName, User user) { 204 if (i18nBundleName.startsWith("resources")) { 205 return ResourceBundle.getBundle(i18nBundleName, user.getLocale()); 206 } else { 207 return ResourceBundle.getBundle("resources.i18n." + i18nBundleName, user.getLocale()); 208 } 209 210 } 211 212 213 /** Load the configuration from both the classpath and the 214 * filesystem. Properties defined on the filesystem will override any contained within 215 * the web application. 216 * 217 * <p>Multiple environments can be specified in properties files using the 218 * STARTENVIRONMENT/ENDENVIRONMENT tags. The tags use the machine's hostname as an 219 * environment specifier. 220 */ 221 protected void loadProperties() { 222 String configPath = System.getProperty(getSystemPropertyKeyConfigPath()); 223 if (configPath==null) { configPath = "."; } 224 File configFile = new File(configPath + getConfigResourceLocation()); 225 try { 226 // load from resources 227 System.out.println("Loading properties file for environment '" + this.getProperty("hostname") + "'"); 228 InputStream is = this.getClass().getClassLoader().getResourceAsStream(getConfigResourceLocation()); 229 if (is==null && getConfigResourceLocation().charAt(0)=='/') { 230 // command-line support 231 is = this.getClass().getClassLoader().getResourceAsStream(getConfigResourceLocation().substring(1)); 232 } 233 if (is==null) { 234 System.out.println("Could not find internal property file"); 235 } else { 236 PropertyParser parser = new PropertyParser(new InputStreamReader(is), this.getProperty("hostname")); 237 Properties props = parser.parse(); 238 is.close(); 239 this.putAll(props); 240 } 241 242 // then load from file (if it exists) 243 if (configFile.exists()) { 244 System.out.println("Loading properties file from '" + configFile.getCanonicalPath() + "'"); 245 is = new BufferedInputStream(new FileInputStream(configFile)); 246 PropertyParser parser = new PropertyParser(new InputStreamReader(is), this.getProperty("hostname")); 247 Properties props = parser.parse(); 248 is.close(); 249 this.putAll(props); 250 } else { 251 System.out.println("Properties file not found at '" + configFile.getCanonicalPath() + "' ... using defaults"); 252 253 } 254 } catch (Exception e) { 255 this.initialisationFailure = e; 256 throw new RuntimeException("Could not load " + getConfigResourceLocation(), e); 257 } 258 } 259 260 261 /** Return a jdbcTemplate object that can be used to query the database 262 * 263 * @return a jdbcTemplate to the database 264 */ 265 public JdbcTemplate getJdbcTemplate() { 266 return getJdbcTemplate(null); 267 } 268 269 /** Returns a named jdbcTemplate object that can be used to query the database 270 * 271 * @param connectionName The name of the connection 272 * 273 * @return a jdbcTemplate to the database 274 */ 275 public synchronized JdbcTemplate getJdbcTemplate(String connectionName) { 276 String prefix = connectionName == null ? "database." : "databases." + connectionName + "."; 277 278 JdbcTemplate jt = jdbcTemplates.get(connectionName); 279 if (jt==null) { 280 String connectionType = getProperty(prefix + "connectionType"); 281 if (connectionType==null) { connectionType = "simple"; } 282 283 if (connectionType.equals("none")) { 284 if (!jdbcTemplates.containsKey(connectionName)) { 285 dataSources.put(connectionName, null); 286 jdbcTemplates.put(connectionName, null); 287 } 288 } else { 289 String driver = getProperty(prefix + "driver"); 290 String url = getProperty(prefix + "url"); 291 String username = getProperty(prefix + "username"); 292 String password = getProperty(prefix + "password"); 293 logger.info("Retrieving " + connectionType + " connection for '" + username + "'/'" + password + "' @ '" + url + "'"); 294 DataSource ds = null; 295 if (!Text.isBlank(driver)) { 296 try { 297 Class.forName(driver); 298 } catch (ClassNotFoundException e) { 299 logger.error("Error loading class '" + driver + "' for database '" + connectionName + "'", e); 300 } 301 } 302 if (connectionType.equals("simple")) { 303 if (url==null) { throw new NullPointerException(prefix + "url has not been initialised"); } 304 Connection connection; 305 try { 306 if (Text.isBlank(username)) { 307 connection = DriverManager.getConnection (url); 308 } else { 309 connection = DriverManager.getConnection (url, username, password); 310 } 311 } catch (SQLException sqle) { 312 throw new DataAccessResourceFailureException("Could not open connection to database", sqle); 313 } 314 ds = new SingleConnectionDataSource(connection, false); 315 316 } else if (connectionType.equals("dbcp")) { 317 throw new UnsupportedOperationException("dbcp is no longer supported; use c3p0 instead"); 318 319 } else if (connectionType.equals("c3p0")) { 320 if (url==null) { throw new NullPointerException(prefix + "url has not been initialised"); } 321 ComboPooledDataSource cpds = new ComboPooledDataSource (); 322 try { 323 cpds.setDriverClass(driver); 324 } catch (PropertyVetoException e) { 325 throw new IllegalStateException("Could not set driverClass '" + driver + "' for c3p0 datasource", e); 326 } 327 @SuppressWarnings("unchecked") 328 Map<String,String> c3poProps = (Map<String, String>) PropertyParser.restrict(this, prefix + "c3p0", true); 329 // remove extensions from props 330 for (Iterator<Map.Entry<String,String>> i = c3poProps.entrySet().iterator(); i.hasNext(); ) { 331 Map.Entry<String,String> e=i.next(); 332 if (e.getKey().startsWith("extensions.")) { i.remove(); } 333 } 334 Struct.setFromMap(cpds, c3poProps, false, true, false); 335 336 // set extensions separately 337 @SuppressWarnings("unchecked") 338 Map<String,String> c3poExtensionProps = (Map<String, String>) PropertyParser.restrict(this, prefix + "c3p0.extensions", true); 339 cpds.setExtensions(c3poExtensionProps); 340 cpds.setJdbcUrl(url); 341 if (!Text.isBlank(username)) { 342 cpds.setUser(username); 343 cpds.setPassword(password); 344 } 345 ds = cpds; 346 347 } else if (connectionType.equals("jndi")) { 348 String jndiName = getProperty(prefix + "jndiName"); 349 try { 350 InitialContext ctx = new InitialContext(); 351 Context envContext = (Context) ctx.lookup("java:/comp/env"); 352 ds = (DataSource) envContext.lookup(jndiName); 353 // could fallback to global datasource if comp/env is not here 354 } catch (NamingException e) { 355 throw new IllegalStateException("Could not retrieve datasource '" + jndiName + "' from JNDI", e); 356 } 357 } else { 358 throw new IllegalStateException("Unknown " + prefix + "connectionType property '" + connectionType + "')"); 359 } 360 361 jt = new JdbcTemplate(ds); 362 dataSources.put(connectionName, ds); 363 jdbcTemplates.put(connectionName, jt); 364 } 365 } 366 return jt; 367 } 368 369 370 /** Return the security context for this application 371 * 372 * @return the security context for this application 373 */ 374 public SecurityContext getSecurityContext() { 375 if (securityContext==null) { 376 throw new IllegalStateException("Security context not initialised"); 377 } 378 return securityContext; 379 } 380 381 382 /** Determines whether a user 'may have' a permission on the application. 383 * See the hasPermission method in SecurityContext for more details. 384 * 385 * @param user The user we are checking permissions for. 386 * @param permission The permission we are checking. 387 * @return 'true' if the user may be authorised to perform the permission, 388 * false otherwise 389 */ 390 public boolean hasPermission(User user, String permission) 391 { 392 if ("false".equals(getProperty("auth.enableSecurityContext"))) { return true; } 393 SecurityContext securityContext = getSecurityContext(); 394 return securityContext.hasPermission(user, permission); 395 } 396 397 /** Determines whether a user 'may have' a permission on the application. 398 * See the hasPermission method in SecurityContext for more details. If the user 399 * does not have the permission, then a SecurityException is thrown 400 * 401 * @see SecurityContext#hasPermission(com.randomnoun.common.security.User, String) 402 * 403 * @param user The user we are checking permissions for. 404 * @param permission The permission we are checking. 405 * 406 * @throws SecurityException if the user does not have the supplied permission 407 */ 408 public void checkPermission(User user, String permission) throws SecurityException { 409 if (!hasPermission(user, permission)) { 410 throw new SecurityException("User does not have '" + permission + "' permission"); 411 } 412 } 413 414 /** Determines whether a user has a permission on the application. 415 * See the hasPermission method in SecurityContext for more details. 416 * 417 * @param user The user we are checking permissions for. 418 * @param permission The permission we are checking. 419 * @param resourceContext Fine-grained resource context 420 * @return 'true' if the user is authorised to perform the permission, 421 * false otherwise 422 */ 423 public boolean hasPermission(User user, String permission, Map<String, Object> resourceContext) 424 { 425 if ("false".equals(getProperty("auth.enableSecurityContext"))) { return true; } 426 SecurityContext securityContext = getSecurityContext(); 427 return securityContext.hasPermission(user, permission, resourceContext); 428 } 429 430 431 /** Provides direct access to the datasource for this application. Most applications should use the 432 * {@link #getJdbcTemplate()} method instead. 433 * 434 * @return the dataSource underlying the jdbcTemplate to the database 435 */ 436 public DataSource getDataSource() { 437 getJdbcTemplate(); // make sure default dataSource is initialised 438 return dataSources.get(null); 439 } 440 441 /** Provides direct access to a named datasource for this application. Most applications should use the 442 * {@link #getJdbcTemplate(String)} method instead. 443 * 444 * @param connectionName the name of the connection 445 * 446 * @return the dataSource underlying the named jdbcTemplate to the database 447 */ 448 public DataSource getDataSource(String connectionName) { 449 getJdbcTemplate(connectionName); // make sure default dataSource is initialised 450 return dataSources.get(connectionName); 451 } 452 453}