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