001 package org.hackystat.sensorbase.resource.users; 002 003 import java.io.File; 004 import java.io.StringReader; 005 import java.io.StringWriter; 006 import java.util.HashMap; 007 import java.util.HashSet; 008 import java.util.Map; 009 import java.util.Set; 010 011 import javax.xml.bind.JAXBContext; 012 import javax.xml.bind.Marshaller; 013 import javax.xml.bind.Unmarshaller; 014 import javax.xml.parsers.DocumentBuilder; 015 import javax.xml.parsers.DocumentBuilderFactory; 016 import javax.xml.transform.Transformer; 017 import javax.xml.transform.TransformerFactory; 018 import javax.xml.transform.dom.DOMSource; 019 import javax.xml.transform.stream.StreamResult; 020 021 import org.hackystat.sensorbase.db.DbManager; 022 import org.hackystat.utilities.stacktrace.StackTrace; 023 import org.hackystat.utilities.tstamp.Tstamp; 024 import org.hackystat.sensorbase.resource.projects.ProjectManager; 025 import org.hackystat.sensorbase.resource.users.jaxb.Properties; 026 import org.hackystat.sensorbase.resource.users.jaxb.Property; 027 import org.hackystat.sensorbase.resource.users.jaxb.User; 028 import org.hackystat.sensorbase.resource.users.jaxb.UserIndex; 029 import org.hackystat.sensorbase.resource.users.jaxb.UserRef; 030 import org.hackystat.sensorbase.resource.users.jaxb.Users; 031 import org.hackystat.sensorbase.server.Server; 032 import static org.hackystat.sensorbase.server.ServerProperties.XML_DIR_KEY; 033 import static org.hackystat.sensorbase.server.ServerProperties.TEST_DOMAIN_KEY; 034 import static org.hackystat.sensorbase.server.ServerProperties.ADMIN_EMAIL_KEY; 035 import static org.hackystat.sensorbase.server.ServerProperties.ADMIN_PASSWORD_KEY; 036 import org.w3c.dom.Document; 037 038 /** 039 * Manages access to the User resources. 040 * Loads default definitions if available. 041 * 042 * Thread Safety Note: This class must NOT invoke any methods from ProjectManager or 043 * SensorDataManager in order to avoid potential deadlock. (ProjectManager and SensorDataManager 044 * both invoke methods from UserManager, so if UserManager were to invoke a method from 045 * either of these two classes, then we would have multiple locks not being acquired in the 046 * same order, which produces the potential for deadlock.) 047 * 048 * @author Philip Johnson 049 */ 050 public class UserManager { 051 052 /** Holds the class-wide JAXBContext, which is thread-safe. */ 053 private JAXBContext jaxbContext; 054 055 /** The Server associated with this UserManager. */ 056 Server server; 057 058 /** The DbManager associated with this server. */ 059 DbManager dbManager; 060 061 /** The UserIndex open tag. */ 062 public static final String userIndexOpenTag = "<UserIndex>"; 063 064 /** The UserIndex close tag. */ 065 public static final String userIndexCloseTag = "</UserIndex>"; 066 067 /** The initial size for Collection instances that hold the Users. */ 068 private static final int userSetSize = 127; 069 070 /** The in-memory repository of Users, keyed by Email. */ 071 private Map<String, User> email2user = new HashMap<String, User>(userSetSize); 072 073 /** The in-memory repository of User XML strings, keyed by User. */ 074 private Map<User, String> user2xml = new HashMap<User, String>(userSetSize); 075 076 /** The in-memory repository of UserRef XML strings, keyed by User. */ 077 private Map<User, String> user2ref = new HashMap<User, String>(userSetSize); 078 079 /** 080 * The constructor for UserManagers. 081 * @param server The Server instance associated with this UserManager. 082 */ 083 public UserManager(Server server) { 084 this.server = server; 085 this.dbManager = (DbManager)this.server.getContext().getAttributes().get("DbManager"); 086 try { 087 this.jaxbContext = 088 JAXBContext.newInstance( 089 org.hackystat.sensorbase.resource.users.jaxb.ObjectFactory.class); 090 loadDefaultUsers(); //NOPMD it's throwing a false warning. 091 initializeCache(); //NOPMD 092 initializeAdminUser(); //NOPMD 093 } 094 catch (Exception e) { 095 String msg = "Exception during UserManager initialization processing"; 096 server.getLogger().warning(msg + "\n" + StackTrace.toString(e)); 097 throw new RuntimeException(msg, e); 098 } 099 } 100 101 /** 102 * Loads the default Users from the defaults file and adds them to the database. 103 * @throws Exception If problems occur. 104 */ 105 private final void loadDefaultUsers() throws Exception { 106 // Get the default User definitions from the XML defaults file. 107 File defaultsFile = findDefaultsFile(); 108 // Add these users to the database if we've found a default file. 109 if (defaultsFile.exists()) { 110 server.getLogger().info("Loading User defaults from " + defaultsFile.getPath()); 111 Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); 112 Users users = (Users) unmarshaller.unmarshal(defaultsFile); 113 for (User user : users.getUser()) { 114 user.setLastMod(Tstamp.makeTimestamp()); 115 this.dbManager.storeUser(user, this.makeUser(user), this.makeUserRefString(user)); 116 } 117 } 118 } 119 120 /** Read in all Users from the database and initialize the in-memory cache. */ 121 private final void initializeCache() { 122 try { 123 UserIndex index = makeUserIndex(this.dbManager.getUserIndex()); 124 for (UserRef ref : index.getUserRef()) { 125 String email = ref.getEmail(); 126 String userString = this.dbManager.getUser(email); 127 User user = makeUser(userString); 128 this.updateCache(user); 129 } 130 } 131 catch (Exception e) { 132 server.getLogger().warning("Failed to initialize users " + StackTrace.toString(e)); 133 } 134 } 135 136 137 /** 138 * Ensures a User exists with the admin role given the data in the sensorbase.properties file. 139 * The admin password will be reset to what was in the sensorbase.properties file. 140 * Note that the "admin" role is managed non-persistently: it is read into the cache from 141 * the sensorbase.properties at startup, and any persistently stored values for it are 142 * ignored. This, of course, will eventually cause confusion. 143 * @throws Exception if problems creating the XML string representations of the admin user. 144 */ 145 private final void initializeAdminUser() throws Exception { 146 String adminEmail = server.getServerProperties().get(ADMIN_EMAIL_KEY); 147 String adminPassword = server.getServerProperties().get(ADMIN_PASSWORD_KEY); 148 // First, clear any existing Admin role property. 149 for (User user : this.email2user.values()) { 150 user.setRole("basic"); 151 } 152 // Now define the admin user with the admin property. 153 if (this.email2user.containsKey(adminEmail)) { 154 User user = this.email2user.get(adminEmail); 155 user.setPassword(adminPassword); 156 user.setRole("admin"); 157 } 158 else { 159 User admin = new User(); 160 admin.setEmail(adminEmail); 161 admin.setPassword(adminPassword); 162 admin.setRole("admin"); 163 this.updateCache(admin); 164 } 165 } 166 167 /** 168 * Updates the in-memory cache with information about this User. 169 * @param user The user to be added to the cache. 170 * @throws Exception If problems occur updating the cache. 171 */ 172 private final void updateCache(User user) throws Exception { 173 if (user.getLastMod() == null) { 174 user.setLastMod(Tstamp.makeTimestamp()); 175 } 176 updateCache(user, this.makeUser(user), this.makeUserRefString(user)); 177 } 178 179 /** 180 * Updates the cache given all the User representations. 181 * @param user The User. 182 * @param userXml The User as an XML string. 183 * @param userRef The User as an XML reference. 184 */ 185 private void updateCache(User user, String userXml, String userRef) { 186 this.email2user.put(user.getEmail(), user); 187 this.user2xml.put(user, userXml); 188 this.user2ref.put(user, userRef); 189 } 190 191 /** 192 * Checks ServerProperties for the XML_DIR property. 193 * If this property is null, returns the File for ./xml/defaults/users.defaults.xml. 194 * @return The File instance (which might not point to an existing file.) 195 */ 196 private File findDefaultsFile() { 197 String defaultsPath = "/defaults/users.defaults.xml"; 198 String xmlDir = server.getServerProperties().get(XML_DIR_KEY); 199 return (xmlDir == null) ? 200 new File (System.getProperty("user.dir") + "/xml" + defaultsPath) : 201 new File (xmlDir + defaultsPath); 202 } 203 204 /** 205 * Returns the XML string containing the UserIndex with all defined Users. 206 * Uses the in-memory cache of UserRef strings. 207 * @return The XML string providing an index to all current Users. 208 */ 209 public synchronized String getUserIndex() { 210 StringBuilder builder = new StringBuilder(512); 211 builder.append(userIndexOpenTag); 212 for (String ref : this.user2ref.values()) { 213 builder.append(ref); 214 } 215 builder.append(userIndexCloseTag); 216 return builder.toString(); 217 } 218 219 /** 220 * Updates the Manager with this User. Any old definition is overwritten. 221 * @param user The User. 222 */ 223 public synchronized void putUser(User user) { 224 try { 225 user.setLastMod(Tstamp.makeTimestamp()); 226 String xmlUser = this.makeUser(user); 227 String xmlRef = this.makeUserRefString(user); 228 this.updateCache(user, xmlUser, xmlRef); 229 this.dbManager.storeUser(user, xmlUser, xmlRef); 230 } 231 catch (Exception e) { 232 server.getLogger().warning("Failed to put User" + StackTrace.toString(e)); 233 } 234 } 235 236 237 /** 238 * Ensures that the passed User is no longer present in this Manager, and 239 * deletes all Projects associated with this user. 240 * @param email The email address of the User to remove if currently present. 241 */ 242 public synchronized void deleteUser(String email) { 243 User user = this.email2user.get(email); 244 // First, delete all the projects owned by this user. 245 ProjectManager projectManager = 246 (ProjectManager)this.server.getContext().getAttributes().get("ProjectManager"); 247 projectManager.deleteProjects(user); 248 // Now delete the user 249 if (user != null) { 250 this.email2user.remove(email); 251 this.user2xml.remove(user); 252 this.user2ref.remove(user); 253 } 254 this.dbManager.deleteUser(email); 255 } 256 257 258 /** 259 * Returns the User associated with this email address if they are currently registered, or null 260 * if not found. 261 * @param email The email address 262 * @return The User, or null if not found. 263 */ 264 public synchronized User getUser(String email) { 265 return (email == null) ? null : email2user.get(email); 266 } 267 268 /** 269 * Returns the User Xml String associated with this email address if they are registered, or 270 * null if user not found. 271 * @param email The email address 272 * @return The User XML string, or null if not found. 273 */ 274 public synchronized String getUserString(String email) { 275 User user = email2user.get(email); 276 return (user == null) ? null : user2xml.get(user); 277 } 278 279 /** 280 * Updates the given User with the passed Properties. 281 * @param user The User whose properties are to be updated. 282 * @param properties The Properties. 283 */ 284 public synchronized void updateProperties(User user, Properties properties) { 285 for (Property property : properties.getProperty()) { 286 user.getProperties().getProperty().add(property); 287 } 288 this.putUser(user); 289 } 290 291 /** 292 * Returns a set containing the current User instances. 293 * For thread safety, a fresh Set of Users is built each time this is called. 294 * @return A Set containing the current Users. 295 */ 296 public synchronized Set<User> getUsers() { 297 Set<User> userSet = new HashSet<User>(userSetSize); 298 userSet.addAll(this.email2user.values()); 299 return userSet; 300 } 301 302 /** 303 * Returns true if the User as identified by their email address is known to this Manager. 304 * @param email The email address of the User of interest. 305 * @return True if found in this Manager. 306 */ 307 public synchronized boolean isUser(String email) { 308 return (email != null) && email2user.containsKey(email); 309 } 310 311 /** 312 * Returns true if the User as identified by their email address and password 313 * is known to this Manager. 314 * @param email The email address of the User of interest. 315 * @param password The password of this user. 316 * @return True if found in this Manager. 317 */ 318 public synchronized boolean isUser(String email, String password) { 319 User user = this.email2user.get(email); 320 return (user != null) && (password != null) && (password.equals(user.getPassword())); 321 } 322 323 /** 324 * Returns true if email is a defined User with Admin privileges. 325 * @param email An email address. 326 * @return True if email is a User with Admin privileges. 327 */ 328 public synchronized boolean isAdmin(String email) { 329 return (email != null) && 330 email2user.containsKey(email) && 331 email.equals(server.getServerProperties().get(ADMIN_EMAIL_KEY)); 332 } 333 334 /** 335 * Returns true if the passed user is a test user. 336 * This is defined as a User whose email address uses the TEST_DOMAIN. 337 * @param user The user. 338 * @return True if the user is a test user. 339 */ 340 public synchronized boolean isTestUser(User user) { 341 return user.getEmail().endsWith(server.getServerProperties().get(TEST_DOMAIN_KEY)); 342 } 343 344 /** 345 * Registers a User, given their email address. 346 * If a User with the passed email address exists, then return the previously registered User. 347 * Otherwise create a new User and return it. 348 * If the email address ends with the test domain, then the password will be the email. 349 * Otherwise, a unique, randomly generated 12 character key is generated as the password. 350 * Defines the Default Project for each new user. 351 * @param email The email address for the user. 352 * @return The retrieved or newly created User. 353 */ 354 public synchronized User registerUser(String email) { 355 // registering happens rarely, so we'll just iterate through the userMap. 356 for (User user : this.email2user.values()) { 357 if (user.getEmail().equals(email)) { 358 return user; 359 } 360 } 361 // if we got here, we need to create a new User. 362 User user = new User(); 363 user.setEmail(email); 364 user.setProperties(new Properties()); 365 // Password is either their Email in the case of a test user, or the randomly generated string. 366 String password = 367 email.endsWith(server.getServerProperties().get(TEST_DOMAIN_KEY)) ? 368 email : PasswordGenerator.make(); 369 user.setPassword(password); 370 this.putUser(user); 371 return user; 372 } 373 374 /** 375 * Takes a String encoding of a Properties in XML format and converts it to an instance. 376 * 377 * @param xmlString The XML string representing a Properties. 378 * @return The corresponding Properties instance. 379 * @throws Exception If problems occur during unmarshalling. 380 */ 381 public final synchronized Properties makeProperties(String xmlString) throws Exception { 382 Unmarshaller unmarshaller = this.jaxbContext.createUnmarshaller(); 383 return (Properties)unmarshaller.unmarshal(new StringReader(xmlString)); 384 } 385 386 /** 387 * Takes a String encoding of a User in XML format and converts it to an instance. 388 * 389 * @param xmlString The XML string representing a User 390 * @return The corresponding User instance. 391 * @throws Exception If problems occur during unmarshalling. 392 */ 393 public final synchronized User makeUser(String xmlString) throws Exception { 394 Unmarshaller unmarshaller = this.jaxbContext.createUnmarshaller(); 395 return (User)unmarshaller.unmarshal(new StringReader(xmlString)); 396 } 397 398 /** 399 * Takes a String encoding of a UserIndex in XML format and converts it to an instance. 400 * 401 * @param xmlString The XML string representing a UserIndex. 402 * @return The corresponding UserIndex instance. 403 * @throws Exception If problems occur during unmarshalling. 404 */ 405 public final synchronized UserIndex makeUserIndex(String xmlString) 406 throws Exception { 407 Unmarshaller unmarshaller = this.jaxbContext.createUnmarshaller(); 408 return (UserIndex)unmarshaller.unmarshal(new StringReader(xmlString)); 409 } 410 411 /** 412 * Returns the passed User instance as a String encoding of its XML representation. 413 * Final because it's called in constructor. 414 * @param user The User instance. 415 * @return The XML String representation. 416 * @throws Exception If problems occur during translation. 417 */ 418 public final synchronized String makeUser (User user) throws Exception { 419 Marshaller marshaller = jaxbContext.createMarshaller(); 420 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 421 dbf.setNamespaceAware(true); 422 DocumentBuilder documentBuilder = dbf.newDocumentBuilder(); 423 Document doc = documentBuilder.newDocument(); 424 marshaller.marshal(user, doc); 425 DOMSource domSource = new DOMSource(doc); 426 StringWriter writer = new StringWriter(); 427 StreamResult result = new StreamResult(writer); 428 TransformerFactory tf = TransformerFactory.newInstance(); 429 Transformer transformer = tf.newTransformer(); 430 transformer.transform(domSource, result); 431 String xmlString = writer.toString(); 432 // Now remove the processing instruction. This approach seems like a total hack. 433 xmlString = xmlString.substring(xmlString.indexOf('>') + 1); 434 return xmlString; 435 } 436 437 /** 438 * Returns the passed Properties instance as a String encoding of its XML representation. 439 * @param properties The Properties instance. 440 * @return The XML String representation. 441 * @throws Exception If problems occur during translation. 442 */ 443 public synchronized String makeProperties (Properties properties) throws Exception { 444 Marshaller marshaller = jaxbContext.createMarshaller(); 445 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 446 dbf.setNamespaceAware(true); 447 DocumentBuilder documentBuilder = dbf.newDocumentBuilder(); 448 Document doc = documentBuilder.newDocument(); 449 marshaller.marshal(properties, doc); 450 DOMSource domSource = new DOMSource(doc); 451 StringWriter writer = new StringWriter(); 452 StreamResult result = new StreamResult(writer); 453 TransformerFactory tf = TransformerFactory.newInstance(); 454 Transformer transformer = tf.newTransformer(); 455 transformer.transform(domSource, result); 456 String xmlString = writer.toString(); 457 // Now remove the processing instruction. This approach seems like a total hack. 458 xmlString = xmlString.substring(xmlString.indexOf('>') + 1); 459 return xmlString; 460 } 461 462 /** 463 * Returns the passed User instance as a String encoding of its XML representation 464 * as a UserRef object. 465 * Final because it's called in constructor. 466 * @param user The User instance. 467 * @return The XML String representation of it as a UserRef 468 * @throws Exception If problems occur during translation. 469 */ 470 public final synchronized String makeUserRefString (User user) 471 throws Exception { 472 UserRef ref = makeUserRef(user); 473 Marshaller marshaller = jaxbContext.createMarshaller(); 474 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 475 dbf.setNamespaceAware(true); 476 DocumentBuilder documentBuilder = dbf.newDocumentBuilder(); 477 Document doc = documentBuilder.newDocument(); 478 marshaller.marshal(ref, doc); 479 DOMSource domSource = new DOMSource(doc); 480 StringWriter writer = new StringWriter(); 481 StreamResult result = new StreamResult(writer); 482 TransformerFactory tf = TransformerFactory.newInstance(); 483 Transformer transformer = tf.newTransformer(); 484 transformer.transform(domSource, result); 485 String xmlString = writer.toString(); 486 // Now remove the processing instruction. This approach seems like a total hack. 487 xmlString = xmlString.substring(xmlString.indexOf('>') + 1); 488 return xmlString; 489 } 490 491 /** 492 * Returns a UserRef instance constructed from a User instance. 493 * @param user The User instance. 494 * @return A UserRef instance. 495 */ 496 public synchronized UserRef makeUserRef(User user) { 497 UserRef ref = new UserRef(); 498 ref.setEmail(user.getEmail()); 499 ref.setHref(this.server.getHostName() + "users/" + user.getEmail()); 500 return ref; 501 } 502 503 } 504