keycloak-memoizeit

Merge pull request #516 from stianst/master KEYCLOAK-512

7/14/2014 8:01:56 AM

Details

diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/MongoStore.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/MongoStore.java
index ccf41b5..87d3669 100755
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/MongoStore.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/MongoStore.java
@@ -31,6 +31,10 @@ public interface MongoStore {
 
     <T extends MongoIdentifiableEntity> List<T> loadEntities(Class<T> type, DBObject query, MongoStoreInvocationContext context);
 
+    <T extends MongoIdentifiableEntity> List<T> loadEntities(Class<T> type, DBObject query, DBObject sort, MongoStoreInvocationContext context, int firstResult, int maxResults);
+
+    <T extends MongoIdentifiableEntity> int countEntities(Class<T> type, DBObject query, MongoStoreInvocationContext context);
+
     boolean removeEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context);
 
     boolean removeEntity(Class<? extends MongoIdentifiableEntity> type, String id, MongoStoreInvocationContext context);
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/MongoStoreImpl.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/MongoStoreImpl.java
index 9f96056..889194d 100755
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/MongoStoreImpl.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/MongoStoreImpl.java
@@ -280,6 +280,29 @@ public class MongoStoreImpl implements MongoStore {
         return convertCursor(type, cursor, context);
     }
 
+    @Override
+    public <T extends MongoIdentifiableEntity> List<T> loadEntities(Class<T> type, DBObject query, DBObject sort, MongoStoreInvocationContext context, int firstResult, int maxResults) {
+        // First we should execute all pending tasks before searching DB
+        context.beforeDBSearch(type);
+
+        DBCollection dbCollection = getDBCollectionForType(type);
+        DBCursor cursor = dbCollection.find(query);
+        cursor.skip(firstResult);
+        cursor.limit(maxResults);
+        if (sort != null) {
+            cursor.sort(sort);
+        }
+
+        return convertCursor(type, cursor, context);
+    }
+
+    public <T extends MongoIdentifiableEntity> int countEntities(Class<T> type, DBObject query, MongoStoreInvocationContext context) {
+        context.beforeDBSearch(type);
+
+        DBCollection dbCollection = getDBCollectionForType(type);
+        DBCursor cursor = dbCollection.find(query);
+        return cursor.size();
+    }
 
     @Override
     public boolean removeEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context) {
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js
index 2671891..60560f3 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js
@@ -44,20 +44,48 @@ module.controller('ApplicationCredentialsCtrl', function($scope, $location, real
 });
 
 module.controller('ApplicationSessionsCtrl', function($scope, realm, sessionCount, application,
-                                                      ApplicationUserSessions,
-                                                    $location, Dialog, Notifications) {
+                                                      ApplicationUserSessions) {
     $scope.realm = realm;
     $scope.count = sessionCount.count;
     $scope.sessions = [];
     $scope.application = application;
 
+    $scope.page = 0;
+
+    $scope.query = {
+        realm : realm.realm,
+        application: $scope.application.name,
+        max : 5,
+        first : 0
+    }
+
+    $scope.firstPage = function() {
+        $scope.query.first = 0;
+        if ($scope.query.first < 0) {
+            $scope.query.first = 0;
+        }
+        $scope.loadUsers();
+    }
+
+    $scope.previousPage = function() {
+        $scope.query.first -= parseInt($scope.query.max);
+        if ($scope.query.first < 0) {
+            $scope.query.first = 0;
+        }
+        $scope.loadUsers();
+    }
+
+    $scope.nextPage = function() {
+        $scope.query.first += parseInt($scope.query.max);
+        $scope.loadUsers();
+    }
+
     $scope.toDate = function(val) {
         return new Date(val);
     };
 
     $scope.loadUsers = function() {
-        ApplicationUserSessions.query({ realm : realm.realm, application: $scope.application.name }, function(updated) {
-            $scope.count = updated.length;
+        ApplicationUserSessions.query($scope.query, function(updated) {
             $scope.sessions = updated;
         })
     };
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-sessions.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-sessions.html
index 93c388e..248838a 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-sessions.html
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-sessions.html
@@ -28,19 +28,30 @@
         </form>
         <table class="table table-striped table-bordered" data-ng-show="count > 0">
             <thead>
-            <tr>
+            <tr data-ng-hide="sessions">
                 <th class="kc-table-actions" colspan="3">
                     <div class="pull-right">
-                        <a class="btn btn-primary" ng-click="loadUsers()">Show Users</a>
+                        <a class="btn btn-primary" ng-click="loadUsers()">Show Sessions</a>
                     </div>
                 </th>
             </tr>
-            <tr>
+            <tr data-ng-show="sessions">
                 <th>User</th>
                 <th>From IP</th>
                 <th>Session Start</th>
             </tr>
             </thead>
+            <tfoot data-ng-show="sessions">
+            <tr>
+                <td colspan="7">
+                    <div class="table-nav">
+                        <button data-ng-click="firstPage()" class="first" ng-disabled="query.first == 0">First page</button>
+                        <button data-ng-click="previousPage()" class="prev" ng-disabled="query.first == 0">Previous page</button>
+                        <button data-ng-click="nextPage()" class="next" ng-disabled="sessions.length < query.max">Next page</button>
+                    </div>
+                </td>
+            </tr>
+            </tfoot>
             <tbody>
             <tr data-ng-repeat="session in sessions">
                 <td><a href="#/realms/{{realm.realm}}/users/{{session.user}}">{{session.user}}</a></td>
diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
index f678328..fa643b3 100755
--- a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
@@ -16,6 +16,7 @@ public interface UserSessionProvider extends Provider {
     UserSessionModel getUserSession(RealmModel realm, String id);
     List<UserSessionModel> getUserSessions(RealmModel realm, UserModel user);
     List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client);
+    List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults);
     int getActiveUserSessions(RealmModel realm, ClientModel client);
     void removeUserSession(RealmModel realm, UserSessionModel session);
     void removeUserSessions(RealmModel realm, UserModel user);
diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java
index df406f7..2f27f34 100755
--- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java
+++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java
@@ -18,8 +18,8 @@ import java.util.Collection;
  */
 @Entity
 @NamedQueries({
-        @NamedQuery(name = "getUserSessionByUser", query = "select s from UserSessionEntity s where s.realmId = :realmId and s.userId = :userId"),
-        @NamedQuery(name = "getUserSessionByClient", query = "select s from UserSessionEntity s join s.clients c where s.realmId = :realmId and c.clientId = :clientId"),
+        @NamedQuery(name = "getUserSessionByUser", query = "select s from UserSessionEntity s where s.realmId = :realmId and s.userId = :userId order by s.started, s.id"),
+        @NamedQuery(name = "getUserSessionByClient", query = "select s from UserSessionEntity s join s.clients c where s.realmId = :realmId and c.clientId = :clientId order by s.started, s.id"),
         @NamedQuery(name = "getActiveUserSessionByClient", query = "select count(s) from UserSessionEntity s join s.clients c where s.realmId = :realmId and c.clientId = :clientId"),
         @NamedQuery(name = "removeUserSessionByRealm", query = "delete from UserSessionEntity s where s.realmId = :realmId"),
         @NamedQuery(name = "removeUserSessionByUser", query = "delete from UserSessionEntity s where s.realmId = :realmId and s.userId = :userId"),
diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java
index 770c326..182019e 100644
--- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java
+++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java
@@ -97,10 +97,16 @@ public class JpaUserSessionProvider implements UserSessionProvider {
 
     @Override
     public List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client) {
+        return getUserSessions(realm, client, 0, Integer.MAX_VALUE);
+    }
+
+    public List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
         List<UserSessionModel> list = new LinkedList<UserSessionModel>();
         TypedQuery<UserSessionEntity> query = em.createNamedQuery("getUserSessionByClient", UserSessionEntity.class)
                 .setParameter("realmId", realm.getId())
-                .setParameter("clientId", client.getClientId());
+                .setParameter("clientId", client.getClientId())
+                .setFirstResult(firstResult)
+                .setMaxResults(maxResults);
         for (UserSessionEntity entity : query.getResultList()) {
             list.add(new UserSessionAdapter(session, em, realm, entity));
         }
diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java
index ba24c1d..d5369e4 100644
--- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java
+++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java
@@ -15,6 +15,8 @@ import org.keycloak.models.sessions.mem.entities.UsernameLoginFailureKey;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.util.Time;
 
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
@@ -80,10 +82,22 @@ public class MemUserSessionProvider implements UserSessionProvider {
                 clientSessions.add(new UserSessionAdapter(session, realm, s));
             }
         }
+        Collections.sort(clientSessions, new UserSessionSort());
         return clientSessions;
     }
 
     @Override
+    public List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
+        List<UserSessionModel> userSessions = getUserSessions(realm, client);
+        if (firstResult > userSessions.size()) {
+            return Collections.emptyList();
+        }
+
+        int toIndex = (firstResult + maxResults) < userSessions.size() ? firstResult + maxResults : userSessions.size();
+        return userSessions.subList(firstResult, toIndex);
+    }
+
+    @Override
     public int getActiveUserSessions(RealmModel realm, ClientModel client) {
         int count = 0;
         for (UserSessionEntity s : sessions.values()) {
@@ -195,4 +209,17 @@ public class MemUserSessionProvider implements UserSessionProvider {
     public void close() {
     }
 
+    private class UserSessionSort implements Comparator<UserSessionModel> {
+
+        @Override
+        public int compare(UserSessionModel o1, UserSessionModel o2) {
+            int r = o1.getStarted() - o2.getStarted();
+            if (r == 0) {
+                return o1.getId().compareTo(o2.getId());
+            } else {
+                return r;
+            }
+        }
+    }
+
 }
diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java
index 68de8f4..1cbcb41 100644
--- a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java
+++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java
@@ -84,9 +84,26 @@ public class MongoUserSessionProvider implements UserSessionProvider {
         return result;
     }
 
+    public List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
+        DBObject query = new QueryBuilder()
+                .and("associatedClientIds").is(client.getId())
+                .get();
+        DBObject sort = new BasicDBObject("started", 1).append("id", 1);
+
+        List<MongoUserSessionEntity> sessions = mongoStore.loadEntities(MongoUserSessionEntity.class, query, sort, invocationContext, firstResult, maxResults);
+        List<UserSessionModel> result = new LinkedList<UserSessionModel>();
+        for (MongoUserSessionEntity session : sessions) {
+            result.add(new UserSessionAdapter(session, realm, invocationContext));
+        }
+        return result;
+    }
+
     @Override
     public int getActiveUserSessions(RealmModel realm, ClientModel client) {
-        return getUserSessions(realm, client).size();
+        DBObject query = new QueryBuilder()
+                .and("associatedClientIds").is(client.getId())
+                .get();
+        return mongoStore.countEntities(MongoUserSessionEntity.class, query, invocationContext);
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java
index 6814ae8..919fcce 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java
@@ -340,10 +340,12 @@ public class ApplicationResource {
     @GET
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
-    public List<UserSessionRepresentation> getUserSessions() {
+    public List<UserSessionRepresentation> getUserSessions(@QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults) {
         auth.requireView();
+        firstResult = firstResult != null ? firstResult : 0;
+        maxResults = maxResults != null ? maxResults : Integer.MAX_VALUE;
         List<UserSessionRepresentation> sessions = new ArrayList<UserSessionRepresentation>();
-        for (UserSessionModel userSession : session.sessions().getUserSessions(application.getRealm(), application)) {
+        for (UserSessionModel userSession : session.sessions().getUserSessions(application.getRealm(), application, firstResult, maxResults)) {
             UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(userSession);
             sessions.add(rep);
         }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
index c73a021..cb58949 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
@@ -4,6 +4,7 @@ import org.junit.After;
 import org.junit.Before;
 import org.junit.ClassRule;
 import org.junit.Test;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
@@ -113,6 +114,40 @@ public class UserSessionProviderTest {
     }
 
     @Test
+    public void testGetByClientPaginated() {
+        for (int i = 0; i < 25; i++) {
+            UserSessionModel userSession = session.sessions().createUserSession(realm, realm.getUser("user1"), "127.0.0." + i);
+            userSession.setStarted(Time.currentTime() + i);
+            userSession.associateClient(realm.findClient("test-app"));
+        }
+
+        resetSession();
+
+        assertPaginatedSession(realm, realm.findClient("test-app"), 0, 1, 1);
+        assertPaginatedSession(realm, realm.findClient("test-app"), 0, 10, 10);
+        assertPaginatedSession(realm, realm.findClient("test-app"), 10, 10, 10);
+        assertPaginatedSession(realm, realm.findClient("test-app"), 20, 10, 5);
+        assertPaginatedSession(realm, realm.findClient("test-app"), 30, 10, 0);
+    }
+
+    private void assertPaginatedSession(RealmModel realm, ClientModel client, int start, int max, int expectedSize) {
+        List<UserSessionModel> sessions = session.sessions().getUserSessions(realm, client, start, max);
+        String[] actualIps = new String[sessions.size()];
+        for (int i = 0; i < actualIps.length; i++) {
+            actualIps[i] = sessions.get(i).getIpAddress();
+        }
+
+        String[] expectedIps = new String[expectedSize];
+        for (int i = 0; i < expectedSize; i++) {
+            expectedIps[i] = "127.0.0." + (i + start);
+        }
+
+        assertArrayEquals(expectedIps, actualIps);
+    }
+
+
+
+    @Test
     public void testGetCountByClient() {
         createSessions();
 
@@ -175,5 +210,4 @@ public class UserSessionProviderTest {
 
         assertArrayEquals(clients, actualClients);
     }
-
 }