Implemented deregister public client task.

Change-Id: I6ad6c54ff1c44d5313b8cf23bdddf42230c213cd
diff --git a/full/Changes b/full/Changes
index 93157b5..16df079 100644
--- a/full/Changes
+++ b/full/Changes
@@ -1,7 +1,9 @@
 version 0.60.2
-09/04/2018
+10/04/2018
 	- implemented OAuth2 client registration (margaretha)
+	- implemented OAuth2 client authentication (margaretha)
 	- changed virtual corpus search to retrieval (margaretha)
+	- implemented deregister public client task (margaretha)
 	
 version 0.60.1
 28/03/2018
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/OAuth2ClientDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/OAuth2ClientDao.java
index 94ad319..21b3e2a 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/OAuth2ClientDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/OAuth2ClientDao.java
@@ -27,8 +27,8 @@
     private EntityManager entityManager;
 
     public void registerClient (String id, String secret, String name,
-            OAuth2ClientType type, String url, String redirectURI,
-            String registeredBy) throws KustvaktException {
+            OAuth2ClientType type, String url, int urlHashCode,
+            String redirectURI, String registeredBy) throws KustvaktException {
         ParameterChecker.checkStringValue(id, "client id");
         ParameterChecker.checkStringValue(name, "client name");
         ParameterChecker.checkObjectValue(type, "client type");
@@ -42,9 +42,9 @@
         client.setSecret(secret);
         client.setType(type);
         client.setUrl(url);
+        client.setUrlHashCode(urlHashCode);
         client.setRedirectURI(redirectURI);
         client.setRegisteredBy(registeredBy);
-
         entityManager.persist(client);
     }
 
@@ -69,5 +69,11 @@
         }
     }
 
+    public void deregisterClient (OAuth2Client client) {
+        if (!entityManager.contains(client)) {
+            client = entityManager.merge(client);
+        }
+        entityManager.remove(client);
+    }
 
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/entity/OAuth2Client.java b/full/src/main/java/de/ids_mannheim/korap/entity/OAuth2Client.java
index 9d91d93..c6d179f 100644
--- a/full/src/main/java/de/ids_mannheim/korap/entity/OAuth2Client.java
+++ b/full/src/main/java/de/ids_mannheim/korap/entity/OAuth2Client.java
@@ -28,6 +28,8 @@
     @Enumerated(EnumType.STRING)
     private OAuth2ClientType type;
     private String url;
+    @Column(name = "url_hashcode")
+    private int urlHashCode;
     @Column(name = "redirect_uri")
     private String redirectURI;
     private String registeredBy;
diff --git a/full/src/main/java/de/ids_mannheim/korap/handlers/OAuthDb.java b/full/src/main/java/de/ids_mannheim/korap/handlers/OAuthDb.java
index 828d61e..04736b0 100644
--- a/full/src/main/java/de/ids_mannheim/korap/handlers/OAuthDb.java
+++ b/full/src/main/java/de/ids_mannheim/korap/handlers/OAuthDb.java
@@ -249,7 +249,7 @@
             jlog.error("removing client '{}' failed", info.getClient_id());
             throw new DatabaseException(new KustvaktException(user.getId(),
                     StatusCodes.ILLEGAL_ARGUMENT, "arguments given not valid",
-                    info.toJSON()), StatusCodes.CLIENT_REMOVAL_FAILURE,
+                    info.toJSON()), StatusCodes.CLIENT_DEREGISTRATION_FAILED,
                     info.toJSON());
 
         }
diff --git a/full/src/main/java/de/ids_mannheim/korap/service/OAuth2ClientService.java b/full/src/main/java/de/ids_mannheim/korap/service/OAuth2ClientService.java
index ec146fd..75d09fb 100644
--- a/full/src/main/java/de/ids_mannheim/korap/service/OAuth2ClientService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/service/OAuth2ClientService.java
@@ -1,5 +1,7 @@
 package de.ids_mannheim.korap.service;
 
+import java.sql.SQLException;
+
 import org.apache.commons.validator.routines.UrlValidator;
 import org.apache.oltu.oauth2.common.message.types.GrantType;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -9,6 +11,7 @@
 import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
 import de.ids_mannheim.korap.constant.AuthenticationScheme;
 import de.ids_mannheim.korap.constant.OAuth2ClientType;
+import de.ids_mannheim.korap.dao.AdminDao;
 import de.ids_mannheim.korap.dao.OAuth2ClientDao;
 import de.ids_mannheim.korap.dto.OAuth2ClientDto;
 import de.ids_mannheim.korap.entity.OAuth2Client;
@@ -24,12 +27,15 @@
     @Autowired
     private OAuth2ClientDao clientDao;
     @Autowired
+    private AdminDao adminDao;
+    @Autowired
     private UrlValidator urlValidator;
     @Autowired
     private EncryptionIface encryption;
     @Autowired
     private HttpAuthorizationHandler authorizationHandler;
 
+
     public OAuth2ClientDto registerClient (OAuth2ClientJson clientJson,
             String registeredBy) throws KustvaktException {
         if (!urlValidator.isValid(clientJson.getUrl())) {
@@ -48,19 +54,49 @@
         }
 
         String id = encryption.createRandomNumber();
-
-        clientDao.registerClient(id, secret, clientJson.getName(),
-                clientJson.getType(), clientJson.getUrl(),
-                clientJson.getRedirectURI(), registeredBy);
+        try {
+            clientDao.registerClient(id, secret, clientJson.getName(),
+                    clientJson.getType(), clientJson.getUrl(),
+                    clientJson.getUrl().hashCode(), clientJson.getRedirectURI(),
+                    registeredBy);
+        }
+        catch (Exception e) {
+            Throwable cause = e;
+            Throwable lastCause = null;
+            while ((cause = cause.getCause()) != null
+                    && !cause.equals(lastCause)) {
+                if (cause instanceof SQLException) {
+                    throw new KustvaktException(
+                            StatusCodes.CLIENT_REGISTRATION_FAILED,
+                            cause.getMessage(), cause);
+                }
+                lastCause = cause;
+            }
+        }
 
         return new OAuth2ClientDto(id, secret);
     }
 
 
-    public OAuth2ClientDto deregisterClient (String clientId, String username) {
+    public void deregisterClient (String clientId, String username)
+            throws KustvaktException {
 
-
-        return null;
+        OAuth2Client client = clientDao.retrieveClientById(clientId);
+        if (adminDao.isAdmin(username)) {
+            clientDao.deregisterClient(client);
+        }
+        else if (client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
+            throw new KustvaktException(
+                    StatusCodes.CLIENT_DEREGISTRATION_FAILED,
+                    "Service is limited to public clients.");
+        }
+        else if (client.getRegisteredBy().equals(username)) {
+            clientDao.deregisterClient(client);
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
     }
 
 
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 d766537..a770e0f 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,11 +1,14 @@
 package de.ids_mannheim.korap.web.controller;
 
 import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.FormParam;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
 import javax.ws.rs.core.SecurityContext;
 
 import org.springframework.beans.factory.annotation.Autowired;
@@ -64,45 +67,44 @@
     }
 
 
-    //    /** Deregisters a client via owner authentication. 
-    //     * 
-    //     * EM: who can deregister clients? The user registered the clients or the client itself?
-    //     * 
-    //     * @param securityContext
-    //     * @param clientId
-    //     * @return HTTP Response OK if successful.
-    //     */
-    //    @POST
-    //    @Path("deregister")
-    //    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
-    //    @ResourceFilters({ AuthenticationFilter.class, BlockingFilter.class })
-    //    public OAuth2ClientDto deregisterClient (
-    //            @Context SecurityContext securityContext,
-    //            @FormParam("client_id") String clientId) {
-    //        TokenContext context =
-    //                (TokenContext) securityContext.getUserPrincipal();
-    //        try {
-    //            return clientService.deregisterClient(clientId,
-    //                    context.getUsername());
-    //        }
-    //        catch (KustvaktException e) {
-    //            throw responseHandler.throwit(e);
-    //        }
-    //    }
-    //
-    //    @POST
-    //    @Path("deregister")
-    //    @ResourceFilters({ OAuth2ClientAuthenticationFilter.class,
-    //            BlockingFilter.class })
-    //    public OAuth2ClientDto deregisterClient (
-    //            @Context SecurityContext securityContext) {
-    //        TokenContext context =
-    //                (TokenContext) securityContext.getUserPrincipal();
-    //        try {
-    //            return clientService.deregisterClient();
-    //        }
-    //        catch (KustvaktException e) {
-    //            throw responseHandler.throwit(e);
-    //        }
-    //    }
+    /** Deregisters a public client via owner authentication.
+     * 
+     * 
+     * @param securityContext
+     * @param clientId the client id
+     * @return HTTP Response OK if successful.
+     */
+    @DELETE
+    @Path("deregister")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    @ResourceFilters({ AuthenticationFilter.class, BlockingFilter.class })
+    public Response deregisterClient (
+            @Context SecurityContext securityContext,
+            @FormParam("client_id") String clientId) {
+        TokenContext context =
+                (TokenContext) securityContext.getUserPrincipal();
+        try {
+            clientService.deregisterClient(clientId,
+                    context.getUsername());
+            return Response.ok().build();
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+    
+
+//    @POST
+//    @Path("deregister")
+//    public OAuth2ClientDto deregisterClient (
+//            @Context SecurityContext securityContext) {
+//        TokenContext context =
+//                (TokenContext) securityContext.getUserPrincipal();
+//        try {
+//            return clientService.deregisterClient();
+//        }
+//        catch (KustvaktException e) {
+//            throw responseHandler.throwit(e);
+//        }
+//    }
 }
diff --git a/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql b/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
index c228c08..f616782 100644
--- a/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
+++ b/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
@@ -1,51 +1,51 @@
 -- EM: modified from Michael Hanl version
 
 -- oauth2 db tables
-create table if not exists oauth2_client (
+CREATE TABLE IF NOT EXISTS oauth2_client (
 	id VARCHAR(100) UNIQUE PRIMARY KEY,
 	name VARCHAR(200) NOT NULL,
 	secret VARCHAR(200),
 	type VARCHAR(200) NOT NULL,
-	url TEXT UNIQUE NOT NULL,
+	url TEXT NOT NULL,
+	url_hashcode UNIQUE INTEGER NOT NULL,
 	redirect_uri TEXT NOT NULL,
---is_confidential BOOLEAN DEFAULT FALSE,
 	registeredBy VARCHAR(100) NOT NULL
 );
 
-
--- status 1 = valid, 0 = revoked, -1 = disabled
-create table if not exists oauth2_access_token (
-id INTEGER PRIMARY KEY AUTO_INCREMENT,
-access_token VARCHAR(300),
-auth_code VARCHAR(250),
-client_id VARCHAR(100),
-user_id INTEGER,
--- make boolean --
-status INTEGER DEFAULT 1,
--- in case of code authorization, should match auth code scopes!
--- use scopes for levelaccess descriptor level[rw],level[r]
-scopes VARCHAR(350),
-expiration TIMESTAMP,
-FOREIGN KEY (user_id)
-REFERENCES korap_users(id)
-ON DELETE CASCADE,
-FOREIGN KEY (client_id)
-REFERENCES oauth2_client(client_id)
-ON DELETE CASCADE
-);
-
-
--- also scopes?
-create table if not exists oauth2_refresh_token (
-id INTEGER PRIMARY KEY AUTO_INCREMENT,
-client_id VARCHAR(100),
-user_id INTEGER,
-expiration TIMESTAMP,
-scopes VARCHAR(350),
-FOREIGN KEY (user_id)
-REFERENCES korap_users(id)
-ON DELETE CASCADE,
-FOREIGN KEY (client_id)
-REFERENCES oauth2_client(client_id)
-ON DELETE CASCADE
-);
\ No newline at end of file
+--
+---- status 1 = valid, 0 = revoked, -1 = disabled
+--create table if not exists oauth2_access_token (
+--id INTEGER PRIMARY KEY AUTO_INCREMENT,
+--access_token VARCHAR(300),
+--auth_code VARCHAR(250),
+--client_id VARCHAR(100),
+--user_id INTEGER,
+---- make boolean --
+--status INTEGER DEFAULT 1,
+---- in case of code authorization, should match auth code scopes!
+---- use scopes for levelaccess descriptor level[rw],level[r]
+--scopes VARCHAR(350),
+--expiration TIMESTAMP,
+--FOREIGN KEY (user_id)
+--REFERENCES korap_users(id)
+--ON DELETE CASCADE,
+--FOREIGN KEY (client_id)
+--REFERENCES oauth2_client(client_id)
+--ON DELETE CASCADE
+--);
+--
+--
+---- also scopes?
+--create table if not exists oauth2_refresh_token (
+--id INTEGER PRIMARY KEY AUTO_INCREMENT,
+--client_id VARCHAR(100),
+--user_id INTEGER,
+--expiration TIMESTAMP,
+--scopes VARCHAR(350),
+--FOREIGN KEY (user_id)
+--REFERENCES korap_users(id)
+--ON DELETE CASCADE,
+--FOREIGN KEY (client_id)
+--REFERENCES oauth2_client(client_id)
+--ON DELETE CASCADE
+--);
\ No newline at end of file
diff --git a/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql b/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
index fe0391f..4b42442 100644
--- a/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
+++ b/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
@@ -1,12 +1,16 @@
 -- EM: modified from Michael Hanl version
 
 -- oauth2 db tables
-create table IF NOT EXISTS oauth2_client (
-	id VARCHAR(100) NOT NULL,
+CREATE TABLE IF NOT EXISTS oauth2_client (
+	id VARCHAR(100) PRIMARY KEY NOT NULL,
 	name VARCHAR(200) NOT NULL,
-	secret VARCHAR(200) NOT NULL,
+	secret VARCHAR(200),
 	type VARCHAR(200) NOT NULL,
-	url TEXT UNIQUE NOT NULL,
+	url TEXT NOT NULL,
+	url_hashcode INTEGER NOT NULL,
 	redirect_uri TEXT NOT NULL,
 	registeredBy VARCHAR(100) NOT NULL
 );
+
+CREATE UNIQUE INDEX client_id_index on oauth2_client(id);
+CREATE UNIQUE INDEX client_url_index on oauth2_client(url_hashcode);
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
index 97cce38..dc052ec 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
@@ -2,6 +2,9 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import javax.ws.rs.core.MultivaluedMap;
 
 import org.apache.http.entity.ContentType;
 import org.junit.Test;
@@ -9,14 +12,18 @@
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.net.HttpHeaders;
+import com.sun.jersey.api.client.ClientHandlerException;
 import com.sun.jersey.api.client.ClientResponse;
 import com.sun.jersey.api.client.ClientResponse.Status;
+import com.sun.jersey.api.client.UniformInterfaceException;
+import com.sun.jersey.core.util.MultivaluedMapImpl;
 
 import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
 import de.ids_mannheim.korap.config.Attributes;
 import de.ids_mannheim.korap.config.SpringJerseyTest;
 import de.ids_mannheim.korap.constant.OAuth2ClientType;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.utils.JsonUtils;
 import de.ids_mannheim.korap.web.input.OAuth2ClientJson;
 
@@ -25,9 +32,9 @@
     @Autowired
     private HttpAuthorizationHandler handler;
     private String username = "OAuth2ClientControllerTest";
-    
-    @Test
-    public void testRegisterClient () throws KustvaktException {
+
+    private ClientResponse testRegisterConfidentialClient ()
+            throws KustvaktException {
 
         OAuth2ClientJson json = new OAuth2ClientJson();
         json.setName("OAuth2ClientTest");
@@ -35,6 +42,40 @@
         json.setUrl("http://example.client.com");
         json.setRedirectURI("http://example.client.com/redirect");
 
+        return resource().path("oauth2").path("client").path("register")
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue(username,
+                                "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .entity(json).post(ClientResponse.class);
+    }
+
+    @Test
+    public void testRegisterClientNonUniqueURL () throws KustvaktException {
+        ClientResponse response = testRegisterConfidentialClient();
+        String entity = response.getEntity(String.class);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        JsonNode node = JsonUtils.readTree(entity);
+        assertNotNull(node.at("/client_id").asText());
+        assertNotNull(node.at("/client_secret").asText());
+
+        response = testRegisterConfidentialClient();
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        node = JsonUtils.readTree(response.getEntity(String.class));
+        assertEquals(StatusCodes.CLIENT_REGISTRATION_FAILED,
+                node.at("/errors/0/0").asInt());
+    }
+
+    @Test
+    public void testRegisterPublicClient () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        OAuth2ClientJson json = new OAuth2ClientJson();
+        json.setName("OAuth2PublicClient");
+        json.setType(OAuth2ClientType.PUBLIC);
+        json.setUrl("http://public.client.com");
+        json.setRedirectURI("http://public.client.com/redirect");
+
         ClientResponse response = resource().path("oauth2").path("client")
                 .path("register")
                 .header(Attributes.AUTHORIZATION,
@@ -46,9 +87,31 @@
 
         String entity = response.getEntity(String.class);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        
         JsonNode node = JsonUtils.readTree(entity);
-        assertNotNull(node.at("/client_id").asText());
-        assertNotNull(node.at("/client_secret").asText());
+        String clientId = node.at("/client_id").asText();
+        assertNotNull(clientId);
+        assertTrue(node.at("/client_secret").isMissingNode());
+
+        testDeregisterPublicClient(clientId);
+    }
+
+    private void testDeregisterPublicClient (String clientId)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("client_id", clientId);
+
+        ClientResponse response = resource().path("oauth2").path("client")
+                .path("deregister")
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue(username,
+                                "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).delete(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
     }
 }