Added the list-user-client controller.

Change-Id: I65b6d392da59d1f3412b28ae81ae0bec321d2077
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessScopeDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessScopeDao.java
index de0cfac..a714420 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessScopeDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessScopeDao.java
@@ -15,7 +15,6 @@
 
 import de.ids_mannheim.korap.constant.OAuth2Scope;
 import de.ids_mannheim.korap.oauth2.entity.AccessScope;
-import de.ids_mannheim.korap.oauth2.entity.AccessScope;
 
 @Repository
 @Transactional
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
index 31fa2b3..444659e 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
@@ -1,22 +1,32 @@
 package de.ids_mannheim.korap.oauth2.dao;
 
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.List;
+
 import javax.persistence.EntityManager;
 import javax.persistence.NoResultException;
 import javax.persistence.PersistenceContext;
 import javax.persistence.Query;
+import javax.persistence.TypedQuery;
 import javax.persistence.criteria.CriteriaBuilder;
 import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.ListJoin;
+import javax.persistence.criteria.Predicate;
 import javax.persistence.criteria.Root;
 
 import org.springframework.stereotype.Repository;
 import org.springframework.transaction.annotation.Transactional;
 
+import de.ids_mannheim.korap.config.Attributes;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
 import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
 import de.ids_mannheim.korap.oauth2.entity.OAuth2ClientUrl;
 import de.ids_mannheim.korap.oauth2.entity.OAuth2Client_;
+import de.ids_mannheim.korap.oauth2.entity.RefreshToken;
+import de.ids_mannheim.korap.oauth2.entity.RefreshToken_;
 import de.ids_mannheim.korap.utils.ParameterChecker;
 
 @Transactional
@@ -97,4 +107,29 @@
         client = entityManager.merge(client);
     }
 
+    public List<OAuth2Client> retrieveUserClients (String username)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(username, "username");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<OAuth2Client> query =
+                builder.createQuery(OAuth2Client.class);
+
+        Root<OAuth2Client> client = query.from(OAuth2Client.class);
+        ListJoin<OAuth2Client, RefreshToken> refreshToken =
+                client.join(OAuth2Client_.refreshTokens);
+        Predicate condition = builder.and(
+                builder.equal(refreshToken.get(RefreshToken_.userId), username),
+                builder.equal(refreshToken.get(RefreshToken_.isRevoked), false),
+                builder.greaterThan(
+                        refreshToken
+                                .<ZonedDateTime> get(RefreshToken_.expiryDate),
+                        ZonedDateTime
+                                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE))));
+        query.select(client);
+        query.where(condition);
+        TypedQuery<OAuth2Client> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/RefreshTokenDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/RefreshTokenDao.java
index e98c627..8ee798b 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/RefreshTokenDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/RefreshTokenDao.java
@@ -8,8 +8,10 @@
 import javax.persistence.EntityManager;
 import javax.persistence.PersistenceContext;
 import javax.persistence.Query;
+import javax.persistence.TypedQuery;
 import javax.persistence.criteria.CriteriaBuilder;
 import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Join;
 import javax.persistence.criteria.Root;
 
 import org.springframework.beans.factory.annotation.Autowired;
@@ -20,6 +22,8 @@
 import de.ids_mannheim.korap.config.FullConfiguration;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.oauth2.entity.AccessScope;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client_;
 import de.ids_mannheim.korap.oauth2.entity.RefreshToken;
 import de.ids_mannheim.korap.oauth2.entity.RefreshToken_;
 import de.ids_mannheim.korap.utils.ParameterChecker;
@@ -32,6 +36,8 @@
     private EntityManager entityManager;
     @Autowired
     private FullConfiguration config;
+    @Autowired
+    private OAuth2ClientDao clientDao;
 
     public RefreshToken storeRefreshToken (String refreshToken, String userId,
             ZonedDateTime userAuthenticationTime, String clientId,
@@ -43,12 +49,13 @@
 
         ZonedDateTime now =
                 ZonedDateTime.now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
+        OAuth2Client client = clientDao.retrieveClientById(clientId);
 
         RefreshToken token = new RefreshToken();
         token.setToken(refreshToken);
         token.setUserId(userId);
         token.setUserAuthenticationTime(userAuthenticationTime);
-        token.setClientId(clientId);
+        token.setClient(client);
         token.setCreatedDate(now);
         token.setExpiryDate(now.plusSeconds(config.getRefreshTokenExpiry()));
         token.setScopes(scopes);
@@ -63,13 +70,14 @@
         CriteriaQuery<RefreshToken> query =
                 builder.createQuery(RefreshToken.class);
         Root<RefreshToken> root = query.from(RefreshToken.class);
+        root.fetch(RefreshToken_.client);
+        
         query.select(root);
         query.where(builder.equal(root.get(RefreshToken_.token), token));
         Query q = entityManager.createQuery(query);
         return (RefreshToken) q.getSingleResult();
     }
 
-    @SuppressWarnings("unchecked")
     public List<RefreshToken> retrieveRefreshTokenByClientId (String clientId)
             throws KustvaktException {
         ParameterChecker.checkStringValue(clientId, "client_id");
@@ -78,9 +86,11 @@
         CriteriaQuery<RefreshToken> query =
                 builder.createQuery(RefreshToken.class);
         Root<RefreshToken> root = query.from(RefreshToken.class);
+        Join<RefreshToken, OAuth2Client> client =
+                root.join(RefreshToken_.client);
         query.select(root);
-        query.where(builder.equal(root.get(RefreshToken_.clientId), clientId));
-        Query q = entityManager.createQuery(query);
+        query.where(builder.equal(client.get(OAuth2Client_.id), clientId));
+        TypedQuery<RefreshToken> q = entityManager.createQuery(query);
         return q.getResultList();
     }
 
@@ -91,4 +101,28 @@
         token = entityManager.merge(token);
         return token;
     }
+
+    // public List<RefreshToken> retrieveRefreshTokenByUser (String
+    // username)
+    // throws KustvaktException {
+    // ParameterChecker.checkStringValue(username, "username");
+    //
+    // CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+    // CriteriaQuery<RefreshToken> query =
+    // builder.createQuery(RefreshToken.class);
+    //
+    // Root<RefreshToken> root = query.from(RefreshToken.class);
+    // Predicate condition = builder.and(
+    // builder.equal(root.get(RefreshToken_.userId), username),
+    // builder.equal(root.get(RefreshToken_.isRevoked), false),
+    // builder.greaterThan(
+    // root.<ZonedDateTime> get(RefreshToken_.expiryDate),
+    // ZonedDateTime
+    // .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE)))
+    // );
+    // query.select(root);
+    // query.where(condition);
+    // TypedQuery<RefreshToken> q = entityManager.createQuery(query);
+    // return q.getResultList();
+    // }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2UserClientDto.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2UserClientDto.java
new file mode 100644
index 0000000..02ab148
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2UserClientDto.java
@@ -0,0 +1,20 @@
+package de.ids_mannheim.korap.oauth2.dto;
+
+public class OAuth2UserClientDto {
+    
+    private String clientId;
+    private String clientName;
+    
+    public String getClientName () {
+        return clientName;
+    }
+    public void setClientName (String clientName) {
+        this.clientName = clientName;
+    }
+    public String getClientId () {
+        return clientId;
+    }
+    public void setClientId (String clientId) {
+        this.clientId = clientId;
+    }
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/OAuth2Client.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/OAuth2Client.java
index 09c6e35..8023e32 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/OAuth2Client.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/OAuth2Client.java
@@ -1,5 +1,7 @@
 package de.ids_mannheim.korap.oauth2.entity;
 
+import java.util.List;
+
 import javax.persistence.CascadeType;
 import javax.persistence.Column;
 import javax.persistence.Entity;
@@ -8,6 +10,7 @@
 import javax.persistence.FetchType;
 import javax.persistence.Id;
 import javax.persistence.JoinColumn;
+import javax.persistence.OneToMany;
 import javax.persistence.OneToOne;
 import javax.persistence.Table;
 
@@ -19,7 +22,7 @@
  */
 @Entity
 @Table(name = "oauth2_client")
-public class OAuth2Client {
+public class OAuth2Client implements Comparable<OAuth2Client>{
 
     @Id
     private String id;
@@ -40,6 +43,9 @@
     @JoinColumn(name = "url_id")
     private OAuth2ClientUrl clientUrl;
 
+    @OneToMany(fetch = FetchType.LAZY, mappedBy = "client")
+    private List<RefreshToken> refreshTokens;
+    
     @Override
     public String toString () {
         return "id=" + id + ", name=" + name + ", secret=" + secret + ", type="
@@ -119,4 +125,9 @@
     public void setClientUrl (OAuth2ClientUrl clientUrl) {
         this.clientUrl = clientUrl;
     }
+
+    @Override
+    public int compareTo (OAuth2Client o) {
+        return this.getName().compareTo(o.getName());
+    }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/RefreshToken.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/RefreshToken.java
index a43a493..a60aef5 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/RefreshToken.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/RefreshToken.java
@@ -12,6 +12,7 @@
 import javax.persistence.JoinColumn;
 import javax.persistence.JoinTable;
 import javax.persistence.ManyToMany;
+import javax.persistence.ManyToOne;
 import javax.persistence.OneToMany;
 import javax.persistence.Table;
 import javax.persistence.UniqueConstraint;
@@ -30,8 +31,8 @@
     private ZonedDateTime expiryDate;
     @Column(name = "user_id")
     private String userId;
-    @Column(name = "client_id")
-    private String clientId;
+//    @Column(name = "client_id")
+//    private String clientId;
     @Column(name = "user_auth_time", updatable = false)
     private ZonedDateTime userAuthenticationTime;
     @Column(name = "is_revoked")
@@ -39,6 +40,10 @@
 
     @OneToMany(fetch = FetchType.EAGER, mappedBy = "refreshToken")
     private Set<AccessToken> accessTokens;
+    
+    @ManyToOne(fetch=FetchType.LAZY)
+    @JoinColumn(name="client")
+    private OAuth2Client client;
 
     @ManyToMany(fetch = FetchType.EAGER)
     @JoinTable(name = "oauth2_refresh_token_scope",
@@ -82,14 +87,6 @@
         this.userId = userId;
     }
 
-    public String getClientId () {
-        return clientId;
-    }
-
-    public void setClientId (String clientId) {
-        this.clientId = clientId;
-    }
-
     public ZonedDateTime getUserAuthenticationTime () {
         return userAuthenticationTime;
     }
@@ -123,4 +120,12 @@
         this.scopes = scopes;
     }
 
+    public OAuth2Client getClient () {
+        return client;
+    }
+
+    public void setClient (OAuth2Client client) {
+        this.client = client;
+    }
+
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
index 6bf3b39..7b4ac81 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
@@ -120,7 +120,7 @@
                     "Refresh token is not found", OAuth2Error.INVALID_GRANT);
         }
 
-        if (!clientId.equals(refreshToken.getClientId())) {
+        if (!clientId.equals(refreshToken.getClient().getId())) {
             throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
                     "Client " + clientId + "is not authorized",
                     OAuth2Error.INVALID_CLIENT);
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
index 47dffe3..23b8267 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
@@ -5,6 +5,8 @@
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import org.apache.commons.validator.routines.UrlValidator;
@@ -25,6 +27,7 @@
 import de.ids_mannheim.korap.oauth2.dao.RefreshTokenDao;
 import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientDto;
 import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientInfoDto;
+import de.ids_mannheim.korap.oauth2.dto.OAuth2UserClientDto;
 import de.ids_mannheim.korap.oauth2.entity.AccessToken;
 import de.ids_mannheim.korap.oauth2.entity.Authorization;
 import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
@@ -337,4 +340,36 @@
                     "Unauthorized operation for user: " + username, username);
         }
     }
+
+    public OAuth2Client retrieveClient (String clientId)
+            throws KustvaktException {
+        return clientDao.retrieveClientById(clientId);
+    }
+
+    public List<OAuth2Client> retrieveUserClients (String username)
+            throws KustvaktException {
+        return clientDao.retrieveUserClients(username);
+    }
+
+    public List<OAuth2UserClientDto> listUserClients (String username,
+            String clientId, String clientSecret) throws KustvaktException {
+        OAuth2Client client = authenticateClient(clientId, clientSecret);
+        if (!client.isSuper()) {
+            throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
+                    "Only super client is allowed to list user clients.",
+                    OAuth2Error.UNAUTHORIZED_CLIENT);
+        }
+        List<OAuth2Client> userClients = retrieveUserClients(username);
+        Collections.sort(userClients);
+        
+        List<OAuth2UserClientDto> dtoList = new ArrayList<>(userClients.size());
+        for (OAuth2Client uc : userClients) {
+            if (uc.isSuper()) continue;
+            OAuth2UserClientDto dto = new OAuth2UserClientDto();
+            dto.setClientId(uc.getId());
+            dto.setClientName(uc.getName());
+            dtoList.add(dto);
+        }
+        return dtoList;
+    }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
index ca7e168..02b190f 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
@@ -1,5 +1,7 @@
 package de.ids_mannheim.korap.web.controller;
 
+import java.util.List;
+
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.FormParam;
@@ -22,6 +24,7 @@
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientDto;
 import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientInfoDto;
+import de.ids_mannheim.korap.oauth2.dto.OAuth2UserClientDto;
 import de.ids_mannheim.korap.oauth2.service.OAuth2ClientService;
 import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
 import de.ids_mannheim.korap.security.context.TokenContext;
@@ -55,7 +58,8 @@
  */
 @Controller
 @Path("{version}/oauth2/client")
-@ResourceFilters({APIVersionFilter.class, AuthenticationFilter.class, BlockingFilter.class })
+@ResourceFilters({ APIVersionFilter.class, AuthenticationFilter.class,
+        BlockingFilter.class })
 public class OAuthClientController {
 
     @Autowired
@@ -180,7 +184,8 @@
      * authorization codes are invalidated.
      * 
      * @param securityContext
-     * @param clientId OAuth2 client id
+     * @param clientId
+     *            OAuth2 client id
      * @param super
      *            true indicating super client, false otherwise
      * @return Response status OK, if successful
@@ -222,4 +227,43 @@
             throw responseHandler.throwit(e);
         }
     }
+
+    /**
+     * Lists user clients having refresh tokens. This service is not
+     * part of the OAuth2 specification. It is intended to facilitate
+     * users revoking any suspicious and misused access or refresh
+     * tokens.
+     * 
+     * Only super clients are allowed to use this service. It requires
+     * user and client authentications.
+     * 
+     * @param context
+     * @return a list of clients having refresh tokens of the
+     *         given user
+     */
+    @POST
+    @Path("list")
+    @ResourceFilters({ AuthenticationFilter.class, BlockingFilter.class })
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    public List<OAuth2UserClientDto> listUserApp (
+            @Context SecurityContext context,
+            @FormParam("client_id") String clientId,
+            @FormParam("client_secret") String clientSecret) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+        String username = tokenContext.getUsername();
+
+        try {
+            scopeService.verifyScope(tokenContext,
+                    OAuth2Scope.LIST_USER_CLIENT);
+
+            return clientService.listUserClients(username, clientId,
+                    clientSecret);
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+
 }