[Yanel-dev] User management resource: pagination

Cedric Staub cedric.staub at wyona.com
Wed Sep 1 15:06:06 CEST 2010


Hello

Here is the next version of my patch. It does not include a search
feature yet, but it now uses iterators instead of arrays and is
substantially faster as a result. I attached both a patch for Yanel and
a patch for the security library.

Please note note that Yarep does not support using iterators yet, so
loading the nodes from Yarep is still slow. However, the User objects
are only created when they are actually used now (as opposed to before).

So to summarize: with this patch it'll still be slow but not as slow as
the current implementation ;-).

Cheers
Cedric
-------------- next part --------------
Index: htdocs/list-users.jelly
===================================================================
--- htdocs/list-users.jelly	(revision 52644)
+++ htdocs/list-users.jelly	(working copy)
@@ -3,6 +3,11 @@
   <html xmlns="http://www.w3.org/1999/xhtml">
     <body>
       <h1>Users</h1>
+      <p>
+        Showing users ${resource.getLowerBound()} through ${resource.getUpperBound()}
+        out of a total of ${resource.getTotalUsers()}.
+      </p>
+      <!-- Table of users -->
       <table border="1">
         <tr>
           <th>ID</th>
@@ -16,13 +21,23 @@
             <td>${user.getName()}</td>
             <td>${user.getEmail()}</td>
             <td><a href="update-user-admin.html?userID=${user.getID()}">Update/Edit</a></td>
-<!-- Edit user whereas one needs to enter old password and also cannot change group settings
-            <td><a href="update-user.html?userID=${user.getID()}">Edit</a></td>
--->
             <td><a href="delete-user.html?userID=${user.getID()}">Delete</a></td>
           </tr>
         </j:forEach>
       </table>
+      <!-- Pagination -->
+      <p>
+        <j:if test="${resource.getCurrentPage() &gt; 1}">
+          <a href="?page=${resource.getCurrentPage()-1}">? Prev</a>
+          &#160;
+        </j:if>
+        Page ${resource.getCurrentPage()}/${resource.getTotalPages()}
+        <j:if test="${resource.hasNext()}">
+          &#160;
+          <a href="?page=${resource.getCurrentPage()+1}">Next ?</a>
+        </j:if>
+      </p>
+      <!-- Links -->
       <a href="create-user.html">Create new user</a>
     </body>
   </html>
Index: src/java/org/wyona/yanel/impl/resources/ListUsersResource.java
===================================================================
--- src/java/org/wyona/yanel/impl/resources/ListUsersResource.java	(revision 52644)
+++ src/java/org/wyona/yanel/impl/resources/ListUsersResource.java	(working copy)
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Wyona
+ * Copyright 2010 Wyona
  *
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -16,25 +16,167 @@
 
 package org.wyona.yanel.impl.resources;
 
-import org.wyona.security.core.api.AccessManagementException;
 import org.wyona.security.core.api.User;
 import org.wyona.security.core.api.UserManager;
+import org.wyona.security.core.api.AccessManagementException;
 import org.wyona.yanel.impl.resources.usecase.UsecaseException;
 import org.wyona.yanel.impl.resources.usecase.UsecaseResource;
 
+import java.lang.System;
+import java.lang.Integer;
+import java.lang.Boolean;
 
+import java.util.List;
+import java.util.LinkedList;
+import java.util.Iterator;
+
 /**
- *
+ * Resource to list all users.
  */
 public class ListUsersResource extends UsecaseResource {
+    // Constants
+    private static final int DEFAULT_ITEMS_PER_PAGE = 100;
 
-    public User[] getUsers() throws UsecaseException {
+    // Variables
+    private int currentPage = 1;
+    private int totalPages = 1;
+    private int itemsPerPage = 1;
+    private int lowerBound = 1;
+    private int upperBound = 1;
+    private int totalUsers = 0;
+    private boolean hasNext = false;
+    private List<User> users = null;
+    private boolean initialized = false;
+
+    /**
+     * Initialize all variables.
+     * This function intializes various private variables. You don't need
+     * to call this function, it will be called automatically the first time
+     * you access a variable and the object finds that it is not initialized.
+     */
+    private void initVars() throws UsecaseException {
         UserManager userManager = getRealm().getIdentityManager().getUserManager();
+
+        // Pagination
+        // Current page
         try {
-            return userManager.getUsers(true);
+            String p = getParameterAsString("page");
+            currentPage = Integer.parseInt(p);
+            if(currentPage < 1) currentPage = 1;
+        } catch(Exception e) {
+            currentPage = 1;
+        }
+
+        // Items per page
+        try {
+            String i = getResourceConfigProperty("items-per-page");
+            itemsPerPage = Integer.parseInt(i);
+            if(itemsPerPage < 1) itemsPerPage = 1;
+        } catch(Exception e) {
+            itemsPerPage = DEFAULT_ITEMS_PER_PAGE;
+        }
+
+        // Result
+        try {
+            // All users matching search term,
+            // or all users overall if search term is empty
+            Iterator<User> allUsers;
+            allUsers = userManager.getAllUsers();
+
+            // Boundaries...
+            lowerBound = (currentPage-1)*itemsPerPage;
+            upperBound = lowerBound+itemsPerPage-1;
+            totalUsers = userManager.getUserCount();
+            totalPages = totalUsers/itemsPerPage+1;
+
+            int idx = 0;
+            users = new LinkedList<User>();
+            while(allUsers.hasNext()) {
+                User current = allUsers.next();
+                if(idx >= lowerBound && idx < upperBound) {
+                    users.add(current);
+                }
+                idx = idx + 1;
+            }
+
+            if(idx < upperBound) {
+                hasNext = false;
+                upperBound = idx;
+            } else {
+                hasNext = true;
+            }
+
+            lowerBound++;
+            initialized = true;
         } catch (AccessManagementException e) {
+            initialized = false;
             throw new UsecaseException(e.toString(), e);
         }
     }
 
+    /**
+     * Get users on current page.
+     */
+    public Iterator<User> getUsers() throws UsecaseException {
+        if(!initialized) initVars();
+        return users.iterator();
+    }
+
+    /**
+     * Get the current page being displayed.
+     */
+    public String getCurrentPage() throws UsecaseException {
+        if(!initialized) initVars();
+        return Integer.toString(currentPage);
+    }
+
+    /**
+     * Get the lower bound being displayed.
+     * For example, if we're on page 2 and there are
+     * 10 users being displayed per page, this value
+     * will be 11 - because the first user on the page
+     * is user number 11 in the array.
+     */
+    public String getLowerBound() throws UsecaseException {
+        if(!initialized) initVars();
+        return Integer.toString(lowerBound);
+    }
+
+    /**
+     * Get the upper bound being displayed.
+     * For example, if we're on page 2 and there are
+     * 10 users being displayed per page, this value
+     * will be 20 - because the last user on the page
+     * is user number 20 in the array.
+     */
+    public String getUpperBound() throws UsecaseException {
+        if(!initialized) initVars();
+        return Integer.toString(upperBound);
+    }
+
+    /**
+     * Get the total amount of pages.
+     */
+    public String getTotalPages() throws UsecaseException {
+        if(!initialized) initVars();
+        return Integer.toString(totalPages);
+    }
+
+    /**
+     * Get the total amount of existing users.
+     */
+    public String getTotalUsers() throws UsecaseException {
+        if(!initialized) initVars();
+        return Integer.toString(totalUsers);
+    }
+
+    /**
+     * Check if there is another page.
+     * Return true if there is another page beyond the 
+     * current one, false if there is not.
+     */
+    public String hasNext() throws UsecaseException {
+        if(!initialized) initVars();
+        return Boolean.toString(hasNext);
+    }
 }
-------------- next part --------------
Index: src/impl/java/org/wyona/security/impl/yarep/YarepUserManager.java
===================================================================
--- src/impl/java/org/wyona/security/impl/yarep/YarepUserManager.java	(revision 52863)
+++ src/impl/java/org/wyona/security/impl/yarep/YarepUserManager.java	(working copy)
@@ -97,6 +97,20 @@
             throw new AccessManagementException(errorMsg, e);
         }
     }
+    
+    /**
+     * Get user count.
+     */
+    public int getUserCount() {
+        log.info("Load users from repository '" + identitiesRepository.getConfigFile() + "'");
+        try {
+            Node usersParentNode = getUsersParentNode();
+            Node[] userNodes = usersParentNode.getNodes();
+            return userNodes.length;
+        } catch(Exception e) {
+            return 0;
+        }
+    }
 
     /**
      * Loads a specific user from persistance storage into memory
@@ -319,7 +333,7 @@
      * @see org.wyona.security.core.api.UserManager#getAllUsers()
      */
     public java.util.Iterator<User> getAllUsers() throws AccessManagementException {
-        return new YarepUsersIterator();
+        return new YarepUsersIterator(identityManager, identitiesRepository, cacheEnabled, resolveGroupsAtCreation);
     }
 
     /**
Index: src/impl/java/org/wyona/security/impl/yarep/YarepUsersIterator.java
===================================================================
--- src/impl/java/org/wyona/security/impl/yarep/YarepUsersIterator.java	(revision 52863)
+++ src/impl/java/org/wyona/security/impl/yarep/YarepUsersIterator.java	(working copy)
@@ -1,34 +1,112 @@
 package org.wyona.security.impl.yarep;
 
 import org.apache.log4j.Logger;
+import org.apache.avalon.framework.configuration.Configuration;
+import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
 
+import java.util.List;
+import java.util.LinkedList;
+import java.util.Iterator;
+import java.lang.UnsupportedOperationException;
+
+import org.wyona.security.core.api.User;
+import org.wyona.security.core.api.IdentityManager;
+import org.wyona.security.core.api.AccessManagementException;
+
+import org.wyona.yarep.core.Node;
+import org.wyona.yarep.core.Repository;
+
 /**
- *
+ * Yarep users iterator.
+ * Warning! This implementation does not scale, because
+ * Yarep doesn't have the ability to give us an Iterator.
+ * Therefore, we have to request all nodes in an array,
+ * which somehow defeats the purpose of this iterator.
+ * But until Yarep gains that capability, we're don't
+ * have much of a choice.
  */
-public class YarepUsersIterator implements java.util.Iterator {
-
+public class YarepUsersIterator extends YarepUserManager implements java.util.Iterator {
+    // Constants
     private static Logger log = Logger.getLogger(YarepUsersIterator.class);
+    private static DefaultConfigurationBuilder configBuilder = new DefaultConfigurationBuilder(true);
 
+    // Variables
+    private int totalUsers;
+    private int currentUser;
+    private List<Node> userNodeList;
+    private Iterator<Node> listIterator;
+
     /**
+     * Constructor.
+     */
+    public YarepUsersIterator(IdentityManager identityManager, 
+                              Repository identitiesRepository, 
+                              boolean cacheEnabled, 
+                              boolean resolveGroupsAtCreation) 
+                              throws AccessManagementException {
+        // Call parent
+        super(identityManager, identitiesRepository, cacheEnabled, resolveGroupsAtCreation);
+        log.info("Load users from repository '" + identitiesRepository.getConfigFile() + "'");
+
+        // TODO: This is inefficient! If Yarep were able to
+        // return an iterator instead of an array, this could
+        // be done in a more effective manner, but until Yarep
+        // gains that capability we're currently stuck like this.
+        try {
+            Node usersParentNode = super.getUsersParentNode();
+            Node[] userNodes = usersParentNode.getNodes();
+
+            userNodeList = new LinkedList<Node>();
+            for(Node n : userNodes) {
+                if(n.isResource()) {
+                    userNodeList.add(n);
+                }
+            }
+
+            listIterator = userNodeList.listIterator();
+        } catch(Exception e) {
+            String errorMsg = "Could not read users from repository: " + e.getMessage();
+            log.error(errorMsg, e);
+            throw new AccessManagementException(errorMsg, e);
+        }
+    }
+
+    /**
      * @see java.util.Iterator#hasNext()
      */
+    @Override
     public boolean hasNext() {
-        log.error("TODO: Not implemented yet!");
-        return false;
+        return listIterator.hasNext();
     }
 
     /**
      * @see java.util.Iterator#next()
      */
+    @Override
     public Object next() {
-        log.error("TODO: Not implemented yet!");
+        Node n = listIterator.next();
+        try {
+            Configuration config = configBuilder.build(n.getInputStream());
+            // Also support identity for backwards compatibility
+            if(config.getName().equals(YarepUser.USER) || config.getName().equals("identity")) {
+                User user = super.constructUser(this.identityManager, n);
+                log.debug("User (re)loaded: " + n.getName() + ", " + user.getID());
+                return user;
+            }
+        } catch(Exception e) {
+            // We can't throw an exception here, if this user
+            // is invalid/broken we'll just have to return null.
+            log.error(e.getMessage(), e);
+        }
+
         return null;
     }
 
     /**
      * @see java.util.Iterator#remove()
      */
-    public void remove() {
-        log.error("TODO: Not implemented yet!");
+    @Override
+    public void remove() throws UnsupportedOperationException {
+        throw new UnsupportedOperationException("Not implemented.");
     }
 }
Index: src/impl/java/org/wyona/security/impl/ldap/LDAPUserManagerImpl.java
===================================================================
--- src/impl/java/org/wyona/security/impl/ldap/LDAPUserManagerImpl.java	(revision 52863)
+++ src/impl/java/org/wyona/security/impl/ldap/LDAPUserManagerImpl.java	(working copy)
@@ -44,6 +44,19 @@
     }
 
     /**
+     * @see org.wyona.security.core.api.UserManager#getUserCount()
+     */
+    public int getUserCount() {
+        log.error("TODO: LDAP Yarep Implementation not finished yet! Use Yarep-only implementation instead.");
+        try {
+            return new org.wyona.security.impl.yarep.YarepUserManager(identityManager, identitiesRepository, cacheEnabled, resolveGroupsAtCreation).getUserCount();
+        } catch(Exception e) {
+            log.error(e.getMessage(), e);
+            return 0;
+        }
+    }
+
+    /**
      * @see org.wyona.security.core.api.UserManager#removeUser(String)
      */
     public void removeUser(String userName) throws AccessManagementException {
Index: src/core/java/org/wyona/security/core/api/UserManager.java
===================================================================
--- src/core/java/org/wyona/security/core/api/UserManager.java	(revision 52863)
+++ src/core/java/org/wyona/security/core/api/UserManager.java	(working copy)
@@ -31,6 +31,11 @@
     java.util.Iterator<User> getUsers(String query) throws AccessManagementException;
 
     /**
+     * Get total number of users.
+     */
+    int getUserCount();
+
+    /**
      * Gets all users in no particular order, whereas provides a parameter to tell the implementation to refresh possibly cached entries
      * 
      * XXX: this does not scale UI-wise for many users: cf. {@link UserManager#getUsers()} for rationale.


More information about the Yanel-development mailing list